mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
test(dashboard): guard the hero rotation at the narrow width too
The rotation guard ran only at the wide width, before any slide was enriched. It now runs a second unattended rotation after the contrast pass, at the narrow width, where slides wrap the most and every slide carries a rating and a two-line overview. On master's hero that second rotation also fails, because its dots move. measureBackdropTextContrast clipped its screenshot to the range of the element's line boxes. A title cut by the 2-line clamp has line boxes below the visible box, so the probe measured the pill row there and scored about 1.0. The Xtream mock gives the recently-added slide a different title per run, so this failed whenever a long one came up. The clip is now the line boxes intersected with the element's own box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
86e4576fc5
commit
a12d554448
3 files changed
+44
-18
No files matched your search
@@ -207,6 +207,25 @@ async function rotationLayoutShift(page: Page): Promise<RotationShift> {
|
||||
);
|
||||
}
|
||||
|
||||
/** One unattended rotation moves next to nothing on the page, and nothing
|
||||
* at all inside the hero. */
|
||||
async function expectStableRotation(
|
||||
page: Page,
|
||||
label: string,
|
||||
results: string[]
|
||||
): Promise<void> {
|
||||
const shift = await rotationLayoutShift(page);
|
||||
const sources = shift.sources.join('; ');
|
||||
results.push(
|
||||
`${label} rotation layout shift ${shift.score.toFixed(5)} [${sources}]`
|
||||
);
|
||||
expect(shift.score, `${label}: ${sources}`).toBeLessThan(0.001);
|
||||
expect(
|
||||
shift.sources.filter((source) => source.includes('hero__')),
|
||||
`${label}: nothing in the hero moves`
|
||||
).toEqual([]);
|
||||
}
|
||||
|
||||
/** Adds what a TMDB-enriched slide shows: a rating chip and an overview.
|
||||
* Once per slide: a slide keeps its content while another one is shown. */
|
||||
async function enrichSlide(slide: Locator): Promise<void> {
|
||||
@@ -321,18 +340,10 @@ test.describe('Dashboard hero legibility', () => {
|
||||
// heights resized it and moved every rail below by 7px (a score
|
||||
// of 0.005). Every rotation dot keeps its width, so the active
|
||||
// one no longer pushes its neighbours either.
|
||||
const shift = await rotationLayoutShift(page);
|
||||
results.push(
|
||||
`rotation layout shift ${shift.score.toFixed(5)} ` +
|
||||
`[${shift.sources.join('; ')}]`
|
||||
);
|
||||
expect(shift.score, shift.sources.join('; ')).toBeLessThan(0.001);
|
||||
expect(
|
||||
shift.sources.filter((source) => source.includes('hero__')),
|
||||
'nothing in the hero moves'
|
||||
).toEqual([]);
|
||||
await expectStableRotation(page, 'wide', results);
|
||||
|
||||
await page.getByTestId('dashboard-hero-pause').click();
|
||||
const pause = page.getByTestId('dashboard-hero-pause');
|
||||
await pause.click();
|
||||
const kinds = await slideKinds(page);
|
||||
for (const theme of ['light', 'dark'] as const) {
|
||||
await applyTheme(page, theme);
|
||||
@@ -379,6 +390,12 @@ test.describe('Dashboard hero legibility', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Again at the narrow width, where slides wrap the most, now that
|
||||
// every slide also carries a rating and a two-line overview.
|
||||
await pause.click();
|
||||
await expect(pause).toHaveAttribute('aria-pressed', 'false');
|
||||
await expectStableRotation(page, 'narrow enriched', results);
|
||||
} finally {
|
||||
const report = testInfo.outputPath('contrast.txt');
|
||||
writeFileSync(report, results.join('\n'));
|
||||
|
||||
@@ -228,7 +228,15 @@ export async function measureBackdropTextContrast(
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
const box = range.getBoundingClientRect();
|
||||
// The line boxes, cut to the element's own box: a line-clamped or
|
||||
// ellipsized text has line boxes past it that never show, and those
|
||||
// would measure whatever sits there (the next row's pills).
|
||||
const lines = range.getBoundingClientRect();
|
||||
const own = element.getBoundingClientRect();
|
||||
const left = Math.max(lines.left, own.left);
|
||||
const top = Math.max(lines.top, own.top);
|
||||
const right = Math.min(lines.right, own.right);
|
||||
const bottom = Math.min(lines.bottom, own.bottom);
|
||||
const style = (element as HTMLElement).style;
|
||||
const previous = style.getPropertyValue('color');
|
||||
style.setProperty('color', 'transparent', 'important');
|
||||
@@ -238,10 +246,10 @@ export async function measureBackdropTextContrast(
|
||||
// Whole pixels inside the line boxes, clear of glyph edges that
|
||||
// spill past them.
|
||||
clip: {
|
||||
x: Math.ceil(box.left),
|
||||
y: Math.ceil(box.top),
|
||||
width: Math.max(1, Math.floor(box.width) - 1),
|
||||
height: Math.max(1, Math.floor(box.height) - 1),
|
||||
x: Math.ceil(left),
|
||||
y: Math.ceil(top),
|
||||
width: Math.max(1, Math.floor(right - left) - 1),
|
||||
height: Math.max(1, Math.floor(bottom - top) - 1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -193,8 +193,9 @@ slide hooks mark the shown slide only; the inert slides carry none.
|
||||
`dashboard-hero-rotation.e2e.ts` drives the real fill animation (with a
|
||||
shortened `--hero-rotation-ms`) to prove its `animationend` still advances
|
||||
and that pause holds the slide. `dashboard-hero-legibility.e2e.ts` sums the
|
||||
layout shifts of one unattended rotation: under 0.001 in all, and none
|
||||
inside the hero.
|
||||
layout shifts of an unattended rotation, at the wide width and again at the
|
||||
narrow one once every slide carries a rating and an overview: under 0.001 in
|
||||
all, and none inside the hero.
|
||||
|
||||
## Rail Contract
|
||||
|
||||
|
||||
Reference in new issue
Block a user