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>
142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
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")
|
|
}
|
|
}
|