Files
ucvl-home-vision/test_theme.js
T

62 lines
3.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const css = fs.readFileSync(path.join(__dirname, 'web/chinese-theme.css'), 'utf8');
const html = fs.readFileSync(path.join(__dirname, 'web/index.html'), 'utf8');
const root = css.match(/:root\s*\{([^}]+)\}/)[1];
const token = name => root.match(new RegExp('--' + name + ':\\s*(#[0-9a-f]{6})'))[1];
function luminance(hex) {
const values = hex.slice(1).match(/../g).map(v => parseInt(v, 16) / 255)
.map(v => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4);
return values[0] * .2126 + values[1] * .7152 + values[2] * .0722;
}
function contrast(a, b) {
const x = luminance(a), y = luminance(b);
return (Math.max(x, y) + .05) / (Math.min(x, y) + .05);
}
test('paper theme is loaded last, versioned and uses only local fonts', () => {
const styles = [...html.matchAll(/<link rel="stylesheet" href="([^"]+)"/g)].map(x => x[1]);
assert.equal(styles.at(-1), '/chinese-theme.css?v=__ASSET_VERSION__');
assert.doesNotMatch(css, /@import|https?:|@font-face/i);
assert.match(html, /name="theme-color" content="#213d34"/);
assert.match(html, /class="courtyard-landscape"[^>]+aria-hidden="true"/);
});
test('primary text, muted text and actions have at least 4.5:1 contrast', () => {
for (const [fg, bg] of [
[token('ink'), token('paper')], [token('muted'), token('paper')],
[token('muted'), token('panel')], ['#fffdf6', token('accent')],
['#c9d1be', '#213d34'], ['#244638', '#efebdc'],
['#355b3d', '#e4eddf'], ['#835428', '#f2e8d9'],
['#994437', '#fbf8ef'], ['#d9dfca', '#24372b'],
]) assert.ok(contrast(fg, bg) >= 4.5, `${fg} on ${bg}: ${contrast(fg, bg).toFixed(2)}`);
});
test('monitor fullscreen, reduced motion and mobile navigation are explicitly styled', () => {
assert.match(css, /#page-live:fullscreen\s*\{[^}]+color-scheme: dark/);
assert.match(css, /@media \(prefers-reduced-motion: reduce\)/);
assert.match(css, /overflow-x: auto/);
assert.match(css, /\.family-node\.self\s*\{[^}]+background: #f1ddb0/);
assert.match(css, /--timeline-paper:/);
});
test('recording timeline paints with theme tokens and keeps range geometry', () => {
const vm = require('node:vm');
const source = fs.readFileSync(path.join(__dirname, 'web/recording-ui.js'), 'utf8');
const draw = source.slice(source.indexOf('function drawTimeline()'), source.indexOf('function playbackParams('));
const rectangles = [];
const ctx = { scale() {}, beginPath() {}, moveTo() {}, lineTo() {}, stroke() {},
fillRect(...coordinates) { rectangles.push({ color: this.fillStyle, coordinates }); } };
const canvas = { clientWidth: 480, getContext: () => ctx };
const values = { '--timeline-paper': '#ede8dd', '--timeline-grid': '#d3ccbc', '--timeline-range': '#4c7761' };
const scope = vm.createContext({ $: () => canvas, window: { devicePixelRatio: 2 }, document: { documentElement: {} },
getComputedStyle: () => ({ getPropertyValue: name => values[name] }),
queryStart: new Date(0), queryEnd: new Date(60000), spans: [{ start: 10000, end: 20000 }] });
vm.runInContext(draw + ';drawTimeline()', scope);
assert.equal(canvas.width, 960);
assert.equal(canvas.height, 152);
assert.deepEqual(rectangles, [
{ color: '#ede8dd', coordinates: [0, 0, 480, 76] },
{ color: '#4c7761', coordinates: [80, 25, 80, 25] },
]);
assert.equal(ctx.strokeStyle, '#d3ccbc');
});