429 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-30 02:43:31 +00:00
1 parent 472c19bceb
commit 963cce81a7
566 files changed
+50311 -47616

No files matched your search

@@ -273,3 +273,27 @@ describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => {
expect(chip(el)).to.equal(null);
});
});
describe("complete-dialog require_tag_scan note", () => {
it("shows the scan hint when the task demands proof of presence", async () => {
const { hass } = createMockHass({});
const el = await fixture<MaintenanceCompleteDialog>(html`
<maintenance-complete-dialog
.hass=${hass}
.entryId=${"entry1"}
.taskId=${"task1"}
.taskName=${"Filter Wechsel"}
.lang=${"en"}
.requireTagScan=${true}
></maintenance-complete-dialog>
`);
el.open();
await el.updateComplete;
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.exist;
});
it("stays silent for ordinary tasks", async () => {
const { el } = await mount();
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.equal(null);
});
});
@@ -5,7 +5,7 @@
import { expect } from "@open-wc/testing";
import { formatDate, formatDateTime, setDateTimePrefs } from "../styles";
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined });
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined }, null);
describe("formatDate with HA profile date_format (#97)", () => {
afterEach(reset);
@@ -66,3 +66,39 @@ describe("formatDateTime with HA profile time_format (#97)", () => {
expect(out.toLowerCase()).to.match(/pm|nachm/);
});
});
describe("server country regionalizes the language default (#140)", () => {
afterEach(reset);
it('"en" + AU renders dd/mm/yyyy without any profile setting', () => {
setDateTimePrefs({ date_format: "language" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
});
it('"en" + US keeps mm/dd/yyyy; "de" + AT stays dd.mm.yyyy', () => {
setDateTimePrefs({ date_format: "language" }, "US");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "language" }, "AT");
expect(formatDate("2026-08-10", "de")).to.equal("10.08.2026");
});
it("an explicit profile format always beats the country", () => {
setDateTimePrefs({ date_format: "MDY" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "YMD" }, "AU");
expect(formatDate("2026-08-10", "en")).to.equal("2026-08-10");
});
it("garbage/unset country falls back to the language mapping", () => {
setDateTimePrefs({ date_format: "language" }, "!!");
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
setDateTimePrefs({ date_format: "language" }, null);
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
});
it("country survives a later prefs update that omits it (undefined ≠ clear)", () => {
setDateTimePrefs({ date_format: "language" }, "AU");
setDateTimePrefs({ date_format: "language" });
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
});
});
@@ -114,6 +114,83 @@ describe("panel deep links (QR scan routing)", () => {
expect(dlg.taskName).to.equal("No Defaults");
});
it("quick_complete refused for missing details falls back to the dialog WITH the scan flag", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el, sent } = await mountPanel(
[obj("e1", [task({ name: "Needs Notes", require_tag_scan: true })])],
{
"maintenance_supporter/task/quick_complete": () => {
throw { code: "completion_details_required", message: "notes required" };
},
},
);
await settleRaf(el);
await settleRaf(el);
await waitForOpenCompleteDialog(el);
const dlg = completeDialog(el)!;
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist;
expect(dlg.taskName).to.equal("Needs Notes");
// The scan already happened: the gate is met, the note is not shown …
expect(dlg.viaTagScan).to.equal(true);
expect(dlg.requireTagScan).to.equal(true);
expect(dlg.shadowRoot!.querySelector(".scan-required-note")).to.be.null;
// … and the completion carries via_tag_scan for the backend's gate.
const buttons = [...dlg.shadowRoot!.querySelectorAll(".dialog-actions ha-button")];
(buttons[buttons.length - 1] as HTMLElement).click();
await new Promise((r) => setTimeout(r, 20));
const complete = sent.find((m) => m.type === "maintenance_supporter/task/complete");
expect(complete, "task/complete sent").to.exist;
expect(complete!.via_tag_scan).to.equal(true);
});
it("a plain complete (no scan) opens the dialog WITHOUT the scan flag", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=complete");
const { el } = await mountPanel([obj("e1", [task({ name: "Gated", require_tag_scan: true })])]);
await settleRaf(el);
await waitForOpenCompleteDialog(el);
const dlg = completeDialog(el)!;
expect(dlg.viaTagScan).to.equal(false);
expect(dlg.shadowRoot!.querySelector(".scan-required-note"), "note shown").to.exist;
});
it("any other quick_complete refusal relays the server's message", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el } = await mountPanel(
[obj("e1", [task({ name: "Early" })])],
{
"maintenance_supporter/task/quick_complete": () => {
throw { code: "too_early", message: "Earliest completion is 2026-09-01" };
},
},
);
await settleRaf(el);
await settleRaf(el);
expect((el as unknown as { _toastMessage: string })._toastMessage).to.equal(
"Earliest completion is 2026-09-01",
);
const dlg = completeDialog(el);
expect(dlg?.shadowRoot?.querySelector("ha-dialog") ?? null, "no dialog").to.be.null;
});
it("a silent quick_complete success reloads the data", async () => {
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
const { el, sent } = await mountPanel(
[obj("e1", [task({ name: "Quick" })])],
{ "maintenance_supporter/task/quick_complete": () => ({ success: true, via: "quick" }) },
);
await settleRaf(el);
await settleRaf(el);
// The deep link is only routed once the objects are known, so every
// objects fetch AFTER the quick_complete is the reload it triggered.
const atQuick = sent.findIndex((m) => m.type === "maintenance_supporter/task/quick_complete");
expect(atQuick, "quick_complete fired").to.be.greaterThan(-1);
const reloaded = sent
.slice(atQuick + 1)
.some((m) => m.type === "maintenance_supporter/objects");
expect(reloaded, "objects refetched after the silent complete").to.equal(true);
});
it("an unknown entry_id lands safely on the overview", async () => {
setDeepLink("entry_id=does-not-exist&task_id=t1&action=complete");
const { el } = await mountPanel([obj("e1", [task({ name: "Real" })])]);
@@ -215,6 +215,19 @@ describe("task-detail renderer", () => {
expect(host2.querySelector(".kpi-bar")).to.be.null;
});
it("history tab offers 'add past completion' routing to openComplete (#142)", () => {
let completed: MaintenanceTask | null = null;
const t2 = task();
const host = mount(t2, ctx({
activeTab: "history",
openComplete: (tk) => { completed = tk; },
}));
const btn = host.querySelector(".history-add-past-btn") as HTMLElement;
expect(btn, "add-past button").to.exist;
btn.click();
expect(completed).to.equal(t2);
});
it("object-manual row falls back to an attached manual when the URL is empty", () => {
let opened: unknown = null;
const host = mount(task(), ctx({