101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import { THEME_PRESETS, contrastRatio } from '../config/theme';
|
||
|
||
/**
|
||
* The passepartout frame prints the EXIF line on the mat, in --mat-text on
|
||
* --mat-bg. Until v0.20.0 the mat never showed — the photo covered it, and the
|
||
* line sat on the photo at 1.3–2.4:1 — so its colours had never been checked.
|
||
*
|
||
* Tokens are resolved the way the cascade does on <html>, where every block
|
||
* of app/tokens.css matches at once: a preset's light block beats everything,
|
||
* then the later of the one-attribute blocks wins — and every preset block
|
||
* comes after the generic light block, so a preset that sets a mat token for
|
||
* dark mode only would carry it into light mode too.
|
||
*/
|
||
const css = fs.readFileSync(path.join(process.cwd(), 'app', 'tokens.css'), 'utf8');
|
||
|
||
function block(selector: string): string | undefined {
|
||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
return new RegExp(`(?:^|\\n)${escaped}\\s*\\{([^}]*)\\}`).exec(css)?.[1];
|
||
}
|
||
|
||
function declared(selector: string, name: string): string | undefined {
|
||
const body = block(selector);
|
||
if (!body) return undefined;
|
||
return new RegExp(`--${name}:\\s*([^;]+);`).exec(body)?.[1].trim();
|
||
}
|
||
|
||
function token(preset: string, mode: 'dark' | 'light', name: string): string | undefined {
|
||
const chain =
|
||
mode === 'dark'
|
||
? [`[data-preset='${preset}']`, ':root']
|
||
: [
|
||
`[data-preset='${preset}'][data-theme='light']`,
|
||
`[data-preset='${preset}']`,
|
||
"[data-theme='light']",
|
||
':root',
|
||
];
|
||
for (const selector of chain) {
|
||
const value = declared(selector, name);
|
||
if (value === undefined) continue;
|
||
const ref = /^var\(--([\w-]+)\)$/.exec(value);
|
||
return ref ? token(preset, mode, ref[1]) : value;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
// WCAG AA for text at the caption's size (0.6875–0.75rem).
|
||
const TEXT_MIN = 4.5;
|
||
|
||
describe('passepartout mat tokens', () => {
|
||
it('are defined for both modes', () => {
|
||
expect(declared(':root', 'mat-bg')).toBeDefined();
|
||
expect(declared("[data-theme='light']", 'mat-bg')).toBeDefined();
|
||
});
|
||
|
||
it('are no longer hard-coded in the theme files', () => {
|
||
const themes = path.join(process.cwd(), 'app', 'themes');
|
||
const globals = fs.readFileSync(path.join(process.cwd(), 'app', 'globals.css'), 'utf8');
|
||
const sources = [
|
||
globals,
|
||
...fs.readdirSync(themes).map((f) => fs.readFileSync(path.join(themes, f), 'utf8')),
|
||
];
|
||
for (const source of sources) {
|
||
for (const rule of source.matchAll(/\[data-photo-frame='passepartout'\][^{]*\{([^}]*)\}/g)) {
|
||
expect(rule[1]).not.toMatch(/(background|border|color)[^;]*#[0-9a-f]{3,6}/i);
|
||
}
|
||
}
|
||
});
|
||
|
||
for (const preset of Object.keys(THEME_PRESETS)) {
|
||
for (const mode of ['dark', 'light'] as const) {
|
||
it(`${preset} (${mode}): the EXIF line is readable on the mat`, () => {
|
||
const bg = token(preset, mode, 'mat-bg')!;
|
||
const fg = token(preset, mode, 'mat-text')!;
|
||
expect(bg).toMatch(/^#[0-9a-f]{6}$/i);
|
||
expect(fg).toMatch(/^#[0-9a-f]{6}$/i);
|
||
expect(contrastRatio(fg, bg)!).toBeGreaterThanOrEqual(TEXT_MIN);
|
||
});
|
||
}
|
||
}
|
||
|
||
it('keeps the light mat light in a preset that tints it', () => {
|
||
// classic sets a warm dark mat; without its own light value that dark
|
||
// mat would win over the generic light one.
|
||
expect(token('classic', 'light', 'mat-bg')).toBe('#fffdf5');
|
||
expect(token('classic', 'dark', 'mat-bg')).toBe('#1a1816');
|
||
});
|
||
|
||
it('tones the mat to the wall in the gallery presets', () => {
|
||
// kunsthalle hangs on grey, salon on oxblood and plaster rose; the default
|
||
// black and white boards cut a hole in either wall.
|
||
for (const preset of ['kunsthalle', 'salon']) {
|
||
expect(token(preset, 'dark', 'mat-bg')).not.toBe(token('studio', 'dark', 'mat-bg'));
|
||
expect(token(preset, 'light', 'mat-bg')).not.toBe(token('studio', 'light', 'mat-bg'));
|
||
expect(token(preset, 'dark', 'mat-bg')).not.toBe(token(preset, 'light', 'mat-bg'));
|
||
}
|
||
});
|
||
});
|