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"}}, ".forecast-day__precip": {&extractortest.MockNode{TextValue: "10%"}}, ".forecast-day__icon": {&extractortest.MockNode{Attrs: map[string]string{"alt": "PartlyCloudy"}}}, }, }, &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"}}, ".forecast-day__precip": {&extractortest.MockNode{TextValue: "80%"}}, ".forecast-day__icon": {&extractortest.MockNode{Attrs: map[string]string{"alt": "Rain"}}}, }, }, }, "div.module--weather .module__hourly-item": { &extractortest.MockNode{ Children: map[string]extractor.Nodes{ ".hourly-item__time": {&extractortest.MockNode{TextValue: "3 PM"}}, ".hourly-item__temp": {&extractortest.MockNode{TextValue: "74°"}}, ".hourly-item__condition": {&extractortest.MockNode{TextValue: "Partly Cloudy"}}, ".hourly-item__precip": {&extractortest.MockNode{TextValue: "5%"}}, ".hourly-item__icon": {&extractortest.MockNode{Attrs: map[string]string{"alt": "MostlyCloudy"}}}, }, }, &extractortest.MockNode{ Children: map[string]extractor.Nodes{ ".hourly-item__time": {&extractortest.MockNode{TextValue: "4 PM"}}, ".hourly-item__temp": {&extractortest.MockNode{TextValue: "73°"}}, ".hourly-item__condition": {&extractortest.MockNode{TextValue: "Cloudy"}}, ".hourly-item__icon": {&extractortest.MockNode{Attrs: map[string]string{"alt": "Cloudy"}}}, }, }, &extractortest.MockNode{ Children: map[string]extractor.Nodes{ ".hourly-item__time": {&extractortest.MockNode{TextValue: "5 PM"}}, ".hourly-item__temp": {&extractortest.MockNode{TextValue: "70°"}}, ".hourly-item__condition": {&extractortest.MockNode{TextValue: "Rain"}}, ".hourly-item__precip": {&extractortest.MockNode{TextValue: "60%"}}, ".hourly-item__icon": {&extractortest.MockNode{Attrs: map[string]string{"aria-label": "HeavyRain"}}}, }, }, }, }, }, } } 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") } // Daily forecast 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[0].Precipitation != 10 { t.Errorf("Forecast[0].Precipitation = %d, want 10", data.Forecast[0].Precipitation) } if data.Forecast[0].IconHint != "PartlyCloudy" { t.Errorf("Forecast[0].IconHint = %q, want %q", data.Forecast[0].IconHint, "PartlyCloudy") } if data.Forecast[1].Condition != "Rain" { t.Errorf("Forecast[1].Condition = %q, want %q", data.Forecast[1].Condition, "Rain") } if data.Forecast[1].Precipitation != 80 { t.Errorf("Forecast[1].Precipitation = %d, want 80", data.Forecast[1].Precipitation) } if data.Forecast[1].IconHint != "Rain" { t.Errorf("Forecast[1].IconHint = %q, want %q", data.Forecast[1].IconHint, "Rain") } // Hourly forecast if len(data.Hourly) != 3 { t.Fatalf("Hourly len = %d, want 3", len(data.Hourly)) } if data.Hourly[0].Time != "3 PM" { t.Errorf("Hourly[0].Time = %q, want %q", data.Hourly[0].Time, "3 PM") } if data.Hourly[0].Temp != 74 { t.Errorf("Hourly[0].Temp = %v, want 74", data.Hourly[0].Temp) } if data.Hourly[0].Condition != "Partly Cloudy" { t.Errorf("Hourly[0].Condition = %q, want %q", data.Hourly[0].Condition, "Partly Cloudy") } if data.Hourly[0].Precipitation != 5 { t.Errorf("Hourly[0].Precipitation = %d, want 5", data.Hourly[0].Precipitation) } if data.Hourly[0].IconHint != "MostlyCloudy" { t.Errorf("Hourly[0].IconHint = %q, want %q", data.Hourly[0].IconHint, "MostlyCloudy") } // Second hourly item has no precipitation if data.Hourly[1].Time != "4 PM" { t.Errorf("Hourly[1].Time = %q, want %q", data.Hourly[1].Time, "4 PM") } if data.Hourly[1].Precipitation != -1 { t.Errorf("Hourly[1].Precipitation = %d, want -1 (unavailable)", data.Hourly[1].Precipitation) } if data.Hourly[1].IconHint != "Cloudy" { t.Errorf("Hourly[1].IconHint = %q, want %q", data.Hourly[1].IconHint, "Cloudy") } // Third hourly item uses aria-label for icon hint if data.Hourly[2].Precipitation != 60 { t.Errorf("Hourly[2].Precipitation = %d, want 60", data.Hourly[2].Precipitation) } if data.Hourly[2].IconHint != "HeavyRain" { t.Errorf("Hourly[2].IconHint = %q, want %q", data.Hourly[2].IconHint, "HeavyRain") } } 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) } if len(data.Hourly) != 3 { t.Errorf("Hourly len = %d, want 3", len(data.Hourly)) } } 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") } if len(data.Hourly) != 0 { t.Errorf("expected no hourly data for empty doc, got %d", len(data.Hourly)) } } func TestExtractWeather_NoPrecipitation(t *testing.T) { doc := &extractortest.MockDocument{ MockNode: extractortest.MockNode{ Children: map[string]extractor.Nodes{ "div.module--weather .module__forecast-day": { &extractortest.MockNode{ Children: map[string]extractor.Nodes{ ".forecast-day__name": {&extractortest.MockNode{TextValue: "Wed"}}, ".forecast-day__high": {&extractortest.MockNode{TextValue: "85°"}}, ".forecast-day__low": {&extractortest.MockNode{TextValue: "70°"}}, ".forecast-day__condition": {&extractortest.MockNode{TextValue: "Clear"}}, }, }, }, "div.module--weather .module__hourly-item": { &extractortest.MockNode{ Children: map[string]extractor.Nodes{ ".hourly-item__time": {&extractortest.MockNode{TextValue: "12 PM"}}, ".hourly-item__temp": {&extractortest.MockNode{TextValue: "82°"}}, }, }, }, }, }, } data, err := extractWeather(doc) if err != nil { t.Fatalf("extractWeather() error: %v", err) } if len(data.Forecast) != 1 { t.Fatalf("Forecast len = %d, want 1", len(data.Forecast)) } if data.Forecast[0].Precipitation != -1 { t.Errorf("Forecast[0].Precipitation = %d, want -1 (unavailable)", data.Forecast[0].Precipitation) } if data.Forecast[0].IconHint != "" { t.Errorf("Forecast[0].IconHint = %q, want empty", data.Forecast[0].IconHint) } if len(data.Hourly) != 1 { t.Fatalf("Hourly len = %d, want 1", len(data.Hourly)) } if data.Hourly[0].Precipitation != -1 { t.Errorf("Hourly[0].Precipitation = %d, want -1 (unavailable)", data.Hourly[0].Precipitation) } if data.Hourly[0].IconHint != "" { t.Errorf("Hourly[0].IconHint = %q, want empty", data.Hourly[0].IconHint) } } func TestExtractIconHint_Priority(t *testing.T) { // aria-label takes priority over title and alt nodes := extractor.Nodes{ &extractortest.MockNode{ Attrs: map[string]string{ "aria-label": "Snow", "title": "SnowTitle", "alt": "SnowAlt", }, }, } if got := extractIconHint(nodes); got != "Snow" { t.Errorf("extractIconHint() = %q, want %q (aria-label priority)", got, "Snow") } // title used when aria-label absent nodes = extractor.Nodes{ &extractortest.MockNode{ Attrs: map[string]string{ "title": "Drizzle", "alt": "DrizzleAlt", }, }, } if got := extractIconHint(nodes); got != "Drizzle" { t.Errorf("extractIconHint() = %q, want %q (title fallback)", got, "Drizzle") } // alt used as last fallback nodes = extractor.Nodes{ &extractortest.MockNode{ Attrs: map[string]string{"alt": "MostlyClear"}, }, } if got := extractIconHint(nodes); got != "MostlyClear" { t.Errorf("extractIconHint() = %q, want %q (alt fallback)", got, "MostlyClear") } // empty when no nodes if got := extractIconHint(nil); got != "" { t.Errorf("extractIconHint(nil) = %q, want empty", got) } }