Files
houseplan-card/demo/smoke_device_inbox.mjs
T
2026-08-27 23:17:52 +00:00

84 lines
3.9 KiB
JavaScript

// #29: one lifecycle catalog replaces the separate Add / hidden-device paths.
// This smoke exercises the real Lit dialog and verifies that browsing it is
// read-only while nested marker flows return to the same catalog state.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 980, height: 820 });
const out = await page.evaluate(async () => {
const c = window.__card;
const root = () => c.renderRoot || c.shadowRoot;
const writes = [];
const originalCallWS = c.hass.callWS;
c.hass = { ...c.hass, callWS: async (message) => {
if (/^houseplan\/(config|layout)\/(set|update|delete)$/.test(message.type)) {
writes.push(message.type);
}
return originalCallWS(message);
} };
c._setMode('devices');
await c.updateComplete;
const toolbar = [...root().querySelectorAll('.editbar .btn')];
const labels = toolbar.map((node) => node.textContent.trim());
const result = {
oneCatalogEntryPoint: labels.some((label) => /Devices/i.test(label))
&& !labels.some((label) => /Hidden and disabled|^Add$/i.test(label)),
};
const before = JSON.stringify({
config: c._serverCfg, layout: c._layout, cfgRev: c._cfgRev, layoutRev: c._layoutRev,
});
c._openDeviceInbox();
await c.updateComplete;
const dialog = root().querySelector('hp-dialog');
const dialogSurface = dialog?.renderRoot?.querySelector('.surface');
const tabs = [...root().querySelectorAll('.device-inbox-tabs [role="tab"]')];
result.dialogHasFourTabs = !!dialog && tabs.length === 4;
result.desktopCatalogIsWide = !!dialogSurface && dialogSurface.getBoundingClientRect().width > 800;
result.hasOverflowMenu = !!root().querySelector('.device-inbox-menu');
result.onPlanHasRows = root().querySelectorAll('.device-inbox-row[data-category="on_plan"]').length > 0;
result.noHorizontalOverflow = !!dialog && dialog.scrollWidth <= dialog.clientWidth + 1;
const search = root().querySelector('.device-inbox-search');
search.value = 'Ceiling light';
search.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await c.updateComplete;
result.searchUsesFullSnapshot = root().querySelectorAll('.device-inbox-row').length === 1
&& root().querySelector('.device-inbox-row')?.textContent.includes('Ceiling light');
// Arrow-key tab navigation is part of the keyboard contract.
const tablist = root().querySelector('.device-inbox-tabs');
tablist.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
await c.updateComplete;
result.arrowChangesTab = c._deviceInbox?.tab === 'available';
// Restore a non-default context, open the real marker dialog, then cancel.
c._deviceInbox = { ...c._deviceInbox, tab: 'on_plan', search: 'lamp', onlyNew: false };
await c.updateComplete;
const row = c._deviceInboxRows().find((item) => item.category === 'on_plan' && item.canEdit);
c._openInboxMarker(row);
await c.updateComplete;
result.nestedDialogOpened = !!c._markerDialog && !c._deviceInbox;
c._closeMarkerDialog();
await c.updateComplete;
result.nestedCancelReturnsContext = c._deviceInbox?.tab === 'on_plan'
&& c._deviceInbox?.search === 'lamp' && c._deviceInbox?.anchor === row.key;
// Find is navigation only: it closes the dialog and selects a marker briefly.
c._deviceInbox = { ...c._deviceInbox, search: '' };
const findable = c._deviceInboxRows().find((item) => item.category === 'on_plan' && item.canFind);
c._findInboxDevice(findable);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
await c.updateComplete;
result.findClosesAndSelects = !c._deviceInbox && c._selId === findable.deviceId;
const after = JSON.stringify({
config: c._serverCfg, layout: c._layout, cfgRev: c._cfgRev, layoutRev: c._layoutRev,
});
result.browsingIsReadOnly = before === after && writes.length === 0;
return result;
});
checkAll(out);
await finish(browser, out);