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

225 lines
8.0 KiB
JavaScript

/** Verify v2.1.0 KPI click filtering (Discussion #49 — @byoung79).
*
* Loads the panel, clicks each clickable KPI, asserts the panel's
* filter dropdown reflects the new state and the .stat-item.active class
* lights up correctly.
*/
import { chromium } from "playwright";
const HA = "http://localhost:8125";
const HA_TOKEN = process.env.HA_TOKEN;
if (!HA_TOKEN) {
console.error("Set HA_TOKEN env var.");
process.exit(1);
}
async function getRefreshToken() {
async function post(path, body, ctype = "application/json") {
const data = typeof body === "string" ? body : JSON.stringify(body);
const r = await fetch(`${HA}${path}`, {
method: "POST",
headers: { "Content-Type": ctype },
body: data,
});
return await r.json();
}
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 tokens = await post(
"/auth/token",
`grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`,
"application/x-www-form-urlencoded",
);
return tokens.refresh_token;
}
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage();
const errors = [];
page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`));
const refreshToken = await getRefreshToken();
await page.goto(HA);
await page.waitForTimeout(1000);
await page.evaluate(({ ha, r }) => {
localStorage.setItem(
"hassTokens",
JSON.stringify({
hassUrl: ha,
clientId: `${ha}/`,
refresh_token: r,
access_token: "",
token_type: "Bearer",
expires_in: 1800,
expires: 0,
}),
);
}, { ha: HA, r: refreshToken });
await page.goto(`${HA}/maintenance-supporter`);
await page.waitForTimeout(12000);
if (errors.length) {
console.log("Pre-evaluate page errors:");
errors.slice(0, 5).forEach((e) => console.log(" " + e.substring(0, 300)));
}
// Helper that drills into the panel's shadow root and finds the stat items.
const PANEL_QUERY = `
(function () {
const ha = document.querySelector("home-assistant");
const sr = ha?.shadowRoot
?.querySelector("home-assistant-main")?.shadowRoot
?.querySelector("ha-drawer")
?.querySelector("partial-panel-resolver")
?.querySelector("ha-panel-custom")
?.querySelector("maintenance-supporter-panel")?.shadowRoot;
return sr;
})()
`;
const result = await page.evaluate(async (q) => {
const out = {};
const sr = eval(q);
if (!sr) {
out.error = "panel shadowRoot not found";
return out;
}
// Wrap each step so a single failure doesn't abort the whole probe.
const safe = async (label, fn) => {
try { await fn(); }
catch (e) { out[`${label}_error`] = String(e).substring(0, 150); }
};
const queryItems = () => [...sr.querySelectorAll(".stat-item.clickable")];
const findKpi = (label) =>
queryItems().find((i) =>
i.querySelector(".stat-label")?.textContent?.trim().toLowerCase() ===
label.toLowerCase(),
);
const findTab = (label) =>
[...sr.querySelectorAll(".tab")].find((t) =>
t.textContent?.trim().toLowerCase().includes(label.toLowerCase()),
);
const activeTabLabel = () =>
sr.querySelector(".tab.active")?.textContent?.trim();
out.kpiCount = queryItems().length;
out.kpiLabels = queryItems().map((i) =>
i.querySelector(".stat-label")?.textContent?.trim(),
);
// ── A. From Dashboard tab — overdue click ─────────────────────────────
out.startTab = activeTabLabel();
await safe("A", async () => {
const overdueItem = findKpi("Overdue") || findKpi("Überfällig");
overdueItem.click();
await new Promise((r) => setTimeout(r, 600));
const select = sr.querySelector(".filter-bar select");
out.A_afterOverdueClick_dropdownValue = select?.value;
out.A_afterOverdueClick_overdueActive = findKpi("Overdue")?.classList.contains("active");
out.A_tabAfter = activeTabLabel();
});
// ── B. Click Tasks to clear filter ────────────────────────────────────
await safe("B", async () => {
const tasksItem = findKpi("Tasks") || findKpi("Aufgaben");
tasksItem.click();
await new Promise((r) => setTimeout(r, 600));
const select = sr.querySelector(".filter-bar select");
out.B_afterTasksClick_dropdownValue = select?.value;
out.B_afterTasksClick_overdueActive =
findKpi("Overdue")?.classList.contains("active") ?? null;
});
// ── C. Switch to Calendar tab — are KPIs still visible? ───────────────
await safe("C", async () => {
const calendarTab = findTab("Calendar") || findTab("Kalender");
calendarTab.click();
await new Promise((r) => setTimeout(r, 600));
out.C_tabAfterCalendarClick = activeTabLabel();
out.C_kpisVisibleOnCalendar = queryItems().length > 0;
});
// ── D. From Calendar tab, click Overdue KPI ───────────────────────────
await safe("D", async () => {
const overdueOnCalendar = findKpi("Overdue") || findKpi("Überfällig");
if (!overdueOnCalendar) {
out.D_skipped = "Overdue KPI not visible from Calendar tab";
return;
}
overdueOnCalendar.click();
await new Promise((r) => setTimeout(r, 800));
out.D_tabAfterOverdueFromCalendar = activeTabLabel();
const select = sr.querySelector(".filter-bar select");
out.D_dropdownValue = select?.value;
});
// ── E. Switch to Settings tab — KPIs still visible? ───────────────────
await safe("E", async () => {
const dashTab = findTab("Dashboard") || findTab("Übersicht") || findTab("Overview");
dashTab?.click();
await new Promise((r) => setTimeout(r, 300));
const settingsTab = findTab("Settings") || findTab("Einstellungen");
if (!settingsTab) return;
settingsTab.click();
await new Promise((r) => setTimeout(r, 600));
out.E_tabAfterSettingsClick = activeTabLabel();
out.E_kpisVisibleOnSettings = queryItems().length > 0;
});
return out;
}, PANEL_QUERY);
console.log(JSON.stringify(result, null, 2));
const okFiveKpis = result.kpiCount === 5;
const okOverdueClick =
result.A_afterOverdueClick_dropdownValue === "overdue" &&
result.A_afterOverdueClick_overdueActive === true;
const okTasksClick =
result.B_afterTasksClick_dropdownValue === "" &&
result.B_afterTasksClick_overdueActive === false;
const okKpisOnCalendar = result.C_kpisVisibleOnCalendar === true;
const okFilterFromCalendar =
result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("dashboard") ===
true ||
result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("übersicht") ===
true;
const okKpisOnSettings = result.E_kpisVisibleOnSettings === true;
console.log(
"\n=== SUMMARY ===\n" +
` 5 clickable KPIs (Dashboard tab): ${okFiveKpis ? "✓" : "✗"} (${result.kpiCount})\n` +
` Overdue click → dropdown=overdue: ${okOverdueClick ? "✓" : "✗"}\n` +
` Tasks click clears + dehighlight: ${okTasksClick ? "✓" : "✗"}\n` +
` KPIs visible on Calendar tab: ${okKpisOnCalendar ? "✓" : "✗"} (${result.C_kpisVisibleOnCalendar})\n` +
` KPIs visible on Settings tab: ${okKpisOnSettings ? "✓" : "✗"} (${result.E_kpisVisibleOnSettings})\n` +
` Filter-from-Calendar tab works: ${okFilterFromCalendar ? "✓" : "✗"} (tab-after=${result.D_tabAfterOverdueFromCalendar}, dropdown=${result.D_dropdownValue})`,
);
if (errors.length) {
console.log("\n=== Browser errors ===");
errors.slice(0, 5).forEach((e) => console.log(` ${e.substring(0, 200)}`));
}
await browser.close();
process.exit(
okFiveKpis &&
okOverdueClick &&
okTasksClick &&
okKpisOnCalendar &&
okKpisOnSettings &&
okFilterFromCalendar
? 0
: 1,
);