Files
houseplan-card/demo/smoke_wall_union_isolation.mjs
T
2026-09-06 07:06:54 +03:00

121 lines
5.3 KiB
JavaScript

/** #278: degraded canonical components render everywhere; strict writes do not persist. */
import { readFileSync } from 'node:fs';
import { launch, checkAll, finish } from './serve.mjs';
const fixture = JSON.parse(readFileSync(
new URL('../test/fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
));
const { page, browser } = await launch({ width: 1000, height: 780 }, 1);
const result = await page.evaluate(async (fixtureConfig) => {
const out = {};
const card = window.__card;
const root = () => card.renderRoot || card.shadowRoot;
const frame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const settle = async () => { await card.updateComplete; await frame(); };
const cfg = structuredClone(fixtureConfig);
cfg.spaces[0].settings = {
...(cfg.spaces[0].settings || {}), show_borders: true, fill_mode: 'none',
};
card._serverCfg = cfg;
card._layout = {};
card._space = cfg.spaces[0].id;
card._cfgEpoch++;
card._modelCache = null;
card._frame = null;
card._wallUnionCache = null;
card._physicalBodiesCache = null;
card._lightBarrierCache = null;
card._setMode('plan');
card._tool = 'select';
card.requestUpdate();
await settle();
const sourceBeforeRender = JSON.stringify(card._serverCfg.spaces[0]);
const canonical = card._wallUnionGeometry();
out.typedDegradedResult = canonical?.status === 'degraded-extra';
out.planRendersBothComponents = root().querySelectorAll('.wallbody[data-component]').length === 2
&& root().querySelectorAll('.wallbody-fill[data-component]').length === 2;
const planPaths = [...root().querySelectorAll('.wallbody[data-component]')]
.map((path) => path.getAttribute('d')).sort();
const model = card._spaceModel();
const polys = model.rooms.map((room) => ({ r: room, poly: room.poly }));
const barriers = card._lightBarriers(model, polys, card._physicalBodiesR(model));
out.lightUsesBothComponents = barriers.masonryGeometry.length
=== canonical.components.reduce((sum, component) => sum + component.geom.length, 0)
&& barriers.occluders.length > 0;
card._setMode('view');
card.requestUpdate();
await settle();
out.viewMatchesPlan = JSON.stringify([...root().querySelectorAll('.wallbody[data-component]')]
.map((path) => path.getAttribute('d')).sort()) === JSON.stringify(planPaths);
await customElements.whenDefined('houseplan-space-card');
const staticCard = document.createElement('houseplan-space-card');
staticCard.setConfig({
type: 'custom:houseplan-space-card', space: cfg.spaces[0].id, show_button: false,
});
const baseCall = card.hass.callWS.bind(card.hass);
staticCard.hass = { ...card.hass, callWS: async (message) => {
if (message.type === 'houseplan/config/get') return { config: structuredClone(cfg), rev: 1 };
if (message.type === 'houseplan/layout/get') return { layout: {}, rev: 1 };
return baseCall(message);
} };
document.body.appendChild(staticCard);
const started = Date.now();
while (staticCard.renderRoot?.querySelectorAll('.wallbody[data-component]').length !== 2
&& Date.now() - started < 6000) await new Promise((resolve) => setTimeout(resolve, 50));
await staticCard.updateComplete;
out.staticMatchesPlan = JSON.stringify([...staticCard.renderRoot.querySelectorAll(
'.wallbody[data-component]',
)].map((path) => path.getAttribute('d')).sort()) === JSON.stringify(planPaths);
staticCard.remove();
const labs = Object.freeze(['iso']);
card._onLabsSnapshot({ alpha: true, active: labs, space: '' });
window.__hpAlpha = true;
window.__hpLabs = labs;
card._setProjection('iso');
await window.__hpEnsureHarnessIsoRuntime(card);
card.requestUpdate();
await settle();
const iso = card._isoSource().build();
out.hiddenIsoUsesBothComponents = iso.walls.length
=== canonical.components.reduce((sum, component) => sum + component.geom.length, 0)
&& !!root().querySelector('[data-hp="iso-walls"]');
out.renderNeverWrites = JSON.stringify(card._serverCfg.spaces[0]) === sourceBeforeRender;
let writes = 0;
card.hass = { ...card.hass, callWS: async (message) => {
if (message.type === 'houseplan/config/set') { writes++; return { rev: 9 }; }
return baseCall(message);
} };
card._setProjection('flat');
card._setMode('plan');
await settle();
card._geometryHistory.clear();
const before = card._geometrySnapshot();
const beforeJson = JSON.stringify(before);
card._curSpaceCfg.walls[0].cm += 1;
const committed = card._commitPhysicalGeometry('unsafe test', before);
await settle();
out.degradedPhysicalEditRejected = committed === false
&& JSON.stringify(card._geometrySnapshot()) === beforeJson;
out.rejectedEditHasNoHistoryOrWrite = card._geometryHistory.size === 0 && writes === 0;
out.rejectedEditHasLocalizedToast = card._toast === card._t('toast.geometry_unsafe');
let strictChecks = 0;
const strictCheck = card._checkSpacePhysicalGeometry.bind(card);
card._checkSpacePhysicalGeometry = (...args) => { strictChecks++; return strictCheck(...args); };
card._curSpaceCfg.title = 'Non geometry edit';
card._saveConfig();
await new Promise((resolve) => setTimeout(resolve, 700));
out.nonGeometryEditBypassesStrictBarrier = strictChecks === 0 && writes === 1;
card._checkSpacePhysicalGeometry = strictCheck;
return out;
}, fixture.config);
checkAll(result);
await finish(browser, result);