|
| 1 | +import { test } from "node:test"; |
| 2 | +import assert from "node:assert/strict"; |
| 3 | +import fs from "node:fs"; |
| 4 | +import path from "node:path"; |
| 5 | +import { fileURLToPath } from "node:url"; |
| 6 | + |
| 7 | +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 8 | + |
| 9 | +function readHtml(relativePath) { |
| 10 | + return fs.readFileSync(path.join(rootDir, relativePath), "utf8"); |
| 11 | +} |
| 12 | + |
| 13 | +function extractStyle(html, relativePath) { |
| 14 | + const match = html.match(/<style>([\s\S]*?)<\/style>/i); |
| 15 | + assert.ok(match, `${relativePath} should include inline CSS`); |
| 16 | + return match[1]; |
| 17 | +} |
| 18 | + |
| 19 | +function extractVariableSet(block) { |
| 20 | + const variables = new Map(); |
| 21 | + for (const match of block.matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/gi)) { |
| 22 | + variables.set(match[1], match[2].trim()); |
| 23 | + } |
| 24 | + return variables; |
| 25 | +} |
| 26 | + |
| 27 | +function extractThemeBlocks(css, relativePath) { |
| 28 | + const lightMatch = css.match(/:root\s*\{([\s\S]*?)\}/); |
| 29 | + assert.ok(lightMatch, `${relativePath} should define light :root variables`); |
| 30 | + |
| 31 | + const darkMatch = css.match(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{\s*:root\s*\{([\s\S]*?)\}\s*\}/); |
| 32 | + assert.ok(darkMatch, `${relativePath} should define a dark prefers-color-scheme override`); |
| 33 | + |
| 34 | + return { |
| 35 | + light: extractVariableSet(lightMatch[1]), |
| 36 | + dark: extractVariableSet(darkMatch[1]) |
| 37 | + }; |
| 38 | +} |
| 39 | + |
| 40 | +function extractUsedVariables(css) { |
| 41 | + return new Set(Array.from(css.matchAll(/var\(--([a-z0-9-]+)\)/gi), (match) => match[1])); |
| 42 | +} |
| 43 | + |
| 44 | +function assertThemeCoverage(relativePath, pageSpecificVariables = []) { |
| 45 | + const html = readHtml(relativePath); |
| 46 | + const css = extractStyle(html, relativePath); |
| 47 | + const { light, dark } = extractThemeBlocks(css, relativePath); |
| 48 | + |
| 49 | + const requiredVariables = [ |
| 50 | + "bg", |
| 51 | + "text", |
| 52 | + "text-muted", |
| 53 | + "border", |
| 54 | + "btn-bg", |
| 55 | + "btn-hover", |
| 56 | + "btn-border", |
| 57 | + "accent", |
| 58 | + "accent-hover", |
| 59 | + "error", |
| 60 | + "success", |
| 61 | + ...pageSpecificVariables |
| 62 | + ]; |
| 63 | + |
| 64 | + for (const variable of requiredVariables) { |
| 65 | + assert.ok(light.has(variable), `${relativePath} light theme should define --${variable}`); |
| 66 | + assert.ok(dark.has(variable), `${relativePath} dark theme should define --${variable}`); |
| 67 | + } |
| 68 | + |
| 69 | + for (const variable of extractUsedVariables(css)) { |
| 70 | + assert.ok(light.has(variable), `${relativePath} uses --${variable} in CSS but does not define it for light mode`); |
| 71 | + assert.ok(dark.has(variable), `${relativePath} uses --${variable} in CSS but does not define it for dark mode`); |
| 72 | + } |
| 73 | + |
| 74 | + assert.notEqual(light.get("bg"), dark.get("bg"), `${relativePath} dark mode should override --bg`); |
| 75 | + assert.notEqual(light.get("text"), dark.get("text"), `${relativePath} dark mode should override --text`); |
| 76 | + assert.notEqual(light.get("border"), dark.get("border"), `${relativePath} dark mode should override --border`); |
| 77 | + assert.match(css, /body\s*\{[^}]*color:\s*var\(--text\)[^}]*background:\s*var\(--bg\)/s); |
| 78 | +} |
| 79 | + |
| 80 | +test("popup page has complete light and dark theme variables", () => { |
| 81 | + assertThemeCoverage("ui/popup.html", ["discover-bg", "discover-border"]); |
| 82 | +}); |
| 83 | + |
| 84 | +test("options page has complete light and dark theme variables", () => { |
| 85 | + assertThemeCoverage("ui/options.html", [ |
| 86 | + "input-border", |
| 87 | + "input-bg", |
| 88 | + "card-bg", |
| 89 | + "section-bg", |
| 90 | + "info-bg", |
| 91 | + "info-border" |
| 92 | + ]); |
| 93 | +}); |
0 commit comments