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 = require('./test-support/ui-source')(); 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('Vue theme builds locally after Bootstrap without remote fonts', () => { const main=fs.readFileSync('frontend/src/main.js','utf8'); assert.ok(main.indexOf('bootstrap/dist/css/bootstrap.min.css') { 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'); });