Files
RustProxy/tools/checks/admin_themes_0323_ui_test.mjs
T
2026-10-13 21:29:57 +02:00

78 lines
4.0 KiB
JavaScript

// Offline UI/theme contract: selectors, cached appearance, and CSS readability.
// Does not substitute for a browser session or Rust API integration test.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const base = 'src/admin/web/';
const css = fs.readFileSync(`${base}style.css`, 'utf8');
const html = fs.readFileSync(`${base}index.html`, 'utf8');
const login = fs.readFileSync(`${base}login.js`, 'utf8');
const app = fs.readFileSync(`${base}app/preferences.js`, 'utf8');
const rust = fs.readFileSync('src/admin/preferences.rs', 'utf8');
const schema = JSON.parse(fs.readFileSync('src/admin/openapi.json', 'utf8'));
const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations;
const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations;
const themes = ['dark', 'ultra_dark', 'light', 'cream'];
const declaration = app.match(/^const ADMIN_THEMES = .*;$/m)?.[0];
const normalize = app.match(/^function normalizeAdminTheme\(value\) \{[^\n]*\}$/m)?.[0];
const apply = app.split('function applyAdminTheme(value, cache = true) {')[1]
?.split('function applyAdminLanguage(')[0];
assert(declaration && normalize && apply);
const stored = new Map();
const select = { value: '' };
const root = { dataset: {} };
const ctx = {
document: { documentElement: root },
$: id => id === 'ui-theme' ? select : null,
localStorage: { setItem: (k, v) => stored.set(k, v), getItem: k => stored.get(k) },
};
vm.createContext(ctx);
vm.runInContext(`${declaration}\n${normalize}\nfunction applyAdminTheme(value, cache = true) {${apply}`, ctx);
for (const theme of themes) {
assert(html.includes(`value="${theme}"`), `option ${theme}`);
assert(login.includes('"ultra_dark", "light", "cream"'), 'cached theme also applies at login');
assert(rust.includes('"dark" | "ultra_dark" | "light" | "cream"'));
assert.equal(vm.runInContext(`normalizeAdminTheme("${theme}")`, ctx), theme);
assert.equal(vm.runInContext(`applyAdminTheme("${theme}")`, ctx), theme);
assert.equal(root.dataset.theme, theme);
assert.equal(select.value, theme);
assert.equal(stored.get('rp-theme'), theme);
}
assert.equal(vm.runInContext('normalizeAdminTheme("unknown")', ctx), 'dark');
vm.runInContext('applyAdminTheme("ultra_dark", false)', ctx);
assert.equal(stored.get('rp-theme'), 'cream', 'preview without caching preserves selected preference');
assert(en.themeUltraDark && en.themeCream && pl.themeUltraDark && pl.themeCream);
assert.equal(schema.components.schemas.AdminPreferences.properties.theme.enum.length, 4);
function block(selector) {
const start = css.indexOf(`${selector} {`);
assert(start >= 0, selector);
return css.slice(start, css.indexOf('\n}', start) + 2);
}
function token(section, key) {
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = section.match(new RegExp(`--${escaped}:\\s*(#[0-9a-fA-F]{6});`));
assert(match, `missing ${key}`);
return match[1];
}
function luminance(hex) {
const rgb = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255);
const [r, g, b] = rgb.map(v => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrast(a, b) {
const x = luminance(a), y = luminance(b);
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
}
for (const theme of ['ultra_dark', 'cream']) {
const section = block(`html[data-theme="${theme}"]`);
const bg = token(section, 'bg'), panel = token(section, 'panel'), panel2 = token(section, 'panel2');
const text = token(section, 'text'), muted = token(section, 'muted'), accent = token(section, 'accent');
for (const [name, foreground, surface] of [
['text on background', text, bg], ['text on panel', text, panel],
['muted on card', muted, panel2], ['accent on background', accent, bg],
]) assert(contrast(foreground, surface) >= 4.5, `${theme}: ${name}`);
}
console.log('PASS: four account themes, persisted and cached selection, server API contract, and accessible black/cream palette contrast');