2026-09-23 12:27:35 -04:00
|
|
|
/**
|
|
|
|
|
* Render the built quest-log card under jsdom and report its location.
|
|
|
|
|
*
|
|
|
|
|
* Usage: node render-quest-log.mjs < spec.json
|
|
|
|
|
*
|
|
|
|
|
* The spec JSON carries {bundle, url, config, hass, idle_ms?, probe_ms?};
|
|
|
|
|
* the card is mounted at the given URL (it resolves its child from the
|
|
|
|
|
* pathname's last segment), after the optional probe wait one snapshot of
|
|
|
|
|
* the render is taken mid-wait, then after the optional idle wait stdout
|
2026-09-26 09:02:07 -04:00
|
|
|
* receives one JSON object {location, headline, countdown, html, tiles,
|
2026-09-26 10:22:01 -04:00
|
|
|
* probe, tap} — the final window pathname, the rendered title, the
|
2026-09-26 09:02:07 -04:00
|
|
|
* complete-screen countdown text, the rendered shadow DOM, each quest
|
2026-09-26 10:22:01 -04:00
|
|
|
* card's icon tile, and the probe snapshot when probe_ms was given. With
|
|
|
|
|
* tap_complete: <instance id> the harness taps that card's Complete button,
|
|
|
|
|
* records whether the confirm dialog opened, taps the dialog's Complete, and
|
|
|
|
|
* reports every hass.callService call as tap: {dialog_opened, service_calls}.
|
2026-09-27 13:01:53 -04:00
|
|
|
* With tap_return: true the harness taps the header's return control and
|
|
|
|
|
* reports it as returned: {rendered, label} before the final snapshot.
|
2026-09-26 10:22:01 -04:00
|
|
|
* The Python panel tests drive this harness so the
|
2026-09-23 12:27:35 -04:00
|
|
|
* assertions run against the exact bundle HACS ships.
|
|
|
|
|
*/
|
|
|
|
|
import { readFileSync } from "node:fs";
|
|
|
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
|
import { JSDOM } from "jsdom";
|
|
|
|
|
|
|
|
|
|
const spec = JSON.parse(readFileSync(0, "utf8"));
|
|
|
|
|
|
|
|
|
|
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
|
|
|
|
url: spec.url ?? "http://homeassistant.local/nestquest/ada",
|
|
|
|
|
pretendToBeVisual: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Expose the jsdom globals the Lit bundle expects. Node's own intrinsics
|
|
|
|
|
// (Object, Array, ...) are kept — only what globalThis lacks is copied.
|
|
|
|
|
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
|
|
|
|
if (!(key in globalThis)) {
|
|
|
|
|
try {
|
|
|
|
|
globalThis[key] = dom.window[key];
|
|
|
|
|
} catch {
|
|
|
|
|
// jsdom getters may throw on access; the card does not need them.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
globalThis.window = dom.window;
|
|
|
|
|
// The card navigates by dispatching new Event("location-changed") on this
|
|
|
|
|
// window; jsdom's EventTarget rejects Node's own global Event class, so the
|
|
|
|
|
// jsdom Event must shadow it for the return navigation to be delivered.
|
|
|
|
|
globalThis.Event = dom.window.Event;
|
|
|
|
|
|
|
|
|
|
await import(pathToFileURL(spec.bundle).href);
|
|
|
|
|
|
2026-09-26 10:22:01 -04:00
|
|
|
const serviceCalls = [];
|
|
|
|
|
const hass = {
|
|
|
|
|
...spec.hass,
|
|
|
|
|
callService(domain, service, data) {
|
|
|
|
|
serviceCalls.push({ domain, service, data });
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-23 12:27:35 -04:00
|
|
|
const element = document.createElement("nestquest-quest-log-card");
|
|
|
|
|
element.setConfig(spec.config);
|
2026-09-26 10:22:01 -04:00
|
|
|
element.hass = hass;
|
2026-09-23 12:27:35 -04:00
|
|
|
document.body.appendChild(element);
|
|
|
|
|
await element.updateComplete;
|
|
|
|
|
|
2026-09-26 10:22:01 -04:00
|
|
|
let tap = null;
|
|
|
|
|
if (spec.tap_complete !== undefined) {
|
|
|
|
|
element.shadowRoot
|
|
|
|
|
.querySelector(`.quest[data-instance-id="${spec.tap_complete}"] button.complete`)
|
|
|
|
|
.click();
|
|
|
|
|
await element.updateComplete;
|
|
|
|
|
const confirm = element.shadowRoot.querySelector("button.do-complete");
|
|
|
|
|
confirm?.click();
|
|
|
|
|
await element.updateComplete;
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
|
tap = { dialog_opened: confirm !== null, service_calls: serviceCalls };
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-27 13:01:53 -04:00
|
|
|
let returned = null;
|
|
|
|
|
if (spec.tap_return) {
|
|
|
|
|
const control = element.shadowRoot.querySelector(".header button.return");
|
|
|
|
|
returned = {
|
|
|
|
|
rendered: control !== null,
|
|
|
|
|
label: control?.textContent?.trim() ?? "",
|
|
|
|
|
};
|
|
|
|
|
control?.click();
|
|
|
|
|
await element.updateComplete;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 12:27:35 -04:00
|
|
|
const snapshot = () => ({
|
|
|
|
|
location: window.location.pathname,
|
|
|
|
|
headline: element.shadowRoot.querySelector(".title")?.textContent?.trim() ?? "",
|
|
|
|
|
countdown:
|
|
|
|
|
element.shadowRoot.querySelector(".complete-countdown")?.textContent?.trim() ??
|
|
|
|
|
"",
|
|
|
|
|
html: element.shadowRoot.innerHTML,
|
2026-09-26 09:02:07 -04:00
|
|
|
// Each quest card's icon tile: the registry glyph it rendered (its
|
|
|
|
|
// data-icon key and svg fill), its text (an emoji), and whether it shows
|
|
|
|
|
// the static star (the star's polygon).
|
|
|
|
|
tiles: [...element.shadowRoot.querySelectorAll(".quest")].map((quest) => {
|
|
|
|
|
const tile = quest.querySelector(".tile");
|
|
|
|
|
const glyph = tile?.querySelector("[data-icon]");
|
|
|
|
|
return {
|
|
|
|
|
id: Number(quest.dataset.instanceId),
|
|
|
|
|
sealed: quest.classList.contains("sealed"),
|
|
|
|
|
icon: glyph?.getAttribute("data-icon") ?? null,
|
|
|
|
|
fill: glyph?.getAttribute("fill") ?? null,
|
|
|
|
|
paths: tile?.querySelectorAll("path").length ?? 0,
|
|
|
|
|
text: tile?.textContent?.trim() ?? "",
|
|
|
|
|
star: Boolean(tile?.querySelector("polygon")),
|
|
|
|
|
};
|
|
|
|
|
}),
|
2026-09-23 12:27:35 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let probe = null;
|
|
|
|
|
if (spec.probe_ms !== undefined) {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, spec.probe_ms));
|
|
|
|
|
probe = snapshot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (spec.idle_ms !== undefined) {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, spec.idle_ms));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-27 13:01:53 -04:00
|
|
|
const payload = JSON.stringify({ ...snapshot(), probe, tap, returned });
|
2026-09-23 12:27:35 -04:00
|
|
|
await new Promise((resolve) => process.stdout.write(payload, () => resolve()));
|
|
|
|
|
process.exit(0);
|