197 files

This commit is contained in:
Home Assistant Version Control
2026-08-21 18:53:37 +00:00
parent 89d9f5e49b
commit 383e9ed04e
197 changed files with 4386 additions and 1837 deletions
@@ -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;
}
});
});
@@ -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 ─────────────────