mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
- space dialog 'Display' section: room borders/names toggles, color+opacity, fill by zigbee signal or lights (tri-state: on/off/no-lights) - draggable room name labels persisted as layout rl_<roomId> - 'no image, outline by hand' space source with canvas orientation; image optional now; switching an existing space to draw detaches its plan - demo/ synthetic-home harness lives in the repo (serve.mjs + smokes + icons gen) - docs/TESTING.md checklist (same-commit update policy); self-run found and fixed: plan_url not detached on image->draw, _stateClass crash on states without entity_id; perf measured (162 devices ~14ms build) - backend: SPACE_DISPLAY_SCHEMA validation (+test)
75 lines
4.0 KiB
JavaScript
75 lines
4.0 KiB
JavaScript
import { launch } 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;
|
|
});
|
|
console.log(JSON.stringify(res, null, 1));
|
|
await browser.close();
|