Files
houseplan-card/demo/smoke_zigbee_topology_hover.mjs
T
2026-09-05 10:50:19 +03:00

159 lines
8.2 KiB
JavaScript

import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const wait = (predicate, label, timeout = 5000) => new Promise((resolve, reject) => {
const started = performance.now();
const tick = () => {
if (predicate()) return resolve();
if (performance.now() - started > timeout) return reject(new Error(`topology smoke timeout: ${label}`));
setTimeout(tick, 20);
};
tick();
});
const mouse = (type, relatedTarget = null) => new PointerEvent(type, {
pointerType: 'mouse', bubbles: true, composed: true, clientX: 200, clientY: 200, relatedTarget,
});
const out = {};
await card.updateComplete;
out.defaultOff = !root().querySelector('hp-zigbee-topology-overlay');
out.offHasNoLazyChunk = !performance.getEntriesByType('resource')
.some((entry) => /zigbee-topology-runtime|hp-zigbee-topology-overlay/.test(entry.name));
let zhaCalls = 0;
const originalHass = card.hass;
card.hass = { ...originalHass, callWS: async (message) => {
if (message.type !== 'zha/devices') return originalHass.callWS(message);
zhaCalls++;
return [
{ ieee: '00124b0000000001', device_reg_id: 'd_light1', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000002', lqi: 180, relationship: 'Parent' },
{ ieee: '00124b0000000003', lqi: 80, relationship: 'Child' },
] },
{ ieee: '00124b0000000002', device_reg_id: 'd_lamp', device_type: 'Coordinator', neighbors: [
{ ieee: '00124b0000000001', lqi: 170, relationship: 'Child' },
{ ieee: '00124b0000000006', lqi: 140, relationship: 'Child' },
] },
{ ieee: '00124b0000000003', device_reg_id: 'd_mower', device_type: 'EndDevice', neighbors: [
{ ieee: '00124b0000000001', lqi: 75, relationship: 'Parent' },
] },
{ ieee: '00124b0000000004', device_reg_id: 'd_temp', device_type: 'EndDevice', neighbors: [
{ ieee: '00124b0000000006', lqi: 20, relationship: 'Parent' },
] },
{ ieee: '00124b0000000005', device_reg_id: 'd_tv', device_type: 'EndDevice', neighbors: [] },
{ ieee: '00124b0000000006', device_reg_id: 'not_on_plan', device_type: 'Router', neighbors: [
{ ieee: '00124b0000000002', lqi: 130, relationship: 'Parent' },
{ ieee: '00124b0000000004', lqi: 25, relationship: 'Child' },
] },
];
} };
await card._ensureEditorRuntime();
card._openSettingsDialog();
await card.updateComplete;
let settings = root().querySelector('hp-zigbee-topology-settings');
await wait(() => !!settings?.shadowRoot, 'settings mounted');
settings._emit({ enabled: true, z2mBaseTopics: [] });
await card.updateComplete;
await card._saveSettingsDialog();
await card.updateComplete;
await wait(() => !!root().querySelector('hp-zigbee-topology-overlay'), 'overlay mounted');
out.settingPersists = card._serverCfg.settings.zigbee_topology?.enabled === true;
out.enableDoesNotFetch = zhaCalls === 0;
card._openSettingsDialog();
await card.updateComplete;
settings = root().querySelector('hp-zigbee-topology-settings');
await wait(() => !!settings?.shadowRoot?.querySelector('button'), 'settings button');
await settings._readZha();
await wait(() => settings._snapshot?.states?.zha?.phase === 'ready', 'ZHA ready');
out.explicitZhaRead = zhaCalls === 1;
card._settingsDialog = null;
card.requestUpdate();
await card.updateComplete;
const source = root().querySelector('.dev[data-id="d_light1"]');
source.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-hp="zigbee-topology-lines"]'), 'local route');
const overlay = root().querySelector('hp-zigbee-topology-overlay');
out.incidentOnly = overlay.shadowRoot.querySelectorAll('line').length === 1
&& overlay.shadowRoot.querySelectorAll('[data-hp="zigbee-topology-neighbor"]').length === 1;
const routeLine = overlay.shadowRoot.querySelector('line[data-direction="toward-neighbor"]');
const routeArrow = overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-arrow"]');
const lineBox = routeLine?.getBoundingClientRect();
const arrowBox = routeArrow?.getBoundingClientRect();
out.localRouteArrow = !!routeLine && routeArrow?.getAttribute('data-direction') === 'toward-neighbor'
&& !!lineBox && !!arrowBox && arrowBox.width > 3 && arrowBox.height > 3;
out.crossSpaceCount = overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]')
?.textContent.trim() === '+1 in other spaces';
out.pointerTransparent = getComputedStyle(overlay).pointerEvents === 'none'
&& getComputedStyle(overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]')).pointerEvents === 'none';
card._commitSpace('garden', true);
card.requestUpdate();
await card.updateComplete;
await wait(() => root().querySelector('.dev[data-id="d_mower"]'), 'garden marker');
const remoteChild = root().querySelector('.dev[data-id="d_mower"]');
remoteChild.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-hp="zigbee-topology-parent-bubble"]'), 'remote parent bubble');
let activeOverlay = root().querySelector('hp-zigbee-topology-overlay');
const firstTitle = card._serverCfg.spaces.find((space) => space.id === 'f1')?.title;
out.remoteParentBubble = activeOverlay.shadowRoot
.querySelector('[data-hp="zigbee-topology-parent-bubble"]')?.textContent.trim() === firstTitle
&& activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-parent-bubble"]')
?.getAttribute('data-kind') === 'remote-space'
&& !!activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-parent-arrow"]')
&& !activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]');
card._commitSpace('f1', true);
card.requestUpdate();
await card.updateComplete;
await wait(() => root().querySelector('.dev[data-id="d_temp"]'), 'temperature marker');
const temperature = root().querySelector('.dev[data-id="d_temp"]');
temperature.dispatchEvent(mouse('pointerover'));
await wait(() => root().querySelector('hp-zigbee-topology-overlay')?.shadowRoot
?.querySelector('[data-kind="unplaced-device"]'), 'unplaced device bubble');
activeOverlay = root().querySelector('hp-zigbee-topology-overlay');
out.unplacedDeviceBubble = activeOverlay.shadowRoot.querySelector('[data-kind="unplaced-device"]')
?.textContent.trim() === 'device is not on the plan';
activeOverlay.devices = activeOverlay.devices.map((device) => (
device.id === 'd_lamp' ? { ...device, hidden: true } : device
));
await activeOverlay.updateComplete;
const router = root().querySelector('.dev[data-id="d_light1"]');
router.dispatchEvent(mouse('pointerover'));
await wait(() => activeOverlay.shadowRoot.querySelector('[data-kind="unplaced-coordinator"]'),
'unplaced coordinator bubble');
out.unplacedCoordinatorBubble = activeOverlay.shadowRoot
.querySelector('[data-kind="unplaced-coordinator"]')?.textContent.trim()
=== 'coordinator is not on the plan';
router.dispatchEvent(mouse('pointerout', root().querySelector('.stage')));
await activeOverlay.updateComplete;
out.leaveClears = !activeOverlay.shadowRoot.querySelector('line');
router.dispatchEvent(mouse('pointerover'));
await wait(() => activeOverlay.shadowRoot.querySelector('line'), 'route restored');
root().querySelector('.stage').dispatchEvent(new PointerEvent('pointermove', {
pointerType: 'touch', bubbles: true, composed: true, pointerId: 7,
}));
await wait(() => !activeOverlay.shadowRoot.querySelector('line'), 'touch clears');
out.touchClears = !activeOverlay.shadowRoot.querySelector('line');
card._setMode('plan');
await card.updateComplete;
out.editorHasNoOverlay = !root().querySelector('hp-zigbee-topology-overlay');
card._setMode('view');
card.hass = { ...card.hass, user: { ...card.hass.user, is_admin: false } };
await card.updateComplete;
out.nonAdminHasNoOverlay = !root().querySelector('hp-zigbee-topology-overlay') && zhaCalls === 1;
return out;
});
checkAll(result);
await finish(browser, result);