Files
houseplan-card/demo/verify_ha_dialog_discard_recovery.mjs
2026-09-23 02:00:48 +03:00

175 lines
7.3 KiB
JavaScript

// #607: explicit authentic Home Assistant dialog diagnostic. This is not a
// regular smoke: it loads the pinned official HA frontend fixture (~124 MB),
// then exercises all four settings-dialog owners through HA's real close
// button and WebAwesome modal surface.
import assert from 'node:assert/strict';
import { assertFreshDemoBundle } from './bundle-freshness.mjs';
import { launchHaDialogFixture } from './helpers/ha-dialog-fixture.mjs';
const cases = [
{ kind: 'marker', input: '#marker-name', state: '_markerDialog' },
{ kind: 'room', input: '#room-name', state: '_roomDialog' },
{ kind: 'space', input: '#space-title', state: '_spaceDialog' },
{ kind: 'settings', input: '#gs-north', state: '_settingsDialog' },
];
const fixture = await launchHaDialogFixture({
authentic: true,
viewport: { width: 1280, height: 900 },
});
const { page } = fixture;
const results = {};
const waitOwnerOpen = async (testCase) => page.waitForFunction(({ kind, state }) => {
const card = window.__card;
const dialog = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`);
const ha = dialog?.shadowRoot.querySelector('ha-dialog');
const surface = ha?.shadowRoot.querySelector('wa-dialog')?.shadowRoot.querySelector('dialog');
return Boolean(card[state]) && ha?.open === true && surface?.matches(':modal') === true;
}, testCase);
const waitOwnerClosed = async ({ kind, state }) => page.waitForFunction(({ kind, state }) => {
const card = window.__card;
return !card[state] && !card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`)
&& !card.shadowRoot.querySelector('hp-confirm');
}, { kind, state });
const open = async (testCase) => {
await page.evaluate(async ({ kind }) => {
const card = window.__card;
if (card._ensureEditorRuntime && !await card._ensureEditorRuntime()) {
throw new Error('Editor runtime failed to load');
}
if (kind === 'marker') {
const device = card._devices.find((item) => item.id === 'd_light1') || card._devices[0];
card._openMarkerDialog(device);
} else if (kind === 'room') {
card._openRoomEdit(card._curSpaceCfg.rooms[0]);
} else if (kind === 'space') {
card._openSpaceDialog('edit', card._space);
} else {
card._openSettingsDialog();
}
card.requestUpdate();
await card.updateComplete;
}, testCase);
await waitOwnerOpen(testCase);
};
const edit = async (testCase, suffix) => page.evaluate(async ({ kind, input, suffix }) => {
const card = window.__card;
const field = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"] ${input}`);
if (!field) throw new Error(`${kind}: missing field ${input}`);
if (kind === 'settings') field.value = suffix === ' first' ? '47' : '48';
else field.value = `${field.value}${suffix}`;
field.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await card.updateComplete;
return field.value;
}, { ...testCase, suffix });
const clickAuthenticClose = async (testCase) => page.evaluate(({ kind }) => {
const card = window.__card;
const dialog = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`);
const ha = dialog?.shadowRoot.querySelector('ha-dialog');
const button = ha?.shadowRoot.querySelector('ha-icon-button[data-dialog="close"]');
if (!button) throw new Error(`${kind}: authentic HA close button not found`);
dialog.__hp607CloseCount = dialog.__hp607CloseCount || 0;
if (!dialog.__hp607Counting) {
dialog.__hp607Counting = true;
dialog.addEventListener('hp-close', () => { dialog.__hp607CloseCount += 1; });
}
button.click();
}, testCase);
const waitConfirm = async (testCase) => {
try {
await page.waitForFunction(({ kind }) => {
const card = window.__card;
const dialog = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`);
const ha = dialog?.shadowRoot.querySelector('ha-dialog');
return ha?.open === false && Boolean(card.shadowRoot.querySelector('hp-confirm'));
}, testCase);
} catch (error) {
const state = await page.evaluate(({ kind, state }) => {
const card = window.__card;
const dialog = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`);
const ha = dialog?.shadowRoot.querySelector('ha-dialog');
return {
owner: Boolean(card[state]), dialog: Boolean(dialog), haOpen: ha?.open,
closeCount: dialog?.__hp607CloseCount,
confirm: Boolean(card.shadowRoot.querySelector('hp-confirm')),
buttons: [...(ha?.shadowRoot.querySelectorAll('ha-icon-button') || [])]
.map((button) => ({ dataDialog: button.getAttribute('data-dialog'), label: button.getAttribute('aria-label') })),
};
}, testCase);
throw new Error(`${testCase.kind}: confirmation did not open: ${JSON.stringify(state)}`, { cause: error });
}
};
const decide = async (accept) => page.evaluate((accept) => {
const card = window.__card;
const selector = accept ? '[data-hp="dialog-confirm"]' : '[data-hp="dialog-cancel"]';
const button = card.shadowRoot.querySelector(`hp-confirm ${selector}`);
if (!button) throw new Error(`Confirmation button not found: ${selector}`);
button.click();
}, accept);
try {
await page.goto(`${fixture.url}/product.html`);
await page.waitForFunction(() => window.__card?._model?.length > 0
&& window.__card?._devices?.length > 0 && window.__card?._booting === false);
await assertFreshDemoBundle(page);
for (const testCase of cases) {
console.log(`Checking ${testCase.kind}`);
await open(testCase);
const firstDraft = await edit(testCase, ' first');
await clickAuthenticClose(testCase);
await waitConfirm(testCase);
await decide(false);
await waitOwnerOpen(testCase);
const secondDraft = await edit(testCase, ' second');
const continued = await page.evaluate(({ kind, state, input, firstDraft, secondDraft }) => {
const card = window.__card;
const dialog = card.shadowRoot.querySelector(`hp-dialog[data-kind="${kind}"]`);
const field = dialog?.querySelector(input);
return {
sameDraft: Boolean(card[state]) && firstDraft !== secondDraft && field?.value === secondDraft,
oneClose: dialog?.__hp607CloseCount === 1,
};
}, { ...testCase, firstDraft, secondDraft });
await clickAuthenticClose(testCase);
await waitConfirm(testCase);
const twoCloses = await page.evaluate(({ kind }) => window.__card.shadowRoot
.querySelector(`hp-dialog[data-kind="${kind}"]`)?.__hp607CloseCount === 2, testCase);
await decide(true);
await waitOwnerClosed(testCase);
await open(testCase);
await clickAuthenticClose(testCase);
await waitOwnerClosed(testCase);
results[`${testCase.kind}: continue restores modal`] = continued.sameDraft;
results[`${testCase.kind}: one close before retry`] = continued.oneClose;
results[`${testCase.kind}: retry emits exactly one more close`] = twoCloses;
results[`${testCase.kind}: discard and clean close finish`] = true;
}
await fixture.assertClean();
for (const [name, value] of Object.entries(results)) assert.equal(value, true, name);
console.log(JSON.stringify({
issue: 607,
authenticHaFrontend: fixture.provenance,
results,
pageErrors: fixture.errors,
externalRequests: fixture.externalRequests,
websocketAttempts: fixture.websocketAttempts,
}, null, 2));
console.log('OK');
} finally {
await fixture.close();
}