mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-05-19 10:59:34 +00:00
Release 2.36.0 (#4127)
## Release Notes Thanks to: @cgillinger, @khassel, @KristjanESPERANTO, @sonnyb9 > ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change to previous release) [Compare to previous Release v2.35.0](https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.35.0...v2.36.0) This release falls outside the quarterly schedule. We opted for an early release due to: - Security fix for the internal cors proxy - API change of the weather provider smi - Several bug fixes ### Breaking Changes The cors proxy is now disabled by default. If required, it must be explicitly enabled in the `config.js` file. See the [documentation](https://docs.magicmirror.builders/configuration/cors.html). ### ⚠️ Security You can find several publicly accessible MagicMirror² instances. This should never be done. Doing so makes your entire configuration, including secrets and API keys, publicly visible. Furthermore, it allows attackers to target the host; this is only prevented beginning with this release. Public MagicMirror² instances should always run behind a reverse proxy with authentication. ### [core] - Prepare Release 2.36.0 (#4126) - Allow HTTPFetcher to pass through 304 responses (#4120) - fix(http-fetcher): fall back to reloadInterval after retries exhausted (#4113) - config endpoint must handle functions in module configs (#4106) - fix replaceSecretPlaceholder (#4104) - restrict replaceSecretPlaceholder to cors with allowWhitelist (#4102) - fix: prevent crash when config is undefined in socket handler (#4096) - fix cors function for alpine linux (#4091) - fix(cors): prevent SSRF via DNS rebinding (#4090) - add option to disable or restrict cors endpoint (#4087) - fix: prevent SSRF via /cors endpoint by blocking private/reserved IPs (#4084) - chore: add permissions section to enforce pull-request rules workflow (#4079) - update version for develop ### [dependencies] - update dependencies (#4124) - chore: update dependencies (#4088) - refactor: enable ESLint rule "no-unused-vars" and handle related issues (#4080) ### [modules/newsfeed] - fix(newsfeed): prevent duplicate parse error callback when using pipeline (#4083) ### [modules/updatenotification] - fix(updatenotification): harden git command execution + simplify checkUpdates (#4115) - fix(tests): correct import path for git_helper module in updatenotification tests (#4078) ### [modules/weather] - fix(weather): use nearest openmeteo hourly data (#4123) - fix(weather): avoid loading state after reconnect (#4121) - weather: fix UV index display and add WeatherFlow precipitation (#4108) - fix(weather): restore OpenWeatherMap v2.5 support (#4101) - fix(weather): use stable instanceId to prevent duplicate fetchers (#4092) - SMHI: migrate to SNOW1gv1 API (replace deprecated PMP3gv2) (#4082) ### [testing] - ci(actions): set explicit token permissions (#4114) - fix(http_fetcher): use undici.fetch when dispatcher is present (#4097) - ci(codeql): also scan develop branch on push and PR (#4086) - refactor: replace implicit global config with explicit global.config (#4085) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com> Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: Koen Konst <koenspero@gmail.com> Co-authored-by: Koen Konst <c.h.konst@avisi.nl> Co-authored-by: dathbe <github@beffa.us> Co-authored-by: Marcel <m-idler@users.noreply.github.com> Co-authored-by: Kevin G. <crazylegstoo@gmail.com> Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com> Co-authored-by: Jboucly <contact@jboucly.fr> Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com> Co-authored-by: Jordan Welch <JordanHWelch@gmail.com> Co-authored-by: Blackspirits <blackspirits@gmail.com> Co-authored-by: Samed Ozdemir <samed@xsor.io> Co-authored-by: in-voker <58696565+in-voker@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: cgillinger <christian.gillinger@gmail.com> Co-authored-by: Sonny B <43247590+sonnyb9@users.noreply.github.com> Co-authored-by: sonnyb9 <sonnyb9@users.noreply.github.com>
This commit is contained in:
116
tests/unit/modules/default/weather/node_helper_spec.js
Normal file
116
tests/unit/modules/default/weather/node_helper_spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import Module from "node:module";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* Creates a fresh weather node helper instance with isolated mocks.
|
||||
* @returns {Promise<object>} The mocked weather node helper.
|
||||
*/
|
||||
async function loadWeatherNodeHelper () {
|
||||
vi.resetModules();
|
||||
|
||||
const loggerMock = {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
};
|
||||
const originalRequire = Module.prototype.require;
|
||||
|
||||
Module.prototype.require = function (id) {
|
||||
if (id === "node_helper") {
|
||||
return {
|
||||
create: vi.fn((definition) => definition)
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "logger") {
|
||||
return loggerMock;
|
||||
}
|
||||
|
||||
return originalRequire.apply(this, arguments);
|
||||
};
|
||||
|
||||
let helper;
|
||||
try {
|
||||
const helperModule = await import("../../../../../defaultmodules/weather/node_helper");
|
||||
helper = helperModule.default || helperModule;
|
||||
} finally {
|
||||
Module.prototype.require = originalRequire;
|
||||
}
|
||||
|
||||
helper.providers = {};
|
||||
helper.lastData = {};
|
||||
helper.sendSocketNotification = vi.fn();
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe("weather node_helper reconnect handling", () => {
|
||||
it("re-sends cached weather data when a client reconnects", async () => {
|
||||
const helper = await loadWeatherNodeHelper();
|
||||
const instanceId = "weather-current";
|
||||
const cachedPayload = {
|
||||
instanceId,
|
||||
type: "current",
|
||||
data: { temperature: 8.5 }
|
||||
};
|
||||
|
||||
helper.providers[instanceId] = { locationName: "Munich, BY" };
|
||||
helper.lastData[instanceId] = cachedPayload;
|
||||
|
||||
await helper.initWeatherProvider({
|
||||
weatherProvider: "openmeteo",
|
||||
instanceId,
|
||||
type: "current"
|
||||
});
|
||||
|
||||
expect(helper.sendSocketNotification).toHaveBeenNthCalledWith(1, "WEATHER_INITIALIZED", {
|
||||
instanceId,
|
||||
locationName: "Munich, BY"
|
||||
});
|
||||
expect(helper.sendSocketNotification).toHaveBeenNthCalledWith(2, "WEATHER_DATA", cachedPayload);
|
||||
expect(helper.sendSocketNotification).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not send WEATHER_DATA on reconnect when no cached payload exists", async () => {
|
||||
const helper = await loadWeatherNodeHelper();
|
||||
const instanceId = "weather-current";
|
||||
|
||||
helper.providers[instanceId] = { locationName: "Munich, BY" };
|
||||
|
||||
await helper.initWeatherProvider({
|
||||
weatherProvider: "openmeteo",
|
||||
instanceId,
|
||||
type: "current"
|
||||
});
|
||||
|
||||
expect(helper.sendSocketNotification).toHaveBeenCalledWith("WEATHER_INITIALIZED", {
|
||||
instanceId,
|
||||
locationName: "Munich, BY"
|
||||
});
|
||||
expect(helper.sendSocketNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cleans up provider and cached data when stopping an instance", async () => {
|
||||
const helper = await loadWeatherNodeHelper();
|
||||
const instanceId = "weather-current";
|
||||
const stop = vi.fn();
|
||||
|
||||
helper.providers[instanceId] = { stop };
|
||||
helper.lastData[instanceId] = {
|
||||
instanceId,
|
||||
type: "current",
|
||||
data: { temperature: 8.5 }
|
||||
};
|
||||
|
||||
helper.stopWeatherProvider(instanceId);
|
||||
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
expect(helper.providers[instanceId]).toBeUndefined();
|
||||
expect(helper.lastData[instanceId]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -144,12 +144,15 @@ describe("OpenMeteoProvider", () => {
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
const currentHourUnix = Math.floor(currentData.current_weather.time / 3600) * 3600;
|
||||
const currentHourIndex = currentData.hourly.time.findIndex((time) => time === currentHourUnix);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.temperature).toBe(8.5);
|
||||
expect(result.windSpeed).toBeCloseTo(4.7, 1);
|
||||
expect(result.windFromDirection).toBe(9);
|
||||
expect(result.humidity).toBe(83);
|
||||
expect(result.humidity).toBe(currentData.hourly.relativehumidity_2m[currentHourIndex]);
|
||||
expect(result.humidity).not.toBe(currentData.hourly.relativehumidity_2m[0]);
|
||||
});
|
||||
|
||||
it("should include sunrise and sunset from daily data", async () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import { setupServer } from "msw/node";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from "vitest";
|
||||
|
||||
import onecallData from "../../../../../mocks/weather_owm_onecall.json" with { type: "json" };
|
||||
import currentData from "../../../../../mocks/weather_owm_current.json" with { type: "json" };
|
||||
import forecastData from "../../../../../mocks/weather_owm_forecast.json" with { type: "json" };
|
||||
|
||||
let server;
|
||||
|
||||
@@ -232,4 +234,321 @@ describe("OpenWeatherMapProvider", () => {
|
||||
expect(provider.locationName).toBe("America/New_York");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Current Weather (/weather endpoint)", () => {
|
||||
it("should parse current weather from /weather endpoint", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/weather",
|
||||
type: "current"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/weather", () => {
|
||||
return HttpResponse.json(currentData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(result.temperature).toBe(-0.27);
|
||||
expect(result.feelsLikeTemp).toBe(-3.9);
|
||||
expect(result.humidity).toBe(54);
|
||||
expect(result.windSpeed).toBe(3.09);
|
||||
expect(result.windFromDirection).toBe(220);
|
||||
expect(result.weatherType).toBe("cloudy-windy");
|
||||
expect(result.sunrise).toBeInstanceOf(Date);
|
||||
expect(result.sunset).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("should set location name from city name and country", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/weather",
|
||||
type: "current"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/weather", () => {
|
||||
return HttpResponse.json(currentData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
await dataPromise;
|
||||
|
||||
expect(provider.locationName).toBe("Munich, DE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Forecast (/forecast endpoint)", () => {
|
||||
it("should parse /forecast endpoint into daily grouped forecast", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should correctly aggregate min/max temperatures per day", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1: temp_min values: -1.5, -1.5, -1.0, 0.5, 1.5, 1.0, 0.5, -0.5 → min=-1.5
|
||||
expect(result[0].minTemperature).toBe(-1.5);
|
||||
// Day 1: temp_max values: -0.5, -0.9, 0.0, 1.5, 2.5, 2.0, 1.2, 0.1 → max=2.5
|
||||
expect(result[0].maxTemperature).toBe(2.5);
|
||||
// Day 2: temp_min values: 0.0, 0.5, 1.5, 3.0, 4.5, 4.0, 2.5, 1.0 → min=0.0
|
||||
expect(result[1].minTemperature).toBe(0.0);
|
||||
// Day 2: temp_max values: 1.0, 1.5, 2.5, 4.0, 5.5, 5.0, 3.5, 2.0 → max=5.5
|
||||
expect(result[1].maxTemperature).toBe(5.5);
|
||||
});
|
||||
|
||||
it("should pick daytime weather type (8-17h)", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1 daytime entries have icon "10d" → "rain"
|
||||
expect(result[0].weatherType).toBe("rain");
|
||||
// Day 2 daytime entries have icon "09d" → "showers"
|
||||
expect(result[1].weatherType).toBe("showers");
|
||||
});
|
||||
|
||||
it("should accumulate precipitation per day", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1: two rain entries of 0.6 each = 1.2
|
||||
expect(result[0].rain).toBeCloseTo(1.2);
|
||||
expect(result[0].precipitationAmount).toBeCloseTo(1.2);
|
||||
// Day 2: one snow entry of 0.5
|
||||
expect(result[1].snow).toBeCloseTo(0.5);
|
||||
expect(result[1].precipitationAmount).toBeCloseTo(0.5);
|
||||
});
|
||||
|
||||
it("should set location name from city in forecast response", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
await dataPromise;
|
||||
|
||||
expect(provider.locationName).toBe("Munich, DE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Hourly (/forecast endpoint with type hourly)", () => {
|
||||
it("should return individual 3h entries instead of aggregating", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(forecastData.list.length);
|
||||
});
|
||||
|
||||
it("should map temperature and wind from each 3h slot", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(result[0].temperature).toBe(forecastData.list[0].main.temp);
|
||||
expect(result[0].windSpeed).toBe(forecastData.list[0].wind.speed);
|
||||
expect(result[0].precipitationProbability).toBe(forecastData.list[0].pop * 100);
|
||||
});
|
||||
|
||||
it("should include precipitation when present in a slot", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Entry at index 3 has rain: { "3h": 0.6 }
|
||||
expect(result[3].rain).toBe(0.6);
|
||||
expect(result[3].precipitationAmount).toBe(0.6);
|
||||
// Entry at index 11 has snow: { "3h": 0.5 }
|
||||
expect(result[11].snow).toBe(0.5);
|
||||
expect(result[11].precipitationAmount).toBe(0.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* Tests data parsing for current, forecast, and hourly weather types.
|
||||
* SMHI provides data only for Sweden, uses metric system.
|
||||
*
|
||||
* Fixture: weather_smhi.json uses SNOW1gv1 format (replaced PMP3gv2 2026-03-31)
|
||||
*/
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
@@ -123,11 +125,10 @@ describe("SMHIProvider", () => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
// Use data with rain (pcat=3 at index 2)
|
||||
// Use entry at index 2 which has ptype=5 (snow) but pmedian=0.0
|
||||
const rainData = JSON.parse(JSON.stringify(smhiData));
|
||||
// Make the rain entry the closest to "now"
|
||||
rainData.timeSeries = [rainData.timeSeries[2]];
|
||||
rainData.timeSeries[0].validTime = new Date().toISOString();
|
||||
rainData.timeSeries[0].time = new Date().toISOString();
|
||||
|
||||
server.use(
|
||||
http.get("https://opendata-download-metfcst.smhi.se/*", () => {
|
||||
@@ -140,7 +141,8 @@ describe("SMHIProvider", () => {
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(result.rain).toBe(0.0); // pmedian value with pcat=3 (rain)
|
||||
// pmedian is 0.0 at this entry, so all precipitation amounts are 0
|
||||
expect(result.rain).toBe(0);
|
||||
expect(result.precipitationAmount).toBe(0.0);
|
||||
expect(result.snow).toBe(0);
|
||||
});
|
||||
|
||||
@@ -240,6 +240,7 @@ describe("WeatherAPIProvider", () => {
|
||||
expect(result[0].minTemperature).toBe(-8);
|
||||
expect(result[0].maxTemperature).toBe(-1);
|
||||
expect(result[0].weatherType).toBe("day-sprinkle");
|
||||
expect(result[0].uvIndex).toBe(1);
|
||||
expect(result[0].sunrise).toBeInstanceOf(Date);
|
||||
expect(result[0].sunset).toBeInstanceOf(Date);
|
||||
});
|
||||
@@ -275,6 +276,7 @@ describe("WeatherAPIProvider", () => {
|
||||
expect(result[0].humidity).toBe(85);
|
||||
expect(result[0].windFromDirection).toBe(210);
|
||||
expect(result[0].weatherType).toBe("night-sprinkle");
|
||||
expect(result[0].uvIndex).toBe(0);
|
||||
expect(result[0].precipitationProbability).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,6 +85,9 @@ describe("WeatherFlowProvider", () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.temperature).toBe(16);
|
||||
expect(result.humidity).toBe(28);
|
||||
expect(result.precipitationAmount).toBe(0);
|
||||
expect(result.precipitationUnits).toBe("mm");
|
||||
expect(result.precipitationProbability).toBe(0);
|
||||
expect(result.weatherType).not.toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { describe, it, expect, vi, beforeAll, beforeEach, afterAll, afterEach }
|
||||
import yrData from "../../../../../mocks/weather_yr.json" with { type: "json" };
|
||||
|
||||
const YR_FORECAST_URL = "https://api.met.no/weatherapi/locationforecast/**";
|
||||
const YR_SUNRISE_URL = "https://api.met.no/weatherapi/sunrise/**";
|
||||
|
||||
// Fixed time: 30 minutes after the first timeseries entry (2026-02-06T21:00:00Z)
|
||||
// This ensures timeseries[0] is always chosen as the closest past entry.
|
||||
|
||||
Reference in New Issue
Block a user