mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
test(ui): measure detail action edges over rendered artwork
This commit is contained in:
1 parent
3cdbb2bcff
commit
2137ba2090
3 files changed
+82
-6
No files matched your search
@@ -5,6 +5,7 @@ import type {
|
||||
TestInfo,
|
||||
} from '@playwright/test';
|
||||
import { expect } from './fixtures';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export async function setInputValue(
|
||||
input: Locator,
|
||||
@@ -134,6 +135,73 @@ export async function surfaceContrast(locator: Locator): Promise<{
|
||||
});
|
||||
}
|
||||
|
||||
/** Compare the actual hero edge with the same pixels with just its border hidden.
|
||||
* Both rasters retain artwork, gradients, opacity and backdrop filters. Matching
|
||||
* pixels prevent artwork detail from being mistaken for a visible button edge. */
|
||||
export async function rasterizedBorderContrast(
|
||||
locator: Locator
|
||||
): Promise<number> {
|
||||
const screenshot = () =>
|
||||
locator.screenshot({ animations: 'disabled', scale: 'css' });
|
||||
const visible = await screenshot();
|
||||
const originalStyle = await locator.getAttribute('style');
|
||||
let hidden: Buffer;
|
||||
try {
|
||||
await locator.evaluate((element) => {
|
||||
const style = (element as HTMLElement).style;
|
||||
style.setProperty('transition', 'none', 'important');
|
||||
style.setProperty('border-top-color', 'transparent', 'important');
|
||||
});
|
||||
hidden = await screenshot();
|
||||
} finally {
|
||||
await locator.evaluate((element, style) => {
|
||||
if (style === null) element.removeAttribute('style');
|
||||
else element.setAttribute('style', style);
|
||||
}, originalStyle);
|
||||
}
|
||||
const decode = (buffer: Buffer) =>
|
||||
sharp(buffer).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||
const [before, after] = await Promise.all([
|
||||
decode(visible),
|
||||
decode(hidden),
|
||||
]);
|
||||
expect(after.info).toEqual(before.info);
|
||||
const luminance = (data: Buffer, x: number, y: number) => {
|
||||
const offset = (y * before.info.width + x) * before.info.channels;
|
||||
return [0.2126, 0.7152, 0.0722].reduce((sum, weight, index) => {
|
||||
const value = data[offset + index] / 255;
|
||||
return (
|
||||
sum +
|
||||
weight *
|
||||
(value <= 0.04045
|
||||
? value / 12.92
|
||||
: ((value + 0.055) / 1.055) ** 2.4)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
const ratios: number[] = [];
|
||||
// Avoid rounded corners. Inspect three raster rows to tolerate a fractional
|
||||
// CSS edge, retaining the strongest edge pixel in each sampled column.
|
||||
for (
|
||||
let x = Math.ceil(before.info.width / 4);
|
||||
x < (before.info.width * 3) / 4;
|
||||
x++
|
||||
) {
|
||||
let strongest = 1;
|
||||
for (let y = 0; y < Math.min(3, before.info.height); y++) {
|
||||
const a = luminance(before.data, x, y);
|
||||
const b = luminance(after.data, x, y);
|
||||
strongest = Math.max(
|
||||
strongest,
|
||||
(Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)
|
||||
);
|
||||
}
|
||||
ratios.push(strongest);
|
||||
}
|
||||
ratios.sort((a, b) => a - b);
|
||||
return ratios[Math.floor(ratios.length / 2)];
|
||||
}
|
||||
|
||||
/** Check the shared series UI under a provider host before testing watched state. */
|
||||
export async function expectSeriesSurfacesInBothThemes(
|
||||
page: Page,
|
||||
@@ -148,11 +216,12 @@ export async function expectSeriesSurfacesInBothThemes(
|
||||
theme === 'dark'
|
||||
);
|
||||
const shell = page.locator('app-portal-detail-shell');
|
||||
for (const selector of [
|
||||
'.favorite-btn',
|
||||
'.episode-card',
|
||||
'mat-button-toggle-group',
|
||||
]) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
rasterizedBorderContrast(shell.locator('.favorite-btn').first())
|
||||
)
|
||||
.toBeGreaterThan(1.1);
|
||||
for (const selector of ['.episode-card', 'mat-button-toggle-group']) {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
rasterizedBorderContrast,
|
||||
setInputValue,
|
||||
surfaceContrast,
|
||||
waitForScrollIdle,
|
||||
@@ -1249,7 +1250,10 @@ for (const theme of ['light', 'dark']) {
|
||||
await page.mouse.move(0, 0);
|
||||
// A subtle edge must survive compositing on the actual theme surface.
|
||||
// This catches white-alpha borders that disappear in the light theme.
|
||||
for (const surface of [favorite, card, toggle]) {
|
||||
await expect
|
||||
.poll(() => rasterizedBorderContrast(favorite))
|
||||
.toBeGreaterThan(1.1);
|
||||
for (const surface of [card, toggle]) {
|
||||
await expect
|
||||
.poll(async () => (await surfaceContrast(surface)).border)
|
||||
.toBeGreaterThan(1.15);
|
||||
|
||||
@@ -380,6 +380,9 @@ partial so Xtream and Stalker share the same behavior.
|
||||
|
||||
Browser regression coverage measures the composited neutral edges and selected
|
||||
toggle fill, in addition to capturing light/dark grid and list screenshots.
|
||||
Hero action edges use matching pixels from rendered screenshots with the border
|
||||
visible and transparent, retaining the artwork and gradient behind the button;
|
||||
flat ancestor-color compositing is only appropriate outside that layered hero.
|
||||
|
||||
## Settings Surfaces
|
||||
|
||||
|
||||
Reference in new issue
Block a user