Files
houseplan-card/demo/smoke_glow.mjs
T
Matysh 7d152e04f9
Validate / hacs (push) Failing after 11s
Validate / hassfest (push) Failing after 10s
Validate / frontend (push) Successful in 2m21s
Validate / backend (push) Failing after 8m22s
Validate / golden (push) Failing after 7m32s
Validate / smoke (push) Failing after 16m23s
Validate / performance (push) Failing after 1h35m4s
test: align smoke contracts with independent Glow
2026-08-10 00:35:23 +03:00

122 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const spId = c._space;
// включить glow-режим
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
...s, settings: { ...(s.settings || {}), fill_mode: 'glow' } })) };
c.requestUpdate(); await c.updateComplete;
await new Promise((r) => setTimeout(r, 250));
// 1) legacy glow переносится в отдельный base-overlay, не в data-fill комнат
const modelRooms = c._spaceModel().rooms.length;
out.dataFillSeparated = sr().querySelectorAll('.room.filled').length === 0;
out.glowBaseRooms = sr().querySelectorAll('.glow-base-layer .glow-base').length === modelRooms;
// 2) пятна от включённых ламп
const litLight = c._devices.find((d) => d.space === spId && d.entities.some((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on'));
out.hasLitLight = !!litLight;
out.spots = sr().querySelectorAll('.glowlayer circle').length;
out.spotsMatchLights = out.spots === c._devices.filter((d) => d.space === spId && d.entities.some((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on')).length;
// 3) выключение лампы убирает её пятно
const eid = litLight.entities.find((e) => e.startsWith('light.') && c.hass.states[e]?.state === 'on');
const st0 = c.hass.states[eid];
c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: { ...st0, state: 'off' } } };
await c.updateComplete;
out.spotGone = sr().querySelectorAll('.glowlayer circle').length === out.spots - 1;
c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: st0 } }; await c.updateComplete;
// 4) rgb-лампа красит градиент своим цветом (форсируем rgb у лампы)
c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: { ...st0, state: 'on', attributes: { ...st0.attributes, rgb_color: [255, 0, 0] } } } };
await c.updateComplete;
out.gradientColored = [...sr().querySelectorAll('defs radialGradient stop')].some((s2) => s2.getAttribute('stop-color') === 'rgb(255, 0, 0)');
c.hass = { ...c.hass, states: { ...c.hass.states, [eid]: st0 } }; await c.updateComplete;
// 5) пятно обрезано комнатой (есть clipPath)
out.clipped = sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]').length > 0;
// 6) дверь в соседнюю комнату → в clip добавлен сектор (2 сабпути M)
// добавим дверь между r1 и r2 на общей стене x=0.55… найдём общую стену программно:
const spm = c._spaceModel();
const r1 = spm.rooms.find((r) => r.id === 'r1');
const r2 = spm.rooms.find((r) => r.id === 'r2');
// возьмём точку на границе r1, ближайшую к центру r2
const c2 = c._roomCenter(r2);
const poly1 = r1.poly || [[r1.x, r1.y], [r1.x + r1.w, r1.y], [r1.x + r1.w, r1.y + r1.h], [r1.x, r1.y + r1.h]];
// общая стена вертикальная — дверь ставим на неё
const H = 1000; // square canvas
const doorPt = (() => {
let best = null, bd = 1e9;
for (const [x, y] of [[550, 150], [550, 200], [550, 250]]) {
const d2 = Math.hypot(x - c2[0], y - c2[1]);
if (d2 < bd) { bd = d2; best = [x, y]; }
}
return best;
})();
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
...s, openings: [{ id: 'gd', type: 'door', x: doorPt[0] / 1000, y: doorPt[1] / H, angle: 90, length: 0.09 }] })) };
c.requestUpdate(); await c.updateComplete;
// источник детерминированно ставим в центр r1 (двигаем реальную включённую лампу)
const c1 = c._roomCenter(r1);
c._layout = { ...c._layout, [litLight.id]: { s: spId, x: c1[0] / 1000, y: c1[1] / 1000 } };
// радиус 6 м, чтобы дверь заведомо была в зоне досягаемости
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 600 } };
c.requestUpdate(); await c.updateComplete;
// теперь каждый под-контур — отдельный path внутри clipPath
const clipEls = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')];
out.sectorAdded = clipEls.some((cp) => cp.querySelectorAll('path').length >= 2);
// и сектор в том же направлении обхода не выедает комнату: каждый path — один контур
out.singleSubpathPerPath = clipEls.every((cp) => [...cp.querySelectorAll('path')]
.every((p) => ((p.getAttribute('d') || '').match(/M /g) || []).length === 1));
// у входной двери (наружу) сектора нет: дверь на внешней стене r1 (x=min)
const minX = Math.min(...poly1.map((p) => p[0]));
const yMid = (Math.min(...poly1.map((p) => p[1])) + Math.max(...poly1.map((p) => p[1]))) / 2;
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s : ({
...s, openings: [{ id: 'gd2', type: 'door', x: minX / 1000, y: yMid / 1000, angle: 90, length: 0.09 }] })) };
c.requestUpdate(); await c.updateComplete;
const clipEls2 = [...sr().querySelectorAll('defs clipPath[id^="hp-glowclip"]')];
out.entranceNoSector = clipEls2.every((cp) => cp.querySelectorAll('path').length === 1);
// 6б) профиль градиента: плато до 80%, спад на внешних 20%
const stops = [...sr().querySelectorAll('defs radialGradient')][0];
const offs = stops ? [...stops.querySelectorAll('stop')].map((st2) => [st2.getAttribute('offset'), st2.getAttribute('stop-opacity')]) : [];
out.plateau80 = offs.length === 3 && offs[0][0] === '0%' && offs[1][0] === '70%'
&& offs[0][1] === offs[1][1] && offs[2][1] === '0';
// 7а) персональный радиус источника перекрывает глобальный
const litMarkerId = litLight.id;
c._serverCfg = { ...c._serverCfg, markers: [
...(c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId),
{ id: litMarkerId, binding: litLight.bindingKind === 'virtual' ? 'virtual' : litLight.bindingKind + ':' + litLight.bindingRef, glow_radius_cm: 150 },
] };
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
const rOwn = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
out.perSourceRadius = Math.abs(rOwn - c._cmToUnits(150)) < 0.5;
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== litMarkerId) };
c._regSignature = ''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
// 6в) флаг «источник света»: умный выключатель с обычными светильниками
const swDev = c._devices.find((d) => d.space === spId && d.entities.some((e) => e.startsWith('switch.')));
if (swDev) {
const swEid = swDev.entities.find((e) => e.startsWith('switch.'));
c.hass = { ...c.hass, states: { ...c.hass.states, [swEid]: { ...c.hass.states[swEid], state: 'on' } } };
const spotsBefore = sr().querySelectorAll('.glowlayer circle').length;
c._serverCfg = { ...c._serverCfg, markers: [
...(c._serverCfg.markers || []).filter((m) => m.id !== swDev.id),
{ id: swDev.id, binding: swDev.bindingKind + ':' + swDev.bindingRef, is_light: true },
] };
c._regSignature = ''; c._maybeRebuildDevices(); c._saveConfig(); c.requestUpdate(); await c.updateComplete;
out.switchGlows = sr().querySelectorAll('.glowlayer circle').length === spotsBefore + 1;
c._serverCfg = { ...c._serverCfg, markers: (c._serverCfg.markers || []).filter((m) => m.id !== swDev.id) };
c._regSignature = ''; c._maybeRebuildDevices(); c._saveConfig(); c.requestUpdate(); await c.updateComplete;
out.switchGlowsOffByDefault = sr().querySelectorAll('.glowlayer circle').length === spotsBefore;
} else { out.switchGlows = 'no-switch'; out.switchGlowsOffByDefault = 'no-switch'; }
// 7) радиус из настроек: 600 см против 300 см — вдвое больше
const r600 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
c._serverCfg = { ...c._serverCfg, settings: { ...(c._serverCfg.settings || {}), glow_radius_cm: 300 } };
c.requestUpdate(); await c.updateComplete;
const r300 = Number(sr().querySelector('.glowlayer circle')?.getAttribute('r'));
out.radiusReacts = Math.abs(r600 / r300 - 2) < 0.01;
return out;
});
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
checkAll(res, {
"spots": 1,
});
await finish(browser, res);