Skip to content

Commit 69b977b

Browse files
Copilothotlong
andcommitted
test: add comprehensive tests for Q1 2026 features
- WCAG contrast checking (contrastRatio, meetsContrastLevel) - 10 tests - SchemaRenderer AriaProps injection - 6 tests - Spec-aligned i18n formatters (plural, date, number, locale) - 20 tests - Mobile breakpoints and responsive value resolution - 13 tests Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 03a1d6f commit 69b977b

4 files changed

Lines changed: 472 additions & 0 deletions

File tree

packages/core/src/theme/__tests__/ThemeEngine.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
mergeThemes,
2222
resolveThemeInheritance,
2323
resolveMode,
24+
contrastRatio,
25+
meetsContrastLevel,
2426
} from '../ThemeEngine';
2527

2628
// ============================================================================
@@ -604,3 +606,63 @@ describe('resolveMode', () => {
604606
window.matchMedia = original;
605607
});
606608
});
609+
610+
// ============================================================================
611+
// WCAG Contrast Checking (v2.0.7)
612+
// ============================================================================
613+
614+
describe('contrastRatio', () => {
615+
it('should return 21 for black and white', () => {
616+
expect(contrastRatio('#000000', '#ffffff')).toBeCloseTo(21, 0);
617+
});
618+
619+
it('should return 1 for identical colors', () => {
620+
expect(contrastRatio('#336699', '#336699')).toBeCloseTo(1, 1);
621+
});
622+
623+
it('should return null for invalid hex', () => {
624+
expect(contrastRatio('invalid', '#000000')).toBeNull();
625+
expect(contrastRatio('#000000', 'xyz')).toBeNull();
626+
});
627+
628+
it('should handle shorthand hex (#RGB)', () => {
629+
const ratio = contrastRatio('#000', '#fff');
630+
expect(ratio).toBeCloseTo(21, 0);
631+
});
632+
633+
it('should be order-independent', () => {
634+
const ratio1 = contrastRatio('#000000', '#336699');
635+
const ratio2 = contrastRatio('#336699', '#000000');
636+
expect(ratio1).toBe(ratio2);
637+
});
638+
});
639+
640+
describe('meetsContrastLevel', () => {
641+
it('should pass AA for black on white (normal text)', () => {
642+
expect(meetsContrastLevel('#000000', '#ffffff', 'AA')).toBe(true);
643+
});
644+
645+
it('should pass AAA for black on white (normal text)', () => {
646+
expect(meetsContrastLevel('#000000', '#ffffff', 'AAA')).toBe(true);
647+
});
648+
649+
it('should fail AA for similar grays', () => {
650+
// #777 on #999 gives ~1.6:1 ratio
651+
expect(meetsContrastLevel('#777777', '#999999', 'AA')).toBe(false);
652+
});
653+
654+
it('should use lower threshold for large text (AA)', () => {
655+
// #767676 on white gives ~4.54:1 (passes AA normal, passes AA large)
656+
expect(meetsContrastLevel('#767676', '#ffffff', 'AA', false)).toBe(true);
657+
expect(meetsContrastLevel('#767676', '#ffffff', 'AA', true)).toBe(true);
658+
});
659+
660+
it('should use lower threshold for large text (AAA)', () => {
661+
// Black on white: 21:1 — passes both
662+
expect(meetsContrastLevel('#000000', '#ffffff', 'AAA', true)).toBe(true);
663+
});
664+
665+
it('should return false for invalid colors', () => {
666+
expect(meetsContrastLevel('invalid', '#ffffff', 'AA')).toBe(false);
667+
});
668+
});
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
resolvePlural,
4+
formatDateSpec,
5+
formatNumberSpec,
6+
applyLocaleConfig,
7+
type SpecPluralRule,
8+
} from '../utils/spec-formatters';
9+
10+
// ============================================================================
11+
// PluralRuleSchema Consumer Tests
12+
// ============================================================================
13+
14+
describe('resolvePlural', () => {
15+
const rule: SpecPluralRule = {
16+
key: 'items.count',
17+
zero: 'No items',
18+
one: '{count} item',
19+
other: '{count} items',
20+
};
21+
22+
it('should resolve zero form', () => {
23+
// English 'other' is used for 0 by Intl.PluralRules, but 'zero' is explicitly checked
24+
// In English, 0 maps to "other" category, but we fall back to rule.other
25+
const result = resolvePlural(rule, 0, 'en');
26+
expect(result).toBe('0 items');
27+
});
28+
29+
it('should resolve one form', () => {
30+
expect(resolvePlural(rule, 1, 'en')).toBe('1 item');
31+
});
32+
33+
it('should resolve other form for plural', () => {
34+
expect(resolvePlural(rule, 5, 'en')).toBe('5 items');
35+
});
36+
37+
it('should fallback to other when form is not defined', () => {
38+
const simpleRule: SpecPluralRule = {
39+
key: 'items',
40+
other: '{count} things',
41+
};
42+
expect(resolvePlural(simpleRule, 3, 'en')).toBe('3 things');
43+
});
44+
45+
it('should work with different locales', () => {
46+
const ruleWithTwo: SpecPluralRule = {
47+
key: 'items',
48+
one: '{count} element',
49+
two: '{count} éléments (dual)',
50+
other: '{count} éléments',
51+
};
52+
// Arabic has a 'two' plural category
53+
expect(resolvePlural(ruleWithTwo, 2, 'ar')).toBe('2 éléments (dual)');
54+
});
55+
});
56+
57+
// ============================================================================
58+
// DateFormatSchema Consumer Tests
59+
// ============================================================================
60+
61+
describe('formatDateSpec', () => {
62+
it('should format date with dateStyle', () => {
63+
const date = new Date('2026-02-11T12:00:00Z');
64+
const result = formatDateSpec(date, { dateStyle: 'short' }, 'en-US');
65+
expect(result).toBeTruthy();
66+
expect(typeof result).toBe('string');
67+
});
68+
69+
it('should return string for invalid date', () => {
70+
expect(formatDateSpec('invalid-date', { dateStyle: 'medium' })).toBe('invalid-date');
71+
});
72+
73+
it('should handle timeZone', () => {
74+
const date = new Date('2026-02-11T12:00:00Z');
75+
const result = formatDateSpec(date, {
76+
dateStyle: 'medium',
77+
timeZone: 'America/New_York',
78+
}, 'en-US');
79+
expect(result).toBeTruthy();
80+
});
81+
82+
it('should handle hour12 option', () => {
83+
const date = new Date('2026-02-11T15:30:00Z');
84+
const result12 = formatDateSpec(date, { timeStyle: 'short', hour12: true }, 'en-US');
85+
const result24 = formatDateSpec(date, { timeStyle: 'short', hour12: false }, 'en-US');
86+
expect(result12).toBeTruthy();
87+
expect(result24).toBeTruthy();
88+
});
89+
});
90+
91+
// ============================================================================
92+
// NumberFormatSchema Consumer Tests
93+
// ============================================================================
94+
95+
describe('formatNumberSpec', () => {
96+
it('should format decimal numbers', () => {
97+
const result = formatNumberSpec(1234.56, { style: 'decimal' }, 'en-US');
98+
expect(result).toContain('1,234');
99+
});
100+
101+
it('should format currency', () => {
102+
const result = formatNumberSpec(42.99, {
103+
style: 'currency',
104+
currency: 'USD',
105+
}, 'en-US');
106+
expect(result).toContain('$');
107+
expect(result).toContain('42.99');
108+
});
109+
110+
it('should format percentage', () => {
111+
const result = formatNumberSpec(0.75, { style: 'percent' }, 'en-US');
112+
expect(result).toContain('75');
113+
expect(result).toContain('%');
114+
});
115+
116+
it('should respect fraction digits', () => {
117+
const result = formatNumberSpec(3.14159, {
118+
style: 'decimal',
119+
maximumFractionDigits: 2,
120+
}, 'en-US');
121+
expect(result).toBe('3.14');
122+
});
123+
124+
it('should respect useGrouping', () => {
125+
const withGrouping = formatNumberSpec(1234567, {
126+
style: 'decimal',
127+
useGrouping: true,
128+
}, 'en-US');
129+
const withoutGrouping = formatNumberSpec(1234567, {
130+
style: 'decimal',
131+
useGrouping: false,
132+
}, 'en-US');
133+
expect(withGrouping).toContain(',');
134+
expect(withoutGrouping).not.toContain(',');
135+
});
136+
});
137+
138+
// ============================================================================
139+
// LocaleConfigSchema Consumer Tests
140+
// ============================================================================
141+
142+
describe('applyLocaleConfig', () => {
143+
it('should create bound formatting functions', () => {
144+
const locale = applyLocaleConfig({
145+
code: 'en-US',
146+
direction: 'ltr',
147+
numberFormat: { style: 'decimal', useGrouping: true },
148+
dateFormat: { dateStyle: 'medium' },
149+
});
150+
151+
expect(locale.code).toBe('en-US');
152+
expect(locale.direction).toBe('ltr');
153+
expect(locale.fallbackChain).toEqual([]);
154+
});
155+
156+
it('should format dates using configured locale', () => {
157+
const locale = applyLocaleConfig({
158+
code: 'en-US',
159+
dateFormat: { dateStyle: 'short' },
160+
});
161+
162+
const result = locale.formatDate(new Date('2026-02-11'));
163+
expect(result).toBeTruthy();
164+
});
165+
166+
it('should format numbers using configured locale', () => {
167+
const locale = applyLocaleConfig({
168+
code: 'en-US',
169+
numberFormat: { style: 'decimal', maximumFractionDigits: 2 },
170+
});
171+
172+
const result = locale.formatNumber(1234.5678);
173+
expect(result).toBeTruthy();
174+
});
175+
176+
it('should allow overrides in formatting calls', () => {
177+
const locale = applyLocaleConfig({
178+
code: 'en-US',
179+
numberFormat: { style: 'decimal' },
180+
});
181+
182+
const result = locale.formatNumber(42.99, {
183+
style: 'currency',
184+
currency: 'EUR',
185+
});
186+
expect(result).toContain('€');
187+
});
188+
189+
it('should resolve plurals with configured locale', () => {
190+
const locale = applyLocaleConfig({
191+
code: 'en',
192+
});
193+
194+
const rule: SpecPluralRule = {
195+
key: 'items',
196+
one: '{count} item',
197+
other: '{count} items',
198+
};
199+
200+
expect(locale.resolvePlural(rule, 1)).toBe('1 item');
201+
expect(locale.resolvePlural(rule, 5)).toBe('5 items');
202+
});
203+
204+
it('should default direction to ltr', () => {
205+
const locale = applyLocaleConfig({ code: 'en' });
206+
expect(locale.direction).toBe('ltr');
207+
});
208+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect } from 'vitest';
10+
import {
11+
BREAKPOINTS,
12+
BREAKPOINT_ORDER,
13+
resolveResponsiveValue,
14+
getCurrentBreakpoint,
15+
} from '../breakpoints';
16+
17+
describe('breakpoints', () => {
18+
describe('BREAKPOINTS', () => {
19+
it('should define Tailwind-compatible breakpoint values', () => {
20+
expect(BREAKPOINTS.xs).toBe(0);
21+
expect(BREAKPOINTS.sm).toBe(640);
22+
expect(BREAKPOINTS.md).toBe(768);
23+
expect(BREAKPOINTS.lg).toBe(1024);
24+
expect(BREAKPOINTS.xl).toBe(1280);
25+
expect(BREAKPOINTS['2xl']).toBe(1536);
26+
});
27+
});
28+
29+
describe('BREAKPOINT_ORDER', () => {
30+
it('should be ordered from smallest to largest', () => {
31+
expect(BREAKPOINT_ORDER).toEqual(['xs', 'sm', 'md', 'lg', 'xl', '2xl']);
32+
});
33+
});
34+
35+
describe('resolveResponsiveValue', () => {
36+
it('should return direct value for non-object values', () => {
37+
expect(resolveResponsiveValue(42, 'md')).toBe(42);
38+
expect(resolveResponsiveValue('hello', 'lg')).toBe('hello');
39+
});
40+
41+
it('should return value for exact breakpoint', () => {
42+
expect(resolveResponsiveValue({ xs: 1, md: 2, lg: 3 }, 'md')).toBe(2);
43+
});
44+
45+
it('should fall back to smaller breakpoint', () => {
46+
expect(resolveResponsiveValue({ xs: 1, lg: 3 }, 'md')).toBe(1);
47+
});
48+
49+
it('should return undefined when no breakpoint matches', () => {
50+
expect(resolveResponsiveValue({ lg: 3 }, 'sm')).toBeUndefined();
51+
});
52+
53+
it('should handle null values', () => {
54+
expect(resolveResponsiveValue(null as any, 'md')).toBeNull();
55+
});
56+
});
57+
58+
describe('getCurrentBreakpoint', () => {
59+
it('should return xs for small widths', () => {
60+
expect(getCurrentBreakpoint(320)).toBe('xs');
61+
});
62+
63+
it('should return sm for 640px', () => {
64+
expect(getCurrentBreakpoint(640)).toBe('sm');
65+
});
66+
67+
it('should return md for 768px', () => {
68+
expect(getCurrentBreakpoint(768)).toBe('md');
69+
});
70+
71+
it('should return lg for 1024px', () => {
72+
expect(getCurrentBreakpoint(1024)).toBe('lg');
73+
});
74+
75+
it('should return xl for 1280px', () => {
76+
expect(getCurrentBreakpoint(1280)).toBe('xl');
77+
});
78+
79+
it('should return 2xl for 1536px+', () => {
80+
expect(getCurrentBreakpoint(1536)).toBe('2xl');
81+
expect(getCurrentBreakpoint(2000)).toBe('2xl');
82+
});
83+
});
84+
});

0 commit comments

Comments
 (0)