feature: add DuckDuckGo weather and stock widget extractors
All checks were successful
CI / vet (pull_request) Successful in 29s
CI / build (pull_request) Successful in 46s
CI / test (pull_request) Successful in 48s

Add weather.go with GetWeather() for extracting structured weather data
(location, temp, conditions, forecast) and stock.go with GetStockQuote()
and GetStockChart() for stock data extraction and chart screenshots.

Both include mock-based tests. CSS selectors may need tuning against
the live site since DuckDuckGo's React-rendered widgets use dynamic
class names.

Closes #25, #26
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 16:40:53 +00:00
parent dcc977c0cc
commit 461b704792
4 changed files with 520 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
package duckduckgo
import (
"context"
"testing"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/extractortest"
)
func makeStockDoc() *extractortest.MockDocument {
return &extractortest.MockDocument{
URLValue: "https://duckduckgo.com/?q=aapl+stock",
MockNode: extractortest.MockNode{
Children: map[string]extractor.Nodes{
"div.module--stocks .module__title__link": {
&extractortest.MockNode{TextValue: "AAPL"},
},
"div.module--stocks .module__subtitle": {
&extractortest.MockNode{TextValue: "Apple Inc."},
},
"div.module--stocks .module__price": {
&extractortest.MockNode{TextValue: "$189.84"},
},
"div.module--stocks .module__price-change": {
&extractortest.MockNode{TextValue: "+2.45"},
},
"div.module--stocks .module__price-change-pct": {
&extractortest.MockNode{TextValue: "(1.31%)"},
},
},
},
}
}
func TestExtractStock(t *testing.T) {
doc := makeStockDoc()
data, err := extractStock(doc)
if err != nil {
t.Fatalf("extractStock() error: %v", err)
}
if data.Symbol != "AAPL" {
t.Errorf("Symbol = %q, want %q", data.Symbol, "AAPL")
}
if data.Name != "Apple Inc." {
t.Errorf("Name = %q, want %q", data.Name, "Apple Inc.")
}
if data.Price != 189.84 {
t.Errorf("Price = %v, want 189.84", data.Price)
}
if data.Change != 2.45 {
t.Errorf("Change = %v, want 2.45", data.Change)
}
if data.ChangePct != 1.31 {
t.Errorf("ChangePct = %v, want 1.31", data.ChangePct)
}
}
func TestGetStockQuote_MockBrowser(t *testing.T) {
doc := makeStockDoc()
browser := &extractortest.MockBrowser{
Documents: map[string]*extractortest.MockDocument{
"https://duckduckgo.com/?kp=-2&q=aapl+stock": doc,
},
}
data, err := DefaultConfig.GetStockQuote(context.Background(), browser, "aapl")
if err != nil {
t.Fatalf("GetStockQuote() error: %v", err)
}
if data.Symbol != "AAPL" {
t.Errorf("Symbol = %q, want %q", data.Symbol, "AAPL")
}
if data.Price != 189.84 {
t.Errorf("Price = %v, want 189.84", data.Price)
}
}
func TestExtractStock_Empty(t *testing.T) {
doc := &extractortest.MockDocument{
MockNode: extractortest.MockNode{
Children: map[string]extractor.Nodes{},
},
}
data, err := extractStock(doc)
if err != nil {
t.Fatalf("extractStock() error: %v", err)
}
if data.Symbol != "" || data.Price != 0 {
t.Error("expected zero values for empty doc")
}
}