mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-26 06:21:27 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69bae60ab2 | |||
| 85378b2179 |
@@ -52,6 +52,23 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
currentArtifact = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchTranslations(currentArtifact);
|
||||
} catch (err) {
|
||||
// Local builds should work offline or without valid GitHub credentials,
|
||||
// so fall back to English only. CI must fail instead of silently
|
||||
// building without translations.
|
||||
if (process.env.CI) {
|
||||
throw err;
|
||||
}
|
||||
console.warn(
|
||||
"Failed to fetch nightly translations, continuing with English only:",
|
||||
err.message
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchTranslations(currentArtifact) {
|
||||
// To store file writing promises
|
||||
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
|
||||
const writings = [];
|
||||
@@ -130,11 +147,6 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
if (!latestArtifact) {
|
||||
throw Error("Latest nightly workflow run has no translations artifact");
|
||||
}
|
||||
writings.push(
|
||||
createExtractDir.then(
|
||||
writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
|
||||
)
|
||||
);
|
||||
|
||||
// Remove the current translations
|
||||
const deleteCurrent = Promise.all(writings).then(
|
||||
@@ -160,7 +172,12 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
await new Promise((resolve, reject) => {
|
||||
extractStream.on("close", resolve).on("error", reject);
|
||||
});
|
||||
});
|
||||
|
||||
// Record the artifact only after successful extraction, so a failed fetch
|
||||
// is retried by the next build instead of being considered current.
|
||||
await createExtractDir;
|
||||
await writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2));
|
||||
}
|
||||
|
||||
gulp.task(
|
||||
"setup-and-fetch-nightly-translations",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import type { Lovelace } from "../../../src/panels/lovelace/types";
|
||||
import { setDemoAreas } from "../stubs/area_registry";
|
||||
import { energyEntities } from "../stubs/entities";
|
||||
import { setDemoFloors } from "../stubs/floor_registry";
|
||||
import { getDemoTheme } from "../stubs/frontend";
|
||||
import type { DemoConfig, DemoTheme } from "./types";
|
||||
|
||||
@@ -39,8 +37,6 @@ export const setDemoConfig = async (
|
||||
selectedDemoConfigIndex = index;
|
||||
selectedDemoConfig = confProm;
|
||||
|
||||
setDemoFloors(hass, config.floors);
|
||||
setDemoAreas(hass, config.areas);
|
||||
hass.addEntities(config.entities(hass.localize), true);
|
||||
hass.addEntities(energyEntities());
|
||||
lovelace.saveConfig(config.lovelace(hass.localize));
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import type { LovelaceConfig } from "../../../src/data/lovelace/config/types";
|
||||
import type { EntityInput } from "../../../src/fake_data/entities/types";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import type { DemoArea } from "../stubs/area_registry";
|
||||
import type { DemoFloor } from "../stubs/floor_registry";
|
||||
|
||||
export type DemoTheme = ThemeSettings | (() => Record<string, string> | null);
|
||||
|
||||
@@ -17,7 +15,5 @@ export interface DemoConfig {
|
||||
string | ((localize: LocalizeFunc) => string | TemplateResult<1>);
|
||||
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
|
||||
entities: (localize: LocalizeFunc) => EntityInput[];
|
||||
floors?: DemoFloor[];
|
||||
areas?: DemoArea[];
|
||||
theme: DemoTheme;
|
||||
}
|
||||
|
||||
@@ -41,30 +41,25 @@ class CastDemoRow extends LitElement implements LovelaceRow {
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
import("../../../src/cast/cast_manager").then(({ getCastManager }) =>
|
||||
getCastManager().then(
|
||||
(mgr) => {
|
||||
this._castManager = mgr;
|
||||
mgr.addEventListener("state-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
mgr.castContext.addEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(ev) => {
|
||||
// On Android, opening a new session always results in SESSION_RESUMED.
|
||||
// So treat both as the same.
|
||||
if (
|
||||
ev.sessionState === "SESSION_STARTED" ||
|
||||
ev.sessionState === "SESSION_RESUMED"
|
||||
) {
|
||||
castSendShowDemo(mgr);
|
||||
}
|
||||
getCastManager().then((mgr) => {
|
||||
this._castManager = mgr;
|
||||
mgr.addEventListener("state-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
mgr.castContext.addEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(ev) => {
|
||||
// On Android, opening a new session always results in SESSION_RESUMED.
|
||||
// So treat both as the same.
|
||||
if (
|
||||
ev.sessionState === "SESSION_STARTED" ||
|
||||
ev.sessionState === "SESSION_RESUMED"
|
||||
) {
|
||||
castSendShowDemo(mgr);
|
||||
}
|
||||
);
|
||||
},
|
||||
() => {
|
||||
this._castManager = null;
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
-15
@@ -6,7 +6,7 @@ import { provideHass } from "../../src/fake_data/provide_hass";
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import { applyDemoTheme, selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry, setDemoAreas } from "./stubs/area_registry";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { demoDevices } from "./stubs/devices";
|
||||
import { mockDeviceRegistry } from "./stubs/device_registry";
|
||||
@@ -14,7 +14,7 @@ import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry, setDemoFloors } from "./stubs/floor_registry";
|
||||
import { mockFloorRegistry } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockIntegration } from "./stubs/integration";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
@@ -29,7 +29,6 @@ import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
import { mockUsagePrediction } from "./stubs/usage_prediction";
|
||||
import "./cloud/cloud-demo-controls";
|
||||
|
||||
// WS command / REST path prefixes whose mocks live in the lazily imported
|
||||
@@ -75,17 +74,11 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
|
||||
// The cloud account page only fetches backup config and the webhook count
|
||||
// when those integrations are loaded. Enable them here (demo only) so the
|
||||
// mocked backup/config/info and webhook/list are queried. usage_prediction
|
||||
// is needed for common-controls sections in strategy dashboards.
|
||||
// mocked backup/config/info and webhook/list are queried.
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
components: [
|
||||
...(hass.config?.components ?? []),
|
||||
"backup",
|
||||
"webhook",
|
||||
"usage_prediction",
|
||||
],
|
||||
components: [...(hass.config?.components ?? []), "backup", "webhook"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -129,7 +122,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockDeviceRegistry(hass, demoDevices);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockUsagePrediction(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
@@ -177,11 +169,9 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
|
||||
hass.addEntities(energyEntities());
|
||||
|
||||
// Once config is loaded AND localize, set registries, entities and theme.
|
||||
// Once config is loaded AND localize, set entities and apply theme.
|
||||
Promise.all([selectedDemoConfig, localizePromise]).then(
|
||||
([conf, localize]) => {
|
||||
setDemoFloors(hass, conf.floors);
|
||||
setDemoAreas(hass, conf.areas);
|
||||
hass.addEntities(conf.entities(localize));
|
||||
applyDemoTheme(hass, conf.theme);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,14 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export interface DemoArea {
|
||||
area_id: string;
|
||||
name: string;
|
||||
floor_id?: string;
|
||||
icon?: string;
|
||||
temperature_entity_id?: string;
|
||||
humidity_entity_id?: string;
|
||||
}
|
||||
|
||||
let areas: AreaRegistryEntry[] = [];
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DemoArea[] = []
|
||||
data: AreaRegistryEntry[] = []
|
||||
) => {
|
||||
hass.mockWS("config/area_registry/list", () => areas);
|
||||
setDemoAreas(hass, data);
|
||||
};
|
||||
|
||||
/** Set the areas of the currently loaded demo config. */
|
||||
export const setDemoAreas = (
|
||||
hass: MockHomeAssistant,
|
||||
demoAreas: DemoArea[] = []
|
||||
) => {
|
||||
areas = demoAreas.map((area) => ({
|
||||
area_id: area.area_id,
|
||||
name: area.name,
|
||||
floor_id: area.floor_id ?? null,
|
||||
icon: area.icon ?? null,
|
||||
temperature_entity_id: area.temperature_entity_id ?? null,
|
||||
humidity_entity_id: area.humidity_entity_id ?? null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
picture: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
}));
|
||||
const areasById: Record<string, AreaRegistryEntry> = {};
|
||||
areas.forEach((area) => {
|
||||
areasById[area.area_id] = area;
|
||||
hass.mockWS("config/area_registry/list", () => data);
|
||||
const areas = {};
|
||||
data.forEach((area) => {
|
||||
areas[area.area_id] = area;
|
||||
});
|
||||
hass.updateHass({ areas: areasById });
|
||||
hass.updateHass({ areas });
|
||||
};
|
||||
|
||||
@@ -1,40 +1,7 @@
|
||||
import type { FloorRegistryEntry } from "../../../src/data/floor_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export interface DemoFloor {
|
||||
floor_id: string;
|
||||
name: string;
|
||||
level?: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
let floors: FloorRegistryEntry[] = [];
|
||||
|
||||
export const mockFloorRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DemoFloor[] = []
|
||||
) => {
|
||||
hass.mockWS("config/floor_registry/list", () => floors);
|
||||
setDemoFloors(hass, data);
|
||||
};
|
||||
|
||||
/** Set the floors of the currently loaded demo config. */
|
||||
export const setDemoFloors = (
|
||||
hass: MockHomeAssistant,
|
||||
demoFloors: DemoFloor[] = []
|
||||
) => {
|
||||
floors = demoFloors.map((floor) => ({
|
||||
floor_id: floor.floor_id,
|
||||
name: floor.name,
|
||||
level: floor.level ?? null,
|
||||
icon: floor.icon ?? null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
}));
|
||||
const floorsById: Record<string, FloorRegistryEntry> = {};
|
||||
floors.forEach((floor) => {
|
||||
floorsById[floor.floor_id] = floor;
|
||||
});
|
||||
hass.updateHass({ floors: floorsById });
|
||||
};
|
||||
data: FloorRegistryEntry[] = []
|
||||
) => hass.mockWS("config/floor_registry/list", () => data);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { CommonControlsResult } from "../../../src/data/usage_prediction";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockUsagePrediction = (hass: MockHomeAssistant) => {
|
||||
// Entities that don't exist in the currently loaded demo config are
|
||||
// filtered out by the common-controls section strategy.
|
||||
hass.mockWS("usage_prediction/common_control", (): CommonControlsResult => ({
|
||||
entities: [
|
||||
"light.living_room_floor_lamp",
|
||||
"climate.living_room",
|
||||
"cover.living_room_blinds",
|
||||
"media_player.living_room_speaker",
|
||||
"light.kitchen_spotlights",
|
||||
"switch.coffee_machine",
|
||||
"light.bedside_lamp",
|
||||
"alarm_control_panel.home_alarm",
|
||||
],
|
||||
}));
|
||||
};
|
||||
@@ -2,10 +2,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
mockAreaRegistry,
|
||||
type DemoArea,
|
||||
} from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
@@ -13,6 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
@@ -138,20 +136,45 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS: DemoArea[] = [
|
||||
const AREAS: AreaRegistryEntry[] = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
floor_id: null,
|
||||
name: "Backyard",
|
||||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
floor_id: null,
|
||||
name: "Bedroom",
|
||||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
floor_id: null,
|
||||
name: "Livingroom",
|
||||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
mockAreaRegistry,
|
||||
type DemoArea,
|
||||
} from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import {
|
||||
mockFloorRegistry,
|
||||
type DemoFloor,
|
||||
} from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
import {
|
||||
showDialog,
|
||||
@@ -151,43 +147,75 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS: DemoArea[] = [
|
||||
const AREAS: AreaRegistryEntry[] = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
floor_id: "ground",
|
||||
name: "Backyard",
|
||||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
floor_id: "first",
|
||||
name: "Bedroom",
|
||||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
floor_id: "ground",
|
||||
name: "Livingroom",
|
||||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
temperature_entity_id: null,
|
||||
humidity_entity_id: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const FLOORS: DemoFloor[] = [
|
||||
const FLOORS: FloorRegistryEntry[] = [
|
||||
{
|
||||
floor_id: "ground",
|
||||
name: "Ground floor",
|
||||
level: 0,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
floor_id: "first",
|
||||
name: "First floor",
|
||||
level: 1,
|
||||
icon: "mdi:numeric-1",
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
floor_id: "second",
|
||||
name: "Second floor",
|
||||
level: 2,
|
||||
icon: "mdi:numeric-2",
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+6
-5
@@ -108,7 +108,7 @@
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.12",
|
||||
"js-yaml": "5.2.2",
|
||||
"js-yaml": "5.2.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -152,7 +152,7 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.1",
|
||||
"@rspack/core": "2.1.5",
|
||||
"@rspack/core": "2.1.4",
|
||||
"@rspack/dev-server": "2.1.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -196,7 +196,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.1.1",
|
||||
"lint-staged": "17.1.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -207,7 +207,7 @@
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.1.0",
|
||||
"tar": "7.5.21",
|
||||
"tar": "7.5.20",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
@@ -226,7 +226,8 @@
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.7.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.17.1",
|
||||
"volta": {
|
||||
|
||||
+10
-11
@@ -9,17 +9,16 @@ export const castApiAvailable = () => {
|
||||
|
||||
loadedPromise = new Promise((resolve) => {
|
||||
(window as any).__onGCastApiAvailable = resolve;
|
||||
|
||||
// Any element with a specific ID will get set as a JS variable on window
|
||||
// This will override the cast SDK if the iconset is loaded afterwards.
|
||||
// Conflicting IDs will no longer mess with window, so we'll just append one.
|
||||
const el = document.createElement("div");
|
||||
el.id = "cast";
|
||||
document.body.append(el);
|
||||
|
||||
loadJS(
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||
).catch(() => resolve(false));
|
||||
});
|
||||
// Any element with a specific ID will get set as a JS variable on window
|
||||
// This will override the cast SDK if the iconset is loaded afterwards.
|
||||
// Conflicting IDs will no longer mess with window, so we'll just append one.
|
||||
const el = document.createElement("div");
|
||||
el.id = "cast";
|
||||
document.body.append(el);
|
||||
|
||||
loadJS(
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"
|
||||
);
|
||||
return loadedPromise;
|
||||
};
|
||||
|
||||
@@ -24,8 +24,6 @@ export interface ToastClosedEventDetail {
|
||||
export class HaToast extends LitElement {
|
||||
@property({ attribute: "label-text" }) public labelText = "";
|
||||
|
||||
@property({ attribute: "announce-text" }) public announceText?: string;
|
||||
|
||||
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
|
||||
|
||||
@property({ type: Number, attribute: "bottom-offset" }) public bottomOffset =
|
||||
@@ -192,6 +190,8 @@ export class HaToast extends LitElement {
|
||||
style=${styleMap({
|
||||
"--ha-toast-bottom-offset": `${this.bottomOffset}px`,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<span class="message">${this.labelText}</span>
|
||||
@@ -200,14 +200,6 @@ export class HaToast extends LitElement {
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="assistive-message"
|
||||
role="status"
|
||||
aria-live=${this._active ? "polite" : "off"}
|
||||
aria-atomic="true"
|
||||
>
|
||||
${this.announceText ?? this.labelText}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -262,18 +254,6 @@ export class HaToast extends LitElement {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.assistive-message {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,13 +36,3 @@ export interface DirtyStateContext<
|
||||
* boundary.
|
||||
*/
|
||||
export const dirtyStateContext = createContext<DirtyStateContext>("dirtyState");
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isDirtyState?: boolean;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"dirty-state-changed": { isDirty: boolean };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ export class HaImagecropperDialog
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (!changedProperties.has("_params") || !this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,38 +16,6 @@ export const demoPanels: Panels = {
|
||||
config: null,
|
||||
url_path: "home",
|
||||
},
|
||||
light: {
|
||||
component_name: "light",
|
||||
icon: "mdi:lamps",
|
||||
title: "light",
|
||||
default_visible: false,
|
||||
config: null,
|
||||
url_path: "light",
|
||||
},
|
||||
climate: {
|
||||
component_name: "climate",
|
||||
icon: "mdi:home-thermometer",
|
||||
title: "climate",
|
||||
default_visible: false,
|
||||
config: null,
|
||||
url_path: "climate",
|
||||
},
|
||||
security: {
|
||||
component_name: "security",
|
||||
icon: "mdi:security",
|
||||
title: "security",
|
||||
default_visible: false,
|
||||
config: null,
|
||||
url_path: "security",
|
||||
},
|
||||
maintenance: {
|
||||
component_name: "maintenance",
|
||||
icon: "mdi:wrench",
|
||||
title: "maintenance",
|
||||
default_visible: false,
|
||||
config: null,
|
||||
url_path: "maintenance",
|
||||
},
|
||||
"dev-state": {
|
||||
component_name: "dev-state",
|
||||
icon: null,
|
||||
|
||||
@@ -27,8 +27,6 @@ export class MockBaseEntity {
|
||||
|
||||
public state: string;
|
||||
|
||||
public areaId?: string;
|
||||
|
||||
public baseAttributes: EntityAttributes;
|
||||
|
||||
public attributes: EntityAttributes;
|
||||
@@ -49,7 +47,6 @@ export class MockBaseEntity {
|
||||
this.domain = domain;
|
||||
this.objectId = objectId;
|
||||
this.state = input.state;
|
||||
this.areaId = input.area_id;
|
||||
this.lastChanged = randomTime();
|
||||
this.lastUpdated = randomTime();
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ export type EntityAttributes = HassEntityAttributeBase & Record<string, any>;
|
||||
export type EntityInput = Pick<
|
||||
HassEntity,
|
||||
"entity_id" | "state" | "attributes"
|
||||
> & {
|
||||
/** Area the entity is assigned to in the mocked entity registry */
|
||||
area_id?: string;
|
||||
};
|
||||
>;
|
||||
|
||||
/**
|
||||
* The hass mock object interface, kept intentionally loose
|
||||
|
||||
@@ -299,7 +299,6 @@ export const provideHass = (
|
||||
icon: undefined,
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
area_id: ent.areaId,
|
||||
} satisfies EntityRegistryDisplayEntry;
|
||||
});
|
||||
if (replace) {
|
||||
|
||||
@@ -13,13 +13,8 @@ export interface ShowToastParams {
|
||||
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
|
||||
id?: string;
|
||||
message:
|
||||
| string
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
|
||||
announceMessage?:
|
||||
| string
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
|
||||
string | { translationKey: LocalizeKeys; args?: Record<string, string> };
|
||||
action?: ToastActionParams;
|
||||
secondaryAction?: ToastActionParams;
|
||||
dismiss?: () => void;
|
||||
duration?: number;
|
||||
dismissable?: boolean;
|
||||
@@ -28,10 +23,8 @@ export interface ShowToastParams {
|
||||
|
||||
export interface ToastActionParams {
|
||||
action: () => void;
|
||||
primary?: boolean;
|
||||
text:
|
||||
| string
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
|
||||
string | { translationKey: LocalizeKeys; args?: Record<string, string> };
|
||||
}
|
||||
|
||||
@customElement("notification-manager")
|
||||
@@ -100,22 +93,31 @@ class NotificationManager extends LitElement {
|
||||
)
|
||||
: this._parameters.message
|
||||
}
|
||||
.announceText=${
|
||||
this._parameters.announceMessage
|
||||
? typeof this._parameters.announceMessage !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters.announceMessage.translationKey,
|
||||
this._parameters.announceMessage.args
|
||||
)
|
||||
: this._parameters.announceMessage
|
||||
: undefined
|
||||
}
|
||||
.timeoutMs=${this._parameters.duration!}
|
||||
.bottomOffset=${this._parameters.bottomOffset ?? 0}
|
||||
@toast-closed=${this._toastClosed}
|
||||
>
|
||||
${this._renderAction(this._parameters.secondaryAction, true)}
|
||||
${this._renderAction(this._parameters.action, false)}
|
||||
${
|
||||
this._parameters?.action
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
slot="action"
|
||||
@click=${this._buttonClicked}
|
||||
>
|
||||
${
|
||||
typeof this._parameters?.action.text !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters?.action.text.translationKey,
|
||||
this._parameters?.action.text.args
|
||||
)
|
||||
: this._parameters?.action.text
|
||||
}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._parameters?.dismissable
|
||||
? html`
|
||||
@@ -132,37 +134,11 @@ class NotificationManager extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAction(
|
||||
action: ToastActionParams | undefined,
|
||||
secondary: boolean
|
||||
) {
|
||||
if (!action) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-button
|
||||
appearance=${action.primary ? "filled" : "plain"}
|
||||
size="s"
|
||||
slot="action"
|
||||
@click=${secondary ? this._secondaryButtonClicked : this._buttonClicked}
|
||||
>
|
||||
${
|
||||
typeof action.text !== "string"
|
||||
? this.hass.localize(action.text.translationKey, action.text.args)
|
||||
: action.text
|
||||
}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _buttonClicked() {
|
||||
this._toast?.hide("action");
|
||||
this._parameters?.action?.action();
|
||||
}
|
||||
|
||||
private _secondaryButtonClicked() {
|
||||
this._toast?.hide("action");
|
||||
this._parameters?.secondaryAction?.action();
|
||||
if (this._parameters?.action) {
|
||||
this._parameters?.action.action();
|
||||
}
|
||||
}
|
||||
|
||||
private _dismissClicked() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { provide } from "@lit/context";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { LitElement, PropertyValues } from "lit";
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { shallowEqual } from "../common/util/shallow-equal";
|
||||
import {
|
||||
@@ -18,18 +17,6 @@ export type CompareStrategy<State> =
|
||||
| { type: "shallow" }
|
||||
| { type: "custom"; compare: (a: State, b: State) => boolean };
|
||||
|
||||
const connectedDirtyStateProviders = new Map<object, boolean>();
|
||||
|
||||
const publishGlobalDirtyState = (): void => {
|
||||
const isDirty = Array.from(connectedDirtyStateProviders.values()).some(
|
||||
Boolean
|
||||
);
|
||||
if (isDirty !== window.isDirtyState) {
|
||||
window.isDirtyState = isDirty;
|
||||
fireEvent(window, "dirty-state-changed", { isDirty });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mixin that provides dirty-state tracking via Lit context.
|
||||
*
|
||||
@@ -39,9 +26,6 @@ const publishGlobalDirtyState = (): void => {
|
||||
* so independent contributors (e.g. a helper form alongside the entity
|
||||
* registry editor) can coexist without overwriting each other.
|
||||
*
|
||||
* Connected providers contribute to the global dirty state. It remains dirty
|
||||
* while any provider has unsaved changes.
|
||||
*
|
||||
* `isEffectiveDirty` runs the same comparison, but first passes each slice's
|
||||
* initial and current value through the optional `effectiveNormalize` function
|
||||
* given to `_initDirtyTracking`. Provide a normalizer that collapses values you
|
||||
@@ -134,24 +118,6 @@ export const DirtyStateProviderMixin =
|
||||
this._dirtyStateContext = this._buildContextValue();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
connectedDirtyStateProviders.set(this, this.isDirtyState);
|
||||
publishGlobalDirtyState();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
connectedDirtyStateProviders.set(this, this.isDirtyState);
|
||||
publishGlobalDirtyState();
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
connectedDirtyStateProviders.delete(this);
|
||||
publishGlobalDirtyState();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _writeSlice(key: Key | DefaultDirtyStateKey, value: State): void {
|
||||
const slice = this._dirtySlices.get(key);
|
||||
if (!slice) {
|
||||
|
||||
@@ -203,7 +203,6 @@ class DialogBackupOnboarding
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_step") && this._step === "key") {
|
||||
this._save();
|
||||
}
|
||||
|
||||
@@ -145,10 +145,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Re-apply the type filter from the URL when the page is (re)displayed,
|
||||
// e.g. when navigating back to a cached instance of this page with a
|
||||
// different `type` query param.
|
||||
this._setFiltersFromUrl();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,6 @@ export class HuiDialogEditBadge
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!changedProps.has("_badgeConfig")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,6 @@ export class HuiDialogEditCard
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._cardConfig || !changedProps.has("_cardConfig")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ export class HuiDialogEditSection
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const sectionConfig = {
|
||||
...this._config,
|
||||
|
||||
@@ -94,7 +94,6 @@ export class HuiDialogEditView extends DirtyStateProviderMixin<LovelaceViewConfi
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const viewConfig = {
|
||||
...this._config,
|
||||
|
||||
@@ -43,7 +43,6 @@ export class HuiDialogEditViewFooter extends DirtyStateProviderMixin<LovelaceVie
|
||||
@state() private _open = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const config = {
|
||||
...this._config,
|
||||
|
||||
@@ -44,7 +44,6 @@ export class HuiDialogEditViewHeader extends DirtyStateProviderMixin<LovelaceVie
|
||||
@state() private _open = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const config = {
|
||||
...this._config,
|
||||
|
||||
@@ -107,7 +107,7 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
.value=${Number(stateObj.state).toString()}
|
||||
.value=${stateObj.state}
|
||||
type="number"
|
||||
@change=${this._selectedValueChanged}
|
||||
>
|
||||
|
||||
@@ -104,7 +104,6 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
const oldLovelace = changedProps.get("lovelace") as Lovelace | undefined;
|
||||
if (
|
||||
!this._saving &&
|
||||
|
||||
@@ -2510,8 +2510,8 @@
|
||||
"triggered": "Triggered {name}",
|
||||
"dismiss": "Dismiss",
|
||||
"no_matching_link_found": "No matching My link found for {path}",
|
||||
"new_version_available": "A new version of the frontend is available. This page will update in {seconds, plural, one {# second} other {# seconds}}.",
|
||||
"update_now": "Update now",
|
||||
"new_version_available": "A new version of the frontend is available.",
|
||||
"reload": "Reload",
|
||||
"theme_save_failed": "Unable to save theme settings to your user profile.",
|
||||
"theme_preferences_unavailable": "Unable to load user profile theme settings.",
|
||||
"onboarding_survey": {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { showToast } from "./toast";
|
||||
|
||||
const UPDATE_DELAY = 60_000;
|
||||
const UPDATE_TOAST_ID = "frontend-update-available";
|
||||
|
||||
export const supportsServiceWorker = () =>
|
||||
"serviceWorker" in navigator &&
|
||||
(location.protocol === "https:" || location.hostname === "localhost");
|
||||
@@ -26,92 +23,6 @@ export const registerServiceWorker = async (
|
||||
return;
|
||||
}
|
||||
|
||||
let pendingWorker: ServiceWorker | undefined;
|
||||
let updateDeadline = 0;
|
||||
let updateInterval = 0;
|
||||
|
||||
const hideUpdateToast = () => {
|
||||
showToast(rootEl, {
|
||||
id: UPDATE_TOAST_ID,
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const clearUpdateCountdown = () => {
|
||||
clearInterval(updateInterval);
|
||||
};
|
||||
|
||||
const activateUpdate = () => {
|
||||
if (!pendingWorker) {
|
||||
return;
|
||||
}
|
||||
clearUpdateCountdown();
|
||||
hideUpdateToast();
|
||||
pendingWorker.postMessage({ type: "skipWaiting" });
|
||||
pendingWorker = undefined;
|
||||
};
|
||||
|
||||
const showUpdateToast = () => {
|
||||
const seconds = Math.ceil((updateDeadline - Date.now()) / 1000);
|
||||
if (seconds <= 0) {
|
||||
activateUpdate();
|
||||
return;
|
||||
}
|
||||
const announceSeconds =
|
||||
seconds > 40 ? 60 : seconds > 20 ? 40 : seconds > 5 ? 20 : 5;
|
||||
showToast(rootEl, {
|
||||
id: UPDATE_TOAST_ID,
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds },
|
||||
},
|
||||
announceMessage: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: announceSeconds },
|
||||
},
|
||||
action: {
|
||||
action: activateUpdate,
|
||||
primary: true,
|
||||
text: { translationKey: "ui.notification_toast.update_now" },
|
||||
},
|
||||
secondaryAction: {
|
||||
action: () => {
|
||||
pendingWorker = undefined;
|
||||
clearUpdateCountdown();
|
||||
},
|
||||
text: { translationKey: "ui.common.cancel" },
|
||||
},
|
||||
duration: -1,
|
||||
});
|
||||
};
|
||||
|
||||
const startUpdateCountdown = () => {
|
||||
updateDeadline = Date.now() + UPDATE_DELAY;
|
||||
showUpdateToast();
|
||||
updateInterval = window.setInterval(showUpdateToast, 1000);
|
||||
};
|
||||
|
||||
window.addEventListener("dirty-state-changed", () => {
|
||||
if (!pendingWorker) {
|
||||
return;
|
||||
}
|
||||
if (window.isDirtyState) {
|
||||
clearUpdateCountdown();
|
||||
hideUpdateToast();
|
||||
return;
|
||||
}
|
||||
startUpdateCountdown();
|
||||
});
|
||||
|
||||
const updateReady = (worker: ServiceWorker) => {
|
||||
clearUpdateCountdown();
|
||||
pendingWorker = worker;
|
||||
if (!window.isDirtyState) {
|
||||
startUpdateCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
reg.addEventListener("updatefound", () => {
|
||||
const installingWorker = reg.installing;
|
||||
|
||||
@@ -126,11 +37,22 @@ export const registerServiceWorker = async (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
updateReady(installingWorker);
|
||||
|
||||
// Notify users a new frontend is available.
|
||||
showToast(rootEl, {
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
},
|
||||
action: {
|
||||
// We tell the service worker to call skipWaiting, which activates
|
||||
// the new service worker. Above we listen for `controllerchange`
|
||||
// so we reload the page once a new service worker activates.
|
||||
action: () => installingWorker.postMessage({ type: "skipWaiting" }),
|
||||
text: { translationKey: "ui.notification_toast.reload" },
|
||||
},
|
||||
duration: -1,
|
||||
dismissable: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (reg.waiting && navigator.serviceWorker.controller) {
|
||||
updateReady(reg.waiting);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HaToast } from "../../src/components/ha-toast";
|
||||
import "../../src/components/ha-toast";
|
||||
|
||||
let toast: HaToast | undefined;
|
||||
|
||||
const mountToast = async (properties: Partial<HaToast> = {}) => {
|
||||
toast = document.createElement("ha-toast");
|
||||
Object.assign(toast, properties);
|
||||
document.body.append(toast);
|
||||
await toast.updateComplete;
|
||||
return toast;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
toast?.remove();
|
||||
toast = undefined;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const showToast = async (element: HaToast) => {
|
||||
vi.useFakeTimers();
|
||||
const shown = element.show();
|
||||
await element.updateComplete;
|
||||
Object.defineProperty(
|
||||
element.shadowRoot!.querySelector(".toast"),
|
||||
"getAnimations",
|
||||
{
|
||||
configurable: true,
|
||||
value: () => [],
|
||||
}
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
await shown;
|
||||
};
|
||||
|
||||
describe("ha-toast", () => {
|
||||
it("renders its message and bottom offset", async () => {
|
||||
const element = await mountToast({
|
||||
labelText: "Configuration saved",
|
||||
bottomOffset: 24,
|
||||
});
|
||||
|
||||
expect(element.shadowRoot?.querySelector(".message")?.textContent).toBe(
|
||||
"Configuration saved"
|
||||
);
|
||||
expect(
|
||||
(
|
||||
element.shadowRoot?.querySelector(".toast") as HTMLElement
|
||||
).style.getPropertyValue("--ha-toast-bottom-offset")
|
||||
).toBe("24px");
|
||||
});
|
||||
|
||||
it("renders assigned action and dismiss content", async () => {
|
||||
toast = document.createElement("ha-toast");
|
||||
const action = document.createElement("button");
|
||||
action.slot = "action";
|
||||
const dismiss = document.createElement("button");
|
||||
dismiss.slot = "dismiss";
|
||||
toast.append(action, dismiss);
|
||||
document.body.append(toast);
|
||||
await toast.updateComplete;
|
||||
toast.requestUpdate();
|
||||
await toast.updateComplete;
|
||||
|
||||
expect(
|
||||
toast.shadowRoot
|
||||
?.querySelector<HTMLSlotElement>('slot[name="action"]')
|
||||
?.assignedElements()
|
||||
).toEqual([action]);
|
||||
expect(
|
||||
toast.shadowRoot
|
||||
?.querySelector<HTMLSlotElement>('slot[name="dismiss"]')
|
||||
?.assignedElements()
|
||||
).toEqual([dismiss]);
|
||||
expect(
|
||||
toast.shadowRoot
|
||||
?.querySelector(".actions")
|
||||
?.classList.contains("has-action")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps changing visual text separate from live-region text", async () => {
|
||||
const element = await mountToast({
|
||||
labelText: "Updating in 59 seconds",
|
||||
announceText: "Updating in 60 seconds",
|
||||
});
|
||||
|
||||
const visibleMessage = element.shadowRoot!.querySelector(".message")!;
|
||||
const liveRegion = element.shadowRoot!.querySelector(".assistive-message")!;
|
||||
expect(visibleMessage.textContent).toBe("Updating in 59 seconds");
|
||||
expect(visibleMessage.closest('[role="status"]')).toBeNull();
|
||||
expect(liveRegion.textContent?.trim()).toBe("Updating in 60 seconds");
|
||||
expect(liveRegion.getAttribute("role")).toBe("status");
|
||||
expect(liveRegion.getAttribute("aria-atomic")).toBe("true");
|
||||
});
|
||||
|
||||
it("falls back to announcing the visible message", async () => {
|
||||
const element = await mountToast({ labelText: "Configuration saved" });
|
||||
|
||||
expect(
|
||||
element
|
||||
.shadowRoot!.querySelector(".assistive-message")!
|
||||
.textContent?.trim()
|
||||
).toBe("Configuration saved");
|
||||
});
|
||||
|
||||
it("activates its live region while shown", async () => {
|
||||
const element = await mountToast();
|
||||
const liveRegion = element.shadowRoot!.querySelector(".assistive-message")!;
|
||||
expect(liveRegion.getAttribute("aria-live")).toBe("off");
|
||||
|
||||
await showToast(element);
|
||||
|
||||
expect(liveRegion.getAttribute("aria-live")).toBe("polite");
|
||||
expect(
|
||||
element.shadowRoot!.querySelector(".toast")!.classList.contains("visible")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["action", "dismiss", "programmatic"] as const)(
|
||||
"reports a %s close reason",
|
||||
async (reason) => {
|
||||
const element = await mountToast({ timeoutMs: -1 });
|
||||
const listener = vi.fn();
|
||||
element.addEventListener("toast-closed", listener);
|
||||
await showToast(element);
|
||||
|
||||
await element.hide(reason);
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener.mock.calls[0][0].detail).toEqual({ reason });
|
||||
expect(
|
||||
element
|
||||
.shadowRoot!.querySelector(".toast")!
|
||||
.classList.contains("active")
|
||||
).toBe(false);
|
||||
expect(
|
||||
element
|
||||
.shadowRoot!.querySelector(".assistive-message")!
|
||||
.getAttribute("aria-live")
|
||||
).toBe("off");
|
||||
}
|
||||
);
|
||||
|
||||
it("closes with a timeout reason", async () => {
|
||||
const element = await mountToast({ timeoutMs: 1000 });
|
||||
const listener = vi.fn();
|
||||
element.addEventListener("toast-closed", listener);
|
||||
await showToast(element);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(listener.mock.calls[0][0].detail).toEqual({ reason: "timeout" });
|
||||
});
|
||||
|
||||
it("does not close automatically when the timeout is disabled", async () => {
|
||||
const element = await mountToast({ timeoutMs: -1 });
|
||||
const listener = vi.fn();
|
||||
element.addEventListener("toast-closed", listener);
|
||||
await showToast(element);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit a close event when already hidden", async () => {
|
||||
const element = await mountToast();
|
||||
const listener = vi.fn();
|
||||
element.addEventListener("toast-closed", listener);
|
||||
|
||||
await element.hide("dismiss");
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,119 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import "../../src/managers/notification-manager";
|
||||
|
||||
vi.mock("../../src/components/ha-button", () => {
|
||||
customElements.define("ha-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
const localize = (key: string, args?: Record<string, string | number>) =>
|
||||
args ? `${key}:${JSON.stringify(args)}` : key;
|
||||
|
||||
@customElement("test-notification-lifecycle-host")
|
||||
class TestNotificationLifecycleHost extends LitElement {
|
||||
@property({ attribute: false }) public hass = {
|
||||
localize,
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
protected render() {
|
||||
return html`<notification-manager
|
||||
.hass=${this.hass}
|
||||
></notification-manager>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-notification-lifecycle-host": TestNotificationLifecycleHost;
|
||||
}
|
||||
}
|
||||
|
||||
let host: TestNotificationLifecycleHost | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
host?.remove();
|
||||
host = undefined;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("notification toast lifecycle", () => {
|
||||
it("updates a visible toast and closes it after its primary action", async () => {
|
||||
vi.useFakeTimers();
|
||||
host = document.createElement("test-notification-lifecycle-host");
|
||||
document.body.append(host);
|
||||
await host.updateComplete;
|
||||
const manager = host.shadowRoot!.querySelector("notification-manager")!;
|
||||
const action = vi.fn();
|
||||
|
||||
const firstShow = manager.showDialog({
|
||||
id: "frontend-update-available",
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
announceMessage: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
action: { action, primary: true, text: "Update now" },
|
||||
duration: -1,
|
||||
});
|
||||
await Promise.resolve();
|
||||
await manager.updateComplete;
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
Object.defineProperty(
|
||||
toast.shadowRoot!.querySelector(".toast"),
|
||||
"getAnimations",
|
||||
{
|
||||
configurable: true,
|
||||
value: () => [],
|
||||
}
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
await firstShow;
|
||||
|
||||
expect(toast.shadowRoot!.querySelector(".toast")!.classList).toContain(
|
||||
"visible"
|
||||
);
|
||||
expect(toast.shadowRoot!.querySelector(".message")!.textContent).toContain(
|
||||
'"seconds":60'
|
||||
);
|
||||
expect(
|
||||
toast.shadowRoot!.querySelector(".assistive-message")!.textContent
|
||||
).toContain('"seconds":60');
|
||||
|
||||
await manager.showDialog({
|
||||
id: "frontend-update-available",
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 59 },
|
||||
},
|
||||
announceMessage: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
action: { action, primary: true, text: "Update now" },
|
||||
duration: -1,
|
||||
});
|
||||
|
||||
expect(toast.shadowRoot!.querySelector(".message")!.textContent).toContain(
|
||||
'"seconds":59'
|
||||
);
|
||||
expect(
|
||||
toast.shadowRoot!.querySelector(".assistive-message")!.textContent
|
||||
).toContain('"seconds":60');
|
||||
|
||||
const closed = new Promise<void>((resolve) => {
|
||||
toast.addEventListener("toast-closed", () => resolve(), { once: true });
|
||||
});
|
||||
manager.shadowRoot!.querySelector<HTMLElement>("ha-button")!.click();
|
||||
await closed;
|
||||
await manager.updateComplete;
|
||||
|
||||
expect(action).toHaveBeenCalledOnce();
|
||||
expect(manager.shadowRoot!.querySelector("ha-toast")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,245 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import "../../src/managers/notification-manager";
|
||||
|
||||
const localize = vi.fn((key: string, args?: Record<string, string | number>) =>
|
||||
args ? `${key}:${JSON.stringify(args)}` : key
|
||||
);
|
||||
|
||||
vi.mock("../../src/components/ha-button", () => {
|
||||
customElements.define("ha-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../src/components/ha-icon-button", () => {
|
||||
customElements.define("ha-icon-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../src/components/ha-toast", () => {
|
||||
customElements.define(
|
||||
"ha-toast",
|
||||
class extends HTMLElement {
|
||||
public show = vi.fn();
|
||||
|
||||
public hide = vi.fn();
|
||||
}
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
@customElement("test-notification-manager-host")
|
||||
class TestNotificationManagerHost extends LitElement {
|
||||
@property({ attribute: false }) public hass = {
|
||||
localize,
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
protected render() {
|
||||
return html`<notification-manager
|
||||
.hass=${this.hass}
|
||||
></notification-manager>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-notification-manager-host": TestNotificationManagerHost;
|
||||
}
|
||||
}
|
||||
|
||||
let host: TestNotificationManagerHost | undefined;
|
||||
|
||||
const mountManager = async () => {
|
||||
host = document.createElement(
|
||||
"test-notification-manager-host"
|
||||
) as TestNotificationManagerHost;
|
||||
document.body.append(host);
|
||||
await host.updateComplete;
|
||||
return host.shadowRoot!.querySelector("notification-manager")!;
|
||||
};
|
||||
|
||||
const deferred = () => {
|
||||
let resolve: () => void;
|
||||
const promise = new Promise<void>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
return { promise, resolve: resolve! };
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
host?.remove();
|
||||
host = undefined;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("notification-manager", () => {
|
||||
it("shows a message with the default duration", async () => {
|
||||
const manager = await mountManager();
|
||||
|
||||
await manager.showDialog({ message: "Configuration saved" });
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
expect(toast.labelText).toBe("Configuration saved");
|
||||
expect(toast.timeoutMs).toBe(4000);
|
||||
expect(toast.bottomOffset).toBe(0);
|
||||
expect(toast.show).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("renders a dismiss button and closes with the dismiss reason", async () => {
|
||||
const manager = await mountManager();
|
||||
|
||||
await manager.showDialog({
|
||||
message: "Connection lost",
|
||||
dismissable: true,
|
||||
duration: -1,
|
||||
bottomOffset: 16,
|
||||
});
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
expect(toast.timeoutMs).toBe(-1);
|
||||
expect(toast.bottomOffset).toBe(16);
|
||||
|
||||
manager.shadowRoot!.querySelector<HTMLElement>("ha-icon-button")!.click();
|
||||
expect(toast.hide).toHaveBeenCalledWith("dismiss");
|
||||
});
|
||||
|
||||
it("clears the current notification when duration is zero", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ message: "First message", duration: -1 });
|
||||
|
||||
await manager.showDialog({ message: "", duration: 0 });
|
||||
await manager.updateComplete;
|
||||
|
||||
expect(manager.shadowRoot!.querySelector("ha-toast")).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ duration: 1, expected: 4000 },
|
||||
{ duration: 4000, expected: 4000 },
|
||||
{ duration: 4001, expected: 4001 },
|
||||
{ duration: -1, expected: -1 },
|
||||
])(
|
||||
"normalizes a $duration ms duration to $expected ms",
|
||||
async ({ duration, expected }) => {
|
||||
const manager = await mountManager();
|
||||
|
||||
await manager.showDialog({ message: "Message", duration });
|
||||
|
||||
expect(manager.shadowRoot!.querySelector("ha-toast")!.timeoutMs).toBe(
|
||||
expected
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it("localizes visible and assistive messages with numeric arguments", async () => {
|
||||
const manager = await mountManager();
|
||||
|
||||
await manager.showDialog({
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 59 },
|
||||
},
|
||||
announceMessage: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
});
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
expect(toast.labelText).toContain('"seconds":59');
|
||||
expect(toast.announceText).toContain('"seconds":60');
|
||||
expect(localize).toHaveBeenCalledWith(
|
||||
"ui.notification_toast.new_version_available",
|
||||
{ seconds: 59 }
|
||||
);
|
||||
expect(localize).toHaveBeenCalledWith(
|
||||
"ui.notification_toast.new_version_available",
|
||||
{ seconds: 60 }
|
||||
);
|
||||
});
|
||||
|
||||
it("renders and invokes primary and secondary actions", async () => {
|
||||
const manager = await mountManager();
|
||||
const primary = vi.fn();
|
||||
const secondary = vi.fn();
|
||||
|
||||
await manager.showDialog({
|
||||
message: "Update available",
|
||||
action: { action: primary, primary: true, text: "Update now" },
|
||||
secondaryAction: { action: secondary, text: "Cancel" },
|
||||
duration: -1,
|
||||
});
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
const buttons = manager.shadowRoot!.querySelectorAll("ha-button");
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons[0].textContent?.trim()).toBe("Cancel");
|
||||
expect(buttons[0].getAttribute("appearance")).toBe("plain");
|
||||
expect(buttons[1].textContent?.trim()).toBe("Update now");
|
||||
expect(buttons[1].getAttribute("appearance")).toBe("filled");
|
||||
|
||||
buttons[0].click();
|
||||
expect(toast.hide).toHaveBeenLastCalledWith("action");
|
||||
expect(secondary).toHaveBeenCalledOnce();
|
||||
expect(primary).not.toHaveBeenCalled();
|
||||
|
||||
buttons[1].click();
|
||||
expect(toast.hide).toHaveBeenLastCalledWith("action");
|
||||
expect(primary).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps a same-ID toast visible while replacing its content", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "status", message: "First" });
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
vi.mocked(toast.hide).mockClear();
|
||||
|
||||
await manager.showDialog({ id: "status", message: "Second" });
|
||||
|
||||
expect(toast.hide).not.toHaveBeenCalled();
|
||||
expect(toast.labelText).toBe("Second");
|
||||
});
|
||||
|
||||
it("hides a toast before replacing it with a different ID", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "first", message: "First" });
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
vi.mocked(toast.hide).mockClear();
|
||||
|
||||
await manager.showDialog({ id: "second", message: "Second" });
|
||||
|
||||
expect(toast.hide).toHaveBeenCalledOnce();
|
||||
expect(toast.labelText).toBe("Second");
|
||||
});
|
||||
|
||||
it("ignores a stale replacement after a newer notification starts", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "first", message: "First" });
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
const delayedHide = deferred();
|
||||
vi.mocked(toast.hide).mockReturnValueOnce(delayedHide.promise);
|
||||
|
||||
const staleShow = manager.showDialog({ id: "second", message: "Second" });
|
||||
await manager.showDialog({ id: "third", message: "Third" });
|
||||
delayedHide.resolve();
|
||||
await staleShow;
|
||||
|
||||
expect(toast.labelText).toBe("Third");
|
||||
});
|
||||
|
||||
it("clears rendered state after the toast closes", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ message: "Message" });
|
||||
manager.shadowRoot!.querySelector("ha-toast")!.dispatchEvent(
|
||||
new CustomEvent("toast-closed", {
|
||||
detail: { reason: "programmatic" },
|
||||
})
|
||||
);
|
||||
|
||||
await manager.updateComplete;
|
||||
|
||||
expect(manager.shadowRoot!.querySelector("ha-toast")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,368 +0,0 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../src/data/context/dirty-state";
|
||||
import type { CompareStrategy } from "../../src/mixins/dirty-state-provider-mixin";
|
||||
import { DirtyStateProviderMixin } from "../../src/mixins/dirty-state-provider-mixin";
|
||||
|
||||
interface TestState {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
options?: { mode: string };
|
||||
}
|
||||
|
||||
class TestDirtyStateConsumer extends LitElement {
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
public dirtyState?: DirtyStateContext;
|
||||
}
|
||||
|
||||
customElements.define("test-dirty-state-consumer", TestDirtyStateConsumer);
|
||||
|
||||
class TestDirtyStateProvider extends DirtyStateProviderMixin<TestState>()(
|
||||
LitElement
|
||||
) {
|
||||
public initialize(
|
||||
strategy: CompareStrategy<TestState>,
|
||||
value?: TestState,
|
||||
normalize?: (value: TestState) => TestState
|
||||
) {
|
||||
this._initDirtyTracking(strategy, value, normalize);
|
||||
}
|
||||
|
||||
public setValue(value: TestState) {
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
public markClean() {
|
||||
this._markDirtyStateClean();
|
||||
}
|
||||
|
||||
public discardChanges() {
|
||||
this._discardDirtyStateChanges();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("test-dirty-state-provider", TestDirtyStateProvider);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-dirty-state-consumer": TestDirtyStateConsumer;
|
||||
"test-dirty-state-provider": TestDirtyStateProvider;
|
||||
}
|
||||
}
|
||||
|
||||
let provider: TestDirtyStateProvider | undefined;
|
||||
|
||||
const mountProvider = async () => {
|
||||
provider = document.createElement(
|
||||
"test-dirty-state-provider"
|
||||
) as TestDirtyStateProvider;
|
||||
document.body.append(provider);
|
||||
await provider.updateComplete;
|
||||
return provider;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelectorAll("test-dirty-state-provider").forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
provider = undefined;
|
||||
window.isDirtyState = false;
|
||||
});
|
||||
|
||||
describe("DirtyStateProviderMixin", () => {
|
||||
it("tracks changes against an initial state", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
|
||||
element.setValue({ name: "Kitchen light", enabled: false });
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
|
||||
element.setValue({ name: "Kitchen", enabled: false });
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("marks the current state as clean", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
element.setValue({ name: "Kitchen light", enabled: false });
|
||||
await element.updateComplete;
|
||||
|
||||
element.markClean();
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
|
||||
element.setValue({ name: "Kitchen", enabled: false });
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("discards changes back to the initial state", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
element.setValue({ name: "Kitchen light", enabled: true });
|
||||
await element.updateComplete;
|
||||
|
||||
element.discardChanges();
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
expect(element.isEffectiveDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("uses deep comparison for nested values", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "deep" },
|
||||
{ name: "Kitchen", enabled: false, options: { mode: "auto" } }
|
||||
);
|
||||
element.setValue({
|
||||
name: "Kitchen",
|
||||
enabled: false,
|
||||
options: { mode: "auto" },
|
||||
});
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
|
||||
element.setValue({
|
||||
name: "Kitchen",
|
||||
enabled: false,
|
||||
options: { mode: "manual" },
|
||||
});
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("uses reference comparison for nested values with a shallow strategy", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false, options: { mode: "auto" } }
|
||||
);
|
||||
element.setValue({
|
||||
name: "Kitchen",
|
||||
enabled: false,
|
||||
options: { mode: "auto" },
|
||||
});
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("supports a custom comparison strategy", async () => {
|
||||
const element = await mountProvider();
|
||||
const compare = vi.fn(
|
||||
(initial: TestState, current: TestState) =>
|
||||
initial.name.toLowerCase() === current.name.toLowerCase()
|
||||
);
|
||||
element.initialize(
|
||||
{ type: "custom", compare },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
element.setValue({ name: "KITCHEN", enabled: true });
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
expect(compare).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the deep baseline isolated from initial state mutations", async () => {
|
||||
const element = await mountProvider();
|
||||
const initial = {
|
||||
name: "Kitchen",
|
||||
enabled: false,
|
||||
options: { mode: "auto" },
|
||||
};
|
||||
element.initialize({ type: "deep" }, initial);
|
||||
initial.options.mode = "manual";
|
||||
element.setValue({
|
||||
name: "Kitchen",
|
||||
enabled: false,
|
||||
options: { mode: "auto" },
|
||||
});
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("uses the first deferred value as its baseline", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize({ type: "shallow" });
|
||||
element.setValue({ name: "Kitchen", enabled: false });
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
|
||||
element.setValue({ name: "Bedroom", enabled: false });
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("tracks raw and effective dirty state separately", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false },
|
||||
(value) => ({ ...value, enabled: false })
|
||||
);
|
||||
element.setValue({ name: "Kitchen", enabled: true });
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
expect(element.isEffectiveDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("publishes dirty and clean transitions with event details", async () => {
|
||||
const listener = vi.fn();
|
||||
window.addEventListener("dirty-state-changed", listener);
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
await element.updateComplete;
|
||||
listener.mockClear();
|
||||
|
||||
element.setValue({ name: "Bedroom", enabled: false });
|
||||
await element.updateComplete;
|
||||
element.setValue({ name: "Kitchen", enabled: false });
|
||||
await element.updateComplete;
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listener.mock.calls[0][0].detail).toEqual({ isDirty: true });
|
||||
expect(listener.mock.calls[1][0].detail).toEqual({ isDirty: false });
|
||||
expect(window.isDirtyState).toBe(false);
|
||||
window.removeEventListener("dirty-state-changed", listener);
|
||||
});
|
||||
|
||||
it("clears published dirty state when disconnected", async () => {
|
||||
const listener = vi.fn();
|
||||
window.addEventListener("dirty-state-changed", listener);
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
element.setValue({ name: "Bedroom", enabled: false });
|
||||
await element.updateComplete;
|
||||
listener.mockClear();
|
||||
|
||||
element.remove();
|
||||
|
||||
expect(window.isDirtyState).toBe(false);
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener.mock.calls[0][0].detail).toEqual({ isDirty: false });
|
||||
window.removeEventListener("dirty-state-changed", listener);
|
||||
});
|
||||
|
||||
it("hands global dirty state to the next provider after disconnecting", async () => {
|
||||
const firstProvider = await mountProvider();
|
||||
firstProvider.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
firstProvider.setValue({ name: "Bedroom", enabled: false });
|
||||
await firstProvider.updateComplete;
|
||||
expect(window.isDirtyState).toBe(true);
|
||||
|
||||
firstProvider.remove();
|
||||
expect(window.isDirtyState).toBe(false);
|
||||
|
||||
const nextProvider = await mountProvider();
|
||||
nextProvider.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Hallway", enabled: false }
|
||||
);
|
||||
nextProvider.setValue({ name: "Hallway", enabled: true });
|
||||
await nextProvider.updateComplete;
|
||||
|
||||
expect(window.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("stays globally dirty while any connected provider is dirty", async () => {
|
||||
const dirtyProvider = await mountProvider();
|
||||
dirtyProvider.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
dirtyProvider.setValue({ name: "Bedroom", enabled: false });
|
||||
await dirtyProvider.updateComplete;
|
||||
|
||||
const cleanProvider = document.createElement("test-dirty-state-provider");
|
||||
document.body.append(cleanProvider);
|
||||
cleanProvider.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Hallway", enabled: false }
|
||||
);
|
||||
await cleanProvider.updateComplete;
|
||||
|
||||
expect(window.isDirtyState).toBe(true);
|
||||
|
||||
cleanProvider.remove();
|
||||
|
||||
expect(window.isDirtyState).toBe(true);
|
||||
|
||||
dirtyProvider.setValue({ name: "Kitchen", enabled: false });
|
||||
await dirtyProvider.updateComplete;
|
||||
|
||||
expect(window.isDirtyState).toBe(false);
|
||||
});
|
||||
|
||||
it("restores a provider's dirty state after reconnecting", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize(
|
||||
{ type: "shallow" },
|
||||
{ name: "Kitchen", enabled: false }
|
||||
);
|
||||
element.setValue({ name: "Bedroom", enabled: false });
|
||||
await element.updateComplete;
|
||||
|
||||
element.remove();
|
||||
expect(window.isDirtyState).toBe(false);
|
||||
|
||||
document.body.append(element);
|
||||
|
||||
expect(window.isDirtyState).toBe(true);
|
||||
});
|
||||
|
||||
it("tracks independent context slices and marks all of them clean", async () => {
|
||||
const element = await mountProvider();
|
||||
element.initialize({ type: "shallow" });
|
||||
const consumer = document.createElement("test-dirty-state-consumer");
|
||||
element.append(consumer);
|
||||
await consumer.updateComplete;
|
||||
const setState = (value: TestState, key: "first" | "second") => {
|
||||
Reflect.apply(consumer.dirtyState!.setState, undefined, [value, key]);
|
||||
};
|
||||
|
||||
setState({ name: "Kitchen", enabled: false }, "first");
|
||||
setState({ name: "Bedroom", enabled: false }, "second");
|
||||
await element.updateComplete;
|
||||
setState({ name: "Kitchen light", enabled: false }, "first");
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(true);
|
||||
|
||||
consumer.dirtyState!.markClean();
|
||||
await element.updateComplete;
|
||||
expect(element.isDirtyState).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,341 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ShowToastParams } from "../../src/managers/notification-manager";
|
||||
import {
|
||||
registerServiceWorker,
|
||||
supportsServiceWorker,
|
||||
} from "../../src/util/register-service-worker";
|
||||
|
||||
class FakeWorker extends EventTarget {
|
||||
public state: ServiceWorkerState = "installing";
|
||||
|
||||
public postMessage = vi.fn();
|
||||
}
|
||||
|
||||
class FakeRegistration extends EventTarget {
|
||||
public installing: ServiceWorker | null = null;
|
||||
|
||||
public waiting: ServiceWorker | null = null;
|
||||
}
|
||||
|
||||
class FakeServiceWorkerContainer extends EventTarget {
|
||||
public controller: ServiceWorker | null = null;
|
||||
|
||||
public register = vi.fn();
|
||||
}
|
||||
|
||||
const serviceWorkerDescriptor = Object.getOwnPropertyDescriptor(
|
||||
navigator,
|
||||
"serviceWorker"
|
||||
);
|
||||
|
||||
const restoreServiceWorker = () => {
|
||||
if (serviceWorkerDescriptor) {
|
||||
Object.defineProperty(navigator, "serviceWorker", serviceWorkerDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(navigator, "serviceWorker");
|
||||
}
|
||||
};
|
||||
|
||||
describe("supportsServiceWorker", () => {
|
||||
afterEach(restoreServiceWorker);
|
||||
|
||||
it("returns true when the service worker API is available", () => {
|
||||
Object.defineProperty(navigator, "serviceWorker", {
|
||||
configurable: true,
|
||||
value: new FakeServiceWorkerContainer(),
|
||||
});
|
||||
|
||||
expect(supportsServiceWorker()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when the service worker API is unavailable", () => {
|
||||
Reflect.deleteProperty(navigator, "serviceWorker");
|
||||
|
||||
expect(supportsServiceWorker()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerServiceWorker", () => {
|
||||
let root: HTMLElement;
|
||||
let worker: FakeWorker;
|
||||
let registration: FakeRegistration;
|
||||
let serviceWorker: FakeServiceWorkerContainer;
|
||||
let notifications: ShowToastParams[];
|
||||
|
||||
const latestNotification = () => notifications[notifications.length - 1];
|
||||
|
||||
const setDirtyState = (isDirty: boolean) => {
|
||||
window.isDirtyState = isDirty;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("dirty-state-changed", { detail: { isDirty } })
|
||||
);
|
||||
};
|
||||
|
||||
const installUpdate = () => {
|
||||
registration.installing = worker as unknown as ServiceWorker;
|
||||
registration.dispatchEvent(new Event("updatefound"));
|
||||
worker.state = "installed";
|
||||
worker.dispatchEvent(new Event("statechange"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
globalThis.__BUILD__ = "modern";
|
||||
globalThis.__DEV__ = false;
|
||||
globalThis.__DEMO__ = false;
|
||||
window.isDirtyState = false;
|
||||
notifications = [];
|
||||
root = document.createElement("div");
|
||||
root.addEventListener("hass-notification", (event) => {
|
||||
notifications.push((event as CustomEvent<ShowToastParams>).detail);
|
||||
});
|
||||
worker = new FakeWorker();
|
||||
registration = new FakeRegistration();
|
||||
serviceWorker = new FakeServiceWorkerContainer();
|
||||
serviceWorker.controller = worker as unknown as ServiceWorker;
|
||||
serviceWorker.register.mockResolvedValue(registration);
|
||||
Object.defineProperty(navigator, "serviceWorker", {
|
||||
configurable: true,
|
||||
value: serviceWorker,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
latestNotification()?.secondaryAction?.action();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
window.isDirtyState = false;
|
||||
globalThis.__DEV__ = false;
|
||||
globalThis.__DEMO__ = false;
|
||||
restoreServiceWorker();
|
||||
});
|
||||
|
||||
it("registers the worker for the current build", async () => {
|
||||
await registerServiceWorker(root, false);
|
||||
|
||||
expect(serviceWorker.register).toHaveBeenCalledOnce();
|
||||
expect(serviceWorker.register).toHaveBeenCalledWith("/sw-modern.js");
|
||||
});
|
||||
|
||||
it("does not register when service workers are unsupported", async () => {
|
||||
Reflect.deleteProperty(navigator, "serviceWorker");
|
||||
|
||||
await registerServiceWorker(root);
|
||||
|
||||
expect(serviceWorker.register).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("listens for controller changes before registering", async () => {
|
||||
const addEventListener = vi.spyOn(serviceWorker, "addEventListener");
|
||||
|
||||
await registerServiceWorker(root, false);
|
||||
|
||||
expect(addEventListener).toHaveBeenCalledWith(
|
||||
"controllerchange",
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(addEventListener.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
serviceWorker.register.mock.invocationCallOrder[0]
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ notifyUpdate: false, dev: false, demo: false },
|
||||
{ notifyUpdate: true, dev: true, demo: false },
|
||||
{ notifyUpdate: true, dev: false, demo: true },
|
||||
])(
|
||||
"does not monitor updates with notifyUpdate=$notifyUpdate, dev=$dev, demo=$demo",
|
||||
async ({ notifyUpdate, dev, demo }) => {
|
||||
globalThis.__DEV__ = dev;
|
||||
globalThis.__DEMO__ = demo;
|
||||
const addEventListener = vi.spyOn(registration, "addEventListener");
|
||||
|
||||
await registerServiceWorker(root, notifyUpdate);
|
||||
|
||||
expect(addEventListener).not.toHaveBeenCalledWith(
|
||||
"updatefound",
|
||||
expect.any(Function)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it("ignores update discovery without an installing worker", async () => {
|
||||
await registerServiceWorker(root);
|
||||
|
||||
registration.dispatchEvent(new Event("updatefound"));
|
||||
|
||||
expect(notifications).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("waits for an installing worker to reach the installed state", async () => {
|
||||
await registerServiceWorker(root);
|
||||
registration.installing = worker as unknown as ServiceWorker;
|
||||
registration.dispatchEvent(new Event("updatefound"));
|
||||
|
||||
worker.state = "activating";
|
||||
worker.dispatchEvent(new Event("statechange"));
|
||||
|
||||
expect(notifications).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores an initial installation without an existing controller", async () => {
|
||||
serviceWorker.controller = null;
|
||||
await registerServiceWorker(root);
|
||||
|
||||
installUpdate();
|
||||
|
||||
expect(notifications).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("starts at 60 seconds when an update is installed", async () => {
|
||||
await registerServiceWorker(root);
|
||||
|
||||
installUpdate();
|
||||
|
||||
expect(latestNotification()).toMatchObject({
|
||||
id: "frontend-update-available",
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.new_version_available",
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
announceMessage: {
|
||||
args: { seconds: 60 },
|
||||
},
|
||||
action: {
|
||||
primary: true,
|
||||
text: { translationKey: "ui.notification_toast.update_now" },
|
||||
},
|
||||
secondaryAction: {
|
||||
text: { translationKey: "ui.common.cancel" },
|
||||
},
|
||||
duration: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses an already waiting worker", async () => {
|
||||
registration.waiting = worker as unknown as ServiceWorker;
|
||||
|
||||
await registerServiceWorker(root);
|
||||
|
||||
expect(latestNotification().message).toMatchObject({
|
||||
args: { seconds: 60 },
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores a waiting worker without an existing controller", async () => {
|
||||
registration.waiting = worker as unknown as ServiceWorker;
|
||||
serviceWorker.controller = null;
|
||||
|
||||
await registerServiceWorker(root);
|
||||
|
||||
expect(notifications).toHaveLength(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ elapsed: 1_000, visual: 59, announced: 60 },
|
||||
{ elapsed: 20_000, visual: 40, announced: 40 },
|
||||
{ elapsed: 40_000, visual: 20, announced: 20 },
|
||||
{ elapsed: 55_000, visual: 5, announced: 5 },
|
||||
{ elapsed: 59_000, visual: 1, announced: 5 },
|
||||
])(
|
||||
"shows $visual seconds and announces $announced after $elapsed ms",
|
||||
async ({ elapsed, visual, announced }) => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
|
||||
vi.advanceTimersByTime(elapsed);
|
||||
|
||||
expect(latestNotification().message).toMatchObject({
|
||||
args: { seconds: visual },
|
||||
});
|
||||
expect(latestNotification().announceMessage).toMatchObject({
|
||||
args: { seconds: announced },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it("activates and hides the update when the countdown finishes", async () => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
|
||||
vi.advanceTimersByTime(60_000);
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledOnce();
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({ type: "skipWaiting" });
|
||||
expect(latestNotification()).toMatchObject({
|
||||
id: "frontend-update-available",
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("activates and hides the update immediately on request", async () => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
|
||||
latestNotification().action!.action();
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledOnce();
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({ type: "skipWaiting" });
|
||||
expect(latestNotification()).toMatchObject({ message: "", duration: 0 });
|
||||
});
|
||||
|
||||
it("cancels automatic activation", async () => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
|
||||
latestNotification().secondaryAction!.action();
|
||||
vi.advanceTimersByTime(60_000);
|
||||
|
||||
expect(worker.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defers the message and activation while the page is dirty", async () => {
|
||||
setDirtyState(true);
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
|
||||
expect(notifications).toHaveLength(0);
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(worker.postMessage).not.toHaveBeenCalled();
|
||||
|
||||
setDirtyState(false);
|
||||
expect(latestNotification().message).toMatchObject({
|
||||
args: { seconds: 60 },
|
||||
});
|
||||
});
|
||||
|
||||
it("hides and resets the countdown when the page becomes dirty", async () => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
vi.advanceTimersByTime(10_000);
|
||||
|
||||
setDirtyState(true);
|
||||
|
||||
expect(latestNotification()).toMatchObject({ message: "", duration: 0 });
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(worker.postMessage).not.toHaveBeenCalled();
|
||||
|
||||
setDirtyState(false);
|
||||
expect(latestNotification().message).toMatchObject({
|
||||
args: { seconds: 60 },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the latest installed worker", async () => {
|
||||
await registerServiceWorker(root);
|
||||
installUpdate();
|
||||
const replacement = new FakeWorker();
|
||||
replacement.state = "installed";
|
||||
|
||||
registration.installing = replacement as unknown as ServiceWorker;
|
||||
registration.dispatchEvent(new Event("updatefound"));
|
||||
replacement.dispatchEvent(new Event("statechange"));
|
||||
vi.advanceTimersByTime(60_000);
|
||||
|
||||
expect(worker.postMessage).not.toHaveBeenCalled();
|
||||
expect(replacement.postMessage).toHaveBeenCalledWith({
|
||||
type: "skipWaiting",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4755,65 +4755,65 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.5"
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.4"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.5"
|
||||
"@rspack/binding-darwin-x64@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.4"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.5"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.4"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.5"
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.4"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.5"
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.4"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.5"
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.4"
|
||||
conditions: os=linux & cpu=riscv64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.5"
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.4"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.5"
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.4"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.5"
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.4"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:1.11.2"
|
||||
"@emnapi/runtime": "npm:1.11.2"
|
||||
@@ -4822,43 +4822,43 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.5"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.4"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.5"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.4"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.5"
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.4"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/binding@npm:2.1.5"
|
||||
"@rspack/binding@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding@npm:2.1.4"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.5"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.5"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.5"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.5"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.5"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.5"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.5"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.5"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.5"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.5"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.5"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.5"
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.4"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.4"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.4"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.4"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.4"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -4884,15 +4884,15 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/d1054348d3ba734485f977d574c6311311707adfcc5e53f03c8dbbbdc5778cff9b815e1d675992e2832241606d4cce7678acc3999b259139120b4ec30751f1e1
|
||||
checksum: 10/425bf152dba708992ce16114ce6bc8dfa424071694e85201317518dce1899eeae873a2adbd23b214bb8d223f7290e000fd78ad9bf262c633e12d3d4ff73bbdcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@rspack/core@npm:2.1.5"
|
||||
"@rspack/core@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/core@npm:2.1.4"
|
||||
dependencies:
|
||||
"@rspack/binding": "npm:2.1.5"
|
||||
"@rspack/binding": "npm:2.1.4"
|
||||
peerDependencies:
|
||||
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
|
||||
"@swc/helpers": ^0.5.23
|
||||
@@ -4901,7 +4901,7 @@ __metadata:
|
||||
optional: true
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/714064de701211724f7d859bc269d357fa2ad2f7ea0ac34d1e4e48b534981cf3961bd723c0659b9c093330f04e01e16a0c5587d6a18301727e27d717f764ded7
|
||||
checksum: 10/3c7aa9e8dbe8b132b51fc017a6c2c76ddf346cf663cad2a8912ba61469d80468acd045503d405bf962bb5ec44f44036512dd6d6233ea6d1419979e5c17e1dbd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9955,7 +9955,7 @@ __metadata:
|
||||
"@playwright/test": "npm:1.61.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.6.1"
|
||||
"@rspack/core": "npm:2.1.5"
|
||||
"@rspack/core": "npm:2.1.4"
|
||||
"@rspack/dev-server": "npm:2.1.0"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
@@ -10023,14 +10023,14 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.3.0"
|
||||
intl-messageformat: "npm:11.2.12"
|
||||
js-yaml: "npm:5.2.2"
|
||||
js-yaml: "npm:5.2.1"
|
||||
jsdom: "npm:29.1.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"
|
||||
leaflet.markercluster: "npm:1.5.3"
|
||||
license-checker-rseidelsohn: "npm:5.0.1"
|
||||
lint-staged: "npm:17.1.1"
|
||||
lint-staged: "npm:17.1.0"
|
||||
lit: "npm:3.3.3"
|
||||
lit-analyzer: "npm:2.0.3"
|
||||
lit-html: "npm:3.3.3"
|
||||
@@ -10056,7 +10056,7 @@ __metadata:
|
||||
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
|
||||
stacktrace-js: "npm:2.0.2"
|
||||
superstruct: "npm:2.0.2"
|
||||
tar: "npm:7.5.21"
|
||||
tar: "npm:7.5.20"
|
||||
terser-webpack-plugin: "npm:5.6.1"
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
@@ -10957,14 +10957,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "js-yaml@npm:5.2.2"
|
||||
"js-yaml@npm:5.2.1":
|
||||
version: 5.2.1
|
||||
resolution: "js-yaml@npm:5.2.1"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.mjs
|
||||
checksum: 10/2b4c2933af12c97e1c4894a4f27fe9b06dab70a64a96bb50624b4429bef6bf11008bde20d868bce52a36784473314efc30078ba6025b58cf7537961e23b1ae9c
|
||||
checksum: 10/e1eca2d21c15572585bb236d9fde31d6789eb50b9c63e8753fa7e0777bc480f7521cad517bd7a0c66f27dfc27ddcd7100beeefa51c1a50e10e98f2e009633c3d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11423,9 +11423,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:17.1.1":
|
||||
version: 17.1.1
|
||||
resolution: "lint-staged@npm:17.1.1"
|
||||
"lint-staged@npm:17.1.0":
|
||||
version: 17.1.0
|
||||
resolution: "lint-staged@npm:17.1.0"
|
||||
dependencies:
|
||||
picomatch: "npm:^4.0.5"
|
||||
string-argv: "npm:^0.3.2"
|
||||
@@ -11436,7 +11436,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/a54e063a579c093cecd4fc210f78c424fa0aa1dbe8196cf57160fad05acbfe4cb60ddd50751c799277094a0b7949de0ea3b61392b90e137f5c5bdf8473826e72
|
||||
checksum: 10/050d8ce1f6c2cf218ebc0b693de6d1754b333f04e263ecbee230b1aa742fd684667d0f42beee48c3bb11cbcc4e50c4995289c82c36bb08f6bd0f8157816514e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14501,16 +14501,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:*, tar@npm:7.5.21, tar@npm:^7.4.3, tar@npm:^7.5.4":
|
||||
version: 7.5.21
|
||||
resolution: "tar@npm:7.5.21"
|
||||
"tar@npm:*, tar@npm:7.5.20, tar@npm:^7.4.3, tar@npm:^7.5.4":
|
||||
version: 7.5.20
|
||||
resolution: "tar@npm:7.5.20"
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass": "npm:^4.0.0"
|
||||
chownr: "npm:^3.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
minizlib: "npm:^3.1.0"
|
||||
yallist: "npm:^5.0.0"
|
||||
checksum: 10/a1b7dcee15e4f7dbef7f07c3cf0fe946248efabd1cb029b54c698d817dfc2876c75bcaa164a12dd21191e385759ce3e37fea562cf4aabaa00984ef9ed3c48d17
|
||||
checksum: 10/90a0fe423ac921197ad5eefc5e5f7ad7f42b06e80444c8c347c1e4112384cbe9cb53ade3ef71748eed3684888756869c2aeef6b6303c766101f3217e254d4be9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user