mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
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.
88 lines
4.5 KiB
JavaScript
88 lines
4.5 KiB
JavaScript
import { launch, check, checkAll, finish } from './serve.mjs';
|
||
const { page, browser } = await launch();
|
||
const res = await page.evaluate(async () => {
|
||
const out = {};
|
||
const mk = () => document.createElement('houseplan-card');
|
||
const sr = (c) => c.shadowRoot || c.renderRoot;
|
||
|
||
// 1) пустая инсталляция: ноль устройств/зон, конфиг с пространством без комнат
|
||
const c1 = mk();
|
||
c1.setConfig({ type: 'custom:houseplan-card' });
|
||
document.body.appendChild(c1);
|
||
c1.hass = { language:'en', locale:{language:'en'}, devices:{}, entities:{}, areas:{}, states:{},
|
||
callWS: async (m) => m.type==='houseplan/config/get'
|
||
? { config:{ spaces:[{ id:'s1', title:'Empty', plan_url:null, aspect:1.4, view_box:[0,0,1,1], rooms:[], segments:[] }], markers:[], settings:{} }, rev:1 }
|
||
: { layout:{} },
|
||
connection:{ subscribeEvents: async()=>()=>{} } };
|
||
await new Promise(r=>setTimeout(r,150));
|
||
c1.hass = { ...c1.hass };
|
||
await c1.updateComplete; await new Promise(r=>setTimeout(r,50)); await c1.updateComplete;
|
||
out.emptyDevices = c1._devices.length;
|
||
out.emptyRenders = !!sr(c1).querySelector('.stage');
|
||
out.emptyCount = sr(c1).querySelector('.count')?.textContent.trim();
|
||
c1.remove();
|
||
|
||
// 2) XSS в именах комнат и устройств
|
||
const c2 = mk();
|
||
c2.setConfig({ type: 'custom:houseplan-card' });
|
||
document.body.appendChild(c2);
|
||
const evil = '<img src=x onerror=window.__pwned=1><b>bold</b>';
|
||
c2.hass = { language:'en', locale:{language:'en'},
|
||
devices:{ d1:{ id:'d1', name: evil, model:'M<script>1</script>', area_id:'a1', identifiers:[['x','1']] } },
|
||
entities:{}, areas:{ a1:{ area_id:'a1', name:'A1' } }, states:{},
|
||
callWS: async (m) => m.type==='houseplan/config/get'
|
||
? { config:{ spaces:[{ id:'s1', title:'S', plan_url:null, aspect:1, view_box:[0,0,1,1],
|
||
rooms:[{ id:'r1', name: evil, area:'a1', poly:[[0.1,0.1],[0.9,0.1],[0.9,0.9],[0.1,0.9]] }], segments:[] }], markers:[], settings:{} }, rev:1 }
|
||
: { layout:{} },
|
||
connection:{ subscribeEvents: async()=>()=>{} } };
|
||
await new Promise(r=>setTimeout(r,150));
|
||
c2.hass = { ...c2.hass };
|
||
await c2.updateComplete; await new Promise(r=>setTimeout(r,50)); await c2.updateComplete;
|
||
out.xssPwned = !!window.__pwned;
|
||
const lbl = sr(c2).querySelector('.roomlabel');
|
||
out.xssLabelIsText = lbl ? lbl.innerHTML.includes('<img') || lbl.textContent.includes('<img') : 'no label';
|
||
out.xssDeviceRendered = sr(c2).querySelectorAll('.dev').length;
|
||
c2.remove();
|
||
|
||
// 3) легаси-записи layout v1 {x,y} без ключа s — игнорируются в norm-режиме
|
||
const c = window.__card;
|
||
c._layout = { ...c._layout, d_light1_legacy_test: { x: 500, y: 300 } };
|
||
const fake = { id: 'd_light1_legacy_test', space: 'f1' };
|
||
const p = c._pos(fake);
|
||
out.legacyIgnored = p.y !== 300; // centre y=400 if ignored; raw 300 would mean it leaked through
|
||
delete c._layout.d_light1_legacy_test;
|
||
|
||
// 4) перф: 150 устройств
|
||
const t0 = performance.now();
|
||
const devices = {}; const entities = {}; const states = {};
|
||
for (let i = 0; i < 150; i++) {
|
||
devices['p'+i] = { id:'p'+i, name:'Plug '+i, model:'Smart Plug', area_id:'living_room', identifiers:[['demo','p'+i]] };
|
||
entities['switch.p'+i] = { entity_id:'switch.p'+i, device_id:'p'+i, platform:'demo' };
|
||
states['switch.p'+i] = { state: i%2?'on':'off', attributes:{ linkquality: 50+i } };
|
||
}
|
||
const bigHass = { ...window.__mkHass(), devices:{...window.__mkHass().devices, ...devices},
|
||
entities:{...window.__mkHass().entities, ...entities}, states:{...window.__mkHass().states, ...states} };
|
||
c.hass = bigHass; c._regSignature=''; c._maybeRebuildDevices();
|
||
out.bigBuildMs = Math.round(performance.now() - t0);
|
||
out.bigCount = c._devices.length;
|
||
await c.updateComplete;
|
||
const t1 = performance.now(); c.requestUpdate(); await c.updateComplete;
|
||
out.bigRenderMs = Math.round(performance.now() - t1);
|
||
return out;
|
||
});
|
||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||
// сборка 100+ устройств должна укладываться в бюджет (docs/TESTING.md)
|
||
// timings are asserted as budgets, not frozen values (CI machines vary)
|
||
check("bigBuildMs under 200ms", res.bigBuildMs < 200);
|
||
check("bigRenderMs under 100ms", res.bigRenderMs < 100);
|
||
delete res.bigBuildMs;
|
||
delete res.bigRenderMs;
|
||
checkAll(res, {
|
||
"emptyDevices": 0,
|
||
"emptyCount": "0 dev.",
|
||
"xssPwned": false,
|
||
"xssDeviceRendered": 1,
|
||
"bigCount": 162,
|
||
});
|
||
await finish(browser, res);
|