mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
186 lines
7.8 KiB
JavaScript
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 });
|