197 files
This commit is contained in:
+32
@@ -62,3 +62,35 @@ describe("adopt-problem-sensors dialog: suggested part", () => {
|
||||
expect(byId["binary_sensor.pump_problem"].part_id).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adopt-problem-sensors dialog: flicker filter (#136)", () => {
|
||||
it("defaults to 0 and then omits for_minutes from the payload", async () => {
|
||||
const { el, sent } = await mountOpen();
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
|
||||
expect(input, "the for-minutes field renders").to.exist;
|
||||
expect(input.value).to.equal("0");
|
||||
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
|
||||
selections: Array<{ for_minutes?: number }>;
|
||||
};
|
||||
expect(adopt.selections.every((s) => s.for_minutes === undefined)).to.equal(true);
|
||||
});
|
||||
|
||||
it("forwards the entered minutes on every selection", async () => {
|
||||
const { el, sent } = await mountOpen();
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".for-input")!;
|
||||
input.value = "10";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
el.shadowRoot!.querySelectorAll<HTMLElement>("ha-button")[1].click();
|
||||
await el.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const adopt = sent.find((m) => m.type === "maintenance_supporter/problem_sensors/adopt")! as {
|
||||
selections: Array<{ for_minutes?: number }>;
|
||||
};
|
||||
expect(adopt.selections.length).to.be.greaterThan(0);
|
||||
expect(adopt.selections.every((s) => s.for_minutes === 10)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* the no-fetch fast paths of ensureLocale/isLocaleLoaded.
|
||||
*/
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { t, isLocaleLoaded, ensureLocale } from "../styles";
|
||||
import { t, isLocaleLoaded, ensureLocale, seedEnglish } from "../styles";
|
||||
|
||||
describe("i18n runtime loader", () => {
|
||||
it("serves bundled English synchronously", () => {
|
||||
@@ -60,4 +60,25 @@ describe("i18n runtime loader", () => {
|
||||
delete g!.store.pt;
|
||||
}
|
||||
});
|
||||
|
||||
it("seedEnglish MERGES so a stale first-loader cannot shadow newer keys (#135 regression)", () => {
|
||||
// After an update, a cached app shell can load an OLD card bundle first;
|
||||
// its EN table lacks every key the fresh panel introduced. First-wins
|
||||
// seeding froze that stale table and the panel rendered raw keys
|
||||
// ("BATTERY_FLEET_ADD"). The merge keeps existing entries (steady state
|
||||
// unchanged) but lets every bundle contribute the keys it knows.
|
||||
const g = (window as unknown as {
|
||||
__msLocales: { store: Record<string, Record<string, string>> };
|
||||
}).__msLocales;
|
||||
const original = g.store.en;
|
||||
try {
|
||||
// Simulate the stale bundle having seeded first with a tiny old table.
|
||||
g.store.en = { loading: "Old loading text" };
|
||||
seedEnglish({ loading: "Fresh loading text", brand_new_key: "New feature label" });
|
||||
expect(t("loading", "en"), "existing entries win conflicts").to.equal("Old loading text");
|
||||
expect(t("brand_new_key", "en"), "newer keys are contributed, never raw").to.equal("New feature label");
|
||||
} finally {
|
||||
g.store.en = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+17
@@ -162,6 +162,23 @@ describe("task-dialog trigger_config roundtrip closure (#103 class)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("state_change: the hold filter (for_minutes, #136) survives the roundtrip", async () => {
|
||||
const tc = await saveRoundtrip({
|
||||
type: "state_change",
|
||||
entity_id: "binary_sensor.vacuum_problem",
|
||||
entity_ids: ["binary_sensor.vacuum_problem"],
|
||||
trigger_to_state: "on",
|
||||
trigger_target_changes: 1,
|
||||
trigger_for_minutes: 10,
|
||||
auto_complete_on_recovery: true,
|
||||
});
|
||||
expect(tc).to.deep.include({
|
||||
type: "state_change",
|
||||
trigger_to_state: "on",
|
||||
trigger_for_minutes: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("compound: per-condition attribute/baseline/entity_logic survive via carry", async () => {
|
||||
const tc = await saveRoundtrip({
|
||||
type: "compound",
|
||||
|
||||
@@ -122,6 +122,22 @@ describe("task-documents", () => {
|
||||
window.open = orig;
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses to window.open a non-http(s) weblink (defense-in-depth, XSS)", async () => {
|
||||
const EVIL = { id: "d4", kind: "weblink", title: "Evil", url: "javascript:alert(document.cookie)", tags: [], task_ids: ["t1"] };
|
||||
const orig = window.open;
|
||||
let opened = "NOT_CALLED";
|
||||
window.open = ((u: string) => { opened = u; return null; }) as typeof window.open;
|
||||
try {
|
||||
const { el } = await mount(true, [EVIL]);
|
||||
const info = el.shadowRoot!.querySelector<HTMLElement>(".tdoc-row .tdoc-info")!;
|
||||
info.click();
|
||||
await el.updateComplete;
|
||||
expect(opened, "a javascript: weblink must never reach window.open").to.equal("NOT_CALLED");
|
||||
} finally {
|
||||
window.open = orig;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── part mode (v2.26): same component, linking via part_ids ─────────────────
|
||||
|
||||
Reference in New Issue
Block a user