mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-12 14:47:58 +00:00
73 lines
2.8 KiB
JavaScript
73 lines
2.8 KiB
JavaScript
/* global document, getComputedStyle */
|
|
import assert from 'node:assert/strict';
|
|
import { expect } from '@playwright/test';
|
|
|
|
export async function measureTextContrast(locator) {
|
|
return locator.evaluate((element) => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = canvas.height = 1;
|
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
const rgba = (color) => {
|
|
context.clearRect(0, 0, 1, 1);
|
|
context.fillStyle = color;
|
|
context.fillRect(0, 0, 1, 1);
|
|
return [...context.getImageData(0, 0, 1, 1).data];
|
|
};
|
|
const blend = (front, back) =>
|
|
front
|
|
.slice(0, 3)
|
|
.map((value, index) => (value * front[3]) / 255 + back[index] * (1 - front[3] / 255));
|
|
const ancestors = [];
|
|
for (let node = element; node; node = node.parentElement) ancestors.unshift(node);
|
|
let background = [255, 255, 255];
|
|
for (const node of ancestors)
|
|
background = blend(rgba(getComputedStyle(node).backgroundColor), background);
|
|
const style = getComputedStyle(element);
|
|
const foreground = blend(rgba(style.color), background);
|
|
const luminance = (rgb) =>
|
|
rgb
|
|
.map((value) => {
|
|
const v = value / 255;
|
|
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
})
|
|
.reduce((sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index], 0);
|
|
const values = [luminance(foreground), luminance(background)].sort((a, b) => b - a);
|
|
return {
|
|
text: element.textContent.trim().slice(0, 80),
|
|
foreground,
|
|
background,
|
|
ratio: (values[0] + 0.05) / (values[1] + 0.05),
|
|
outline: style.outline,
|
|
shadow: style.boxShadow,
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function verifyRouteContrast(page, route) {
|
|
const targets = [];
|
|
const primary = page.getByRole('button', { name: 'New Task', exact: true });
|
|
targets.push(['primary-action', primary]);
|
|
if (route === 'drift')
|
|
targets.push(['selected-filter', page.getByRole('button', { name: 'all', exact: true })]);
|
|
if (route === 'operations') {
|
|
const code = page.locator('main code').first();
|
|
await expect(code).toBeVisible(); // Requires the real seeded completed task, never an empty-state pass.
|
|
targets.push(['task-id', code]);
|
|
}
|
|
const results = [];
|
|
for (const [label, target] of targets) {
|
|
await expect(target).toBeVisible();
|
|
await target.scrollIntoViewIfNeeded();
|
|
for (const state of label === 'task-id' ? ['normal'] : ['normal', 'hover', 'focus']) {
|
|
if (state === 'hover') await target.hover();
|
|
if (state === 'focus') {
|
|
await page.mouse.move(0, 0);
|
|
await target.focus();
|
|
}
|
|
const measured = await measureTextContrast(target);
|
|
assert(measured.ratio >= 4.5, `${route}/${label}/${state}: ${measured.ratio.toFixed(2)}:1`);
|
|
results.push({ label, state, ...measured });
|
|
}
|
|
}
|
|
return results;
|
|
}
|