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

260 lines
9.1 KiB
JavaScript

/**
* 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();