feature: add DuckDuckGo weather and stock widget extractors
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:
98
sites/duckduckgo/stock_test.go
Normal file
98
sites/duckduckgo/stock_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user