329 files

This commit is contained in:
Home Assistant Version Control
2026-08-06 13:56:25 +00:00
parent 0df89406fa
commit 7afe7add1d
330 changed files with 13098 additions and 5942 deletions
@@ -66,7 +66,7 @@ export async function mountPanel(
objects: unknown[],
extraHandlers: Record<string, WsHandler> = {},
) {
const { hass, sent } = createMockHass({
const { hass, sent, subscriptions } = createMockHass({
handlers: {
"maintenance_supporter/objects": () => ({ objects }),
"maintenance_supporter/statistics": () => ({
@@ -97,7 +97,7 @@ export async function mountPanel(
await el.updateComplete;
await new Promise((r) => setTimeout(r, 10));
await el.updateComplete;
return { el, sent };
return { el, sent, subscriptions };
}
export function sr(el: HTMLElement): ShadowRoot {
@@ -97,7 +97,10 @@ export type WsHandler = (msg: SentMessage) => Promise<unknown> | unknown;
export interface CreateMockHassResult {
hass: {
language: string;
connection: { sendMessagePromise: (msg: SentMessage) => Promise<unknown> };
connection: {
sendMessagePromise: (msg: SentMessage) => Promise<unknown>;
subscribeMessage: (cb: (event: unknown) => void, msg: SentMessage) => Promise<() => void>;
};
callService: (
domain: string, service: string,
data?: Record<string, unknown>, target?: Record<string, unknown>,
@@ -107,6 +110,8 @@ export interface CreateMockHassResult {
};
sent: SentMessage[];
serviceCalls: ServiceCall[];
/** Captured subscribeMessage registrations — push events via `.push(ev)`. */
subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }>;
}
export interface CreateMockHassOptions {
@@ -157,15 +162,31 @@ export function createMockHass(opts: CreateMockHassOptions = {}): CreateMockHass
serviceCalls.push({ domain, service, data, target });
};
// Captured subscriptions: tests push events into components via
// `subscriptions.find(...).push(event)`.
const subscriptions: Array<{ msg: SentMessage; push: (event: unknown) => void }> = [];
const subscribeMessage = async (
cb: (event: unknown) => void,
msg: SentMessage,
): Promise<() => void> => {
const entry = { msg, push: (event: unknown) => cb(event) };
subscriptions.push(entry);
return () => {
const i = subscriptions.indexOf(entry);
if (i >= 0) subscriptions.splice(i, 1);
};
};
return {
hass: {
language: opts.language ?? "en",
connection: { sendMessagePromise },
connection: { sendMessagePromise, subscribeMessage },
callService,
services: opts.services,
states: opts.states,
},
sent,
serviceCalls,
subscriptions,
};
}
@@ -38,6 +38,31 @@ async function mount(canWrite: boolean): Promise<MaintenancePartsSection> {
}
describe("parts-section", () => {
it("shows the inventory value only when a part has price AND tracked stock (#104)", async () => {
// PARTS as-is: p1 has stock but no cost, p2 no stock → chip hidden.
const bare = await mount(false);
expect(bare.shadowRoot!.querySelector(".inventory-value")).to.equal(null);
const priced: MaintenancePart[] = [
{ id: "p1", name: "Filter", cost: 12.5, stock: 3, is_low: false },
{ id: "p2", name: "Brush", cost: 4, stock: null, is_low: false }, // untracked → no contribution
{ id: "p3", name: "Seal", cost: null, stock: 5, is_low: false }, // unpriced → no contribution
];
const el = await fixture<MaintenancePartsSection>(html`
<maintenance-parts-section
.hass=${{ language: "en", connection: { sendMessagePromise: async () => ({}) } } as never}
.entryId=${"e1"}
.parts=${priced}
.currencySymbol=${"€"}
></maintenance-parts-section>
`);
await el.updateComplete;
const chip = el.shadowRoot!.querySelector(".inventory-value")!;
expect(chip, "value chip rendered").to.exist;
expect(chip.textContent).to.include("37.50");
expect(chip.textContent).to.include("€");
});
it("renders a row per part with stock badge, identifiers and location", async () => {
const el = await mount(false);
const rows = el.shadowRoot!.querySelectorAll(".part-row");
@@ -57,6 +57,9 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
taskId: "t1",
objectName: "Pool Pump",
objectDocUrl: null,
objectManualDocs: [],
openManualDoc: () => {},
setChecklistItem: () => {},
isOperator: false,
actionLoading: false,
moreMenuOpen: false,
@@ -212,6 +215,57 @@ describe("task-detail renderer", () => {
expect(host2.querySelector(".kpi-bar")).to.be.null;
});
it("object-manual row falls back to an attached manual when the URL is empty", () => {
let opened: unknown = null;
const host = mount(task(), ctx({
objectDocUrl: null,
objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }],
openManualDoc: (d) => { opened = d; },
}));
const link = [...host.querySelectorAll(".task-meta-link a")]
.find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined;
expect(link, "fallback manual link rendered").to.exist;
expect(link!.getAttribute("title")).to.equal("Pump Handbook");
link!.click();
expect((opened as { id?: string })?.id).to.equal("d1");
});
it("the URL field still wins over attached manuals", () => {
const host = mount(task(), ctx({
objectDocUrl: "https://vendor.example/manual",
objectManualDocs: [{ id: "d1", title: "Pump Handbook", kind: "file" }],
}));
const link = [...host.querySelectorAll(".task-meta-link a")]
.find((a) => a.textContent?.includes("Pool Pump")) as HTMLElement | undefined;
expect(link, "manual row rendered").to.exist;
expect(link!.getAttribute("href")).to.equal("https://vendor.example/manual");
});
it("checklist ticks render from progress and fire setChecklistItem (#73)", () => {
const calls: Array<[string, boolean]> = [];
const host = mount(
task({ checklist: ["Drain", "Clean", "Refill"], checklist_progress: { Clean: true } }),
ctx({
features: {
adaptive: false, predictions: false, seasonal: false,
environmental: false, budget: false, groups: false,
checklists: true, schedule_time: false, completion_actions: false,
},
setChecklistItem: (item, done) => calls.push([item, done]),
}),
);
const header = host.querySelector(".checklist-preview-header")!;
expect(header.textContent).to.include("1/3");
const boxes = [...host.querySelectorAll<HTMLInputElement>(".checklist-preview-list input")];
expect(boxes.length).to.equal(3);
expect(boxes[1].checked).to.be.true;
expect(boxes[0].checked).to.be.false;
expect(host.querySelectorAll(".checklist-preview-list li.checked").length).to.equal(1);
boxes[0].click();
expect(calls).to.deep.equal([["Drain", true]]);
});
it("KPI bar shows warning days and currency symbol", () => {
const host = mount(task(), ctx({ currencySymbol: "$" }));
const kpi = host.querySelector(".kpi-bar")!;
@@ -54,6 +54,9 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
taskId: "t1",
objectName: "Pool Pump",
objectDocUrl: null,
objectManualDocs: [],
openManualDoc: () => {},
setChecklistItem: () => {},
isOperator: false,
actionLoading: false,
moreMenuOpen: false,