Files
2026-07-08 10:43:39 -04:00

293 lines
11 KiB
JavaScript

/**
* E2E test scenarios for issues #30 and #31.
*
* #30: Task without last_performed should anchor next_due on created_at,
* not "today" every refresh.
* #31: Reset prompt strings should make it clear that the date is the
* "last performed" date, not the next due date.
*
* Usage:
* docker compose up -d homeassistant-dev playwright
* docker restart playwright-server
* cd custom_components/maintenance_supporter/frontend-src
* node e2e-issues-30-31.mjs
*/
import { setup, ws, cleanup } from "./e2e-helpers.mjs";
import { readFile } from "fs/promises";
let passed = 0, failed = 0;
function check(name, ok, detail) {
if (ok) { passed++; console.log(" PASS " + name); }
else { failed++; console.log(" FAIL " + name + (detail ? " — " + detail : "")); }
}
async function runTest(name, fn) {
console.log("\n=== " + name + " ===");
try { await fn(); }
catch (e) { failed++; console.log(" ERROR: " + e.message + (e.stack ? "\n" + e.stack : "")); }
}
const { browser, ctx, page } = await setup();
const RUN = `${Date.now().toString(36)}`;
// Helper: create a temp object + task on a fresh entry, return ids.
async function wsLog(cmd) {
try {
return await ws(page, cmd);
} catch (e) {
const detail = e.message + " · cmd=" + JSON.stringify(cmd);
throw new Error(detail);
}
}
async function createScenarioObject(name, taskOptions) {
const created = await wsLog({
type: "maintenance_supporter/object/create", name,
});
const entryId = created.entry_id;
const taskRes = await wsLog({
type: "maintenance_supporter/task/create",
entry_id: entryId,
...taskOptions,
});
return { entryId, taskId: taskRes.task_id };
}
async function readTask(entryId, taskId) {
const obj = await ws(page, { type: "maintenance_supporter/object", entry_id: entryId });
return obj.tasks.find(t => t.id === taskId);
}
async function deleteEntry(entryId) {
try {
await page.evaluate(async (eid) => {
const ha = document.querySelector("home-assistant");
const conn = ha.__hass.connection;
await conn.sendMessagePromise({
type: "config_entries/remove",
entry_id: eid,
});
}, entryId);
} catch (e) {
console.log(" cleanup: ", e.message);
}
}
// =====================================================================
// SCENARIO #30: created_at fallback for tasks without last_performed
// =====================================================================
await runTest("#30 — Task ohne last_performed bekommt created_at=today", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Issue30 A ${RUN}`, {
name: "Daily Task", task_type: "cleaning",
schedule_type: "time_based", interval_days: 1, warning_days: 0, enabled: true,
});
const task = await readTask(entryId, taskId);
console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due} last_performed=${task.last_performed}`);
// The frontend doesn't expose created_at, but we know that with last_performed=null
// and a fresh anchor, days_until_due must equal interval_days (1 here).
check("days_until_due equals interval_days", task.days_until_due === 1,
`got ${task.days_until_due} (expected 1)`);
check("status is OK with warning_days=0", task.status === "ok",
`got ${task.status}`);
await deleteEntry(entryId);
});
await runTest("#30 — Interval=30 ergibt days_until_due=30", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Issue30 B ${RUN}`, {
name: "Monthly Task", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true,
});
const task = await readTask(entryId, taskId);
console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due}`);
check("days_until_due equals interval_days (30)", task.days_until_due === 30,
`got ${task.days_until_due}`);
check("status is OK (not DUE_SOON)", task.status === "ok",
`got ${task.status}`);
await deleteEntry(entryId);
});
await runTest("#30 — Refresh ändert next_due nicht (Anker stabil)", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Issue30 C ${RUN}`, {
name: "Stable Date", task_type: "cleaning",
schedule_type: "time_based", interval_days: 7, enabled: true,
});
const t1 = await readTask(entryId, taskId);
// Force a refresh by sleeping briefly
await page.waitForTimeout(1500);
const t2 = await readTask(entryId, taskId);
check("next_due stable across refreshes", t1.next_due === t2.next_due,
`t1=${t1.next_due}, t2=${t2.next_due}`);
await deleteEntry(entryId);
});
await runTest("#30 — Task mit last_performed nutzt last_performed (nicht created_at)", async () => {
const past = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
const { entryId, taskId } = await createScenarioObject(`E2E Issue30 D ${RUN}`, {
name: "Past LP Task", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30,
last_performed: past, enabled: true,
});
const task = await readTask(entryId, taskId);
// last_performed=60 days ago + interval=30 → next_due was 30 days ago → OVERDUE
check("status is OVERDUE", task.status === "overdue", `got ${task.status}`);
check("days_until_due negative", task.days_until_due < 0, `got ${task.days_until_due}`);
await deleteEntry(entryId);
});
// =====================================================================
// SCENARIO #31: Reset prompt strings clearer
// =====================================================================
await runTest("#31 — Reset-Prompt Label sagt 'Last performed date'", async () => {
// Create a task to reset
const { entryId, taskId } = await createScenarioObject(`E2E Issue31 ${RUN}`, {
name: "Reset Demo", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30, enabled: true,
});
await page.goto("http://homeassistant-dev:8123/maintenance-supporter");
await page.waitForTimeout(3000);
// Verify shipped strings via the built JS file (runs on host fs, not browser).
const builtJs = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN reset_date_optional shipped",
builtJs.includes("Last performed date (optional, defaults to today)"));
check("EN reset_date_prompt shipped",
builtJs.includes("Mark task as performed?"));
check("DE reset_date_optional shipped",
builtJs.includes("Letztes Erledigungs-Datum"));
check("Old confusing string removed",
!builtJs.includes("Reset this task?"),
"old 'Reset this task?' still present");
// Reset via WS using the new label semantics: setting the date sets last_performed.
const resetDate = "2025-01-15";
await ws(page, {
type: "maintenance_supporter/task/reset", entry_id: entryId, task_id: taskId,
date: resetDate,
});
const task = await readTask(entryId, taskId);
check("reset sets last_performed", task.last_performed === resetDate,
`got ${task.last_performed}`);
// next_due = last_performed + interval = 2025-01-15 + 30 = 2025-02-14
check("next_due = last_performed + interval", task.next_due === "2025-02-14",
`got ${task.next_due}`);
await deleteEntry(entryId);
});
// =====================================================================
// EDGE CASES: TZ + lifecycle interaction
// =====================================================================
await runTest("EDGE — Lifecycle: create → complete → next_due aktualisiert", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Edge LC ${RUN}`, {
name: "Lifecycle Task", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true,
});
// Initial state: no last_performed, days_until_due == interval_days
const t1 = await readTask(entryId, taskId);
check("initial days_until_due == interval_days", t1.days_until_due === 30,
`got ${t1.days_until_due}`);
check("initial last_performed is null", t1.last_performed === null,
`got ${t1.last_performed}`);
// Complete via WS
await ws(page, {
type: "maintenance_supporter/task/complete",
entry_id: entryId, task_id: taskId,
});
const t2 = await readTask(entryId, taskId);
check("after complete: last_performed set", t2.last_performed !== null,
`got ${t2.last_performed}`);
check("after complete: days_until_due == interval", t2.days_until_due === 30,
`got ${t2.days_until_due}`);
// History entry written
check("after complete: history has entry", t2.history.length === 1,
`len=${t2.history.length}`);
// Timestamp must be TZ-aware
const ts = t2.history[0].timestamp;
check("history timestamp has TZ suffix",
/[+-]\d{2}:\d{2}$|Z$/.test(ts),
`ts=${ts}`);
await deleteEntry(entryId);
});
await runTest("EDGE — Reset zu vergangenem Datum: last_performed + history", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Edge RST ${RUN}`, {
name: "Reset Edge", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30, enabled: true,
});
const past = "2025-01-15";
await ws(page, {
type: "maintenance_supporter/task/reset",
entry_id: entryId, task_id: taskId, date: past,
});
const t = await readTask(entryId, taskId);
check("reset: last_performed = past date", t.last_performed === past,
`got ${t.last_performed}`);
check("reset: next_due = past + interval (overdue)",
t.next_due === "2025-02-14",
`got ${t.next_due}`);
check("reset: status is overdue", t.status === "overdue",
`got ${t.status}`);
check("reset: history has reset entry",
t.history.some(h => h.type === "reset"),
`types=${t.history.map(h => h.type).join(",")}`);
await deleteEntry(entryId);
});
await runTest("EDGE — Skip via WS: history entry + last_performed bumped", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Edge SKP ${RUN}`, {
name: "Skip Edge", task_type: "cleaning",
schedule_type: "time_based", interval_days: 14, enabled: true,
});
await ws(page, {
type: "maintenance_supporter/task/skip",
entry_id: entryId, task_id: taskId, reason: "user busy",
});
const t = await readTask(entryId, taskId);
check("skip: last_performed set (today)", t.last_performed !== null);
check("skip: history has skip entry",
t.history.some(h => h.type === "skipped"),
`types=${t.history.map(h => h.type).join(",")}`);
check("skip: next_due == today + interval (14)",
t.days_until_due === 14,
`got ${t.days_until_due}`);
await deleteEntry(entryId);
});
await runTest("EDGE — Update last_performed setzt next_due neu", async () => {
const { entryId, taskId } = await createScenarioObject(`E2E Edge UPD ${RUN}`, {
name: "Update Edge", task_type: "cleaning",
schedule_type: "time_based", interval_days: 30, enabled: true,
});
const past = "2024-06-01";
await ws(page, {
type: "maintenance_supporter/task/update",
entry_id: entryId, task_id: taskId,
last_performed: past,
});
const t = await readTask(entryId, taskId);
check("update: last_performed reflected", t.last_performed === past,
`got ${t.last_performed}`);
check("update: next_due = last + interval",
t.next_due === "2024-07-01",
`got ${t.next_due}`);
await deleteEntry(entryId);
});
// =====================================================================
await cleanup(browser, ctx);
console.log("\n" + "═".repeat(50));
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); }
else console.log("ALL TESTS PASSED!");