test: add weather logic test suite
All checks were successful
CI / Lint, test, typecheck and build (push) Successful in 9m58s
All checks were successful
CI / Lint, test, typecheck and build (push) Successful in 9m58s
This commit is contained in:
41
tests/forecast-utils.test.ts
Normal file
41
tests/forecast-utils.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getEffectiveHourlyWeatherCode, getForecastConditionKey, isForecastNight } from "@/lib/forecast-utils";
|
||||
|
||||
describe("forecast utilities", () => {
|
||||
it("promotes dry weather code to rain when precipitation probability is high", () => {
|
||||
expect(
|
||||
getEffectiveHourlyWeatherCode({
|
||||
weatherCode: 0,
|
||||
precipitation: 0,
|
||||
precipitationProbability: 100,
|
||||
}),
|
||||
).toBe(61);
|
||||
});
|
||||
|
||||
it("keeps clear weather code when there is no precipitation signal", () => {
|
||||
expect(
|
||||
getEffectiveHourlyWeatherCode({
|
||||
weatherCode: 0,
|
||||
precipitation: 0,
|
||||
precipitationProbability: 10,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("recognizes night hours from sunrise and sunset", () => {
|
||||
const day = {
|
||||
sunrise: "2026-06-15T05:00",
|
||||
sunset: "2026-06-15T21:00",
|
||||
};
|
||||
|
||||
expect(isForecastNight("2026-06-15T03:00", day)).toBe(true);
|
||||
expect(isForecastNight("2026-06-15T12:00", day)).toBe(false);
|
||||
expect(isForecastNight("2026-06-15T22:00", day)).toBe(true);
|
||||
});
|
||||
|
||||
it("maps storm and rain weather codes to condition keys", () => {
|
||||
expect(getForecastConditionKey(61)).toBe("forecast.condition.rain");
|
||||
expect(getForecastConditionKey(95)).toBe("forecast.condition.thunderstorm");
|
||||
});
|
||||
});
|
||||
67
tests/warning-regions.test.ts
Normal file
67
tests/warning-regions.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getCountyTerytForLocation, warningMatchesCounty, warningMatchesLocalSelection } from "@/lib/warning-regions";
|
||||
import type { WeatherWarning } from "@/types/imgw";
|
||||
import type { SelectedLocation } from "@/types/location";
|
||||
|
||||
function warning(overrides: Partial<WeatherWarning> = {}): WeatherWarning {
|
||||
return {
|
||||
id: "meteo-1",
|
||||
kind: "meteo",
|
||||
level: 1,
|
||||
title: "Storm",
|
||||
description: null,
|
||||
comment: null,
|
||||
validFrom: null,
|
||||
validTo: null,
|
||||
publishedAt: null,
|
||||
probability: null,
|
||||
areas: [],
|
||||
terytCodes: [],
|
||||
provinces: ["mazowieckie"],
|
||||
office: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function location(overrides: Partial<SelectedLocation> = {}): SelectedLocation {
|
||||
return {
|
||||
name: "Warszawa",
|
||||
province: "mazowieckie",
|
||||
district: "Warszawa",
|
||||
country: "Poland",
|
||||
countryCode: "PL",
|
||||
admin1: "Mazowieckie",
|
||||
admin2: "Warszawa",
|
||||
timezone: "Europe/Warsaw",
|
||||
region: "PL",
|
||||
countyTeryt: "1465",
|
||||
latitude: 52.23,
|
||||
longitude: 21.01,
|
||||
stationId: null,
|
||||
stationName: null,
|
||||
distanceKm: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("warning region matching", () => {
|
||||
it("resolves Mazowieckie county codes from district names", () => {
|
||||
expect(getCountyTerytForLocation("mazowieckie", "Warszawa", null)).toBe("1465");
|
||||
expect(getCountyTerytForLocation("mazowieckie", "radomski", null)).toBe("1425");
|
||||
});
|
||||
|
||||
it("matches meteorological warnings by county TERYT prefix", () => {
|
||||
const warsawWarning = warning({ terytCodes: ["146501"] });
|
||||
|
||||
expect(warningMatchesCounty(warsawWarning, "1465")).toBe(true);
|
||||
expect(warningMatchesCounty(warsawWarning, "1425")).toBe(false);
|
||||
});
|
||||
|
||||
it("uses county filtering before province fallback for selected locations", () => {
|
||||
const radomWarning = warning({ terytCodes: ["142501"] });
|
||||
|
||||
expect(warningMatchesLocalSelection(radomWarning, "mazowieckie", location())).toBe(false);
|
||||
expect(warningMatchesLocalSelection(radomWarning, "mazowieckie", location({ countyTeryt: null }))).toBe(true);
|
||||
});
|
||||
});
|
||||
117
tests/weather-brief.test.ts
Normal file
117
tests/weather-brief.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildTomorrowWeatherBrief, buildWeatherBrief } from "@/lib/weather-brief";
|
||||
import type { WeatherForecast, HourlyForecast } from "@/types/forecast";
|
||||
import type { WeatherWarning } from "@/types/imgw";
|
||||
import { getWeatherCapabilities } from "@/types/weather-region";
|
||||
|
||||
function hour(overrides: Partial<HourlyForecast> = {}): HourlyForecast {
|
||||
return {
|
||||
time: "2026-06-14T08:00",
|
||||
temperature: 10,
|
||||
feelsLike: 9,
|
||||
precipitationProbability: 20,
|
||||
precipitation: 0,
|
||||
weatherCode: 3,
|
||||
windSpeed: 4,
|
||||
source: "open-meteo",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function forecast(hours: HourlyForecast[]): WeatherForecast {
|
||||
return {
|
||||
latitude: 52.23,
|
||||
longitude: 21.01,
|
||||
region: "PL",
|
||||
timezone: "Europe/Warsaw",
|
||||
hourly: hours,
|
||||
daily: [],
|
||||
sources: ["open-meteo"],
|
||||
capabilities: getWeatherCapabilities("PL"),
|
||||
unavailableReasons: [],
|
||||
};
|
||||
}
|
||||
|
||||
function warning(overrides: Partial<WeatherWarning> = {}): WeatherWarning {
|
||||
return {
|
||||
id: "meteo-1",
|
||||
kind: "meteo",
|
||||
level: 2,
|
||||
title: "Thunderstorms",
|
||||
description: null,
|
||||
comment: null,
|
||||
validFrom: "2026-06-15T06:00:00.000Z",
|
||||
validTo: "2026-06-15T18:00:00.000Z",
|
||||
publishedAt: null,
|
||||
probability: 80,
|
||||
areas: [],
|
||||
terytCodes: ["146501"],
|
||||
provinces: ["mazowieckie"],
|
||||
office: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("weather briefs", () => {
|
||||
it("uses selected temperature and wind units in the morning brief", () => {
|
||||
const brief = buildWeatherBrief({
|
||||
forecast: forecast([
|
||||
hour({ time: "2026-06-14T08:00", temperature: 10, windSpeed: 4, precipitationProbability: 80 }),
|
||||
hour({ time: "2026-06-14T09:00", temperature: 12, windSpeed: 5, precipitationProbability: 30 }),
|
||||
]),
|
||||
warnings: [],
|
||||
province: "mazowieckie",
|
||||
countyTeryt: "1465",
|
||||
locationName: "Warsaw",
|
||||
language: "en",
|
||||
temperatureUnit: "f",
|
||||
windSpeedUnit: "mph",
|
||||
now: new Date("2026-06-14T06:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(brief).not.toBeNull();
|
||||
expect(brief?.pushBody).toContain("50°F-54°F");
|
||||
expect(brief?.pushBody).toContain("wind 11 mph");
|
||||
expect(brief?.pushBody).not.toContain("km/h");
|
||||
});
|
||||
|
||||
it("warns about tomorrow storms from model weather codes without IMGW warning", () => {
|
||||
const brief = buildTomorrowWeatherBrief({
|
||||
forecast: forecast([
|
||||
hour({ time: "2026-06-15T08:00", temperature: 14, weatherCode: 3, precipitationProbability: 20 }),
|
||||
hour({ time: "2026-06-15T15:00", temperature: 21, weatherCode: 95, precipitationProbability: 30 }),
|
||||
]),
|
||||
warnings: [],
|
||||
province: "mazowieckie",
|
||||
countyTeryt: "1465",
|
||||
locationName: "Warsaw",
|
||||
language: "en",
|
||||
now: new Date("2026-06-14T16:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(brief?.severity).toBe("warning");
|
||||
expect(brief?.headline).toBe("Thunderstorms are possible tomorrow");
|
||||
expect(brief?.pushBody).toContain("thunderstorms");
|
||||
expect(brief?.pushBody).not.toContain("IMGW");
|
||||
});
|
||||
|
||||
it("prefers relevant IMGW county warning over province-wide context", () => {
|
||||
const brief = buildTomorrowWeatherBrief({
|
||||
forecast: forecast([hour({ time: "2026-06-15T12:00", weatherCode: 3 })]),
|
||||
warnings: [
|
||||
warning({ title: "Radom storm", terytCodes: ["142501"] }),
|
||||
warning({ title: "Warsaw storm", terytCodes: ["146501"] }),
|
||||
],
|
||||
province: "mazowieckie",
|
||||
countyTeryt: "1465",
|
||||
locationName: "Warsaw",
|
||||
language: "en",
|
||||
now: new Date("2026-06-14T16:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(brief?.headline).toBe("Tomorrow: IMGW Warsaw storm");
|
||||
expect(brief?.pushBody).toContain("IMGW: Warsaw storm");
|
||||
expect(brief?.pushBody).not.toContain("Radom storm");
|
||||
});
|
||||
});
|
||||
29
tests/weather-utils.test.ts
Normal file
29
tests/weather-utils.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
formatTemperature,
|
||||
formatWindSpeed,
|
||||
isTemperatureUnit,
|
||||
isWindSpeedUnit,
|
||||
} from "@/lib/weather-utils";
|
||||
|
||||
describe("weather utilities", () => {
|
||||
it("formats wind speed in km/h by default", () => {
|
||||
expect(DEFAULT_WIND_SPEED_UNIT).toBe("kmh");
|
||||
expect(formatWindSpeed(5, "en")).toBe("18 km/h");
|
||||
});
|
||||
|
||||
it("formats selected temperature and wind units", () => {
|
||||
expect(formatTemperature(10, "en", "f")).toBe("50°F");
|
||||
expect(formatWindSpeed(4, "en", "mph")).toBe("9 mph");
|
||||
expect(formatWindSpeed(4, "en", "ms")).toBe("4.0 m/s");
|
||||
});
|
||||
|
||||
it("validates supported presentation units", () => {
|
||||
expect(isTemperatureUnit("c")).toBe(true);
|
||||
expect(isTemperatureUnit("kelvin")).toBe(false);
|
||||
expect(isWindSpeedUnit("kmh")).toBe(true);
|
||||
expect(isWindSpeedUnit("knots")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user