diff --git a/demo/src/custom-cards/ha-demo-card.ts b/demo/src/custom-cards/ha-demo-card.ts index 999aff5afc..2a464c2c1b 100644 --- a/demo/src/custom-cards/ha-demo-card.ts +++ b/demo/src/custom-cards/ha-demo-card.ts @@ -26,7 +26,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { @state() private _switching = false; - private _hidden = localStorage.hide_demo_card; + private _hidden = window.localStorage.getItem("hide_demo_card"); public getCardSize() { return this._hidden ? 0 : 2; diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts index 338fdbeafb..af8fec7d19 100644 --- a/landing-page/src/ha-landing-page.ts +++ b/landing-page/src/ha-landing-page.ts @@ -122,7 +122,10 @@ class HaLandingPage extends LandingPageBaseElement { if (language !== this.language && language) { this.language = language; try { - localStorage.setItem("selectedLanguage", JSON.stringify(language)); + window.localStorage.setItem( + "selectedLanguage", + JSON.stringify(language) + ); } catch (err: any) { // Ignore } diff --git a/package.json b/package.json index d0d75bfd31..21e1cc5cef 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,7 @@ "gulp-rename": "2.0.0", "html-minifier-terser": "7.2.0", "husky": "9.1.7", + "jsdom": "25.0.1", "jszip": "3.10.1", "lint-staged": "15.2.10", "lit-analyzer": "2.0.3", diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 6cfb67b696..ae2d3abcd4 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -327,7 +327,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { this.language = language; try { - localStorage.setItem("selectedLanguage", JSON.stringify(language)); + window.localStorage.setItem("selectedLanguage", JSON.stringify(language)); } catch (err: any) { // Ignore } diff --git a/src/common/auth/token_storage.ts b/src/common/auth/token_storage.ts index 4bfe18981c..5bcd7a64f2 100644 --- a/src/common/auth/token_storage.ts +++ b/src/common/auth/token_storage.ts @@ -1,8 +1,6 @@ import type { AuthData } from "home-assistant-js-websocket"; import { extractSearchParam } from "../url/search-params"; -const storage = window.localStorage || {}; - declare global { interface Window { __tokenCache: { @@ -38,9 +36,15 @@ export function saveTokens(tokens: AuthData | null) { if (tokenCache.writeEnabled) { try { - storage.hassTokens = JSON.stringify(tokens); + window.localStorage.setItem("hassTokens", JSON.stringify(tokens)); } catch (err: any) { // write failed, ignore it. Happens if storage is full or private mode. + // eslint-disable-next-line no-console + console.warn( + "Failed to store tokens; Are you in private mode or is your storage full?" + ); + // eslint-disable-next-line no-console + console.error("Error storing tokens:", err); } } } @@ -51,12 +55,11 @@ export function enableWrite() { saveTokens(tokenCache.tokens); } } + export function loadTokens() { if (tokenCache.tokens === undefined) { try { - // Delete the old token cache. - delete storage.tokens; - const tokens = storage.hassTokens; + const tokens = window.localStorage.getItem("hassTokens"); if (tokens) { tokenCache.tokens = JSON.parse(tokens); tokenCache.writeEnabled = true; diff --git a/src/data/panel.ts b/src/data/panel.ts index e6470e315e..14012b1059 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -4,10 +4,11 @@ import type { HomeAssistant, PanelInfo } from "../types"; /** Panel to show when no panel is picked. */ export const DEFAULT_PANEL = "lovelace"; -export const getStorageDefaultPanelUrlPath = (): string => - localStorage.defaultPanel - ? JSON.parse(localStorage.defaultPanel) - : DEFAULT_PANEL; +export const getStorageDefaultPanelUrlPath = (): string => { + const defaultPanel = window.localStorage.getItem("defaultPanel"); + + return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL; +}; export const setDefaultPanel = ( element: HTMLElement, diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 4716165ebe..5c82e575d8 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -482,7 +482,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { storeState(this.hass!); } else { try { - localStorage.setItem("selectedLanguage", JSON.stringify(language)); + window.localStorage.setItem( + "selectedLanguage", + JSON.stringify(language) + ); } catch (err: any) { // Ignore } diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index 536cdbbf45..1a21545e43 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -481,13 +481,14 @@ export class HaAutomationTrace extends LitElement { if (!traceText) { return; } - localStorage.devTrace = traceText; + window.localStorage.setItem("devTrace", traceText); this._loadLocalTrace(traceText); } private _loadLocalStorageTrace() { - if (localStorage.devTrace) { - this._loadLocalTrace(localStorage.devTrace); + const devTrace = window.localStorage.getItem("devTrace"); + if (devTrace) { + this._loadLocalTrace(devTrace); } } diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index 1551c0fe3c..08b0561312 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -487,13 +487,15 @@ export class HaScriptTrace extends LitElement { if (!traceText) { return; } - localStorage.devTrace = traceText; + + window.localStorage.setItem("devTrace", traceText); this._loadLocalTrace(traceText); } private _loadLocalStorageTrace() { - if (localStorage.devTrace) { - this._loadLocalTrace(localStorage.devTrace); + const devTrace = window.localStorage.getItem("devTrace"); + if (devTrace) { + this._loadLocalTrace(devTrace); } } diff --git a/src/util/ha-pref-storage.ts b/src/util/ha-pref-storage.ts index 2f38739217..4ef259ff0c 100644 --- a/src/util/ha-pref-storage.ts +++ b/src/util/ha-pref-storage.ts @@ -10,16 +10,24 @@ const STORED_STATE = [ "enableShortcuts", "defaultPanel", ]; -const STORAGE = window.localStorage || {}; export function storeState(hass: HomeAssistant) { try { STORED_STATE.forEach((key) => { const value = hass[key]; - STORAGE[key] = JSON.stringify(value === undefined ? null : value); + window.localStorage.setItem( + key, + JSON.stringify(value === undefined ? null : value) + ); }); } catch (err: any) { // Safari throws exception in private mode + // eslint-disable-next-line no-console + console.warn( + "Cannot store state; Are you in private mode or is your storage full?" + ); + // eslint-disable-next-line no-console + console.error(err); } } @@ -27,8 +35,9 @@ export function getState() { const state = {}; STORED_STATE.forEach((key) => { - if (key in STORAGE) { - let value = JSON.parse(STORAGE[key]); + const storageItem = window.localStorage.getItem(key); + if (storageItem !== null) { + let value = JSON.parse(storageItem); // selectedTheme went from string to object on 20200718 if (key === "selectedTheme" && typeof value === "string") { value = { theme: value }; @@ -44,8 +53,5 @@ export function getState() { } export function clearState() { - // STORAGE is an object if localStorage not available. - if (STORAGE.clear) { - STORAGE.clear(); - } + window.localStorage.clear(); } diff --git a/test/common/auth/token_storage/askWrite.test.ts b/test/common/auth/token_storage/askWrite.test.ts new file mode 100644 index 0000000000..2357d23edd --- /dev/null +++ b/test/common/auth/token_storage/askWrite.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; + +let askWrite; + +describe("token_storage.askWrite", () => { + afterEach(() => { + vi.resetModules(); + }); + + test("askWrite", async () => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: undefined, + writeEnabled: true, + }) + ); + + ({ askWrite } = await import("../../../../src/common/auth/token_storage")); + expect(askWrite()).toBe(false); + }); + + test("askWrite prefilled token", async () => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }, + writeEnabled: undefined, + }) + ); + + ({ askWrite } = await import("../../../../src/common/auth/token_storage")); + expect(askWrite()).toBe(true); + }); + + test("askWrite prefilled token, write enabled", async () => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }, + writeEnabled: true, + }) + ); + + ({ askWrite } = await import("../../../../src/common/auth/token_storage")); + expect(askWrite()).toBe(false); + }); +}); diff --git a/test/common/auth/token_storage/saveTokens.test.ts b/test/common/auth/token_storage/saveTokens.test.ts new file mode 100644 index 0000000000..b98d310cf3 --- /dev/null +++ b/test/common/auth/token_storage/saveTokens.test.ts @@ -0,0 +1,140 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import type { AuthData } from "home-assistant-js-websocket"; +import { FallbackStorage } from "../../../test_helper/local-storage-fallback"; + +let saveTokens; + +describe("token_storage.saveTokens", () => { + beforeEach(() => { + window.localStorage = new FallbackStorage(); + }); + + afterEach(() => { + vi.resetModules(); + vi.resetAllMocks(); + }); + + test("saveTokens", async () => { + const tokens: AuthData = { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }; + + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: undefined, + writeEnabled: undefined, + }) + ); + + ({ saveTokens } = await import( + "../../../../src/common/auth/token_storage" + )); + saveTokens(tokens); + expect(window.__tokenCache.tokens).toEqual(tokens); + }); + + test("saveTokens write enabled", async () => { + const tokens: AuthData = { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }; + + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: undefined, + writeEnabled: undefined, + }) + ); + + const extractSearchParamSpy = vi.fn().mockReturnValue("true"); + + vi.doMock("../../../../src/common/url/search-params", () => ({ + extractSearchParam: extractSearchParamSpy, + })); + const setItemSpy = vi.fn(); + window.localStorage.setItem = setItemSpy; + + ({ saveTokens } = await import( + "../../../../src/common/auth/token_storage" + )); + saveTokens(tokens); + expect(window.__tokenCache.tokens).toEqual(tokens); + expect(window.__tokenCache.writeEnabled).toBe(true); + expect(extractSearchParamSpy).toHaveBeenCalledOnce(); + expect(extractSearchParamSpy).toHaveBeenCalledWith("storeToken"); + expect(setItemSpy).toHaveBeenCalledOnce(); + expect(setItemSpy).toHaveBeenCalledWith( + "hassTokens", + JSON.stringify(tokens) + ); + }); + + test("saveTokens write enabled full storage", async () => { + const tokens: AuthData = { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }; + + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: undefined, + writeEnabled: true, + }) + ); + + const extractSearchParamSpy = vi.fn(); + + vi.doMock("../../../../src/common/url/search-params", () => ({ + extractSearchParam: extractSearchParamSpy, + })); + + const setItemSpy = vi.fn(() => { + throw new Error("Full storage"); + }); + + window.localStorage.setItem = setItemSpy; + + // eslint-disable-next-line no-global-assign + console = { + warn: vi.fn(), + error: vi.fn(), + } as unknown as Console; + + ({ saveTokens } = await import( + "../../../../src/common/auth/token_storage" + )); + saveTokens(tokens); + expect(window.__tokenCache.tokens).toEqual(tokens); + expect(window.__tokenCache.writeEnabled).toBe(true); + expect(extractSearchParamSpy).toBeCalledTimes(0); + expect(setItemSpy).toHaveBeenCalledOnce(); + expect(setItemSpy).toHaveBeenCalledWith( + "hassTokens", + JSON.stringify(tokens) + ); + // eslint-disable-next-line no-console + expect(console.warn).toHaveBeenCalledOnce(); + // eslint-disable-next-line no-console + expect(console.warn).toHaveBeenCalledWith( + "Failed to store tokens; Are you in private mode or is your storage full?" + ); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnce(); + }); +}); diff --git a/test/common/auth/token_storage/token_storage.test.ts b/test/common/auth/token_storage/token_storage.test.ts new file mode 100644 index 0000000000..42ed3ead23 --- /dev/null +++ b/test/common/auth/token_storage/token_storage.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, test, vi, afterEach, beforeEach } from "vitest"; +import type { AuthData } from "home-assistant-js-websocket"; +import { FallbackStorage } from "../../../test_helper/local-storage-fallback"; + +describe("token_storage", () => { + beforeEach(() => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: undefined, + writeEnabled: undefined, + }) + ); + window.localStorage = new FallbackStorage(); + }); + + afterEach(() => { + vi.resetAllMocks(); + vi.resetModules(); + }); + + test("initialize tokenCache", async () => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = undefined as any) + ); + + await import("../../../../src/common/auth/token_storage"); + + expect(window.__tokenCache).toEqual({ + tokens: undefined, + writeEnabled: undefined, + }); + }); + + test("should load tokens", async () => { + const tokens: AuthData = { + access_token: "test", + expires: 1800, + expires_in: 1800, + hassUrl: "http://localhost", + refresh_token: "refresh", + clientId: "client", + }; + + const getItemSpy = vi.fn(() => JSON.stringify(tokens)); + window.localStorage.getItem = getItemSpy; + + const { loadTokens } = await import( + "../../../../src/common/auth/token_storage" + ); + + const loadedTokens = loadTokens(); + expect(loadedTokens).toEqual(tokens); + + expect(window.__tokenCache.tokens).toEqual(tokens); + expect(window.__tokenCache.writeEnabled).toBe(true); + expect(getItemSpy).toHaveBeenCalledOnce(); + expect(getItemSpy).toHaveBeenCalledWith("hassTokens"); + }); + + test("should load null tokens", async () => { + const getItemSpy = vi.fn(() => "hello"); + window.localStorage.getItem = getItemSpy; + + const { loadTokens } = await import( + "../../../../src/common/auth/token_storage" + ); + + const loadedTokens = loadTokens(); + expect(loadedTokens).toEqual(null); + + expect(window.__tokenCache.tokens).toEqual(null); + expect(window.__tokenCache.writeEnabled).toBe(undefined); + expect(getItemSpy).toHaveBeenCalledOnce(); + expect(getItemSpy).toHaveBeenCalledWith("hassTokens"); + }); + + it("should enable write", async () => { + const { enableWrite } = await import( + "../../../../src/common/auth/token_storage" + ); + enableWrite(); + expect(window.__tokenCache.writeEnabled).toBe(true); + }); + + it("should enable write with tokens", async () => { + vi.stubGlobal( + "window.__tokenCache", + (window.__tokenCache = { + tokens: "testToken" as any, + }) + ); + + const setItemSpy = vi.fn(); + window.localStorage.setItem = setItemSpy; + + const { enableWrite } = await import( + "../../../../src/common/auth/token_storage" + ); + + enableWrite(); + expect(window.__tokenCache.writeEnabled).toBe(true); + expect(setItemSpy).toHaveBeenCalledOnce(); + expect(setItemSpy).toHaveBeenCalledWith( + "hassTokens", + JSON.stringify("testToken") + ); + }); +}); diff --git a/test/test_helper/local-storage-fallback.test.ts b/test/test_helper/local-storage-fallback.test.ts new file mode 100644 index 0000000000..67215444fe --- /dev/null +++ b/test/test_helper/local-storage-fallback.test.ts @@ -0,0 +1,52 @@ +import { describe, test, expect, beforeEach } from "vitest"; +import { FallbackStorage } from "./local-storage-fallback"; + +describe("FallbackStorage", () => { + let storage; + + beforeEach(() => { + storage = new FallbackStorage(); + }); + + test("should set and get an item", () => { + storage.setItem("key1", "value1"); + expect(storage.getItem("key1")).toBe("value1"); + }); + + test("should return null for non-existing item", () => { + expect(storage.getItem("nonExistingKey")).toBeNull(); + }); + + test("should remove an item", () => { + storage.setItem("key2", "value2"); + storage.removeItem("key2"); + expect(storage.getItem("key2")).toBeNull(); + }); + + test("should clear all items", () => { + storage.setItem("key3", "value3"); + storage.setItem("key4", "value4"); + storage.clear(); + expect(storage.getItem("key3")).toBeNull(); + expect(storage.getItem("key4")).toBeNull(); + }); + + test("should return the correct key for an index", () => { + storage.setItem("key5", "value5"); + storage.setItem("key6", "value6"); + expect(storage.key(0)).toBe("key5"); + expect(storage.key(1)).toBe("key6"); + }); + + test("should throw TypeError if key method is called without arguments", () => { + expect(() => storage.key()).toThrow(TypeError); + }); + + test("should return the correct length", () => { + storage.setItem("key7", "value7"); + storage.setItem("key8", "value8"); + expect(storage.length).toBe(2); + storage.removeItem("key7"); + expect(storage.length).toBe(1); + }); +}); diff --git a/test/test_helper/local-storage-fallback.ts b/test/test_helper/local-storage-fallback.ts new file mode 100644 index 0000000000..b598902806 --- /dev/null +++ b/test/test_helper/local-storage-fallback.ts @@ -0,0 +1,38 @@ +export class FallbackStorage implements Storage { + private valuesMap = new Map(); + + getItem(key) { + const stringKey = String(key); + if (this.valuesMap.has(key)) { + return String(this.valuesMap.get(stringKey)); + } + return null; + } + + setItem(key, val) { + this.valuesMap.set(String(key), String(val)); + } + + removeItem(key) { + this.valuesMap.delete(key); + } + + clear() { + this.valuesMap.clear(); + } + + key(i) { + if (arguments.length === 0) { + // this is a TypeError implemented on Chrome, Firefox throws Not enough arguments to Storage.key. + throw new TypeError( + "Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present." + ); + } + const arr = Array.from(this.valuesMap.keys()); + return arr[i]; + } + + get length() { + return this.valuesMap.size; + } +} diff --git a/test/util/ha-pref-storage.test.ts b/test/util/ha-pref-storage.test.ts new file mode 100644 index 0000000000..c984b7112b --- /dev/null +++ b/test/util/ha-pref-storage.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, afterEach, vi, test, beforeEach } from "vitest"; +import type { HomeAssistant } from "../../src/types"; +import { FallbackStorage } from "../test_helper/local-storage-fallback"; + +describe("ha-pref-storage", () => { + const mockHass = { + dockedSidebar: "auto", + selectedTheme: { theme: "default" }, + unknownKey: "unknownValue", + }; + + beforeEach(() => { + window.localStorage = new FallbackStorage(); + }); + + afterEach(() => { + vi.resetModules(); + vi.resetAllMocks(); + }); + + test("storeState", async () => { + const { storeState } = await import("../../src/util/ha-pref-storage"); + + window.localStorage.setItem = vi.fn(); + + storeState(mockHass as unknown as HomeAssistant); + expect(window.localStorage.setItem).toHaveBeenCalledTimes(8); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "dockedSidebar", + JSON.stringify("auto") + ); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "selectedTheme", + JSON.stringify({ theme: "default" }) + ); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "selectedLanguage", + JSON.stringify(null) + ); + expect(window.localStorage.setItem).not.toHaveBeenCalledWith( + "unknownKey", + JSON.stringify("unknownValue") + ); + }); + + test("storeState fails", async () => { + const { storeState } = await import("../../src/util/ha-pref-storage"); + + window.localStorage.setItem = vi.fn((key) => { + if (key === "selectedTheme") { + throw new Error("Test error"); + } + }); + + // eslint-disable-next-line no-global-assign + console = { + warn: vi.fn(), + error: vi.fn(), + } as unknown as Console; + + storeState(mockHass as unknown as HomeAssistant); + expect(window.localStorage.setItem).toHaveBeenCalledTimes(2); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "dockedSidebar", + JSON.stringify("auto") + ); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "selectedTheme", + JSON.stringify({ theme: "default" }) + ); + expect(window.localStorage.setItem).not.toHaveBeenCalledWith( + "selectedLanguage", + JSON.stringify(null) + ); + // eslint-disable-next-line no-console + expect(console.warn).toHaveBeenCalledOnce(); + // eslint-disable-next-line no-console + expect(console.warn).toHaveBeenCalledWith( + "Cannot store state; Are you in private mode or is your storage full?" + ); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnce(); + }); + + test("getState", async () => { + const { getState } = await import("../../src/util/ha-pref-storage"); + + window.localStorage.setItem("selectedTheme", JSON.stringify("test")); + window.localStorage.setItem("dockedSidebar", JSON.stringify(true)); + window.localStorage.setItem("selectedLanguage", JSON.stringify("german")); + + // should not be in state + window.localStorage.setItem("testEntry", JSON.stringify("this is a test")); + + const state = getState(); + expect(state).toEqual({ + dockedSidebar: "docked", + selectedTheme: { theme: "test" }, + selectedLanguage: "german", + }); + }); + + test("clearState", async () => { + const { clearState } = await import("../../src/util/ha-pref-storage"); + + window.localStorage.setItem("test", "test"); + + expect(window.localStorage.length).toEqual(1); + + clearState(); + expect(window.localStorage.length).toEqual(0); + }); +}); diff --git a/test/vitest.config.ts b/test/vitest.config.ts index ad284c6805..811a8a8a7d 100644 --- a/test/vitest.config.ts +++ b/test/vitest.config.ts @@ -2,13 +2,21 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { + environment: "jsdom", // to run in browser-like environment env: { TZ: "Etc/UTC", IS_TEST: "true", }, setupFiles: ["./test/setup.ts"], coverage: { - include: ["src/data/**/*", "src/common/**/*"], + include: [ + "src/data/**/*", + "src/common/**/*", + "src/external_app/**/*", + "src/hassio/**/*", + "src/panels/**/*", + "src/util/**/*", + ], reporter: ["text", "html"], provider: "v8", reportsDirectory: "test/coverage", diff --git a/yarn.lock b/yarn.lock index 46301df641..3de7c2b5b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6888,6 +6888,15 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^4.1.0": + version: 4.1.0 + resolution: "cssstyle@npm:4.1.0" + dependencies: + rrweb-cssom: "npm:^0.7.1" + checksum: 10/8ca9e2d1f1b24f93bb5f3f20a7a1e271e58060957880e985ee55614e196a798ffab309ec6bac105af8a439a6764546761813835ebb7f929d60823637ee838a8f + languageName: node + linkType: hard + "csstype@npm:^3.1.0": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -6895,6 +6904,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 10/5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46 + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.1": version: 1.0.1 resolution: "data-view-buffer@npm:1.0.1" @@ -6995,6 +7014,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 10/de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 + languageName: node + linkType: hard + "deep-clone-simple@npm:1.1.1": version: 1.1.1 resolution: "deep-clone-simple@npm:1.1.1" @@ -9240,6 +9266,7 @@ __metadata: idb-keyval: "npm:6.2.1" intl-messageformat: "npm:10.7.7" js-yaml: "npm:4.1.0" + jsdom: "npm:25.0.1" jszip: "npm:3.10.1" leaflet: "npm:1.9.4" leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" @@ -9328,6 +9355,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 10/e86efd493293a5671b8239bd099d42128433bb3c7b0fdc7819282ef8e118a21f5dead0ad6f358e024a4e5c84f17ebb7a9b36075220fac0a6222b207248bede6f + languageName: node + linkType: hard + "html-entities@npm:^2.4.0": version: 2.5.2 resolution: "html-entities@npm:2.5.2" @@ -9422,7 +9458,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -9461,7 +9497,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" dependencies: @@ -9510,7 +9546,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9949,7 +9985,7 @@ __metadata: languageName: node linkType: hard -"is-potential-custom-element-name@npm:^1.0.0": +"is-potential-custom-element-name@npm:^1.0.0, is-potential-custom-element-name@npm:^1.0.1": version: 1.0.1 resolution: "is-potential-custom-element-name@npm:1.0.1" checksum: 10/ced7bbbb6433a5b684af581872afe0e1767e2d1146b2207ca0068a648fb5cab9d898495d1ac0583524faaf24ca98176a7d9876363097c2d14fee6dd324f3a1ab @@ -10255,6 +10291,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:25.0.1": + version: 25.0.1 + resolution: "jsdom@npm:25.0.1" + dependencies: + cssstyle: "npm:^4.1.0" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.4.3" + form-data: "npm:^4.0.0" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.5" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.12" + parse5: "npm:^7.1.2" + rrweb-cssom: "npm:^0.7.1" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^5.0.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + ws: "npm:^8.18.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10/e6bf7250ddd2fbcf68da0ea041a0dc63545dc4bf77fa3ff40a46ae45b1dac1ca55b87574ab904d1f8baeeb547c52cec493a22f545d7d413b320011f41150ec49 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": version: 3.0.2 resolution: "jsesc@npm:3.0.2" @@ -11389,6 +11459,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.12": + version: 2.2.16 + resolution: "nwsapi@npm:2.2.16" + checksum: 10/1e5e086cdd4ca4a45f414d37f49bf0ca81d84ed31c6871ac68f531917d2910845db61f77c6d844430dc90fda202d43fce9603024e74038675de95229eb834dba + languageName: node + linkType: hard + "object-assign@npm:^4": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -12220,7 +12297,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:2.3.1, punycode@npm:^2.1.0": +"punycode@npm:2.3.1, punycode@npm:^2.1.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -12673,8 +12750,8 @@ __metadata: linkType: hard "rollup@npm:^2.43.1": - version: 2.79.2 - resolution: "rollup@npm:2.79.2" + version: 2.79.1 + resolution: "rollup@npm:2.79.1" dependencies: fsevents: "npm:~2.3.2" dependenciesMeta: @@ -12682,7 +12759,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/095ba0a82811b1866a76d826987743278db0a87c45092656986bfff490326b66187d5f9ff0c24cf8d5682bc470aa00c36654e0044d6b6335ac0c1201b8280880 + checksum: 10/df087b701304432f30922bbee5f534ab189aa6938bd383b5686c03147e0d00cd1789ea10a462361326ce6b6ebe448ce272ad3f3cc40b82eeb3157df12f33663c languageName: node linkType: hard @@ -12764,6 +12841,13 @@ __metadata: languageName: node linkType: hard +"rrweb-cssom@npm:^0.7.1": + version: 0.7.1 + resolution: "rrweb-cssom@npm:0.7.1" + checksum: 10/e80cf25c223a823921d7ab57c0ce78f5b7ebceab857b400cce99dd4913420ce679834bc5707e8ada47d062e21ad368108a9534c314dc8d72c20aa4a4fa0ed16a + languageName: node + linkType: hard + "rslog@npm:^1.2.3": version: 1.2.3 resolution: "rslog@npm:1.2.3" @@ -12852,6 +12936,15 @@ __metadata: languageName: node linkType: hard +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10/97b50daf6ca3a153e89842efa18a862e446248296622b7473c169c84c823ee8a16e4a43bac2f73f11fc8cb9168c73fbb0d73340f26552bac17970e9052367aa9 + languageName: node + linkType: hard + "schema-utils@npm:^3.1.1": version: 3.3.0 resolution: "schema-utils@npm:3.3.0" @@ -13721,6 +13814,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10/c09a00aadf279d47d0c5c46ca3b6b2fbaeb45f0a184976d599637d412d3a70bbdc043ff33effe1206dea0e36e0ad226cb957112e7ce9a4bf2daedf7fa4f85c53 + languageName: node + linkType: hard + "systemjs@npm:6.15.1": version: 6.15.1 resolution: "systemjs@npm:6.15.1" @@ -13966,6 +14066,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.65": + version: 6.1.65 + resolution: "tldts-core@npm:6.1.65" + checksum: 10/06ae49ae935674163d806aa14abf6bb49a6099878ef3940de035c6bf577cf31b78122df02d2e82d84d0a59faf8a837235d537ee7cbf9a5b0b97b44b7a5f19799 + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.65 + resolution: "tldts@npm:6.1.65" + dependencies: + tldts-core: "npm:^6.1.65" + bin: + tldts: bin/cli.js + checksum: 10/dc8afe3ac8af4f2dc4b4307d447b6877ee5942e4b8164ae170edf034a318241ca7f9bdf89d4560204363141344959aa4d28edc532397b210532e160acbf77b6a + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -14015,6 +14133,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^5.0.0": + version: 5.0.0 + resolution: "tough-cookie@npm:5.0.0" + dependencies: + tldts: "npm:^6.1.32" + checksum: 10/a98d3846ed386e399e8b470c1eb08a6a296944246eabc55c9fe79d629bd2cdaa62f5a6572f271fe0060987906bd20468d72a219a3b4cbe51086bea48d2d677b6 + languageName: node + linkType: hard + "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -14024,6 +14151,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^5.0.0": + version: 5.0.0 + resolution: "tr46@npm:5.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e + languageName: node + linkType: hard + "tree-dump@npm:^1.0.1": version: 1.0.2 resolution: "tree-dump@npm:1.0.2" @@ -14836,6 +14972,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10/d78f59e6b4f924aa53b6dfc56949959229cae7fe05ea9374eb38d11edcec01398b7f5d7a12576bd5acc57ff446abb5c9115cd83b9d882555015437cf858d42f0 + languageName: node + linkType: hard + "wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": version: 1.7.3 resolution: "wbuf@npm:1.7.3" @@ -14876,6 +15021,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 10/4c4f65472c010eddbe648c11b977d048dd96956a625f7f8b9d64e1b30c3c1f23ea1acfd654648426ce5c743c2108a5a757c0592f02902cf7367adb7d14e67721 + languageName: node + linkType: hard + "webpack-bundle-analyzer@npm:4.6.1": version: 4.6.1 resolution: "webpack-bundle-analyzer@npm:4.6.1" @@ -15035,6 +15187,32 @@ __metadata: languageName: node linkType: hard +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10/bbef815eb67f91487c7f2ef96329743f5fd8357d7d62b1119237d25d41c7e452dff8197235b2d3c031365a17f61d3bb73ca49d0ed1582475aa4a670815e79534 + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: 10/894a618e2d90bf444b6f309f3ceb6e58cf21b2beaa00c8b333696958c4076f0c7b30b9d33413c9ffff7c5832a0a0c8569e5bb347ef44beded72aeefd0acd62e8 + languageName: node + linkType: hard + +"whatwg-url@npm:^14.0.0": + version: 14.1.0 + resolution: "whatwg-url@npm:14.1.0" + dependencies: + tr46: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10/3afd325de6cf3a367820ce7c3566a1f78eb1409c4f27b1867c74c76dab096d26acedf49a8b9b71db53df7d806ec2e9ae9ed96990b2f7d1abe6ecf1fe753af6eb + languageName: node + linkType: hard + "whatwg-url@npm:^7.0.0": version: 7.1.0 resolution: "whatwg-url@npm:7.1.0" @@ -15494,7 +15672,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.16.0": +"ws@npm:^8.16.0, ws@npm:^8.18.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: @@ -15536,6 +15714,13 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10/43f30f3f6786e406dd665acf08cd742d5f8a46486bd72517edb04b27d1bcd1599664c2a4a99fc3f1e56a3194bff588b12f178b7972bc45c8047bdc4c3ac8d4a1 + languageName: node + linkType: hard + "xml-parse-from-string@npm:^1.0.0": version: 1.0.1 resolution: "xml-parse-from-string@npm:1.0.1" @@ -15560,6 +15745,13 @@ __metadata: languageName: node linkType: hard +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10/4ad5924974efd004a47cce6acf5c0269aee0e62f9a805a426db3337af7bcbd331099df174b024ace4fb18971b8a56de386d2e73a1c4b020e3abd63a4a9b917f1 + languageName: node + linkType: hard + "xss@npm:1.0.15": version: 1.0.15 resolution: "xss@npm:1.0.15"