Files
houseplan-card/test/optional-space-model-contract.test.mjs
T
2026-08-20 01:38:01 +03:00

73 lines
3.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const methodBody = (name) => {
const start = source.search(new RegExp(`(?:private|protected)\\s+(?:get\\s+|async\\s+)?${name}(?:\\s*=)?(?:\\(|:)`));
assert.notEqual(start, -1, `${name} exists`);
const tail = source.slice(start + 1);
const next = tail.search(/\n (?:private|protected|public)\s/);
return source.slice(start, next < 0 ? source.length : start + 1 + next);
};
test('_spaceModel exposes absence and no call site bypasses it', () => {
assert.match(source, /private _spaceModel\(\): SpaceModel \| undefined/);
assert.match(source, /private _spaceModelById\([^)]*\): SpaceModel \| undefined/);
assert.doesNotMatch(source, /this\._spaceModel\(\s*[^)]/,
'explicit ids belong to the exact selector');
assert.doesNotMatch(source, /this\._spaceModel\(\)\s*!/,
'absence must not be hidden behind a non-null assertion');
assert.doesNotMatch(source, /this\._spaceModel\(\)\s*\./,
'every dereference is guarded or uses an intentional empty fallback');
});
test('authoritative empty-space cleanup aborts every space-bound transaction', () => {
const cleanup = methodBody('_syncEmptySpaceState');
for (const fragment of [
'releasePointerCapture', '_pointers.clear()', '_cancelModeTransition(false)',
"_mode = 'view'", '_clearGeometryGesture()', '_geometryHistory.clear()',
'_resumeDraftBySpace = {}', '_drag = null', '_vacFit = null',
'_markerDialog = null', '_saveConfigDebounced.cancel()', "_commitSpace('', true)",
]) assert.ok(cleanup.includes(fragment), fragment);
const willUpdate = methodBody('willUpdate');
assert.ok(willUpdate.indexOf('_syncEmptySpaceState()')
< willUpdate.indexOf('_captureRenderDeviceSnapshot()'),
'cleanup happens before a new render snapshot is captured');
});
test('stable space ids use exact lookup and abort before side effects', () => {
for (const name of [
'_livePos', '_vacPlanRoomAnchors', '_vacStartFit', '_labelMove', '_rlResizeMove',
]) assert.match(methodBody(name), /_spaceModelById\(/, name);
const saveMarker = methodBody('_saveMarker');
const exactAt = saveMarker.indexOf('_spaceModelById(explicitSpaceId)');
const guardAt = saveMarker.indexOf('if (!targetSpaceModel) return;');
const busyAt = saveMarker.indexOf('busy: true');
const migrateAt = saveMarker.indexOf("type: 'houseplan/files/migrate'");
const configMutationAt = saveMarker.indexOf('cfg.markers = markers');
assert.ok(exactAt >= 0 && exactAt < guardAt && guardAt < busyAt
&& busyAt < migrateAt && migrateAt < configMutationAt);
const savePos = methodBody('_savePos');
const positionExactAt = savePos.indexOf('_spaceModelById(d.space)');
const layoutMutationAt = savePos.indexOf('this._layout =');
const dirtyAt = savePos.indexOf('_dirtyPos.add');
const persistAt = savePos.indexOf('_persistLayout()');
assert.ok(positionExactAt >= 0 && positionExactAt < layoutMutationAt
&& layoutMutationAt < dirtyAt && dirtyAt < persistAt,
'stale position writes abort before layout, dirty and persistence side effects');
});
test('empty render keeps create/import affordances without spatial layers', () => {
const render = methodBody('render');
const emptyAt = render.indexOf('if (!model.length)');
const addAt = render.indexOf("_openSpaceDialog('create')");
const spatialAt = render.indexOf('const space = this._spaceModel()');
assert.ok(emptyAt >= 0 && emptyAt < addAt && addAt < spatialAt);
assert.match(render, /if \(!space\) return nothing;/);
});