Files
houseplan-card/demo/smoke_test_facade.mjs
T
Claudeandclaude[bot] fa319e37b3 test(harness): тестовый фасад window.__hpTest и гейт no-new-private-writes (#629)
- scripts/no-new-private-writes.mjs: смоки и demo/helpers/** не добавляют
  записей в приватное состояние карточки (присваивание, ++/--, delete по
  цепочке с сегментом _x; от this — нет) и вызовов _setMode/_openRoomEdit/
  _openMarkerDialog/_openSpaceDialog. Зачёт правки по полю, перенос блока —
  movedLinesByFile из no-new-any; исключение // private-ok: <причина>.
  --count — остаток на HEAD. Подключён в gate:small и в шаг frontend рядом с
  no-new-any, с той же базой.
- demo/helpers/hp-test.mjs: 10 операций через контрактные хуки и события
  фикстуры (setMode, setTool, switchSpace, openRoomEdit, openMarkerDialog,
  openSpaceDialog, setServerConfig, setLayout, input, close); ставится
  launch*() из demo/serve.mjs. В бандле фасада нет.
- demo/srv/demo.html: доставка houseplan_config_updated/_layout_updated,
  __pushServerConfig/__pushServerLayout; после доставки запись со старым
  expected_rev — conflict, как у настоящего сервера.
- HP_SMOKE_CHECKS=1 печатает имена проверок в finish().
- smoke_area_relocation, smoke_glow, smoke_grid_snap переведены на фасад без
  потери утверждений; новый smoke_test_facade доказывает каждую операцию.
- 7 мутантов, docs/TESTING.md (раздел + правило №6), PROCESS.md §2.7, AGENTS.md.

Issue: #629
User-Visible: no
2026-09-25 01:25:20 +00:00

124 lines
7.1 KiB
JavaScript

// #629: тестовый фасад харнесса window.__hpTest (demo/helpers/hp-test.mjs).
//
// Каждая операция доказывается наблюдаемым результатом настоящего пути:
// кнопка нажата — режим сменился в ha-card[data-hp-mode]; конфиг доставлен
// событием сервера — подпись комнаты на плане другая; ввод — по событию input
// на символ; закрытие — диалог отсоединился. Приватные поля карточки здесь
// только ЧИТАЮТСЯ (правило №6 docs/TESTING.md).
import { launch, check, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 1100, height: 820 }, 1);
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
const hp = window.__hpTest;
const sr = () => c.shadowRoot || c.renderRoot;
const mode = () => sr().querySelector('ha-card')?.getAttribute('data-hp-mode');
const errorOf = async (fn) => { try { await fn(); return null; } catch (error) { return String(error?.message || error); } };
// P2/AC9: бандл сам фасад не определяет — его ставит харнесс.
o.bundleDoesNotDefineFacade = hp?.preinstalled === false;
o.facadeIsFrozen = Object.isFrozen(hp);
// F/AC6: отсутствующий элемент — именованная ошибка, без отката на приватный метод.
const viewError = await errorOf(() => hp.openRoomEdit('r2'));
o.missingElementIsANamedError = !!viewError
&& viewError.includes('__hpTest.openRoomEdit')
&& viewError.includes('[data-hp="room-settings"][data-room="r2"]')
&& viewError.includes('режим view');
o.noFallbackOpenedADialog = !sr().querySelector('[data-hp="dialog"]');
// 1) setMode: настоящая вкладка режима, затем крестик редактора.
await hp.setMode('plan');
o.setModePlan = mode() === 'plan'
&& sr().querySelector('[data-hp="mode-tab"][data-mode="plan"]')?.classList.contains('active') === true;
// 2) setTool: кнопка в панели активного редактора.
const drawButton = await hp.setTool('draw');
o.setToolDraw = c._tool === 'draw' && drawButton?.getAttribute('aria-pressed') === 'true';
await hp.setTool('select');
o.setToolSelect = c._tool === 'select';
// 4) openRoomEdit + 9) input + 10) close через Escape.
const roomDialog = await hp.openRoomEdit('r2');
o.openRoomEdit = roomDialog?.getAttribute('data-kind') === 'room' && roomDialog.isConnected;
const nameField = roomDialog.querySelector('#room-name');
const events = [];
nameField.addEventListener('input', (event) => events.push(event.inputType));
const typed = 'Кухня 2';
await hp.input(nameField, typed);
o.inputTypesCharByChar = events.filter((type) => type === 'insertText').length === typed.length
&& events[0] === 'deleteContentBackward';
o.inputReachesTheDraft = c._nameSel === typed && nameField.value === typed;
const escaped = await hp.close(roomDialog, { via: 'escape' });
// черновик изменён: закрытие либо спрашивает подтверждение, либо закрывает
o.closeEscape = escaped.closed || !!escaped.confirm;
if (escaped.confirm) {
const discard = escaped.confirm.querySelector('[data-hp="dialog-confirm"]');
discard?.click();
await hp.settled();
}
o.roomDialogGone = !roomDialog.isConnected;
// 10) close через крестик окна и через «Отмена» содержимого.
const viaX = await hp.close(await hp.openRoomEdit('r2'), { via: 'x' });
o.closeX = viaX.closed === true && viaX.confirm === null;
const viaCancel = await hp.close(await hp.openRoomEdit('r2'), { via: 'cancel' });
o.closeCancel = viaCancel.closed === true && viaCancel.confirm === null;
// 7) setServerConfig: карточка сама перечитывает конфиг по событию сервера;
// подписи комнат видны в редакторе плана.
const revBefore = c._cfgRev;
const labelText = () => [...sr().querySelectorAll('[data-hp="room-label"]')].map((el) => el.textContent.trim()).join('|');
const rev = await hp.setServerConfig((cfg) => {
cfg.spaces.find((space) => space.id === 'f1').rooms.find((room) => room.id === 'r2').name = 'Kitchen 2';
});
o.setServerConfigAdoptsRevision = rev > revBefore && c._cfgRev >= rev;
o.setServerConfigRepaints = labelText().includes('Kitchen 2');
// 5) openMarkerDialog: «Добавить устройство» и клик по маркеру в режиме устройств.
await hp.setMode('devices');
o.setModeDevices = mode() === 'devices';
const addDialog = await hp.openMarkerDialog();
o.openMarkerDialogAdd = addDialog?.getAttribute('data-kind') === 'marker';
await hp.close(addDialog, { via: 'cancel' });
const lampDialog = await hp.openMarkerDialog('d_lamp');
o.openMarkerDialogDevice = lampDialog?.getAttribute('data-kind') === 'marker'
&& lampDialog.querySelector('#marker-name')?.value === 'Floor lamp';
o.closeMarker = (await hp.close(lampDialog, { via: 'x' })).closed === true;
// 6) openSpaceDialog: создание и настройки существующего пространства.
const createDialog = await hp.openSpaceDialog('create');
o.openSpaceDialogCreate = createDialog?.getAttribute('data-kind') === 'space';
await hp.close(createDialog, { via: 'escape' });
const editDialog = await hp.openSpaceDialog('edit', 'f1');
o.openSpaceDialogEdit = editDialog?.getAttribute('data-kind') === 'space' && editDialog !== createDialog;
await hp.close(editDialog, { via: 'escape' });
o.spaceDialogsClosed = !createDialog.isConnected && !editDialog.isConnected;
// 1) setMode('view') — крестиком на активной вкладке.
await hp.setMode('view');
o.setModeView = mode() === 'view'
&& !sr().querySelector('[data-hp="mode-tab"].active');
// 3) switchSpace — настоящая вкладка пространства.
await hp.switchSpace('garden');
o.switchSpace = c._space === 'garden'
&& sr().querySelector('[data-hp="space-tab"][data-id="garden"]')?.getAttribute('aria-current') === 'page';
await hp.switchSpace('f1');
// 8) setLayout: позиция маркера меняется событием раскладки.
const kettle = () => sr().querySelector('[data-hp="device"][data-id="d_kettle"]');
const leftBefore = Number.parseFloat(kettle()?.style.left || 'NaN');
const layoutRev = await hp.setLayout((layout) => { layout.d_kettle = { ...layout.d_kettle, x: 0.3, y: 0.3 }; });
const leftAfter = Number.parseFloat(kettle()?.style.left || 'NaN');
o.setLayoutAdoptsRevision = c._layoutRev >= layoutRev;
o.setLayoutMovesTheMarker = Number.isFinite(leftBefore) && Number.isFinite(leftAfter)
&& Math.abs(leftAfter - leftBefore) > 5;
return o;
});
for (const [name, value] of Object.entries(out)) check(name, value);
await finish(browser, out);