225 lines
8.0 KiB
JavaScript
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,
|
|
);
|