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:
141
sites/duckduckgo/weather_test.go
Normal file
141
sites/duckduckgo/weather_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package duckduckgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/go-extractor"
|
||||
"gitea.stevedudenhoeffer.com/steve/go-extractor/extractortest"
|
||||
)
|
||||
|
||||
func makeWeatherDoc() *extractortest.MockDocument {
|
||||
return &extractortest.MockDocument{
|
||||
URLValue: "https://duckduckgo.com/?q=weather+new+york",
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
"div.module--weather span.module__title__link": {
|
||||
&extractortest.MockNode{TextValue: "New York, NY"},
|
||||
},
|
||||
"div.module--weather .module__current-temp": {
|
||||
&extractortest.MockNode{TextValue: "72°F"},
|
||||
},
|
||||
"div.module--weather .module__weather-summary": {
|
||||
&extractortest.MockNode{TextValue: "Partly Cloudy"},
|
||||
},
|
||||
"div.module--weather .module__high-temp": {
|
||||
&extractortest.MockNode{TextValue: "78°"},
|
||||
},
|
||||
"div.module--weather .module__low-temp": {
|
||||
&extractortest.MockNode{TextValue: "65°"},
|
||||
},
|
||||
"div.module--weather .module__humidity": {
|
||||
&extractortest.MockNode{TextValue: "55%"},
|
||||
},
|
||||
"div.module--weather .module__wind": {
|
||||
&extractortest.MockNode{TextValue: "SW 10 mph"},
|
||||
},
|
||||
"div.module--weather .module__forecast-day": {
|
||||
&extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
".forecast-day__name": {&extractortest.MockNode{TextValue: "Mon"}},
|
||||
".forecast-day__high": {&extractortest.MockNode{TextValue: "80°"}},
|
||||
".forecast-day__low": {&extractortest.MockNode{TextValue: "66°"}},
|
||||
".forecast-day__condition": {&extractortest.MockNode{TextValue: "Sunny"}},
|
||||
},
|
||||
},
|
||||
&extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{
|
||||
".forecast-day__name": {&extractortest.MockNode{TextValue: "Tue"}},
|
||||
".forecast-day__high": {&extractortest.MockNode{TextValue: "75°"}},
|
||||
".forecast-day__low": {&extractortest.MockNode{TextValue: "62°"}},
|
||||
".forecast-day__condition": {&extractortest.MockNode{TextValue: "Rain"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWeather(t *testing.T) {
|
||||
doc := makeWeatherDoc()
|
||||
|
||||
data, err := extractWeather(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractWeather() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Location != "New York, NY" {
|
||||
t.Errorf("Location = %q, want %q", data.Location, "New York, NY")
|
||||
}
|
||||
if data.CurrentTemp != 72 {
|
||||
t.Errorf("CurrentTemp = %v, want 72", data.CurrentTemp)
|
||||
}
|
||||
if data.Condition != "Partly Cloudy" {
|
||||
t.Errorf("Condition = %q, want %q", data.Condition, "Partly Cloudy")
|
||||
}
|
||||
if data.HighTemp != 78 {
|
||||
t.Errorf("HighTemp = %v, want 78", data.HighTemp)
|
||||
}
|
||||
if data.LowTemp != 65 {
|
||||
t.Errorf("LowTemp = %v, want 65", data.LowTemp)
|
||||
}
|
||||
if data.Humidity != "55%" {
|
||||
t.Errorf("Humidity = %q, want %q", data.Humidity, "55%")
|
||||
}
|
||||
if data.Wind != "SW 10 mph" {
|
||||
t.Errorf("Wind = %q, want %q", data.Wind, "SW 10 mph")
|
||||
}
|
||||
|
||||
if len(data.Forecast) != 2 {
|
||||
t.Fatalf("Forecast len = %d, want 2", len(data.Forecast))
|
||||
}
|
||||
if data.Forecast[0].Day != "Mon" {
|
||||
t.Errorf("Forecast[0].Day = %q, want %q", data.Forecast[0].Day, "Mon")
|
||||
}
|
||||
if data.Forecast[0].HighTemp != 80 {
|
||||
t.Errorf("Forecast[0].HighTemp = %v, want 80", data.Forecast[0].HighTemp)
|
||||
}
|
||||
if data.Forecast[1].Condition != "Rain" {
|
||||
t.Errorf("Forecast[1].Condition = %q, want %q", data.Forecast[1].Condition, "Rain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWeather_MockBrowser(t *testing.T) {
|
||||
doc := makeWeatherDoc()
|
||||
|
||||
browser := &extractortest.MockBrowser{
|
||||
Documents: map[string]*extractortest.MockDocument{
|
||||
"https://duckduckgo.com/?kp=-2&q=weather+new+york": doc,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := DefaultConfig.GetWeather(context.Background(), browser, "new york")
|
||||
if err != nil {
|
||||
t.Fatalf("GetWeather() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Location != "New York, NY" {
|
||||
t.Errorf("Location = %q, want %q", data.Location, "New York, NY")
|
||||
}
|
||||
if data.CurrentTemp != 72 {
|
||||
t.Errorf("CurrentTemp = %v, want 72", data.CurrentTemp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWeather_Empty(t *testing.T) {
|
||||
doc := &extractortest.MockDocument{
|
||||
MockNode: extractortest.MockNode{
|
||||
Children: map[string]extractor.Nodes{},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := extractWeather(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("extractWeather() error: %v", err)
|
||||
}
|
||||
|
||||
if data.Location != "" || data.CurrentTemp != 0 {
|
||||
t.Error("expected zero values for empty doc")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user