Files
houseplan-card/demo/smoke_resize_outer_reconciliation.mjs
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

126 lines
5.7 KiB
JavaScript

// #281: an exact outer-wall partition blocks Resize, explicit Optimize safely
// rehosts its windows, and the production pointer path then moves two rooms.
import { readFileSync } from 'node:fs';
import { launch, checkAll, finish } from './serve.mjs';
const source = JSON.parse(readFileSync(
new URL('../test/fixtures/281-resize-outer-partitions.json', import.meta.url),
'utf8',
));
const clone = (value) => JSON.parse(JSON.stringify(value));
const { page, browser } = await launch({ width: 900, height: 820 });
const out = await page.evaluate(async (fixture) => {
const result = {};
const card = window.__card;
const copy = (value) => JSON.parse(JSON.stringify(value));
const wallHostResolves = (space, opening) => opening.host?.kind === 'wall'
&& (space.wall_segments || []).some((segment) => segment.id === opening.host.id);
let serverConfig = copy(fixture);
let serverLayout = {};
const sent = [];
const baseCall = card.hass.callWS.bind(card.hass);
card.hass = {
...card.hass,
callWS: async (message) => {
if (message.type === 'houseplan/plan/optimize') {
sent.push(message.type);
serverConfig = copy(message.config);
serverLayout = copy(message.layout);
return { ok: true, config_rev: 2, layout_rev: 2, can_undo: true };
}
if (message.type === 'houseplan/config/get')
return { config: copy(serverConfig), rev: 2, can_write: true, can_optimize_undo: true };
if (message.type === 'houseplan/layout/get')
return { layout: copy(serverLayout), rev: 2 };
return baseCall(message);
},
};
const resetResize = async () => {
card._setMode('plan');
card._tool = 'resize';
card._resize.reset();
card._modelCache = null;
card._frame = null;
card.requestUpdate();
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(resolve));
};
const targetHandles = () => [...card.renderRoot.querySelectorAll('.rszhandle')]
.filter((handle) => Math.abs(Number(handle.getAttribute('cx')) - 500) < 1
&& Math.abs(Number(handle.getAttribute('cy')) - 500) < 1);
const screenPoint = (x, y) => {
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const svg = stage.querySelector('svg');
const [vx, vy, vw, vh] = svg.getAttribute('viewBox').split(' ').map(Number);
return [rect.left + ((x - vx) / vw) * rect.width,
rect.top + ((y - vy) / vh) * rect.height];
};
const dispatch = (target, type, x, y, pointerId = 281) => target.dispatchEvent(
new PointerEvent(type, {
bubbles: true, cancelable: true, pointerId,
clientX: x, clientY: y, pointerType: 'mouse',
buttons: type === 'pointerup' ? 0 : 1,
}),
);
card._serverCfg = copy(fixture);
card._layout = {};
card._space = 'resize-outer';
await resetResize();
const beforeHandles = targetHandles();
result.beforeIsExplainedDisabled = beforeHandles.length === 2
&& beforeHandles.every((handle) => handle.getAttribute('aria-disabled') === 'true'
&& /independent wall|partition|перегород/i.test(handle.getAttribute('aria-label') || ''));
const [beforeX, beforeY] = screenPoint(500, 500);
dispatch(beforeHandles[0], 'pointerdown', beforeX, beforeY);
result.disabledCapturesNothing = !card._resize.dragging
&& card._geometryHistory.size === 0;
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
const previewSpace = card._alignDialog?.config?.spaces?.[0];
result.previewProvesOuterRewrite = card._alignDialog?.changed === true
&& card._alignDialog?.preflight?.ok === true
&& card._alignDialog?.report?.partitionsReconciled === 3
&& card._alignDialog?.report?.openingsRehosted === 2
&& card._alignDialog?.config?.model_version === 10
&& previewSpace?.partitions == null
&& previewSpace?.openings?.every((opening) => wallHostResolves(previewSpace, opening));
await card._editorRuntime.optimizePlans.run();
await card.updateComplete;
result.optimizeUsesOneWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1;
const optimized = card._serverCfg.spaces[0];
result.optimizePreservesOpeningFields = optimized.partitions == null
&& optimized.openings.every((opening) => wallHostResolves(optimized, opening))
&& optimized.openings[0].cover === 'cover.left'
&& optimized.openings[0].future_field?.keep === 'left'
&& optimized.openings[1].contact === 'binary_sensor.right';
await resetResize();
const afterHandles = targetHandles();
result.afterIsEnabled = afterHandles.length === 2
&& afterHandles.every((handle) => handle.getAttribute('aria-disabled') === 'false');
const [startX, startY] = screenPoint(500, 500);
const [moveX] = screenPoint(550, 500);
dispatch(afterHandles[0], 'pointerdown', startX, startY);
result.dragStarts = card._resize.dragging;
dispatch(afterHandles[0], 'pointermove', moveX, startY);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
dispatch(afterHandles[0], 'pointerup', moveX, startY);
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const left = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'left');
const right = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'right');
result.gestureMovesExactlySharedEndpoints = Math.abs(left.poly[1][0] - 0.55) < 0.006
&& Math.abs(left.poly[2][0] - 0.55) < 0.006
&& Math.abs(right.poly[0][0] - 0.55) < 0.006
&& Math.abs(right.poly[3][0] - 0.55) < 0.006
&& card._serverCfg.spaces[0].rooms.length === 2
&& card._geometryHistory.size === 1;
return result;
}, clone(source));
await finish(browser, checkAll(out));