Files
houseplan-card/test/form-kit.test.mjs
T
Claudeandclaude[bot] b28907f63d refactor(styles): редактор панели переезжает на общий набор контролов
Набор контролов был поднят из редактора боковой панели в #594, но сама панель
осталась на своей копии тех же правил. Две копии расходятся молча: правка в
наборе не доедет до панели, и наоборот.

Просто вызвать `formKitCss` из панели нельзя. Общие правила в её листе не идут
подряд: между карточкой-группой и обводкой фокуса стоит сетка `.summary-general-grid`,
между фокусом и выключенным состоянием — `button { font: inherit }`, между ним и
рядом-переключателем — `svg`. Вставить один общий кусок значило бы переставить её
объявления, а перестановка правил равной специфичности — уже не подстановка:
проверять пришлось бы каждое правило по отдельности.

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

Доказательство вместо обещания. У настроек панели нет ни одного golden-кадра: в
матрице нет сцены `dialog: 'summary'`, и «панель не изменилась» показать
пикселями нечем. Замена эталону — побайтовое сравнение собранного листа с
замороженным снимком с `origin/dev`:

    длина было 14585 стало 14585 — ПОБАЙТОВО СОВПАЛО

Две находки ревью ТЗ закрыты тестами, а не формулировками:

- проверка «копии не осталось» ловила один фрагмент из пяти (`min-height: 54px`
  встречается только в ряду-переключателе). Теперь цикл по всем пяти текстам;
  проба — фрагмент карточки-группы, оставленный в комментарии, — краснит именно
  этот цикл, а прежняя проверка была бы зелёной;
- «выход `formKitCss` остаётся байт в байт» опиралось на тесты, сравнивающие
  подстроки и счётчики. Добавлены две фикстуры с полным текстом обеих форм листа
  диалогов — с рядом-переключателем и без. Форма с переключателем сегодня не
  используется никем, и её расхождение не поймал бы никто до первого потребителя.

Три замороженные фикстуры — осознанный долг: они удаляются на том шаге эпика,
который законно меняет вид панели. Записано рядом с фикстурой.

Issue: #597
User-Visible: no
2026-09-19 14:19:54 +00:00

152 lines
9.8 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` до правки.
*/
test('#597 лист диалогов карточки не изменился ни на байт', () => {
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;/,
'радиокнопка перестала быть скрытой — сегмент нарисуется дважды');
});