mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
test v1.43.2: smokes that can fail, in CI, and an honest TESTING.md
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.
This commit is contained in:
@@ -34,6 +34,44 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Card bundle in sync with integration
|
- name: Card bundle in sync with integration
|
||||||
run: cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js
|
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:
|
backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files"
|
|||||||
CONTENT_URL = "/api/houseplan/content"
|
CONTENT_URL = "/api/houseplan/content"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.43.1"
|
VERSION = "1.43.2"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
@@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>`}
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}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","");
|
</div>`}}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","");
|
||||||
|
|||||||
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.43.1"
|
"version": "1.43.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -6,10 +6,45 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
const ROOT = dirname(fileURLToPath(import.meta.url)) + '/srv';
|
const ROOT = dirname(fileURLToPath(import.meta.url)) + '/srv';
|
||||||
const CT = { '.html': 'text/html', '.js': 'text/javascript', '.svg': 'image/svg+xml' };
|
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) {
|
export async function launch(viewport = { width: 820, height: 760 }, scale = 1) {
|
||||||
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
||||||
const page = await (await browser.newContext({ viewport, deviceScaleFactor: scale })).newPage();
|
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) => {
|
await page.route('**/*', (r) => {
|
||||||
const u = new URL(r.request().url());
|
const u = new URL(r.request().url());
|
||||||
let p = decodeURIComponent(u.pathname);
|
let p = decodeURIComponent(u.pathname);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -52,5 +52,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.noneInView = guides() === 0;
|
out.noneInView = guides() === 0;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -46,5 +46,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._markerDialog = null;
|
c._markerDialog = null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -47,5 +47,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.delroomIgnored = !confirmCalled;
|
out.delroomIgnored = !confirmCalled;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -57,5 +57,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.lockFiltered = JSON.stringify(last) === JSON.stringify(['homeassistant', 'turn_on', [lights[0]]]);
|
out.lockFiltered = JSON.stringify(last) === JSON.stringify(['homeassistant', 'turn_on', [lights[0]]]);
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -70,5 +70,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.deleteKey = c._decorList.length === n1 - 1;
|
out.deleteKey = c._decorList.length === n1 - 1;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -51,5 +51,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._settingsDialog = null;
|
c._settingsDialog = null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, check, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -70,5 +70,18 @@ const res = await page.evaluate(async () => {
|
|||||||
out.bigRenderMs = Math.round(performance.now() - t1);
|
out.bigRenderMs = Math.round(performance.now() - t1);
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
// сборка 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);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -35,5 +35,8 @@ const res = await page.evaluate(async () => {
|
|||||||
out.tabCrossWorks = c._mode === 'view';
|
out.tabCrossWorks = c._mode === 'view';
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"labels": ["Plan editor", "Device editor", "Background editor"],
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -31,5 +31,5 @@ const res = await page.evaluate(async () => {
|
|||||||
await esc(); out.undoPointStillWorks = c._path.length === 1;
|
await esc(); out.undoPointStillWorks = c._path.length === 1;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -54,5 +54,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._spaceDialog = null; await c.updateComplete;
|
c._spaceDialog = null; await c.updateComplete;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -31,5 +31,8 @@ const res = await page.evaluate(async () => {
|
|||||||
out.addInPlan = !!sr().querySelector('.tab.tabadd');
|
out.addInPlan = !!sr().querySelector('.tab.tabadd');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"alignDelta": 0,
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -26,5 +26,12 @@ const res = await page.evaluate(async () => {
|
|||||||
out.lqiAfter = sr().querySelectorAll('.dev .lqi').length;
|
out.lqiAfter = sr().querySelectorAll('.dev .lqi').length;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
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);
|
||||||
|
|||||||
+6
-3
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -101,5 +101,8 @@ const res = await page.evaluate(async () => {
|
|||||||
out.radiusReacts = Math.abs(r600 / r300 - 2) < 0.01;
|
out.radiusReacts = Math.abs(r600 / r300 - 2) < 0.01;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"spots": 1,
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -33,5 +33,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.notFadedInView = room3 ? Number(getComputedStyle(room3).opacity) > 0.9 : null;
|
out.notFadedInView = room3 ? Number(getComputedStyle(room3).opacity) > 0.9 : null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -14,5 +14,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.opensInView = !!c._settingsDialog;
|
out.opensInView = !!c._settingsDialog;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -27,5 +27,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._markerDialog = null; await c.updateComplete;
|
c._markerDialog = null; await c.updateComplete;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -43,5 +43,10 @@ const res = await page.evaluate(async () => {
|
|||||||
c._openingDialog = null; c._setMode('view');
|
c._openingDialog = null; c._setMode('view');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"lockBadgeInert": "no-badge",
|
||||||
|
"viewDevCursor": "pointer",
|
||||||
|
"devModeCursor": "grab",
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -55,5 +55,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.islandRendered = !!islandEl;
|
out.islandRendered = !!islandEl;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -69,5 +69,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c.remove();
|
c.remove();
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -30,5 +30,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.explicitInfoWins = calls.length === n2 && !!c._infoCard;
|
out.explicitInfoWins = calls.length === n2 && !!c._infoCard;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -47,5 +47,5 @@ const res = await page.evaluate(async () => {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
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;
|
out.newCentered = vpos && Math.abs(vpos.x * 1000 - center[0]) < 1 && Math.abs(vpos.y * (1000 / aspect) - center[1]) < 1;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -20,5 +20,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.splitPicked = !!el2 && getComputedStyle(el2).stroke.includes('255, 193, 77');
|
out.splitPicked = !!el2 && getComputedStyle(el2).stroke.includes('255, 193, 77');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Merge & split room ops (v1.21.0) via the card's markup handlers (norm coords).
|
// 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 { page, browser } = await launch();
|
||||||
const snap = await page.evaluate(() => JSON.stringify(window.__card._serverCfg));
|
const snap = await page.evaluate(() => JSON.stringify(window.__card._serverCfg));
|
||||||
const restore = () => page.evaluate((s) => {
|
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();
|
s = await S();
|
||||||
out.alongWallRefused = !s.roomDlg && !s.pendingSplit;
|
out.alongWallRefused = !s.roomDlg && !s.pendingSplit;
|
||||||
|
|
||||||
console.log(JSON.stringify(out,null,1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(out, {
|
||||||
|
"mergeRooms": 3,
|
||||||
|
"newRoom": "Cabinet",
|
||||||
|
});
|
||||||
|
await finish(browser, out);
|
||||||
|
|||||||
+10
-3
@@ -1,5 +1,5 @@
|
|||||||
// UX modes shell (v1.25.0): view is display-only; plan/devices gate the tools.
|
// 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 { page, browser } = await launch();
|
||||||
const out = {};
|
const out = {};
|
||||||
const q = (sel) => page.evaluate((s) => (window.__card.shadowRoot || window.__card.renderRoot).querySelectorAll(s).length, sel);
|
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
|
// 5) назад в view
|
||||||
await page.evaluate(() => window.__card._setMode('view'));
|
await page.evaluate(() => window.__card._setMode('view'));
|
||||||
out.backToView = (await st()).mode;
|
out.backToView = (await st()).mode;
|
||||||
console.log(JSON.stringify(out, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
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);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -31,5 +31,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._setMode('view'); c._space = 'f1'; c._saveNav(); await c.updateComplete;
|
c._setMode('view'); c._space = 'f1'; c._saveNav(); await c.updateComplete;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -28,5 +28,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.stillKnown = c._serverCfg.settings.known_devices.includes('d_new');
|
out.stillKnown = c._serverCfg.settings.known_devices.includes('d_new');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -39,5 +39,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.noGhostOverExisting = !sr().querySelector('.opghost');
|
out.noGhostOverExisting = !sr().querySelector('.opghost');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -70,5 +70,5 @@ const res = await page.evaluate(async () => {
|
|||||||
} else out.transitive = 'no r3';
|
} else out.transitive = 'no r3';
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -32,5 +32,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._openWallClick([550, 0.25 * H]); await c.updateComplete;
|
c._openWallClick([550, 0.25 * H]); await c.updateComplete;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -32,5 +32,9 @@ const res = await page.evaluate(async () => {
|
|||||||
out.dashesStillRendered = (c.shadowRoot || c.renderRoot).querySelectorAll('.openwall').length > 0;
|
out.dashesStillRendered = (c.shadowRoot || c.renderRoot).querySelectorAll('.openwall').length > 0;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"pairsPerRender": 0,
|
||||||
|
"modelBuildsPer10Renders": 0,
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -28,5 +28,8 @@ const res = await page.evaluate(async () => {
|
|||||||
out.outageSafe = sr().querySelectorAll('.dev.alarm').length === 0;
|
out.outageSafe = sr().querySelectorAll('.dev.alarm').length === 0;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"alarmCount": 1,
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
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;
|
out.dragKeepsScale = c._layout['rl_' + room.id].k <= 3 && c._layout['rl_' + room.id].k >= 2.9;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"labels": 4,
|
||||||
|
"cardsWithMetrics": 4,
|
||||||
|
"sampleMetrics": ["22.4°", "175", "1 of 2"],
|
||||||
|
"partialText": "1 of 2",
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -34,5 +34,5 @@ const res = await page.evaluate(async () => {
|
|||||||
c._setMode('view'); await c.updateComplete;
|
c._setMode('view'); await c.updateComplete;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -58,5 +58,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.glowOptOut = darkCount === areaRooms - 1 && darkCount === styledRooms.length;
|
out.glowOptOut = darkCount === areaRooms - 1 && darkCount === styledRooms.length;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
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'));
|
out.serverHasIt = server.cfg.spaces.some((s) => s.rooms?.some((r) => r.id === 'race_room'));
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -27,7 +27,8 @@ const res = await page.evaluate(async () => {
|
|||||||
})};
|
})};
|
||||||
c.requestUpdate(); await c.updateComplete;
|
c.requestUpdate(); await c.updateComplete;
|
||||||
out.lqiFills = [...sr().querySelectorAll('.room.styled')].filter((r) => (r.getAttribute('style') || '').includes('hsl(')).length;
|
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');
|
const lbl = sr().querySelector('.roomlabel');
|
||||||
c._labelDown({ preventDefault(){}, stopPropagation(){}, clientX: 100, clientY: 100, target: { setPointerCapture(){} }, pointerId: 5 },
|
c._labelDown({ preventDefault(){}, stopPropagation(){}, clientX: 100, clientY: 100, target: { setPointerCapture(){} }, pointerId: 5 },
|
||||||
c._spaceModel().rooms[0], 'f1');
|
c._spaceModel().rooms[0], 'f1');
|
||||||
@@ -46,5 +47,15 @@ const res = await page.evaluate(async () => {
|
|||||||
out.atticNoPlan = attic ? attic.plan_url === null : null;
|
out.atticNoPlan = attic ? attic.plan_url === null : null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
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);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Split now works on a non-grid-aligned room (imported/legacy polygons).
|
// 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 { page, browser } = await launch();
|
||||||
const R = (nx, ny) => page.evaluate(([nx, ny]) => {
|
const R = (nx, ny) => page.evaluate(([nx, ny]) => {
|
||||||
const c = window.__card; const H = 1000 / c._curSpaceCfg.aspect; return [nx * 1000, ny * H];
|
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)); // pick again
|
||||||
await page.evaluate((p)=>window.__card._splitClick(p), await R(0.3,0.3)); // centre click = miss
|
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);
|
out.centreRefused = await page.evaluate(()=>window.__card._splitSel?.a == null);
|
||||||
console.log(JSON.stringify(out));
|
checkAll(out);
|
||||||
await browser.close();
|
await finish(browser, out);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -51,5 +51,8 @@ const res = await page.evaluate(async () => {
|
|||||||
await esc(); out.escExitsMerge = c._tool === 'draw';
|
await esc(); out.escExitsMerge = c._tool === 'draw';
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"partsPolys": [6, 4],
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -30,5 +30,14 @@ const res = await page.evaluate(async () => {
|
|||||||
out.noSmallBadge = !vd[0]?.querySelector('.tval');
|
out.noSmallBadge = !vd[0]?.querySelector('.tval');
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
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);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -31,5 +31,9 @@ const res = await page.evaluate(async () => {
|
|||||||
c._markerDialog = null;
|
c._markerDialog = null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"markerRoomId": "rc",
|
||||||
|
"reopenRoom": "f1#@rc",
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
+13
-6
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -6,7 +6,9 @@ const res = await page.evaluate(async () => {
|
|||||||
const sr = () => c.shadowRoot || c.renderRoot;
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
// 1) в селекте действий 3 опции, дефолт «Карточка устройства»
|
// 1) в селекте действий 3 опции, дефолт «Карточка устройства»
|
||||||
c._setMode('devices'); await c.updateComplete;
|
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;
|
c._openMarkerDialog(dev); await c.updateComplete;
|
||||||
const sel = [...sr().querySelectorAll('.dialog select')].find((s) =>
|
const sel = [...sr().querySelectorAll('.dialog select')].find((s) =>
|
||||||
[...s.options].some((o) => o.textContent === c._t('tap.toggle')));
|
[...s.options].some((o) => o.textContent === c._t('tap.toggle')));
|
||||||
@@ -41,14 +43,19 @@ const res = await page.evaluate(async () => {
|
|||||||
const calls = [];
|
const calls = [];
|
||||||
c.hass = { ...c.hass, callService: (d2, s2, data) => { calls.push([d2, s2, data]); return Promise.resolve(); } };
|
c.hass = { ...c.hass, callService: (d2, s2, data) => { calls.push([d2, s2, data]); return Promise.resolve(); } };
|
||||||
await c.updateComplete;
|
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) {
|
if (plain) {
|
||||||
c._infoCard = null;
|
c._infoCard = null;
|
||||||
c._clickDevice(new MouseEvent('click'), plain);
|
c._clickDevice(new MouseEvent('click'), plain);
|
||||||
out.cardTapIgnored = calls.length === 0 && !!c._infoCard;
|
out.cardTapIgnored = calls.length === 0 && !!c._infoCard;
|
||||||
c._infoCard = null;
|
c._infoCard = null;
|
||||||
} else out.cardTapIgnored = 'no-plain-light';
|
} else out.cardTapIgnored = 'no-plain-device';
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"virtInfo": "no-virt",
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -26,5 +26,13 @@ const res = await page.evaluate(async () => {
|
|||||||
c._spaceDialog = null;
|
c._spaceDialog = null;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
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);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { launch } from './serve.mjs';
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
const { page, browser } = await launch();
|
const { page, browser } = await launch();
|
||||||
// эмуляция тач-устройства: переопределяем matchMedia ДО загрузки бандла
|
// эмуляция тач-устройства: переопределяем matchMedia ДО загрузки бандла
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript(() => {
|
||||||
@@ -19,5 +19,5 @@ const res = await page.evaluate(async () => {
|
|||||||
out.noTipOnTouch = c._tip === null || c._tip === undefined || !c._tip;
|
out.noTipOnTouch = c._tip === null || c._tip === undefined || !c._tip;
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(res));
|
checkAll(res);
|
||||||
await browser.close();
|
await finish(browser, res);
|
||||||
|
|||||||
+11
-3
@@ -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 { page, browser } = await launch({ width: 640, height: 980 }, 2);
|
||||||
const res = await page.evaluate(async () => {
|
const res = await page.evaluate(async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -34,5 +34,13 @@ const res = await page.evaluate(async () => {
|
|||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
await page.screenshot({ path: '/tmp/ux_dialog.png' });
|
await page.screenshot({ path: '/tmp/ux_dialog.png' });
|
||||||
console.log(JSON.stringify(res, null, 1));
|
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||||
await browser.close();
|
checkAll(res, {
|
||||||
|
"filledClass": 1,
|
||||||
|
"unfilled": 3,
|
||||||
|
"tipTemp": 22.4,
|
||||||
|
"fillRadios": 5,
|
||||||
|
"tempInputs": 2,
|
||||||
|
"dialogWidth": 502,
|
||||||
|
});
|
||||||
|
await finish(browser, res);
|
||||||
|
|||||||
@@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>`}
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}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","");
|
</div>`}}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","");
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2488,4 +2488,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
|
|||||||
</button>`}
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`}}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","");
|
</div>`}}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","");
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# Changelog
|
# 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)
|
## v1.43.1 — 2026-07-27 (external audit: P1 fixes)
|
||||||
|
|
||||||
- **Render cost (L1).** Home Assistant replaces `hass` on every state change in
|
- **Render cost (L1).** Home Assistant replaces `hass` on every state change in
|
||||||
|
|||||||
@@ -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 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.
|
- 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).
|
- 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.
|
||||||
|
|||||||
+124
-107
@@ -3,9 +3,23 @@
|
|||||||
> **Policy:** this checklist is updated **in the same commit** as any functional
|
> **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
|
> 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
|
> 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.
|
> 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
|
## Environments matrix
|
||||||
|
|
||||||
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
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
|
## 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
|
- [ ] 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
|
- [ ] 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)
|
- [ ] 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
|
- [ ] 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) ★
|
## 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,
|
- [ ] 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]
|
label or opening does nothing; panning may start on top of an icon [manual]
|
||||||
- [ ] View header: space tabs + count + zoom + mode tabs, ZERO edit buttons [auto]
|
- [ ] 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,
|
- [ ] 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; +/👁/↺/⬡
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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
|
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
|
- [ ] 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
|
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
|
- [ ] 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
|
- [ ] 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 ★
|
## 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: 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
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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)
|
- [ ] Create with an image (SVG, PNG, JPG, WebP) → correct aspect, crisp at zoom (SVG)
|
||||||
- [ ] Oversized plan (>8 MB) → readable error toast, dialog stays open
|
- [ ] 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]
|
- [ ] 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 [auto]
|
- [ ] 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** [auto]
|
- [ ] Edit: rename; replace image; **switch image→draw detaches the plan** [manual]
|
||||||
- [ ] Delete space with rooms/devices → tab disappears, layout of other spaces untouched
|
- [ ] 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]
|
- [ ] 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 [auto]
|
- [ ] 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 [auto]; toggling a light from the plan recolors the room
|
- [ ] 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 [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]
|
- [ ] 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
|
- [ ] 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 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,
|
- [ ] 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)
|
- [ ] 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,
|
- [ ] 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];
|
temp cold/comfy/hot, LQI weak/strong), each with its own opacity slider [manual];
|
||||||
Reset restores defaults; saving defaults stores nothing [auto]
|
Reset restores defaults; saving defaults stores nothing [manual]
|
||||||
- [ ] Custom fill colors apply to the full card AND the static space-card
|
- [ ] 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
|
- [ ] 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]
|
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) [auto]
|
- [ ] 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) [auto]
|
- [ ] 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
|
- [ ] Room hover highlight still works when custom borders/fills are on
|
||||||
- [ ] Settings persist across reload and other browsers (server-side)
|
- [ ] 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
|
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: 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
|
- [ ] 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 —
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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`
|
- [ ] "No area" room (decorative) requires a name; saves with `area: null`
|
||||||
- [ ] Cancel in the dialog reopens the contour (last point undone)
|
- [ ] 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
|
- [ ] Erase tool removes exactly the clicked line; Delete-room removes the polygon after confirm
|
||||||
- [ ] Device icons hidden during markup; visible again on exit
|
- [ ] Device icons hidden during markup; visible again on exit
|
||||||
|
|
||||||
## Devices on the plan ★
|
## Devices on the plan ★
|
||||||
|
|
||||||
- [ ] Auto devices appear only in rooms bound to their area [auto]
|
- [ ] Auto devices appear only in rooms bound to their area [manual]
|
||||||
- [ ] Curation hides bridges/groups/scenes/excluded integrations; 👁 "show all" reveals [auto]
|
- [ ] Curation hides bridges/groups/scenes/excluded integrations; 👁 "show all" reveals [manual]
|
||||||
- [ ] Duplicate "name|area" numbered ("Lamp", "Lamp 2") [auto]
|
- [ ] Duplicate "name|area" numbered ("Lamp", "Lamp 2") [manual]
|
||||||
- [ ] Light groups fold their single lamps; `group_lights=false` unfolds [auto]
|
- [ ] Light groups fold their single lamps; `group_lights=false` unfolds [manual]
|
||||||
- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space
|
- [ ] Drag anywhere (no edit mode), snaps to grid, persists after reload, per space
|
||||||
- [ ] ↺ reset restores auto layout after confirm
|
- [ ] ↺ reset restores auto layout after confirm
|
||||||
- [ ] Temperature badge on thermometers; LQI value under zigbee icons with red→green color
|
- [ ] 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
|
- [ ] 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,
|
- [ ] 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)
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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
|
computed once per config change, not per HA state push — smoke asserts
|
||||||
zero recomputations across 10 state pushes and recomputation after an
|
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
|
- [ ] 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
|
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
|
- [ ] 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
|
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
|
- [ ] Backend hardening (v1.43.1, audit B2-B5): the admin check fails closed
|
||||||
when the entry is unavailable; layout/set honours expected_rev; a
|
when the entry is unavailable; layout/set honours expected_rev; a
|
||||||
config/set without expected_rev over a non-empty store logs a warning;
|
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
|
- [ ] 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
|
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
|
- [ ] 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
|
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
|
- [ ] Authenticated content (v1.43.0, audit B1): plan images and marker files
|
||||||
are only reachable through /api/houseplan/content/… with a session; the
|
are only reachable through /api/houseplan/content/… with a session; the
|
||||||
old /houseplan_files/plans|files paths return 404 after a restart; old
|
old /houseplan_files/plans|files paths return 404 after a restart; old
|
||||||
stored URLs keep working (rewritten on read) [auto+manual]
|
stored URLs keep working (rewritten on read) [auto+manual]
|
||||||
- [ ] Dialog zombies (v1.43.0, audit L3): close a dialog (Esc) while its save is
|
- [ ] 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
|
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,
|
- [ ] 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
|
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
|
- [ ] Card font scales (v1.42.1): three sliders — space-level base (space
|
||||||
dialog) plus per-room name and metrics sizes (room settings), 50–300%,
|
dialog) plus per-room name and metrics sizes (room settings), 50–300%,
|
||||||
multiplied together and on top of resize-k and kiosk multipliers; the
|
multiplied together and on top of resize-k and kiosk multipliers; the
|
||||||
live sample card in both dialogs follows the sliders instantly; name and
|
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
|
- [ ] 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
|
editor opens Room settings (name, HA area incl. the current one, fill
|
||||||
override, temp/hum source); the creation dialog has the same section;
|
override, temp/hum source); the creation dialog has the same section;
|
||||||
fill override repaints only that room (incl. opting OUT of the glow
|
fill override repaints only that room (incl. opting OUT of the glow
|
||||||
darkness); a temp/hum source (device or entity) feeds the room card,
|
darkness); a temp/hum source (device or entity) feeds the room card,
|
||||||
tooltip and temperature fill — works for rooms without an HA area;
|
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
|
- [ ] PDF survival on rebinding (v1.41.2): rebind a marker with attached
|
||||||
PDFs to another device — the server moves /files/<oldId>/ to the new id
|
PDFs to another device — the server moves /files/<oldId>/ 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
|
- [ ] Kiosk mode (v1.41.0): kiosk: true hides the whole header, blocks every
|
||||||
editor (admins incl.), full-height stage; swipe left/right switches
|
editor (admins incl.), full-height stage; swipe left/right switches
|
||||||
spaces at 1:1 (dots indicator, wraps), never while zoomed; double tap
|
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
|
- [ ] 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
|
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
|
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
|
- [ ] Smart guides (v1.40.0): while drawing (outline, cut, decor shapes) or
|
||||||
dragging (icons, room cards, decor) dashed accent guides appear from the
|
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
|
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°
|
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
|
- [ ] Lights toggle by default (v1.39.0): a device whose PRIMARY entity is a
|
||||||
light (bulbs, chandeliers, night lights, light groups) toggles on click
|
light (bulbs, chandeliers, night lights, light groups) toggles on click
|
||||||
out of the box — no per-device setting needed; the device dialog shows
|
out of the box — no per-device setting needed; the device dialog shows
|
||||||
"Toggle" as its effective default; devices where light is a side
|
"Toggle" as its effective default; devices where light is a side
|
||||||
function (kettle: primary = sensor) keep the Device-card default;
|
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
|
- [ ] 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
|
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
|
- [ ] 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
|
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
|
- [ ] Nav persistence (v1.38.2): closing/reopening the tab restores the last
|
||||||
space AND editor mode (admins; localStorage); a #space= deep link beats
|
space AND editor mode (admins; localStorage); a #space= deep link beats
|
||||||
the saved space; a stale cache without the saved space retries after the
|
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
|
- [ ] Tap action cleanup + right click (v1.38.1): the per-device action list
|
||||||
has three options (Device card / HA more-info / Toggle), no "card
|
has three options (Device card / HA more-info / Toggle), no "card
|
||||||
default" — the card editor's global tap option is gone and ignored;
|
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
|
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
|
- [ ] Binding section redesign (v1.38.0): two radios — Virtual / Pick from
|
||||||
the HA list — with a "Show entities" checkbox (tooltip) next to the
|
the HA list — with a "Show entities" checkbox (tooltip) next to the
|
||||||
second; the dropdown (search inside) appears only in HA mode, opens
|
second; the dropdown (search inside) appears only in HA mode, opens
|
||||||
itself when nothing is chosen, closes on pick; Save is blocked until a
|
itself when nothing is chosen, closes on pick; Save is blocked until a
|
||||||
binding is chosen in HA mode; groups/helpers listed always, device
|
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
|
- [ ] 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
|
rooms' solid strokes are trimmed out beneath it (hover doesn't bring
|
||||||
them back), walls elsewhere stay solid; the dashes render ABOVE the
|
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;
|
- [ ] 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
|
near a shared wall it turns pointer and the exact stretch that would
|
||||||
open is previewed (amber dashed); an already-open boundary previews red
|
open is previewed (amber dashed); an already-open boundary previews red
|
||||||
solid (the click will close it); preview follows the cursor and clears
|
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
|
- [ ] Open boundaries (v1.37.0): the Plan editor's "Open boundary" tool
|
||||||
toggles a virtual wall between two rooms by clicking their shared wall
|
toggles a virtual wall between two rooms by clicking their shared wall
|
||||||
(pull like Split; miss → toast); open stretches render dashed (amber
|
(pull like Split; miss → toast); open stretches render dashed (amber
|
||||||
highlight while the tool is active); glow light floods the whole
|
highlight while the tool is active); glow light floods the whole
|
||||||
connected open zone transitively, door sectors work from the zone's
|
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
|
- [ ] Sector wedge fix (v1.36.3): door sectors never darken the light INSIDE
|
||||||
the room — room outline and sectors are separate clipPath children
|
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"
|
- [ ] Per-source glow radius (v1.36.2): the device dialog has a "Glow radius"
|
||||||
field (HA units; empty = general-settings default shown as placeholder);
|
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
|
- [ ] 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,
|
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
|
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
|
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
|
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;
|
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
|
- [ ] Glow fill (v1.35.0): fill mode "Light sources" — every room painted with
|
||||||
one uniform darkness color; lit lamps glow with a radial gradient
|
one uniform darkness color; lit lamps glow with a radial gradient
|
||||||
(rgb_color → color temp → default color; brightness scales opacity),
|
(rgb_color → color temp → default color; brightness scales opacity),
|
||||||
clipped by the source's room plus door sectors into NEIGHBOUR rooms
|
clipped by the source's room plus door sectors into NEIGHBOUR rooms
|
||||||
(entrance doors leak nothing; windows don't spill); radius set in
|
(entrance doors leak nothing; windows don't spill); radius set in
|
||||||
general settings in HA units (m/ft, stored in cm); no shadow casting —
|
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
|
- [ ] 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;
|
(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
|
the parent's fill renders with an evenodd hole so the ring paints
|
||||||
correctly; the island stays clickable; partial overlaps and duplicate
|
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
|
- [ ] 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
|
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
|
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
|
- [ ] 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
|
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
|
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 —
|
- [ ] 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
|
- [ ] 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
|
Device and Background editors too (instant "I'm editing" cue), not in
|
||||||
View; in the Background editor rooms/devices/openings/labels fade to 35%
|
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 /
|
- [ ] Background editor (v1.33.0): third tab with its own toolbar (select /
|
||||||
line / rect / oval / text / erase + color, width, fill, X); shapes drag-
|
line / rect / oval / text / erase + color, width, fill, X); shapes drag-
|
||||||
drawn with grid snap and live preview; degenerate shapes dropped; text
|
drawn with grid snap and live preview; degenerate shapes dropped; text
|
||||||
via dialog (S/M/L, color; dblclick re-edits); Select moves (snap), Delete
|
via dialog (S/M/L, color; dblclick re-edits); Select moves (snap), Delete
|
||||||
removes, Erase deletes on click; Esc: draft → selection → select tool →
|
removes, Erase deletes on click; Esc: draft → selection → select tool →
|
||||||
View; decor renders under rooms, visible everywhere, inert outside the
|
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
|
- [ ] 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
|
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),
|
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 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
|
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
|
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:
|
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:
|
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/split pick highlight (v1.31.2): the first room clicked with the
|
||||||
Merge tool (and the split-selected room) gets an amber outline + fill;
|
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
|
- [ ] 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
|
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
|
- [ ] Room cards (v1.31.0): with metrics enabled in space settings (4
|
||||||
checkboxes: temperature, humidity, avg Zigbee, lights) the room name gets
|
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
|
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
|
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
|
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);
|
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
|
- [ ] Esc closes dialogs (v1.30.4): Escape closes the topmost dialog (opening
|
||||||
info, device info, icon rules, general settings, device editor, opening
|
info, device info, icon rules, general settings, device editor, opening
|
||||||
editor, space dialog incl. abandoning an import queue); stacked dialogs
|
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
|
- [ ] 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"
|
- [ ] 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
|
(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);
|
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
|
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
|
- [ ] 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
|
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
|
- [ ] Lock action (v1.30.0): opening info card (View) shows Unlock (red) when
|
||||||
locked / Lock when unlocked; button calls the lock service; disabled while
|
locked / Lock when unlocked; button calls the lock service; disabled while
|
||||||
locking/unlocking; hidden when unavailable; plan-icon tap still never
|
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
|
- [ ] 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
|
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]
|
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]
|
- [ ] No devices at all in HA (fresh instance) → plan renders, "0 dev.", no console errors [auto: smoke_new_device]
|
||||||
|
|
||||||
## Device dialog (markers) ★
|
## 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
|
- [ ] Virtual device: requires name; room required; renders dashed
|
||||||
- [ ] Sub-area rooms (v1.28.0): a room WITHOUT an HA area appears in the marker
|
- [ ] 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,
|
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
|
- [ ] Room override moves the icon to the room center
|
||||||
- [ ] Tap-action override select (default/info/more-info/toggle) saves and applies
|
- [ ] 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]
|
- [ ] 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]
|
- [ ] 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 ★
|
## Icon rules ★
|
||||||
|
|
||||||
- [ ] ⬡ opens the editor with current rules (defaults if none saved)
|
- [ ] ⬡ opens the editor with current rules (defaults if none saved)
|
||||||
- [ ] Test field resolves live; add/delete/reorder rows; first match wins [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) [auto]
|
- [ ] Invalid regex highlights red and is skipped at runtime (other rules still work) [manual]
|
||||||
- [ ] Reset to defaults; saving defaults stores nothing (settings key removed) [auto]
|
- [ ] 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 [auto]
|
- [ ] 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
|
- [ ] Rules survive reload; second browser sees them after live-sync
|
||||||
|
|
||||||
## Tap actions & gestures ★
|
## Tap actions & gestures ★
|
||||||
|
|
||||||
- [ ] Default: tap → info card; card option `toggle`: tap toggles lights/switches/fans/humidifiers only [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 [auto]; covers/valves toggle only with explicit per-device override [auto]
|
- [ ] 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 [auto]
|
- [ ] 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
|
- [ ] 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
|
## 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
|
- [ ] Pinch zoom + two-finger pan on touch; one-finger pan when zoomed
|
||||||
- [ ] Zoom level persists per space (localStorage), restored on reload
|
- [ ] Zoom level persists per space (localStorage), restored on reload
|
||||||
- [ ] Window resize / sidebar collapse refits without distortion
|
- [ ] 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
|
- [ ] Labels legible on light and dark plans (no text shadow) at min/max zoom
|
||||||
|
|
||||||
## Multi-client & concurrency ★
|
## Multi-client & concurrency ★
|
||||||
@@ -387,13 +404,13 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
|
|
||||||
## Edge cases
|
## 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
|
- [ ] Space with zero rooms → renders; markup hint visible
|
||||||
- [ ] Room without area + borders ON → drawn, click does nothing, no area tooltip signal
|
- [ ] 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]
|
- [ ] No zigbee devices anywhere → no LQI badges, lqi fill leaves all rooms unfilled [manual]
|
||||||
- [ ] 100+ devices in one space → build under ~50 ms [auto], drag stays smooth
|
- [ ] 100+ devices in one space → build under ~50 ms [manual], drag stays smooth
|
||||||
- [ ] Very long device/room names → ellipsis/wrap, no layout explosion
|
- [ ] Very long device/room names → ellipsis/wrap, no layout explosion
|
||||||
- [ ] HTML/emoji in names (`<b>xss</b>`, 🚿) → rendered as text, never as markup [auto]
|
- [ ] HTML/emoji in names (`<b>xss</b>`, 🚿) → 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]
|
- [ ] 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]
|
- [ ] 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
|
- [ ] 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
|
## 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
|
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
|
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`.
|
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
|
still rejects a bad cut. README (en+ru) gained the merge/split/ruler/scale documentation it
|
||||||
was missing. The earlier self-run record follows.
|
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`,
|
(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:
|
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
|
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)
|
## houseplan-space-card (read-only embedded)
|
||||||
- [ ] `type: custom:houseplan-space-card, space: <id>` renders the space identical to the full
|
- [ ] `type: custom:houseplan-space-card, space: <id>` 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,
|
- [ ] 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]
|
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=<id>`) [auto]
|
- [ ] Footer button opens the full component already showing that space (deep-link `#space=<id>`) [manual]
|
||||||
- [ ] Several cards with different `space` coexist on one board; one shared config WS request
|
- [ ] 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
|
- [ ] `show_button: false` hides the footer
|
||||||
- [ ] Full card honours `#space=<id>` on load and on hashchange; invalid id ignored [auto]
|
- [ ] Full card honours `#space=<id>` on load and on hashchange; invalid id ignored [manual]
|
||||||
|
|
||||||
## Presence ripples / per-device icon (v1.22.0)
|
## Presence ripples / per-device icon (v1.22.0)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.43.1",
|
"version": "1.43.2",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
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_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
|
|||||||
Reference in New Issue
Block a user