ui-theme/dist/__tests__/theme-contrast.test.js
Natalie aaf23fa33f feat(@cocotte/ui-theme): extract UI theme package to @ct/@packages
Re-scoped from @lilith/ui-theme to @cocotte/ui-theme. In-set cross-package deps
re-pointed to @cocotte; out-of-set @lilith deps preserved (same Verdaccio).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:04:11 -04:00

177 lines
No EOL
5.7 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { contrastRatio, isOpaqueHexColor } from '../utils/contrast';
import { cyberpunkAdapter } from '../adapters/cyberpunk-adapter';
import { synthwaveAdapter } from '../adapters/synthwave-adapter';
import { synthwaveLightAdapter } from '../adapters/synthwave-light-adapter';
import { luxeAdapter } from '../adapters/luxe-adapter';
import { lilithAdapter } from '../adapters/lilith-adapter';
import { neutralThemeAdapter } from '../adapters/neutral-adapter';
import { corporateThemeAdapter } from '../adapters/corporate-adapter';
import { creatorPortalThemeAdapter } from '../adapters/creator-portal-adapter';
import { pitchDeckAdapter, pitchDeckLightAdapter } from '../adapters/pitch-deck-adapter';
// ---------------------------------------------------------------------------
// Theme registry — all 10 adapters
// ---------------------------------------------------------------------------
const ALL_THEMES = [
['cyberpunk', cyberpunkAdapter],
['synthwave', synthwaveAdapter],
['synthwave-light', synthwaveLightAdapter],
['luxe', luxeAdapter],
['lilith', lilithAdapter],
['neutral', neutralThemeAdapter],
['corporate', corporateThemeAdapter],
['creator-portal', creatorPortalThemeAdapter],
['pitch-deck', pitchDeckAdapter],
['pitch-deck-light', pitchDeckLightAdapter],
];
/**
* Text on backgrounds — WCAG AA normal text (4.5:1)
*/
const TEXT_ON_BACKGROUNDS = [
{
name: 'text.primary on background.primary',
fg: (t) => t.colors.text.primary,
bg: (t) => t.colors.background.primary,
min: 4.5,
},
{
name: 'text.primary on background.secondary',
fg: (t) => t.colors.text.primary,
bg: (t) => t.colors.background.secondary,
min: 4.5,
},
{
name: 'text.primary on background.tertiary',
fg: (t) => t.colors.text.primary,
bg: (t) => t.colors.background.tertiary,
min: 4.5,
},
{
name: 'text.secondary on background.primary',
fg: (t) => t.colors.text.secondary,
bg: (t) => t.colors.background.primary,
min: 4.5,
},
{
name: 'text.secondary on background.secondary',
fg: (t) => t.colors.text.secondary,
bg: (t) => t.colors.background.secondary,
min: 4.5,
},
{
name: 'text.tertiary on background.primary',
fg: (t) => t.colors.text.tertiary,
bg: (t) => t.colors.background.primary,
min: 4.5,
},
{
name: 'text.primary on surface',
fg: (t) => t.colors.text.primary,
bg: (t) => t.colors.surface,
min: 4.5,
},
];
/**
* Semantic status colors — WCAG AA normal text (4.5:1)
*/
const SEMANTIC_STATUS = [
{
name: 'success.text on success.background',
fg: (t) => t.colors.success.text,
bg: (t) => t.colors.success.background,
min: 4.5,
},
{
name: 'warning.text on warning.background',
fg: (t) => t.colors.warning.text,
bg: (t) => t.colors.warning.background,
min: 4.5,
},
{
name: 'error.text on error.background',
fg: (t) => t.colors.error.text,
bg: (t) => t.colors.error.background,
min: 4.5,
},
{
name: 'info.text on info.background',
fg: (t) => t.colors.info.text,
bg: (t) => t.colors.info.background,
min: 4.5,
},
];
/**
* Interactive UI elements — WCAG AA-large (3:1)
*/
const UI_ELEMENTS = [
{
name: 'primary.main on background.primary',
fg: (t) => t.colors.primary.main,
bg: (t) => t.colors.background.primary,
min: 3.0,
},
{
name: 'secondary.main on background.primary',
fg: (t) => t.colors.secondary.main,
bg: (t) => t.colors.background.primary,
min: 3.0,
},
{
name: 'border.default on background.primary',
fg: (t) => t.colors.border.default,
bg: (t) => t.colors.background.primary,
min: 3.0,
},
{
name: 'disabled.text on disabled.background',
fg: (t) => t.colors.disabled.text,
bg: (t) => t.colors.disabled.background,
min: 3.0,
},
];
/**
* Button labels — white text on accent backgrounds (3:1)
*/
const BUTTON_LABELS = [
{
name: '#ffffff on primary.main',
fg: () => '#ffffff',
bg: (t) => t.colors.primary.main,
min: 3.0,
},
{
name: '#ffffff on error.main',
fg: () => '#ffffff',
bg: (t) => t.colors.error.main,
min: 3.0,
},
{
name: '#ffffff on success.main',
fg: () => '#ffffff',
bg: (t) => t.colors.success.main,
min: 3.0,
},
];
const ALL_COLOR_PAIRS = [
...TEXT_ON_BACKGROUNDS,
...SEMANTIC_STATUS,
...UI_ELEMENTS,
...BUTTON_LABELS,
];
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe.each(ALL_THEMES)('Theme: %s', (themeName, theme) => {
it.each(ALL_COLOR_PAIRS)('$name meets contrast ratio $min:1', ({ fg, bg, min }) => {
const fgColor = fg(theme);
const bgColor = bg(theme);
// Skip non-hex or alpha-bearing hex values (e.g. #ff006e15 semantic backgrounds).
// Alpha hex requires compositing against a known base — can't compute contrast in isolation.
if (!isOpaqueHexColor(fgColor) || !isOpaqueHexColor(bgColor)) {
return;
}
const ratio = contrastRatio(fgColor, bgColor);
expect(ratio, `${themeName}: ratio ${ratio.toFixed(2)}:1 (need ${min}:1) — fg: ${fgColor}, bg: ${bgColor}`).toBeGreaterThanOrEqual(min);
});
});
//# sourceMappingURL=theme-contrast.test.js.map