mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
fix v1.44.2: external review CR-1..CR-3
CR-1: the lock invariant is restated precisely (never by an accidental
tap; the door card's labeled button is the ONE sanctioned surface),
unlocking now confirms, and smoke_lock_invariant exercises all five
actuation paths (icon tap, controls[], card entities, _cardToggle,
opening card).
CR-2: attachment migration is transactional — the server COPIES files,
the config is committed with its revision check, and only then the old
folder is removed via the new houseplan/files/cleanup. A rejected save
no longer leaves the stored urls pointing at an emptied folder.
CR-3: migrate returns an exact {source: written} mapping; only confirmed
copies are rewritten, destination name collisions get a unique name
instead of silently linking a pre-existing file, and a failed migration
raises a toast instead of being swallowed.
+1 unit test (119), +1 backend test, +1 smoke (51 total); docs
same-commit
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// review CR-1: exercise EVERY actuation path and prove locks/alarms are safe
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
const { page, browser } = await launch();
|
||||
const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const calls = [];
|
||||
c.hass = { ...c.hass, callService: (d, s, data) => { calls.push(`${d}.${s}:${data.entity_id}`); return Promise.resolve(); },
|
||||
states: { ...c.hass.states,
|
||||
'lock.front_door': { state: 'locked', attributes: { friendly_name: 'Front door' } },
|
||||
'alarm_control_panel.home': { state: 'armed_away', attributes: {} } } };
|
||||
await c.updateComplete;
|
||||
c._setMode('view'); await c.updateComplete;
|
||||
const lockCalls = () => calls.filter((x) => x.includes('lock.') || x.includes('alarm_control_panel.'));
|
||||
// 1) тап по значку устройства с замком
|
||||
const lockDev = c._devices.find((d) => d.entities?.some((e) => e.startsWith('lock.'))) || c._devices[0];
|
||||
const fake = { ...lockDev, primary: 'lock.front_door', tapAction: 'toggle',
|
||||
marker: { ...(lockDev.marker || {}), tap_action: 'toggle' } };
|
||||
c._clickDevice(new MouseEvent('click'), fake);
|
||||
out.iconTapSafe = lockCalls().length === 0;
|
||||
// 2) controls[] с замком внутри
|
||||
const withControls = { ...fake, tapAction: 'toggle',
|
||||
marker: { controls: ['lock.front_door', 'alarm_control_panel.home'], tap_action: 'toggle' } };
|
||||
c._clickDevice(new MouseEvent('click'), withControls);
|
||||
out.controlsSafe = lockCalls().length === 0;
|
||||
// 3) карточка устройства: замок отдаётся в more-info, а не тумблером
|
||||
const kinds = c._cardEntities({ ...fake, entities: ['lock.front_door', 'alarm_control_panel.home'] });
|
||||
out.cardNoToggleForLocks = kinds.every((k) => k.kind !== 'toggle');
|
||||
c._cardToggle('lock.front_door');
|
||||
c._cardToggle('alarm_control_panel.home');
|
||||
out.cardToggleRefuses = lockCalls().length === 0;
|
||||
// 4) кнопка в карточке двери — единственная разрешённая поверхность, и спрашивает подтверждение
|
||||
let asked = null;
|
||||
window.confirm = (msg) => { asked = msg; return false; };
|
||||
c._lockAction('lock.front_door', 'unlock');
|
||||
out.unlockAsksConfirm = asked !== null && lockCalls().length === 0;
|
||||
window.confirm = () => true;
|
||||
c._lockAction('lock.front_door', 'unlock');
|
||||
out.unlockAfterConfirm = calls.at(-1) === 'lock.unlock:lock.front_door';
|
||||
// запирание не спрашивает
|
||||
asked = null;
|
||||
window.confirm = (m) => { asked = m; return true; };
|
||||
c._lockAction('lock.front_door', 'lock');
|
||||
out.lockNoConfirm = asked === null && calls.at(-1) === 'lock.lock:lock.front_door';
|
||||
return out;
|
||||
});
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
Reference in New Issue
Block a user