189 files
This commit is contained in:
@@ -65,6 +65,7 @@ export function obj(entryId: string, tasks: unknown[], name = "Pool Pump") {
|
||||
export async function mountPanel(
|
||||
objects: unknown[],
|
||||
extraHandlers: Record<string, WsHandler> = {},
|
||||
opts: { user?: { id: string; is_admin: boolean } } = {},
|
||||
) {
|
||||
const { hass, sent, subscriptions } = createMockHass({
|
||||
handlers: {
|
||||
@@ -83,7 +84,7 @@ export async function mountPanel(
|
||||
},
|
||||
});
|
||||
// The panel derives write access from hass.user (no user → read-only).
|
||||
(hass as Record<string, unknown>).user = { id: "admin-1", is_admin: true };
|
||||
(hass as Record<string, unknown>).user = opts.user ?? { id: "admin-1", is_admin: true };
|
||||
(hass as Record<string, unknown>).areas = {};
|
||||
|
||||
const el = await fixture<HTMLElement & { updateComplete: Promise<unknown> }>(html`
|
||||
|
||||
+128
-21
@@ -4,7 +4,7 @@
|
||||
* The completion dialog is the money path of the whole product; until now it
|
||||
* was only covered by the lazy-load tripwire. These tests pin the exact
|
||||
* outgoing WS payload (notes / cost / duration / feedback / checklist_state /
|
||||
* photo_doc_id) and the error path (server refusal keeps the dialog open).
|
||||
* photo_doc_ids) and the error path (server refusal keeps the dialog open).
|
||||
*/
|
||||
|
||||
import { expect, fixture, html } from "@open-wc/testing";
|
||||
@@ -100,6 +100,7 @@ describe("complete-dialog", () => {
|
||||
expect("duration" in msg).to.be.false;
|
||||
expect("feedback" in msg).to.be.false;
|
||||
expect("checklist_state" in msg).to.be.false;
|
||||
expect("photo_doc_ids" in msg).to.be.false;
|
||||
expect("photo_doc_id" in msg).to.be.false;
|
||||
expect("completed_at" in msg).to.be.false;
|
||||
});
|
||||
@@ -153,43 +154,149 @@ describe("complete-dialog", () => {
|
||||
expect(el.shadowRoot!.textContent).to.include("future");
|
||||
});
|
||||
|
||||
it("attaches an uploaded photo as photo_doc_id", async () => {
|
||||
const { el, sent } = await mount();
|
||||
|
||||
// Stub the document-upload endpoint the photo picker posts to.
|
||||
/** #161: stub the multipart upload route; every call mints the next id. */
|
||||
function stubUpload(ids: string[]) {
|
||||
const realFetch = window.fetch;
|
||||
const uploads: RequestInit[] = [];
|
||||
let n = 0;
|
||||
window.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
||||
uploads.push(init!);
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ id: "doc-photo-1", deduped: false }),
|
||||
} as Response;
|
||||
const id = ids[n++] ?? `doc-photo-${n}`;
|
||||
return { ok: true, status: 200, json: async () => ({ id, deduped: false }) } as Response;
|
||||
}) as typeof window.fetch;
|
||||
return { uploads, restore: () => { window.fetch = realFetch; } };
|
||||
}
|
||||
|
||||
function pickFiles(el: MaintenanceCompleteDialog, selector: string, names: string[]) {
|
||||
const fileInput = el.shadowRoot!.querySelector<HTMLInputElement>(selector)!;
|
||||
const dt = new DataTransfer();
|
||||
for (const name of names) dt.items.add(new File(["fake-png"], name, { type: "image/png" }));
|
||||
fileInput.files = dt.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
const CAMERA = '.photo-pick-camera input[type="file"]';
|
||||
const GALLERY = '.photo-pick-gallery input[type="file"]';
|
||||
|
||||
it("falls back to one photo per pick inside the Android Companion app (#161 follow-up)", async () => {
|
||||
// The Android app's file chooser drops a multi-select on the floor
|
||||
// (parseResult ignores ClipData) — the gallery picker is single there.
|
||||
(window as unknown as { externalApp?: unknown }).externalApp = {};
|
||||
try {
|
||||
const fileInput = el.shadowRoot!.querySelector<HTMLInputElement>(
|
||||
'.photo-pick input[type="file"]',
|
||||
)!;
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File(["fake-png"], "done.png", { type: "image/png" }));
|
||||
fileInput.files = dt.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
const { el } = await mount();
|
||||
const gallery = el.shadowRoot!.querySelector<HTMLInputElement>(GALLERY)!;
|
||||
expect(gallery.multiple, "no multi-select affordance on Android").to.equal(false);
|
||||
expect(el.shadowRoot!.querySelector(".photo-pick-gallery span")!.textContent!.trim()).to.equal("Choose photo");
|
||||
expect(el.shadowRoot!.querySelector(".photo-android-hint")!.textContent).to.contain("one photo per pick");
|
||||
const { uploads, restore } = stubUpload(["p1", "p2"]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, ["a.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
pickFiles(el, GALLERY, ["b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
expect(uploads.length, "each single pick is added").to.equal(2);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(2);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
} finally {
|
||||
delete (window as unknown as { externalApp?: unknown }).externalApp;
|
||||
}
|
||||
const { el: plain } = await mount();
|
||||
expect(plain.shadowRoot!.querySelector<HTMLInputElement>(GALLERY)!.multiple, "browsers keep multi-select").to.equal(true);
|
||||
expect(plain.shadowRoot!.querySelector(".photo-android-hint")).to.equal(null);
|
||||
});
|
||||
|
||||
it("attaches the uploaded photos as photo_doc_ids (#161)", async () => {
|
||||
const { el, sent } = await mount();
|
||||
const { uploads, restore } = stubUpload(["doc-photo-1", "doc-photo-2", "doc-photo-3"]);
|
||||
try {
|
||||
// The camera picker takes one shot; the gallery picker several at once.
|
||||
pickFiles(el, CAMERA, ["done.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await el.updateComplete;
|
||||
pickFiles(el, GALLERY, ["a.png", "b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
|
||||
// Preview replaces the picker once the upload returns an id.
|
||||
expect(el.shadowRoot!.querySelector(".photo-preview img")).to.exist;
|
||||
expect(uploads.length).to.equal(1);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(3);
|
||||
expect(uploads.length).to.equal(3);
|
||||
const form = uploads[0].body as FormData;
|
||||
expect(form.get("entry_id")).to.equal("entry1");
|
||||
expect(form.get("tags")).to.equal("photo");
|
||||
// The pickers stay available while there is room under the cap.
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.exist;
|
||||
|
||||
clickComplete(el);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const msg = sent.find((m) => m.type === "maintenance_supporter/task/complete")!;
|
||||
expect(msg.photo_doc_id).to.equal("doc-photo-1");
|
||||
expect(msg.photo_doc_ids).to.deep.equal(["doc-photo-1", "doc-photo-2", "doc-photo-3"]);
|
||||
expect("photo_doc_id" in msg).to.be.false;
|
||||
// Completing keeps the uploads — no delete goes out.
|
||||
expect(sent.some((m) => m.type === "maintenance_supporter/documents/delete")).to.be.false;
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("caps a completion at 10 photos and says so (#161)", async () => {
|
||||
const { el } = await mount();
|
||||
const { uploads, restore } = stubUpload([]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, Array.from({ length: 12 }, (_, i) => `p${i}.png`));
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
await el.updateComplete;
|
||||
expect(uploads.length).to.equal(10);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(10);
|
||||
// At the cap the pickers give way to the limit note.
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.equal(null);
|
||||
expect(el.shadowRoot!.querySelector(".photo-limit")!.textContent).to.include("10");
|
||||
expect(el.shadowRoot!.querySelector(".error")!.textContent).to.include("10");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("removing a tile deletes that upload; cancelling drops the rest (#161)", async () => {
|
||||
const { el, sent } = await mount();
|
||||
const { restore } = stubUpload(["doc-photo-1", "doc-photo-2"]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, ["a.png", "b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
|
||||
const removeButtons = el.shadowRoot!.querySelectorAll<HTMLElement>(".photo-remove");
|
||||
expect(removeButtons.length).to.equal(2);
|
||||
removeButtons[0].click();
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(1);
|
||||
let deletes = sent.filter((m) => m.type === "maintenance_supporter/documents/delete");
|
||||
expect(deletes.map((m) => m.doc_id)).to.deep.equal(["doc-photo-1"]);
|
||||
|
||||
// Cancel: the remaining upload is an orphan nobody references.
|
||||
const cancel = el.shadowRoot!.querySelector<HTMLElement>(".dialog-actions ha-button")!;
|
||||
cancel.click();
|
||||
await el.updateComplete;
|
||||
deletes = sent.filter((m) => m.type === "maintenance_supporter/documents/delete");
|
||||
expect(deletes.map((m) => m.doc_id)).to.deep.equal(["doc-photo-1", "doc-photo-2"]);
|
||||
expect(sent.some((m) => m.type === "maintenance_supporter/task/complete")).to.be.false;
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces a 413 as the too-large message and keeps the picker (#161)", async () => {
|
||||
const { el } = await mount();
|
||||
const realFetch = window.fetch;
|
||||
window.fetch = (async () => ({ ok: false, status: 413, json: async () => ({}) }) as Response) as typeof window.fetch;
|
||||
try {
|
||||
pickFiles(el, CAMERA, ["huge.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(0);
|
||||
expect(el.shadowRoot!.querySelector(".error")!.textContent).to.include("large");
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.exist;
|
||||
} finally {
|
||||
window.fetch = realFetch;
|
||||
}
|
||||
|
||||
+26
@@ -213,4 +213,30 @@ describe("documents-section", () => {
|
||||
label.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
expect(clicked, "Enter triggers the hidden file input").to.be.true;
|
||||
});
|
||||
|
||||
it("offers a local filter from 8 documents up and filters tolerantly (#171)", async () => {
|
||||
const many = Array.from({ length: 9 }, (_, i) => ({
|
||||
...FILE_DOC, id: `f${i}`, title: i === 4 ? "Garantie Spülmaschine" : `Rechnung ${i}`, filename: `r${i}.pdf`, tags: i === 4 ? ["warranty"] : ["invoice"],
|
||||
}));
|
||||
const { el } = await mount(true, many);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".doc-filter input");
|
||||
expect(input, "filter box shown").to.exist;
|
||||
expect(el.shadowRoot!.querySelectorAll(".doc-row").length).to.equal(9);
|
||||
input!.value = "spuel garant";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const rows = [...el.shadowRoot!.querySelectorAll(".doc-row")];
|
||||
expect(rows.length).to.equal(1);
|
||||
expect(rows[0].textContent).to.contain("Garantie Spülmaschine");
|
||||
input!.value = "nichts";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelector(".doc-empty")!.textContent).to.contain("No matching documents");
|
||||
expect(el.shadowRoot!.querySelector("h3")!.textContent, "count stays the full count").to.contain("(9)");
|
||||
});
|
||||
|
||||
it("keeps the list uncluttered below 8 documents — no filter box", async () => {
|
||||
const { el } = await mount();
|
||||
expect(el.shadowRoot!.querySelector(".doc-filter")).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { setViewport } from "@web/test-runner-commands";
|
||||
import { mountPanel, obj, resetTaskSeq, sr, task } from "./_panel-utils.js";
|
||||
describe("panel shell", () => {
|
||||
beforeEach(() => {
|
||||
@@ -156,6 +157,63 @@ describe("panel shell", () => {
|
||||
expect(textOf(overdueSection)).to.include("Late");
|
||||
});
|
||||
|
||||
it("Today rows name the responsible person, except under the person filter (#169)", async () => {
|
||||
localStorage.setItem("msp-overview-tab", "today");
|
||||
const { el } = await mountPanel(
|
||||
[
|
||||
obj("e1", [
|
||||
task({ name: "Assigned", status: "overdue", days_until_due: -1, responsible_user_id: "u-anna" }),
|
||||
task({ name: "Nobody", status: "overdue", days_until_due: -2 }),
|
||||
]),
|
||||
],
|
||||
{ "maintenance_supporter/users/list": () => ({ users: [{ id: "u-anna", name: "Anna" }] }) },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const rowOf = (name: string) =>
|
||||
[...sr(el).querySelectorAll(".today-row")].find((r) => r.querySelector(".today-task")?.textContent?.trim() === name)!;
|
||||
expect(rowOf("Assigned").querySelector(".today-person")!.textContent).to.include("Anna");
|
||||
expect(rowOf("Nobody").querySelector(".today-person")).to.equal(null);
|
||||
|
||||
// Filtering by that person makes the chip redundant — every row is hers.
|
||||
(el as unknown as { _filterUser: string | null })._filterUser = "u-anna";
|
||||
await el.updateComplete;
|
||||
expect(sr(el).querySelector(".today-person")).to.equal(null);
|
||||
});
|
||||
|
||||
it("Today person chip stays readable on a phone next to a long object name (#169)", async () => {
|
||||
localStorage.setItem("msp-overview-tab", "today");
|
||||
await setViewport({ width: 360, height: 800 });
|
||||
try {
|
||||
const { el } = await mountPanel(
|
||||
[
|
||||
obj("e1", [task({ name: "Descale and clean the filter thoroughly", status: "overdue", days_until_due: -248, responsible_user_id: "u-max" })],
|
||||
"Dishwasher in the basement utility room 12345"),
|
||||
],
|
||||
{ "maintenance_supporter/users/list": () => ({ users: [{ id: "u-max", name: "Maximiliane Schneider-Hoffmann" }] }) },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const line = sr(el).querySelector<HTMLElement>(".today-object")!;
|
||||
const chip = line.querySelector<HTMLElement>(".today-person")!;
|
||||
const lineBox = line.getBoundingClientRect();
|
||||
const chipBox = chip.getBoundingClientRect();
|
||||
// Narrow: the avatar alone (initials in the member's colour), fully inside the line.
|
||||
const avatar = chip.querySelector<HTMLElement>(".person-avatar")!;
|
||||
expect(avatar.textContent!.trim()).to.equal("MS");
|
||||
expect(getComputedStyle(chip.querySelector(".person-name")!).display, "name hidden on narrow").to.equal("none");
|
||||
const visible = Math.min(chipBox.right, lineBox.right) - Math.max(chipBox.left, lineBox.left);
|
||||
expect(visible, "avatar visible inside the line").to.be.greaterThan(16);
|
||||
expect(chipBox.right, "chip does not overflow the line").to.be.at.most(lineBox.right + 1);
|
||||
// The object text gave way (ellipsis), not the person.
|
||||
const text = line.querySelector<HTMLElement>(".today-object-text")!;
|
||||
expect(text.scrollWidth).to.be.greaterThan(text.clientWidth);
|
||||
expect(chip.getAttribute("title")).to.equal("Maximiliane Schneider-Hoffmann");
|
||||
} finally {
|
||||
await setViewport({ width: 1280, height: 800 });
|
||||
}
|
||||
});
|
||||
|
||||
it("virtualizes the table above the threshold and moves the window on scroll", async () => {
|
||||
const many = Array.from({ length: 150 }, (_, i) =>
|
||||
task({ name: `Bulk ${String(i).padStart(3, "0")}`, days_until_due: (i % 40) + 1 }),
|
||||
|
||||
+40
-1
@@ -21,6 +21,7 @@ function mockHass(opts: {
|
||||
vacationStart?: string | null;
|
||||
vacationEnd?: string | null;
|
||||
exemptIds?: string[];
|
||||
previewRows?: Array<Record<string, unknown>>;
|
||||
} = {}) {
|
||||
const settingsResponse = {
|
||||
...DEFAULT_SETTINGS_RESPONSE,
|
||||
@@ -51,7 +52,7 @@ function mockHass(opts: {
|
||||
"maintenance_supporter/vacation/end_now": () => ({
|
||||
...settingsResponse.vacation, enabled: false, is_active: false,
|
||||
}),
|
||||
"maintenance_supporter/vacation/preview": () => ({ rows: [], window_end: null }),
|
||||
"maintenance_supporter/vacation/preview": () => ({ rows: opts.previewRows ?? [], window_end: null }),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -144,4 +145,42 @@ describe("settings-view vacation section", () => {
|
||||
const endNow = vacationSection(el)!.querySelector(".vac-end-now");
|
||||
expect(endNow, "no end-now in default state").to.not.exist;
|
||||
});
|
||||
it("the preview hides Skip for a task whose skip rule is off (#150 rule in the preview)", async () => {
|
||||
// Bug review 2026-09-04: every time-based row offered Skip — the
|
||||
// backend then rejected it for allow_skip=false tasks.
|
||||
const row = (task_id: string, extra: Record<string, unknown>) => ({
|
||||
task_id,
|
||||
entry_id: "e1",
|
||||
object_name: "Pool",
|
||||
task_name: task_id,
|
||||
kind: "time_based",
|
||||
confidence: "deterministic",
|
||||
events: [{ date: "2099-06-12", status: "due_soon" }],
|
||||
will_suppress: true,
|
||||
...extra,
|
||||
});
|
||||
const { el } = await mount({
|
||||
vacationStart: "2099-06-10",
|
||||
vacationEnd: "2099-06-20",
|
||||
previewRows: [
|
||||
row("Open", { allow_skip: true }),
|
||||
row("Locked", { allow_skip: false }),
|
||||
row("Legacy", {}),
|
||||
row("Sensor", { kind: "sensor_based", allow_skip: true }),
|
||||
],
|
||||
});
|
||||
const section = vacationSection(el)!;
|
||||
section.querySelector<HTMLButtonElement>(".vac-preview-toolbar button")!.click();
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const rows = Array.from(section.querySelectorAll(".vac-preview-row"));
|
||||
expect(rows.length).to.equal(4);
|
||||
const skipLabel = rows[0].querySelectorAll(".vac-preview-actions button")[1].textContent!.trim();
|
||||
const hasSkip = (r: Element) =>
|
||||
Array.from(r.querySelectorAll(".vac-preview-actions button")).some((b) => b.textContent!.trim() === skipLabel);
|
||||
expect(hasSkip(rows[0]), "allow_skip=true keeps Skip").to.be.true;
|
||||
expect(hasSkip(rows[1]), "allow_skip=false hides Skip").to.be.false;
|
||||
expect(hasSkip(rows[2]), "absent field = allowed (older backend)").to.be.true;
|
||||
expect(hasSkip(rows[3]), "sensor rows never had Skip").to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
+11
@@ -125,4 +125,15 @@ describe("storage-section-card", () => {
|
||||
expect(el.shadowRoot!.querySelector(".obj-name")!.textContent!.trim()).to.equal("01234567");
|
||||
expect(el.shadowRoot!.querySelector(".obj-row.clickable"), "unknown object not clickable").to.not.exist;
|
||||
});
|
||||
|
||||
it("shows how much of the library the full-text search can see (#171)", async () => {
|
||||
const el = await mount({ ...SUMMARY, search_index: { total: 12, indexed: 9, no_text: 2, unsupported: 1, pending: 0 } });
|
||||
await expand(el);
|
||||
const status = el.shadowRoot!.querySelector(".index-status")!;
|
||||
expect(status.textContent!.replace(/\s+/g, " ")).to.contain("9 of 12 files indexed");
|
||||
expect(status.textContent).to.contain("3 without text layer");
|
||||
const none = await mount({ ...SUMMARY, search_index: { total: 0, indexed: 0, no_text: 0, unsupported: 0, pending: 0 } });
|
||||
await expand(none);
|
||||
expect(none.shadowRoot!.querySelector(".index-status"), "no line without files").to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
+58
-2
@@ -13,8 +13,8 @@ import "../components/task-dialog.js";
|
||||
import type { MaintenanceTaskDialog } from "../components/task-dialog";
|
||||
import { createMockHass } from "./_test-utils.js";
|
||||
|
||||
async function mountCreate(states: Record<string, unknown>): Promise<MaintenanceTaskDialog> {
|
||||
const { hass } = createMockHass({ states });
|
||||
async function mountCreate(states: Record<string, unknown>, language = "en"): Promise<MaintenanceTaskDialog> {
|
||||
const { hass } = createMockHass({ states, language });
|
||||
const el = await fixture<MaintenanceTaskDialog>(html`
|
||||
<maintenance-task-dialog .hass=${hass}></maintenance-task-dialog>
|
||||
`);
|
||||
@@ -59,6 +59,37 @@ describe("task-dialog live trigger hint", () => {
|
||||
expect(text).to.include("1.5 bar");
|
||||
});
|
||||
|
||||
it("formats the figures in the user's number format (bug review 2026-09-04)", async () => {
|
||||
// Decimal comma + thousands separators for a de user — like the entity
|
||||
// card next to the dialog; the hint used raw String(n).
|
||||
const el = await mountCreate(
|
||||
{
|
||||
"sensor.odometer": { state: "100000", attributes: { unit_of_measurement: "km" } },
|
||||
"sensor.pressure": { state: "1.25", attributes: { unit_of_measurement: "bar" } },
|
||||
},
|
||||
"de",
|
||||
);
|
||||
(el as any)._triggerEntityId = "sensor.odometer";
|
||||
(el as any)._triggerEntityIds = ["sensor.odometer"];
|
||||
(el as any)._triggerType = "counter";
|
||||
(el as any)._triggerDeltaMode = true;
|
||||
(el as any)._triggerTargetValue = "15000";
|
||||
await el.updateComplete;
|
||||
let text = hint(el)!;
|
||||
expect(text).to.include("100.000 km");
|
||||
expect(text).to.include("115.000 km");
|
||||
expect(text).to.include("15.000 km");
|
||||
|
||||
(el as any)._triggerEntityId = "sensor.pressure";
|
||||
(el as any)._triggerEntityIds = ["sensor.pressure"];
|
||||
(el as any)._triggerType = "threshold";
|
||||
(el as any)._triggerAbove = "1.5";
|
||||
await el.updateComplete;
|
||||
text = hint(el)!;
|
||||
expect(text).to.include("1,3 bar"); // 1.25 rounded to one decimal, decimal comma
|
||||
expect(text).to.include("1,5 bar");
|
||||
});
|
||||
|
||||
it("renders nothing without a bound entity or without targets", async () => {
|
||||
const el = await mountCreate({
|
||||
"sensor.pressure": { state: "1.2", attributes: {} },
|
||||
@@ -137,3 +168,28 @@ describe("task-dialog live trigger hint", () => {
|
||||
expect(text).to.include("100 h");
|
||||
});
|
||||
});
|
||||
|
||||
describe("task-dialog state-change latch hint (#167)", () => {
|
||||
const latchHint = (el: MaintenanceTaskDialog): boolean =>
|
||||
[...el.shadowRoot!.querySelectorAll(".field-help")].some((n) => /latch/i.test(n.textContent || ""));
|
||||
|
||||
it("explains the latch (incl. the from-only recovery) for a single transition with a pattern", async () => {
|
||||
const el = await mountCreate({ "sensor.dock_error": { state: "ok", attributes: {} } });
|
||||
(el as any)._triggerEntityId = "sensor.dock_error";
|
||||
(el as any)._triggerEntityIds = ["sensor.dock_error"];
|
||||
(el as any)._triggerType = "state_change";
|
||||
(el as any)._triggerFromState = "ok";
|
||||
(el as any)._triggerTargetChanges = "1";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "from-only, target 1").to.equal(true);
|
||||
|
||||
(el as any)._triggerTargetChanges = "3";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "a counter is no latch").to.equal(false);
|
||||
|
||||
(el as any)._triggerTargetChanges = "";
|
||||
(el as any)._triggerFromState = "";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "no pattern = no latch semantics").to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,4 +183,17 @@ describe("task-documents in part mode", () => {
|
||||
const { el } = await mountPart([P_LINKED]);
|
||||
expect(el.shadowRoot!.querySelector(".tdoc-page")).to.not.exist;
|
||||
});
|
||||
|
||||
it("offers a local filter once 8+ documents are linked (#171)", async () => {
|
||||
const many = Array.from({ length: 8 }, (_, i) => ({ ...LINKED, id: `l${i}`, title: i === 2 ? "Schaltplan" : `Manual ${i}`, filename: `m${i}.pdf` }));
|
||||
const { el } = await mount(true, many);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".doc-filter input");
|
||||
expect(input, "filter box shown").to.exist;
|
||||
input!.value = "schaltpl";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const rows = [...el.shadowRoot!.querySelectorAll(".tdoc-row")];
|
||||
expect(rows.length).to.equal(1);
|
||||
expect(rows[0].textContent).to.contain("Schaltplan");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user