@mseep/dembrandt 0.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +532 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/browser.d.ts +16 -0
- package/dist/lib/browser.js +27 -0
- package/dist/lib/browser.js.map +1 -0
- package/dist/lib/colors.d.ts +101 -0
- package/dist/lib/colors.js +405 -0
- package/dist/lib/colors.js.map +1 -0
- package/dist/lib/compare.d.ts +31 -0
- package/dist/lib/compare.js +46 -0
- package/dist/lib/compare.js.map +1 -0
- package/dist/lib/discovery.d.ts +31 -0
- package/dist/lib/discovery.js +243 -0
- package/dist/lib/discovery.js.map +1 -0
- package/dist/lib/drift.d.ts +64 -0
- package/dist/lib/drift.js +383 -0
- package/dist/lib/drift.js.map +1 -0
- package/dist/lib/dtcg/validate.d.ts +51 -0
- package/dist/lib/dtcg/validate.js +1403 -0
- package/dist/lib/dtcg/validate.js.map +1 -0
- package/dist/lib/exit-codes.d.ts +29 -0
- package/dist/lib/exit-codes.js +26 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/lib/extractors/breakpoints.d.ts +5 -0
- package/dist/lib/extractors/breakpoints.js +450 -0
- package/dist/lib/extractors/breakpoints.js.map +1 -0
- package/dist/lib/extractors/colors.d.ts +2 -0
- package/dist/lib/extractors/colors.js +657 -0
- package/dist/lib/extractors/colors.js.map +1 -0
- package/dist/lib/extractors/components.d.ts +4 -0
- package/dist/lib/extractors/components.js +370 -0
- package/dist/lib/extractors/components.js.map +1 -0
- package/dist/lib/extractors/index.d.ts +9 -0
- package/dist/lib/extractors/index.js +1257 -0
- package/dist/lib/extractors/index.js.map +1 -0
- package/dist/lib/extractors/logo.d.ts +2 -0
- package/dist/lib/extractors/logo.js +626 -0
- package/dist/lib/extractors/logo.js.map +1 -0
- package/dist/lib/extractors/spacing.d.ts +4 -0
- package/dist/lib/extractors/spacing.js +163 -0
- package/dist/lib/extractors/spacing.js.map +1 -0
- package/dist/lib/extractors/teach.d.ts +1 -0
- package/dist/lib/extractors/teach.js +66 -0
- package/dist/lib/extractors/teach.js.map +1 -0
- package/dist/lib/extractors/typography.d.ts +1 -0
- package/dist/lib/extractors/typography.js +163 -0
- package/dist/lib/extractors/typography.js.map +1 -0
- package/dist/lib/findings.d.ts +34 -0
- package/dist/lib/findings.js +166 -0
- package/dist/lib/findings.js.map +1 -0
- package/dist/lib/formatters/dtcg.d.ts +10 -0
- package/dist/lib/formatters/dtcg.js +416 -0
- package/dist/lib/formatters/dtcg.js.map +1 -0
- package/dist/lib/formatters/html.d.ts +25 -0
- package/dist/lib/formatters/html.js +479 -0
- package/dist/lib/formatters/html.js.map +1 -0
- package/dist/lib/formatters/markdown.d.ts +5 -0
- package/dist/lib/formatters/markdown.js +568 -0
- package/dist/lib/formatters/markdown.js.map +1 -0
- package/dist/lib/formatters/pdf.d.ts +12 -0
- package/dist/lib/formatters/pdf.js +1121 -0
- package/dist/lib/formatters/pdf.js.map +1 -0
- package/dist/lib/formatters/terminal.d.ts +6 -0
- package/dist/lib/formatters/terminal.js +954 -0
- package/dist/lib/formatters/terminal.js.map +1 -0
- package/dist/lib/formatters/theme.d.ts +35 -0
- package/dist/lib/formatters/theme.js +37 -0
- package/dist/lib/formatters/theme.js.map +1 -0
- package/dist/lib/merger.d.ts +14 -0
- package/dist/lib/merger.js +362 -0
- package/dist/lib/merger.js.map +1 -0
- package/dist/lib/normalize.d.ts +29 -0
- package/dist/lib/normalize.js +59 -0
- package/dist/lib/normalize.js.map +1 -0
- package/dist/lib/robots.d.ts +12 -0
- package/dist/lib/robots.js +110 -0
- package/dist/lib/robots.js.map +1 -0
- package/dist/lib/run-summary.d.ts +40 -0
- package/dist/lib/run-summary.js +64 -0
- package/dist/lib/run-summary.js.map +1 -0
- package/dist/lib/types.d.ts +329 -0
- package/dist/lib/types.js +7 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/version.d.ts +134 -0
- package/dist/lib/version.js +153 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/mcp-server.d.ts +11 -0
- package/dist/mcp-server.js +311 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/package.json +106 -0
- package/dist/test/_vitest-shim.d.ts +13 -0
- package/dist/test/_vitest-shim.js +23 -0
- package/dist/test/_vitest-shim.js.map +1 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +24 -0
- package/dist/test/cli.test.js.map +1 -0
- package/dist/test/colors.test.d.ts +1 -0
- package/dist/test/colors.test.js +64 -0
- package/dist/test/colors.test.js.map +1 -0
- package/dist/test/compare.test.d.ts +1 -0
- package/dist/test/compare.test.js +57 -0
- package/dist/test/compare.test.js.map +1 -0
- package/dist/test/drift.test.d.ts +1 -0
- package/dist/test/drift.test.js +53 -0
- package/dist/test/drift.test.js.map +1 -0
- package/dist/test/dtcg-formatter.test.d.ts +1 -0
- package/dist/test/dtcg-formatter.test.js +48 -0
- package/dist/test/dtcg-formatter.test.js.map +1 -0
- package/dist/test/dtcg-validate.test.d.ts +1 -0
- package/dist/test/dtcg-validate.test.js +2129 -0
- package/dist/test/dtcg-validate.test.js.map +1 -0
- package/dist/test/exit-codes.test.d.ts +1 -0
- package/dist/test/exit-codes.test.js +53 -0
- package/dist/test/exit-codes.test.js.map +1 -0
- package/dist/test/findings.test.d.ts +1 -0
- package/dist/test/findings.test.js +77 -0
- package/dist/test/findings.test.js.map +1 -0
- package/dist/test/html.test.d.ts +1 -0
- package/dist/test/html.test.js +95 -0
- package/dist/test/html.test.js.map +1 -0
- package/dist/test/markdown.test.d.ts +1 -0
- package/dist/test/markdown.test.js +145 -0
- package/dist/test/markdown.test.js.map +1 -0
- package/dist/test/merger.test.d.ts +1 -0
- package/dist/test/merger.test.js +98 -0
- package/dist/test/merger.test.js.map +1 -0
- package/dist/test/normalize.test.d.ts +1 -0
- package/dist/test/normalize.test.js +47 -0
- package/dist/test/normalize.test.js.map +1 -0
- package/dist/test/run-summary.test.d.ts +1 -0
- package/dist/test/run-summary.test.js +45 -0
- package/dist/test/run-summary.test.js.map +1 -0
- package/dist/test/version.test.d.ts +1 -0
- package/dist/test/version.test.js +73 -0
- package/dist/test/version.test.js.map +1 -0
- package/package.json +106 -0
|
@@ -0,0 +1,2129 @@
|
|
|
1
|
+
import { describe, it, expect } from './_vitest-shim.js';
|
|
2
|
+
import { validateTokens, validateTokensObject } from '../lib/dtcg/validate.js';
|
|
3
|
+
describe('DTCG Validator - W3C Spec Compliant', () => {
|
|
4
|
+
describe('Basic validation', () => {
|
|
5
|
+
it('should reject empty input', () => {
|
|
6
|
+
const result = validateTokens('');
|
|
7
|
+
expect(result.valid).toBe(false);
|
|
8
|
+
expect(result.errors).toContain('Input is empty');
|
|
9
|
+
});
|
|
10
|
+
it('should reject null input', () => {
|
|
11
|
+
const result = validateTokens(null);
|
|
12
|
+
expect(result.valid).toBe(false);
|
|
13
|
+
expect(result.errors).toContain('Input is empty');
|
|
14
|
+
});
|
|
15
|
+
it('should reject invalid JSON', () => {
|
|
16
|
+
const result = validateTokens('{invalid json}');
|
|
17
|
+
expect(result.valid).toBe(false);
|
|
18
|
+
expect(result.errors[0]).toContain('Invalid JSON');
|
|
19
|
+
});
|
|
20
|
+
it('should reject non-object root', () => {
|
|
21
|
+
const result = validateTokens('[]');
|
|
22
|
+
expect(result.valid).toBe(false);
|
|
23
|
+
expect(result.errors).toContain('Root must be an object');
|
|
24
|
+
});
|
|
25
|
+
it('should accept valid empty object', () => {
|
|
26
|
+
const result = validateTokens('{}');
|
|
27
|
+
expect(result.valid).toBe(true);
|
|
28
|
+
expect(result.tokenCount).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('Color tokens', () => {
|
|
32
|
+
it('should reject a bare hex string color value (Color Module §4.1 requires an object)', () => {
|
|
33
|
+
const tokens = JSON.stringify({
|
|
34
|
+
color: {
|
|
35
|
+
primary: {
|
|
36
|
+
$type: 'color',
|
|
37
|
+
$value: '#ff0000'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const result = validateTokens(tokens);
|
|
42
|
+
expect(result.valid).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should accept a color object with optional 6-digit hex fallback', () => {
|
|
45
|
+
const tokens = JSON.stringify({
|
|
46
|
+
color: {
|
|
47
|
+
primary: {
|
|
48
|
+
$type: 'color',
|
|
49
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0], hex: '#ff0000' }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const result = validateTokens(tokens);
|
|
54
|
+
expect(result.valid).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('should reject a bare 8-digit hex string color value', () => {
|
|
57
|
+
const tokens = JSON.stringify({
|
|
58
|
+
color: {
|
|
59
|
+
primary: {
|
|
60
|
+
$type: 'color',
|
|
61
|
+
$value: '#ff0000aa'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const result = validateTokens(tokens);
|
|
66
|
+
expect(result.valid).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
describe('Color spaces - W3C Color Module 2025.10', () => {
|
|
69
|
+
it('should validate srgb color space', () => {
|
|
70
|
+
const tokens = JSON.stringify({
|
|
71
|
+
color: {
|
|
72
|
+
red: {
|
|
73
|
+
$type: 'color',
|
|
74
|
+
$value: {
|
|
75
|
+
colorSpace: 'srgb',
|
|
76
|
+
components: [1, 0, 0]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const result = validateTokens(tokens);
|
|
82
|
+
expect(result.valid).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it('should validate srgb-linear color space', () => {
|
|
85
|
+
const tokens = JSON.stringify({
|
|
86
|
+
color: {
|
|
87
|
+
red: {
|
|
88
|
+
$type: 'color',
|
|
89
|
+
$value: {
|
|
90
|
+
colorSpace: 'srgb-linear',
|
|
91
|
+
components: [1, 0, 0]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const result = validateTokens(tokens);
|
|
97
|
+
expect(result.valid).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('should validate hsl color space', () => {
|
|
100
|
+
const tokens = JSON.stringify({
|
|
101
|
+
color: {
|
|
102
|
+
blue: {
|
|
103
|
+
$type: 'color',
|
|
104
|
+
$value: {
|
|
105
|
+
colorSpace: 'hsl',
|
|
106
|
+
components: [240, 100, 50]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const result = validateTokens(tokens);
|
|
112
|
+
expect(result.valid).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('should validate hwb color space', () => {
|
|
115
|
+
const tokens = JSON.stringify({
|
|
116
|
+
color: {
|
|
117
|
+
red: {
|
|
118
|
+
$type: 'color',
|
|
119
|
+
$value: {
|
|
120
|
+
colorSpace: 'hwb',
|
|
121
|
+
components: [0, 0, 0]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const result = validateTokens(tokens);
|
|
127
|
+
expect(result.valid).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('should validate lab color space', () => {
|
|
130
|
+
const tokens = JSON.stringify({
|
|
131
|
+
color: {
|
|
132
|
+
white: {
|
|
133
|
+
$type: 'color',
|
|
134
|
+
$value: {
|
|
135
|
+
colorSpace: 'lab',
|
|
136
|
+
components: [100, 0, 0]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const result = validateTokens(tokens);
|
|
142
|
+
expect(result.valid).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
it('should validate lch color space', () => {
|
|
145
|
+
const tokens = JSON.stringify({
|
|
146
|
+
color: {
|
|
147
|
+
magenta: {
|
|
148
|
+
$type: 'color',
|
|
149
|
+
$value: {
|
|
150
|
+
colorSpace: 'lch',
|
|
151
|
+
components: [60, 100, 330]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const result = validateTokens(tokens);
|
|
157
|
+
expect(result.valid).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
it('should validate oklab color space', () => {
|
|
160
|
+
const tokens = JSON.stringify({
|
|
161
|
+
color: {
|
|
162
|
+
white: {
|
|
163
|
+
$type: 'color',
|
|
164
|
+
$value: {
|
|
165
|
+
colorSpace: 'oklab',
|
|
166
|
+
components: [1, 0, 0]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const result = validateTokens(tokens);
|
|
172
|
+
expect(result.valid).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('should validate oklch color space', () => {
|
|
175
|
+
const tokens = JSON.stringify({
|
|
176
|
+
color: {
|
|
177
|
+
magenta: {
|
|
178
|
+
$type: 'color',
|
|
179
|
+
$value: {
|
|
180
|
+
colorSpace: 'oklch',
|
|
181
|
+
components: [0.7, 0.3, 330]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const result = validateTokens(tokens);
|
|
187
|
+
expect(result.valid).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
it('should validate display-p3 color space', () => {
|
|
190
|
+
const tokens = JSON.stringify({
|
|
191
|
+
color: {
|
|
192
|
+
vibrantGreen: {
|
|
193
|
+
$type: 'color',
|
|
194
|
+
$value: {
|
|
195
|
+
colorSpace: 'display-p3',
|
|
196
|
+
components: [0, 1, 0]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const result = validateTokens(tokens);
|
|
202
|
+
expect(result.valid).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
it('should validate a98-rgb color space', () => {
|
|
205
|
+
const tokens = JSON.stringify({
|
|
206
|
+
color: {
|
|
207
|
+
red: {
|
|
208
|
+
$type: 'color',
|
|
209
|
+
$value: {
|
|
210
|
+
colorSpace: 'a98-rgb',
|
|
211
|
+
components: [1, 0, 0]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const result = validateTokens(tokens);
|
|
217
|
+
expect(result.valid).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
it('should validate prophoto-rgb color space', () => {
|
|
220
|
+
const tokens = JSON.stringify({
|
|
221
|
+
color: {
|
|
222
|
+
red: {
|
|
223
|
+
$type: 'color',
|
|
224
|
+
$value: {
|
|
225
|
+
colorSpace: 'prophoto-rgb',
|
|
226
|
+
components: [1, 0, 0]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
const result = validateTokens(tokens);
|
|
232
|
+
expect(result.valid).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
it('should validate rec2020 color space', () => {
|
|
235
|
+
const tokens = JSON.stringify({
|
|
236
|
+
color: {
|
|
237
|
+
red: {
|
|
238
|
+
$type: 'color',
|
|
239
|
+
$value: {
|
|
240
|
+
colorSpace: 'rec2020',
|
|
241
|
+
components: [1, 0, 0]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
const result = validateTokens(tokens);
|
|
247
|
+
expect(result.valid).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
it('should validate xyz-d65 color space', () => {
|
|
250
|
+
const tokens = JSON.stringify({
|
|
251
|
+
color: {
|
|
252
|
+
white: {
|
|
253
|
+
$type: 'color',
|
|
254
|
+
$value: {
|
|
255
|
+
colorSpace: 'xyz-d65',
|
|
256
|
+
components: [0.95, 1, 1.09]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const result = validateTokens(tokens);
|
|
262
|
+
expect(result.valid).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it('should validate xyz-d50 color space', () => {
|
|
265
|
+
const tokens = JSON.stringify({
|
|
266
|
+
color: {
|
|
267
|
+
white: {
|
|
268
|
+
$type: 'color',
|
|
269
|
+
$value: {
|
|
270
|
+
colorSpace: 'xyz-d50',
|
|
271
|
+
components: [0.96, 1, 0.82]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
const result = validateTokens(tokens);
|
|
277
|
+
expect(result.valid).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
it('should error on unsupported color space', () => {
|
|
280
|
+
const tokens = JSON.stringify({
|
|
281
|
+
color: {
|
|
282
|
+
red: {
|
|
283
|
+
$type: 'color',
|
|
284
|
+
$value: {
|
|
285
|
+
colorSpace: 'cmyk',
|
|
286
|
+
components: [0, 100, 100, 0]
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const result = validateTokens(tokens);
|
|
292
|
+
expect(result.valid).toBe(false);
|
|
293
|
+
expect(result.errors.some(e => e.includes('unsupported colorSpace'))).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('"none" keyword support', () => {
|
|
297
|
+
it('should validate color with "none" component', () => {
|
|
298
|
+
const tokens = JSON.stringify({
|
|
299
|
+
color: {
|
|
300
|
+
transparent: {
|
|
301
|
+
$type: 'color',
|
|
302
|
+
$value: {
|
|
303
|
+
colorSpace: 'srgb',
|
|
304
|
+
components: ['none', 0, 0]
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
const result = validateTokens(tokens);
|
|
310
|
+
expect(result.valid).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it('should validate color with multiple "none" components', () => {
|
|
313
|
+
const tokens = JSON.stringify({
|
|
314
|
+
color: {
|
|
315
|
+
transparent: {
|
|
316
|
+
$type: 'color',
|
|
317
|
+
$value: {
|
|
318
|
+
colorSpace: 'srgb',
|
|
319
|
+
components: ['none', 'none', 0]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
const result = validateTokens(tokens);
|
|
325
|
+
expect(result.valid).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
describe('Component range validation', () => {
|
|
329
|
+
it('should error on srgb component below 0', () => {
|
|
330
|
+
const tokens = JSON.stringify({
|
|
331
|
+
color: {
|
|
332
|
+
invalid: {
|
|
333
|
+
$type: 'color',
|
|
334
|
+
$value: {
|
|
335
|
+
colorSpace: 'srgb',
|
|
336
|
+
components: [-0.1, 0, 0]
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const result = validateTokens(tokens);
|
|
342
|
+
expect(result.valid).toBe(false);
|
|
343
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 1'))).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
it('should error on srgb component above 1', () => {
|
|
346
|
+
const tokens = JSON.stringify({
|
|
347
|
+
color: {
|
|
348
|
+
invalid: {
|
|
349
|
+
$type: 'color',
|
|
350
|
+
$value: {
|
|
351
|
+
colorSpace: 'srgb',
|
|
352
|
+
components: [1.1, 0, 0]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const result = validateTokens(tokens);
|
|
358
|
+
expect(result.valid).toBe(false);
|
|
359
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 1'))).toBe(true);
|
|
360
|
+
});
|
|
361
|
+
it('should error on hsl saturation below 0', () => {
|
|
362
|
+
const tokens = JSON.stringify({
|
|
363
|
+
color: {
|
|
364
|
+
invalid: {
|
|
365
|
+
$type: 'color',
|
|
366
|
+
$value: {
|
|
367
|
+
colorSpace: 'hsl',
|
|
368
|
+
components: [180, -5, 50]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
const result = validateTokens(tokens);
|
|
374
|
+
expect(result.valid).toBe(false);
|
|
375
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 100'))).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
it('should error on hsl lightness above 100', () => {
|
|
378
|
+
const tokens = JSON.stringify({
|
|
379
|
+
color: {
|
|
380
|
+
invalid: {
|
|
381
|
+
$type: 'color',
|
|
382
|
+
$value: {
|
|
383
|
+
colorSpace: 'hsl',
|
|
384
|
+
components: [180, 50, 105]
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
const result = validateTokens(tokens);
|
|
390
|
+
expect(result.valid).toBe(false);
|
|
391
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 100'))).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
it('should error on hue value at 360 (exclusive upper bound)', () => {
|
|
394
|
+
const tokens = JSON.stringify({
|
|
395
|
+
color: {
|
|
396
|
+
invalid: {
|
|
397
|
+
$type: 'color',
|
|
398
|
+
$value: {
|
|
399
|
+
colorSpace: 'hsl',
|
|
400
|
+
components: [360, 50, 50]
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
const result = validateTokens(tokens);
|
|
406
|
+
expect(result.valid).toBe(false);
|
|
407
|
+
expect(result.errors.some(e => e.includes('must be >= 0 and < 360'))).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
it('should validate hue value at 359.9', () => {
|
|
410
|
+
const tokens = JSON.stringify({
|
|
411
|
+
color: {
|
|
412
|
+
almostRed: {
|
|
413
|
+
$type: 'color',
|
|
414
|
+
$value: {
|
|
415
|
+
colorSpace: 'hsl',
|
|
416
|
+
components: [359.9, 100, 50]
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
const result = validateTokens(tokens);
|
|
422
|
+
expect(result.valid).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
it('should error on lab lightness below 0', () => {
|
|
425
|
+
const tokens = JSON.stringify({
|
|
426
|
+
color: {
|
|
427
|
+
invalid: {
|
|
428
|
+
$type: 'color',
|
|
429
|
+
$value: {
|
|
430
|
+
colorSpace: 'lab',
|
|
431
|
+
components: [-1, 0, 0]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
const result = validateTokens(tokens);
|
|
437
|
+
expect(result.valid).toBe(false);
|
|
438
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 100'))).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
it('should error on lab lightness above 100', () => {
|
|
441
|
+
const tokens = JSON.stringify({
|
|
442
|
+
color: {
|
|
443
|
+
invalid: {
|
|
444
|
+
$type: 'color',
|
|
445
|
+
$value: {
|
|
446
|
+
colorSpace: 'lab',
|
|
447
|
+
components: [101, 0, 0]
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
const result = validateTokens(tokens);
|
|
453
|
+
expect(result.valid).toBe(false);
|
|
454
|
+
expect(result.errors.some(e => e.includes('must be between 0 and 100'))).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
it('should error on lch chroma below 0', () => {
|
|
457
|
+
const tokens = JSON.stringify({
|
|
458
|
+
color: {
|
|
459
|
+
invalid: {
|
|
460
|
+
$type: 'color',
|
|
461
|
+
$value: {
|
|
462
|
+
colorSpace: 'lch',
|
|
463
|
+
components: [50, -1, 180]
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
const result = validateTokens(tokens);
|
|
469
|
+
expect(result.valid).toBe(false);
|
|
470
|
+
expect(result.errors.some(e => e.includes('must be >= 0'))).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
describe('Alpha channel validation', () => {
|
|
474
|
+
it('should validate alpha between 0 and 1', () => {
|
|
475
|
+
const tokens = JSON.stringify({
|
|
476
|
+
color: {
|
|
477
|
+
semiTransparent: {
|
|
478
|
+
$type: 'color',
|
|
479
|
+
$value: {
|
|
480
|
+
colorSpace: 'srgb',
|
|
481
|
+
components: [1, 0, 0],
|
|
482
|
+
alpha: 0.5
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
const result = validateTokens(tokens);
|
|
488
|
+
expect(result.valid).toBe(true);
|
|
489
|
+
});
|
|
490
|
+
it('should error on alpha below 0', () => {
|
|
491
|
+
const tokens = JSON.stringify({
|
|
492
|
+
color: {
|
|
493
|
+
invalid: {
|
|
494
|
+
$type: 'color',
|
|
495
|
+
$value: {
|
|
496
|
+
colorSpace: 'srgb',
|
|
497
|
+
components: [1, 0, 0],
|
|
498
|
+
alpha: -0.1
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
const result = validateTokens(tokens);
|
|
504
|
+
expect(result.valid).toBe(false);
|
|
505
|
+
expect(result.errors.some(e => e.includes('alpha property') && e.includes('must be between 0 and 1'))).toBe(true);
|
|
506
|
+
});
|
|
507
|
+
it('should error on alpha above 1', () => {
|
|
508
|
+
const tokens = JSON.stringify({
|
|
509
|
+
color: {
|
|
510
|
+
invalid: {
|
|
511
|
+
$type: 'color',
|
|
512
|
+
$value: {
|
|
513
|
+
colorSpace: 'srgb',
|
|
514
|
+
components: [1, 0, 0],
|
|
515
|
+
alpha: 1.5
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
const result = validateTokens(tokens);
|
|
521
|
+
expect(result.valid).toBe(false);
|
|
522
|
+
expect(result.errors.some(e => e.includes('alpha property') && e.includes('must be between 0 and 1'))).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
it('should error on non-numeric alpha', () => {
|
|
525
|
+
const tokens = JSON.stringify({
|
|
526
|
+
color: {
|
|
527
|
+
invalid: {
|
|
528
|
+
$type: 'color',
|
|
529
|
+
$value: {
|
|
530
|
+
colorSpace: 'srgb',
|
|
531
|
+
components: [1, 0, 0],
|
|
532
|
+
alpha: '0.5'
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
const result = validateTokens(tokens);
|
|
538
|
+
expect(result.valid).toBe(false);
|
|
539
|
+
expect(result.errors.some(e => e.includes('alpha property') && e.includes('must be a number'))).toBe(true);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
it('should error on missing colorSpace', () => {
|
|
543
|
+
const tokens = JSON.stringify({
|
|
544
|
+
color: {
|
|
545
|
+
primary: {
|
|
546
|
+
$type: 'color',
|
|
547
|
+
$value: {
|
|
548
|
+
components: [1, 0, 0]
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
const result = validateTokens(tokens);
|
|
554
|
+
expect(result.valid).toBe(false);
|
|
555
|
+
expect(result.errors).toContain('Color object at color.primary must have colorSpace property');
|
|
556
|
+
});
|
|
557
|
+
it('should error on invalid components array length', () => {
|
|
558
|
+
const tokens = JSON.stringify({
|
|
559
|
+
color: {
|
|
560
|
+
primary: {
|
|
561
|
+
$type: 'color',
|
|
562
|
+
$value: {
|
|
563
|
+
colorSpace: 'srgb',
|
|
564
|
+
components: [1, 0]
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
const result = validateTokens(tokens);
|
|
570
|
+
expect(result.valid).toBe(false);
|
|
571
|
+
expect(result.errors).toContain('Color object at color.primary must have components array with exactly 3 values');
|
|
572
|
+
});
|
|
573
|
+
it('should error on invalid component type', () => {
|
|
574
|
+
const tokens = JSON.stringify({
|
|
575
|
+
color: {
|
|
576
|
+
primary: {
|
|
577
|
+
$type: 'color',
|
|
578
|
+
$value: {
|
|
579
|
+
colorSpace: 'srgb',
|
|
580
|
+
components: ['red', 0, 0]
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
const result = validateTokens(tokens);
|
|
586
|
+
expect(result.valid).toBe(false);
|
|
587
|
+
expect(result.errors.some(e => e.includes('must be a number or "none"'))).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
describe('Dimension tokens', () => {
|
|
591
|
+
it('should reject a string dimension shorthand (§8.2 requires an object)', () => {
|
|
592
|
+
const tokens = JSON.stringify({
|
|
593
|
+
spacing: {
|
|
594
|
+
small: {
|
|
595
|
+
$type: 'dimension',
|
|
596
|
+
$value: '8px'
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
const result = validateTokens(tokens);
|
|
601
|
+
expect(result.valid).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
it('should reject a bare number dimension (§8.2 requires { value, unit })', () => {
|
|
604
|
+
const tokens = JSON.stringify({
|
|
605
|
+
spacing: {
|
|
606
|
+
small: {
|
|
607
|
+
$type: 'dimension',
|
|
608
|
+
$value: 8
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
const result = validateTokens(tokens);
|
|
613
|
+
expect(result.valid).toBe(false);
|
|
614
|
+
});
|
|
615
|
+
it('should validate dimension object format', () => {
|
|
616
|
+
const tokens = JSON.stringify({
|
|
617
|
+
spacing: {
|
|
618
|
+
small: {
|
|
619
|
+
$type: 'dimension',
|
|
620
|
+
$value: {
|
|
621
|
+
value: 8,
|
|
622
|
+
unit: 'px'
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
const result = validateTokens(tokens);
|
|
628
|
+
expect(result.valid).toBe(true);
|
|
629
|
+
});
|
|
630
|
+
it('should error on dimension with invalid unit (em)', () => {
|
|
631
|
+
const tokens = JSON.stringify({
|
|
632
|
+
spacing: {
|
|
633
|
+
small: {
|
|
634
|
+
$type: 'dimension',
|
|
635
|
+
$value: '8em'
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
const result = validateTokens(tokens);
|
|
640
|
+
expect(result.valid).toBe(false);
|
|
641
|
+
expect(result.errors.some(e => e.includes('"px" or "rem"'))).toBe(true);
|
|
642
|
+
});
|
|
643
|
+
it('should error on dimension object with invalid unit', () => {
|
|
644
|
+
const tokens = JSON.stringify({
|
|
645
|
+
spacing: {
|
|
646
|
+
small: {
|
|
647
|
+
$type: 'dimension',
|
|
648
|
+
$value: {
|
|
649
|
+
value: 8,
|
|
650
|
+
unit: 'em'
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
const result = validateTokens(tokens);
|
|
656
|
+
expect(result.valid).toBe(false);
|
|
657
|
+
expect(result.errors).toContain('Dimension unit at spacing.small must be "px" or "rem"');
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
describe('Font tokens', () => {
|
|
661
|
+
it('should validate fontFamily as string', () => {
|
|
662
|
+
const tokens = JSON.stringify({
|
|
663
|
+
font: {
|
|
664
|
+
body: {
|
|
665
|
+
$type: 'fontFamily',
|
|
666
|
+
$value: 'Arial'
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
const result = validateTokens(tokens);
|
|
671
|
+
expect(result.valid).toBe(true);
|
|
672
|
+
});
|
|
673
|
+
it('should validate fontFamily as array', () => {
|
|
674
|
+
const tokens = JSON.stringify({
|
|
675
|
+
font: {
|
|
676
|
+
body: {
|
|
677
|
+
$type: 'fontFamily',
|
|
678
|
+
$value: ['Arial', 'Helvetica', 'sans-serif']
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
const result = validateTokens(tokens);
|
|
683
|
+
expect(result.valid).toBe(true);
|
|
684
|
+
});
|
|
685
|
+
it('should validate fontWeight as number', () => {
|
|
686
|
+
const tokens = JSON.stringify({
|
|
687
|
+
font: {
|
|
688
|
+
bold: {
|
|
689
|
+
$type: 'fontWeight',
|
|
690
|
+
$value: 700
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
const result = validateTokens(tokens);
|
|
695
|
+
expect(result.valid).toBe(true);
|
|
696
|
+
});
|
|
697
|
+
it('should validate fontWeight string aliases', () => {
|
|
698
|
+
const aliases = ['bold', 'normal', 'light', 'black'];
|
|
699
|
+
aliases.forEach(alias => {
|
|
700
|
+
const tokens = JSON.stringify({
|
|
701
|
+
font: {
|
|
702
|
+
weight: {
|
|
703
|
+
$type: 'fontWeight',
|
|
704
|
+
$value: alias
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
const result = validateTokens(tokens);
|
|
709
|
+
expect(result.valid).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
it('should error on invalid fontWeight string', () => {
|
|
713
|
+
const tokens = JSON.stringify({
|
|
714
|
+
font: {
|
|
715
|
+
weight: {
|
|
716
|
+
$type: 'fontWeight',
|
|
717
|
+
$value: 'super-bold'
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
const result = validateTokens(tokens);
|
|
722
|
+
expect(result.valid).toBe(false);
|
|
723
|
+
expect(result.errors.some(e => e.includes('valid weight alias'))).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
it('should error on fontWeight below 1', () => {
|
|
726
|
+
const tokens = JSON.stringify({
|
|
727
|
+
font: {
|
|
728
|
+
invalid: {
|
|
729
|
+
$type: 'fontWeight',
|
|
730
|
+
$value: 0
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
const result = validateTokens(tokens);
|
|
735
|
+
expect(result.valid).toBe(false);
|
|
736
|
+
expect(result.errors).toContain('fontWeight at font.invalid must be a number between 1-1000');
|
|
737
|
+
});
|
|
738
|
+
it('should error on fontWeight above 1000', () => {
|
|
739
|
+
const tokens = JSON.stringify({
|
|
740
|
+
font: {
|
|
741
|
+
invalid: {
|
|
742
|
+
$type: 'fontWeight',
|
|
743
|
+
$value: 1001
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
const result = validateTokens(tokens);
|
|
748
|
+
expect(result.valid).toBe(false);
|
|
749
|
+
expect(result.errors).toContain('fontWeight at font.invalid must be a number between 1-1000');
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
describe('Duration tokens', () => {
|
|
753
|
+
it('should validate duration with ms unit', () => {
|
|
754
|
+
const tokens = JSON.stringify({
|
|
755
|
+
duration: {
|
|
756
|
+
quick: {
|
|
757
|
+
$type: 'duration',
|
|
758
|
+
$value: {
|
|
759
|
+
value: 100,
|
|
760
|
+
unit: 'ms'
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
const result = validateTokens(tokens);
|
|
766
|
+
expect(result.valid).toBe(true);
|
|
767
|
+
});
|
|
768
|
+
it('should validate duration with s unit', () => {
|
|
769
|
+
const tokens = JSON.stringify({
|
|
770
|
+
duration: {
|
|
771
|
+
slow: {
|
|
772
|
+
$type: 'duration',
|
|
773
|
+
$value: {
|
|
774
|
+
value: 1.5,
|
|
775
|
+
unit: 's'
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
const result = validateTokens(tokens);
|
|
781
|
+
expect(result.valid).toBe(true);
|
|
782
|
+
});
|
|
783
|
+
it('should error on duration with invalid unit', () => {
|
|
784
|
+
const tokens = JSON.stringify({
|
|
785
|
+
duration: {
|
|
786
|
+
invalid: {
|
|
787
|
+
$type: 'duration',
|
|
788
|
+
$value: {
|
|
789
|
+
value: 100,
|
|
790
|
+
unit: 'sec'
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
const result = validateTokens(tokens);
|
|
796
|
+
expect(result.valid).toBe(false);
|
|
797
|
+
expect(result.errors).toContain('Duration unit at duration.invalid must be "ms" or "s"');
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
describe('CubicBezier tokens', () => {
|
|
801
|
+
it('should validate cubicBezier with valid values', () => {
|
|
802
|
+
const tokens = JSON.stringify({
|
|
803
|
+
easing: {
|
|
804
|
+
accelerate: {
|
|
805
|
+
$type: 'cubicBezier',
|
|
806
|
+
$value: [0.5, 0, 1, 1]
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
const result = validateTokens(tokens);
|
|
811
|
+
expect(result.valid).toBe(true);
|
|
812
|
+
});
|
|
813
|
+
it('should error on cubicBezier with wrong array length', () => {
|
|
814
|
+
const tokens = JSON.stringify({
|
|
815
|
+
easing: {
|
|
816
|
+
invalid: {
|
|
817
|
+
$type: 'cubicBezier',
|
|
818
|
+
$value: [0.5, 0, 1]
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
const result = validateTokens(tokens);
|
|
823
|
+
expect(result.valid).toBe(false);
|
|
824
|
+
expect(result.errors).toContain('cubicBezier at easing.invalid must be an array of exactly 4 numbers');
|
|
825
|
+
});
|
|
826
|
+
it('should error on cubicBezier with P1x out of range', () => {
|
|
827
|
+
const tokens = JSON.stringify({
|
|
828
|
+
easing: {
|
|
829
|
+
invalid: {
|
|
830
|
+
$type: 'cubicBezier',
|
|
831
|
+
$value: [1.5, 0, 1, 1]
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
const result = validateTokens(tokens);
|
|
836
|
+
expect(result.valid).toBe(false);
|
|
837
|
+
expect(result.errors).toContain('cubicBezier at easing.invalid[0] (P1x) must be in range [0, 1]');
|
|
838
|
+
});
|
|
839
|
+
it('should error on cubicBezier with P2x out of range', () => {
|
|
840
|
+
const tokens = JSON.stringify({
|
|
841
|
+
easing: {
|
|
842
|
+
invalid: {
|
|
843
|
+
$type: 'cubicBezier',
|
|
844
|
+
$value: [0.5, 0, -0.5, 1]
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
const result = validateTokens(tokens);
|
|
849
|
+
expect(result.valid).toBe(false);
|
|
850
|
+
expect(result.errors).toContain('cubicBezier at easing.invalid[2] (P2x) must be in range [0, 1]');
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
describe('StrokeStyle tokens', () => {
|
|
854
|
+
it('should validate strokeStyle with string value', () => {
|
|
855
|
+
const values = ['solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'outset', 'inset'];
|
|
856
|
+
values.forEach(value => {
|
|
857
|
+
const tokens = JSON.stringify({
|
|
858
|
+
border: {
|
|
859
|
+
style: {
|
|
860
|
+
$type: 'strokeStyle',
|
|
861
|
+
$value: value
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
const result = validateTokens(tokens);
|
|
866
|
+
expect(result.valid).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
it('should validate strokeStyle with object value', () => {
|
|
870
|
+
const tokens = JSON.stringify({
|
|
871
|
+
border: {
|
|
872
|
+
custom: {
|
|
873
|
+
$type: 'strokeStyle',
|
|
874
|
+
$value: {
|
|
875
|
+
dashArray: [{ value: 10, unit: 'px' }, { value: 5, unit: 'px' }],
|
|
876
|
+
lineCap: 'round'
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
const result = validateTokens(tokens);
|
|
882
|
+
expect(result.valid).toBe(true);
|
|
883
|
+
});
|
|
884
|
+
it('should error on invalid strokeStyle string', () => {
|
|
885
|
+
const tokens = JSON.stringify({
|
|
886
|
+
border: {
|
|
887
|
+
invalid: {
|
|
888
|
+
$type: 'strokeStyle',
|
|
889
|
+
$value: 'wavy'
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
const result = validateTokens(tokens);
|
|
894
|
+
expect(result.valid).toBe(false);
|
|
895
|
+
expect(result.errors.some(e => e.includes('must be one of:'))).toBe(true);
|
|
896
|
+
});
|
|
897
|
+
it('should error on strokeStyle object missing dashArray', () => {
|
|
898
|
+
const tokens = JSON.stringify({
|
|
899
|
+
border: {
|
|
900
|
+
invalid: {
|
|
901
|
+
$type: 'strokeStyle',
|
|
902
|
+
$value: {
|
|
903
|
+
lineCap: 'round'
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
const result = validateTokens(tokens);
|
|
909
|
+
expect(result.valid).toBe(false);
|
|
910
|
+
expect(result.errors).toContain('strokeStyle object at border.invalid must have dashArray property');
|
|
911
|
+
});
|
|
912
|
+
it('should error on strokeStyle object with invalid lineCap', () => {
|
|
913
|
+
const tokens = JSON.stringify({
|
|
914
|
+
border: {
|
|
915
|
+
invalid: {
|
|
916
|
+
$type: 'strokeStyle',
|
|
917
|
+
$value: {
|
|
918
|
+
dashArray: [{ value: 10, unit: 'px' }],
|
|
919
|
+
lineCap: 'pointy'
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
const result = validateTokens(tokens);
|
|
925
|
+
expect(result.valid).toBe(false);
|
|
926
|
+
expect(result.errors).toContain('strokeStyle lineCap at border.invalid must be "round", "butt", or "square"');
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
describe('Border tokens', () => {
|
|
930
|
+
it('should validate border with all properties', () => {
|
|
931
|
+
const tokens = JSON.stringify({
|
|
932
|
+
border: {
|
|
933
|
+
heavy: {
|
|
934
|
+
$type: 'border',
|
|
935
|
+
$value: {
|
|
936
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0] },
|
|
937
|
+
width: { value: 3, unit: 'px' },
|
|
938
|
+
style: 'solid'
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
const result = validateTokens(tokens);
|
|
944
|
+
expect(result.valid).toBe(true);
|
|
945
|
+
});
|
|
946
|
+
it('should error on border missing color', () => {
|
|
947
|
+
const tokens = JSON.stringify({
|
|
948
|
+
border: {
|
|
949
|
+
invalid: {
|
|
950
|
+
$type: 'border',
|
|
951
|
+
$value: {
|
|
952
|
+
width: { value: 1, unit: 'px' },
|
|
953
|
+
style: 'solid'
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
const result = validateTokens(tokens);
|
|
959
|
+
expect(result.valid).toBe(false);
|
|
960
|
+
expect(result.errors).toContain('Border at border.invalid must have color property');
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
describe('Transition tokens', () => {
|
|
964
|
+
it('should validate transition with all properties', () => {
|
|
965
|
+
const tokens = JSON.stringify({
|
|
966
|
+
transition: {
|
|
967
|
+
smooth: {
|
|
968
|
+
$type: 'transition',
|
|
969
|
+
$value: {
|
|
970
|
+
duration: { value: 200, unit: 'ms' },
|
|
971
|
+
delay: { value: 0, unit: 'ms' },
|
|
972
|
+
timingFunction: [0.5, 0, 1, 1]
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
const result = validateTokens(tokens);
|
|
978
|
+
expect(result.valid).toBe(true);
|
|
979
|
+
});
|
|
980
|
+
it('should error on transition missing duration', () => {
|
|
981
|
+
const tokens = JSON.stringify({
|
|
982
|
+
transition: {
|
|
983
|
+
invalid: {
|
|
984
|
+
$type: 'transition',
|
|
985
|
+
$value: {
|
|
986
|
+
delay: { value: 0, unit: 'ms' },
|
|
987
|
+
timingFunction: [0.5, 0, 1, 1]
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
const result = validateTokens(tokens);
|
|
993
|
+
expect(result.valid).toBe(false);
|
|
994
|
+
expect(result.errors).toContain('Transition at transition.invalid must have duration property');
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
describe('Shadow tokens', () => {
|
|
998
|
+
it('should validate shadow with all required fields', () => {
|
|
999
|
+
const tokens = JSON.stringify({
|
|
1000
|
+
shadow: {
|
|
1001
|
+
card: {
|
|
1002
|
+
$type: 'shadow',
|
|
1003
|
+
$value: {
|
|
1004
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1005
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1006
|
+
blur: { value: 8, unit: 'px' },
|
|
1007
|
+
spread: { value: 0, unit: 'px' },
|
|
1008
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0.2 }
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
const result = validateTokens(tokens);
|
|
1014
|
+
expect(result.valid).toBe(true);
|
|
1015
|
+
});
|
|
1016
|
+
it('should validate shadow with inset property', () => {
|
|
1017
|
+
const tokens = JSON.stringify({
|
|
1018
|
+
shadow: {
|
|
1019
|
+
inner: {
|
|
1020
|
+
$type: 'shadow',
|
|
1021
|
+
$value: {
|
|
1022
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1023
|
+
offsetY: { value: 2, unit: 'px' },
|
|
1024
|
+
blur: { value: 4, unit: 'px' },
|
|
1025
|
+
spread: { value: 0, unit: 'px' },
|
|
1026
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0] },
|
|
1027
|
+
inset: true
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
const result = validateTokens(tokens);
|
|
1033
|
+
expect(result.valid).toBe(true);
|
|
1034
|
+
});
|
|
1035
|
+
it('should validate shadow array', () => {
|
|
1036
|
+
const tokens = JSON.stringify({
|
|
1037
|
+
shadow: {
|
|
1038
|
+
layered: {
|
|
1039
|
+
$type: 'shadow',
|
|
1040
|
+
$value: [
|
|
1041
|
+
{
|
|
1042
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1043
|
+
offsetY: { value: 2, unit: 'px' },
|
|
1044
|
+
blur: { value: 4, unit: 'px' },
|
|
1045
|
+
spread: { value: 0, unit: 'px' },
|
|
1046
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0] }
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1050
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1051
|
+
blur: { value: 8, unit: 'px' },
|
|
1052
|
+
spread: { value: 0, unit: 'px' },
|
|
1053
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0] }
|
|
1054
|
+
}
|
|
1055
|
+
]
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
const result = validateTokens(tokens);
|
|
1060
|
+
expect(result.valid).toBe(true);
|
|
1061
|
+
});
|
|
1062
|
+
it('should error on shadow missing spread', () => {
|
|
1063
|
+
const tokens = JSON.stringify({
|
|
1064
|
+
shadow: {
|
|
1065
|
+
invalid: {
|
|
1066
|
+
$type: 'shadow',
|
|
1067
|
+
$value: {
|
|
1068
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1069
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1070
|
+
blur: { value: 8, unit: 'px' },
|
|
1071
|
+
color: '#000000'
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
const result = validateTokens(tokens);
|
|
1077
|
+
expect(result.valid).toBe(false);
|
|
1078
|
+
expect(result.errors).toContain('Shadow at shadow.invalid is missing required field: spread');
|
|
1079
|
+
});
|
|
1080
|
+
it('should error on shadow with non-boolean inset', () => {
|
|
1081
|
+
const tokens = JSON.stringify({
|
|
1082
|
+
shadow: {
|
|
1083
|
+
invalid: {
|
|
1084
|
+
$type: 'shadow',
|
|
1085
|
+
$value: {
|
|
1086
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1087
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1088
|
+
blur: { value: 8, unit: 'px' },
|
|
1089
|
+
spread: { value: 0, unit: 'px' },
|
|
1090
|
+
color: '#000000',
|
|
1091
|
+
inset: 'true'
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
const result = validateTokens(tokens);
|
|
1097
|
+
expect(result.valid).toBe(false);
|
|
1098
|
+
expect(result.errors).toContain('Shadow inset property at shadow.invalid must be a boolean');
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
describe('Gradient tokens', () => {
|
|
1102
|
+
it('should validate gradient with stops', () => {
|
|
1103
|
+
const tokens = JSON.stringify({
|
|
1104
|
+
gradient: {
|
|
1105
|
+
blueToRed: {
|
|
1106
|
+
$type: 'gradient',
|
|
1107
|
+
$value: [
|
|
1108
|
+
{
|
|
1109
|
+
color: { colorSpace: 'srgb', components: [0, 0, 1] },
|
|
1110
|
+
position: 0
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
color: { colorSpace: 'srgb', components: [1, 0, 0] },
|
|
1114
|
+
position: 1
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
const result = validateTokens(tokens);
|
|
1121
|
+
expect(result.valid).toBe(true);
|
|
1122
|
+
});
|
|
1123
|
+
it('should error on gradient that is not an array', () => {
|
|
1124
|
+
const tokens = JSON.stringify({
|
|
1125
|
+
gradient: {
|
|
1126
|
+
invalid: {
|
|
1127
|
+
$type: 'gradient',
|
|
1128
|
+
$value: {
|
|
1129
|
+
color: '#0000ff',
|
|
1130
|
+
position: 0
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
const result = validateTokens(tokens);
|
|
1136
|
+
expect(result.valid).toBe(false);
|
|
1137
|
+
expect(result.errors).toContain('Gradient at gradient.invalid must be an array of gradient stops');
|
|
1138
|
+
});
|
|
1139
|
+
it('should error on gradient stop missing color', () => {
|
|
1140
|
+
const tokens = JSON.stringify({
|
|
1141
|
+
gradient: {
|
|
1142
|
+
invalid: {
|
|
1143
|
+
$type: 'gradient',
|
|
1144
|
+
$value: [
|
|
1145
|
+
{
|
|
1146
|
+
position: 0
|
|
1147
|
+
}
|
|
1148
|
+
]
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
const result = validateTokens(tokens);
|
|
1153
|
+
expect(result.valid).toBe(false);
|
|
1154
|
+
expect(result.errors).toContain('Gradient stop at gradient.invalid[0] must have color property');
|
|
1155
|
+
});
|
|
1156
|
+
it('should error on gradient stop missing position', () => {
|
|
1157
|
+
const tokens = JSON.stringify({
|
|
1158
|
+
gradient: {
|
|
1159
|
+
invalid: {
|
|
1160
|
+
$type: 'gradient',
|
|
1161
|
+
$value: [
|
|
1162
|
+
{
|
|
1163
|
+
color: '#0000ff'
|
|
1164
|
+
}
|
|
1165
|
+
]
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
const result = validateTokens(tokens);
|
|
1170
|
+
expect(result.valid).toBe(false);
|
|
1171
|
+
expect(result.errors).toContain('Gradient stop at gradient.invalid[0] must have position property');
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
describe('Typography tokens', () => {
|
|
1175
|
+
it('should validate typography with all required fields', () => {
|
|
1176
|
+
const tokens = JSON.stringify({
|
|
1177
|
+
text: {
|
|
1178
|
+
heading: {
|
|
1179
|
+
$type: 'typography',
|
|
1180
|
+
$value: {
|
|
1181
|
+
fontFamily: 'Arial',
|
|
1182
|
+
fontSize: { value: 24, unit: 'px' },
|
|
1183
|
+
fontWeight: 700,
|
|
1184
|
+
lineHeight: 1.2,
|
|
1185
|
+
letterSpacing: { value: 0.5, unit: 'px' }
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
const result = validateTokens(tokens);
|
|
1191
|
+
expect(result.valid).toBe(true);
|
|
1192
|
+
});
|
|
1193
|
+
it('should error on typography missing fontFamily', () => {
|
|
1194
|
+
const tokens = JSON.stringify({
|
|
1195
|
+
text: {
|
|
1196
|
+
invalid: {
|
|
1197
|
+
$type: 'typography',
|
|
1198
|
+
$value: {
|
|
1199
|
+
fontSize: { value: 16, unit: 'px' },
|
|
1200
|
+
fontWeight: 400,
|
|
1201
|
+
lineHeight: 1.5,
|
|
1202
|
+
letterSpacing: { value: 0, unit: 'px' }
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
const result = validateTokens(tokens);
|
|
1208
|
+
expect(result.valid).toBe(false);
|
|
1209
|
+
expect(result.errors).toContain('Typography at text.invalid is missing required field: fontFamily');
|
|
1210
|
+
});
|
|
1211
|
+
it('should error on typography missing fontSize', () => {
|
|
1212
|
+
const tokens = JSON.stringify({
|
|
1213
|
+
text: {
|
|
1214
|
+
invalid: {
|
|
1215
|
+
$type: 'typography',
|
|
1216
|
+
$value: {
|
|
1217
|
+
fontFamily: 'Arial',
|
|
1218
|
+
fontWeight: 400,
|
|
1219
|
+
lineHeight: 1.5,
|
|
1220
|
+
letterSpacing: { value: 0, unit: 'px' }
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
const result = validateTokens(tokens);
|
|
1226
|
+
expect(result.valid).toBe(false);
|
|
1227
|
+
expect(result.errors).toContain('Typography at text.invalid is missing required field: fontSize');
|
|
1228
|
+
});
|
|
1229
|
+
it('should warn on typography with unknown field', () => {
|
|
1230
|
+
const tokens = JSON.stringify({
|
|
1231
|
+
text: {
|
|
1232
|
+
heading: {
|
|
1233
|
+
$type: 'typography',
|
|
1234
|
+
$value: {
|
|
1235
|
+
fontFamily: 'Arial',
|
|
1236
|
+
fontSize: { value: 24, unit: 'px' },
|
|
1237
|
+
fontWeight: 700,
|
|
1238
|
+
lineHeight: 1.2,
|
|
1239
|
+
letterSpacing: { value: 0.5, unit: 'px' },
|
|
1240
|
+
textDecoration: 'underline'
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
const result = validateTokens(tokens);
|
|
1246
|
+
expect(result.valid).toBe(true);
|
|
1247
|
+
expect(result.warnings).toContain('Typography at text.heading has unknown field: textDecoration');
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
describe('Token naming rules', () => {
|
|
1251
|
+
it('should error on token name with dot', () => {
|
|
1252
|
+
const tokens = JSON.stringify({
|
|
1253
|
+
'color.primary': {
|
|
1254
|
+
$type: 'color',
|
|
1255
|
+
$value: '#ff0000'
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
const result = validateTokens(tokens);
|
|
1259
|
+
expect(result.valid).toBe(false);
|
|
1260
|
+
expect(result.errors.some(e => e.includes('contains invalid characters'))).toBe(true);
|
|
1261
|
+
});
|
|
1262
|
+
it('should error on token name with curly braces', () => {
|
|
1263
|
+
const tokens = JSON.stringify({
|
|
1264
|
+
'color{primary}': {
|
|
1265
|
+
$type: 'color',
|
|
1266
|
+
$value: '#ff0000'
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
const result = validateTokens(tokens);
|
|
1270
|
+
expect(result.valid).toBe(false);
|
|
1271
|
+
expect(result.errors.some(e => e.includes('contains invalid characters'))).toBe(true);
|
|
1272
|
+
});
|
|
1273
|
+
it('should allow valid token names', () => {
|
|
1274
|
+
const tokens = JSON.stringify({
|
|
1275
|
+
'color-primary-500': {
|
|
1276
|
+
$type: 'color',
|
|
1277
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
const result = validateTokens(tokens);
|
|
1281
|
+
expect(result.valid).toBe(true);
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
describe('Token groups and nesting', () => {
|
|
1285
|
+
it('should validate nested token groups', () => {
|
|
1286
|
+
const tokens = JSON.stringify({
|
|
1287
|
+
color: {
|
|
1288
|
+
primary: {
|
|
1289
|
+
500: {
|
|
1290
|
+
$type: 'color',
|
|
1291
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1292
|
+
},
|
|
1293
|
+
600: {
|
|
1294
|
+
$type: 'color',
|
|
1295
|
+
$value: { colorSpace: 'srgb', components: [0.8, 0, 0] }
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
const result = validateTokens(tokens);
|
|
1301
|
+
expect(result.valid).toBe(true);
|
|
1302
|
+
expect(result.tokenCount).toBe(2);
|
|
1303
|
+
});
|
|
1304
|
+
it('should count tokens correctly in complex structure', () => {
|
|
1305
|
+
const tokens = JSON.stringify({
|
|
1306
|
+
color: {
|
|
1307
|
+
primary: {
|
|
1308
|
+
$type: 'color',
|
|
1309
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
spacing: {
|
|
1313
|
+
small: {
|
|
1314
|
+
$type: 'dimension',
|
|
1315
|
+
$value: { value: 8, unit: 'px' }
|
|
1316
|
+
},
|
|
1317
|
+
medium: {
|
|
1318
|
+
$type: 'dimension',
|
|
1319
|
+
$value: { value: 16, unit: 'px' }
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
const result = validateTokens(tokens);
|
|
1324
|
+
expect(result.valid).toBe(true);
|
|
1325
|
+
expect(result.tokenCount).toBe(3);
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
describe('Missing $value', () => {
|
|
1329
|
+
it('should error on token missing $value', () => {
|
|
1330
|
+
const tokens = JSON.stringify({
|
|
1331
|
+
color: {
|
|
1332
|
+
primary: {
|
|
1333
|
+
$type: 'color'
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
const result = validateTokens(tokens);
|
|
1338
|
+
expect(result.valid).toBe(false);
|
|
1339
|
+
expect(result.errors).toContain('Token at color.primary is missing $value');
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
describe('Unknown token types', () => {
|
|
1343
|
+
it('should warn on unknown $type', () => {
|
|
1344
|
+
const tokens = JSON.stringify({
|
|
1345
|
+
custom: {
|
|
1346
|
+
value: {
|
|
1347
|
+
$type: 'customType',
|
|
1348
|
+
$value: 'something'
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
const result = validateTokens(tokens);
|
|
1353
|
+
expect(result.valid).toBe(true);
|
|
1354
|
+
expect(result.warnings).toContain('Unknown $type "customType" at custom.value');
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
describe('validateTokensObject', () => {
|
|
1358
|
+
it('should validate object directly', () => {
|
|
1359
|
+
const tokens = {
|
|
1360
|
+
color: {
|
|
1361
|
+
primary: {
|
|
1362
|
+
$type: 'color',
|
|
1363
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
const result = validateTokensObject(tokens);
|
|
1368
|
+
expect(result.valid).toBe(true);
|
|
1369
|
+
expect(result.tokenCount).toBe(1);
|
|
1370
|
+
});
|
|
1371
|
+
it('should reject null object', () => {
|
|
1372
|
+
const result = validateTokensObject(null);
|
|
1373
|
+
expect(result.valid).toBe(false);
|
|
1374
|
+
expect(result.errors).toContain('Input is empty');
|
|
1375
|
+
});
|
|
1376
|
+
it('should reject undefined object', () => {
|
|
1377
|
+
const result = validateTokensObject(undefined);
|
|
1378
|
+
expect(result.valid).toBe(false);
|
|
1379
|
+
expect(result.errors).toContain('Input is empty');
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
describe('Real world examples', () => {
|
|
1383
|
+
it('should validate a complete design system', () => {
|
|
1384
|
+
const tokens = JSON.stringify({
|
|
1385
|
+
color: {
|
|
1386
|
+
primary: {
|
|
1387
|
+
$type: 'color',
|
|
1388
|
+
$value: { colorSpace: 'srgb', components: [0, 0.4, 0.8] }
|
|
1389
|
+
},
|
|
1390
|
+
semantic: {
|
|
1391
|
+
background: {
|
|
1392
|
+
$type: 'color',
|
|
1393
|
+
$value: {
|
|
1394
|
+
colorSpace: 'srgb',
|
|
1395
|
+
components: [1, 1, 1],
|
|
1396
|
+
hex: '#ffffff'
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
spacing: {
|
|
1402
|
+
small: {
|
|
1403
|
+
$type: 'dimension',
|
|
1404
|
+
$value: {
|
|
1405
|
+
value: 8,
|
|
1406
|
+
unit: 'px'
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
medium: {
|
|
1410
|
+
$type: 'dimension',
|
|
1411
|
+
$value: { value: 16, unit: 'px' }
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
typography: {
|
|
1415
|
+
heading: {
|
|
1416
|
+
$type: 'typography',
|
|
1417
|
+
$value: {
|
|
1418
|
+
fontFamily: 'Arial',
|
|
1419
|
+
fontSize: { value: 24, unit: 'px' },
|
|
1420
|
+
fontWeight: 700,
|
|
1421
|
+
lineHeight: 1.2,
|
|
1422
|
+
letterSpacing: { value: 0, unit: 'px' }
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
shadow: {
|
|
1427
|
+
card: {
|
|
1428
|
+
$type: 'shadow',
|
|
1429
|
+
$value: {
|
|
1430
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1431
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1432
|
+
blur: { value: 8, unit: 'px' },
|
|
1433
|
+
spread: { value: 0, unit: 'px' },
|
|
1434
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0.2 }
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
duration: {
|
|
1439
|
+
quick: {
|
|
1440
|
+
$type: 'duration',
|
|
1441
|
+
$value: {
|
|
1442
|
+
value: 100,
|
|
1443
|
+
unit: 'ms'
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
const result = validateTokens(tokens);
|
|
1449
|
+
expect(result.valid).toBe(true);
|
|
1450
|
+
expect(result.tokenCount).toBe(7);
|
|
1451
|
+
expect(result.errors).toHaveLength(0);
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
describe('Token References (Aliases)', () => {
|
|
1455
|
+
describe('Basic references', () => {
|
|
1456
|
+
it('should validate simple color reference', () => {
|
|
1457
|
+
const tokens = JSON.stringify({
|
|
1458
|
+
color: {
|
|
1459
|
+
base: {
|
|
1460
|
+
$type: 'color',
|
|
1461
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1462
|
+
},
|
|
1463
|
+
primary: {
|
|
1464
|
+
$type: 'color',
|
|
1465
|
+
$value: '{color.base}'
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
const result = validateTokens(tokens);
|
|
1470
|
+
expect(result.valid).toBe(true);
|
|
1471
|
+
expect(result.tokenCount).toBe(2);
|
|
1472
|
+
});
|
|
1473
|
+
it('should validate reference with nested groups', () => {
|
|
1474
|
+
const tokens = JSON.stringify({
|
|
1475
|
+
palette: {
|
|
1476
|
+
brand: {
|
|
1477
|
+
primary: {
|
|
1478
|
+
$type: 'color',
|
|
1479
|
+
$value: { colorSpace: 'srgb', components: [0, 0.4, 0.8] }
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
semantic: {
|
|
1484
|
+
link: {
|
|
1485
|
+
$type: 'color',
|
|
1486
|
+
$value: '{palette.brand.primary}'
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
const result = validateTokens(tokens);
|
|
1491
|
+
expect(result.valid).toBe(true);
|
|
1492
|
+
});
|
|
1493
|
+
it('should error on reference to non-existent token', () => {
|
|
1494
|
+
const tokens = JSON.stringify({
|
|
1495
|
+
color: {
|
|
1496
|
+
primary: {
|
|
1497
|
+
$type: 'color',
|
|
1498
|
+
$value: '{color.nonexistent}'
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
const result = validateTokens(tokens);
|
|
1503
|
+
expect(result.valid).toBe(false);
|
|
1504
|
+
expect(result.errors.some(e => e.includes('non-existent'))).toBe(true);
|
|
1505
|
+
});
|
|
1506
|
+
it('should inherit type from referenced token', () => {
|
|
1507
|
+
const tokens = JSON.stringify({
|
|
1508
|
+
color: {
|
|
1509
|
+
base: {
|
|
1510
|
+
$type: 'color',
|
|
1511
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1512
|
+
},
|
|
1513
|
+
alias: {
|
|
1514
|
+
$value: '{color.base}'
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
const result = validateTokens(tokens);
|
|
1519
|
+
expect(result.valid).toBe(true);
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
describe('Chained references', () => {
|
|
1523
|
+
it('should resolve multi-level reference chains', () => {
|
|
1524
|
+
const tokens = JSON.stringify({
|
|
1525
|
+
color: {
|
|
1526
|
+
base: {
|
|
1527
|
+
$type: 'color',
|
|
1528
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1529
|
+
},
|
|
1530
|
+
level1: {
|
|
1531
|
+
$type: 'color',
|
|
1532
|
+
$value: '{color.base}'
|
|
1533
|
+
},
|
|
1534
|
+
level2: {
|
|
1535
|
+
$type: 'color',
|
|
1536
|
+
$value: '{color.level1}'
|
|
1537
|
+
},
|
|
1538
|
+
level3: {
|
|
1539
|
+
$type: 'color',
|
|
1540
|
+
$value: '{color.level2}'
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
const result = validateTokens(tokens);
|
|
1545
|
+
expect(result.valid).toBe(true);
|
|
1546
|
+
expect(result.tokenCount).toBe(4);
|
|
1547
|
+
});
|
|
1548
|
+
it('should inherit type through reference chain', () => {
|
|
1549
|
+
const tokens = JSON.stringify({
|
|
1550
|
+
dimension: {
|
|
1551
|
+
base: {
|
|
1552
|
+
$type: 'dimension',
|
|
1553
|
+
$value: { value: 16, unit: 'px' }
|
|
1554
|
+
},
|
|
1555
|
+
alias1: {
|
|
1556
|
+
$value: '{dimension.base}'
|
|
1557
|
+
},
|
|
1558
|
+
alias2: {
|
|
1559
|
+
$value: '{dimension.alias1}'
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
const result = validateTokens(tokens);
|
|
1564
|
+
expect(result.valid).toBe(true);
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1567
|
+
describe('Circular references', () => {
|
|
1568
|
+
it('should detect simple circular reference', () => {
|
|
1569
|
+
const tokens = JSON.stringify({
|
|
1570
|
+
color: {
|
|
1571
|
+
a: {
|
|
1572
|
+
$type: 'color',
|
|
1573
|
+
$value: '{color.b}'
|
|
1574
|
+
},
|
|
1575
|
+
b: {
|
|
1576
|
+
$type: 'color',
|
|
1577
|
+
$value: '{color.a}'
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
const result = validateTokens(tokens);
|
|
1582
|
+
expect(result.valid).toBe(false);
|
|
1583
|
+
expect(result.errors.some(e => e.includes('Circular reference'))).toBe(true);
|
|
1584
|
+
});
|
|
1585
|
+
it('should detect multi-level circular reference', () => {
|
|
1586
|
+
const tokens = JSON.stringify({
|
|
1587
|
+
color: {
|
|
1588
|
+
a: {
|
|
1589
|
+
$type: 'color',
|
|
1590
|
+
$value: '{color.b}'
|
|
1591
|
+
},
|
|
1592
|
+
b: {
|
|
1593
|
+
$type: 'color',
|
|
1594
|
+
$value: '{color.c}'
|
|
1595
|
+
},
|
|
1596
|
+
c: {
|
|
1597
|
+
$type: 'color',
|
|
1598
|
+
$value: '{color.a}'
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
const result = validateTokens(tokens);
|
|
1603
|
+
expect(result.valid).toBe(false);
|
|
1604
|
+
expect(result.errors.some(e => e.includes('Circular reference'))).toBe(true);
|
|
1605
|
+
});
|
|
1606
|
+
it('should detect self-reference', () => {
|
|
1607
|
+
const tokens = JSON.stringify({
|
|
1608
|
+
color: {
|
|
1609
|
+
broken: {
|
|
1610
|
+
$type: 'color',
|
|
1611
|
+
$value: '{color.broken}'
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
const result = validateTokens(tokens);
|
|
1616
|
+
expect(result.valid).toBe(false);
|
|
1617
|
+
expect(result.errors.some(e => e.includes('Circular reference'))).toBe(true);
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
describe('Type inheritance through references', () => {
|
|
1621
|
+
it('should validate dimension reference', () => {
|
|
1622
|
+
const tokens = JSON.stringify({
|
|
1623
|
+
spacing: {
|
|
1624
|
+
base: {
|
|
1625
|
+
$type: 'dimension',
|
|
1626
|
+
$value: { value: 8, unit: 'px' }
|
|
1627
|
+
},
|
|
1628
|
+
small: {
|
|
1629
|
+
$value: '{spacing.base}'
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
const result = validateTokens(tokens);
|
|
1634
|
+
expect(result.valid).toBe(true);
|
|
1635
|
+
});
|
|
1636
|
+
it('should validate fontWeight reference', () => {
|
|
1637
|
+
const tokens = JSON.stringify({
|
|
1638
|
+
weight: {
|
|
1639
|
+
normal: {
|
|
1640
|
+
$type: 'fontWeight',
|
|
1641
|
+
$value: 400
|
|
1642
|
+
},
|
|
1643
|
+
body: {
|
|
1644
|
+
$value: '{weight.normal}'
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
const result = validateTokens(tokens);
|
|
1649
|
+
expect(result.valid).toBe(true);
|
|
1650
|
+
});
|
|
1651
|
+
it('should work with explicit types on tokens', () => {
|
|
1652
|
+
const tokens = JSON.stringify({
|
|
1653
|
+
color: {
|
|
1654
|
+
$type: 'color',
|
|
1655
|
+
base: {
|
|
1656
|
+
$type: 'color',
|
|
1657
|
+
$value: { colorSpace: 'srgb', components: [1, 0, 0] }
|
|
1658
|
+
},
|
|
1659
|
+
primary: {
|
|
1660
|
+
$type: 'color',
|
|
1661
|
+
$value: '{color.base}'
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
const result = validateTokens(tokens);
|
|
1666
|
+
// Reference resolves correctly with explicit types
|
|
1667
|
+
expect(result.valid).toBe(true);
|
|
1668
|
+
expect(result.errors.length).toBe(0);
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
describe('References in composite tokens', () => {
|
|
1672
|
+
it('should validate shadow with color reference', () => {
|
|
1673
|
+
const tokens = JSON.stringify({
|
|
1674
|
+
color: {
|
|
1675
|
+
shadowColor: {
|
|
1676
|
+
$type: 'color',
|
|
1677
|
+
$value: { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0.5 }
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
shadow: {
|
|
1681
|
+
card: {
|
|
1682
|
+
$type: 'shadow',
|
|
1683
|
+
$value: {
|
|
1684
|
+
color: '{color.shadowColor}',
|
|
1685
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1686
|
+
offsetY: { value: 4, unit: 'px' },
|
|
1687
|
+
blur: { value: 8, unit: 'px' },
|
|
1688
|
+
spread: { value: 0, unit: 'px' }
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
const result = validateTokens(tokens);
|
|
1694
|
+
expect(result.valid).toBe(true);
|
|
1695
|
+
});
|
|
1696
|
+
it('should validate border with multiple references', () => {
|
|
1697
|
+
const tokens = JSON.stringify({
|
|
1698
|
+
color: {
|
|
1699
|
+
borderColor: {
|
|
1700
|
+
$type: 'color',
|
|
1701
|
+
$value: { colorSpace: 'srgb', components: [0.8, 0.8, 0.8] }
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
dimension: {
|
|
1705
|
+
borderWidth: {
|
|
1706
|
+
$type: 'dimension',
|
|
1707
|
+
$value: { value: 1, unit: 'px' }
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
border: {
|
|
1711
|
+
default: {
|
|
1712
|
+
$type: 'border',
|
|
1713
|
+
$value: {
|
|
1714
|
+
color: '{color.borderColor}',
|
|
1715
|
+
width: '{dimension.borderWidth}',
|
|
1716
|
+
style: 'solid'
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
const result = validateTokens(tokens);
|
|
1722
|
+
expect(result.valid).toBe(true);
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
describe('References inside composite arrays and sub-values (spec §7.3, §9.1)', () => {
|
|
1726
|
+
it('should accept a reference as a shadow array element', () => {
|
|
1727
|
+
const tokens = JSON.stringify({
|
|
1728
|
+
base: {
|
|
1729
|
+
shadow: {
|
|
1730
|
+
$type: 'shadow',
|
|
1731
|
+
$value: {
|
|
1732
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0.2 },
|
|
1733
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1734
|
+
offsetY: { value: 2, unit: 'px' },
|
|
1735
|
+
blur: { value: 4, unit: 'px' },
|
|
1736
|
+
spread: { value: 0, unit: 'px' }
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
layered: {
|
|
1741
|
+
$type: 'shadow',
|
|
1742
|
+
$value: [
|
|
1743
|
+
'{base.shadow}',
|
|
1744
|
+
{
|
|
1745
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0.3 },
|
|
1746
|
+
offsetX: { value: 0, unit: 'px' },
|
|
1747
|
+
offsetY: { value: 8, unit: 'px' },
|
|
1748
|
+
blur: { value: 16, unit: 'px' },
|
|
1749
|
+
spread: { value: 0, unit: 'px' }
|
|
1750
|
+
}
|
|
1751
|
+
]
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
const result = validateTokens(tokens);
|
|
1755
|
+
expect(result.valid).toBe(true);
|
|
1756
|
+
});
|
|
1757
|
+
it('should accept references for gradient stop color and position', () => {
|
|
1758
|
+
const tokens = JSON.stringify({
|
|
1759
|
+
brand: {
|
|
1760
|
+
primary: { $type: 'color', $value: { colorSpace: 'srgb', components: [0, 1, 0.4] } }
|
|
1761
|
+
},
|
|
1762
|
+
end: { $type: 'number', $value: 1 },
|
|
1763
|
+
ramp: {
|
|
1764
|
+
$type: 'gradient',
|
|
1765
|
+
$value: [
|
|
1766
|
+
{ color: { colorSpace: 'srgb', components: [0, 0, 0] }, position: 0 },
|
|
1767
|
+
{ color: '{brand.primary}', position: '{end}' }
|
|
1768
|
+
]
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
const result = validateTokens(tokens);
|
|
1772
|
+
expect(result.valid).toBe(true);
|
|
1773
|
+
});
|
|
1774
|
+
it('should accept references for every typography sub-value', () => {
|
|
1775
|
+
const tokens = JSON.stringify({
|
|
1776
|
+
font: { body: { $type: 'fontFamily', $value: 'Inter' } },
|
|
1777
|
+
size: { md: { $type: 'dimension', $value: { value: 16, unit: 'px' } } },
|
|
1778
|
+
weight: { bold: { $type: 'fontWeight', $value: 700 } },
|
|
1779
|
+
spacing: { tight: { $type: 'dimension', $value: { value: 0, unit: 'px' } } },
|
|
1780
|
+
lh: { normal: { $type: 'number', $value: 1.4 } },
|
|
1781
|
+
heading: {
|
|
1782
|
+
$type: 'typography',
|
|
1783
|
+
$value: {
|
|
1784
|
+
fontFamily: '{font.body}',
|
|
1785
|
+
fontSize: '{size.md}',
|
|
1786
|
+
fontWeight: '{weight.bold}',
|
|
1787
|
+
letterSpacing: '{spacing.tight}',
|
|
1788
|
+
lineHeight: '{lh.normal}'
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
const result = validateTokens(tokens);
|
|
1793
|
+
expect(result.valid).toBe(true);
|
|
1794
|
+
});
|
|
1795
|
+
it('should reject an invalid border sub-value (width as a string)', () => {
|
|
1796
|
+
const tokens = JSON.stringify({
|
|
1797
|
+
border: {
|
|
1798
|
+
bad: {
|
|
1799
|
+
$type: 'border',
|
|
1800
|
+
$value: {
|
|
1801
|
+
color: { colorSpace: 'srgb', components: [0, 0, 0] },
|
|
1802
|
+
width: '3px',
|
|
1803
|
+
style: 'solid'
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
const result = validateTokens(tokens);
|
|
1809
|
+
expect(result.valid).toBe(false);
|
|
1810
|
+
});
|
|
1811
|
+
it('should reject an invalid strokeStyle dashArray entry', () => {
|
|
1812
|
+
const tokens = JSON.stringify({
|
|
1813
|
+
stroke: {
|
|
1814
|
+
bad: {
|
|
1815
|
+
$type: 'strokeStyle',
|
|
1816
|
+
$value: { dashArray: ['3px', { value: 2, unit: 'px' }], lineCap: 'round' }
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
const result = validateTokens(tokens);
|
|
1821
|
+
expect(result.valid).toBe(false);
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
describe('JSON Pointer $ref (RFC 6901, spec §6.6.2 / §7.1.2 / §7.3)', () => {
|
|
1825
|
+
it('should resolve a token-level $ref to another token value (Example 32)', () => {
|
|
1826
|
+
const tokens = JSON.stringify({
|
|
1827
|
+
colors: {
|
|
1828
|
+
blue: {
|
|
1829
|
+
$type: 'color',
|
|
1830
|
+
$value: { colorSpace: 'srgb', components: [0, 0.4, 0.8], hex: '#0066cc' }
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
semantic: {
|
|
1834
|
+
primary: { $ref: '#/colors/blue/$value', $type: 'color' }
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
const result = validateTokens(tokens);
|
|
1838
|
+
expect(result.valid).toBe(true);
|
|
1839
|
+
expect(result.tokenCount).toBe(2);
|
|
1840
|
+
});
|
|
1841
|
+
it('should resolve a token-level $ref to a single component (number)', () => {
|
|
1842
|
+
const tokens = JSON.stringify({
|
|
1843
|
+
colors: {
|
|
1844
|
+
blue: { $type: 'color', $value: { colorSpace: 'srgb', components: [0, 0.4, 0.8] } }
|
|
1845
|
+
},
|
|
1846
|
+
primaryHue: { $ref: '#/colors/blue/$value/components/0', $type: 'number' }
|
|
1847
|
+
});
|
|
1848
|
+
const result = validateTokens(tokens);
|
|
1849
|
+
expect(result.valid).toBe(true);
|
|
1850
|
+
});
|
|
1851
|
+
it('should accept property-level $ref inside color components (Example 35)', () => {
|
|
1852
|
+
const tokens = JSON.stringify({
|
|
1853
|
+
base: {
|
|
1854
|
+
blue: { $type: 'color', $value: { colorSpace: 'srgb', components: [0.2, 0.4, 0.9] } }
|
|
1855
|
+
},
|
|
1856
|
+
semantic: {
|
|
1857
|
+
primary: {
|
|
1858
|
+
$type: 'color',
|
|
1859
|
+
$value: {
|
|
1860
|
+
colorSpace: 'srgb',
|
|
1861
|
+
components: [
|
|
1862
|
+
{ $ref: '#/base/blue/$value/components/0' },
|
|
1863
|
+
{ $ref: '#/base/blue/$value/components/1' },
|
|
1864
|
+
0.7
|
|
1865
|
+
]
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
const result = validateTokens(tokens);
|
|
1871
|
+
expect(result.valid).toBe(true);
|
|
1872
|
+
});
|
|
1873
|
+
it('should accept property-level $ref for dimension value and unit (Example 36)', () => {
|
|
1874
|
+
const tokens = JSON.stringify({
|
|
1875
|
+
base: { spacing: { $type: 'dimension', $value: { value: 16, unit: 'px' } } },
|
|
1876
|
+
layout: {
|
|
1877
|
+
small: {
|
|
1878
|
+
$type: 'dimension',
|
|
1879
|
+
$value: { value: { $ref: '#/base/spacing/$value/value' }, unit: 'rem' }
|
|
1880
|
+
},
|
|
1881
|
+
large: {
|
|
1882
|
+
$type: 'dimension',
|
|
1883
|
+
$value: { value: 32, unit: { $ref: '#/base/spacing/$value/unit' } }
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
const result = validateTokens(tokens);
|
|
1888
|
+
expect(result.valid).toBe(true);
|
|
1889
|
+
});
|
|
1890
|
+
it('should error on an unresolvable JSON Pointer', () => {
|
|
1891
|
+
const tokens = JSON.stringify({
|
|
1892
|
+
a: { $ref: '#/does/not/exist', $type: 'color' }
|
|
1893
|
+
});
|
|
1894
|
+
const result = validateTokens(tokens);
|
|
1895
|
+
expect(result.valid).toBe(false);
|
|
1896
|
+
expect(result.errors.some((e) => e.includes('cannot be resolved'))).toBe(true);
|
|
1897
|
+
});
|
|
1898
|
+
it('should error on a circular $ref chain', () => {
|
|
1899
|
+
const tokens = JSON.stringify({
|
|
1900
|
+
a: { $ref: '#/b', $type: 'color' },
|
|
1901
|
+
b: { $ref: '#/a', $type: 'color' }
|
|
1902
|
+
});
|
|
1903
|
+
const result = validateTokens(tokens);
|
|
1904
|
+
expect(result.valid).toBe(false);
|
|
1905
|
+
});
|
|
1906
|
+
});
|
|
1907
|
+
describe('Reference validation edge cases', () => {
|
|
1908
|
+
it('should accept malformed reference syntax as literal string for non-color types', () => {
|
|
1909
|
+
const tokens = JSON.stringify({
|
|
1910
|
+
text: {
|
|
1911
|
+
value: {
|
|
1912
|
+
$type: 'fontFamily',
|
|
1913
|
+
$value: '{incomplete'
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
const result = validateTokens(tokens);
|
|
1918
|
+
// This should be valid since fontFamily can be any string
|
|
1919
|
+
expect(result.valid).toBe(true);
|
|
1920
|
+
});
|
|
1921
|
+
it('should treat empty braces as literal string, not reference', () => {
|
|
1922
|
+
const tokens = JSON.stringify({
|
|
1923
|
+
color: {
|
|
1924
|
+
broken: {
|
|
1925
|
+
$type: 'color',
|
|
1926
|
+
$value: '{}'
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
const result = validateTokens(tokens);
|
|
1931
|
+
// {} doesn't match the reference pattern, so it is treated as a literal
|
|
1932
|
+
// string — which is not a valid color value (must be an object) → error.
|
|
1933
|
+
expect(result.valid).toBe(false);
|
|
1934
|
+
});
|
|
1935
|
+
it('should validate references across different type groups', () => {
|
|
1936
|
+
const tokens = JSON.stringify({
|
|
1937
|
+
base: {
|
|
1938
|
+
size: {
|
|
1939
|
+
$type: 'dimension',
|
|
1940
|
+
$value: { value: 16, unit: 'px' }
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
typography: {
|
|
1944
|
+
body: {
|
|
1945
|
+
$type: 'typography',
|
|
1946
|
+
$value: {
|
|
1947
|
+
fontFamily: 'Arial',
|
|
1948
|
+
fontSize: '{base.size}',
|
|
1949
|
+
fontWeight: 400,
|
|
1950
|
+
lineHeight: 1.5,
|
|
1951
|
+
letterSpacing: { value: 0, unit: 'px' }
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
const result = validateTokens(tokens);
|
|
1957
|
+
expect(result.valid).toBe(true);
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1961
|
+
describe('$root, $deprecated, $extends and reserved names (Format §5.1.1, §5.2.4, §6.2, §6.4)', () => {
|
|
1962
|
+
it('should validate a $root token within a group (Example 24)', () => {
|
|
1963
|
+
const tokens = JSON.stringify({
|
|
1964
|
+
spacing: {
|
|
1965
|
+
$type: 'dimension',
|
|
1966
|
+
$root: { $value: { value: 16, unit: 'px' } },
|
|
1967
|
+
small: { $value: { value: 8, unit: 'px' } },
|
|
1968
|
+
large: { $value: { value: 32, unit: 'px' } }
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
const result = validateTokens(tokens);
|
|
1972
|
+
expect(result.valid).toBe(true);
|
|
1973
|
+
expect(result.tokenCount).toBe(3);
|
|
1974
|
+
});
|
|
1975
|
+
it('should resolve a curly reference to a $root token but not to the group', () => {
|
|
1976
|
+
const ok = JSON.stringify({
|
|
1977
|
+
color: { accent: { $type: 'color', $root: { $value: { colorSpace: 'srgb', components: [1, 0, 0] } } } },
|
|
1978
|
+
alias: { $type: 'color', $value: '{color.accent.$root}' }
|
|
1979
|
+
});
|
|
1980
|
+
expect(validateTokens(ok).valid).toBe(true);
|
|
1981
|
+
const bad = JSON.stringify({
|
|
1982
|
+
color: { accent: { $type: 'color', $root: { $value: { colorSpace: 'srgb', components: [1, 0, 0] } } } },
|
|
1983
|
+
alias: { $type: 'color', $value: '{color.accent}' }
|
|
1984
|
+
});
|
|
1985
|
+
expect(validateTokens(bad).valid).toBe(false);
|
|
1986
|
+
});
|
|
1987
|
+
it('should accept $deprecated as boolean or string', () => {
|
|
1988
|
+
const tokens = JSON.stringify({
|
|
1989
|
+
color: {
|
|
1990
|
+
old: { $type: 'color', $deprecated: true, $value: { colorSpace: 'srgb', components: [0, 0, 0] } },
|
|
1991
|
+
legacy: { $type: 'color', $deprecated: 'Use {color.new} instead.', $value: { colorSpace: 'srgb', components: [0, 0, 0] } }
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
expect(validateTokens(tokens).valid).toBe(true);
|
|
1995
|
+
});
|
|
1996
|
+
it('should reject a non-boolean/non-string $deprecated', () => {
|
|
1997
|
+
const tokens = JSON.stringify({
|
|
1998
|
+
color: { x: { $type: 'color', $deprecated: 42, $value: { colorSpace: 'srgb', components: [0, 0, 0] } } }
|
|
1999
|
+
});
|
|
2000
|
+
expect(validateTokens(tokens).valid).toBe(false);
|
|
2001
|
+
});
|
|
2002
|
+
it('should reject a token/group name beginning with $', () => {
|
|
2003
|
+
const tokens = JSON.stringify({
|
|
2004
|
+
$custom: { $type: 'color', $value: { colorSpace: 'srgb', components: [0, 0, 0] } }
|
|
2005
|
+
});
|
|
2006
|
+
const result = validateTokens(tokens);
|
|
2007
|
+
expect(result.valid).toBe(false);
|
|
2008
|
+
expect(result.errors.some((e) => e.includes('must not begin with "$"'))).toBe(true);
|
|
2009
|
+
});
|
|
2010
|
+
it('should validate a group $extends referencing another group (Example 13)', () => {
|
|
2011
|
+
const tokens = JSON.stringify({
|
|
2012
|
+
button: {
|
|
2013
|
+
$type: 'color',
|
|
2014
|
+
background: { $value: { colorSpace: 'srgb', components: [0, 0.4, 0.8] } }
|
|
2015
|
+
},
|
|
2016
|
+
'button-primary': {
|
|
2017
|
+
$extends: '{button}',
|
|
2018
|
+
background: { $type: 'color', $value: { colorSpace: 'srgb', components: [0.8, 0, 0.4] } }
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
expect(validateTokens(tokens).valid).toBe(true);
|
|
2022
|
+
});
|
|
2023
|
+
it('should reject $extends that references a token, not a group', () => {
|
|
2024
|
+
const tokens = JSON.stringify({
|
|
2025
|
+
base: { $type: 'color', $value: { colorSpace: 'srgb', components: [0, 0, 0] } },
|
|
2026
|
+
derived: { $extends: '{base}', x: { $type: 'color', $value: { colorSpace: 'srgb', components: [1, 1, 1] } } }
|
|
2027
|
+
});
|
|
2028
|
+
const result = validateTokens(tokens);
|
|
2029
|
+
expect(result.valid).toBe(false);
|
|
2030
|
+
expect(result.errors.some((e) => e.includes('must not reference a token'))).toBe(true);
|
|
2031
|
+
});
|
|
2032
|
+
it('should detect a circular $extends chain (Example 17)', () => {
|
|
2033
|
+
const tokens = JSON.stringify({
|
|
2034
|
+
groupA: { $extends: '{groupB}', token: { $type: 'color', $value: { colorSpace: 'srgb', components: [0, 0, 0] } } },
|
|
2035
|
+
groupB: { $extends: '{groupA}', token: { $type: 'color', $value: { colorSpace: 'srgb', components: [1, 1, 1] } } }
|
|
2036
|
+
});
|
|
2037
|
+
const result = validateTokens(tokens);
|
|
2038
|
+
expect(result.valid).toBe(false);
|
|
2039
|
+
expect(result.errors.some((e) => e.includes('Circular $extends'))).toBe(true);
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
describe('Resolver module (.resolver.json, Resolver Module 2025.10)', () => {
|
|
2043
|
+
it('should validate a resolver document with sets and a modifier (Example 6)', () => {
|
|
2044
|
+
const doc = JSON.stringify({
|
|
2045
|
+
version: '2025.10',
|
|
2046
|
+
sets: {
|
|
2047
|
+
size: { sources: [{ $ref: 'foundation/size.json' }] },
|
|
2048
|
+
typography: { sources: [{ $ref: 'foundation/typography.json' }] }
|
|
2049
|
+
},
|
|
2050
|
+
modifiers: {
|
|
2051
|
+
theme: {
|
|
2052
|
+
description: 'Color theme',
|
|
2053
|
+
contexts: {
|
|
2054
|
+
light: [{ $ref: 'theme/light.json' }],
|
|
2055
|
+
dark: [{ $ref: 'theme/dark.json' }]
|
|
2056
|
+
},
|
|
2057
|
+
default: 'light'
|
|
2058
|
+
}
|
|
2059
|
+
},
|
|
2060
|
+
resolutionOrder: [
|
|
2061
|
+
{ $ref: '#/sets/size' },
|
|
2062
|
+
{ $ref: '#/sets/typography' },
|
|
2063
|
+
{ $ref: '#/modifiers/theme' }
|
|
2064
|
+
]
|
|
2065
|
+
});
|
|
2066
|
+
const result = validateTokens(doc);
|
|
2067
|
+
expect(result.documentType).toBe('resolver');
|
|
2068
|
+
expect(result.valid).toBe(true);
|
|
2069
|
+
});
|
|
2070
|
+
it('should error on a missing or wrong version', () => {
|
|
2071
|
+
const doc = JSON.stringify({
|
|
2072
|
+
version: '2024.01',
|
|
2073
|
+
resolutionOrder: [{ type: 'set', name: 'S', sources: [] }]
|
|
2074
|
+
});
|
|
2075
|
+
const result = validateTokens(doc);
|
|
2076
|
+
expect(result.valid).toBe(false);
|
|
2077
|
+
expect(result.errors.some((e) => e.includes('version must be "2025.10"'))).toBe(true);
|
|
2078
|
+
});
|
|
2079
|
+
it('should error on a modifier with an empty contexts map', () => {
|
|
2080
|
+
const doc = JSON.stringify({
|
|
2081
|
+
version: '2025.10',
|
|
2082
|
+
modifiers: { broken: { contexts: {} } },
|
|
2083
|
+
resolutionOrder: [{ $ref: '#/modifiers/broken' }]
|
|
2084
|
+
});
|
|
2085
|
+
const result = validateTokens(doc);
|
|
2086
|
+
expect(result.valid).toBe(false);
|
|
2087
|
+
expect(result.errors.some((e) => e.includes('empty contexts map'))).toBe(true);
|
|
2088
|
+
});
|
|
2089
|
+
it('should error when default is not one of the contexts', () => {
|
|
2090
|
+
const doc = JSON.stringify({
|
|
2091
|
+
version: '2025.10',
|
|
2092
|
+
modifiers: {
|
|
2093
|
+
theme: { contexts: { light: [], dark: [] }, default: 'blue' }
|
|
2094
|
+
},
|
|
2095
|
+
resolutionOrder: [{ $ref: '#/modifiers/theme' }]
|
|
2096
|
+
});
|
|
2097
|
+
const result = validateTokens(doc);
|
|
2098
|
+
expect(result.valid).toBe(false);
|
|
2099
|
+
expect(result.errors.some((e) => e.includes('default "blue"'))).toBe(true);
|
|
2100
|
+
});
|
|
2101
|
+
it('should error on an inline resolutionOrder item missing type/name (Example 8)', () => {
|
|
2102
|
+
const doc = JSON.stringify({
|
|
2103
|
+
version: '2025.10',
|
|
2104
|
+
resolutionOrder: [{ sources: [{ $ref: 'foundation/size.json' }] }]
|
|
2105
|
+
});
|
|
2106
|
+
const result = validateTokens(doc);
|
|
2107
|
+
expect(result.valid).toBe(false);
|
|
2108
|
+
expect(result.errors.some((e) => e.includes('type "set" or "modifier"'))).toBe(true);
|
|
2109
|
+
});
|
|
2110
|
+
it('should error on a reference object pointing into resolutionOrder (Example 9)', () => {
|
|
2111
|
+
const doc = JSON.stringify({
|
|
2112
|
+
version: '2025.10',
|
|
2113
|
+
resolutionOrder: [{ $ref: '#/resolutionOrder/0' }]
|
|
2114
|
+
});
|
|
2115
|
+
const result = validateTokens(doc);
|
|
2116
|
+
expect(result.valid).toBe(false);
|
|
2117
|
+
});
|
|
2118
|
+
it('should warn on a modifier with only one context', () => {
|
|
2119
|
+
const doc = JSON.stringify({
|
|
2120
|
+
version: '2025.10',
|
|
2121
|
+
modifiers: { theme: { contexts: { only: [] } } },
|
|
2122
|
+
resolutionOrder: [{ $ref: '#/modifiers/theme' }]
|
|
2123
|
+
});
|
|
2124
|
+
const result = validateTokens(doc);
|
|
2125
|
+
expect(result.warnings.some((w) => w.includes('two or more contexts'))).toBe(true);
|
|
2126
|
+
});
|
|
2127
|
+
});
|
|
2128
|
+
});
|
|
2129
|
+
//# sourceMappingURL=dtcg-validate.test.js.map
|