mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
151 lines
7.7 KiB
JavaScript
151 lines
7.7 KiB
JavaScript
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
const { page, browser } = await launch({ width: 640, height: 980 }, 2);
|
|
const res = await page.evaluate(async () => {
|
|
const out = {};
|
|
const c = window.__card;
|
|
const sr = () => c.shadowRoot || c.renderRoot;
|
|
c.hass = {
|
|
...c.hass,
|
|
locale: { language: 'ru' },
|
|
states: {
|
|
...c.hass.states,
|
|
'sensor.living_hum': {
|
|
entity_id: 'sensor.living_hum',
|
|
state: '47.6',
|
|
attributes: {
|
|
friendly_name: 'Living humidity',
|
|
device_class: 'humidity',
|
|
unit_of_measurement: '%',
|
|
},
|
|
},
|
|
},
|
|
entities: {
|
|
...c.hass.entities,
|
|
'sensor.living_hum': {
|
|
entity_id: 'sensor.living_hum',
|
|
device_id: 'd_temp',
|
|
platform: 'demo',
|
|
disabled_by: null,
|
|
},
|
|
},
|
|
};
|
|
await c.updateComplete;
|
|
// заливка temp вкл → класс filled и тултип с температурой
|
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : ({ ...s,
|
|
settings: { show_borders: true, show_names: true, fill_mode: 'temp', temp_min: 20, temp_max: 25 } })) };
|
|
c._regSignature=''; c._maybeRebuildDevices(); c.requestUpdate(); await c.updateComplete;
|
|
out.filledClass = sr().querySelectorAll('.room.styled.filled').length; // только living (термометр)
|
|
out.unfilled = sr().querySelectorAll('.room.styled:not(.filled)').length;
|
|
// тултип комнаты: площадь и агрегаты температуры/влажности в стабильном порядке
|
|
const rooms = [...sr().querySelectorAll('.room')];
|
|
const room = rooms[0];
|
|
room.dispatchEvent(new PointerEvent('pointermove', {
|
|
pointerType: 'mouse', bubbles: true, composed: true, clientX: 200, clientY: 200,
|
|
}));
|
|
await c.updateComplete;
|
|
out.tipTemp = c._tip?.temp;
|
|
out.tipHum = c._tip?.hum;
|
|
out.tipHasTempLine = (sr().querySelector('.tip')?.textContent || '').includes('средняя температура');
|
|
out.tipHasHumLine = (sr().querySelector('.tip')?.textContent || '').includes('средняя влажность: 48%');
|
|
const metricLines = [...sr().querySelectorAll('.tip .m')].map((line) => line.textContent.trim());
|
|
const tempLine = metricLines.findIndex((line) => line.includes('средняя температура'));
|
|
const humLine = metricLines.findIndex((line) => line.includes('средняя влажность'));
|
|
const lqiLine = metricLines.findIndex((line) => line.includes(c._t('tip.lqi')));
|
|
out.tipMetricOrder = tempLine >= 0 && humLine === tempLine + 1 && lqiLine === humLine + 1;
|
|
out.tipPositionUnchanged = sr().querySelector('.tip')?.getAttribute('style') === 'left:212px;top:212px';
|
|
// _roomArea consumes the rendered room geometry (0..1000), not the stored
|
|
// normalized config polygon (0..1).
|
|
const expectedArea = c._roomArea(c._spaceModel().rooms[0]);
|
|
out.tipHasArea = !!expectedArea && c._tip?.meta === c._t('tip.area', { value: expectedArea });
|
|
out.tipHasAreaLine = (sr().querySelector('.tip')?.textContent || '').includes(expectedArea);
|
|
|
|
// Комната без источника влажности не получает пустую строку.
|
|
rooms[1].dispatchEvent(new PointerEvent('pointermove', {
|
|
pointerType: 'mouse', bubbles: true, composed: true, clientX: 210, clientY: 210,
|
|
}));
|
|
await c.updateComplete;
|
|
out.noHumidityOmitted = c._tip?.hum == null
|
|
&& !(sr().querySelector('.tip')?.textContent || '').includes('средняя влажность');
|
|
|
|
// Общий device tooltip остаётся без room humidity.
|
|
sr().querySelector('.dev').dispatchEvent(new PointerEvent('pointermove', {
|
|
pointerType: 'mouse', bubbles: true, composed: true, clientX: 220, clientY: 220,
|
|
}));
|
|
await c.updateComplete;
|
|
out.deviceTooltipHasNoHumidity = c._tip?.hum == null
|
|
&& !(sr().querySelector('.tip')?.textContent || '').includes('средняя влажность');
|
|
|
|
// Touch/pen input never creates hover-only tooltips.
|
|
room.dispatchEvent(new PointerEvent('pointerdown', {
|
|
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
|
|
clientX: 230, clientY: 230,
|
|
}));
|
|
c._tip = null;
|
|
room.dispatchEvent(new PointerEvent('pointermove', {
|
|
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
|
|
clientX: 230, clientY: 230,
|
|
}));
|
|
await c.updateComplete;
|
|
out.noHoverSuppressesTooltip = c._tip === null;
|
|
room.dispatchEvent(new PointerEvent('pointerup', {
|
|
pointerType: 'touch', pointerId: 91, bubbles: true, composed: true,
|
|
clientX: 230, clientY: 230,
|
|
}));
|
|
// диалог: радио заливки, компактные поля, ширина
|
|
c._openSpaceDialog('edit', 'f1'); await c.updateComplete;
|
|
out.fillRadios = sr().querySelectorAll('input[name="fillmode"]').length;
|
|
out.fillLabels = [...sr().querySelectorAll('input[name="fillmode"]')]
|
|
// Read the radio's own label only. The selected temperature mode also
|
|
// renders two nested range controls inside this label.
|
|
.map((input) => input.nextElementSibling?.textContent?.trim());
|
|
out.fillHasNone = out.fillLabels.includes(c._t('fill.none'));
|
|
out.glowToggle = [...sr().querySelectorAll('hp-dialog label.srcrow')].some((label) =>
|
|
label.textContent.trim() === c._t('space.glow_enabled')
|
|
&& !!label.querySelector('ha-switch,input[type="checkbox"]'));
|
|
out.tempInputs = sr().querySelectorAll('.temprange .tempin').length;
|
|
const hpDialog = sr().querySelector('hp-dialog');
|
|
out.dialogWide = !!hpDialog?.hasAttribute('wide') && !!hpDialog.querySelector('.srcrow');
|
|
out.dialogWidth = Math.round(hpDialog.shadowRoot.querySelector('.surface').getBoundingClientRect().width);
|
|
// NaN-защита: пустой ввод не ломает границы
|
|
const before = c._spaceDialog.tempMax;
|
|
const fakeEv = { target: { value: '' } };
|
|
// симулируем input с пустым значением через обработчик радио-строки — парсер должен сохранить старое
|
|
const n = parseFloat('');
|
|
out.nanGuard = !Number.isFinite(n) && c._spaceDialog.tempMax === before;
|
|
// Space-level `none` remains readable, but the current dialog offers the
|
|
// ordinary selectable custom colour in its place.
|
|
c._spaceDialog = null;
|
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : ({
|
|
...s, settings: { ...(s.settings || {}), fill_mode: 'none', glow_enabled: true },
|
|
})) };
|
|
c._openSpaceDialog('edit', 'f1');
|
|
await c.updateComplete;
|
|
out.legacyNoneProjection = c._spaceDialog.fillMode;
|
|
out.legacyNoneAlpha = c._spaceDialog.customFill?.a;
|
|
out.legacyNoneGlowBases = sr().querySelectorAll('.glow-base-layer .glow-base').length;
|
|
return out;
|
|
});
|
|
// артефакт для глазами: путь берём у ОС, а не хардкодим unix-овый — на Windows
|
|
// '/tmp/...' указывает в несуществующий C:\tmp и смоук падал, не дойдя до
|
|
// ассертов (портируемость, ревью 2026-07-27)
|
|
await page.screenshot({ path: join(tmpdir(), 'houseplan_ux_dialog.png') }).catch(() => {});
|
|
// Geometry values were re-baselined for the shared border-box hp-dialog in
|
|
// v1.59.2; the usable wide-dialog width remains 500 px.
|
|
checkAll(res, {
|
|
"filledClass": 1,
|
|
"unfilled": 3,
|
|
"tipTemp": 22.4,
|
|
"tipHum": 48,
|
|
"fillRadios": 4,
|
|
"fillLabels": ["Свой цвет", "По силе зигби-сигнала", "По освещению", "По температуре"],
|
|
"fillHasNone": false,
|
|
"legacyNoneProjection": "custom",
|
|
"legacyNoneAlpha": 0,
|
|
"legacyNoneGlowBases": 4,
|
|
"tempInputs": 2,
|
|
"dialogWidth": 500,
|
|
});
|
|
await finish(browser, res);
|