fix(ui): restore detail surface boundaries in light theme

This commit is contained in:
4gray committed 2026-09-05 22:17:50 +02:00
1 parent 812ec69fd4
commit 3cdbb2bcff
9 files changed
+261 -32

No files matched your search

+113 -1
View File
@@ -1,4 +1,9 @@
import type { APIRequestContext, Locator } from '@playwright/test';
import type {
APIRequestContext,
Locator,
Page,
TestInfo,
} from '@playwright/test';
import { expect } from './fixtures';
export async function setInputValue(
@@ -71,3 +76,110 @@ export async function waitForScrollIdle(scrollOwner: Locator): Promise<void> {
})
);
}
/** Measure flat UI surfaces after alpha compositing, including CSS color-mix. */
export async function surfaceContrast(locator: Locator): Promise<{
border: number;
fill: number;
}> {
return locator.evaluate((element) => {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 1;
const context = canvas.getContext('2d');
if (!context)
throw new Error('Canvas is required to measure surface contrast');
const paint = (color: string) => {
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
};
const luminance = () => {
const channels = Array.from(context.getImageData(0, 0, 1, 1).data)
.slice(0, 3)
.map((value) => {
const channel = value / 255;
return channel <= 0.04045
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
});
return (
channels[0] * 0.2126 +
channels[1] * 0.7152 +
channels[2] * 0.0722
);
};
const ratio = (first: number, second: number) =>
(Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
const ancestors: Element[] = [];
for (
let parent = element.parentElement;
parent;
parent = parent.parentElement
) {
ancestors.unshift(parent);
}
paint('white');
ancestors.forEach((parent) =>
paint(getComputedStyle(parent).backgroundColor)
);
const behind = luminance();
const style = getComputedStyle(element);
paint(style.backgroundColor);
const fill = luminance();
paint(
parseFloat(style.borderTopWidth) > 0
? style.borderTopColor
: 'transparent'
);
return { border: ratio(luminance(), fill), fill: ratio(fill, behind) };
});
}
/** Check the shared series UI under a provider host before testing watched state. */
export async function expectSeriesSurfacesInBothThemes(
page: Page,
testInfo: TestInfo
): Promise<void> {
// Shared episode surfaces must also resolve the Stalker host's theme.
await page.setViewportSize({ width: 1600, height: 1100 });
await expect(page.locator('.hero__content')).toHaveCSS('opacity', '1');
for (const theme of ['light', 'dark']) {
await page.evaluate(
(dark) => document.body.classList.toggle('dark-theme', dark),
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(
async () =>
(await surfaceContrast(shell.locator(selector).first()))
.border
)
.toBeGreaterThan(1.15);
}
await page
.getByRole('radio', { name: 'List view', exact: true })
.click();
await expect
.poll(
async () =>
(
await surfaceContrast(
shell.locator('.episode-list-item').first()
)
).border
)
.toBeGreaterThan(1.15);
await shell.screenshot({
path: testInfo.outputPath(`stalker-series-list-${theme}.png`),
animations: 'disabled',
});
await page
.getByRole('radio', { name: 'Grid view', exact: true })
.click();
}
}