Files
houseplan-card/test/houseplan-render-lifecycle.test.mjs
T
2026-09-04 22:03:45 +03:00

94 lines
3.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { RenderLifecycle } from '../test-build/houseplan-render-lifecycle.js';
import { LiveRuntime } from '../test-build/live-interaction-runtime.js';
const row = (state = 'off') => ({ entity_id: 'light.plan', state, attributes: {} });
const base = () => {
const connection = {};
const entities = {};
const devices = {};
const areas = {};
const themes = {};
const user = {};
const config = {};
const floors = {};
const services = {};
const panels = {};
const locale = { language: 'en', number_format: 'language', time_format: 'language' };
return {
connection, entities, devices, areas, themes, user, config, floors, services, panels, locale,
language: 'en', states: { 'light.plan': row(), 'sensor.other': { state: '0' } },
};
};
test('HA intake runs once even when an unrelated visual update is skipped', () => {
const lifecycle = new RenderLifecycle();
const before = base();
const after = { ...before, states: { ...before.states, 'sensor.other': { state: '1' } } };
let intakes = 0;
lifecycle.observe(before, after, { entityIds: ['light.plan'] }, () => intakes++);
lifecycle.intake(after, () => intakes++);
assert.equal(intakes, 1);
});
const liveHost = (lifecycle) => ({
_renderLife: lifecycle, _pointers: new Map(), _cameraTransition: { active: false },
_deviceDrag: null, _physicalDrag: null, _physicalRotate: null, _decorMove: null,
_decorDraft: null, _dtDrag: null, _bdDrag: null, _opDrag: null,
_resize: { dragging: false },
});
test('a relevant HA tick is deferred only while a continuous interaction is active', () => {
const lifecycle = new RenderLifecycle();
const host = liveHost(lifecycle);
const live = new LiveRuntime(host);
const before = base();
const after = { ...before, states: { ...before.states, 'light.plan': row('on') } };
host._pointers.set(1, {});
assert.equal(live.hass(before, after, { entityIds: ['light.plan'] }, () => {}), false);
assert.equal(live.take(), true);
assert.equal(live.take(), false);
host._pointers.clear();
const final = { ...after, states: { ...after.states, 'light.plan': row('off') } };
assert.equal(live.hass(after, final, { entityIds: ['light.plan'] }, () => {}), true);
});
test('gesture reconciliation is last-wins and a normal full update subsumes it', () => {
const lifecycle = new RenderLifecycle();
const host = liveHost(lifecycle);
const live = new LiveRuntime(host);
const before = base();
const first = { ...before, states: { ...before.states, 'light.plan': row('on') } };
const second = { ...first, states: { ...first.states, 'light.plan': row('unavailable') } };
host._pointers.set(1, {});
assert.equal(live.hass(before, first, { entityIds: ['light.plan'] }, () => {}), false);
assert.equal(live.hass(first, second, { entityIds: ['light.plan'] }, () => {}), false);
assert.equal(live.take(), true);
assert.equal(live.take(), false);
live.hass(second, first, { entityIds: ['light.plan'] }, () => {});
live.clear();
assert.equal(live.take(), false);
});
test('diagnostics scan is cached and invalidated by tracked state presence', () => {
const lifecycle = new RenderLifecycle();
const before = base();
const markers = [{ binding: 'device:kitchen' }];
let scans = 0;
const resolve = () => {
scans++;
return { kind: 'active', enabledEntityIds: ['light.plan'], allEntityIds: ['light.plan'] };
};
assert.equal(lifecycle.diagnostics(before, markers, resolve).bindings.active, 1);
assert.equal(lifecycle.diagnostics(before, markers, resolve).bindings.active, 1);
assert.equal(scans, 1);
const missing = { ...before, states: { 'sensor.other': before.states['sensor.other'] } };
lifecycle.observe(before, missing, { entityIds: ['light.plan'] }, () => {});
lifecycle.diagnostics(missing, markers, resolve);
assert.equal(scans, 2);
});