Files
HomeAssistantVS/custom_components/maintenance_supporter/frontend-src/capture-v1041-screenshots.mjs
T
2026-07-08 10:43:39 -04:00

251 lines
10 KiB
JavaScript

/**
* Capture screenshots of the user-visible features added between v1.0.22
* and v1.0.41. Assumes:
* - ha-maint is up with seeded demo data (setup_demo.py + seed_history.py)
* - playwright-server is reachable on ws://localhost:3000
* - docker/.env contains HA_TOKEN (or HA_REFRESH_TOKEN is in env)
*
* Writes PNGs into docs/images/ using filenames prefixed with the feature.
*
* Run:
* cd custom_components/maintenance_supporter/frontend-src
* node capture-v1041-screenshots.mjs
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { setup, ws, cleanup } from "./e2e-helpers.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT = path.resolve(__dirname, "../../../docs/images");
fs.mkdirSync(OUTPUT, { recursive: true });
const PANEL_EVAL = `
window._ha=document.querySelector('home-assistant');
window._main=_ha.shadowRoot.querySelector('home-assistant-main');
window._drawer=_main.shadowRoot.querySelector('ha-drawer');
window._resolver=_drawer.querySelector('partial-panel-resolver');
window._custom=_resolver.querySelector('ha-panel-custom');
window._panel=_custom.querySelector('maintenance-supporter-panel');
window._sr=_panel.shadowRoot;
`;
async function screenshot(page, name, opts = {}) {
const file = path.join(OUTPUT, `${name}.png`);
await page.screenshot({ path: file, ...opts });
console.log(` saved ${name}.png`);
}
async function main() {
console.log("== Launch browser + login ==");
const { browser, ctx, page } = await setup({ mobile: false });
console.log("\n== Enable all advanced feature flags ==");
await ws(page, {
type: "maintenance_supporter/global/update",
settings: {
advanced_adaptive_visible: true,
advanced_predictions_visible: true,
advanced_seasonal_visible: true,
advanced_environmental_visible: true,
advanced_budget_visible: true,
advanced_groups_visible: true,
advanced_checklists_visible: true,
advanced_schedule_time_visible: true,
},
});
console.log("\n== Pick a time-based task and enrich it ==");
const objects = (await ws(page, { type: "maintenance_supporter/objects" })).objects;
// Use a time_based task so the schedule_time field actually renders in the dialog
const familyCar = objects.find(o => o.object.name === "Family Car");
const tireTask = familyCar.tasks.find(t => t.name === "Tire Rotation");
console.log(` target task: ${familyCar.object.name} / ${tireTask.name}`);
// Add a checklist + schedule_time so the screenshots have content.
// Note: checklist is sent separately via task/update because schedule_time
// is currently accepted by the WS schema for any schedule_type.
await ws(page, {
type: "maintenance_supporter/task/update",
entry_id: familyCar.entry_id,
task_id: tireTask.id,
checklist: [
"Raise the vehicle with a jack at each wheel",
"Loosen and remove each wheel's lug nuts",
"Rotate tires: front-to-back, same side (FWD) or crosswise (AWD/RWD)",
"Torque lug nuts to manufacturer spec (~110 Nm)",
"Reset TPMS learn mode if equipped",
],
schedule_time: "09:00",
});
// Also give the HVAC filter a checklist so the sensor_based detail view is still
// useful elsewhere — but the primary demo task is now Tire Rotation.
const hvac = objects.find(o => o.object.name === "HVAC System");
const filterTask = hvac.tasks.find(t => t.name.toLowerCase().includes("filter"));
console.log("\n== Give the HVAC filter task manual seasonal overrides ==");
try {
await ws(page, {
type: "maintenance_supporter/task/seasonal_overrides",
entry_id: hvac.entry_id,
task_id: filterTask.id,
overrides: {1: 1.4, 2: 1.3, 7: 0.6, 8: 0.6, 12: 1.5},
});
} catch (e) { console.log(" seasonal skipped:", e?.message); }
console.log("\n== Create a demo group so the groups section renders ==");
try {
await ws(page, {
type: "maintenance_supporter/group/create",
name: "Quarterly Appliances",
description: "All tasks for appliances on a 90-day cycle",
task_refs: [
{ entry_id: familyCar.entry_id, task_id: tireTask.id },
{ entry_id: hvac.entry_id, task_id: filterTask.id },
],
});
} catch (e) { console.log(" group skipped:", e?.message); }
// Force-refresh the panel so the new state shows up
await page.evaluate((fn) => { eval(fn); return window._panel._loadData(); }, PANEL_EVAL);
await page.waitForTimeout(2500);
console.log("\n== (1) panel overview with KPIs, budget bar, status chips ==");
await page.evaluate((fn) => {
eval(fn);
window._panel._view = "overview";
window._panel.requestUpdate();
}, PANEL_EVAL);
await page.waitForTimeout(1500);
await screenshot(page, "overview-v1041");
console.log("\n== (2) settings view with 8 feature toggles ==");
await page.evaluate((fn) => {
eval(fn);
window._panel._view = "overview";
window._panel._overviewTab = "settings";
window._panel.requestUpdate();
}, PANEL_EVAL);
await page.waitForTimeout(1500);
// Scroll down so the 8 feature flags are visible
await page.evaluate((fn) => {
eval(fn);
const sv = window._sr.querySelector("maintenance-settings-view");
if (sv?.shadowRoot) {
const el = sv.shadowRoot.querySelector(".features-section, .features, .feature-flags");
if (el?.scrollIntoView) el.scrollIntoView({ block: "start" });
}
}, PANEL_EVAL);
await page.waitForTimeout(500);
await screenshot(page, "settings-features", { fullPage: true });
console.log("\n== (3) task detail with checklist preview + schedule_time ==");
await page.evaluate(({ eid, tid, fn }) => {
eval(fn);
window._panel._selectedEntryId = eid;
window._panel._selectedTaskId = tid;
window._panel._view = "task";
window._panel.requestUpdate();
}, { eid: familyCar.entry_id, tid: tireTask.id, fn: PANEL_EVAL });
await page.waitForTimeout(1500);
await screenshot(page, "task-detail-checklist-and-time", { fullPage: true });
console.log("\n== (4) task edit dialog with schedule_time + checklist editor ==");
const freshTask = (await ws(page, { type: "maintenance_supporter/object", entry_id: familyCar.entry_id }))
.tasks.find(t => t.id === tireTask.id);
await page.evaluate(({ eid, task, fn }) => {
eval(fn);
window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task);
}, { eid: familyCar.entry_id, task: freshTask, fn: PANEL_EVAL });
await page.waitForTimeout(1200);
await screenshot(page, "task-dialog-new-fields");
// Scroll the dialog to the checklist textarea so it's visible in the shot
await page.evaluate((fn) => {
eval(fn);
const content = window._sr.querySelector("maintenance-task-dialog").shadowRoot.querySelector(".content");
const ta = window._sr.querySelector("maintenance-task-dialog").shadowRoot.querySelector("textarea.checklist-textarea");
if (ta && content) content.scrollTop = Math.max(0, ta.offsetTop - 100);
}, PANEL_EVAL);
await page.waitForTimeout(500);
await screenshot(page, "task-dialog-checklist-editor");
// Close
await page.evaluate((fn) => {
eval(fn);
window._sr.querySelector("maintenance-task-dialog")._open = false;
}, PANEL_EVAL);
await page.waitForTimeout(500);
console.log("\n== (5) object dialog with ha-area-picker ==");
await page.evaluate(({ eid, fn }) => {
eval(fn);
// object-dialog doesn't have an openEdit that takes an id — find the
// "Edit object" entrypoint via the panel's menu
const obj = window._panel._objects.find(o => o.entry_id === eid);
if (obj) {
window._sr.querySelector("maintenance-object-dialog").openEdit(obj.entry_id, obj.object);
}
}, { eid: hvac.entry_id, fn: PANEL_EVAL });
await page.waitForTimeout(1200);
await screenshot(page, "object-dialog-area-picker");
await page.evaluate((fn) => {
eval(fn);
window._sr.querySelector("maintenance-object-dialog")._open = false;
}, PANEL_EVAL);
await page.waitForTimeout(500);
console.log("\n== (6) seasonal overrides dialog (12-month editor) ==");
try {
const seasonalTask = (await ws(page, { type: "maintenance_supporter/object", entry_id: hvac.entry_id }))
.tasks.find(t => t.id === filterTask.id);
await page.evaluate(({ eid, tid, fn }) => {
eval(fn);
window._panel._selectedEntryId = eid;
window._panel._selectedTaskId = tid;
window._panel._view = "task";
window._panel.requestUpdate();
}, { eid: hvac.entry_id, tid: filterTask.id, fn: PANEL_EVAL });
await page.waitForTimeout(1000);
await page.evaluate(({ task, fn }) => {
eval(fn);
window._sr.querySelector("maintenance-seasonal-overrides-dialog")
.open(window._panel._selectedEntryId, window._panel._selectedTaskId, task.adaptive_config?.seasonal_overrides);
}, { task: seasonalTask, fn: PANEL_EVAL });
await page.waitForTimeout(1200);
await screenshot(page, "seasonal-overrides-dialog");
await page.evaluate((fn) => {
eval(fn);
window._sr.querySelector("maintenance-seasonal-overrides-dialog")._open = false;
}, PANEL_EVAL);
} catch (e) { console.log(" seasonal-dialog skipped:", e?.message); }
console.log("\n== (7) localized validation error in dialog ==");
// Open a task dialog, paste an oversized name, hit save
await page.evaluate(({ eid, task, fn }) => {
eval(fn);
window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task);
}, { eid: familyCar.entry_id, task: freshTask, fn: PANEL_EVAL });
await page.waitForTimeout(1000);
await page.evaluate((fn) => {
eval(fn);
const dlg = window._sr.querySelector("maintenance-task-dialog");
dlg._name = "X".repeat(500); // triggers WS vol.Length rejection
dlg.requestUpdate();
}, PANEL_EVAL);
await page.waitForTimeout(300);
await page.evaluate((fn) => {
eval(fn);
window._sr.querySelector("maintenance-task-dialog")._save();
}, PANEL_EVAL);
await page.waitForTimeout(1200);
await screenshot(page, "validation-error-localized");
await cleanup(browser, ctx);
console.log("\n== Done. Screenshots in docs/images/ ==");
}
main().catch((e) => { console.error(e); process.exit(1); });