Files
houseplan-card/demo/smoke_room_link.mjs
2026-08-19 17:12:02 +03:00

186 lines
7.8 KiB
JavaScript

import { launch, checkAll, finish } from './serve.mjs';
const checks = {};
const geometry = [];
for (const scale of [1, 2]) {
const { page, browser } = await launch({ width: 820, height: 760 }, scale);
for (const dark of [false, true]) {
const key = `dpr${scale}-${dark ? 'dark' : 'light'}`;
const setup = await page.evaluate(async ({ dark }) => {
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const settle = async () => {
c.requestUpdate();
await c.updateComplete;
const deadline = performance.now() + 2000;
while (c._modeTransitionBusy && performance.now() < deadline)
await new Promise((resolve) => setTimeout(resolve, 20));
await c.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const labelById = (id) => [...sr().querySelectorAll('.roomlabel')]
.find((label) => label.dataset.id === id);
const relativeBox = (label, selector) => {
const anchorBox = label.getBoundingClientRect();
const box = label.querySelector(selector)?.getBoundingClientRect();
if (!box) return null;
const anchorX = anchorBox.left + anchorBox.width / 2;
const anchorY = anchorBox.top + anchorBox.height / 2;
return {
left: box.left - anchorX,
top: box.top - anchorY,
width: box.width,
height: box.height,
};
};
const snapshot = (label) => ({
name: relativeBox(label, '.rlname'),
metrics: relativeBox(label, '.rlmetrics'),
metricsText: label.querySelector('.rlmetrics')?.textContent?.replace(/\s+/g, ' ').trim() || '',
});
c.hass = {
...c.hass,
themes: { ...(c.hass.themes || {}), darkMode: dark },
};
c._serverCfg = {
...c._serverCfg,
spaces: c._serverCfg.spaces.map((space) => space.id !== c._space ? space : ({
...space,
settings: {
...(space.settings || {}),
show_names: true,
label_temp: true,
label_hum: true,
label_lqi: true,
label_light: true,
},
})),
};
const targetRoom = c._spaceModel().rooms.find((room) => room.name && room.area);
if (targetRoom?.id) {
c._layout = {
...c._layout,
[`rl_${targetRoom.id}`]: { s: c._space, x: 0.16, y: 0.22 },
};
}
c._setMode('view');
await settle();
const labels = [...sr().querySelectorAll('.roomlabel')];
const label = labels.find((candidate) => candidate.querySelector('.rlgo')
&& candidate.querySelector('.rlmetrics'));
if (!label) return { missingFixture: true };
const roomId = label.dataset.id;
const view = snapshot(label);
const links = sr().querySelectorAll('.rlgo');
const withArea = c._spaceModel().rooms.filter((room) => room.name && room.area).length;
const room = sr().querySelector('.room');
const viewIcon = label.querySelector('.rlgo');
const viewCursor = getComputedStyle(viewIcon).cursor;
const viewPointerEvents = getComputedStyle(viewIcon).pointerEvents;
let viewNav = null;
const viewPushState = history.pushState;
history.pushState = (state, title, url) => { viewNav = url; };
viewIcon.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
await c.updateComplete;
history.pushState = viewPushState;
c._setMode('plan');
await settle();
const planLabel = labelById(roomId);
const planIcon = planLabel?.querySelector('.rlgo');
const plan = planLabel ? snapshot(planLabel) : null;
let planNav = null;
const planPushState = history.pushState;
history.pushState = (state, title, url) => { planNav = url; };
planIcon?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
await c.updateComplete;
history.pushState = planPushState;
const iconBox = planIcon?.getBoundingClientRect();
window.__roomLinkOriginalPushState = history.pushState;
window.__roomLinkDragNav = null;
history.pushState = (state, title, url) => { window.__roomLinkDragNav = url; };
return {
missingFixture: false,
roomId,
view,
plan,
roomCursor: getComputedStyle(room).cursor,
viewLinkCount: links.length,
withArea,
viewCursor,
viewPointerEvents,
viewNav,
planLinkCount: sr().querySelectorAll('.rlgo').length,
planCursor: planIcon ? getComputedStyle(planIcon).cursor : '',
planTitle: planIcon?.getAttribute('title') || '',
planNav,
planHandles: planLabel?.querySelectorAll('.rlhandle').length || 0,
planSettingsButtons: sr().querySelectorAll('.rlgearbtn').length,
beforeLayout: c._layout[`rl_${roomId}`] || null,
dragTarget: iconBox ? { x: iconBox.left + iconBox.width / 2, y: iconBox.top + iconBox.height / 2 } : null,
themeApplied: c.hass.themes.darkMode === dark,
};
}, { dark });
if (setup.missingFixture) {
checks[`${key}:fixture`] = false;
continue;
}
let drag = { changed: false, dragNav: 'not-run', stayedInPlan: false };
if (setup.dragTarget) {
await page.mouse.move(setup.dragTarget.x, setup.dragTarget.y);
await page.mouse.down();
await page.mouse.move(setup.dragTarget.x + 24, setup.dragTarget.y + 16, { steps: 4 });
await page.mouse.up();
drag = await page.evaluate(async ({ roomId, beforeLayout }) => {
const c = window.__card;
await c.updateComplete;
const after = c._layout[`rl_${roomId}`] || null;
const changed = !!after && (!beforeLayout
|| Math.abs(after.x - beforeLayout.x) > 1e-6
|| Math.abs(after.y - beforeLayout.y) > 1e-6);
const dragNav = window.__roomLinkDragNav;
history.pushState = window.__roomLinkOriginalPushState;
delete window.__roomLinkOriginalPushState;
delete window.__roomLinkDragNav;
return { changed, dragNav, stayedInPlan: c._mode === 'plan' };
}, { roomId: setup.roomId, beforeLayout: setup.beforeLayout });
}
const values = ['left', 'top', 'width', 'height'];
const nameDeltas = values.map((field) => Math.abs(setup.view.name[field] - setup.plan.name[field]));
const metricDeltas = values.map((field) => Math.abs(setup.view.metrics[field] - setup.plan.metrics[field]));
geometry.push({ key, nameDeltas, metricDeltas });
checks[`${key}:fixture`] = !setup.missingFixture;
checks[`${key}:room-cursor`] = setup.roomCursor === 'default';
checks[`${key}:view-link-per-area-room`] = setup.viewLinkCount === setup.withArea;
checks[`${key}:view-link-interactive`] = setup.viewCursor === 'pointer'
&& setup.viewPointerEvents === 'auto';
checks[`${key}:view-navigates`] = typeof setup.viewNav === 'string'
&& setup.viewNav.includes('/config/areas/area/');
checks[`${key}:plan-keeps-links`] = setup.planLinkCount === setup.withArea;
checks[`${key}:plan-link-is-drag-affordance`] = setup.planCursor === 'grab'
&& setup.planTitle === '';
checks[`${key}:plan-does-not-navigate`] = setup.planNav == null
&& drag.dragNav == null && drag.stayedInPlan;
checks[`${key}:plan-icon-drags-label`] = drag.changed;
checks[`${key}:core-name-parity`] = nameDeltas.every((delta) => delta <= 0.5);
checks[`${key}:core-metrics-parity`] = metricDeltas.every((delta) => delta <= 0.5)
&& setup.view.metricsText === setup.plan.metricsText;
checks[`${key}:plan-controls-preserved`] = setup.planHandles === 4
&& setup.planSettingsButtons > 0;
checks[`${key}:theme-applied`] = setup.themeApplied;
}
await browser.close();
}
checkAll(checks);
await finish(null, { checks, geometry });