fix: crop the documentation frames on whole pixels

Замер снял главное: три снимка подряд в одном состоянии страницы совпадают
побайтово у всех десяти сценариев. Значит рендер детерминирован, а плавает то,
что приходит на вход съёмке.

Обрезка считается из живого DOM через getBoundingClientRect и приходит
дробной. Дробная обрезка заставляет Chromium ресемплить кадр — и тогда сдвиг
раскладки на десятую пикселя переписывает границы всех элементов на единицы
уровней. Ровно эта подпись в #410: 76 пикселей, максимум 2 уровня, alpha не
тронута, всё на сглаженных границах полей.

Рамка расширяется наружу, а не округляется к ближайшему: обрезка обязана
содержать цель целиком.

Issue: #410
User-Visible: no
This commit is contained in:
Matysh
2026-09-01 23:13:59 +03:00
parent d945b03208
commit ed700c8c8e
+27 -1
View File
@@ -262,9 +262,35 @@ try {
const runtime = await applyDocumentationState(page, scenario);
if (scenario.expectDialog && !runtime.dialog)
throw new Error(`documentation scenario did not open its dialog: ${scenario.id}`);
const clip = scenario.capture === 'room-card'
const rawClip = scenario.capture === 'room-card'
? await roomCardClip(page)
: await goldenClip(page, scenario.capture);
// Обрезка выравнивается по целым пикселям (#410).
//
// Прямоугольник считается из живого DOM через getBoundingClientRect, то есть
// приходит дробным. Дробная обрезка заставляет Chromium ресемплить кадр, и
// тогда сдвиг раскладки на десятую пикселя — от метрики шрифта, от
// появившегося скроллбара — переписывает границы всех элементов на единицы
// уровней. Ровно эта подпись и была в #410: 76 пикселей, максимум 2 уровня,
// alpha не тронута, всё на сглаженных границах полей.
//
// Замер показал, где искать: три снимка подряд в одном состоянии страницы
// совпадают побайтово у всех десяти сценариев, а между прогонами плавают
// два. Значит дело не в рендере, а в том, что приходит на вход съёмке.
//
// Внешняя рамка расширяется, а не округляется к ближайшему: обрезка обязана
// содержать цель целиком, потерять полпикселя по краю нельзя.
const clip = rawClip && {
x: Math.floor(rawClip.x),
y: Math.floor(rawClip.y),
width: Math.ceil(rawClip.x + rawClip.width) - Math.floor(rawClip.x),
height: Math.ceil(rawClip.y + rawClip.height) - Math.floor(rawClip.y),
};
if (STABILITY_SHOTS && rawClip) {
console.log(`${scenario.id} обрезка: сырая`
+ ` ${rawClip.x},${rawClip.y} ${rawClip.width}x${rawClip.height}`
+ ` → целая ${clip.x},${clip.y} ${clip.width}x${clip.height}`);
}
// Два кадра ожидания перед съёмкой — как в golden. `animations: 'disabled'`
// гасит анимации, но не гарантирует, что уже запланированный ре-рендер
// успел лечь в композитор до захвата.