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

293 lines
11 KiB
JavaScript

/**
* E2E tests for features that previously had WS endpoints without UI.
*
* Usage:
* docker compose up -d homeassistant-dev playwright
* docker restart playwright-server
* cd custom_components/maintenance_supporter/frontend-src
* node e2e-new-features.mjs
*/
import { setup, ws, cleanup } from "./e2e-helpers.mjs";
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); }
}
const { browser, ctx, page } = await setup();
// =====================================================================
// Batch A: Test-Notification Button in Settings
// =====================================================================
await runTest("Test-Notification: WS endpoint reachable", async () => {
const res = await ws(page, { type: "maintenance_supporter/global/test_notification" });
check("WS response has success field", typeof res.success === "boolean",
`got ${JSON.stringify(res)}`);
// When no notify_service is configured the backend returns success=false
// with a "no_service" message. Either outcome is a valid response —
// what matters is that the endpoint is reachable.
check("WS response has message field", typeof res.message === "string" || res.success,
`got ${JSON.stringify(res)}`);
});
await runTest("Test-Notification: Button renders in Settings view", async () => {
// Navigate to maintenance panel and open settings. In the base panel the
// settings tab is not always pre-rendered; we verify the string keys exist
// in the bundled JS so the button can be rendered when the tab is opened.
const { readFile } = await import("fs/promises");
const js = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN send_test string shipped", js.includes("Send test"));
check("EN test_notification string shipped", js.includes("Test notification"));
check("DE test_notification string shipped", js.includes("Test-Benachrichtigung"));
check("testing state string shipped",
js.includes("Sending\\u2026") || js.includes("Sending…"));
});
// =====================================================================
// Batch B: Re-analyze Button in Task Detail view
// =====================================================================
await runTest("Re-analyze: WS endpoint returns analysis", async () => {
const objs = await ws(page, { type: "maintenance_supporter/objects" });
const obj = objs.objects[0];
const t1 = obj.tasks[0];
if (!t1) { console.log(" SKIP: no task in first object"); return; }
const res = await ws(page, {
type: "maintenance_supporter/task/analyze_interval",
entry_id: obj.entry_id, task_id: t1.id,
});
check("analysis has current_interval field",
"current_interval" in res, `got keys=${Object.keys(res).join(",")}`);
check("analysis has confidence field", typeof res.confidence === "string");
check("analysis has data_points number", typeof res.data_points === "number");
});
await runTest("Re-analyze: Button strings shipped", async () => {
const { readFile } = await import("fs/promises");
const js = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN reanalyze label shipped", js.includes("Re-analyze"));
check("DE reanalyze label shipped", js.includes("Neu analysieren"));
check("reanalyze_result string shipped", js.includes("New analysis"));
check("reanalyze_insufficient_data shipped",
js.includes("Not enough data to produce"));
});
// =====================================================================
// Batch B.2: Environmental Entity Selector (Task Dialog, sensor_based)
// =====================================================================
await runTest("Environmental entity: WS endpoint persists adaptive_config", async () => {
// Create a sensor-based task, then set and clear the env entity.
const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `env ${Date.now().toString(36)}` });
const task = await ws(page, {
type: "maintenance_supporter/task/create",
entry_id: obj.entry_id, name: "env-task",
schedule_type: "sensor_based", interval_days: 30,
trigger_config: { type: "threshold", entity_id: "sensor.dummy", trigger_above: 10 },
enabled: true,
});
// Set
const r1 = await ws(page, {
type: "maintenance_supporter/task/set_environmental_entity",
entry_id: obj.entry_id, task_id: task.task_id,
environmental_entity: "sensor.outdoor_temperature",
environmental_attribute: null,
});
check("set succeeds", r1.success === true, JSON.stringify(r1));
const detail = await ws(page, {
type: "maintenance_supporter/object", entry_id: obj.entry_id,
});
const t = detail.tasks.find(x => x.id === task.task_id);
check("adaptive_config reflects env entity",
t?.adaptive_config?.environmental_entity === "sensor.outdoor_temperature",
`got ${JSON.stringify(t?.adaptive_config)}`);
// Clear
await ws(page, {
type: "maintenance_supporter/task/set_environmental_entity",
entry_id: obj.entry_id, task_id: task.task_id,
environmental_entity: null,
});
// Cleanup (as admin via config_entries)
try {
await page.evaluate(async (eid) => {
const ha = document.querySelector("home-assistant");
await ha.hass.connection.sendMessagePromise({
type: "config_entries/remove", entry_id: eid,
});
}, obj.entry_id);
} catch { /* best-effort */ }
});
await runTest("Environmental entity: dialog strings shipped", async () => {
const { readFile } = await import("fs/promises");
const js = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN environmental_entity label", js.includes("Environmental sensor"));
check("DE environmental_entity label", js.includes("Umgebungs-Sensor"));
check("helper text shipped", js.includes("adjusts the interval"));
});
// =====================================================================
// Batch C: Seasonal Overrides Dialog
// =====================================================================
await runTest("Seasonal overrides: WS roundtrip set + clear", async () => {
const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `seas ${Date.now().toString(36)}` });
const task = await ws(page, {
type: "maintenance_supporter/task/create",
entry_id: obj.entry_id, name: "seas-task",
schedule_type: "time_based", interval_days: 30, enabled: true,
});
// Set overrides for Jan + Jul
const r1 = await ws(page, {
type: "maintenance_supporter/task/seasonal_overrides",
entry_id: obj.entry_id, task_id: task.task_id,
overrides: { 1: 2.0, 7: 0.5 },
});
check("set succeeds", r1.success === true, JSON.stringify(r1));
const detail = await ws(page, {
type: "maintenance_supporter/object", entry_id: obj.entry_id,
});
const t = detail.tasks.find(x => x.id === task.task_id);
const ov = t?.adaptive_config?.seasonal_overrides;
check("adaptive_config has overrides",
ov && (ov["1"] === 2.0 || ov[1] === 2.0) && (ov["7"] === 0.5 || ov[7] === 0.5),
`got ${JSON.stringify(ov)}`);
// Clear
const r2 = await ws(page, {
type: "maintenance_supporter/task/seasonal_overrides",
entry_id: obj.entry_id, task_id: task.task_id,
overrides: {},
});
check("clear succeeds", r2.success === true);
try {
await page.evaluate(async (eid) => {
const ha = document.querySelector("home-assistant");
await ha.hass.connection.sendMessagePromise({
type: "config_entries/remove", entry_id: eid,
});
}, obj.entry_id);
} catch { /* best-effort */ }
});
await runTest("Seasonal overrides: dialog + button strings shipped", async () => {
const { readFile } = await import("fs/promises");
const js = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN edit button", js.includes("Edit seasonal factors"));
check("EN title", js.includes("Seasonal factors (override)"));
check("clear_all string", js.includes("Clear all"));
check("DE title", js.includes("Saisonale Faktoren"));
});
// =====================================================================
// Batch D: Groups CRUD UI
// =====================================================================
await runTest("Groups: WS roundtrip create + update + delete", async () => {
// Need an object + task to reference
const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `grp-obj ${Date.now().toString(36)}` });
const task = await ws(page, {
type: "maintenance_supporter/task/create",
entry_id: obj.entry_id, name: "grp-task",
schedule_type: "time_based", interval_days: 30, enabled: true,
});
// Create group
const create = await ws(page, {
type: "maintenance_supporter/group/create",
name: "E2E Group",
description: "created by e2e",
task_refs: [{ entry_id: obj.entry_id, task_id: task.task_id }],
});
check("group/create returns group_id", typeof create.group_id === "string",
JSON.stringify(create));
const gid = create.group_id;
// Read back
const list = await ws(page, { type: "maintenance_supporter/groups" });
check("group appears in list", !!list.groups[gid],
`got ${Object.keys(list.groups).join(",")}`);
check("task_refs persisted",
list.groups[gid].task_refs.length === 1,
JSON.stringify(list.groups[gid]));
// Update
const update = await ws(page, {
type: "maintenance_supporter/group/update",
group_id: gid,
name: "E2E Group renamed",
task_refs: [],
});
check("group/update succeeds", update.success === true);
const list2 = await ws(page, { type: "maintenance_supporter/groups" });
check("group renamed", list2.groups[gid].name === "E2E Group renamed");
check("task_refs cleared", list2.groups[gid].task_refs.length === 0);
// Delete
const del = await ws(page, {
type: "maintenance_supporter/group/delete", group_id: gid,
});
check("group/delete succeeds", del.success === true);
const list3 = await ws(page, { type: "maintenance_supporter/groups" });
check("group gone", !list3.groups[gid]);
// Cleanup object
try {
await page.evaluate(async (eid) => {
const ha = document.querySelector("home-assistant");
await ha.hass.connection.sendMessagePromise({
type: "config_entries/remove", entry_id: eid,
});
}, obj.entry_id);
} catch { /* best-effort */ }
});
await runTest("Groups: dialog + buttons strings shipped", async () => {
const { readFile } = await import("fs/promises");
const js = await readFile(
new URL("../frontend/maintenance-panel.js", import.meta.url),
"utf-8",
);
check("EN new_group", js.includes("New group"));
check("EN edit_group", js.includes("Edit group"));
check("EN no_groups", js.includes("No groups yet"));
check("EN group_select_tasks", js.includes("Select tasks"));
check("DE new_group", js.includes("Neue Gruppe"));
});
// =====================================================================
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!");