fix: polish vacuum map routes

Treat explicit empty route lists as authoritative, preserve them through single-space export, group deleted-space routes, and render vacuums from the immutable vacuum-only snapshot subset.

Issue: #443
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-03 23:24:08 +03:00
parent 83201da15d
commit cebd20a9b5
42 changed files with 553 additions and 292 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ test('i18n: lazy editor dictionaries carry matching keys and placeholders', () =
const supportEn = supportDictionaries.get('en');
const keys = Object.keys(supportEn).sort();
const placeholders = (value) => (String(value).match(/\{\w+\}/gu) || []).sort();
assert.equal(keys.length, 77);
assert.equal(keys.length, 78);
assert.equal(Object.keys(en).filter((key) => key.startsWith('support.')).join(','), 'support.title');
for (const { code } of LANGUAGE_REGISTRY) {
const dictionary = supportDictionaries.get(code);
+1 -1
View File
@@ -34,7 +34,7 @@ test('all current floor/live layers remain in the one main scene', () => {
for (const renderer of [
'_renderDecorLayer()', '_renderRoomHoverFill(roomHover)', '_renderGlowLayer(space, disp)',
'_renderSunRays(space)', '_renderOpenings(disp)',
'_renderVacuums(this._renderDevices, view, space.id)',
'_renderVacuums(this._renderVacuumDevices, view, space.id)',
]) assert.ok(card.includes(renderer), `missing ${renderer}`);
assert.match(card, /const point = this\._scenePoint\(\[pos\.x, pos\.y\]\)/);
assert.match(card, /const point = this\._scenePoint\(\[p\.x, p\.y\]\)/);
+22
View File
@@ -63,6 +63,28 @@ test('RenderDeviceSnapshot keeps immutable facts and excludes live HA capabiliti
assert.equal('set' in snapshot.facts, false);
});
test('RenderDeviceSnapshot exposes one immutable vacuum-only roster subset', () => {
const devices = [
{ id: 'lamp', entities: ['light.lamp'] },
{ id: 'robot', entities: ['vacuum.robot'] },
{ id: 'sensor', entities: ['sensor.temp'] },
];
const snapshot = createRenderDeviceSnapshot({
sourceSequence: 8,
hass: { states: {}, entities: {}, devices: {} },
devices,
presentations: new Map(),
facts: new Map([['vacuum:robot', { moving: false }]]),
});
devices[1].entities.push('camera.late');
assert.deepEqual(snapshot.vacuumDevices.map((device) => device.id), ['robot']);
assert.equal(snapshot.vacuumDevices[0], snapshot.devices[1], 'subset reuses the same cloned row');
assert.deepEqual(snapshot.vacuumDevices[0].entities, ['vacuum.robot']);
assert.ok(Object.isFrozen(snapshot.vacuumDevices));
assert.throws(() => snapshot.vacuumDevices.push({ id: 'late' }), TypeError);
});
const methodBody = (source, name) => {
const start = source.search(new RegExp(`private\\s+(?:async\\s+)?${name}\\(`));
assert.notEqual(start, -1, `${name} exists`);
+15
View File
@@ -96,6 +96,21 @@ test('явные маршруты вытесняют легаси-калибро
assert.equal(routes[0].space, 'floor2');
});
test('explicit empty routes remain authoritative over legacy calibration', () => {
const legacy = { source: 'camera.robot', calibration: { m1: IDENTITY } };
assert.equal(effectiveRoutes('mk', legacy, 'floor1').length, 1, 'absent keeps legacy');
assert.equal(
effectiveRoutes('mk', { ...legacy, map_routes: null }, 'floor1').length,
1,
'null keeps legacy compatibility',
);
assert.deepEqual(
effectiveRoutes('mk', { ...legacy, map_routes: [] }, 'floor1'),
[],
'an explicit empty array disables every legacy route',
);
});
test('без источника легаси-маршрутов не возникает, но discovery его подставляет', () => {
const marker = { calibration: { m1: IDENTITY } };
assert.deepEqual(effectiveRoutes('mk', marker, 'floor1'), []);