Assert the panel motion contract and hard rules, not just build them

The confirm dialog (fade + scale .96->1 over --nq-dur-modal/--nq-ease-out),
the seal (fade + scale 1.15->1 over --nq-dur-base), the quest-complete
12 s countdown return and the 40 s idle return already matched
PANEL-SPEC, but nothing pinned them: a token or keyframe edit could
silently drift the panel off the approved mockup timing.

tests/test_panel_cards.py now asserts the entrance timing and easing,
the byte-identical served token copy, and the section 1/6 hard rules
(22px body floor, token-only inks, no hover states, no animation loops,
inert dock strip, whole-plate tap target, control-free away plate), and
the quest-log harness drives the 1c countdown return end-to-end against
the bundle HACS ships.
This commit is contained in:
2026-09-23 11:25:49 -04:00
parent 881f48de43
commit 999597bfd7
2 changed files with 357 additions and 13 deletions
+25 -11
View File
@@ -3,12 +3,15 @@
*
* Usage: node render-quest-log.mjs < spec.json
*
* The spec JSON carries {bundle, url, config, hass, idle_ms?}; the card is
* mounted at the given URL (it resolves its child from the pathname's last
* segment), and after the optional idle wait stdout receives one JSON object
* {location, headline, html} — the window pathname and the rendered title.
* The Python panel tests drive this harness so the assertions run against
* the exact bundle HACS ships.
* 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
* receives one JSON object {location, headline, countdown, html, probe} —
* the final window pathname, the rendered title, the complete-screen
* countdown text, the rendered shadow DOM, and the probe snapshot when
* probe_ms was given. The Python panel tests drive this harness so the
* assertions run against the exact bundle HACS ships.
*/
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
@@ -46,14 +49,25 @@ element.hass = spec.hass;
document.body.appendChild(element);
await element.updateComplete;
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,
});
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));
}
const payload = JSON.stringify({
location: window.location.pathname,
headline: element.shadowRoot.querySelector(".title")?.textContent?.trim() ?? "",
html: element.shadowRoot.innerHTML,
});
const payload = JSON.stringify({ ...snapshot(), probe });
await new Promise((resolve) => process.stdout.write(payload, () => resolve()));
process.exit(0);