Skip to content

Commit 9b9160b

Browse files
test(theme-neon): add unit tests with WCAG contrast validation for neon theme
1 parent 29dd950 commit 9b9160b

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

lib/svg/themes/neon.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { themes } from '../themes';
3+
import { generateSVG } from '../generator';
4+
import type { BadgeParams, ContributionCalendar, StreakStats } from '../../../types';
5+
import { contrastRatio } from './test-utils';
6+
7+
describe('neon theme', () => {
8+
it('exists as a key in the themes object', () => {
9+
expect(themes).toHaveProperty('neon');
10+
});
11+
12+
it('has valid 6-digit hex color strings (without #) for bg, text, and accent', () => {
13+
const hexRegex = /^[0-9a-fA-F]{6}$/;
14+
15+
expect(themes.neon.bg).toMatch(hexRegex);
16+
expect(themes.neon.text).toMatch(hexRegex);
17+
expect(themes.neon.accent).toMatch(hexRegex);
18+
});
19+
20+
it('contains the specific neon hex colors in generated SVG output', () => {
21+
const mockStats: StreakStats = {
22+
currentStreak: 5,
23+
longestStreak: 10,
24+
totalContributions: 100,
25+
todayDate: '2024-06-12',
26+
};
27+
const mockCalendar: ContributionCalendar = {
28+
totalContributions: 10,
29+
weeks: [
30+
{
31+
contributionDays: [
32+
{ contributionCount: 5, date: '2024-06-11' },
33+
{ contributionCount: 5, date: '2024-06-12' },
34+
],
35+
},
36+
],
37+
};
38+
const neonParams: BadgeParams = {
39+
user: 'testuser',
40+
bg: themes.neon.bg,
41+
text: themes.neon.text,
42+
accent: themes.neon.accent,
43+
speed: '8s',
44+
scale: 'linear',
45+
};
46+
47+
const svg = generateSVG(mockStats, neonParams, mockCalendar);
48+
49+
expect(svg).toContain(`#${themes.neon.bg}`);
50+
expect(svg).toContain(`#${themes.neon.text}`);
51+
expect(svg).toContain(`#${themes.neon.accent}`);
52+
});
53+
54+
it('matches the defined neon color values for the design spec', () => {
55+
expect(themes.neon.bg).toBe('000000');
56+
expect(themes.neon.text).toBe('00ffcc');
57+
expect(themes.neon.accent).toBe('ff00ff');
58+
});
59+
60+
it('provides sufficient WCAG AA contrast between background and text', () => {
61+
const ratio = contrastRatio(themes.neon.bg, themes.neon.text);
62+
expect(ratio).toBeGreaterThanOrEqual(4.5);
63+
});
64+
});

lib/svg/themes/test-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
2+
const normalized = hex.replace(/^#/, '');
3+
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) {
4+
throw new Error(
5+
`Invalid hex color: "${hex}". Expected 6-digit hex string (with or without #).`
6+
);
7+
}
8+
return {
9+
r: parseInt(normalized.slice(0, 2), 16),
10+
g: parseInt(normalized.slice(2, 4), 16),
11+
b: parseInt(normalized.slice(4, 6), 16),
12+
};
13+
}
14+
15+
export function relativeLuminance(hex: string): number {
16+
const { r, g, b } = hexToRgb(hex);
17+
const [rl, gl, bl] = [r, g, b].map((c) => {
18+
const s = c / 255;
19+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
20+
});
21+
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
22+
}
23+
24+
export function contrastRatio(bg: string, text: string): number {
25+
const lBg = relativeLuminance(bg);
26+
const lText = relativeLuminance(text);
27+
const lighter = Math.max(lBg, lText);
28+
const darker = Math.min(lBg, lText);
29+
return (lighter + 0.05) / (darker + 0.05);
30+
}

0 commit comments

Comments
 (0)