mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на пару «пространство × язык»), английский статически как слой отката; рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback, общий тост отказа), src/i18n/namespace-language.ts. - Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер только после того, как словарь текущего языка осел; гейт хоста учитывает словари загруженной на нём поверхности (смена языка на лету — ветка warm). Оверлей Zigbee ждёт topology до первой отрисовки. - bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500 (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки ленивости и владения по содержимому. - Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык синхронно, ждут гейт вместо ослабления проверок; пять мутантов. - monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro и export на каждый) и записи манифеста; gzip каждой поверхности при этом уменьшился. Issue: #627 User-Visible: no
157 lines
8.2 KiB
JavaScript
157 lines
8.2 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;
|
|
// #627: ru strings of the editor dialogs are a lazy chunk; the switch to ru
|
|
// above holds the previous frame (inert + aria-busy) until it settles.
|
|
for (let i = 0; i < 250 && c.hasAttribute('aria-busy'); i++) await new Promise((r) => setTimeout(r, 20));
|
|
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')?.style.left === '212px'
|
|
&& sr().querySelector('.tip')?.style.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;
|
|
// #600 §4.2: заливка — сегмент (радиогруппа `space-fill-mode`), свечение —
|
|
// полная строка-тумблер, границы температуры — поля с единицей внутри рамки.
|
|
out.fillRadios = sr().querySelectorAll('input[name="space-fill-mode"]').length;
|
|
out.fillLabels = [...sr().querySelectorAll('input[name="space-fill-mode"]')]
|
|
.map((input) => input.closest('label')?.querySelector('span:last-of-type')?.textContent?.trim());
|
|
out.fillHasNone = out.fillLabels.includes(c._t('fill.none'));
|
|
out.glowToggle = [...sr().querySelectorAll('hp-dialog .hpf-toggle')].some((label) =>
|
|
label.querySelector('.hpf-toggle-title')?.textContent.trim() === c._t('space.glow_enabled')
|
|
&& !!label.querySelector('input[type="checkbox"]'));
|
|
out.tempInputs = sr().querySelectorAll('#space-temp-min, #space-temp-max').length;
|
|
const hpDialog = sr().querySelector('hp-dialog');
|
|
out.dialogWide = !!hpDialog?.hasAttribute('wide') && !!hpDialog.querySelector('.hpf-toggle');
|
|
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 remained 500 px until #600 (Q7):
|
|
// the settings dialogs carry `form-shell` and the reference width of 560.
|
|
checkAll(res, {
|
|
"filledClass": 1,
|
|
"unfilled": 3,
|
|
"tipTemp": 22.4,
|
|
"tipHum": 48,
|
|
"fillRadios": 4,
|
|
"fillLabels": ["Свой цвет", "Zigbee", "Свет", "Температура"],
|
|
"fillHasNone": false,
|
|
"legacyNoneProjection": "custom",
|
|
"legacyNoneAlpha": 0,
|
|
"legacyNoneGlowBases": 4,
|
|
"tempInputs": 2,
|
|
"dialogWidth": 560,
|
|
});
|
|
await finish(browser, res);
|