Files
houseplan-card/test/form-kit.test.mjs
T
ClaudeandClaude 4344ede13a feat: общий набор контролов формы и диалог комнаты по нему (#594)
Шаг 1 эпика #591. Набор поднят из редактора боковой панели — он сделан по
макетам дизайнера и уже живёт в продукте, — а не собран заново по прототипу.

Стили набора выдаёт генератор, параметризованный именами: контейнер формы,
префикс классов, карточки, футер и токены цвета. Это не украшение архитектуры,
а условие переезда панели на общий источник: тест с замороженной фикстурой
показывает, что с параметрами панели генератор отдаёт её нынешние правила
дословно — карточку, обводку фокуса, выключенное состояние и ряд-переключатель.
После этого переезд станет подстановкой, а не редизайном, и пиксели панели не
дрогнут. Сама панель в этом коммите не тронута.

Диалог комнаты собран в четыре карточки: «Основное», «Заливка», «Источники»,
«Размеры шрифтов». Порядок полей прежний, записи прежние — те же ключи
черновика, те же условия «Сохранить», тот же `hp-color-opacity` с атомарной
записью цвета и прозрачности. Пояснения уехали под «?» у заголовков групп
(решение владельца 18.09), выбор источника стал сегментированным
переключателем — под капотом это по-прежнему радиогруппа, поэтому стрелки и
скринридер работают как раньше.

Два измерения определили форму решения, и оба стоило сделать до кода.

Первое: лист набора в синхронном графе дал +1459 Б gzip и пробил потолок
initial View на 415 Б. Диалоги — инструмент администратора, во View их не
открывают, поэтому лист уехал в ленивый редакторский граф и вносится в теневой
корень при открытии диалога — тем же способом, которым это делает боковая
панель. Потолок не поднимался.

Второе: `css` с интерполяцией запрещён минификатором сборки (он намеренно
падает, чтобы не зависеть от рантайма), поэтому лист собирается `unsafeCSS`.
Цена — несжатые пробелы генератора, замерено 59 Б gzip; копия литералом стоила
бы расхождения с генератором, то есть единого источника.

Оставшийся рост initial View — 291 356 → 291 872 Б (+516) — это четыре текста
подсказок в двух встроенных локалях. Тексты сокращены до одного предложения:
подсказка под «?» и должна быть короткой.

`_renderRoomSource` переехал в модуль комнаты вместе со стабом карточки: у него
остался ровно один потребитель.

Issue: #594
User-Visible: yes
2026-09-19 11:08:03 +03:00

95 lines
5.6 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';
/**
* #594. Набор контролов поднят из редактора боковой панели, и главный вопрос к
* нему один: общий ли он на самом деле. Ответ обязан быть машинным — с
* параметрами панели генератор должен выдавать её нынешние правила ДОСЛОВНО.
* Тогда переезд панели на общий источник (следующий шаг эпика #591) окажется
* подстановкой, а не редизайном, и её пиксели не дрогнут.
*
* Фрагменты ниже скопированы из `src/summary-panel-editor-style.ts` как есть.
* Тест читает тот же файл и проверяет, что фрагмент всё ещё в нём: иначе
* «замороженная» фикстура тихо разошлась бы с панелью и сравнивала генератор
* сама с собой.
*/
const PANEL_STYLE = readFileSync(
new URL('../src/summary-panel-editor-style.ts', 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(PANEL_STYLE.includes(fragment),
`${name}: фикстура разошлась с листом панели — сверьте src/summary-panel-editor-style.ts`);
assert.ok(generated.includes(fragment),
`${name}: генератор с параметрами панели больше не выдаёт её правило дословно`);
}
});
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;/,
'радиокнопка перестала быть скрытой — сегмент нарисуется дважды');
});