New apps Added
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
/** Verify the dashboard strategy registers in HA 2026.5+'s frontend.
|
||||
*
|
||||
* Headless Chromium logs into ha-maint, then evaluates window.customStrategies
|
||||
* to confirm our entry is there. Plus tries the actual generate() call to make
|
||||
* sure the WS round-trip works.
|
||||
*
|
||||
* Run after: npm run build && docker restart ha-maint
|
||||
* node verify-strategy.mjs
|
||||
*/
|
||||
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 (long-lived access token).");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// HA expects a *refresh* token in localStorage, not an LLAT. Quick refresh-token
|
||||
// dance: hit /auth/login_flow with username/password.
|
||||
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 consoleErrors = [];
|
||||
page.on("pageerror", (err) => consoleErrors.push(`pageerror: ${err.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") consoleErrors.push(`console.error: ${msg.text()}`);
|
||||
});
|
||||
|
||||
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);
|
||||
await page.waitForTimeout(8000); // let frontend extra modules load
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
|
||||
// 1. window.customStrategies populated?
|
||||
out.customStrategies = (window.customStrategies || []).map((s) => ({
|
||||
type: s.type,
|
||||
strategyType: s.strategyType,
|
||||
name: s.name,
|
||||
}));
|
||||
|
||||
// 2. Custom element registered?
|
||||
out.elementRegistered = !!customElements.get(
|
||||
"ll-strategy-dashboard-maintenance-supporter",
|
||||
);
|
||||
|
||||
// 3. Try the actual generate() call in ALL group_by modes
|
||||
try {
|
||||
const StrategyClass = customElements.get(
|
||||
"ll-strategy-dashboard-maintenance-supporter",
|
||||
);
|
||||
const ha = document.querySelector("home-assistant");
|
||||
if (StrategyClass?.generate && ha?.hass) {
|
||||
for (const mode of ["area", "status", "floor", "due_date"]) {
|
||||
const cfg = await StrategyClass.generate(
|
||||
{ type: "custom:maintenance-supporter", group_by: mode },
|
||||
ha.hass,
|
||||
);
|
||||
out[`generated_${mode}`] = {
|
||||
viewCount: cfg.views?.length ?? 0,
|
||||
viewTitles: (cfg.views || []).map((v) => v.title),
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
out.generateError = String(e);
|
||||
}
|
||||
|
||||
// 4. Editor element registered + setConfig works + dispatches event
|
||||
try {
|
||||
out.editorRegistered = !!customElements.get(
|
||||
"hui-maintenance-supporter-strategy-editor",
|
||||
);
|
||||
if (out.editorRegistered) {
|
||||
const StrategyClass = customElements.get(
|
||||
"ll-strategy-dashboard-maintenance-supporter",
|
||||
);
|
||||
const editor = await StrategyClass.getConfigElement();
|
||||
document.body.appendChild(editor);
|
||||
editor.setConfig({
|
||||
type: "custom:maintenance-supporter",
|
||||
group_by: "status",
|
||||
});
|
||||
const select = editor.querySelector("select");
|
||||
out.editorPreselected = select?.value;
|
||||
let dispatchedConfig = null;
|
||||
editor.addEventListener("config-changed", (e) => {
|
||||
dispatchedConfig = e.detail.config;
|
||||
});
|
||||
select.value = "due_date";
|
||||
select.dispatchEvent(new Event("change"));
|
||||
out.editorDispatched = dispatchedConfig;
|
||||
editor.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
out.editorError = String(e);
|
||||
}
|
||||
|
||||
// 5. v1.8.0 — Header + Sidebar present on Overview view
|
||||
try {
|
||||
const StrategyClass = customElements.get(
|
||||
"ll-strategy-dashboard-maintenance-supporter",
|
||||
);
|
||||
const ha = document.querySelector("home-assistant");
|
||||
if (StrategyClass?.generate && ha?.hass) {
|
||||
const cfg = await StrategyClass.generate(
|
||||
{ type: "custom:maintenance-supporter" },
|
||||
ha.hass,
|
||||
);
|
||||
const overview = cfg.views?.find((v) => v.path === "overview");
|
||||
out.overviewHeader = overview?.header?.card?.type;
|
||||
out.overviewSidebar = overview?.sidebar?.sections?.[0]?.cards?.length ?? 0;
|
||||
out.overviewSidebarVisibility =
|
||||
overview?.sidebar?.visibility?.[0]?.condition;
|
||||
}
|
||||
} catch (e) {
|
||||
out.overviewError = String(e);
|
||||
}
|
||||
|
||||
// 6. v1.8.0 — Section strategy registered + generates
|
||||
try {
|
||||
out.sectionStrategyInRegistry = (window.customStrategies || []).some(
|
||||
(s) => s.type === "maintenance-supporter-section" && s.strategyType === "section",
|
||||
);
|
||||
out.sectionElementRegistered = !!customElements.get(
|
||||
"ll-strategy-section-maintenance-supporter-section",
|
||||
);
|
||||
const SectionClass = customElements.get(
|
||||
"ll-strategy-section-maintenance-supporter-section",
|
||||
);
|
||||
const ha = document.querySelector("home-assistant");
|
||||
if (SectionClass?.generate && ha?.hass) {
|
||||
const sec = await SectionClass.generate(
|
||||
{
|
||||
type: "custom:maintenance-supporter-section",
|
||||
filter_status: ["overdue", "triggered"],
|
||||
title: "Hot list",
|
||||
},
|
||||
ha.hass,
|
||||
);
|
||||
out.sectionGenerated = {
|
||||
type: sec.type,
|
||||
cardCount: sec.cards?.length ?? 0,
|
||||
firstCardType: sec.cards?.[0]?.type,
|
||||
lastCardType: sec.cards?.[sec.cards.length - 1]?.type,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
out.sectionError = String(e);
|
||||
}
|
||||
|
||||
// 7. v1.9.0 — fire-dom-event handler opens dialog IN PLACE (object dialog)
|
||||
// rather than navigating. Detect by checking if the dialog element was
|
||||
// appended to body and is in "open" state.
|
||||
try {
|
||||
// Use HA's REAL fire-dom-event shape — the action config is the event
|
||||
// detail and our payload sits under `.ll_custom`. Dispatching the payload
|
||||
// at the top level (as this verifier used to) tested a shape that never
|
||||
// occurs in production and masked issue #69.
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("ll-custom", {
|
||||
detail: {
|
||||
action: "fire-dom-event",
|
||||
ll_custom: { type: "maintenance-supporter:add-object" },
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
// Give the synchronous mount a tick to settle
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const dlg = document.body.querySelector("maintenance-object-dialog");
|
||||
out.objectDialogMounted = !!dlg;
|
||||
out.objectDialogOpen = !!dlg && dlg.shadowRoot?.querySelector("ha-dialog, dialog, .dialog") != null;
|
||||
// Close it via internal state if possible (don't leave a dialog open)
|
||||
if (dlg) {
|
||||
dlg._open = false;
|
||||
dlg.requestUpdate?.();
|
||||
}
|
||||
} catch (e) {
|
||||
out.fireDomError = String(e);
|
||||
}
|
||||
|
||||
// 8. v1.9.0 — group_by=calendar generates 4 windowed calendar views
|
||||
try {
|
||||
const StrategyClass = customElements.get(
|
||||
"ll-strategy-dashboard-maintenance-supporter",
|
||||
);
|
||||
const ha = document.querySelector("home-assistant");
|
||||
if (StrategyClass?.generate && ha?.hass) {
|
||||
const cfg = await StrategyClass.generate(
|
||||
{ type: "custom:maintenance-supporter", group_by: "calendar" },
|
||||
ha.hass,
|
||||
);
|
||||
out.generated_calendar = {
|
||||
viewCount: cfg.views?.length ?? 0,
|
||||
viewTitles: (cfg.views || []).map((v) => v.title),
|
||||
firstCalendarCard: cfg.views?.[1]?.cards?.[0]?.type,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
out.generateCalendarError = String(e);
|
||||
}
|
||||
|
||||
// 9. v1.9.0 — Calendar card element registered + customCards entry present
|
||||
try {
|
||||
out.calendarCardRegistered = !!customElements.get(
|
||||
"maintenance-supporter-calendar-card",
|
||||
);
|
||||
out.calendarCardInRegistry = (window.customCards || []).some(
|
||||
(c) => c.type === "maintenance-supporter-calendar-card",
|
||||
);
|
||||
} catch (e) {
|
||||
out.calendarError = String(e);
|
||||
}
|
||||
|
||||
// 10. v1.9.1 — Calendar card editor: registers, setConfig works, dispatches
|
||||
// config-changed when user picks a different window_days option.
|
||||
try {
|
||||
out.calendarEditorRegistered = !!customElements.get(
|
||||
"maintenance-supporter-calendar-card-editor",
|
||||
);
|
||||
if (out.calendarEditorRegistered) {
|
||||
const CardClass = customElements.get(
|
||||
"maintenance-supporter-calendar-card",
|
||||
);
|
||||
const editor = CardClass.getConfigElement();
|
||||
document.body.appendChild(editor);
|
||||
editor.setConfig({
|
||||
type: "custom:maintenance-supporter-calendar-card",
|
||||
window_days: 14,
|
||||
});
|
||||
// Wait for first render
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const sel = editor.shadowRoot?.querySelector("#window");
|
||||
out.calendarEditorPreselected = sel?.value;
|
||||
let dispatchedConfig = null;
|
||||
editor.addEventListener("config-changed", (e) => {
|
||||
dispatchedConfig = e.detail.config;
|
||||
});
|
||||
sel.value = "365";
|
||||
sel.dispatchEvent(new Event("change"));
|
||||
out.calendarEditorDispatched = dispatchedConfig;
|
||||
editor.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
out.calendarEditorError = String(e);
|
||||
}
|
||||
|
||||
// 4. HA version
|
||||
const ha = document.querySelector("home-assistant");
|
||||
out.haVersion = ha?.hass?.config?.version;
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
const ours = result.customStrategies.find(
|
||||
(s) => s.type === "maintenance-supporter",
|
||||
);
|
||||
const okRegister = !!ours;
|
||||
const okElement = result.elementRegistered;
|
||||
const okArea = (result.generated_area?.viewCount ?? 0) > 0;
|
||||
const okStatus = (result.generated_status?.viewCount ?? 0) > 0;
|
||||
const okFloor = (result.generated_floor?.viewCount ?? 0) > 0;
|
||||
const okDueDate = (result.generated_due_date?.viewCount ?? 0) > 0;
|
||||
const okEditor =
|
||||
result.editorRegistered === true &&
|
||||
result.editorPreselected === "status" &&
|
||||
result.editorDispatched?.group_by === "due_date";
|
||||
const okHeader = result.overviewHeader === "markdown";
|
||||
const okSidebar =
|
||||
(result.overviewSidebar ?? 0) > 0 &&
|
||||
result.overviewSidebarVisibility === "view_columns";
|
||||
const okSection =
|
||||
result.sectionStrategyInRegistry === true &&
|
||||
result.sectionElementRegistered === true &&
|
||||
(result.sectionGenerated?.cardCount ?? 0) >= 2 &&
|
||||
result.sectionGenerated?.firstCardType === "heading" &&
|
||||
result.sectionGenerated?.lastCardType === "custom:maintenance-supporter-card";
|
||||
const okFireDom = result.objectDialogMounted === true;
|
||||
const okCalendarMode =
|
||||
(result.generated_calendar?.viewCount ?? 0) === 5 && // Overview + 4 windows
|
||||
result.generated_calendar?.firstCalendarCard === "custom:maintenance-supporter-calendar-card";
|
||||
const okCalendarCard =
|
||||
result.calendarCardRegistered === true &&
|
||||
result.calendarCardInRegistry === true;
|
||||
const okCalendarEditor =
|
||||
result.calendarEditorRegistered === true &&
|
||||
result.calendarEditorPreselected === "14" &&
|
||||
result.calendarEditorDispatched?.window_days === 365;
|
||||
|
||||
console.log(
|
||||
"\n=== SUMMARY ===\n" +
|
||||
` HA version: ${result.haVersion}\n` +
|
||||
` customStrategies has our entry: ${okRegister ? "✓" : "✗"}\n` +
|
||||
` custom element registered: ${okElement ? "✓" : "✗"}\n` +
|
||||
` generate(group_by=area): ${okArea ? "✓" : "✗"} (${result.generated_area?.viewCount} views)\n` +
|
||||
` generate(group_by=status): ${okStatus ? "✓" : "✗"} (${result.generated_status?.viewCount} views)\n` +
|
||||
` generate(group_by=floor): ${okFloor ? "✓" : "✗"} (${result.generated_floor?.viewCount} views)\n` +
|
||||
` generate(group_by=due_date): ${okDueDate ? "✓" : "✗"} (${result.generated_due_date?.viewCount} views)\n` +
|
||||
` editor registered + works: ${okEditor ? "✓" : "✗"}\n` +
|
||||
` Overview has KPI header: ${okHeader ? "✓" : "✗"} (${result.overviewHeader})\n` +
|
||||
` Overview has sidebar (large): ${okSidebar ? "✓" : "✗"} (${result.overviewSidebar} cards, visibility=${result.overviewSidebarVisibility})\n` +
|
||||
` Section strategy registered+works: ${okSection ? "✓" : "✗"} (${result.sectionGenerated?.cardCount} cards)\n` +
|
||||
` fire-dom-event opens in-place: ${okFireDom ? "✓" : "✗"} (object dialog mounted=${result.objectDialogMounted})\n` +
|
||||
` generate(group_by=calendar): ${okCalendarMode ? "✓" : "✗"} (${result.generated_calendar?.viewCount} views, first-card=${result.generated_calendar?.firstCalendarCard})\n` +
|
||||
` Calendar card registered+listed: ${okCalendarCard ? "✓" : "✗"} (element=${result.calendarCardRegistered}, customCards=${result.calendarCardInRegistry})\n` +
|
||||
` Calendar editor + works: ${okCalendarEditor ? "✓" : "✗"} (preselect=${result.calendarEditorPreselected}, dispatched=${result.calendarEditorDispatched?.window_days})`,
|
||||
);
|
||||
|
||||
if (consoleErrors.length) {
|
||||
console.log("\n=== BROWSER CONSOLE ERRORS ===");
|
||||
consoleErrors.slice(0, 10).forEach((e) => console.log(` ${e.substring(0, 300)}`));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
process.exit(
|
||||
okRegister &&
|
||||
okElement &&
|
||||
okArea &&
|
||||
okStatus &&
|
||||
okFloor &&
|
||||
okDueDate &&
|
||||
okEditor &&
|
||||
okHeader &&
|
||||
okSidebar &&
|
||||
okSection &&
|
||||
okFireDom &&
|
||||
okCalendarMode &&
|
||||
okCalendarCard &&
|
||||
okCalendarEditor
|
||||
? 0
|
||||
: 1,
|
||||
);
|
||||
Reference in New Issue
Block a user