/** * Capture v1.4.x screenshots: * - object-detail-with-manual.png #43: documentation_url on object header * - task-detail-with-manual.png #43+1.4.1: parent manual link on task page * - settings-notification-title-style.png #44: dropdown * - overview-v142.png refreshed dashboard with v1.4 polish * * Same login + panel-traversal pattern as capture-v131-screenshots.mjs. * * docker compose --profile testing up -d # ensure playwright is up * node capture-v14-screenshots.mjs */ import { chromium } from "playwright"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const HA = "http://homeassistant-dev:8123"; const OUTPUT = path.resolve(__dirname, "../../../docs/images"); fs.mkdirSync(OUTPUT, { recursive: true }); function token() { if (process.env.HA_TOKEN) return process.env.HA_TOKEN; const env = path.resolve(__dirname, "../../../docker/.env"); const lines = fs.readFileSync(env, "utf8").split("\n"); for (const line of lines) { if (line.startsWith("HA_TOKEN=")) return line.split("=")[1].trim(); } throw new Error("HA_TOKEN not found"); } const ACCESS_TOKEN = token(); function panelJS() { return ` var ha = document.querySelector('home-assistant'); var main = ha && ha.shadowRoot && ha.shadowRoot.querySelector('home-assistant-main'); var drawer = main && main.shadowRoot && main.shadowRoot.querySelector('ha-drawer'); var resolver = drawer && drawer.querySelector('partial-panel-resolver'); var custom = resolver && resolver.querySelector('ha-panel-custom'); var p = custom && custom.querySelector('maintenance-supporter-panel'); `; } async function login(page) { await page.goto(HA); await page.waitForTimeout(1000); await page.evaluate((t) => { localStorage.setItem("hassTokens", JSON.stringify({ hassUrl: "http://homeassistant-dev:8123", clientId: "http://homeassistant-dev:8123/", refresh_token: "", access_token: t, token_type: "Bearer", expires_in: 86400, expires: Date.now() + 86400000, })); }, ACCESS_TOKEN); await page.goto(HA + "/maintenance-supporter"); await page.waitForTimeout(8000); } async function setEnglishDark(page) { // The set_user_data round-trip occasionally crashes the page (HA frontend // reloads itself when language changes). Wrap in try/catch — if the page // crashed, the caller will recreate it via the recovery branch below. try { await page.evaluate(async () => { const ha = document.querySelector("home-assistant"); const hass = ha && (ha.__hass || ha.hass); if (hass && hass.connection) { await hass.connection.sendMessagePromise({ type: "frontend/set_user_data", key: "language", value: { language: "en", number_format: "language" }, }); await hass.connection.sendMessagePromise({ type: "frontend/set_user_data", key: "core", value: { selectedTheme: { theme: "default", dark: true } }, }); } }); } catch (e) { console.log(" (language/theme change crashed, recovering)"); return false; } await page.goto(HA + "/maintenance-supporter"); await page.waitForTimeout(8000); return true; } async function shot(page, name) { const filePath = path.join(OUTPUT, name); await page.screenshot({ path: filePath }); console.log(` ${name}`); } // ─── Setup helpers ──────────────────────────────────────────────────── async function getObjects(page) { const raw = await page.evaluate(`{ ${panelJS()} var result = []; if (p && p._objects) { for (var i = 0; i < p._objects.length; i++) { var obj = p._objects[i]; var tasks = []; if (obj.tasks) for (var j = 0; j < obj.tasks.length; j++) { tasks.push({ id: obj.tasks[j].id, name: obj.tasks[j].name }); } result.push({ entry_id: obj.entry_id, name: obj.object.name, tasks: tasks }); } } JSON.stringify(result); }`); return JSON.parse(raw); } async function setObjectDocUrl(page, entryId, url) { // v1.4.0 #43: set documentation_url on the object so the new // manual-link line renders in the object detail header (and via // v1.4.1 also on each task detail page belonging to this object). return await page.evaluate(async ({ entry_id, documentation_url }) => { const ha = document.querySelector("home-assistant"); const hass = ha && (ha.__hass || ha.hass); return await hass.connection.sendMessagePromise({ type: "maintenance_supporter/object/update", entry_id, documentation_url, }); }, { entry_id: entryId, documentation_url: url }); } async function setNotificationTitleStyle(page, style) { // v1.4.0 #44: flip the global setting so the dropdown screenshot // shows it pre-selected (rather than the boring default). return await page.evaluate(async ({ s }) => { const ha = document.querySelector("home-assistant"); const hass = ha && (ha.__hass || ha.hass); return await hass.connection.sendMessagePromise({ type: "maintenance_supporter/global/update", settings: { notification_title_style: s, notifications_enabled: true }, }); }, { s: style }); } // ─── Main ───────────────────────────────────────────────────────────── const browser = await chromium.connect("ws://localhost:3000"); console.log("Connected to Playwright server\n"); const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "en", colorScheme: "dark", }); let page = await ctx.newPage(); console.log("Login + setup …"); await login(page); const langOk = await setEnglishDark(page); if (!langOk) { try { await page.close(); } catch { /* ignore */ } page = await ctx.newPage(); await login(page); } console.log("\nPick a target object …"); const objects = await getObjects(page); const target = objects.find(o => o.name === "HVAC System") || objects.find(o => o.tasks.length > 0) || objects[0]; if (!target) { console.error("No objects in demo"); process.exit(1); } console.log(` using: ${target.name} (${target.tasks.length} tasks)`); console.log("\nSet documentation_url on the object …"); const r1 = await setObjectDocUrl(page, target.entry_id, "https://www.energystar.gov/products/hvac/maintenance-guide.pdf"); console.log(" result:", JSON.stringify(r1)); console.log("\nFlip notification_title_style to object_name + enable notifications …"); const r2 = await setNotificationTitleStyle(page, "object_name"); console.log(" flag result keys:", Object.keys(r2 || {})); // Reload so the panel picks up the changes await page.goto(HA + "/maintenance-supporter"); await page.waitForTimeout(6000); // ─── Screenshot 1: object detail with the new documentation URL link ── console.log("\n1) object-detail-with-manual.png"); await page.evaluate(`{ ${panelJS()} if (p) p._showObject('${target.entry_id}'); }`); await page.waitForTimeout(2500); await shot(page, "object-detail-with-manual.png"); // ─── Screenshot 2: task detail with parent object's manual link ────── console.log("\n2) task-detail-with-manual.png"); if (target.tasks.length > 0) { const task = target.tasks[0]; await page.evaluate(`{ ${panelJS()} if (p) p._showTask('${target.entry_id}', '${task.id}'); }`); await page.waitForTimeout(3500); await shot(page, "task-detail-with-manual.png"); } else { console.log(" skipped — target object has no tasks"); } // ─── Screenshot 3: notification title style dropdown in Settings ───── console.log("\n3) settings-notification-title-style.png"); await page.evaluate(`{ ${panelJS()} if (p) { p._view = 'overview'; p._overviewTab = 'settings'; p.requestUpdate(); } }`); await page.waitForTimeout(3500); // Scroll the notification settings section into view await page.evaluate(`{ ${panelJS()} if (p) { var sv = p.shadowRoot && p.shadowRoot.querySelector('maintenance-settings-view'); if (sv && sv.shadowRoot) { // Try to find the title-style row var rows = sv.shadowRoot.querySelectorAll('.notif-row, .row, [data-key=notification_title_style]'); // Fallback: scroll the settings root into the middle of the viewport var anchor = sv.shadowRoot.querySelector('select[data-key=notification_title_style]') || sv.shadowRoot.querySelector('[data-key=notification_title_style]') || rows[0]; if (anchor) anchor.scrollIntoView({behavior: 'instant', block: 'center'}); } } }`); await page.waitForTimeout(1500); await shot(page, "settings-notification-title-style.png"); // ─── Screenshot 4: refreshed overview (just dashboard tab) ──────────── console.log("\n4) overview-v142.png"); await page.evaluate(`{ ${panelJS()} if (p) { p._view = 'overview'; p._overviewTab = 'dashboard'; p.requestUpdate(); } }`); await page.waitForTimeout(2500); await shot(page, "overview-v142.png"); console.log("\nDone."); await browser.close();