// #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);