v0.32.2
This commit is contained in:
1 parent
bb025dcf44
commit
b0f211555a
74 files changed
+6089
-556
No files matched your search
@@ -0,0 +1,77 @@
|
||||
// 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');
|
||||
Reference in new issue
Block a user