Files
houseplan-card/test/optimize-plans-dialog.test.mjs
T
Claudeandclaude[bot] 66ae952c2a refactor(editor): диалог «Оптимизировать планы» — свой модуль с узким портом (#642)
Первый вынос из монолита по образцу live-*/RadarSetupController.
src/optimize-plans-dialog.ts: OptimizePlansDialogState, чистые
preflightDiagnostics/preflightVersionsDiffer и класс OptimizePlansDialog
(open/preview/toggleLivePositions/run/copyDiagnostics/
reportPreflightFailure/render, clipboardFallback) за портом из 18 членов.
Модуль не знает HouseplanEditorHostPort; импортирует его только рантайм,
карточка берёт тип.

Инлайн-фолбэк буфера обмена — WeakMap по объекту диалога
(CODE-REVIEW-295-r1 M2), дедуп dev-лога — поле класса: из карточки и порта
ушли _preflightClipboardFallback и _reportedPreflightFingerprint, пять
делегатов и две стрелки-заглушки. Харнесс (13 смоков, wall-draw-click,
golden) зовёт card._editorRuntime.optimizePlans.* — меняется только адрес.

11 текстовых утверждений i18n.test.mjs о разметке и тосте диалога и гварды
8 мутантов переехали в юнит test/optimize-plans-dialog.test.mjs
(test-build); новый мутант optimize-dialog-imports-host-port.

monolith-baseline: delegates 159→154, portMembers 350→348,
hostRefs 4948→4869, portPrivates 96→94, harnessPrivates 107→101,
bundleBytes 2499647→2499182. Потолки ядер опущены на выигрыш;
потолок lazy editor gzip пересчитан (+555 Б gzip при −465 Б сырых).

Issue: #642
User-Visible: no
2026-09-24 08:00:58 +00:00

505 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// #642: диалог «Оптимизировать планы» — исполнением через test-build, а не
// текстом монолита. Сюда же переехали 11 утверждений `i18n.test.mjs` о
// разметке и тосте диалога (AC3) и гварды восьми мутантов реестра (AC4).
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import ts from 'typescript';
import {
OptimizePlansDialog, preflightDiagnostics, preflightVersionsDiffer,
} from '../test-build/optimize-plans-dialog.js';
import { formatLatticeShiftCm } from '../test-build/coordinate-canonicalization.js';
import { spacePhysicalGeometryFingerprint } from '../test-build/plan-geometry-preflight.js';
import { contentFingerprint } from '../test-build/visual-continuity.js';
const MODULE_SOURCE = readFileSync(new URL('../src/optimize-plans-dialog.ts', import.meta.url), 'utf8');
const BUILT_SOURCE = readFileSync(new URL('../test-build/optimize-plans-dialog.js', import.meta.url), 'utf8');
// ---------------------------------------------------------------- fixtures
const space = (id, title, x) => ({
id, title, view_box: [0, 0, 1, 1], cell_cm: 5,
rooms: [{ id: `${id}-room`, name: 'R', area: null, poly: [[0.1, 0.1], [x, 0.1], [x, 0.4], [0.1, 0.4]] }],
});
const report = (patch = {}) => ({
moved: 0, total: 0, rotated: 0, maxShift: 0, maxShiftCm: 0, maxSpace: '',
modelFrom: 10, modelTo: 10, migrated: 0, glowSpacesMigrated: 0, glowRoomsMigrated: 0,
canonicalized: 0, wallSegmentsMigrated: 0, roomDraftsMigrated: 0, roomDraftSegmentsMigrated: 0,
legacyZeroWallsMigrated: 0, wallsMerged: 0, spansMerged: 0, partitionsMerged: 0,
partitionsReconciled: 0, openingsRehosted: 0, wallsStraightened: 0, wallsStraightenSkipped: 0,
maxStraightenShiftCm: 0, maxStraightenSpace: '', coordsCanonicalized: 0,
latticeCoordinatesCanonicalized: 0, latticeCoordinatesFar: 0, latticeMaxShift: 0,
latticeMaxShiftCm: 0, latticeSpaces: [],
spaceRefsRemapped: 0, roomRefsRemapped: 0, positionsRemapped: 0, markersDetached: 0,
orphanRoomLabelsRemoved: 0, orphanDevicePositionsRemoved: 0, orphanGroupPositionsRemoved: 0,
liveMissingPositionsRemoved: 0, nestedRefsUnresolved: 0,
removedPositions: [], liveMissingPositions: [], unverifiedPositions: [],
...patch,
});
const okPreflight = (config) => ({
fingerprint: contentFingerprint(config), spaces: [], failures: [], ok: true,
});
const failedPreflight = (fingerprint = 'fp-1') => ({
fingerprint, spaces: [], ok: false,
failures: [
{ spaceId: 'a', displayName: 'Alpha', status: 'failed', reason: 'wall-exception', detail: 'Error' },
{ spaceId: 'b2', displayName: 'Second', status: 'failed', reason: 'floor-null' },
],
});
const dialogOf = (patch = {}) => {
const config = patch.config ?? { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} };
return {
report: report(), config, layout: {}, preflight: okPreflight(config),
cm: 0, where: '', changed: true, busy: false, removeLiveMissingPositions: false,
...patch,
};
};
/** Фейковый порт: ровно члены `OptimizePlansDialogPort`, каждый считает вызовы. */
function fakePort(overrides = {}) {
const calls = {
commit: [], checkGeometry: [], toasts: [], requestUpdate: 0, migration: [],
gesture: 0, reload: 0, referenceContext: [],
};
const state = {
dialog: null,
config: { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} },
integration: null,
};
const port = {
dialog: () => state.dialog,
setDialog: (next) => { state.dialog = next; },
t: (key, vars) => (vars ? `${key}${JSON.stringify(vars)}` : key),
hass: () => ({}),
config: () => state.config,
planReady: () => true,
layout: () => ({}),
integrationVersion: () => state.integration,
cardVersion: () => '9.9.9',
requestUpdate: () => { calls.requestUpdate += 1; },
showToast: (message) => { calls.toasts.push(message); },
errorText: (error) => String(error?.message ?? error),
checkGeometry: (config) => { calls.checkGeometry.push(config); return okPreflight(config); },
referenceContext: (removeLive) => { calls.referenceContext.push(removeLive); return { removeLiveMissingPositions: removeLive }; },
showMigrationBlocked: (error) => { calls.migration.push(error); },
clearGeometryGesture: () => { calls.gesture += 1; },
commit: async (config, layout) => { calls.commit.push([config, layout]); },
reloadAfterConflict: async () => { calls.reload += 1; },
...overrides,
};
return { port, calls, state, dialog: new OptimizePlansDialog(port) };
}
// ------------------------------------------------------ TemplateResult helpers
const isTemplate = (value) => !!value && typeof value === 'object'
&& Array.isArray(value.strings) && Array.isArray(value.values);
/** Сплющенная разметка: строки шаблона и значения, `nothing` — пусто. */
function flatten(value) {
if (value == null || value === false || typeof value === 'symbol' || typeof value === 'function') return '';
if (Array.isArray(value)) return value.map(flatten).join('');
if (isTemplate(value)) {
return value.strings.reduce((out, part, index) => (
out + part + (index < value.values.length ? flatten(value.values[index]) : '')
), '');
}
return String(value);
}
/** Обработчики `@event=${fn}` вместе с текстом перед ними — чтобы найти кнопку. */
function handlers(value, out = []) {
if (Array.isArray(value)) { value.forEach((item) => handlers(item, out)); return out; }
if (!isTemplate(value)) return out;
value.values.forEach((item, index) => {
const match = /@([\w-]+)=$/.exec(value.strings[index]);
// Текст от начала тега до обработчика: по нему кнопка и находится.
const before = value.strings.slice(0, index + 1).join('');
const tag = before.slice(before.lastIndexOf('<'));
if (match && typeof item === 'function') out.push({ event: match[1], before: tag, fn: item });
else handlers(item, out);
});
return out;
}
const handlerFor = (template, event, marker) => handlers(template)
.find((item) => item.event === event && (!marker || item.before.includes(marker)))?.fn;
/**
* `navigator` в Node — геттер без сеттера, `t.mock.property` его не
* восстанавливает. Подменяем дескриптор и возвращаем исходный сами.
*/
function withClipboard(t, clipboard) {
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
Object.defineProperty(globalThis, 'navigator', { value: { clipboard }, configurable: true });
t.after(() => {
if (original) Object.defineProperty(globalThis, 'navigator', original);
else delete globalThis.navigator;
});
}
// ------------------------------------------------------------- pure helpers
test('#642 preflightDiagnostics: candidate hash, reason, detail, version, fixed clock', () => {
const candidate = { spaces: [space('a', 'Alpha', 0.6)] };
const now = new Date('2026-09-24T10:00:00.000Z');
const block = preflightDiagnostics(failedPreflight(), candidate, { cardVersion: '1.2.3', now });
assert.equal(block.kind, 'houseplan-optimize-preflight');
assert.equal(block.origin, 'runtime');
assert.equal(block.cardVersion, '1.2.3');
assert.equal(block.checkedAt, '2026-09-24T10:00:00.000Z');
assert.equal(block.preflightFingerprint, 'fp-1');
assert.deepEqual(block.failures.map((failure) => [failure.spaceId, failure.reason, failure.detail]), [
['a', 'wall-exception', 'Error'], ['b2', 'floor-null', null],
]);
assert.equal(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(candidate.spaces[0]));
assert.equal(block.failures[1].spaceGeometryFingerprint, null, 'a space missing from the candidate has no hash');
// Смок передаёт кандидата не всегда; блок всё равно собирается.
assert.equal(preflightDiagnostics(failedPreflight(), undefined, { cardVersion: 'x', now }).failures[0]
.spaceGeometryFingerprint, null);
});
test('#642 preflightVersionsDiffer: only a known, different integration version advises an update', () => {
assert.equal(preflightVersionsDiffer(null, '1.0.0'), false);
assert.equal(preflightVersionsDiffer('', '1.0.0'), false);
assert.equal(preflightVersionsDiffer('1.0.0', '1.0.0'), false);
assert.equal(preflightVersionsDiffer('0.0.1-other', '1.0.0'), true);
});
// ------------------------------------------------------------------ preview
test('#642 preview: a failed migration reports once and opens nothing', () => {
const boom = new Error('wall model');
const { dialog, calls, state } = fakePort({ referenceContext: () => { throw boom; } });
dialog.preview(false);
assert.deepEqual(calls.migration, [boom]);
assert.equal(state.dialog, null);
});
test('#642 preview: nothing without a plan or a saved config', () => {
const noPlan = fakePort({ planReady: () => false });
noPlan.dialog.open();
assert.equal(noPlan.state.dialog, null);
const noConfig = fakePort();
noConfig.state.config = null;
noConfig.dialog.open();
assert.equal(noConfig.state.dialog, null);
assert.deepEqual(noConfig.calls.referenceContext, []);
});
test('#642 preview: cm is rounded UP, the space is named only when there are several', () => {
const two = fakePort();
two.state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.50337)], markers: [], settings: {} };
two.dialog.preview(false);
const d = two.state.dialog;
assert.equal(d.changed, true);
assert.equal(d.report.maxSpace, 'b');
assert.equal(d.cm, Math.ceil(d.report.maxShiftCm * 10) / 10);
assert.ok(d.cm >= d.report.maxShiftCm, 'the promise is never smaller than the deed');
assert.equal(d.where, 'Beta');
assert.equal(d.busy, false);
assert.equal(d.removeLiveMissingPositions, false);
assert.deepEqual(two.calls.checkGeometry, [d.config], 'a changed candidate is preflighted');
assert.deepEqual(two.calls.referenceContext, [false]);
const one = fakePort();
one.state.config = { spaces: [space('b', 'Beta', 0.50337)], markers: [], settings: {} };
one.dialog.preview(false);
assert.equal(one.state.dialog.where, '');
});
test('#642 toggleLivePositions re-previews with the flag flipped, only when live positions exist', () => {
const { dialog, state, calls } = fakePort();
state.dialog = dialogOf();
dialog.toggleLivePositions();
assert.deepEqual(calls.referenceContext, [], 'no live-missing positions — nothing to toggle');
state.dialog = dialogOf({ report: report({ liveMissingPositions: [{ id: 'm', name: 'Lamp', kind: 'device', spaceId: 'a' }] }) });
dialog.toggleLivePositions();
assert.deepEqual(calls.referenceContext, [true]);
assert.equal(state.dialog.removeLiveMissingPositions, true);
});
// --------------------------------------------------------------------- run
test('#642 open: preview never writes, and cancel closes without a write', async () => {
const { dialog, state, calls } = fakePort();
state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.50337)], markers: [], settings: {} };
dialog.open();
await Promise.resolve();
assert.ok(state.dialog?.changed && state.dialog.preflight?.ok, 'a writable preview is open');
assert.deepEqual(calls.commit, [], 'opening the preview must not persist anything');
handlerFor(dialog.render(), 'click', 'data-hp="dialog-cancel"')();
assert.equal(state.dialog, null);
assert.deepEqual(calls.commit, []);
});
test('#642 run: red preflight is a hard write barrier', async () => {
const { dialog, state, calls } = fakePort();
state.dialog = dialogOf({ preflight: failedPreflight() });
await dialog.run();
assert.deepEqual(calls.commit, []);
assert.equal(calls.gesture, 0);
state.dialog = dialogOf({ changed: false });
await dialog.run();
state.dialog = dialogOf({ busy: true });
await dialog.run();
assert.deepEqual(calls.commit, [], 'unchanged or busy dialogs do not write either');
});
test('#642 run: a stale fingerprint is rechecked, and a red recheck stops the write', async () => {
const red = failedPreflight('fresh');
const { dialog, state, calls } = fakePort({
checkGeometry: (config) => { calls.checkGeometry.push(config); return red; },
});
const warns = [];
const original = console.warn;
console.warn = (...args) => { warns.push(args); };
try {
state.dialog = dialogOf({ preflight: { ...okPreflight({}), fingerprint: 'stale' } });
const candidate = state.dialog.config;
await dialog.run();
assert.deepEqual(calls.checkGeometry, [candidate]);
assert.equal(state.dialog.preflight, red, 'the dialog now shows the fresh refusal');
assert.deepEqual(calls.commit, []);
assert.equal(warns.length, 1, 'the fresh refusal is dev-logged');
} finally {
console.warn = original;
}
});
test('#642 run: success writes the exact pair, closes, and toasts counts including lattice cleanup', async () => {
const seen = [];
const { dialog, state, calls } = fakePort({
commit: async (config, layout) => { seen.push(state.dialog?.busy); calls.commit.push([config, layout]); },
});
state.dialog = dialogOf({ report: report({ moved: 2, latticeCoordinatesCanonicalized: 7, orphanRoomLabelsRemoved: 1 }) });
const { config, layout } = state.dialog;
await dialog.run();
assert.equal(calls.gesture, 1);
assert.deepEqual(seen, [true], 'busy while the write is in flight');
assert.equal(calls.commit.length, 1);
assert.equal(calls.commit[0][0], config);
assert.equal(calls.commit[0][1], layout);
assert.deepEqual(calls.checkGeometry, [], 'an unchanged fingerprint is not rechecked');
assert.equal(state.dialog, null);
assert.deepEqual(calls.toasts, ['gs.align_done{"n":"2","m":"7","r":"1"}']);
});
test('#642 run: conflict reloads both stores; outdated client gets its own toast', async () => {
const conflict = fakePort({ commit: async () => { throw Object.assign(new Error('c'), { code: 'conflict' }); } });
conflict.state.dialog = dialogOf();
await conflict.dialog.run();
assert.equal(conflict.calls.reload, 1);
assert.equal(conflict.state.dialog.busy, false, 'the dialog stays open and usable');
assert.deepEqual(conflict.calls.toasts, ['toast.error{"err":"c"}']);
const outdated = fakePort({ commit: async () => { throw { code: 'wall_model_client_outdated' }; } });
outdated.state.dialog = dialogOf();
await outdated.dialog.run();
assert.equal(outdated.calls.reload, 0);
assert.deepEqual(outdated.calls.toasts, ['toast.wall_model_client_outdated']);
});
// -------------------------------------------------------------- diagnostics
test('#642 copyDiagnostics hashes the candidate, not the saved config', async (t) => {
const { dialog, state, calls } = fakePort();
const candidate = { spaces: [space('a', 'Alpha', 0.6)], markers: [], settings: {} };
state.config = { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} };
state.dialog = dialogOf({ config: candidate, preflight: failedPreflight() });
let copied = null;
withClipboard(t, { writeText: async (text) => { copied = text; } });
await dialog.copyDiagnostics();
const block = JSON.parse(copied);
assert.equal(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(candidate.spaces[0]));
assert.notEqual(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(state.config.spaces[0]));
assert.equal(block.cardVersion, '9.9.9');
assert.deepEqual(calls.toasts, ['gs.preflight_copied']);
assert.equal(dialog.clipboardFallback, null);
});
test('#642 copyDiagnostics: the inline fallback belongs to one dialog showing', async (t) => {
const { dialog, state, calls } = fakePort();
withClipboard(t, { writeText: async () => { throw new Error('denied'); } });
state.dialog = dialogOf({ preflight: failedPreflight() });
await dialog.copyDiagnostics();
assert.match(dialog.clipboardFallback, /houseplan-optimize-preflight/);
assert.equal(calls.requestUpdate, 1, 'the fallback is not @state — the module asks for the render');
assert.match(flatten(dialog.render()), /<pre[^>]*>\{\n {2}"kind": "houseplan-optimize-preflight"/);
// Закрытие настоящим обработчиком hp-close.
handlerFor(dialog.render(), 'hp-close')();
assert.equal(state.dialog, null);
assert.equal(dialog.clipboardFallback, null);
// Новый отказ — новый объект диалога — без JSON прошлого отказа.
state.dialog = dialogOf({ preflight: failedPreflight('fp-2') });
assert.equal(dialog.clipboardFallback, null);
assert.doesNotMatch(flatten(dialog.render()), /<pre/);
});
test('#642 copyDiagnostics does nothing for a green or missing preflight', async (t) => {
const { dialog, state, calls } = fakePort();
let writes = 0;
withClipboard(t, { writeText: async () => { writes += 1; } });
await dialog.copyDiagnostics();
state.dialog = dialogOf();
await dialog.copyDiagnostics();
assert.equal(writes, 0);
assert.deepEqual(calls.toasts, []);
});
test('#642 reportPreflightFailure logs one structured record per distinct fingerprint', () => {
const { dialog } = fakePort();
const warns = [];
const original = console.warn;
console.warn = (...args) => { warns.push(args); };
try {
dialog.reportPreflightFailure(failedPreflight('fp-1'), null);
dialog.reportPreflightFailure(failedPreflight('fp-1'), null);
dialog.reportPreflightFailure({ ...okPreflight({}), fingerprint: 'fp-ok' }, null);
assert.equal(warns.length, 1, 'dedup: the same fingerprint logs once, a green one never');
assert.equal(warns[0][0], '[houseplan] optimize preflight failed');
assert.equal(warns[0][1].kind, 'houseplan-optimize-preflight');
assert.equal(warns[0][1].failures[0].reason, 'wall-exception');
dialog.reportPreflightFailure(failedPreflight('fp-2'), null);
assert.equal(warns.length, 2);
} finally {
console.warn = original;
}
});
// ------------------------------------------------------------------ render
test('#642 render: changes — every counter, lattice lines and orphan report (i18n.test AC3)', () => {
const { dialog, state } = fakePort();
state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.5)], markers: [], settings: {} };
const r = report({
moved: 2, total: 9, coordsCanonicalized: 3, partitionsMerged: 5, migrated: 1, canonicalized: 4,
wallsMerged: 6, spansMerged: 8, partitionsReconciled: 2, openingsRehosted: 1,
legacyZeroWallsMigrated: 1, latticeCoordinatesCanonicalized: 4, latticeMaxShiftCm: 0.0123,
latticeSpaces: [{ space: 'Alpha', canonicalized: 4, far: 1 }],
orphanRoomLabelsRemoved: 1, orphanDevicePositionsRemoved: 2, orphanGroupPositionsRemoved: 0,
removedPositions: [{ id: 'dev1', kind: 'device', spaceId: 'gone' }],
wallsStraightened: 1, maxStraightenShiftCm: 1.23, maxStraightenSpace: 'b',
});
state.dialog = dialogOf({ report: r, cm: 1.5, where: 'Beta' });
const text = flatten(dialog.render());
assert.match(text, /gs\.align_count\{"n":"2","total":"9","cm":"1.5"\}/);
assert.match(text, /gs\.align_where\{"s":"Beta"\}/);
assert.match(text, /gs\.zero_walls_migrated\{"n":"1"\}/);
// p — шум координат, i — независимые стены (#229): счётчики показываются, а не копятся молча.
assert.match(text, /gs\.optimize_changes\{"m":"1","c":"4","p":"3","w":"6","s":"8","i":"5"\}/);
assert.match(text, /gs\.optimize_coincident_partitions\{"n":"2"\}/);
assert.match(text, /gs\.optimize_openings_rehosted\{"n":"1"\}/);
assert.ok(text.includes(`gs.optimize_lattice_summary{"n":"4","cm":"${formatLatticeShiftCm(0.0123)}"}`));
assert.match(text, /gs\.optimize_lattice_space\{"space":"Alpha","n":"4","far":"1"\}/);
assert.match(text, /gs\.optimize_walls_straightened\{"n":"1","cm":"1.3"\}/);
assert.match(text, /gs\.optimize_walls_straightened_where\{"s":"Beta"\}/);
assert.match(text, /gs\.optimize_orphans_removed\{"total":"3","rooms":"1","devices":"2","groups":"0"\}/);
assert.match(text, /<details class="optimize-details">/);
assert.match(text, /gs\.align_warn/);
assert.doesNotMatch(text, /gs\.optimize_reference_warning/);
assert.match(text, /data-hp="dialog-confirm"/);
});
test('#642 render: failed preflight names each reason and offers no Apply', () => {
const { dialog, state } = fakePort();
state.dialog = dialogOf({ preflight: failedPreflight() });
const text = flatten(dialog.render());
assert.match(text, /gs\.align_preflight_failed\{"spaces":"Alpha, Second","more":""\}/);
assert.match(text, /Alpha: gs\.preflight_reason_wall-exception/);
assert.match(text, /Second: gs\.preflight_reason_floor-null/);
assert.match(text, /gs\.preflight_copy/);
assert.doesNotMatch(text, /data-hp="dialog-confirm"/, 'the failure state is not a dismissible warning');
assert.doesNotMatch(text, /gs\.preflight_update_hint/);
state.integration = '0.0.1-other';
assert.match(flatten(dialog.render()), /gs\.preflight_update_hint/);
});
test('#642 render: nothing to do — plain «none» or «no automatic changes», never Apply', () => {
const { dialog, state } = fakePort();
state.dialog = dialogOf({ changed: false, preflight: null });
let text = flatten(dialog.render());
assert.match(text, /gs\.align_none/);
assert.doesNotMatch(text, /data-hp="dialog-confirm"/);
state.dialog = dialogOf({ changed: false, preflight: null, report: report({ nestedRefsUnresolved: 2 }) });
text = flatten(dialog.render());
assert.match(text, /gs\.optimize_no_automatic_changes/);
assert.match(text, /gs\.optimize_vacuum_warning\{"n":"2"\}/);
});
test('#642 render: live positions — names, toggle state and the selected hint', () => {
const { dialog, state, calls } = fakePort();
const live = ['A', 'B', 'C', 'D'].map((name, index) => ({ id: `m${index}`, name, kind: 'device', spaceId: 'a' }));
state.dialog = dialogOf({ report: report({ liveMissingPositions: live }) });
let text = flatten(dialog.render());
assert.ok(text.includes('gs.optimize_live_positions{"n":"4","names":"gs.optimize_live_names{\\"names\\":\\"A, B, C\\",\\"more\\":\\"gs.optimize_reference_more{\\\\\\"n\\\\\\":\\\\\\"1\\\\\\"}\\"}"}'));
assert.match(text, /aria-pressed=false/);
assert.match(text, /gs\.optimize_live_remove/);
assert.doesNotMatch(text, /gs\.optimize_live_selected/);
state.dialog = { ...state.dialog, removeLiveMissingPositions: true };
text = flatten(dialog.render());
assert.match(text, /gs\.optimize_live_positions_remove/);
assert.match(text, /aria-pressed=true/);
assert.match(text, /gs\.optimize_live_keep/);
assert.match(text, /gs\.optimize_live_selected/);
handlerFor(dialog.render(), 'click', 'optimize-cleanup')();
assert.deepEqual(calls.referenceContext, [false], 'the toggle re-previews with the flag flipped back');
});
test('#642 render: details list at most ten items, statuses by kind, and the remainder', () => {
const { dialog, state } = fakePort();
const removed = Array.from({ length: 8 }, (_, index) => ({ id: `r${index}`, kind: 'room_label', spaceId: 'x' }));
const unverified = Array.from({ length: 4 }, (_, index) => ({
id: `u${index}`, kind: index ? 'group' : 'mystery', spaceId: 'y', reason: 'registry_unavailable',
}));
state.dialog = dialogOf({ report: report({ removedPositions: removed, unverifiedPositions: unverified }) });
const text = flatten(dialog.render());
assert.equal(text.split('<li>').length - 1, 10);
assert.match(text, /gs\.optimize_detail_item\{"status":"gs\.optimize_detail_removed","kind":"gs\.optimize_detail_room_label","id":"r0","space":"x"\}/);
assert.match(text, /gs\.optimize_detail_item\{"status":"gs\.optimize_detail_unverified","kind":"gs\.optimize_detail_unknown","id":"u0","space":"y"\}/);
assert.match(text, /gs\.optimize_details_more\{"n":"2"\}/);
assert.match(text, /gs\.optimize_unverified\{"n":"4"\}/);
assert.match(text, /gs\.optimize_registry_limited/);
});
// ------------------------------------------------------------ AC2: the port
const parsedModule = ts.createSourceFile('optimize-plans-dialog.ts', MODULE_SOURCE, ts.ScriptTarget.Latest, true);
const portMembers = () => {
const port = parsedModule.statements.find((statement) => ts.isInterfaceDeclaration(statement)
&& statement.name.text === 'OptimizePlansDialogPort');
assert.ok(port, 'the module declares its own port');
return port.members.map((member) => member.name.getText(parsedModule)).sort();
};
test('#642 AC2: the dialog port is narrow and the module never reaches the editor host', () => {
// Импорты — и типовые тоже: `import type` стирается в сборке, поэтому
// разбирается исходник модуля, а собранный файл проверяется отдельно.
const specifiers = parsedModule.statements
.filter((statement) => ts.isImportDeclaration(statement))
.map((statement) => statement.moduleSpecifier.text);
const host = /\/houseplan-(?:card|editor-runtime)$/;
assert.deepEqual(specifiers.filter((specifier) => host.test(specifier)), []);
const identifiers = new Set();
const walk = (node) => {
if (ts.isIdentifier(node)) identifiers.add(node.text);
ts.forEachChild(node, walk);
};
walk(parsedModule);
assert.equal(identifiers.has('HouseplanEditorHostPort'), false, 'the wide editor port is not referenced');
assert.equal(identifiers.has('host'), false, 'no `host` field or parameter — only the narrow port');
assert.doesNotMatch(BUILT_SOURCE, /houseplan-(?:card|editor-runtime)\.js/);
const members = portMembers();
assert.ok(members.length <= 20, `port has ${members.length} members`);
const { port, dialog } = fakePort();
assert.ok(Object.keys(port).length <= 20);
assert.deepEqual(Object.keys(port).sort(), members, 'the fake port is exactly the declared port');
assert.equal('host' in dialog, false);
});