// 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');