20a7d7d9d5
Locked UI/copy pass on proto-per-user-woo-creds (Settings/Woo gate polish out of scope): - Inventory PNG thumbs at 3:4 contain (Review, Publish preview, Photos ready); stop 1:1 cover squaring - Dimension overlay labels fixed OVERLAY_FONT_PX=36 for every product - Page 1–3 Jeff-plain copy; drop Measure red tech essays / px scale rows - Review: lock SVG, consistent red missing/not-ready, View prompt gated (Settings default off)
134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import fs from 'node:fs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ARTIFACTS = path.resolve(__dirname, 'artifacts');
|
|
|
|
const shot = async (page: Page, name: string) => {
|
|
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
await page.screenshot({
|
|
path: path.join(ARTIFACTS, name),
|
|
fullPage: true,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* WCAG contrast helpers (in-page evaluate). Binary observable: ratio < 4.5:1 fails.
|
|
* Matches visual-qa color check patterns for luminance/contrast.
|
|
*/
|
|
async function assertReadableContrast(
|
|
page: Page,
|
|
selectors: string[],
|
|
label: string
|
|
): Promise<void> {
|
|
const minRatio = await page.evaluate((sels: string[]) => {
|
|
function luminance(r: number, g: number, b: number): number {
|
|
const a = [r, g, b].map((v) => {
|
|
v /= 255;
|
|
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
});
|
|
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
|
}
|
|
function contrastRatio(c1: number[], c2: number[]): number {
|
|
const l1 = luminance(c1[0], c1[1], c1[2]);
|
|
const l2 = luminance(c2[0], c2[1], c2[2]);
|
|
const hi = Math.max(l1, l2);
|
|
const lo = Math.min(l1, l2);
|
|
return (hi + 0.05) / (lo + 0.05);
|
|
}
|
|
function parse(c: string): [number, number, number] {
|
|
const m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
return m ? [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)] : [255, 255, 255];
|
|
}
|
|
|
|
let minR = 21;
|
|
for (const sel of sels) {
|
|
const els = document.querySelectorAll(sel);
|
|
for (const el of Array.from(els)) {
|
|
if (!(el instanceof HTMLElement)) continue;
|
|
const fg = parse(window.getComputedStyle(el).color);
|
|
// collect ALL non-transparent bgs in ancestor chain; pick darkest (min lum) as the surface
|
|
const bgs: [number, number, number][] = [];
|
|
let p: HTMLElement | null = el;
|
|
while (p) {
|
|
const pst = window.getComputedStyle(p);
|
|
const bstr = pst.backgroundColor;
|
|
if (!bstr.includes('0)')) {
|
|
const b = parse(bstr);
|
|
// avoid pure black default if transparent resolved
|
|
if (!(b[0] === 0 && b[1] === 0 && b[2] === 0)) {
|
|
bgs.push(b);
|
|
}
|
|
}
|
|
p = p.parentElement;
|
|
}
|
|
// also include body/html
|
|
const bodyB = parse(window.getComputedStyle(document.body).backgroundColor);
|
|
if (!(bodyB[0] === 0 && bodyB[1] === 0 && bodyB[2] === 0)) bgs.push(bodyB);
|
|
const htmlB = parse(window.getComputedStyle(document.documentElement).backgroundColor);
|
|
if (!(htmlB[0] === 0 && htmlB[1] === 0 && htmlB[2] === 0)) bgs.push(htmlB);
|
|
|
|
let surface = [255, 255, 255] as [number, number, number];
|
|
let minLum = 1;
|
|
for (const b of bgs) {
|
|
const l = luminance(b[0], b[1], b[2]);
|
|
if (l < minLum) {
|
|
minLum = l;
|
|
surface = b;
|
|
}
|
|
}
|
|
const r = contrastRatio(fg, surface);
|
|
if (r < minR) minR = r;
|
|
}
|
|
}
|
|
return minR;
|
|
}, selectors);
|
|
|
|
expect(minRatio, `contrast < 4.5:1 (${label})`).toBeGreaterThanOrEqual(4.5);
|
|
}
|
|
|
|
test.describe('Settings contrast (Gitea bug #75 black-on-black)', () => {
|
|
test('Happy: Settings form labels/inputs/selects readable (contrast ≥4.5:1)', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: 'Settings (dev)' }).click();
|
|
await shot(page, 'settings-happy.png');
|
|
// labels, inputs, selects on the form
|
|
await assertReadableContrast(page, ['label', 'input', 'select'], 'happy-settings-form');
|
|
});
|
|
|
|
test('Edge: <select> dropdown options visible and readable on dark surface', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: 'Settings (dev)' }).click();
|
|
await shot(page, 'settings-edge.png');
|
|
// options may render on dark surface when select is dark-themed
|
|
await assertReadableContrast(page, ['select', 'option', 'label'], 'edge-select-dropdown');
|
|
});
|
|
|
|
test('Regression: SlabList.svelte / Review.svelte / Publish.svelte / GenerateText.svelte remain unchanged (still pass their existing contrast)', async ({ page }) => {
|
|
const regressionScreens = [
|
|
{ name: 'Slabs (dev)', heading: /slabs/i },
|
|
{ name: 'Review (dev)', heading: 'Review slab' },
|
|
{ name: 'Publish (dev)', heading: 'Publish to WooCommerce' },
|
|
{ name: 'Generate (dev)', heading: 'Generate text' },
|
|
];
|
|
|
|
for (const scr of regressionScreens) {
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: scr.name }).click();
|
|
if (scr.heading) {
|
|
await expect(page.getByRole('heading', { name: scr.heading })).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
// these already set color:#eee on root → must still pass ≥4.5
|
|
await assertReadableContrast(page, ['body', 'main', 'label', 'input', 'select'], `regression-${scr.name}`);
|
|
await shot(page, `regression-${scr.name.replace(/\s|\(|\)/g, '')}.png`);
|
|
}
|
|
|
|
// regression also confirms settings still exhibits the low-contrast bug (until T2 fix)
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: 'Settings (dev)' }).click();
|
|
await assertReadableContrast(page, ['label', 'input', 'select', 'option'], 'regression-settings-still-bad');
|
|
});
|
|
});
|