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

252 lines
9.1 KiB
JavaScript

/**
* Screenshot the new interactive section cards (Vacation/Budget/Groups)
* + the existing task quick-actions dialog (after the button-row fix)
* + the complete-dialog (to investigate the user's "fields are missing" report).
*
* Creates a temporary Lovelace dashboard with all three cards, navigates,
* captures shots in light + dark mode at desktop width.
*/
import { chromium } from "playwright";
import { promises as fs } from "fs";
import http from "http";
import path from "path";
const HA = "http://localhost:8125";
const OUT = path.resolve("../../../screenshots-section-cards");
async function getRefreshToken() {
function post(p, body, ct = "application/json") {
return new Promise((resolve, reject) => {
const data = typeof body === "string" ? body : JSON.stringify(body);
const opts = {
hostname: "localhost", port: 8125, path: p, method: "POST",
headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(data) },
};
const req = http.request(opts, (r) => {
let d = "";
r.on("data", (c) => (d += c));
r.on("end", () => resolve(JSON.parse(d)));
});
req.on("error", reject);
req.write(data);
req.end();
});
}
const flow = await post("/auth/login_flow", {
client_id: HA + "/", handler: ["homeassistant", null], redirect_uri: HA + "/",
});
const auth = await post(`/auth/login_flow/${flow.flow_id}`, {
client_id: HA + "/", username: "dev", password: "dev",
});
const tok = await post(
"/auth/token",
`grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`,
"application/x-www-form-urlencoded",
);
return tok.refresh_token;
}
async function ws(page, cmd) {
return page.evaluate(async (c) => {
const ha = document.querySelector("home-assistant");
if (!ha?.hass?.connection) throw new Error("no hass connection");
return await ha.hass.connection.sendMessagePromise(c);
}, cmd);
}
async function ensureDashboard(page) {
const dashboards = await ws(page, { type: "lovelace/dashboards/list" });
const slug = "section-cards-test";
const existing = dashboards.find((d) => d.url_path === slug);
if (!existing) {
await ws(page, {
type: "lovelace/dashboards/create",
url_path: slug,
title: "Section Cards Test",
icon: "mdi:test-tube",
mode: "storage",
show_in_sidebar: false,
require_admin: false,
});
}
// Save the lovelace config with our 3 cards
await ws(page, {
type: "lovelace/config/save",
url_path: slug,
config: {
views: [
{
title: "Sections",
path: "default",
icon: "mdi:view-dashboard",
cards: [
{ type: "custom:maintenance-vacation-section-card" },
{ type: "custom:maintenance-budget-section-card" },
{ type: "custom:maintenance-groups-section-card" },
],
},
{
title: "Card",
path: "card",
icon: "mdi:card-text",
cards: [
{ type: "custom:maintenance-supporter-card", show_actions: true, max_items: 6 },
],
},
],
},
});
return slug;
}
async function shot(page, name) {
await page.waitForTimeout(800);
const file = path.join(OUT, name + ".png");
await page.screenshot({ path: file, fullPage: false });
console.log(` 📸 ${name}.png`);
}
async function setColorScheme(page, scheme) {
// Force HA's theme by injecting localStorage entry + reloading
await page.evaluate((s) => {
localStorage.setItem("selectedTheme", JSON.stringify({ dark: s === "dark" }));
}, scheme);
}
async function main() {
await fs.mkdir(OUT, { recursive: true });
console.log("Getting refresh token…");
const refreshToken = await getRefreshToken();
const browser = await chromium.launch({ headless: true });
for (const colorScheme of ["dark", "light"]) {
console.log(`\n=== ${colorScheme} mode ===`);
const ctx = await browser.newContext({
viewport: { width: 1280, height: 900 },
colorScheme,
ignoreHTTPSErrors: true,
});
const page = await ctx.newPage();
page.on("pageerror", (e) => console.log("PAGE ERROR:", e.message));
page.on("console", (m) => {
if (m.type() === "error") console.log("CONSOLE:", m.text());
});
await page.goto(HA);
await page.waitForTimeout(800);
await page.evaluate((args) => {
localStorage.setItem("hassTokens", JSON.stringify({
hassUrl: args.ha, clientId: args.ha + "/",
refresh_token: args.r, access_token: "",
token_type: "Bearer", expires_in: 1800, expires: 0,
}));
localStorage.setItem("selectedTheme", JSON.stringify({ dark: args.cs === "dark" }));
}, { ha: HA, r: refreshToken, cs: colorScheme });
await page.goto(HA);
await page.waitForTimeout(3500);
const slug = await ensureDashboard(page);
await page.goto(`${HA}/${slug}/default`);
await page.waitForTimeout(4000);
await shot(page, `sections-${colorScheme}`);
// Card view — click a task row to open quick-actions dialog
await page.goto(`${HA}/${slug}/card`);
await page.waitForTimeout(3500);
await shot(page, `card-${colorScheme}`);
// Find a task row that's likely to have adaptive data (Oil Change has the
// longest history in the demo dataset). Falls back to first row.
const taskClicked = await page.evaluate(() => {
function findInAllShadows(root, sel) {
if (!root) return null;
if (root.querySelector?.(sel)) return root.querySelector(sel);
for (const el of root.querySelectorAll?.("*") || []) {
if (el.shadowRoot) {
const found = findInAllShadows(el.shadowRoot, sel);
if (found) return found;
}
}
return null;
}
const card = findInAllShadows(document, "maintenance-supporter-card");
if (!card?.shadowRoot) return "no card";
const rows = Array.from(card.shadowRoot.querySelectorAll(".task-item.clickable"));
if (rows.length === 0) return "no rows";
// Prefer "Filter Replacement" (HVAC) — has interval recommendation,
// then "Oil Change" (Family Car) — has sensor + long history.
const filterReplace = rows.find((r) => /filter replacement|filter wechsel/i.test(r.textContent || ""));
const oilChange = rows.find((r) => /oil change|ölwechsel/i.test(r.textContent || ""));
const target = filterReplace || oilChange || rows[0];
target.click();
return "clicked: " + (target.querySelector(".task-name")?.textContent || "?");
});
console.log(` Task row click: ${taskClicked}`);
if (taskClicked.startsWith("clicked")) {
// Wait for both /object load AND /settings (features) load — the stats
// toggle only renders after features arrive
await page.waitForTimeout(4000);
await shot(page, `quick-actions-${colorScheme}`);
// Click the 2nd link (= "Show stats + graphs")
const statsClick = await page.evaluate(() => {
const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog");
const links = dlg?.shadowRoot?.querySelectorAll(".details-toggle button.link") || [];
if (links.length < 2) return `only ${links.length} link(s)`;
links[1].click();
return "clicked: " + (links[1].textContent || "").trim();
});
console.log(` Stats toggle: ${statsClick}`);
await page.waitForTimeout(1500);
await shot(page, `quick-actions-stats-${colorScheme}`);
// Capture the graphs section — scroll dialog and shoot again
await page.evaluate(() => {
const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog");
const inner = dlg?.shadowRoot?.querySelector(".dialog");
if (inner) inner.scrollTop = Math.max(0, inner.scrollHeight - 700);
}).catch(() => {});
await page.waitForTimeout(400);
await shot(page, `quick-actions-stats-bottom-${colorScheme}`);
// Restore scroll for complete dialog test
await page.evaluate(() => {
const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog");
const inner = dlg?.shadowRoot?.querySelector(".dialog");
if (inner) inner.scrollTop = 0;
}).catch(() => {});
await page.waitForTimeout(300);
const completeClicked = await page.evaluate(() => {
const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog");
if (!dlg?.shadowRoot) return "no quick-actions dialog on body";
const btns = dlg.shadowRoot.querySelectorAll(".actions.primary-row button");
// The first button in primary-row IS Complete
if (btns.length === 0) return "no buttons in primary-row";
btns[0].click();
return "clicked: " + (btns[0].textContent || "").trim();
});
console.log(` Complete click: ${completeClicked}`);
await page.waitForTimeout(2000);
await shot(page, `complete-dialog-${colorScheme}`);
// Close everything
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
}
await ctx.close();
}
await browser.close();
console.log(`\nDone. Screenshots in ${OUT}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});