Files
houseplan-card/demo/smoke_render_invalidation.mjs
T
2026-09-04 23:45:57 +03:00

151 lines
6.2 KiB
JavaScript

// #451: production-bundle guard for the boundary between HA intake, full Lit
// renders and RAF-owned interaction layers. The large benchmark owns timings;
// this fast smoke runs on every selected-smoke pass and owns wiring semantics.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const until = async (predicate, timeout = 5000) => {
const started = performance.now();
while (!predicate()) {
if (performance.now() - started > timeout) throw new Error('render invalidation smoke timed out');
await new Promise((done) => setTimeout(done, 10));
}
};
const out = {};
card._setMode('view', false);
await card.updateComplete;
if ('_bootSoft' in card) await until(() => card._bootSoft === false);
await frame();
let renders = 0;
let intakes = 0;
let scans = 0;
const originalRenderBody = card._renderBody.bind(card);
const originalObserve = card._renderLife.observe.bind(card._renderLife);
const originalBindingStatus = card._bindingStatus.bind(card);
card._renderBody = (...args) => { renders++; return originalRenderBody(...args); };
card._renderLife.observe = (...args) => { intakes++; return originalObserve(...args); };
card._bindingStatus = (...args) => { scans++; return originalBindingStatus(...args); };
const dependency = [...(card._visibleDeviceSnapshot?.entityIds || [])]
.find((id) => card.hass.states?.[id]);
const originalHass = card.hass;
const stableConfig = JSON.stringify(card._serverCfg);
const stableLayout = JSON.stringify(card._layout);
const withState = (id, state) => ({
...card.hass,
states: { ...card.hass.states, [id]: { ...card.hass.states[id], state } },
});
const irrelevantBefore = renders;
const intakeBefore = intakes;
for (let index = 0; index < 5; index++) {
card.hass = {
...card.hass,
states: {
...card.hass.states,
[`sensor.hp_451_irrelevant_${index}`]: { entity_id: `sensor.hp_451_irrelevant_${index}`, state: String(index), attributes: {} },
},
};
}
await card.updateComplete;
await frame();
out.irrelevantHaHasNoFullRender = renders - irrelevantBefore === 0;
out.irrelevantHaStillIntakes = intakes - intakeBefore === 5;
const relevantBefore = renders;
if (dependency) card.hass = withState(dependency, '451-outside');
await card.updateComplete;
await frame();
out.relevantHaRendersOnce = !!dependency && renders - relevantBefore === 1;
scans = 0;
const heavy = card.renderRoot.querySelector('.wallbodies');
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const hoverBefore = renders;
const device = card.renderRoot.querySelector('[data-hp="device"]');
if (device) {
device.dispatchEvent(new PointerEvent('pointerover', {
bubbles: true, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 220, clientY: rect.top + 220,
}));
device.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 225, clientY: rect.top + 225,
}));
}
await frame();
const liveTip = card.renderRoot.querySelector('[data-hp-live-tip]');
out.hoverUsesLightweightPaint = !!device && renders === hoverBefore
&& !!liveTip && !liveTip.hidden;
device?.dispatchEvent(new PointerEvent('pointerleave', {
bubbles: false, composed: true, pointerId: 450, pointerType: 'mouse', isPrimary: true,
}));
await frame();
const pointer = (type, x, y, buttons) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, pointerId: 451, pointerType: 'mouse', isPrimary: true,
button: 0, buttons, clientX: rect.left + x, clientY: rect.top + y,
}));
pointer('pointerdown', 300, 300, 1);
await card.updateComplete;
const panBefore = renders;
const panIntakeBefore = intakes;
for (let index = 0; index < 3; index++) {
pointer('pointermove', 320 + index * 20, 315 + index * 10, 1);
if (dependency) card.hass = withState(dependency, `451-drag-${index}`);
}
await card.updateComplete;
await frame();
out.panAndDeferredHaSkipFullRender = renders === panBefore;
out.deferredHaTicksStillIntake = intakes - panIntakeBefore === 3;
const panTerminalBefore = renders;
pointer('pointerup', 360, 335, 0);
await card.updateComplete;
await frame();
out.panTerminalRendersOnce = renders - panTerminalBefore === 1;
out.deferredHaIsLastWins = !!dependency
&& card._renderPlanHass.states?.[dependency]?.state === '451-drag-2';
card._setMode('plan', false);
card._tool = 'draw';
card._path = [[100, 100]];
card.requestUpdate();
await card.updateComplete;
await until(() => !card._modeTransitionBusy && card._continuity.state === 'steady');
for (let index = 0; index < 5; index++) await frame();
await frame();
const editorBefore = renders;
const editorPaintBefore = card._liveEditorPaintCount || 0;
for (let index = 0; index < 20; index++) {
stage.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, composed: true, pointerId: 452, pointerType: 'mouse', isPrimary: true,
clientX: rect.left + 110 + index, clientY: rect.top + 120 + index,
}));
}
await frame();
out.editorMovesSkipFullRender = renders === editorBefore;
out.editorMovesUseRafPaint = (card._liveEditorPaintCount || 0) > editorPaintBefore;
card._cursorPt = null;
card._path = [];
const editorTerminalBefore = renders;
card.requestUpdate();
await card.updateComplete;
await frame();
out.editorTerminalHasAtMostOneRender = renders - editorTerminalBefore <= 1;
out.heavySceneIdentityStable = card.renderRoot.querySelector('.wallbodies') === heavy;
out.pointerWindowsSkipDiagnosticScans = scans === 0;
out.interactionsDoNotWriteConfig = JSON.stringify(card._serverCfg) === stableConfig;
out.interactionsDoNotWriteLayout = JSON.stringify(card._layout) === stableLayout;
card.hass = originalHass;
return out;
});
checkAll(result);
await finish(browser, result);