mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
General settings colour tiles (room fills and glow) paint their colour at its opacity over the checkerboard again, so "No light sources" at 0 % no longer looks like solid grey. The label ink is chosen from the visible colour (hex mixed with the checkerboard by alpha). Colour plates (flat-swatch without cover-swatch) stay solid — now witnessed by the - hp-color-opacity: one `_solidSwatch` condition drives background and opacity; cover-swatch keeps the checkerboard - editors/color-tile-ink.ts: pure isLightHex/colorTileInk, unit-tested - smoke_dialog_polish_605: tileAlpha, defaultsDiffer, noneInkDark, liveAlpha, platesFlat, plateSolid; oneSurface drops the opaque check - mutants M-615-tile, M-615-plate, M-615-label - USER-GUIDE (ru/en): "Colour tile" row; both changelogs Issue: #615 User-Visible: yes
195 lines
13 KiB
JavaScript
195 lines
13 KiB
JavaScript
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';
|
||
import {
|
||
TILE_INK_DARK, TILE_INK_LIGHT, colorTileInk, isLightHex,
|
||
} from '../test-build/editors/color-tile-ink.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 — тест выше).
|
||
* #602 намеренно выровнял тумблеры, поля прозрачности и строки шкал; фикстуры
|
||
* снова пересняты вместе с объявленным изменением UI.
|
||
* #605 меняет только объявленную полировку плиток, кнопок Data и поля иконки;
|
||
* лист панели по-прежнему остаётся замороженным без изменений.
|
||
*/
|
||
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');
|
||
// Только тело `segmented`: дальше в файле есть другие радиогруппы (radioRow,
|
||
// choiceCards), и срез «до конца файла» находил бы `type="radio"` у них,
|
||
// пропуская подмену внутри самого сегмента (мутант
|
||
// form-kit-segment-drops-radio-semantics выживал именно так).
|
||
const from = kit.indexOf('export function segmented');
|
||
const to = kit.indexOf('\nexport ', from + 1);
|
||
const segment = kit.slice(from, to > from ? to : undefined);
|
||
// Доступность здесь не украшение: сегмент заменил радиосписок, и стрелки со
|
||
// скринридером обязаны работать как раньше. Радиокнопка под капотом — это и
|
||
// есть механизм, а не пожелание.
|
||
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;/,
|
||
'радиокнопка перестала быть скрытой — сегмент нарисуется дважды');
|
||
});
|
||
|
||
test('#615: подпись плитки цвета выбирается по видимому цвету, а не по одному hex', () => {
|
||
// Плитка показывает α шахматкой. При α=0 под подписью только светлая
|
||
// шахматка, и белая подпись на ней пропадает даже у почти чёрного цвета —
|
||
// ровно тот случай, который ловит мутант M-615-label (подпись по hex без α).
|
||
assert.equal(colorTileInk('#0d1b2a', 0), TILE_INK_DARK, 'α=0: тёмный цвет, но видна шахматка — подпись тёмная');
|
||
assert.equal(colorTileInk('#0d1b2a', 1), TILE_INK_LIGHT, 'α=1: плотный тёмный цвет — подпись белая');
|
||
for (const a of [0, 0.18, 0.5, 1]) {
|
||
assert.equal(colorTileInk('#ffd45c', a), TILE_INK_DARK, `светлый #ffd45c при α=${a} — подпись тёмная`);
|
||
}
|
||
// `light_none` по умолчанию: #6b7480 при 0 %.
|
||
assert.equal(colorTileInk('#6b7480', 0), TILE_INK_DARK);
|
||
// При α=1 выбор обязан совпасть с прежним правилом `isLightHex(hex)`.
|
||
for (const hex of ['#0d1b2a', '#ffd45c', '#6b7480', '#ffffff', '#000000', '#3f8f5a', '#e05252', '#9aa4ad']) {
|
||
assert.equal(colorTileInk(hex, 1), isLightHex(hex) ? TILE_INK_DARK : TILE_INK_LIGHT, `α=1 для ${hex}`);
|
||
}
|
||
// Смешивание монотонно по α: у тёмного цвета подпись переключается ровно
|
||
// один раз — с тёмной на белую.
|
||
const flips = [...Array(101).keys()].map((i) => colorTileInk('#0d1b2a', i / 100))
|
||
.filter((ink, i, all) => i > 0 && ink !== all[i - 1]).length;
|
||
assert.equal(flips, 1);
|
||
// Некорректный hex и нечисловая α не роняют плитку.
|
||
assert.equal(colorTileInk('nope', 0.5), TILE_INK_DARK);
|
||
assert.equal(colorTileInk('#0d1b2a', Number.NaN), TILE_INK_LIGHT);
|
||
});
|