Files
houseplan-card/demo/smoke_edge_cases.mjs
T
Matysh 41b20e1901 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.
2026-07-27 11:20:31 +03:00

88 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('&lt;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);