92 lines
5.3 KiB
JavaScript
92 lines
5.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile, readdir } from 'node:fs/promises';
|
|
import { palette, references, themes, themeNames, getTheme } from '../src/tokens.mjs';
|
|
|
|
const file = name => readFile(new URL(`../${name}`, import.meta.url));
|
|
const json = async name => JSON.parse(await file(name));
|
|
const luminance = hex => hex.match(/[\da-f]{2}/gi).map(v => parseInt(v, 16) / 255)
|
|
.map(v => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4)
|
|
.reduce((a, v, i) => a + v * [.2126, .7152, .0722][i], 0);
|
|
const contrast = (a, b) => (Math.max(luminance(a), luminance(b)) + .05) / (Math.min(luminance(a), luminance(b)) + .05);
|
|
|
|
test('four production themes resolve every palette reference and expose the same contract', () => {
|
|
assert.deepEqual([...themeNames].sort(), ['canvas-dark', 'canvas-light', 'home-dark', 'home-light']);
|
|
const keys = Object.keys(references['home-light']).sort();
|
|
for (const name of themeNames) {
|
|
assert.deepEqual(Object.keys(references[name]).sort(), keys);
|
|
for (const ref of Object.values(references[name])) assert.ok(Object.hasOwn(palette, ref), `${name}: ${ref}`);
|
|
for (const key of ['card', 'popover', 'accent', 'input', 'ring', 'destructive', 'chart-5', 'sidebar', 'sidebar-ring']) assert.ok(keys.includes(key));
|
|
assert.equal(themes[name]['accent'], themes[name].selection);
|
|
assert.notEqual(themes[name].accent, themes[name]['brand-mocha']);
|
|
}
|
|
assert.equal(getTheme('canvas-light').background, '#FAFAFA');
|
|
assert.equal(getTheme('canvas-dark').background, '#141414');
|
|
assert.throws(() => getTheme('canvas-dark-proposed'), RangeError);
|
|
assert.throws(() => getTheme('toString'), RangeError);
|
|
});
|
|
|
|
test('committed CSS and Tailwind bridge match the canonical tokens', async () => {
|
|
const css = (await file('src/themes.css')).toString();
|
|
const bridge = (await file('src/tailwind.css')).toString();
|
|
for (const [key, value] of Object.entries(palette)) assert.ok(css.includes(`--el-${key}: ${value};`), key);
|
|
for (const name of themeNames) {
|
|
const marker = `[data-theme="${name}"]`;
|
|
const block = css.slice(css.indexOf(marker)).split('}')[0];
|
|
for (const [key, ref] of Object.entries(references[name])) assert.ok(block.includes(`--${key}: var(--el-${ref});`), `${name}/${key}`);
|
|
}
|
|
for (const key of Object.keys(references['home-light'])) {
|
|
if (!['radius', 'shadow', 'scrim'].includes(key)) assert.ok(bridge.includes(`--color-${key}: var(--${key});`), key);
|
|
}
|
|
assert.ok(css.includes(':root:not([data-theme]):not(.dark)'));
|
|
assert.ok(bridge.includes('@theme inline'));
|
|
});
|
|
|
|
test('semantic foreground/background pairs and required controls have sufficient contrast', () => {
|
|
for (const [name, t] of Object.entries(themes)) {
|
|
for (const base of ['primary', 'secondary', 'muted', 'accent', 'card', 'popover', 'sidebar', 'sidebar-primary', 'sidebar-accent', 'destructive']) {
|
|
assert.ok(contrast(t[base], t[`${base}-foreground`]) >= 4.5, `${name}/${base}`);
|
|
}
|
|
assert.ok(contrast(t.background, t.foreground) >= 4.5, name);
|
|
for (const state of ['primary-hover', 'primary-active']) assert.ok(contrast(t[state], t['primary-foreground']) >= 4.5, `${name}/${state}`);
|
|
for (const fg of ['input', 'ring']) for (const bg of ['background', 'card']) assert.ok(contrast(t[fg], t[bg]) >= 3, `${name}/${fg}/${bg}`);
|
|
}
|
|
});
|
|
|
|
test('all eight SVG variants preserve the approved paths and reference colors', async () => {
|
|
const geometry = await json('tokens/geometry.json');
|
|
const logo = await json('tokens/logo.json');
|
|
for (const filename of await readdir(new URL('../assets/svg/', import.meta.url))) {
|
|
const svg = (await file(`assets/svg/${filename}`)).toString();
|
|
assert.equal((svg.match(/<path /g) ?? []).length, 2, filename);
|
|
assert.ok(!/<(?:image|script|foreignObject)\b/i.test(svg));
|
|
for (const shape of Object.values(geometry)) {
|
|
assert.ok(svg.includes(`d="${shape.d}"`), filename);
|
|
assert.ok(svg.includes(`transform="${shape.transform}"`), filename);
|
|
}
|
|
const expected = filename.startsWith('mono-') ? null : palette[logo.rim];
|
|
if (expected) assert.ok(svg.includes(expected));
|
|
}
|
|
assert.equal(palette[logo.rim], '#A47764');
|
|
});
|
|
|
|
test('PNG sizes and the PNG frames inside ICOs match the manifest', async () => {
|
|
const manifest = await json('assets/manifest.json');
|
|
for (const variant of Object.keys(manifest.variants)) for (const size of manifest.png_sizes) {
|
|
const png = await file(`assets/png/${variant}/endoleap-${size}.png`);
|
|
assert.equal(png.subarray(1, 4).toString(), 'PNG');
|
|
assert.equal(png.readUInt32BE(16), size); assert.equal(png.readUInt32BE(20), size);
|
|
}
|
|
for (const [name, variant] of [['favicon.ico', 'icon-dark'], ['favicon-transparent.ico', 'logo-light'], ['favicon-light.ico', 'icon-light']]) {
|
|
const ico = await file(`assets/icons/${name}`);
|
|
assert.equal(ico.readUInt16LE(0), 0); assert.equal(ico.readUInt16LE(2), 1);
|
|
assert.equal(ico.readUInt16LE(4), manifest.ico_sizes.length);
|
|
for (const [i, size] of manifest.ico_sizes.entries()) {
|
|
const p = 6 + i * 16;
|
|
assert.equal(ico[p] || 256, size); assert.equal(ico[p + 1] || 256, size);
|
|
const length = ico.readUInt32LE(p + 8), offset = ico.readUInt32LE(p + 12);
|
|
assert.deepEqual(ico.subarray(offset, offset + length), await file(`assets/png/${variant}/endoleap-${size}.png`));
|
|
}
|
|
}
|
|
});
|