Files
houseplan-card/test/form-kit.test.mjs
T
Claude 30da00403d feat(form-kit): набор контролов по референсу и оболочка диалогов настроек
Серия 0 из пяти по #600. Ничего из четырёх диалогов ещё не переложено — эта
серия даёт им инструменты и оболочку.

Набор (`form-kit.ts` + `form-kit.styles.ts`), числа из §3.1 SPEC.md референса,
цвета через переменные темы HA:
- полная строка настройки `toggleRow` — сетка 28/1fr/44: иконка, название +
  «?», подпись, тумблер справа; тумблер — нативный чекбокс, нарисованный CSS,
  как у редактора панели: один вид в HA, на стенде и в golden, и всё тот же
  `input[type=checkbox]` для смоков; компактный вариант для однотипных списков;
- `field`, `fieldGrid` (две колонки и 1fr/175), `subsection`, `unitInput`,
  `rangeLine`, `rangeEnds`, `radioRow`, `choiceCards`;
- `valueTiles` (4 колонки), плитки цвета в CSS; `colorField` — компактная
  плашка вокруг существующего `hp-color-opacity` (Q5): свотч открывает ту же
  панель, рядом hex, число прозрачности и Reset; нативного пикера нет;
- `callout` (К5: сообщения о состоянии остаются на виду), `compass` 44 px,
  `footerStatus`, `chip`, `tintBlock`, `textLink`, `divider`;
- `segmented` получил образцы линии, иконки и `disabled`; вид — как в
  референсе: контейнер на канве, активный сегмент на тинте.

Оболочка (`hp-dialog.ts`, атрибут `form-shell` у четырёх диалогов настроек):
ширина 560 через `--hp-dialog-wide-width`, которую `:host([wide])` уже читал;
высота min(940px, 100vh − 48); единственный скроллер — `.content`, тело формы
высотой не владеет (К6, дефект 7 из issue); «✕» 44 px; бейдж имени пространства
в заголовке (`badge`). Generic `wide` и остальные диалоги не тронуты (Q7).

`hp-dialog[form-shell] .body` — без max-height и overflow, канва 16 px.

Фикстуры листа диалогов `test/fixtures/form-kit-card-dialog*.css` пересняты
намеренно; лист панели совпал с замороженным побайтово (К9, AC11).

`editor.loading` → «Загрузка…» в четырёх локалях (дефект 9); aria-текст прежний.

Потолок ленивого редакторского графа 222 900 → 225 900 с доводом в
`bundle-budget.mjs`: рост — CSS-строки набора, не код; значение промежуточное,
серии 1–4 выкидывают старые правила, итог замеряется в конце.

Гейты: typecheck, build, bundle:sync, `npm test` (2791 pass; красными были
только потолок — поднят — и неотслеженные файлы манифеста — добавлены),
`smoke_settings_dialog_cards`, `smoke_room_settings`.

Issue: #600
User-Visible: no
2026-09-20 09:39:13 +03:00

157 lines
10 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.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
CARD_DIALOG_FORM_KIT, SUMMARY_PANEL_FORM_KIT, formKitCss,
} from '../test-build/styles/form-kit.styles.js';
import { summaryPanelEditorCss } from '../test-build/summary-panel-editor-style.js';
/**
* #594. Набор контролов поднят из редактора боковой панели, и главный вопрос к
* нему один: общий ли он на самом деле. Ответ обязан быть машинным — с
* параметрами панели генератор должен выдавать её нынешние правила ДОСЛОВНО.
*
* #597 довёл это до конца: панель больше не описывает эти правила у себя, она
* подставляет фрагменты набора. Поэтому доказательство переехало с «фрагмент
* есть в исходнике панели» на «СОБРАННЫЙ лист панели совпадает с замороженным
* побайтово» — оно сильнее и ловит в том числе перестановку правил.
*/
const FROZEN_PANEL_CSS = readFileSync(
new URL('./fixtures/summary-panel-editor.css', import.meta.url), 'utf8',
);
/**
* #597 M2 ревью ТЗ. К3 обещает, что разрез генератора на фрагменты не меняет
* ни байта в листе диалогов, а сослаться было не на что: существующие тесты
* набора проверяют подстроки и счётчики, но не полный текст. Лишний пробел на
* стыке фрагментов прошёл бы незамеченным — в CSS он безвреден, но обещание
* «байт в байт» без свидетеля остаётся обещанием.
*/
const FROZEN_CARD_KIT_CSS = readFileSync(
new URL('./fixtures/form-kit-card-dialog.css', import.meta.url), 'utf8',
);
const FROZEN_CARD_KIT_WITH_SWITCH_CSS = readFileSync(
new URL('./fixtures/form-kit-card-dialog-with-switch.css', import.meta.url), 'utf8',
);
const PANEL_FRAGMENTS = {
'карточка-группа': ` .summary-general,
.summary-blocks-card {
min-width: 0;
padding: 14px;
border: 1px solid var(--summary-editor-line);
border-radius: 11px;
background: var(--summary-editor-surface);
}`,
'обводка фокуса': ` .summary-editor input:focus-visible,
.summary-editor select:focus-visible,
.summary-editor button:focus-visible,
.summary-editor-footer button:focus-visible {
outline: 2px solid var(--summary-editor-accent);
outline-offset: 2px;
}`,
'выключенное состояние': ` .summary-editor button:disabled,
.summary-editor input:disabled,
.summary-editor select:disabled { opacity: .4; cursor: default; }`,
'ряд-переключатель': ` .summary-editor .summary-switch {
display: flex;
min-width: 0;
min-height: 54px;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0;
color: var(--primary-text-color);
cursor: pointer;
}`,
'подпись ряда': ` .summary-switch-caption { display: grid; min-width: 0; gap: 3px; }
.summary-switch-caption strong { font-size: .875rem; font-weight: 600; overflow-wrap: anywhere; }
.summary-switch-caption small { font-size: .8125rem; color: var(--secondary-text-color); overflow-wrap: anywhere; }`,
};
test('#594 AC11 генератор воспроизводит правила панели дословно', () => {
const generated = formKitCss(SUMMARY_PANEL_FORM_KIT);
for (const [name, fragment] of Object.entries(PANEL_FRAGMENTS)) {
assert.ok(FROZEN_PANEL_CSS.includes(fragment),
`${name}: фикстура разошлась с замороженным листом панели`);
assert.ok(generated.includes(fragment),
`${name}: генератор с параметрами панели больше не выдаёт её правило дословно`);
}
});
/**
* #597. Главное доказательство шага и единственное, которое ловит перестановку
* правил: у настроек панели нет ни одного golden-кадра (в матрице нет сцены
* `dialog: 'summary'`), поэтому «панель не изменилась» нельзя показать
* пикселями. Сравнение собранного листа с замороженным — замена эталону.
*
* Фикстура `test/fixtures/summary-panel-editor.css` снята с `origin/dev` до
* правки. Она живёт ровно до того шага эпика #591, который законно меняет вид
* панели: там её обновляют вместе с кадрами и объясняют расхождение.
*/
test('#597 собранный лист панели совпадает с замороженным побайтово', () => {
assert.equal(summaryPanelEditorCss.length, FROZEN_PANEL_CSS.length,
'длина листа панели изменилась — значит изменился и он сам');
assert.equal(summaryPanelEditorCss, FROZEN_PANEL_CSS);
// И фрагменты действительно пришли из набора, а не остались литералами:
// иначе тест выше сравнивал бы панель сама с собой.
const source = readFileSync(
new URL('../src/summary-panel-editor-style.ts', import.meta.url), 'utf8',
);
for (const fn of ['formKitCardsCss', 'formKitFocusCss', 'formKitDisabledCss',
'formKitSwitchRowCss', 'formKitSwitchCaptionCss']) {
assert.ok(source.includes(`\${${fn}(SUMMARY_PANEL_FORM_KIT)}`),
`${fn}: панель не подставляет фрагмент набора`);
}
// #597 M1 ревью ТЗ: проверять надо отсутствие ВСЕХ пяти фрагментов, а не одной
// характерной подстроки. `min-height: 54px` встречается только в ряду-
// переключателе, поэтому литерал, оставленный «на всякий случай» для любого из
// четырёх остальных, прошёл бы и это правило, и побайтовое сравнение выше:
// на собранный вывод лишний литерал не влияет, он просто вторая копия.
for (const [name, fragment] of Object.entries(PANEL_FRAGMENTS)) {
assert.ok(!source.includes(fragment),
`${name}: правило осталось литералом в листе панели — копия не устранена`);
}
});
/**
* #597 M2. Разрез `sharedCss` на пять функций обязан быть механическим: лист
* диалогов карточки собирается из тех же фрагментов и не имеет права измениться
* ни на байт. Фикстуры сняты с `origin/dev` до правки.
*
* #600 расширил extras набора до полного референса (ряд-тумблер, плитки, плашка
* цвета, компас, футер со статусом) — фикстуры пересняты **намеренно** этим же
* коммитом. Смысл проверки прежний: лист диалогов меняется только тогда, когда
* это объявлено в issue, а не заодно с правкой панели (К9/AC11 — тест выше).
*/
test('#597/#600 лист диалогов карточки совпадает с объявленным', () => {
assert.equal(formKitCss(CARD_DIALOG_FORM_KIT, { withSwitch: false }), FROZEN_CARD_KIT_CSS);
assert.equal(formKitCss(CARD_DIALOG_FORM_KIT), FROZEN_CARD_KIT_WITH_SWITCH_CSS);
});
test('#594 имена параметризованы, а не зашиты', () => {
const card = formKitCss(CARD_DIALOG_FORM_KIT, { withSwitch: false });
assert.ok(card.includes('.hpf-card {'), 'карточка набора не переименовалась под префикс карточки');
assert.ok(card.includes('.hpf-form input:focus-visible'), 'фокус не привязался к форме карточки');
assert.ok(!card.includes('summary'), 'в лист карточки протекли имена панели');
assert.ok(!card.includes('-switch'), 'ряды-переключатели попали в граф, где их некому рисовать');
assert.ok(formKitCss(CARD_DIALOG_FORM_KIT).includes('.hpf-switch'),
'с включённым флагом ряд-переключатель обязан появляться');
});
test('#594 сегментированный переключатель остаётся радиогруппой', () => {
const kit = readFileSync(new URL('../src/editors/form-kit.ts', import.meta.url), 'utf8');
const segment = kit.slice(kit.indexOf('export function segmented'));
// Доступность здесь не украшение: сегмент заменил радиосписок, и стрелки со
// скринридером обязаны работать как раньше. Радиокнопка под капотом — это и
// есть механизм, а не пожелание.
assert.match(segment, /type="radio"/, 'сегмент перестал быть группой радиокнопок');
assert.match(segment, /role="radiogroup"/, 'группа потеряла роль для скринридера');
assert.match(segment, /name=\$\{name\}/, 'радиокнопки перестали делить имя группы');
const css = formKitCss(CARD_DIALOG_FORM_KIT, { withSwitch: false });
assert.match(css, /\.hpf-seg label \{[\s\S]*?min-height: 44px;/,
'цель нажатия сегмента опустилась ниже 44 px');
assert.match(css, /\.hpf-seg input \{[\s\S]*?opacity: 0;/,
'радиокнопка перестала быть скрытой — сегмент нарисуется дважды');
});