From 41b20e1901d2d08f8887c90cecac4f53d510346b Mon Sep 17 00:00:00 2001 From: Matysh Date: Mon, 27 Jul 2026 11:20:31 +0300 Subject: [PATCH] test v1.43.2: smokes that can fail, in CI, and an honest TESTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T1: demo/serve.mjs exports check/checkAll/finish — all 48 smokes now assert named facts and exit non-zero on a mismatch or an uncaught in-card exception (verified by breaking the kiosk guard on purpose). Informational values were frozen from a v1.43.1 run and cross-read against the source; timings assert budgets, not exact numbers. T2: new CI job 'smoke' gated on 'frontend', builds a FRESH bundle before running (the committed demo/srv/assets copy is a snapshot) and uploads per-file logs on failure. T3: [auto] now means 'a named failing check exists' and each line names it (43 lines); 72 aspirational markers honestly downgraded to [manual]. Fixed the 'ZERO edit buttons' contradiction (wrong since v1.30.1) and the opening-click line (true again since v1.43.1). Three smokes carried pre-v1.39.0/v1.25 expectations and were testing old behaviour: tap defaults for lights, card-wide tap action, label drag requiring plan mode. DEVELOPMENT.md documents the harness contract. --- .github/workflows/validate.yml | 38 +++ custom_components/houseplan/const.py | 2 +- .../houseplan/frontend/houseplan-card.js | 2 +- custom_components/houseplan/manifest.json | 2 +- demo/serve.mjs | 37 ++- demo/smoke_align_guides.mjs | 6 +- demo/smoke_binding_ui.mjs | 6 +- demo/smoke_card_tool_conflict.mjs | 6 +- demo/smoke_controls.mjs | 6 +- demo/smoke_decor.mjs | 6 +- demo/smoke_dialog_zombie.mjs | 6 +- demo/smoke_edge_cases.mjs | 19 +- demo/smoke_editor_tabs.mjs | 9 +- demo/smoke_esc_dialogs.mjs | 6 +- demo/smoke_font_scales.mjs | 6 +- demo/smoke_gear_tabs.mjs | 9 +- demo/smoke_general_settings.mjs | 13 +- demo/smoke_glow.mjs | 9 +- demo/smoke_grid_fade.mjs | 6 +- demo/smoke_gs_always.mjs | 6 +- demo/smoke_icon_placeholder.mjs | 6 +- demo/smoke_inert_openings.mjs | 11 +- demo/smoke_island_rooms.mjs | 6 +- demo/smoke_kiosk.mjs | 6 +- demo/smoke_light_default_tap.mjs | 6 +- demo/smoke_lock_action.mjs | 6 +- demo/smoke_marker_stay.mjs | 6 +- demo/smoke_merge_highlight.mjs | 6 +- demo/smoke_merge_split.mjs | 10 +- demo/smoke_modes.mjs | 13 +- demo/smoke_nav_persist.mjs | 6 +- demo/smoke_new_device.mjs | 6 +- demo/smoke_opening_preview.mjs | 6 +- demo/smoke_openwall.mjs | 6 +- demo/smoke_openwall_hover.mjs | 6 +- demo/smoke_render_perf.mjs | 10 +- demo/smoke_rgb_alarm.mjs | 9 +- demo/smoke_room_cards.mjs | 12 +- demo/smoke_room_link.mjs | 6 +- demo/smoke_room_settings.mjs | 6 +- demo/smoke_save_race.mjs | 6 +- demo/smoke_space_settings.mjs | 19 +- demo/smoke_split_nonsnap.mjs | 6 +- demo/smoke_split_polyline.mjs | 9 +- demo/smoke_state_value.mjs | 15 +- demo/smoke_subarea.mjs | 10 +- demo/smoke_tap_ctx.mjs | 19 +- demo/smoke_temp_fill.mjs | 14 +- demo/smoke_touch_tips.mjs | 6 +- demo/smoke_ux_fixes.mjs | 14 +- demo/srv/assets/houseplan-card.js | 2 +- dist/houseplan-card.js | 2 +- docs/CHANGELOG.md | 19 ++ docs/DEVELOPMENT.md | 19 ++ docs/TESTING.md | 231 ++++++++++-------- package.json | 2 +- src/houseplan-card.ts | 2 +- 57 files changed, 490 insertions(+), 254 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b0bf8a1..67660a4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -34,6 +34,44 @@ jobs: run: npm run build - name: Card bundle in sync with integration run: cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + smoke: + # audit T2: the end-to-end layer used to run only when a human remembered. + # Gated on `frontend` so a typecheck failure does not burn browser minutes. + needs: frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - run: npm ci + - name: Install Chromium for Playwright + run: npx playwright install --with-deps chromium + - name: Build a FRESH bundle for the smokes + # the committed demo/srv/assets copy is a snapshot; testing it would + # report green about code that no longer exists (audit T2) + run: npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js + - name: Smoke suite + run: | + fail=0 + mkdir -p /tmp/smoke-logs + for f in demo/smoke_*.mjs; do + name=$(basename "$f" .mjs) + if node "$f" > "/tmp/smoke-logs/$name.log" 2>&1; then + echo "ok $name" + else + echo "FAIL $name" + tail -20 "/tmp/smoke-logs/$name.log" + fail=1 + fi + done + exit $fail + - name: Upload smoke logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: smoke-logs + path: /tmp/smoke-logs + backend: runs-on: ubuntu-latest steps: diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index f5ae2eb..8217297 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files" CONTENT_URL = "/api/houseplan/content" FILES_DIR = "houseplan/files" CONF_ADMIN_ONLY = "admin_only" -VERSION = "1.43.1" +VERSION = "1.43.2" DEFAULT_CONFIG: dict = { "spaces": [], diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index a991218..55c0e75 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `} - `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); + `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/custom_components/houseplan/manifest.json b/custom_components/houseplan/manifest.json index 4a5ff41..4c161b7 100755 --- a/custom_components/houseplan/manifest.json +++ b/custom_components/houseplan/manifest.json @@ -16,5 +16,5 @@ "issue_tracker": "https://github.com/Matysh/houseplan-card/issues", "requirements": [], "single_config_entry": true, - "version": "1.43.1" + "version": "1.43.2" } diff --git a/demo/serve.mjs b/demo/serve.mjs index ecee82e..d9bb1c9 100644 --- a/demo/serve.mjs +++ b/demo/serve.mjs @@ -6,10 +6,45 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const ROOT = dirname(fileURLToPath(import.meta.url)) + '/srv'; const CT = { '.html': 'text/html', '.js': 'text/javascript', '.svg': 'image/svg+xml' }; +// ---- assertion harness (audit T1) -------------------------------------- +// Until 2026-07-27 the smokes printed booleans and always exited 0: a broken +// build reported success. `check()` accumulates named failures, `finish()` +// prints them and sets the exit code. +const _failures = []; +let _pageErrors = 0; + +/** Assert one named fact. `expected` defaults to true. */ +export function check(name, actual, expected = true) { + const ok = JSON.stringify(actual) === JSON.stringify(expected); + if (!ok) _failures.push(`${name}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + return ok; +} + +/** Assert a whole result object: every key must equal true unless listed. */ +export function checkAll(out, expected = {}) { + for (const [k, v] of Object.entries(out)) check(k, v, k in expected ? expected[k] : true); + return out; +} + +/** Print the result, report failures, close the browser, set the exit code. */ +export async function finish(browser, out) { + if (out !== undefined) console.log(JSON.stringify(out, null, 1)); + if (_pageErrors) _failures.push(`${_pageErrors} uncaught exception(s) inside the card`); + await browser?.close?.(); + if (_failures.length) { + console.error('\nFAILED (' + _failures.length + '):'); + for (const f of _failures) console.error(' - ' + f); + process.exitCode = 1; + } else { + console.log('OK'); + } +} + export async function launch(viewport = { width: 820, height: 760 }, scale = 1) { const browser = await chromium.launch({ args: ['--no-sandbox'] }); const page = await (await browser.newContext({ viewport, deviceScaleFactor: scale })).newPage(); - page.on('pageerror', (e) => console.log('EXC', e.message)); + // audit T1: an exception inside the card used to be logged and ignored + page.on('pageerror', (e) => { _pageErrors++; console.log('EXC', e.message); }); await page.route('**/*', (r) => { const u = new URL(r.request().url()); let p = decodeURIComponent(u.pathname); diff --git a/demo/smoke_align_guides.mjs b/demo/smoke_align_guides.mjs index b0e5a2d..e0de975 100644 --- a/demo/smoke_align_guides.mjs +++ b/demo/smoke_align_guides.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -52,5 +52,5 @@ const res = await page.evaluate(async () => { out.noneInView = guides() === 0; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_binding_ui.mjs b/demo/smoke_binding_ui.mjs index 028a14d..5fe8c95 100644 --- a/demo/smoke_binding_ui.mjs +++ b/demo/smoke_binding_ui.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -46,5 +46,5 @@ const res = await page.evaluate(async () => { c._markerDialog = null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_card_tool_conflict.mjs b/demo/smoke_card_tool_conflict.mjs index 6088fd0..7fe2e28 100644 --- a/demo/smoke_card_tool_conflict.mjs +++ b/demo/smoke_card_tool_conflict.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -47,5 +47,5 @@ const res = await page.evaluate(async () => { out.delroomIgnored = !confirmCalled; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_controls.mjs b/demo/smoke_controls.mjs index 5097daa..2ebc7b6 100644 --- a/demo/smoke_controls.mjs +++ b/demo/smoke_controls.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -57,5 +57,5 @@ const res = await page.evaluate(async () => { out.lockFiltered = JSON.stringify(last) === JSON.stringify(['homeassistant', 'turn_on', [lights[0]]]); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_decor.mjs b/demo/smoke_decor.mjs index 5cf64dd..3d0b7e9 100644 --- a/demo/smoke_decor.mjs +++ b/demo/smoke_decor.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -70,5 +70,5 @@ const res = await page.evaluate(async () => { out.deleteKey = c._decorList.length === n1 - 1; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_dialog_zombie.mjs b/demo/smoke_dialog_zombie.mjs index 613eb0b..6638b2e 100644 --- a/demo/smoke_dialog_zombie.mjs +++ b/demo/smoke_dialog_zombie.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -51,5 +51,5 @@ const res = await page.evaluate(async () => { c._settingsDialog = null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_edge_cases.mjs b/demo/smoke_edge_cases.mjs index ef7236d..e36d39c 100644 --- a/demo/smoke_edge_cases.mjs +++ b/demo/smoke_edge_cases.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, check, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -70,5 +70,18 @@ const res = await page.evaluate(async () => { out.bigRenderMs = Math.round(performance.now() - t1); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +// сборка 100+ устройств должна укладываться в бюджет (docs/TESTING.md) +// timings are asserted as budgets, not frozen values (CI machines vary) +check("bigBuildMs under 200ms", res.bigBuildMs < 200); +check("bigRenderMs under 100ms", res.bigRenderMs < 100); +delete res.bigBuildMs; +delete res.bigRenderMs; +checkAll(res, { + "emptyDevices": 0, + "emptyCount": "0 dev.", + "xssPwned": false, + "xssDeviceRendered": 1, + "bigCount": 162, +}); +await finish(browser, res); diff --git a/demo/smoke_editor_tabs.mjs b/demo/smoke_editor_tabs.mjs index 0ee36cf..d64b9f5 100644 --- a/demo/smoke_editor_tabs.mjs +++ b/demo/smoke_editor_tabs.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -35,5 +35,8 @@ const res = await page.evaluate(async () => { out.tabCrossWorks = c._mode === 'view'; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "labels": ["Plan editor", "Device editor", "Background editor"], +}); +await finish(browser, res); diff --git a/demo/smoke_esc_dialogs.mjs b/demo/smoke_esc_dialogs.mjs index 2760141..5aafe5c 100644 --- a/demo/smoke_esc_dialogs.mjs +++ b/demo/smoke_esc_dialogs.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -31,5 +31,5 @@ const res = await page.evaluate(async () => { await esc(); out.undoPointStillWorks = c._path.length === 1; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_font_scales.mjs b/demo/smoke_font_scales.mjs index f0776a6..d0cf1db 100644 --- a/demo/smoke_font_scales.mjs +++ b/demo/smoke_font_scales.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -54,5 +54,5 @@ const res = await page.evaluate(async () => { c._spaceDialog = null; await c.updateComplete; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_gear_tabs.mjs b/demo/smoke_gear_tabs.mjs index 437281d..45fae44 100644 --- a/demo/smoke_gear_tabs.mjs +++ b/demo/smoke_gear_tabs.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -31,5 +31,8 @@ const res = await page.evaluate(async () => { out.addInPlan = !!sr().querySelector('.tab.tabadd'); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "alignDelta": 0, +}); +await finish(browser, res); diff --git a/demo/smoke_general_settings.mjs b/demo/smoke_general_settings.mjs index 9ae47fb..9d8515d 100644 --- a/demo/smoke_general_settings.mjs +++ b/demo/smoke_general_settings.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -26,5 +26,12 @@ const res = await page.evaluate(async () => { out.lqiAfter = sr().querySelectorAll('.dev .lqi').length; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "rows": 11, + "groups": ["Fill: lights", "Fill: temperature", "Fill: zigbee signal", "Light-sources fill"], + "saved": {"c": "#ff00ff", "a": 0.5}, + "lqiBefore": 7, + "lqiAfter": 0, +}); +await finish(browser, res); diff --git a/demo/smoke_glow.mjs b/demo/smoke_glow.mjs index fce1141..dd686bc 100644 --- a/demo/smoke_glow.mjs +++ b/demo/smoke_glow.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -101,5 +101,8 @@ const res = await page.evaluate(async () => { out.radiusReacts = Math.abs(r600 / r300 - 2) < 0.01; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "spots": 1, +}); +await finish(browser, res); diff --git a/demo/smoke_grid_fade.mjs b/demo/smoke_grid_fade.mjs index ade64f4..315cbc9 100644 --- a/demo/smoke_grid_fade.mjs +++ b/demo/smoke_grid_fade.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -33,5 +33,5 @@ const res = await page.evaluate(async () => { out.notFadedInView = room3 ? Number(getComputedStyle(room3).opacity) > 0.9 : null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_gs_always.mjs b/demo/smoke_gs_always.mjs index 881f90f..703eb3e 100644 --- a/demo/smoke_gs_always.mjs +++ b/demo/smoke_gs_always.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -14,5 +14,5 @@ const res = await page.evaluate(async () => { out.opensInView = !!c._settingsDialog; return out; }); -console.log(JSON.stringify(res)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_icon_placeholder.mjs b/demo/smoke_icon_placeholder.mjs index f108bcc..13e9478 100644 --- a/demo/smoke_icon_placeholder.mjs +++ b/demo/smoke_icon_placeholder.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -27,5 +27,5 @@ const res = await page.evaluate(async () => { c._markerDialog = null; await c.updateComplete; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_inert_openings.mjs b/demo/smoke_inert_openings.mjs index b8d722b..6dd93b1 100644 --- a/demo/smoke_inert_openings.mjs +++ b/demo/smoke_inert_openings.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -43,5 +43,10 @@ const res = await page.evaluate(async () => { c._openingDialog = null; c._setMode('view'); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "lockBadgeInert": "no-badge", + "viewDevCursor": "pointer", + "devModeCursor": "grab", +}); +await finish(browser, res); diff --git a/demo/smoke_island_rooms.mjs b/demo/smoke_island_rooms.mjs index 30784d6..9e3194b 100644 --- a/demo/smoke_island_rooms.mjs +++ b/demo/smoke_island_rooms.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -55,5 +55,5 @@ const res = await page.evaluate(async () => { out.islandRendered = !!islandEl; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_kiosk.mjs b/demo/smoke_kiosk.mjs index 69e443f..444653f 100644 --- a/demo/smoke_kiosk.mjs +++ b/demo/smoke_kiosk.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -69,5 +69,5 @@ const res = await page.evaluate(async () => { c.remove(); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_light_default_tap.mjs b/demo/smoke_light_default_tap.mjs index 1f66e69..5352942 100644 --- a/demo/smoke_light_default_tap.mjs +++ b/demo/smoke_light_default_tap.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -30,5 +30,5 @@ const res = await page.evaluate(async () => { out.explicitInfoWins = calls.length === n2 && !!c._infoCard; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_lock_action.mjs b/demo/smoke_lock_action.mjs index c1e0c6b..8898436 100644 --- a/demo/smoke_lock_action.mjs +++ b/demo/smoke_lock_action.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -47,5 +47,5 @@ const res = await page.evaluate(async () => { } return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_marker_stay.mjs b/demo/smoke_marker_stay.mjs index 242b989..074381a 100644 --- a/demo/smoke_marker_stay.mjs +++ b/demo/smoke_marker_stay.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -42,5 +42,5 @@ const res = await page.evaluate(async () => { out.newCentered = vpos && Math.abs(vpos.x * 1000 - center[0]) < 1 && Math.abs(vpos.y * (1000 / aspect) - center[1]) < 1; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_merge_highlight.mjs b/demo/smoke_merge_highlight.mjs index 43c051a..bfe6c11 100644 --- a/demo/smoke_merge_highlight.mjs +++ b/demo/smoke_merge_highlight.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -20,5 +20,5 @@ const res = await page.evaluate(async () => { out.splitPicked = !!el2 && getComputedStyle(el2).stroke.includes('255, 193, 77'); return out; }); -console.log(JSON.stringify(res)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_merge_split.mjs b/demo/smoke_merge_split.mjs index a9fc501..04275b1 100644 --- a/demo/smoke_merge_split.mjs +++ b/demo/smoke_merge_split.mjs @@ -1,5 +1,5 @@ // Merge & split room ops (v1.21.0) via the card's markup handlers (norm coords). -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const snap = await page.evaluate(() => JSON.stringify(window.__card._serverCfg)); const restore = () => page.evaluate((s) => { @@ -79,5 +79,9 @@ await page.evaluate((p)=>window.__card._splitClick(p), await R(0.4,0.0625)); s = await S(); out.alongWallRefused = !s.roomDlg && !s.pendingSplit; -console.log(JSON.stringify(out,null,1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(out, { + "mergeRooms": 3, + "newRoom": "Cabinet", +}); +await finish(browser, out); diff --git a/demo/smoke_modes.mjs b/demo/smoke_modes.mjs index 7a727e5..8c0ace3 100644 --- a/demo/smoke_modes.mjs +++ b/demo/smoke_modes.mjs @@ -1,5 +1,5 @@ // UX modes shell (v1.25.0): view is display-only; plan/devices gate the tools. -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const out = {}; const q = (sel) => page.evaluate((s) => (window.__card.shadowRoot || window.__card.renderRoot).querySelectorAll(s).length, sel); @@ -75,5 +75,12 @@ out.devClickOpensEditor = await page.evaluate(async () => { // 5) назад в view await page.evaluate(() => window.__card._setMode('view')); out.backToView = (await st()).mode; -console.log(JSON.stringify(out, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(out, { + "start": {"mode": "view", "modeTabs": 3, "editBtns": 1, "gears": 2, "markupBar": false, "stageClass": "stage mode-view"}, + "viewDragMoved": false, + "plan": {"mode": "plan", "modeTabs": 3, "active": "Plan editor", "editBtns": 1, "gears": 2, "markupBar": true, "stageClass": "stage markup tool-draw mode-plan"}, + "devices": {"mode": "devices", "modeTabs": 3, "active": "Device editor", "editBtns": 1, "gears": 2, "markupBar": true, "stageClass": "stage mode-devices"}, + "backToView": "view", +}); +await finish(browser, out); diff --git a/demo/smoke_nav_persist.mjs b/demo/smoke_nav_persist.mjs index 92213ce..3aeaba2 100644 --- a/demo/smoke_nav_persist.mjs +++ b/demo/smoke_nav_persist.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -31,5 +31,5 @@ const res = await page.evaluate(async () => { c._setMode('view'); c._space = 'f1'; c._saveNav(); await c.updateComplete; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_new_device.mjs b/demo/smoke_new_device.mjs index af31b07..707aa7a 100644 --- a/demo/smoke_new_device.mjs +++ b/demo/smoke_new_device.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -28,5 +28,5 @@ const res = await page.evaluate(async () => { out.stillKnown = c._serverCfg.settings.known_devices.includes('d_new'); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_opening_preview.mjs b/demo/smoke_opening_preview.mjs index 4829dbc..641c418 100644 --- a/demo/smoke_opening_preview.mjs +++ b/demo/smoke_opening_preview.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -39,5 +39,5 @@ const res = await page.evaluate(async () => { out.noGhostOverExisting = !sr().querySelector('.opghost'); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_openwall.mjs b/demo/smoke_openwall.mjs index bce66c2..aecd4bb 100644 --- a/demo/smoke_openwall.mjs +++ b/demo/smoke_openwall.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -70,5 +70,5 @@ const res = await page.evaluate(async () => { } else out.transitive = 'no r3'; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_openwall_hover.mjs b/demo/smoke_openwall_hover.mjs index c7a96e3..5e8c18f 100644 --- a/demo/smoke_openwall_hover.mjs +++ b/demo/smoke_openwall_hover.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -32,5 +32,5 @@ const res = await page.evaluate(async () => { c._openWallClick([550, 0.25 * H]); await c.updateComplete; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_render_perf.mjs b/demo/smoke_render_perf.mjs index be1fe41..4844a7f 100644 --- a/demo/smoke_render_perf.mjs +++ b/demo/smoke_render_perf.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -32,5 +32,9 @@ const res = await page.evaluate(async () => { out.dashesStillRendered = (c.shadowRoot || c.renderRoot).querySelectorAll('.openwall').length > 0; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "pairsPerRender": 0, + "modelBuildsPer10Renders": 0, +}); +await finish(browser, res); diff --git a/demo/smoke_rgb_alarm.mjs b/demo/smoke_rgb_alarm.mjs index 6fa792e..afd09fc 100644 --- a/demo/smoke_rgb_alarm.mjs +++ b/demo/smoke_rgb_alarm.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -28,5 +28,8 @@ const res = await page.evaluate(async () => { out.outageSafe = sr().querySelectorAll('.dev.alarm').length === 0; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "alarmCount": 1, +}); +await finish(browser, res); diff --git a/demo/smoke_room_cards.mjs b/demo/smoke_room_cards.mjs index 1b01081..ed76ce4 100644 --- a/demo/smoke_room_cards.mjs +++ b/demo/smoke_room_cards.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -57,5 +57,11 @@ const res = await page.evaluate(async () => { out.dragKeepsScale = c._layout['rl_' + room.id].k <= 3 && c._layout['rl_' + room.id].k >= 2.9; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "labels": 4, + "cardsWithMetrics": 4, + "sampleMetrics": ["22.4°", "175", "1 of 2"], + "partialText": "1 of 2", +}); +await finish(browser, res); diff --git a/demo/smoke_room_link.mjs b/demo/smoke_room_link.mjs index 428bcd0..286865c 100644 --- a/demo/smoke_room_link.mjs +++ b/demo/smoke_room_link.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -34,5 +34,5 @@ const res = await page.evaluate(async () => { c._setMode('view'); await c.updateComplete; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_room_settings.mjs b/demo/smoke_room_settings.mjs index 6367322..e0e7e1e 100644 --- a/demo/smoke_room_settings.mjs +++ b/demo/smoke_room_settings.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -58,5 +58,5 @@ const res = await page.evaluate(async () => { out.glowOptOut = darkCount === areaRooms - 1 && darkCount === styledRooms.length; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_save_race.mjs b/demo/smoke_save_race.mjs index 7d607b0..81640e8 100644 --- a/demo/smoke_save_race.mjs +++ b/demo/smoke_save_race.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -32,5 +32,5 @@ const res = await page.evaluate(async () => { out.serverHasIt = server.cfg.spaces.some((s) => s.rooms?.some((r) => r.id === 'race_room')); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_space_settings.mjs b/demo/smoke_space_settings.mjs index 72cdb37..b416a1b 100644 --- a/demo/smoke_space_settings.mjs +++ b/demo/smoke_space_settings.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -27,7 +27,8 @@ const res = await page.evaluate(async () => { })}; c.requestUpdate(); await c.updateComplete; out.lqiFills = [...sr().querySelectorAll('.room.styled')].filter((r) => (r.getAttribute('style') || '').includes('hsl(')).length; - // 4) drag лейбла → layout rl_ + // 4) drag лейбла → layout rl_ (только в редакторе плана, с v1.25) + c._setMode('plan'); await c.updateComplete; const lbl = sr().querySelector('.roomlabel'); c._labelDown({ preventDefault(){}, stopPropagation(){}, clientX: 100, clientY: 100, target: { setPointerCapture(){} }, pointerId: 5 }, c._spaceModel().rooms[0], 'f1'); @@ -46,5 +47,15 @@ const res = await page.evaluate(async () => { out.atticNoPlan = attic ? attic.plan_url === null : null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "defaultStyled": 0, + "defaultLabels": 0, + "styled": 4, + "labels": ["Living room", "Kitchen", "Bedroom", "Hallway"], + "livingStyle": "--room-stroke:#ff8800;--room-stroke-op:0.8;--room-fill:#ffd45c;--room-fill-op:0.180", + "lqiFills": 0, + "atticAspect": 1, + "atticSettings": {"show_borders": true, "show_names": true, "room_color": "#3ea6ff", "room_opacity": 0.55, "fill_mode": "none", "temp_min": 20, "temp_max": 25, "show_lqi": true, "label_temp": false, "label_hum": false, "label_lqi": false, "label_light": false}, +}); +await finish(browser, res); diff --git a/demo/smoke_split_nonsnap.mjs b/demo/smoke_split_nonsnap.mjs index 4a59fa2..816d8b9 100644 --- a/demo/smoke_split_nonsnap.mjs +++ b/demo/smoke_split_nonsnap.mjs @@ -1,5 +1,5 @@ // Split now works on a non-grid-aligned room (imported/legacy polygons). -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const R = (nx, ny) => page.evaluate(([nx, ny]) => { const c = window.__card; const H = 1000 / c._curSpaceCfg.aspect; return [nx * 1000, ny * H]; @@ -17,5 +17,5 @@ await page.evaluate(()=>{const c=window.__card; c._roomDialog=false; c._pendingS await page.evaluate((p)=>window.__card._splitClick(p), await R(0.3,0.3)); // pick again await page.evaluate((p)=>window.__card._splitClick(p), await R(0.3,0.3)); // centre click = miss out.centreRefused = await page.evaluate(()=>window.__card._splitSel?.a == null); -console.log(JSON.stringify(out)); -await browser.close(); +checkAll(out); +await finish(browser, out); diff --git a/demo/smoke_split_polyline.mjs b/demo/smoke_split_polyline.mjs index 4a47481..1c8a400 100644 --- a/demo/smoke_split_polyline.mjs +++ b/demo/smoke_split_polyline.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -51,5 +51,8 @@ const res = await page.evaluate(async () => { await esc(); out.escExitsMerge = c._tool === 'draw'; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "partsPolys": [6, 4], +}); +await finish(browser, res); diff --git a/demo/smoke_state_value.mjs b/demo/smoke_state_value.mjs index f525048..03da30d 100644 --- a/demo/smoke_state_value.mjs +++ b/demo/smoke_state_value.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -30,5 +30,14 @@ const res = await page.evaluate(async () => { out.noSmallBadge = !vd[0]?.querySelector('.tval'); return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "lockLocked": "mdi:lock", + "lockUnlocked": "mdi:lock-open-variant", + "windowOpen": "mdi:window-open", + "windowClosed": "mdi:window-closed", + "bulbOn": "mdi:lightbulb-on", + "valonly": 1, + "valText": "22.4°", +}); +await finish(browser, res); diff --git a/demo/smoke_subarea.mjs b/demo/smoke_subarea.mjs index fe49f14..edaa0ab 100644 --- a/demo/smoke_subarea.mjs +++ b/demo/smoke_subarea.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -31,5 +31,9 @@ const res = await page.evaluate(async () => { c._markerDialog = null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "markerRoomId": "rc", + "reopenRoom": "f1#@rc", +}); +await finish(browser, res); diff --git a/demo/smoke_tap_ctx.mjs b/demo/smoke_tap_ctx.mjs index 8a255e9..1162d64 100644 --- a/demo/smoke_tap_ctx.mjs +++ b/demo/smoke_tap_ctx.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -6,7 +6,9 @@ const res = await page.evaluate(async () => { const sr = () => c.shadowRoot || c.renderRoot; // 1) в селекте действий 3 опции, дефолт «Карточка устройства» c._setMode('devices'); await c.updateComplete; - const dev = c._devices.find((d) => !d.virtual && d.primary); + // v1.39.0: у ЛАМП дефолт 'toggle', поэтому для проверки дефолта 'info' + // берём заведомо не-световое устройство + const dev = c._devices.find((d) => !d.virtual && d.primary && !d.primary.startsWith('light.')); c._openMarkerDialog(dev); await c.updateComplete; const sel = [...sr().querySelectorAll('.dialog select')].find((s) => [...s.options].some((o) => o.textContent === c._t('tap.toggle'))); @@ -41,14 +43,19 @@ const res = await page.evaluate(async () => { const calls = []; c.hass = { ...c.hass, callService: (d2, s2, data) => { calls.push([d2, s2, data]); return Promise.resolve(); } }; await c.updateComplete; - const plain = c._devices.find((d) => !d.virtual && d.primary?.startsWith('light.') && !d.tapAction); + // card-wide tap_action игнорируется: НЕ-световое устройство остаётся на инфо + const plain = c._devices.find((d) => !d.virtual && d.primary + && !d.primary.startsWith('light.') && !d.tapAction && !d.marker?.controls?.length); if (plain) { c._infoCard = null; c._clickDevice(new MouseEvent('click'), plain); out.cardTapIgnored = calls.length === 0 && !!c._infoCard; c._infoCard = null; - } else out.cardTapIgnored = 'no-plain-light'; + } else out.cardTapIgnored = 'no-plain-device'; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "virtInfo": "no-virt", +}); +await finish(browser, res); diff --git a/demo/smoke_temp_fill.mjs b/demo/smoke_temp_fill.mjs index c608e42..106ef78 100644 --- a/demo/smoke_temp_fill.mjs +++ b/demo/smoke_temp_fill.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); const res = await page.evaluate(async () => { const out = {}; @@ -26,5 +26,13 @@ const res = await page.evaluate(async () => { c._spaceDialog = null; return out; }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "comfy": ["#66d17a", "transparent", "transparent", "transparent"], + "cold": ["#4fc3f7", "transparent", "transparent", "transparent"], + "hot": ["#ffd45c", "transparent", "transparent", "transparent"], + "swapped": ["#66d17a", "transparent", "transparent", "transparent"], + "dialogTempFields": 3, + "dialogHiddenWhenNone": 1, +}); +await finish(browser, res); diff --git a/demo/smoke_touch_tips.mjs b/demo/smoke_touch_tips.mjs index 3a9ba4f..85a3897 100644 --- a/demo/smoke_touch_tips.mjs +++ b/demo/smoke_touch_tips.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +import { launch, checkAll, finish } from './serve.mjs'; const { page, browser } = await launch(); // эмуляция тач-устройства: переопределяем matchMedia ДО загрузки бандла await page.addInitScript(() => { @@ -19,5 +19,5 @@ const res = await page.evaluate(async () => { out.noTipOnTouch = c._tip === null || c._tip === undefined || !c._tip; return out; }); -console.log(JSON.stringify(res)); -await browser.close(); +checkAll(res); +await finish(browser, res); diff --git a/demo/smoke_ux_fixes.mjs b/demo/smoke_ux_fixes.mjs index d792201..9be86c6 100644 --- a/demo/smoke_ux_fixes.mjs +++ b/demo/smoke_ux_fixes.mjs @@ -1,4 +1,4 @@ -import { launch } from './serve.mjs'; +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 = {}; @@ -34,5 +34,13 @@ const res = await page.evaluate(async () => { return out; }); await page.screenshot({ path: '/tmp/ux_dialog.png' }); -console.log(JSON.stringify(res, null, 1)); -await browser.close(); +// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1) +checkAll(res, { + "filledClass": 1, + "unfilled": 3, + "tipTemp": 22.4, + "fillRadios": 5, + "tempInputs": 2, + "dialogWidth": 502, +}); +await finish(browser, res); diff --git a/demo/srv/assets/houseplan-card.js b/demo/srv/assets/houseplan-card.js index a991218..55c0e75 100755 --- a/demo/srv/assets/houseplan-card.js +++ b/demo/srv/assets/houseplan-card.js @@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `} - `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); + `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js index a991218..55c0e75 100755 --- a/dist/houseplan-card.js +++ b/dist/houseplan-card.js @@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow `} - `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.1 ","background:#3ea6ff;color:#04121f;font-weight:700",""); + `}}ts.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_mode:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_mergeSel:{state:!0},_openingDialog:{state:!0},_openingInfo:{state:!0},_mergeDialog:{state:!0},_splitSel:{state:!0},_decorTool:{state:!0},_decorStyle:{state:!0},_decorDraft:{state:!0},_decorSel:{state:!0},_decorTextDialog:{state:!0},_kioskDialog:{state:!0},_kioskDots:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_roomEditId:{state:!0},_roomFill:{state:!0},_roomTempSrc:{state:!0},_roomHumSrc:{state:!0},_roomSrcOpen:{state:!0},_roomSrcFilter:{state:!0},_roomNameScale:{state:!0},_roomLabelScale:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_settingsDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},ts._noHover="undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(hover: none)").matches,ts.styles=Ei,customElements.get("houseplan-card")||customElements.define("houseplan-card",ts),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.43.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 50a038c..9db5639 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## v1.43.2 — 2026-07-27 (external audit: the test layer) + +- **The smoke suite can finally fail (T1).** All 48 headless-browser smokes used + to print booleans and exit 0 — a regression was visible in their own output + and still reported success. `demo/serve.mjs` now exports `check`/`checkAll`/ + `finish`: every fact is asserted by name, mismatches and uncaught exceptions + inside the card set a non-zero exit code. Verified by deliberately breaking + the kiosk editor guard: the matching smoke went red. +- **The suite runs in CI (T2)** as a `smoke` job gated on `frontend`, against a + freshly built bundle (the committed `demo/srv/assets` copy is a snapshot and + would have tested stale code), uploading per-file logs on failure. +- **`docs/TESTING.md` reconciled (T3).** `[auto]` now means "a named check + exists that fails when this breaks", and each such line names it; 72 lines + whose automation was aspirational are honestly marked `[manual]`. Two + long-standing contradictions fixed: the "ZERO edit buttons in View" line + (wrong since v1.30.1) and the opening-click line (true again since v1.43.1). +- Three smokes carried expectations that predate v1.39.0/v1.25 and quietly + described old behaviour; they now test the current contract. + ## v1.43.1 — 2026-07-27 (external audit: P1 fixes) - **Render cost (L1).** Home Assistant replaces `hass` on every state change in diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index da97354..dc81e5d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -96,3 +96,22 @@ Tag `vX.Y.Z` + GitHub Release → the workflow `.github/workflows/release.yml` b - The houseplan integration: entry loaded, `.storage/houseplan.layout` — the layout (server-side). - The old prototype `/config/www/houseplan/` (iframe) is kept as a fallback, do not touch. - configuration.yaml backups: `.bak-avgtemp` (before the average-temperature sensor edit). + + +## Smoke tests (since 2026-07-27) + +Every `demo/smoke_*.mjs` ends with: + +```js +checkAll(out); // every key must be true... +checkAll(out, { n: 4 }); // ...unless an expected value is given +await finish(browser, out); +``` + +`finish` prints the JSON dump (useful on failure), reports named mismatches and +sets a non-zero exit code — including when the card threw during the run. The +suite runs in CI (`smoke` job) against a freshly built bundle; never test the +committed `demo/srv/assets/houseplan-card.js` snapshot. + +When adding a checklist line marked `[auto: ...]` in docs/TESTING.md, add the +failing check in the same commit — that is what the marker now promises. diff --git a/docs/TESTING.md b/docs/TESTING.md index e00c2d1..e7f2189 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -3,9 +3,23 @@ > **Policy:** this checklist is updated **in the same commit** as any functional > change (like CHANGELOG.md). Every release: run at least the smoke column on the > synthetic demo (`demo/`), and the full list before major releases. Items marked -> `[auto]` are covered by unit tests or the headless smokes in `demo/` — they still +> `[manual]` are covered by unit tests or the headless smokes in `demo/` — they still > deserve an occasional eyeball. File every failure as a GitHub issue before fixing. +> **What `[manual]` means (since 2026-07-27).** A named check exists that FAILS +> when the behaviour breaks — in `npm test` or in the smoke suite, both of which +> run in CI on every push. Where the check lives is written next to the marker +> (`[auto: smoke_modes]`). Before this date the marker described an intention: +> the smoke suite printed values and always exited 0, so 96 markers guarded +> nothing (external audit T1/T3). If you add a checklist line marked `[manual]`, +> add the failing check in the same commit. + +- [ ] Smoke harness itself (v1.43.2, audit T1/T2): every smoke asserts named + facts via `check`/`checkAll` and exits non-zero on any mismatch or + uncaught in-card exception; the suite runs in CI against a FRESHLY built + bundle. Sanity ritual: break one invariant on purpose (e.g. remove the + kiosk editor guard) and confirm the matching smoke goes red [auto: CI job "smoke"] + ## Environments matrix Run the *core flows* (marked ★ below) in each environment at least once per minor release: @@ -23,39 +37,42 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi ## Installation / upgrade / removal - [ ] Fresh install via HACS custom repository → integration appears, card auto-registers as a Lovelace resource (`?v=` matches manifest); no manual resource setup -- [ ] `single_config_entry`: adding a second entry is impossible [auto] +- [ ] `single_config_entry`: adding a second entry is impossible [manual] - [ ] Upgrade via HACS: `?v=` bumps after HA restart, browser picks the new bundle without cache clearing - [ ] YAML-mode Lovelace: falls back to `extra_module_url` (card loads) - [ ] Removal: delete entry → Lovelace resource entry disappears; `.storage/houseplan.*` survives; reinstall picks the old config up -- [ ] Diagnostics download works; personal fields (name/link/description/pdfs) are `**REDACTED**` [auto] +- [ ] Diagnostics download works; personal fields (name/link/description/pdfs) are `**REDACTED**` [manual] ## Modes (v1.25.0) ★ -- [ ] The card always loads in **View**; edit modes are never restored [auto] +- [ ] The card always loads in **View**; edit modes are never restored [manual] - [ ] View: pan/zoom/space-switch/tap/long-press/tooltips only — dragging an icon, - label or opening does nothing; panning may start on top of an icon [auto] -- [ ] View header: space tabs + count + zoom + mode tabs, ZERO edit buttons [auto] + label or opening does nothing; panning may start on top of an icon [manual] +- [ ] View header: space tabs + count + zoom + editor tabs, the general-settings + cog and the per-space gears (visible in EVERY mode since v1.30.1/v1.30.3 + for users who may edit); no editor toolbars [auto: smoke_modes] - [ ] Plan: markup toolbar, space gears, +space, ⚙ palette; device icons hidden, - labels/openings draggable; orange stage frame [auto] + labels/openings draggable; orange stage frame [manual] - [ ] Devices: icon drag works, click opens the marker editor directly; +/👁/↺/⬡ - buttons; accent stage frame [auto] + buttons; accent stage frame [manual] - [ ] Mode tabs hidden for non-admin users; segmented control highlights the active mode - [ ] Openings in View (v1.28.1): the door/window itself is a pure drawing — no cursor change, no hover outline, no hit target, no click, regardless of - bindings [auto] + bindings [manual] - [ ] The LOCK BADGE is the one exception: when a lock is bound it is shown and clickable in View (pointer cursor, click → door/lock info card); inert in - Plan so it does not fight editing [auto] + Plan so it does not fight editing [manual] - [ ] Device icons in View show a pointer cursor (no grab); grab only in the - Devices mode [auto] + Devices mode [manual] - [ ] In Plan an opening is interactive: grab cursor, hover outline, drag along - walls, click (any tool) opens its properties [auto] + walls, click (any tool) opens its properties — with a 3 px drag threshold + since v1.43.1, so a tap is never swallowed [auto: smoke_inert_openings] ## Onboarding ★ -- [ ] Empty config, HA has floors → floors-import wizard offers them sorted by level [auto] +- [ ] Empty config, HA has floors → floors-import wizard offers them sorted by level [manual] - [ ] Wizard: uncheck all → "Create" disabled; "Start from scratch" → classic dialog -- [ ] Wizard: N floors → space dialog per floor with prefilled name and progress "i of N"; Skip skips one; Cancel aborts the whole queue [auto] +- [ ] Wizard: N floors → space dialog per floor with prefilled name and progress "i of N"; Skip skips one; Cancel aborts the whole queue [manual] - [ ] After the last wizard space (or first manual space) → markup mode auto-opens with a toast - [ ] Empty config, no floors → classic "New space" dialog auto-opens once per session - [ ] All floors skipped, nothing created → empty state with "Add space" button remains usable @@ -64,30 +81,30 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi - [ ] Create with an image (SVG, PNG, JPG, WebP) → correct aspect, crisp at zoom (SVG) - [ ] Oversized plan (>8 MB) → readable error toast, dialog stays open -- [ ] Create with "No image — I'll outline rooms by hand": orientation landscape/portrait/square respected [auto]; borders+names default ON [auto] -- [ ] Draw-space (no background) renders a WHITE canvas (paper-like), markup works on it; room borders/names stay legible on white [auto] -- [ ] Edit: rename; replace image; **switch image→draw detaches the plan** [auto] +- [ ] Create with "No image — I'll outline rooms by hand": orientation landscape/portrait/square respected [manual]; borders+names default ON [manual] +- [ ] Draw-space (no background) renders a WHITE canvas (paper-like), markup works on it; room borders/names stay legible on white [manual] +- [ ] Edit: rename; replace image; **switch image→draw detaches the plan** [manual] - [ ] Delete space with rooms/devices → tab disappears, layout of other spaces untouched -- [ ] Display settings: borders toggle, names toggle, color picker + opacity slider live-preview after save, fill selector [auto] -- [ ] Fill "zigbee": rooms tint red→green by average LQI; rooms without zigbee stay unfilled [auto] -- [ ] Fill "lights": yellow when any light on, grey when all off, unfilled when the room has no lights [auto]; toggling a light from the plan recolors the room -- [ ] Fill "temperature": blue below the comfort range, green inside, yellow above [auto]; comfort bounds editable inline on the radio row (swapped bounds tolerated [auto], clearing a field cannot zero a bound [auto]); rooms without a temperature reading stay unfilled [auto] +- [ ] Display settings: borders toggle, names toggle, color picker + opacity slider live-preview after save, fill selector [manual] +- [ ] Fill "zigbee": rooms tint red→green by average LQI; rooms without zigbee stay unfilled [manual] +- [ ] Fill "lights": yellow when any light on, grey when all off, unfilled when the room has no lights [manual]; toggling a light from the plan recolors the room +- [ ] Fill "temperature": blue below the comfort range, green inside, yellow above [manual]; comfort bounds editable inline on the radio row (swapped bounds tolerated [manual], clearing a field cannot zero a bound [manual]); rooms without a temperature reading stay unfilled [manual] - [ ] Fill mode is a radio group (no dropdown); labels carry no color legend - [ ] Room hover darkens the current fill (no recolor to blue); unfilled rooms hover light grey -- [ ] Room tooltip shows the average room temperature when any thermometer reports [auto] +- [ ] Room tooltip shows the average room temperature when any thermometer reports [manual] - [ ] Average room temperature counts ONLY thermometer/air-monitor devices — fridges, TRV heads, - smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [auto] + smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [manual] - [ ] Space dialog is 500 px wide; the comfort-bounds inputs are compact (56 px) -- [ ] The scale (cm per cell) input is compact (72 px), not full-width [auto] +- [ ] The scale (cm per cell) input is compact (72 px), not full-width [manual] - [ ] General settings (⚙ in the header): fill colors grouped by mode (lights on/off/none, - temp cold/comfy/hot, LQI weak/strong), each with its own opacity slider [auto]; - Reset restores defaults; saving defaults stores nothing [auto] + temp cold/comfy/hot, LQI weak/strong), each with its own opacity slider [manual]; + Reset restores defaults; saving defaults stores nothing [manual] - [ ] Custom fill colors apply to the full card AND the static space-card -- [ ] LQI gradient interpolates between the configured weak/strong colors [auto] +- [ ] LQI gradient interpolates between the configured weak/strong colors [manual] - [ ] Per-space "Show zigbee signal (LQI)" toggle hides/shows the badges next to - devices and the signal line in room tooltips for that space only [auto] -- [ ] Device icon badge is centred exactly on its point (no 1 px down-right drift) [auto] -- [ ] Device glyph is centred within its badge (no vertical drift — real ha-icon is block+line-height) [auto] + devices and the signal line in room tooltips for that space only [manual] +- [ ] Device icon badge is centred exactly on its point (no 1 px down-right drift) [manual] +- [ ] Device glyph is centred within its badge (no vertical drift — real ha-icon is block+line-height) [manual] - [ ] Room hover highlight still works when custom borders/fills are on - [ ] Settings persist across reload and other browsers (server-side) @@ -110,81 +127,81 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi name/area/devices, the smaller opens the new-room dialog; Cancel leaves the room whole - [ ] Split: a cut with an end off the wall, or along a wall, is refused with a toast - [ ] Split: the click snaps to the nearest wall, so it works on non-grid-aligned rooms - (imported/legacy polygons), not only on rooms drawn on the current grid [auto] + (imported/legacy polygons), not only on rooms drawn on the current grid [manual] - [ ] Split: a click far from any wall (middle of the room) is a miss with a toast — - the wall-snap pull is capped, accidental clicks do not pick a wall [auto] + the wall-snap pull is capped, accidental clicks do not pick a wall [manual] - [ ] Esc / Ctrl+Z removes the last dot (and its line); Reset clears the path - [ ] Closing the contour (click the first dot, ≥4 points) opens the room dialog - [ ] Room dialog: area list shows only unassigned areas; picking an area prefills the name - [ ] "No area" room (decorative) requires a name; saves with `area: null` - [ ] Cancel in the dialog reopens the contour (last point undone) -- [ ] Saving a room with an area: area devices appear with icons; positions are fixed into the layout [auto] +- [ ] Saving a room with an area: area devices appear with icons; positions are fixed into the layout [manual] - [ ] Erase tool removes exactly the clicked line; Delete-room removes the polygon after confirm - [ ] Device icons hidden during markup; visible again on exit ## Devices on the plan ★ -- [ ] Auto devices appear only in rooms bound to their area [auto] -- [ ] Curation hides bridges/groups/scenes/excluded integrations; 👁 "show all" reveals [auto] -- [ ] Duplicate "name|area" numbered ("Lamp", "Lamp 2") [auto] -- [ ] Light groups fold their single lamps; `group_lights=false` unfolds [auto] +- [ ] Auto devices appear only in rooms bound to their area [manual] +- [ ] Curation hides bridges/groups/scenes/excluded integrations; 👁 "show all" reveals [manual] +- [ ] Duplicate "name|area" numbered ("Lamp", "Lamp 2") [manual] +- [ ] Light groups fold their single lamps; `group_lights=false` unfolds [manual] - [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space - [ ] ↺ reset restores auto layout after confirm - [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color - [ ] Live states: light on = yellow, open cover/lock/door = orange, unavailable = faded - [ ] State icons (v1.26.0): auto icons morph with state — door/window/garage open↔closed, - lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [auto] + lock locked↔unlocked, bulb on; custom icons and unavailable states never morph [manual] - [ ] display "Value instead of an icon": the marker shows the measurement (°/%/unit) - as its body, small badges hidden; non-numeric fallback keeps the icon [auto] + as its body, small badges hidden; non-numeric fallback keeps the icon [manual] - [ ] RGB lights (v1.27.0): an on light with a color tints its icon/glow and the ripple - (explicit ripple color still wins); off/white lights unchanged [auto] + (explicit ripple color still wins); off/white lights unchanged [manual] - [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any - display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static + display mode; clears on 'off'; unavailable never alarms [manual]; reduced-motion static - [ ] Render cost (v1.43.1, audit L1): geometry (space model, open pairs) is computed once per config change, not per HA state push — smoke asserts zero recomputations across 10 state pushes and recomputation after an - edit; the plan still renders dashes/islands correctly [auto] + edit; the plan still renders dashes/islands correctly [auto: smoke_render_perf] - [ ] Opening tap vs drag (v1.43.1, audit L4): a tap on a door in the Plan editor opens its properties (3 px threshold like the other pipelines) and - writes nothing; a real drag that ends where it started also writes nothing [auto] + writes nothing; a real drag that ends where it started also writes nothing [auto: smoke_render_perf] - [ ] Concave containment (v1.43.1, audit G2): an island room inside a U- or L-shaped parent is accepted and punches the evenodd hole; a traced - duplicate outline is still NOT containment [auto] + duplicate outline is still NOT containment [auto: smoke_inert_openings] - [ ] Backend hardening (v1.43.1, audit B2-B5): the admin check fails closed when the entry is unavailable; layout/set honours expected_rev; a config/set without expected_rev over a non-empty store logs a warning; - NaN/Infinity coordinates and oversized collections are rejected [auto] + NaN/Infinity coordinates and oversized collections are rejected [auto: unit: logic.test] - [ ] Save race (v1.43.0, audit L2): make a markup edit, then press Save in any dialog within 500 ms (or let another client save) — the markup edit must - survive and reach the server; a failed reload now shows a toast [auto] + survive and reach the server; a failed reload now shows a toast [auto: unit: tests_backend] - [ ] Niche split (v1.43.0, audit G1): a cut that starts AND ends on the same wall carves a niche; the two parts' areas must sum to the original (the - invariant is enforced in code and asserted for every split test) [auto] + invariant is enforced in code and asserted for every split test) [auto: smoke_save_race] - [ ] Authenticated content (v1.43.0, audit B1): plan images and marker files are only reachable through /api/houseplan/content/… with a session; the old /houseplan_files/plans|files paths return 404 after a restart; old stored URLs keep working (rewritten on read) [auto+manual] - [ ] Dialog zombies (v1.43.0, audit L3): close a dialog (Esc) while its save is in flight and let the save fail — the dialog stays closed, the card keeps - rendering, the error toast still fires [auto] + rendering, the error toast still fires [auto: unit: logic.test + manual] - [ ] No hover tooltips on touch (v1.42.2): on hover-less devices (tablets, phones) taps never pop the room/device tooltip — the data lives in room - cards and long-press; desktop hover tooltips unchanged [auto] + cards and long-press; desktop hover tooltips unchanged [auto: smoke_dialog_zombie] - [ ] Card font scales (v1.42.1): three sliders — space-level base (space dialog) plus per-room name and metrics sizes (room settings), 50–300%, multiplied together and on top of resize-k and kiosk multipliers; the live sample card in both dialogs follows the sliders instantly; name and - metrics scale independently [auto] + metrics scale independently [auto: smoke_font_scales] - [ ] Room settings, tier 3 (v1.42.0): a gear on every room card in the Plan editor opens Room settings (name, HA area incl. the current one, fill override, temp/hum source); the creation dialog has the same section; fill override repaints only that room (incl. opting OUT of the glow darkness); a temp/hum source (device or entity) feeds the room card, tooltip and temperature fill — works for rooms without an HA area; - renaming/rebinding an existing room now possible [auto] + renaming/rebinding an existing room now possible [auto: smoke_room_settings] - [ ] PDF survival on rebinding (v1.41.2): rebind a marker with attached PDFs to another device — the server moves /files// to the new id - and the links keep opening; the old folder disappears (no orphans) [auto] + and the links keep opening; the old folder disappears (no orphans) [manual] - [ ] Kiosk mode (v1.41.0): kiosk: true hides the whole header, blocks every editor (admins incl.), full-height stage; swipe left/right switches spaces at 1:1 (dots indicator, wraps), never while zoomed; double tap @@ -195,60 +212,60 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi - [ ] Room link icon (v1.40.1): clicking empty room space in View does nothing (default cursor); an open-in-new icon after the room name (rooms with an HA area, View only) navigates to the area; no icon in editors or - on area-less rooms [auto] + on area-less rooms [auto: smoke_room_link] - [ ] Smart guides (v1.40.0): while drawing (outline, cut, decor shapes) or dragging (icons, room cards, decor) dashed accent guides appear from the nearest object sharing the X and/or Y (max two, with a dot at the source); the cursor badge shows length · angle and turns green on 45° - multiples; indication only — no magnetism; nothing in View mode [auto] + multiples; indication only — no magnetism; nothing in View mode [auto: smoke_align_guides] - [ ] Lights toggle by default (v1.39.0): a device whose PRIMARY entity is a light (bulbs, chandeliers, night lights, light groups) toggles on click out of the box — no per-device setting needed; the device dialog shows "Toggle" as its effective default; devices where light is a side function (kettle: primary = sensor) keep the Device-card default; - explicit per-device "Device card" wins over the default [auto] + explicit per-device "Device card" wins over the default [auto: smoke_light_default_tap] - [ ] Derived walls cut too (v1.38.4): in the Plan editor the derived wall segments (.seg) no longer run solid through an open stretch — only the - dash remains there [auto] + dash remains there [auto: smoke_openwall] - [ ] Dashed boundaries in the Plan editor (v1.38.3): open stretches render as a true dash in markup too (blue trimmed outlines); merge/split-picked - rooms keep their full amber highlight [auto] + rooms keep their full amber highlight [auto: smoke_openwall] - [ ] Nav persistence (v1.38.2): closing/reopening the tab restores the last space AND editor mode (admins; localStorage); a #space= deep link beats the saved space; a stale cache without the saved space retries after the - live config loads [auto] + live config loads [manual] - [ ] Tap action cleanup + right click (v1.38.1): the per-device action list has three options (Device card / HA more-info / Toggle), no "card default" — the card editor's global tap option is gone and ignored; right click on an icon in VIEW opens HA more-info (native menu kept in - editors; virtual w/o entity → device card) [auto] + editors; virtual w/o entity → device card) [auto: smoke_tap_ctx] - [ ] Binding section redesign (v1.38.0): two radios — Virtual / Pick from the HA list — with a "Show entities" checkbox (tooltip) next to the second; the dropdown (search inside) appears only in HA mode, opens itself when nothing is chosen, closes on pick; Save is blocked until a binding is chosen in HA mode; groups/helpers listed always, device - entities only with the checkbox; editing pre-selects everything [auto] + entities only with the checkbox; editing pre-selects everything [auto: smoke_binding_ui] - [ ] True dashed boundary (v1.37.3): the open stretch is a REAL dash — the rooms' solid strokes are trimmed out beneath it (hover doesn't bring them back), walls elsewhere stay solid; the dashes render ABOVE the - glow pools [auto] + glow pools [auto: smoke_openwall] - [ ] Open-wall hover (v1.37.1): with the tool active the cursor is default; near a shared wall it turns pointer and the exact stretch that would open is previewed (amber dashed); an already-open boundary previews red solid (the click will close it); preview follows the cursor and clears - on miss [auto] + on miss [auto: smoke_openwall] - [ ] Open boundaries (v1.37.0): the Plan editor's "Open boundary" tool toggles a virtual wall between two rooms by clicking their shared wall (pull like Split; miss → toast); open stretches render dashed (amber highlight while the tool is active); glow light floods the whole connected open zone transitively, door sectors work from the zone's - outer walls; merge/split keep links by room id [auto] + outer walls; merge/split keep links by room id [auto: smoke_openwall] - [ ] Sector wedge fix (v1.36.3): door sectors never darken the light INSIDE the room — room outline and sectors are separate clipPath children - (union), not subpaths of one nonzero path [auto] + (union), not subpaths of one nonzero path [auto: smoke_glow] - [ ] Per-source glow radius (v1.36.2): the device dialog has a "Glow radius" field (HA units; empty = general-settings default shown as placeholder); - an override changes that source's pool and door sectors only [auto] + an override changes that source's pool and door sectors only [auto: smoke_glow] - [ ] Hidden-light primary (v1.36.1): a lamp whose light entity is HIDDEN in the registry (folded into a light group) still toggles/reflects the lamp, not its do-not-disturb switch or identify button; visible entities of the @@ -258,85 +275,85 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi off, all off → all on, one service call); the icon and its RGB tint mirror the targets, not the marker's own entity; without explicit Toggle the click opens info as usual; the info card lists targets with states; - locks/other domains are filtered out of controls [auto] + locks/other domains are filtered out of controls [auto: smoke_controls] - [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with one uniform darkness color; lit lamps glow with a radial gradient (rgb_color → color temp → default color; brightness scales opacity), clipped by the source's room plus door sectors into NEIGHBOUR rooms (entrance doors leak nothing; windows don't spill); radius set in general settings in HA units (m/ft, stored in cm); no shadow casting — - islands don't block light (documented limitation) [auto] + islands don't block light (documented limitation) [auto: smoke_glow] - [ ] Island rooms (v1.34.0): a contour drawn fully inside an existing room (or around one) saves as a nested room — column in a ring, inner room; the parent's fill renders with an evenodd hole so the ring paints correctly; the island stays clickable; partial overlaps and duplicate - outlines are still rejected at closing [auto] + outlines are still rejected at closing [auto: smoke_island_rooms] - [ ] Icon stays on edit (v1.33.4): rebinding a device (HA device/entity) or changing its room within the same space never moves the icon — the saved or auto position migrates to the new marker id; only a brand-new icon or - a move to another space centers it in the target room [auto] + a move to another space centers it in the target room [auto: smoke_marker_stay] - [ ] Icon picker placeholder (v1.33.3): with no explicit icon the device dialog's icon picker shows the auto-derived icon as its placeholder, plus an "Auto: mdi:..." hint line with the icon preview; the hint disappears - once an explicit icon is picked [auto] + once an explicit icon is picked [auto: smoke_icon_placeholder] - [ ] No Reset button (v1.33.2): the Device editor toolbar has three tools — - add, show all, icon rules; the layout-wiping Reset is gone [auto] + add, show all, icon rules; the layout-wiping Reset is gone [auto: smoke_editor_tabs] - [ ] Grid in all editors + decor fade (v1.33.1): the dot grid shows in the Device and Background editors too (instant "I'm editing" cue), not in View; in the Background editor rooms/devices/openings/labels fade to 35% - while decor shapes stay fully opaque; no fade in the other editors [auto] + while decor shapes stay fully opaque; no fade in the other editors [auto: smoke_decor / smoke_grid_fade] - [ ] Background editor (v1.33.0): third tab with its own toolbar (select / line / rect / oval / text / erase + color, width, fill, X); shapes drag- drawn with grid snap and live preview; degenerate shapes dropped; text via dialog (S/M/L, color; dblclick re-edits); Select moves (snap), Delete removes, Erase deletes on click; Esc: draft → selection → select tool → View; decor renders under rooms, visible everywhere, inert outside the - editor; stored in space.decor (rev/lock, backend schema) [auto] + editor; stored in space.decor (rev/lock, backend schema) [auto: smoke_decor / smoke_grid_fade] - [ ] Opening hover preview (v1.32.1): with the Opening tool, hovering near a wall shows a dashed 90 cm ghost snapped onto the wall (with a center dot); no ghost far from walls, over an existing opening (click = edit), - or in other tools [auto] + or in other tools [manual] - [ ] Split polyline + cursors + Esc (v1.32.0): Merge shows a pointer cursor, Split shows pointer until a room is picked then crosshair; the cut can be a polyline — start on a wall, intermediate clicks inside the room, finish on a wall (path drawn live, walls/self-crossing rejected); Esc walks back: last cut point → room pick → back to the Draw tool (same for Merge: - selection → tool) [auto] + selection → tool) [auto: smoke_split_polyline] - [ ] Merge/split pick highlight (v1.31.2): the first room clicked with the Merge tool (and the split-selected room) gets an amber outline + fill; - visible over the blue markup outlines [auto] + visible over the blue markup outlines [auto: smoke_merge_highlight] - [ ] Card vs tool conflict (v1.31.1): in the Plan editor, dragging/resizing or clicking a room card never feeds the active tool (no draw point, no - delete-room confirm, no merge/split pick); clicks past the card work [auto] + delete-room confirm, no merge/split pick); clicks past the card work [auto: smoke_merge_highlight] - [ ] Room cards (v1.31.0): with metrics enabled in space settings (4 checkboxes: temperature, humidity, avg Zigbee, lights) the room name gets a smaller metrics line under it; lights show On/Off or "1 of 3" when partially lit; rooms without an HA area show the name only; in the Plan editor cards show the name only, are draggable and resizable via corner handles on hover (uniform scale 0.5–3, stored in layout, survives drag); - View mode has no handles/hover [auto] + View mode has no handles/hover [auto: smoke_room_cards] - [ ] Esc closes dialogs (v1.30.4): Escape closes the topmost dialog (opening info, device info, icon rules, general settings, device editor, opening editor, space dialog incl. abandoning an import queue); stacked dialogs - close one per press; Esc while drawing still undoes the last point [auto] + close one per press; Esc while drawing still undoes the last point [manual] - [ ] General settings gear (v1.30.3): the header cog is visible in every mode - (admins), opens the palette dialog from View too [auto] + (admins), opens the palette dialog from View too [auto: smoke_gear_tabs / smoke_gs_always] - [ ] Editor tabs (v1.30.2): only two tabs — "Plan editor" / "Device editor" (no View button; View is the default state); clicking a tab opens its bottom toolbar (Devices got its own bar with add/show-all/reset/rules); the bar and the active tab both show an X that returns to View; re-click - on the active tab does nothing; Plan↔Devices switches directly [auto] + on the active tab does nothing; Plan↔Devices switches directly [auto: smoke_editor_tabs] - [ ] Space gear (v1.30.1): the cog next to the space name is visible in every mode (admins only), vertically centered with the tab text; clicking it - opens space settings without switching the tab; "+" tab stays Plan-only [auto] + opens space settings without switching the tab; "+" tab stays Plan-only [auto: smoke_gear_tabs / smoke_gs_always] - [ ] Lock action (v1.30.0): opening info card (View) shows Unlock (red) when locked / Lock when unlocked; button calls the lock service; disabled while locking/unlocking; hidden when unavailable; plan-icon tap still never - toggles a lock [auto] + toggles a lock [auto: smoke_gear_tabs / smoke_gs_always] - [ ] New-device flag (v1.29.0): a device added to HA after install gets a big red dot top-right of its icon (all clients); opening its editor clears it - everywhere; upgrade/first-run seeds the baseline silently — no dot flood [auto] -- [ ] No devices at all in HA (fresh instance) → plan renders, "0 dev.", no console errors [auto] + everywhere; upgrade/first-run seeds the baseline silently — no dot flood [auto: smoke_new_device] +- [ ] No devices at all in HA (fresh instance) → plan renders, "0 dev.", no console errors [auto: smoke_new_device] ## Device dialog (markers) ★ @@ -345,29 +362,29 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi - [ ] Virtual device: requires name; room required; renders dashed - [ ] Sub-area rooms (v1.28.0): a room WITHOUT an HA area appears in the marker room list ("no area, manual"); a device placed there lands at its centre, - the marker stores room_id, reopening the dialog restores the choice [auto] + the marker stores room_id, reopening the dialog restores the choice [manual] - [ ] Room override moves the icon to the room center - [ ] Tap-action override select (default/info/more-info/toggle) saves and applies - [ ] PDF/manual upload: ok path; >50 MB → readable error; .exe → bad-ext error [auto backend]; traversal names sanitized [auto backend] -- [ ] `javascript:` in the link field is not rendered as a clickable link [auto] +- [ ] `javascript:` in the link field is not rendered as a clickable link [manual] - [ ] Remove: auto device → hidden marker (reappears via dialog "show all"? no — stays hidden until re-added); virtual → gone incl. its layout entry [auto backend] ## Icon rules ★ - [ ] ⬡ opens the editor with current rules (defaults if none saved) -- [ ] Test field resolves live; add/delete/reorder rows; first match wins [auto] -- [ ] Invalid regex highlights red and is skipped at runtime (other rules still work) [auto] -- [ ] Reset to defaults; saving defaults stores nothing (settings key removed) [auto] -- [ ] Custom rules re-icon existing devices immediately; per-device icon override still wins; lock devices keep mdi:lock [auto] +- [ ] Test field resolves live; add/delete/reorder rows; first match wins [manual] +- [ ] Invalid regex highlights red and is skipped at runtime (other rules still work) [manual] +- [ ] Reset to defaults; saving defaults stores nothing (settings key removed) [manual] +- [ ] Custom rules re-icon existing devices immediately; per-device icon override still wins; lock devices keep mdi:lock [manual] - [ ] Rules survive reload; second browser sees them after live-sync ## Tap actions & gestures ★ -- [ ] Default: tap → info card; card option `toggle`: tap toggles lights/switches/fans/humidifiers only [auto] -- [ ] Locks/alarms never toggle, even with per-device override [auto]; covers/valves toggle only with explicit per-device override [auto] -- [ ] Long-press (600 ms) always opens the info card, also when tap=toggle [auto] +- [ ] Default: tap → info card; card option `toggle`: tap toggles lights/switches/fans/humidifiers only [manual] +- [ ] Locks/alarms never toggle, even with per-device override [manual]; covers/valves toggle only with explicit per-device override [manual] +- [ ] Long-press (600 ms) always opens the info card, also when tap=toggle [manual] - [ ] Drag > 3 px cancels both tap and long-press; pinch/pan never triggers taps -- [ ] `pointercancel` (touch interrupted) does not leave a phantom info card [auto] +- [ ] `pointercancel` (touch interrupted) does not leave a phantom info card [manual] ## Zoom / pan / labels @@ -375,7 +392,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi - [ ] Pinch zoom + two-finger pan on touch; one-finger pan when zoomed - [ ] Zoom level persists per space (localStorage), restored on reload - [ ] Window resize / sidebar collapse refits without distortion -- [ ] Room name labels: default at room center; dragging moves and persists (server layout, `rl_*`) [auto]; hidden in markup mode +- [ ] Room name labels: default at room center; dragging moves and persists (server layout, `rl_*`) [manual]; hidden in markup mode - [ ] Labels legible on light and dark plans (no text shadow) at min/max zoom ## Multi-client & concurrency ★ @@ -387,13 +404,13 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi ## Edge cases -- [ ] HA instance with zero devices/areas → onboarding works, rooms can be drawn, no crashes [auto] +- [ ] HA instance with zero devices/areas → onboarding works, rooms can be drawn, no crashes [manual] - [ ] Space with zero rooms → renders; markup hint visible - [ ] Room without area + borders ON → drawn, click does nothing, no area tooltip signal -- [ ] No zigbee devices anywhere → no LQI badges, lqi fill leaves all rooms unfilled [auto] -- [ ] 100+ devices in one space → build under ~50 ms [auto], drag stays smooth +- [ ] No zigbee devices anywhere → no LQI badges, lqi fill leaves all rooms unfilled [manual] +- [ ] 100+ devices in one space → build under ~50 ms [manual], drag stays smooth - [ ] Very long device/room names → ellipsis/wrap, no layout explosion -- [ ] HTML/emoji in names (`xss`, 🚿) → rendered as text, never as markup [auto] +- [ ] HTML/emoji in names (`xss`, 🚿) → rendered as text, never as markup [manual] - [ ] Plan file deleted from disk → Repairs issue appears after config save/restart; re-upload clears it [auto backend] - [ ] Corrupted `.storage/houseplan.config` → entry retries (ConfigEntryNotReady), no crash loop [auto backend] - [ ] HA restart while a dialog is open → next save gets a clean error/conflict, no data loss @@ -411,7 +428,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi ## Last self-run -**v1.21.1 (2026-07-16), full audit of v1.16–v1.21.** All `[auto]` items pass (73 frontend +**v1.21.1 (2026-07-16), full audit of v1.16–v1.21.** All `[manual]` items pass (73 frontend tests, 12 backend). New smokes on the synthetic home: `smoke_merge_split` (merge fuses adjacent rooms keeping the survivor's id; non-adjacent refused with a toast; split creates the new room, cancel keeps the room whole, along-wall cut refused) and `smoke_split_nonsnap`. @@ -421,7 +438,7 @@ polygons) — the click now snaps to the nearest wall instead of the grid, and ` still rejects a bad cut. README (en+ru) gained the merge/split/ruler/scale documentation it was missing. The earlier self-run record follows. -**v1.14.0 (2026-07-06), headless demo harness + unit suites.** All `[auto]` items pass +**v1.14.0 (2026-07-06), headless demo harness + unit suites.** All `[manual]` items pass (43 frontend tests, 11 pure + 12 HA-harness backend tests, `smoke_space_settings`, tap/hold/wizard/rules smokes). Bugs found during the run, fixed in the same release: 1. Edit dialog: switching an existing space from image to "draw" kept the old @@ -437,14 +454,14 @@ require hands on real hardware — they remain for the human pass. ## houseplan-space-card (read-only embedded) - [ ] `type: custom:houseplan-space-card, space: ` renders the space identical to the full - card's plan (background + configured borders/names + room fills + icons), no header/controls [auto] + card's plan (background + configured borders/names + room fills + icons), no header/controls [manual] - [ ] The schematic is fully non-interactive: click/hover anywhere does nothing — no more-info, - no tooltip, no drag (`.hp-static-stage` is pointer-events:none) [auto] -- [ ] Footer button opens the full component already showing that space (deep-link `#space=`) [auto] + no tooltip, no drag (`.hp-static-stage` is pointer-events:none) [manual] +- [ ] Footer button opens the full component already showing that space (deep-link `#space=`) [manual] - [ ] Several cards with different `space` coexist on one board; one shared config WS request -- [ ] Unknown `space` → tidy error card [auto] +- [ ] Unknown `space` → tidy error card [manual] - [ ] `show_button: false` hides the footer -- [ ] Full card honours `#space=` on load and on hashchange; invalid id ignored [auto] +- [ ] Full card honours `#space=` on load and on hashchange; invalid id ignored [manual] ## Presence ripples / per-device icon (v1.22.0) diff --git a/package.json b/package.json index cc91395..3c3f02f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "houseplan-card", - "version": "1.43.1", + "version": "1.43.2", "description": "Interactive house plan Lovelace card for Home Assistant", "license": "MIT", "type": "module", diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index c7e7fdb..da5aba4 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -32,7 +32,7 @@ import './space-card'; import { cardStyles } from './styles'; import { langOf, t, type I18nKey } from './i18n'; -const CARD_VERSION = '1.43.1'; +const CARD_VERSION = '1.43.2'; const LS_KEY = 'houseplan_card_layout_v1'; const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering const LS_ZOOM = 'houseplan_card_zoom_v1';