252 lines
9.1 KiB
JavaScript
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);
|
|
});
|