mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
R2-1 (high): plan replacement committed filesystem state before the config CAS.
The upload wrote the final name and unlinked the other extension, so a rejected
config write left the live plan already replaced — or the stored config
pointing at a deleted file. Uploads now go to <space>.<token>.<ext> and delete
nothing; houseplan/plan/cleanup runs only after the config write is accepted.
The '.' separator is load-bearing: a space id cannot contain one, so cleaning
'f1' can never reach the files of 'f1-attic'.
R2-2: the backend signs at most MAX_SIGN_PATHS (200) per request and ignores
the rest silently, while the card sent its whole cache in one call and trusted
any cached entry forever — past 200 attachments the later ones stopped being
refreshed and expired for good. Requests are chunked to the shared constant,
entries carry their issue time (aging urls keep rendering while a replacement
is fetched, expired ones are dropped), and the cache is pruned to urls the live
config still references.
R2-3: areaClimate() rescanned the whole registry per room and per measurement.
areaClimateMap() classifies once and returns Map<area,{temp,hum}>, memoized on
hass identity so fresh states are always observed. Smoke measurement: 133
registry scans per update with 44 rooms before, 2 after, flat in room count.
Also: smoke_ux_fixes wrote its screenshot to a hard-coded /tmp path and could
not run on Windows.
Tests: smoke_plan_upload_reject, smoke_sign_cap, smoke_climate_once (all fail
on v1.44.8), three backend tests for versioned plan names and cleanup scoping,
unit tests for chunk/referencedContentUrls and areaClimateMap.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
52 lines
3.0 KiB
JavaScript
52 lines
3.0 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' } };
|
||
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 room = sr().querySelector('.room');
|
||
room.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, composed: true, clientX: 200, clientY: 200 }));
|
||
await c.updateComplete;
|
||
out.tipTemp = c._tip?.temp;
|
||
out.tipHasTempLine = (sr().querySelector('.tip')?.textContent || '').includes('средняя температура');
|
||
c._tip = null;
|
||
// диалог: радио заливки, компактные поля, ширина
|
||
c._openSpaceDialog('edit', 'f1'); await c.updateComplete;
|
||
out.fillRadios = sr().querySelectorAll('input[name="fillmode"]').length;
|
||
out.tempInputs = sr().querySelectorAll('.temprange .tempin').length;
|
||
out.dialogWide = !!sr().querySelector('.dialog.wide .srcrow');
|
||
out.dialogWidth = Math.round(sr().querySelector('.dialog').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;
|
||
return out;
|
||
});
|
||
// артефакт для глазами: путь берём у ОС, а не хардкодим unix-овый — на Windows
|
||
// '/tmp/...' указывает в несуществующий C:\tmp и смоук падал, не дойдя до
|
||
// ассертов (портируемость, ревью 2026-07-27)
|
||
await page.screenshot({ path: join(tmpdir(), 'houseplan_ux_dialog.png') }).catch(() => {});
|
||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||
checkAll(res, {
|
||
"filledClass": 1,
|
||
"unfilled": 3,
|
||
"tipTemp": 22.4,
|
||
"fillRadios": 5,
|
||
"tempInputs": 2,
|
||
"dialogWidth": 502,
|
||
});
|
||
await finish(browser, res);
|