Files
houseplan-card/demo/smoke_optional_space_model.mjs
T
2026-08-19 02:49:39 +03:00

91 lines
3.4 KiB
JavaScript

// #113: deleting the final space and receiving an empty plan over WS must be
// a supported lifecycle state, not a synthetic SpaceModel or an exception.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 820, height: 760 });
const result = await page.evaluate(async () => {
const out = {};
const card = window.__card;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const root = () => card.shadowRoot || card.renderRoot;
const settle = async () => {
await card.updateComplete;
await sleep(40);
await card.updateComplete;
};
const original = structuredClone(card._serverCfg);
const first = structuredClone(original.spaces[0]);
// Exercise the real delete command with exactly one remaining space.
card._serverCfg = { ...structuredClone(original), spaces: [first] };
card._space = first.id;
card._cfgEpoch++;
card.requestUpdate();
await settle();
card._openSpaceDialog('edit', first.id);
card._mode = 'plan';
card._path = [[100, 100], [200, 100]];
card._resumeDraftBySpace = { [first.id]: 'stale-draft' };
card._pointers.set(113, { x: 10, y: 10 });
card._drag = { id: 'stale-device', sx: 0, sy: 0, ox: 0, oy: 0, moved: true };
card._saveConfigDebounced();
const confirmBefore = window.confirm;
window.confirm = () => true;
try {
await card._deleteSpace();
} finally {
window.confirm = confirmBefore;
}
await settle();
out.deleteLastRendersEmpty = !!root().querySelector('.empty') && !root().querySelector('.stage');
out.deleteLastClearsSelection = card._space === '' && card._spaceModel() === undefined;
out.deleteLastAbortsEditorState = card._mode === 'view'
&& card._path.length === 0 && card._pointers.size === 0 && card._drag === null
&& Object.keys(card._resumeDraftBySpace).length === 0;
out.deleteLastClosesEditDialog = card._spaceDialog === null;
out.deleteLastCancelsPendingWrite = card._saveConfigDebounced.pending() === false;
// The empty state still owns global recovery flows.
const add = root().querySelector('.empty button.btn.on');
add?.click();
await settle();
out.createFlowSurvivesEmpty = card._spaceDialog?.mode === 'create';
// Recreate a plan, then reproduce the same transition as a config-updated WS
// event. This also arms the once-per-empty cleanup for a second cycle.
card._spaceDialog = null;
card._serverCfg = structuredClone(original);
card._space = original.spaces[0].id;
card._cfgEpoch++;
card.requestUpdate();
await settle();
out.recreateRestoresPlan = !!root().querySelector('.stage') && card._spaceModel()?.id === card._space;
card._mode = 'devices';
card._pointers.set(114, { x: 20, y: 20 });
card._serverCfg = { ...structuredClone(original), spaces: [] };
card._cfgEpoch++;
card.requestUpdate();
await settle();
out.wsEmptyAbortsLiveGesture = card._mode === 'view'
&& card._pointers.size === 0 && card._spaceModel() === undefined;
// Empty plans must remain inert under unrelated HA/render ticks.
card._serverCanWrite = false;
card.hass = {
...card.hass,
themes: { ...(card.hass.themes || {}), darkMode: !card.hass.themes?.darkMode },
};
window.dispatchEvent(new Event('resize'));
await settle();
out.emptySurvivesThemeResizeReadonly = !!root().querySelector('.empty')
&& !root().querySelector('.stage') && card._mode === 'view';
return out;
});
checkAll(result);
await finish(browser, result);