mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Прежде эталон принимался только из артефакта CI: растеризация шрифтов на другой машине может отличаться, а доказать обратное было нечем. Цена — два полных прогона на каждый визуальный фикс, при версии матрицы 48 она платится часто. Доказательство теперь эмпирическое: среда равна раннеру, если каждая сцена, которую менять не собирались, совпала со своим эталоном. Расхождение растеризации спрятать нельзя — оно задевает все сцены с текстом. Ревьюер объявляет намерение через --expect-change, всё разошедшееся помимо списка приёмку запрещает. Поэтому неверно угаданный тег образа не может испортить эталоны: он может только не сработать. То же правило независимо от среды запрещает «принять всё, чтобы CI позеленел» — именно так эталон перестаёт быть эталоном, молча и одной командой. scripts/golden-container.mjs снимает кандидатов в образе Playwright той же версии, что залочена в package-lock. Хозяйский node_modules прячется анонимным томом: он собран под Windows, и npm ci внутри контейнера сломал бы дерево. Обёртка, а не правка demo/golden/accept.mjs, — намеренно. sourceFingerprint включает ВСЕ .mjs из demo/golden, включая accept.mjs и policy.mjs, которые исполняются после съёмки и ни одного пикселя изменить не могут. Их правка объявляет устаревшими бандл и оба манифеста, то есть требует ровно того двойного цикла, который эта задача убирает. Сужение корпуса отпечатка — отдельная задача: сам source-fingerprint.mjs в корпусе, и одна пересборка бандла неизбежна. Issue: #334 User-Visible: no
60 lines
4.5 KiB
JavaScript
60 lines
4.5 KiB
JavaScript
/**
|
||
* Правило допустимости съёмки golden-кандидатов (#334).
|
||
*
|
||
* Прежде эталон принимался только из артефакта CI: растеризация шрифтов на
|
||
* другой машине может отличаться, а доказать обратное было нечем. Цена —
|
||
* двойной roundtrip на каждый визуальный фикс: пуш, семь минут CI, скачивание
|
||
* артефакта, приёмка, второй пуш, второй полный прогон. При версии матрицы 48
|
||
* это платится регулярно.
|
||
*
|
||
* Доказательство есть, и оно эмпирическое: среда съёмки равна раннеру, если
|
||
* КАЖДАЯ сцена, которую менять не собирались, совпала со своим принятым
|
||
* эталоном. Расхождение растеризации нельзя спрятать — оно задевает все сцены
|
||
* с текстом, а не только правленные. Поэтому ревьюер объявляет намерение
|
||
* списком `--expect-change`, и всё, что разошлось помимо списка, приёмку
|
||
* запрещает: либо это регрессия рендера, либо среда не та.
|
||
*
|
||
* То же правило ловит другое, независимо от среды: «принять всё, чтобы CI
|
||
* позеленел». Именно так эталон перестаёт быть эталоном — молча, одной
|
||
* командой, без единого названного намерения.
|
||
*
|
||
* `missing-baseline` объявления не требует: у новой сцены нет эталона, которому
|
||
* она могла бы противоречить.
|
||
*
|
||
* Почему это лежит в `scripts/`, а не рядом с `accept.mjs`. Отпечаток
|
||
* `sourceFingerprint` включает ВСЕ `.mjs` из `demo/golden`, включая
|
||
* `accept.mjs` и `policy.mjs`, которые исполняются после того, как картинка уже
|
||
* снята, и ни одного пикселя изменить не могут. Правка любого из них объявляет
|
||
* устаревшими и закоммиченный бандл, и манифест скриншотов документации, и
|
||
* манифест эталонов — то есть требует ровно того двойного цикла, который эта
|
||
* задача убирает. Сужение корпуса отпечатка — отдельная задача: оно неизбежно
|
||
* требует пересборки бандла, потому что сам `source-fingerprint.mjs` в корпусе.
|
||
*/
|
||
|
||
export const goldenAcceptanceRefusal = (results, declared = []) => {
|
||
if (!Array.isArray(results)) return 'отчёт кандидатов не содержит результатов сцен';
|
||
const expected = new Set(declared.filter(Boolean));
|
||
const known = new Set(results.map((result) => result.id));
|
||
const unknown = [...expected].filter((id) => !known.has(id));
|
||
if (unknown.length) {
|
||
return `в --expect-change названы сцены, которых нет в отчёте: ${unknown.sort().join(', ')}`;
|
||
}
|
||
const undeclared = results
|
||
.filter((result) => result.status === 'different' && !expected.has(result.id))
|
||
.map((result) => result.id)
|
||
.sort();
|
||
if (undeclared.length) {
|
||
return 'съёмка разошлась с принятыми эталонами в сценах, которые менять не собирались:'
|
||
+ ` ${undeclared.join(', ')}.`
|
||
+ ' Либо это регрессия рендера, либо среда съёмки не совпадает с раннером —'
|
||
+ ' в обоих случаях приёмка запрещена. Намеренные сцены перечисляются в'
|
||
+ ' --expect-change=<id,id>';
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/** Сцены, объявленные изменёнными, но совпавшие с эталоном: не ошибка, но и не молчание. */
|
||
export const goldenSilentDeclarations = (results, declared = []) => declared
|
||
.filter((id) => results.find((result) => result.id === id)?.status === 'passed')
|
||
.sort();
|