diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index f07f56b6..8322625a 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -11099,7 +11099,8 @@ const MUTANT_DEFINITIONS = [ }, { id: 'general-required-number-falls-back-to-hidden-value', - guard: 'node demo/smoke_general_settings_form.mjs', + // #631 AC3: гард — юнит test-build (секунды) вместо смока с браузером. + guard: 'node --test test/dialog-form-problems.test.mjs', because: '#614 AC1: an empty required numeric draft must stay visible and block Save; ' + 'validating the last typed number silently restores the pre-fix hidden-value fallback', patches: [{ @@ -11110,7 +11111,8 @@ const MUTANT_DEFINITIONS = [ }, { id: 'space-required-temperature-forgets-raw-draft', - guard: 'node demo/smoke_space_settings_form.mjs', + // #631 AC3: гард — юнит test-build (секунды) вместо смока с браузером. + guard: 'node --test test/dialog-form-problems.test.mjs', because: '#614 AC1: each required temperature input owns its raw draft and its own ' + 'problem count; consulting the last valid number hides an empty lower bound', patches: [{ @@ -11190,7 +11192,8 @@ const MUTANT_DEFINITIONS = [ }, { id: 'marker-virtual-name-remains-a-late-toast', - guard: 'node demo/smoke_device_settings_form.mjs', + // #631 AC3: гард — юнит test-build (секунды) вместо смока с браузером. + guard: 'node --test test/dialog-form-problems.test.mjs', because: '#614 AC2: a missing virtual name is an inline form problem before Save; ' + 'dropping this problem restores the late _saveMarker guard with no field guidance', patches: [{ @@ -11199,6 +11202,75 @@ const MUTANT_DEFINITIONS = [ replace: " if (d.binding === 'virtual' && !d.name.trim() && false) {", }], }, + // #631: контракты снимка и валидации четырёх диалогов — юнитами test-build. + { + id: 'dialog-baseline-key-order-sensitive', + guard: 'node --test --test-name-pattern="#631" test/dialog-baseline.test.mjs', + because: '#631 AC1: одинаковый черновик с другим порядком ключей верхнего уровня — ' + + 'не изменение. Без сортировки «Сохранить» загорается у нетронутого диалога, а ' + + 'закрытие спрашивает о правках, которых не было', + patches: [{ + file: 'src/editors/dialog-baseline.ts', + find: ' .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));', + replace: ' ;', + }], + }, + { + id: 'dialog-baseline-missing-reads-clean', + guard: 'node --test --test-name-pattern="#631" test/dialog-baseline.test.mjs', + because: '#631 AC1: без снимка диалог считается изменённым — Save доступен. Обратный ' + + 'ответ блокирует кнопку у диалога, открытого путём без снимка, и правку нельзя сохранить', + patches: [{ + file: 'src/editors/dialog-baseline.ts', + find: ' if (baseline === undefined) return true;', + replace: ' if (baseline === undefined) return false;', + }], + }, + { + id: 'space-dialog-key-counts-raw-scale-input', + guard: 'node --test --test-name-pattern="#631" test/dialog-baseline.test.mjs', + because: '#631 AC1: сырой текст поля масштаба транзиентен — значение судится по cellCm. ' + + 'Включить его в отпечаток — и «5» против «5,0» делает нетронутый диалог изменённым', + patches: [{ + file: 'src/editors/space-form-state.ts', + find: " 'busy', 'pickSaved', 'saved', 'savedBusy', 'savedAspect', 'cellCmInput',", + replace: " 'busy', 'pickSaved', 'saved', 'savedBusy', 'savedAspect',", + }], + }, + { + id: 'room-draft-key-drops-label-scale', + guard: 'node --test --test-name-pattern="#631" test/dialog-baseline.test.mjs', + because: '#631 AC1: отпечаток комнаты собирает все десять полей черновика. Потерять одно — ' + + 'и правка размера подписей не делает диалог изменённым: Save не загорается, закрытие ' + + 'не спрашивает', + patches: [{ + file: 'src/editors/room-form-state.ts', + find: ' nameScale: h._roomNameScale, labelScale: h._roomLabelScale,', + replace: ' nameScale: h._roomNameScale,', + }], + }, + { + id: 'room-create-ignores-area', + guard: 'node --test --test-name-pattern="#631" test/dialog-form-problems.test.mjs', + because: '#631 AC2 (§6.2 #600): новая комната создаётся по зоне без имени. Требовать имя ' + + 'и в create — ложная ошибка под полем и заблокированный Save у корректного черновика', + patches: [{ + file: 'src/editors/room-form-state.ts', + find: ' if (edit ? !h._nameSel.trim() : !(h._areaSel || h._nameSel.trim())) {', + replace: ' if (!h._nameSel.trim()) {', + }], + }, + { + id: 'marker-ha-binding-accepts-virtual', + guard: 'node --test --test-name-pattern="#631" test/dialog-form-problems.test.mjs', + because: '#631 AC2: в режиме HA «virtual» — не привязка. Принять его — и маркер сохраняется ' + + 'без сущности, хотя форма обещала привязку к Home Assistant', + patches: [{ + file: 'src/editors/marker-form-state.ts', + find: " if (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual')) {", + replace: " if (d.bindingMode === 'ha' && !d.binding) {", + }], + }, { id: 'marker-save-click-forgets-baseline-before-result', guard: 'node demo/smoke_device_settings_form.mjs', diff --git a/test/dialog-baseline.test.mjs b/test/dialog-baseline.test.mjs index 13647ce2..89e6f73f 100644 --- a/test/dialog-baseline.test.mjs +++ b/test/dialog-baseline.test.mjs @@ -7,8 +7,22 @@ import { forgetDialogBaseline, rememberDialogBaseline, restoreDialogBaseline, + restoreWarmDialogBaseline, stableKey, + warmDialogBaseline, } from '../test-build/editors/dialog-baseline.js'; +import { + forgetSpaceDialogBaseline, rememberSpaceDialogBaseline, spaceDialogDirty, spaceDialogDraftKey, +} from '../test-build/editors/space-form-state.js'; +import { + forgetGeneralBaseline, generalDirty, generalDraftKey, rememberGeneralBaseline, +} from '../test-build/editors/general-form-state.js'; +import { + MARKER_DIALOG_TRANSIENT_KEYS, forgetMarkerBaseline, markerDirty, markerDraftKey, rememberMarkerBaseline, +} from '../test-build/editors/marker-form-state.js'; +import { + forgetRoomBaseline, rememberRoomBaseline, roomDirty, roomDraftKey, +} from '../test-build/editors/room-form-state.js'; test('warm transfer preserves clean and dirty meaning on a replacement host (#614)', () => { const oldHost = {}; @@ -37,3 +51,189 @@ test('baseline transfer is isolated by dialog kind and can be explicitly forgott assert.equal(dialogDirty(host, 'marker', 'marker-v1'), true); assert.equal(dialogDirty(host, 'settings', 'settings-v1'), false); }); + +// #631 AC1: контракт отпечатка и снимка — юнитом, а не только браузерными смоками. +// Проверяется то, что человек видит как «Сохранить активна / нет» и вопрос при +// закрытии: одинаковый черновик не должен выглядеть изменённым, транзиентное +// поле (busy, сырой ввод, фильтр списка) — тоже. + +test('#631 stableKey: top-level key order does not change the key', () => { + const a = stableKey({ name: 'Kitchen', area: 'kitchen', fill: 'temp' }); + const b = stableKey({ fill: 'temp', name: 'Kitchen', area: 'kitchen' }); + assert.equal(a, b); + assert.notEqual(stableKey({ name: 'Kitchen' }), stableKey({ name: 'Hall' })); + // Лишний ключ — изменение, даже если его значение пустое. + assert.notEqual(stableKey({ name: 'Kitchen' }), stableKey({ name: 'Kitchen', area: null })); +}); + +test('#631 stableKey: transient keys are ignored, all others are compared', () => { + const transient = new Set(['busy', 'filter']); + const clean = stableKey({ name: 'Lamp', busy: false, filter: '' }, transient); + assert.equal(stableKey({ name: 'Lamp', busy: true, filter: 'kit' }, transient), clean); + assert.equal(stableKey({ name: 'Lamp' }, transient), clean); + assert.notEqual(stableKey({ name: 'Lamp 2', busy: false, filter: '' }, transient), clean); + // Без набора транзиентных ключей сравнивается всё. + assert.notEqual(stableKey({ name: 'Lamp', busy: true }), stableKey({ name: 'Lamp', busy: false })); +}); + +test('#631 stableKey: nested objects are compared as-is — values structurally, nested key order included', () => { + // Выбранный контракт: сортируется только верхний уровень. Вложенный объект + // сериализуется JSON.stringify как есть — равные значения в том же порядке + // ключей дают равный отпечаток, другой порядок вложенных ключей — другой. + // Черновики строятся из одних и тех же конструкторов, поэтому порядок + // вложенных ключей стабилен; перестановка — это изменение, а не шум. + const fill = { c: '#ff0000', a: 0.5 }; + assert.equal(stableKey({ customFill: fill }), stableKey({ customFill: { c: '#ff0000', a: 0.5 } })); + assert.notEqual(stableKey({ customFill: fill }), stableKey({ customFill: { c: '#ff0000', a: 0.6 } })); + assert.notEqual(stableKey({ customFill: fill }), stableKey({ customFill: { a: 0.5, c: '#ff0000' } })); + assert.equal(stableKey({ list: [1, 2] }), stableKey({ list: [1, 2] })); + assert.notEqual(stableKey({ list: [1, 2] }), stableKey({ list: [2, 1] })); +}); + +test('#631 dialogDirty: no baseline means dirty — Save stays available', () => { + const host = {}; + const key = stableKey({ name: 'Kitchen' }); + for (const kind of ['space', 'settings', 'room', 'marker']) { + assert.equal(dialogDirty(host, kind, key), true, kind); + } + rememberDialogBaseline(host, 'room', key); + assert.equal(dialogDirty(host, 'room', key), false); + // Снимок одного вида не делает чистым другой вид того же хоста. + assert.equal(dialogDirty(host, 'marker', key), true); + // И не переносится на другой хост. + assert.equal(dialogDirty({}, 'room', key), true); +}); + +test('#631 restoreDialogBaseline(undefined) forgets; warm helpers accept only the four dialog kinds', () => { + const host = {}; + rememberDialogBaseline(host, 'space', 'k1'); + restoreDialogBaseline(host, 'space', undefined); + assert.equal(dialogBaseline(host, 'space'), undefined); + assert.equal(dialogDirty(host, 'space', 'k1'), true); + + const warm = {}; + for (const kind of ['space', 'settings', 'room', 'marker']) { + restoreWarmDialogBaseline(warm, kind, `${kind}-key`); + assert.equal(warmDialogBaseline(warm, kind), `${kind}-key`, kind); + } + restoreWarmDialogBaseline(warm, 'wall', 'wall-key'); + assert.equal(warmDialogBaseline(warm, 'wall'), undefined); + restoreWarmDialogBaseline(warm, 'marker', undefined); + assert.equal(warmDialogBaseline(warm, 'marker'), undefined); +}); + +// --- транзиентные наборы четырёх диалогов --------------------------------- + +const spaceDraft = (patch = {}) => ({ + mode: 'edit', spaceId: 's1', title: 'Ground floor', planUrl: null, planFile: null, + source: 'draw', showBorders: true, showNames: true, zeroWallStyle: 'dashed', + hideDecor: false, hideOpenings: false, roomColor: '#888888', roomOpacity: 0.2, + bgColor: null, bgMode: null, northDeg: null, sunRays: null, fillMode: 'none', + customFill: null, glowEnabled: true, tempMin: 18, tempMax: 26, cellCm: 5, + ...patch, +}); + +test('#631 space dialog: every transient key is ignored, a real setting makes it dirty', () => { + const host = {}; + const base = spaceDraft(); + rememberSpaceDialogBaseline(host, base); + assert.equal(spaceDialogDirty(host, base), false); + assert.equal(spaceDialogDirty(host, spaceDraft({ fillMode: 'temp', title: 'Ground floor' })), true); + assert.equal(spaceDialogDirty(host, spaceDraft({ title: 'First floor' })), true); + const transient = { + busy: true, pickSaved: true, saved: [{ name: 'a.png' }], savedBusy: true, savedAspect: 1.5, + cellCmInput: '5,0', tempMinInput: '', tempMaxInput: 'x', northDegInput: '12', + cellCmTouched: true, displayTouched: true, deleteBlockers: ['r1'], copy: { busy: true }, + }; + for (const [k, v] of Object.entries(transient)) { + assert.equal(spaceDialogDirty(host, spaceDraft({ [k]: v })), false, k); + } + assert.equal(spaceDialogDraftKey(spaceDraft(transient)), spaceDialogDraftKey(base)); + forgetSpaceDialogBaseline(host); + assert.equal(spaceDialogDirty(host, base), true); +}); + +test('#631 space dialog: a picked plan file is compared by name, not by its bytes', () => { + const file = (b64) => ({ ext: 'png', b64, aspect: 1.2, name: 'plan.png' }); + assert.equal(spaceDialogDraftKey(spaceDraft({ planFile: file('AAAA') })), + spaceDialogDraftKey(spaceDraft({ planFile: file('BBBB') }))); + assert.notEqual(spaceDialogDraftKey(spaceDraft({ planFile: file('AAAA') })), + spaceDialogDraftKey(spaceDraft({ planFile: { ...file('AAAA'), name: 'other.png' } }))); + assert.notEqual(spaceDialogDraftKey(spaceDraft({ planFile: file('AAAA') })), spaceDialogDraftKey(spaceDraft())); +}); + +const generalDraft = (patch = {}) => ({ + glowRadius: 25, glowRadiusInput: '25', northDeg: null, northDegInput: '', fillColors: {}, + busy: false, ...patch, +}); + +test('#631 general settings: busy and raw inputs are transient, stored values are not', () => { + const host = {}; + const base = generalDraft(); + rememberGeneralBaseline(host, base); + assert.equal(generalDirty(host, base), false); + for (const [k, v] of Object.entries({ busy: true, glowRadiusInput: '30', northDegInput: '90' })) { + assert.equal(generalDirty(host, generalDraft({ [k]: v })), false, k); + } + assert.equal(generalDirty(host, generalDraft({ glowRadius: 30 })), true); + assert.equal(generalDirty(host, generalDraft({ northDeg: 90 })), true); + assert.equal(generalDraftKey(generalDraft({ busy: true })), generalDraftKey(base)); + forgetGeneralBaseline(host); + assert.equal(generalDirty(host, base), true); +}); + +const markerDraft = (patch = {}) => ({ + binding: 'light.kitchen', bindingMode: 'ha', name: 'Lamp', icon: 'mdi:lamp', + tapAction: 'toggle', tapTarget: '', valueBadgeTouched: false, valueBadgeEnabled: false, + valueBadgeSource: null, glowRadius: null, originalBinding: 'light.kitchen', ...patch, +}); + +test('#631 marker dialog: exactly the declared transient keys are ignored, Touched/original are not', () => { + assert.deepEqual([...MARKER_DIALOG_TRANSIENT_KEYS].sort(), [ + 'autoIcon', 'bindingFilter', 'bindingOpen', 'busy', 'controlsFilter', 'runFilter', 'tapHintAnnouncement', 'uploadId', + ]); + const host = {}; + const base = markerDraft(); + rememberMarkerBaseline(host, base); + assert.equal(markerDirty(host, base), false); + const transient = { + bindingOpen: true, bindingFilter: 'kit', runFilter: 'scr', controlsFilter: 'x', busy: true, + tapHintAnnouncement: 'hint', uploadId: 7, autoIcon: 'mdi:lightbulb', + }; + for (const [k, v] of Object.entries(transient)) { + assert.equal(markerDirty(host, markerDraft({ [k]: v })), false, k); + } + assert.equal(markerDirty(host, markerDraft({ valueBadgeTouched: true })), true); + assert.equal(markerDirty(host, markerDraft({ originalBinding: 'light.hall' })), true); + assert.equal(markerDirty(host, markerDraft({ name: 'Lamp 2' })), true); + assert.equal(markerDraftKey(markerDraft(transient)), markerDraftKey(base)); + forgetMarkerBaseline(host); + assert.equal(markerDirty(host, base), true); +}); + +const roomHost = (patch = {}) => ({ + _nameSel: 'Kitchen', _areaSel: 'kitchen', _roomFill: '', _roomCustomFill: null, + _roomTempMin: '', _roomTempMax: '', _roomTempSrc: '', _roomHumSrc: '', + _roomNameScale: 1, _roomLabelScale: 1, _roomEditId: 'r1', ...patch, +}); + +test('#631 room dialog: the key collects the ten draft fields of the host and nothing else', () => { + const host = roomHost(); + rememberRoomBaseline(host); + assert.equal(roomDirty(host), false); + // Поля хоста вне черновика (id редактируемой комнаты, что угодно ещё) не входят. + assert.equal(roomDraftKey(roomHost({ _roomEditId: 'r2', _busy: true })), roomDraftKey(roomHost())); + const fields = { + _nameSel: 'Hall', _areaSel: 'hall', _roomFill: 'temp', _roomCustomFill: { c: '#00ff00', a: 1 }, + _roomTempMin: '18', _roomTempMax: '24', _roomTempSrc: 'sensor.t', _roomHumSrc: 'sensor.h', + _roomNameScale: 1.2, _roomLabelScale: 0.8, + }; + for (const [k, v] of Object.entries(fields)) { + host[k] = v; + assert.equal(roomDirty(host), true, k); + host[k] = roomHost()[k]; + assert.equal(roomDirty(host), false, `${k} restored`); + } + forgetRoomBaseline(host); + assert.equal(roomDirty(host), true); +}); diff --git a/test/dialog-form-problems.test.mjs b/test/dialog-form-problems.test.mjs index 6c63ff81..a08b616a 100644 --- a/test/dialog-form-problems.test.mjs +++ b/test/dialog-form-problems.test.mjs @@ -4,6 +4,7 @@ import assert from 'node:assert/strict'; import { generalProblems } from '../test-build/editors/general-form-state.js'; import { markerProblems } from '../test-build/editors/marker-form-state.js'; import { spaceDialogProblems } from '../test-build/editors/space-form-state.js'; +import { roomProblems } from '../test-build/editors/room-form-state.js'; import { GRID_CELL_CM_MAX, GRID_CELL_CM_MIN } from '../test-build/grid-scale.js'; const fields = (problems) => problems.map(({ field, message }) => [field, message]); @@ -130,3 +131,99 @@ test('#614 markerProblems exposes virtual name, Run target and badge source inli valueBadgeSource: null, }))), []); }); + +// #631 AC2: каждый код ошибки четырёх форм и пустой список — юнитом. Порядок +// ошибок — порядок полей формы: «Review N fields» ведёт к первому сверху. + +test('#631 generalProblems: empty list for a valid draft, glow before north when both fail', () => { + assert.deepEqual(fields(generalProblems(generalDraft('25', '90'))), []); + assert.deepEqual(fields(generalProblems(generalDraft('', '400'))), [ + ['gs-glow-radius', 'gs.error_glow_radius'], + ['gs-north', 'gs.error_north'], + ]); +}); + +test('#631 spaceDialogProblems: title and plan codes, empty list for a valid draft', () => { + assert.deepEqual(spaceFields(), []); + assert.deepEqual(spaceFields({ title: ' ' }), [['space-title', 'space.error_title']]); + assert.deepEqual(spaceFields({ source: 'file' }), [['space-plan', 'space.error_plan']]); + assert.deepEqual(spaceFields({ source: 'file', planUrl: '/local/plan.png' }), []); + assert.deepEqual(spaceFields({ source: 'file', planFile: { ext: 'png', b64: 'AA', aspect: 1, name: 'p.png' } }), []); + // Температура проверяется только в режиме заливки «температура». + assert.deepEqual(spaceFields({ fillMode: 'light', tempMinInput: '', tempMaxInput: 'x' }), []); + // Сырой ввод отсутствует — судится сохранённое число. + assert.deepEqual(spaceFields({ cellCmInput: undefined, cellCm: 5 }), []); + assert.deepEqual(spaceFields({ fillMode: 'temp', tempMinInput: undefined, tempMaxInput: undefined }), []); + // Префикс идентификатора поля берётся от вызывающего. + assert.deepEqual(fields(spaceDialogProblems(baseSpace({ title: '' }), 'sd2')), [['sd2-title', 'space.error_title']]); +}); + +test('#631 spaceDialogProblems: all problems at once come in form order', () => { + assert.deepEqual(spaceFields({ + title: '', cellCmInput: 'x', source: 'file', fillMode: 'temp', + tempMinInput: '', tempMaxInput: '', northDeg: 0, northDegInput: '400', + }), [ + ['space-title', 'space.error_title'], + ['space-cell-cm', 'space.error_scale'], + ['space-plan', 'space.error_plan'], + ['space-temp-min', 'space.error_temp_value'], + ['space-temp-max', 'space.error_temp_value'], + ['space-north-deg', 'space.error_north'], + ]); +}); + +test('#631 markerProblems: HA mode requires a real binding; all four codes come in form order', () => { + const bindingError = [['marker-binding', 'marker.error_binding']]; + assert.deepEqual(fields(markerProblems(baseMarker({ bindingMode: 'ha', binding: '' }))), bindingError); + assert.deepEqual(fields(markerProblems(baseMarker({ bindingMode: 'ha', binding: null }))), bindingError); + // «virtual» в режиме HA — не привязка; имя при этом проверяется как у виртуального. + assert.deepEqual(fields(markerProblems(baseMarker({ bindingMode: 'ha', binding: 'virtual' }))), bindingError); + assert.deepEqual(fields(markerProblems(baseMarker({ bindingMode: 'ha', binding: 'light.kitchen', name: '' }))), []); + assert.deepEqual(fields(markerProblems(baseMarker({ + bindingMode: 'ha', binding: 'virtual', name: '', tapAction: 'run', + valueBadgeTouched: true, valueBadgeEnabled: true, + }))), [ + ['marker-name', 'marker.error_virtual_name'], + ['marker-binding', 'marker.error_binding'], + ['marker-run-target', 'marker.error_run_target'], + ['marker-value-badge-source', 'marker.error_value_badge_source'], + ]); +}); + +test('#631 markerProblems: the Run target follows the effective tap action, not the stored one', () => { + const runError = [['marker-run-target', 'marker.error_run_target']]; + assert.deepEqual(fields(markerProblems(baseMarker({ tapAction: 'more-info' }), 'run')), runError); + assert.deepEqual(fields(markerProblems(baseMarker({ tapAction: 'run' }), 'toggle')), []); + assert.deepEqual(fields(markerProblems(baseMarker({ tapAction: 'run' }))), runError); +}); + +const baseRoom = (patch = {}) => ({ + _nameSel: 'Kitchen', _areaSel: '', _roomEditId: 'r1', _roomTempMin: '', _roomTempMax: '', ...patch, +}); +const roomFields = (patch) => fields(roomProblems(baseRoom(patch))); + +test('#631 roomProblems: edit needs a name, create needs a name or an area', () => { + const nameError = [['room-name', 'room.error_name']]; + assert.deepEqual(roomFields({}), []); + assert.deepEqual(roomFields({ _nameSel: ' ' }), nameError); + // В edit зона имя не заменяет. + assert.deepEqual(roomFields({ _nameSel: '', _areaSel: 'kitchen' }), nameError); + // В create — заменяет. + assert.deepEqual(roomFields({ _roomEditId: null, _nameSel: '', _areaSel: 'kitchen' }), []); + assert.deepEqual(roomFields({ _roomEditId: null, _nameSel: 'Hall', _areaSel: '' }), []); + assert.deepEqual(roomFields({ _roomEditId: null, _nameSel: ' ', _areaSel: '' }), nameError); +}); + +test('#631 roomProblems: comfort bounds are numbers or empty; name comes before the range', () => { + const rangeError = [['room-temp-min', 'room.error_temp_range']]; + for (const [min, max] of [['', ''], ['18', ''], ['', '24'], ['18,5', '24'], ['24', '18']]) { + assert.deepEqual(roomFields({ _roomTempMin: min, _roomTempMax: max }), [], `${min}..${max}`); + } + for (const [min, max] of [['cold', ''], ['', 'hot'], ['18', 'x']]) { + assert.deepEqual(roomFields({ _roomTempMin: min, _roomTempMax: max }), rangeError, `${min}..${max}`); + } + assert.deepEqual(roomFields({ _nameSel: '', _roomTempMin: 'x' }), [ + ['room-name', 'room.error_name'], + ['room-temp-min', 'room.error_temp_range'], + ]); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json index 3f2f358b..858994a6 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -23,7 +23,7 @@ "src/devices.ts", "src/device-inbox.ts", "src/virtual-light-state.ts", "src/config-store.ts", "src/config-reload-authority.ts", "src/config-write-conflict.ts", "src/summary-panel.ts", "src/summary-panel-metrics.ts", "src/summary-panel-i18n.ts", "src/summary-panel-identity.ts", "src/summary-panel-picker.ts", "src/summary-panel-runtime-loaded.ts", "src/backdrop-probe.ts", - "src/types.ts", "src/editors/dialog-baseline.ts", "src/editors/general-form-state.ts", "src/editors/space-form-state.ts", "src/editors/marker-form-state.ts", + "src/types.ts", "src/editors/dialog-baseline.ts", "src/editors/general-form-state.ts", "src/editors/space-form-state.ts", "src/editors/marker-form-state.ts", "src/editors/room-form-state.ts", "src/space-geometry.ts", "src/junction-limits.ts", "src/space-order.ts", "src/card-editor-validation.ts", "src/signing.ts", "src/initial-load.ts", "src/space-model-selection.ts", "src/space-dialog.ts",