@pep/term-deck 1.0.10
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 +356 -0
- package/bin/term-deck.ts +45 -0
- package/examples/slides/01-welcome.md +9 -0
- package/examples/slides/02-features.md +12 -0
- package/examples/slides/03-colors.md +17 -0
- package/examples/slides/04-ascii-art.md +11 -0
- package/examples/slides/05-gradients.md +14 -0
- package/examples/slides/06-themes.md +13 -0
- package/examples/slides/07-markdown.md +13 -0
- package/examples/slides/08-controls.md +13 -0
- package/examples/slides/09-thanks.md +11 -0
- package/examples/slides/deck.config.ts +13 -0
- package/examples/slides-hacker/01-welcome.md +9 -0
- package/examples/slides-hacker/02-features.md +12 -0
- package/examples/slides-hacker/03-colors.md +17 -0
- package/examples/slides-hacker/04-ascii-art.md +11 -0
- package/examples/slides-hacker/05-gradients.md +14 -0
- package/examples/slides-hacker/06-themes.md +13 -0
- package/examples/slides-hacker/07-markdown.md +13 -0
- package/examples/slides-hacker/08-controls.md +13 -0
- package/examples/slides-hacker/09-thanks.md +11 -0
- package/examples/slides-hacker/deck.config.ts +13 -0
- package/examples/slides-matrix/01-welcome.md +9 -0
- package/examples/slides-matrix/02-features.md +12 -0
- package/examples/slides-matrix/03-colors.md +17 -0
- package/examples/slides-matrix/04-ascii-art.md +11 -0
- package/examples/slides-matrix/05-gradients.md +14 -0
- package/examples/slides-matrix/06-themes.md +13 -0
- package/examples/slides-matrix/07-markdown.md +13 -0
- package/examples/slides-matrix/08-controls.md +13 -0
- package/examples/slides-matrix/09-thanks.md +11 -0
- package/examples/slides-matrix/deck.config.ts +13 -0
- package/examples/slides-minimal/01-welcome.md +9 -0
- package/examples/slides-minimal/02-features.md +12 -0
- package/examples/slides-minimal/03-colors.md +17 -0
- package/examples/slides-minimal/04-ascii-art.md +11 -0
- package/examples/slides-minimal/05-gradients.md +14 -0
- package/examples/slides-minimal/06-themes.md +13 -0
- package/examples/slides-minimal/07-markdown.md +13 -0
- package/examples/slides-minimal/08-controls.md +13 -0
- package/examples/slides-minimal/09-thanks.md +11 -0
- package/examples/slides-minimal/deck.config.ts +13 -0
- package/examples/slides-neon/01-welcome.md +9 -0
- package/examples/slides-neon/02-features.md +12 -0
- package/examples/slides-neon/03-colors.md +17 -0
- package/examples/slides-neon/04-ascii-art.md +11 -0
- package/examples/slides-neon/05-gradients.md +14 -0
- package/examples/slides-neon/06-themes.md +13 -0
- package/examples/slides-neon/07-markdown.md +13 -0
- package/examples/slides-neon/08-controls.md +13 -0
- package/examples/slides-neon/09-thanks.md +11 -0
- package/examples/slides-neon/deck.config.ts +13 -0
- package/examples/slides-retro/01-welcome.md +9 -0
- package/examples/slides-retro/02-features.md +12 -0
- package/examples/slides-retro/03-colors.md +17 -0
- package/examples/slides-retro/04-ascii-art.md +11 -0
- package/examples/slides-retro/05-gradients.md +14 -0
- package/examples/slides-retro/06-themes.md +13 -0
- package/examples/slides-retro/07-markdown.md +13 -0
- package/examples/slides-retro/08-controls.md +13 -0
- package/examples/slides-retro/09-thanks.md +11 -0
- package/examples/slides-retro/deck.config.ts +13 -0
- package/package.json +66 -0
- package/src/cli/__tests__/errors.test.ts +201 -0
- package/src/cli/__tests__/help.test.ts +157 -0
- package/src/cli/__tests__/init.test.ts +110 -0
- package/src/cli/commands/export.ts +33 -0
- package/src/cli/commands/init.ts +125 -0
- package/src/cli/commands/present.ts +29 -0
- package/src/cli/errors.ts +77 -0
- package/src/core/__tests__/slide.test.ts +1759 -0
- package/src/core/__tests__/theme.test.ts +1103 -0
- package/src/core/slide.ts +509 -0
- package/src/core/theme.ts +388 -0
- package/src/export/__tests__/recorder.test.ts +566 -0
- package/src/export/recorder.ts +639 -0
- package/src/index.ts +36 -0
- package/src/presenter/__tests__/main.test.ts +244 -0
- package/src/presenter/main.ts +658 -0
- package/src/renderer/__tests__/screen-extended.test.ts +801 -0
- package/src/renderer/__tests__/screen.test.ts +525 -0
- package/src/renderer/screen.ts +671 -0
- package/src/schemas/__tests__/config.test.ts +429 -0
- package/src/schemas/__tests__/slide.test.ts +349 -0
- package/src/schemas/__tests__/theme.test.ts +970 -0
- package/src/schemas/__tests__/validation.test.ts +256 -0
- package/src/schemas/config.ts +58 -0
- package/src/schemas/slide.ts +56 -0
- package/src/schemas/theme.ts +203 -0
- package/src/schemas/validation.ts +64 -0
- package/src/themes/matrix/index.ts +53 -0
- package/themes/hacker.ts +53 -0
- package/themes/minimal.ts +53 -0
- package/themes/neon.ts +53 -0
- package/themes/retro.ts +53 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { HexColorSchema, GradientSchema, ThemeSchema, ColorTokens, COLOR_TOKEN_PATTERN, PartialThemeSchema, DEFAULT_THEME, type Theme, type ColorToken, type PartialTheme, type DeepPartial } from '../theme'
|
|
3
|
+
|
|
4
|
+
describe('HexColorSchema', () => {
|
|
5
|
+
it('accepts valid hex color #ff0066', () => {
|
|
6
|
+
expect(() => HexColorSchema.parse('#ff0066')).not.toThrow()
|
|
7
|
+
expect(HexColorSchema.parse('#ff0066')).toBe('#ff0066')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('accepts valid hex color with uppercase letters', () => {
|
|
11
|
+
expect(() => HexColorSchema.parse('#FF0066')).not.toThrow()
|
|
12
|
+
expect(HexColorSchema.parse('#AABBCC')).toBe('#AABBCC')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('accepts valid hex color with mixed case', () => {
|
|
16
|
+
expect(() => HexColorSchema.parse('#aAbBcC')).not.toThrow()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects color name "red"', () => {
|
|
20
|
+
const result = HexColorSchema.safeParse('red')
|
|
21
|
+
expect(result.success).toBe(false)
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
expect(result.error.issues[0].message).toContain('valid hex color')
|
|
24
|
+
expect(result.error.issues[0].message).toContain('#ff0066')
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects short hex "#fff"', () => {
|
|
29
|
+
const result = HexColorSchema.safeParse('#fff')
|
|
30
|
+
expect(result.success).toBe(false)
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
expect(result.error.issues[0].message).toContain('valid hex color')
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejects invalid hex characters "#GGGGGG"', () => {
|
|
37
|
+
const result = HexColorSchema.safeParse('#GGGGGG')
|
|
38
|
+
expect(result.success).toBe(false)
|
|
39
|
+
if (!result.success) {
|
|
40
|
+
expect(result.error.issues[0].message).toContain('valid hex color')
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('rejects hex without # prefix', () => {
|
|
45
|
+
const result = HexColorSchema.safeParse('ff0066')
|
|
46
|
+
expect(result.success).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rejects 8-digit hex (with alpha)', () => {
|
|
50
|
+
const result = HexColorSchema.safeParse('#ff006699')
|
|
51
|
+
expect(result.success).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('error message includes example of valid format', () => {
|
|
55
|
+
const result = HexColorSchema.safeParse('invalid')
|
|
56
|
+
expect(result.success).toBe(false)
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
expect(result.error.issues[0].message).toContain('#ff0066')
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('GradientSchema', () => {
|
|
64
|
+
it('accepts valid gradient with 2 colors', () => {
|
|
65
|
+
const gradient = ['#ff0000', '#00ff00']
|
|
66
|
+
expect(() => GradientSchema.parse(gradient)).not.toThrow()
|
|
67
|
+
expect(GradientSchema.parse(gradient)).toEqual(['#ff0000', '#00ff00'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('accepts gradient with 3+ colors', () => {
|
|
71
|
+
const gradient = ['#ff0000', '#00ff00', '#0000ff']
|
|
72
|
+
expect(() => GradientSchema.parse(gradient)).not.toThrow()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('accepts gradient with many colors', () => {
|
|
76
|
+
const gradient = ['#ff0000', '#ff6600', '#ffff00', '#00ff00', '#0000ff', '#6600ff']
|
|
77
|
+
expect(() => GradientSchema.parse(gradient)).not.toThrow()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects single color array', () => {
|
|
81
|
+
const result = GradientSchema.safeParse(['#ff0000'])
|
|
82
|
+
expect(result.success).toBe(false)
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
expect(result.error.issues[0].message).toContain('at least 2 colors')
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('rejects empty array', () => {
|
|
89
|
+
const result = GradientSchema.safeParse([])
|
|
90
|
+
expect(result.success).toBe(false)
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
expect(result.error.issues[0].message).toContain('at least 2 colors')
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('rejects gradient with invalid hex color', () => {
|
|
97
|
+
const result = GradientSchema.safeParse(['#ff0000', 'red'])
|
|
98
|
+
expect(result.success).toBe(false)
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
expect(result.error.issues[0].message).toContain('valid hex color')
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('rejects gradient with short hex', () => {
|
|
105
|
+
const result = GradientSchema.safeParse(['#ff0000', '#fff'])
|
|
106
|
+
expect(result.success).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('validates each color in gradient', () => {
|
|
110
|
+
const result = GradientSchema.safeParse(['invalid', 'also-invalid'])
|
|
111
|
+
expect(result.success).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('ThemeSchema', () => {
|
|
116
|
+
// Valid minimal theme for testing
|
|
117
|
+
const validMinimalTheme = {
|
|
118
|
+
name: 'test',
|
|
119
|
+
colors: {
|
|
120
|
+
primary: '#ff0066',
|
|
121
|
+
accent: '#00ff66',
|
|
122
|
+
background: '#000000',
|
|
123
|
+
text: '#ffffff',
|
|
124
|
+
muted: '#666666',
|
|
125
|
+
},
|
|
126
|
+
gradients: {
|
|
127
|
+
fire: ['#ff0000', '#ff6600'],
|
|
128
|
+
},
|
|
129
|
+
glyphs: '0123456789abcdef',
|
|
130
|
+
animations: {
|
|
131
|
+
revealSpeed: 1.0,
|
|
132
|
+
matrixDensity: 50,
|
|
133
|
+
glitchIterations: 5,
|
|
134
|
+
lineDelay: 30,
|
|
135
|
+
matrixInterval: 80,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Valid full theme with all optional fields
|
|
140
|
+
const validFullTheme = {
|
|
141
|
+
name: 'full-test',
|
|
142
|
+
description: 'A test theme with all fields',
|
|
143
|
+
author: 'Test Author',
|
|
144
|
+
version: '1.0.0',
|
|
145
|
+
colors: {
|
|
146
|
+
primary: '#ff0066',
|
|
147
|
+
secondary: '#0066ff',
|
|
148
|
+
accent: '#00ff66',
|
|
149
|
+
background: '#000000',
|
|
150
|
+
text: '#ffffff',
|
|
151
|
+
muted: '#666666',
|
|
152
|
+
success: '#00cc00',
|
|
153
|
+
warning: '#ffcc00',
|
|
154
|
+
error: '#cc0000',
|
|
155
|
+
},
|
|
156
|
+
gradients: {
|
|
157
|
+
fire: ['#ff0000', '#ff6600'],
|
|
158
|
+
cool: ['#0066ff', '#00ccff', '#00ffcc'],
|
|
159
|
+
},
|
|
160
|
+
glyphs: '0123456789abcdefghijklmnop',
|
|
161
|
+
animations: {
|
|
162
|
+
revealSpeed: 1.5,
|
|
163
|
+
matrixDensity: 75,
|
|
164
|
+
glitchIterations: 10,
|
|
165
|
+
lineDelay: 50,
|
|
166
|
+
matrixInterval: 100,
|
|
167
|
+
},
|
|
168
|
+
window: {
|
|
169
|
+
borderStyle: 'double' as const,
|
|
170
|
+
shadow: false,
|
|
171
|
+
padding: {
|
|
172
|
+
top: 2,
|
|
173
|
+
bottom: 2,
|
|
174
|
+
left: 4,
|
|
175
|
+
right: 4,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe('accepts valid themes', () => {
|
|
181
|
+
it('accepts valid minimal theme', () => {
|
|
182
|
+
expect(() => ThemeSchema.parse(validMinimalTheme)).not.toThrow()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('accepts valid full theme with all optional fields', () => {
|
|
186
|
+
expect(() => ThemeSchema.parse(validFullTheme)).not.toThrow()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('applies default values for animation settings', () => {
|
|
190
|
+
const themeWithEmptyAnimations = {
|
|
191
|
+
...validMinimalTheme,
|
|
192
|
+
animations: {},
|
|
193
|
+
}
|
|
194
|
+
const parsed = ThemeSchema.parse(themeWithEmptyAnimations)
|
|
195
|
+
expect(parsed.animations.revealSpeed).toBe(1.0)
|
|
196
|
+
expect(parsed.animations.matrixDensity).toBe(50)
|
|
197
|
+
expect(parsed.animations.glitchIterations).toBe(5)
|
|
198
|
+
expect(parsed.animations.lineDelay).toBe(30)
|
|
199
|
+
expect(parsed.animations.matrixInterval).toBe(80)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('applies default values for window settings', () => {
|
|
203
|
+
const themeWithEmptyWindow = {
|
|
204
|
+
...validMinimalTheme,
|
|
205
|
+
window: {},
|
|
206
|
+
}
|
|
207
|
+
const parsed = ThemeSchema.parse(themeWithEmptyWindow)
|
|
208
|
+
expect(parsed.window?.borderStyle).toBe('line')
|
|
209
|
+
expect(parsed.window?.shadow).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('validates name field', () => {
|
|
214
|
+
it('rejects missing name', () => {
|
|
215
|
+
const { name, ...themeWithoutName } = validMinimalTheme
|
|
216
|
+
const result = ThemeSchema.safeParse(themeWithoutName)
|
|
217
|
+
expect(result.success).toBe(false)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('rejects empty name', () => {
|
|
221
|
+
const themeWithEmptyName = { ...validMinimalTheme, name: '' }
|
|
222
|
+
const result = ThemeSchema.safeParse(themeWithEmptyName)
|
|
223
|
+
expect(result.success).toBe(false)
|
|
224
|
+
if (!result.success) {
|
|
225
|
+
expect(result.error.issues[0].message).toContain('Theme name is required')
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('validates colors field', () => {
|
|
231
|
+
it('rejects missing required colors', () => {
|
|
232
|
+
const themeWithMissingPrimary = {
|
|
233
|
+
...validMinimalTheme,
|
|
234
|
+
colors: {
|
|
235
|
+
accent: '#00ff66',
|
|
236
|
+
background: '#000000',
|
|
237
|
+
text: '#ffffff',
|
|
238
|
+
muted: '#666666',
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
const result = ThemeSchema.safeParse(themeWithMissingPrimary)
|
|
242
|
+
expect(result.success).toBe(false)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('rejects invalid hex color', () => {
|
|
246
|
+
const themeWithInvalidColor = {
|
|
247
|
+
...validMinimalTheme,
|
|
248
|
+
colors: {
|
|
249
|
+
...validMinimalTheme.colors,
|
|
250
|
+
primary: 'red',
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
const result = ThemeSchema.safeParse(themeWithInvalidColor)
|
|
254
|
+
expect(result.success).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('accepts optional colors', () => {
|
|
258
|
+
const themeWithOptionalColors = {
|
|
259
|
+
...validMinimalTheme,
|
|
260
|
+
colors: {
|
|
261
|
+
...validMinimalTheme.colors,
|
|
262
|
+
secondary: '#0066ff',
|
|
263
|
+
success: '#00cc00',
|
|
264
|
+
warning: '#ffcc00',
|
|
265
|
+
error: '#cc0000',
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
expect(() => ThemeSchema.parse(themeWithOptionalColors)).not.toThrow()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('validates gradients field', () => {
|
|
273
|
+
it('rejects empty gradients object', () => {
|
|
274
|
+
const themeWithNoGradients = {
|
|
275
|
+
...validMinimalTheme,
|
|
276
|
+
gradients: {},
|
|
277
|
+
}
|
|
278
|
+
const result = ThemeSchema.safeParse(themeWithNoGradients)
|
|
279
|
+
expect(result.success).toBe(false)
|
|
280
|
+
if (!result.success) {
|
|
281
|
+
expect(result.error.issues[0].message).toContain('At least one gradient must be defined')
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('rejects gradient with single color', () => {
|
|
286
|
+
const themeWithInvalidGradient = {
|
|
287
|
+
...validMinimalTheme,
|
|
288
|
+
gradients: {
|
|
289
|
+
fire: ['#ff0000'],
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
const result = ThemeSchema.safeParse(themeWithInvalidGradient)
|
|
293
|
+
expect(result.success).toBe(false)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('accepts multiple gradients', () => {
|
|
297
|
+
const themeWithMultipleGradients = {
|
|
298
|
+
...validMinimalTheme,
|
|
299
|
+
gradients: {
|
|
300
|
+
fire: ['#ff0000', '#ff6600'],
|
|
301
|
+
cool: ['#0066ff', '#00ccff'],
|
|
302
|
+
rainbow: ['#ff0000', '#ff6600', '#ffff00', '#00ff00', '#0066ff', '#6600ff'],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
expect(() => ThemeSchema.parse(themeWithMultipleGradients)).not.toThrow()
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe('validates glyphs field', () => {
|
|
310
|
+
it('rejects glyphs with less than 10 characters', () => {
|
|
311
|
+
const themeWithShortGlyphs = {
|
|
312
|
+
...validMinimalTheme,
|
|
313
|
+
glyphs: '012345678',
|
|
314
|
+
}
|
|
315
|
+
const result = ThemeSchema.safeParse(themeWithShortGlyphs)
|
|
316
|
+
expect(result.success).toBe(false)
|
|
317
|
+
if (!result.success) {
|
|
318
|
+
expect(result.error.issues[0].message).toContain('at least 10 characters')
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('accepts glyphs with exactly 10 characters', () => {
|
|
323
|
+
const themeWith10Glyphs = {
|
|
324
|
+
...validMinimalTheme,
|
|
325
|
+
glyphs: '0123456789',
|
|
326
|
+
}
|
|
327
|
+
expect(() => ThemeSchema.parse(themeWith10Glyphs)).not.toThrow()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('accepts glyphs with unicode characters', () => {
|
|
331
|
+
const themeWithUnicodeGlyphs = {
|
|
332
|
+
...validMinimalTheme,
|
|
333
|
+
glyphs: 'アイウエオカキクケコ',
|
|
334
|
+
}
|
|
335
|
+
expect(() => ThemeSchema.parse(themeWithUnicodeGlyphs)).not.toThrow()
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('validates animations field', () => {
|
|
340
|
+
it('rejects revealSpeed below 0.1', () => {
|
|
341
|
+
const themeWithLowSpeed = {
|
|
342
|
+
...validMinimalTheme,
|
|
343
|
+
animations: { ...validMinimalTheme.animations, revealSpeed: 0.05 },
|
|
344
|
+
}
|
|
345
|
+
const result = ThemeSchema.safeParse(themeWithLowSpeed)
|
|
346
|
+
expect(result.success).toBe(false)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('rejects revealSpeed above 5.0', () => {
|
|
350
|
+
const themeWithHighSpeed = {
|
|
351
|
+
...validMinimalTheme,
|
|
352
|
+
animations: { ...validMinimalTheme.animations, revealSpeed: 6.0 },
|
|
353
|
+
}
|
|
354
|
+
const result = ThemeSchema.safeParse(themeWithHighSpeed)
|
|
355
|
+
expect(result.success).toBe(false)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('rejects matrixDensity below 10', () => {
|
|
359
|
+
const themeWithLowDensity = {
|
|
360
|
+
...validMinimalTheme,
|
|
361
|
+
animations: { ...validMinimalTheme.animations, matrixDensity: 5 },
|
|
362
|
+
}
|
|
363
|
+
const result = ThemeSchema.safeParse(themeWithLowDensity)
|
|
364
|
+
expect(result.success).toBe(false)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('rejects matrixDensity above 200', () => {
|
|
368
|
+
const themeWithHighDensity = {
|
|
369
|
+
...validMinimalTheme,
|
|
370
|
+
animations: { ...validMinimalTheme.animations, matrixDensity: 250 },
|
|
371
|
+
}
|
|
372
|
+
const result = ThemeSchema.safeParse(themeWithHighDensity)
|
|
373
|
+
expect(result.success).toBe(false)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('rejects glitchIterations below 1', () => {
|
|
377
|
+
const themeWithZeroIterations = {
|
|
378
|
+
...validMinimalTheme,
|
|
379
|
+
animations: { ...validMinimalTheme.animations, glitchIterations: 0 },
|
|
380
|
+
}
|
|
381
|
+
const result = ThemeSchema.safeParse(themeWithZeroIterations)
|
|
382
|
+
expect(result.success).toBe(false)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('rejects glitchIterations above 20', () => {
|
|
386
|
+
const themeWithHighIterations = {
|
|
387
|
+
...validMinimalTheme,
|
|
388
|
+
animations: { ...validMinimalTheme.animations, glitchIterations: 25 },
|
|
389
|
+
}
|
|
390
|
+
const result = ThemeSchema.safeParse(themeWithHighIterations)
|
|
391
|
+
expect(result.success).toBe(false)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('rejects lineDelay below 0', () => {
|
|
395
|
+
const themeWithNegativeDelay = {
|
|
396
|
+
...validMinimalTheme,
|
|
397
|
+
animations: { ...validMinimalTheme.animations, lineDelay: -10 },
|
|
398
|
+
}
|
|
399
|
+
const result = ThemeSchema.safeParse(themeWithNegativeDelay)
|
|
400
|
+
expect(result.success).toBe(false)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('rejects lineDelay above 500', () => {
|
|
404
|
+
const themeWithHighDelay = {
|
|
405
|
+
...validMinimalTheme,
|
|
406
|
+
animations: { ...validMinimalTheme.animations, lineDelay: 600 },
|
|
407
|
+
}
|
|
408
|
+
const result = ThemeSchema.safeParse(themeWithHighDelay)
|
|
409
|
+
expect(result.success).toBe(false)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('rejects matrixInterval below 20', () => {
|
|
413
|
+
const themeWithLowInterval = {
|
|
414
|
+
...validMinimalTheme,
|
|
415
|
+
animations: { ...validMinimalTheme.animations, matrixInterval: 10 },
|
|
416
|
+
}
|
|
417
|
+
const result = ThemeSchema.safeParse(themeWithLowInterval)
|
|
418
|
+
expect(result.success).toBe(false)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('rejects matrixInterval above 200', () => {
|
|
422
|
+
const themeWithHighInterval = {
|
|
423
|
+
...validMinimalTheme,
|
|
424
|
+
animations: { ...validMinimalTheme.animations, matrixInterval: 250 },
|
|
425
|
+
}
|
|
426
|
+
const result = ThemeSchema.safeParse(themeWithHighInterval)
|
|
427
|
+
expect(result.success).toBe(false)
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
describe('validates window field', () => {
|
|
432
|
+
it('accepts valid borderStyle values', () => {
|
|
433
|
+
const borderStyles = ['line', 'double', 'rounded', 'none'] as const
|
|
434
|
+
for (const borderStyle of borderStyles) {
|
|
435
|
+
const themeWithBorder = {
|
|
436
|
+
...validMinimalTheme,
|
|
437
|
+
window: { borderStyle, shadow: true },
|
|
438
|
+
}
|
|
439
|
+
expect(() => ThemeSchema.parse(themeWithBorder)).not.toThrow()
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('rejects invalid borderStyle value', () => {
|
|
444
|
+
const themeWithInvalidBorder = {
|
|
445
|
+
...validMinimalTheme,
|
|
446
|
+
window: { borderStyle: 'dashed', shadow: true },
|
|
447
|
+
}
|
|
448
|
+
const result = ThemeSchema.safeParse(themeWithInvalidBorder)
|
|
449
|
+
expect(result.success).toBe(false)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('validates padding constraints', () => {
|
|
453
|
+
const themeWithInvalidPadding = {
|
|
454
|
+
...validMinimalTheme,
|
|
455
|
+
window: {
|
|
456
|
+
borderStyle: 'line' as const,
|
|
457
|
+
shadow: true,
|
|
458
|
+
padding: { top: 10, bottom: 1, left: 2, right: 2 },
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
const result = ThemeSchema.safeParse(themeWithInvalidPadding)
|
|
462
|
+
expect(result.success).toBe(false)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('applies default padding values', () => {
|
|
466
|
+
const themeWithEmptyPadding = {
|
|
467
|
+
...validMinimalTheme,
|
|
468
|
+
window: {
|
|
469
|
+
borderStyle: 'line' as const,
|
|
470
|
+
shadow: true,
|
|
471
|
+
padding: {},
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
const parsed = ThemeSchema.parse(themeWithEmptyPadding)
|
|
475
|
+
expect(parsed.window?.padding?.top).toBe(1)
|
|
476
|
+
expect(parsed.window?.padding?.bottom).toBe(1)
|
|
477
|
+
expect(parsed.window?.padding?.left).toBe(2)
|
|
478
|
+
expect(parsed.window?.padding?.right).toBe(2)
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
describe('type inference', () => {
|
|
483
|
+
it('infers correct type from parsed theme', () => {
|
|
484
|
+
const parsed: Theme = ThemeSchema.parse(validFullTheme)
|
|
485
|
+
expect(parsed.name).toBe('full-test')
|
|
486
|
+
expect(parsed.colors.primary).toBe('#ff0066')
|
|
487
|
+
expect(parsed.gradients.fire).toEqual(['#ff0000', '#ff6600'])
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('ColorTokens', () => {
|
|
493
|
+
it('includes all required built-in color tokens', () => {
|
|
494
|
+
expect(ColorTokens).toContain('GREEN')
|
|
495
|
+
expect(ColorTokens).toContain('ORANGE')
|
|
496
|
+
expect(ColorTokens).toContain('CYAN')
|
|
497
|
+
expect(ColorTokens).toContain('PINK')
|
|
498
|
+
expect(ColorTokens).toContain('WHITE')
|
|
499
|
+
expect(ColorTokens).toContain('GRAY')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('includes all required theme-mapped color tokens', () => {
|
|
503
|
+
expect(ColorTokens).toContain('PRIMARY')
|
|
504
|
+
expect(ColorTokens).toContain('SECONDARY')
|
|
505
|
+
expect(ColorTokens).toContain('ACCENT')
|
|
506
|
+
expect(ColorTokens).toContain('MUTED')
|
|
507
|
+
expect(ColorTokens).toContain('TEXT')
|
|
508
|
+
expect(ColorTokens).toContain('BACKGROUND')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('has exactly 12 color tokens', () => {
|
|
512
|
+
expect(ColorTokens.length).toBe(12)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('is a const array (readonly)', () => {
|
|
516
|
+
// This ensures TypeScript treats it as a tuple of literal types
|
|
517
|
+
const firstToken: ColorToken = ColorTokens[0]
|
|
518
|
+
expect(firstToken).toBe('GREEN')
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('COLOR_TOKEN_PATTERN', () => {
|
|
523
|
+
it('matches {GREEN} token', () => {
|
|
524
|
+
const text = 'Hello {GREEN}world{/}'
|
|
525
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
526
|
+
expect(matches).toEqual(['{GREEN}', '{/}'])
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('matches {ORANGE} token', () => {
|
|
530
|
+
const text = '{ORANGE}warning{/}'
|
|
531
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
532
|
+
expect(matches).toEqual(['{ORANGE}', '{/}'])
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('matches {CYAN} token', () => {
|
|
536
|
+
const text = '{CYAN}info{/}'
|
|
537
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
538
|
+
expect(matches).toEqual(['{CYAN}', '{/}'])
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('matches {PINK} token', () => {
|
|
542
|
+
const text = '{PINK}highlight{/}'
|
|
543
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
544
|
+
expect(matches).toEqual(['{PINK}', '{/}'])
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('matches {WHITE} token', () => {
|
|
548
|
+
const text = '{WHITE}text{/}'
|
|
549
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
550
|
+
expect(matches).toEqual(['{WHITE}', '{/}'])
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('matches {GRAY} token', () => {
|
|
554
|
+
const text = '{GRAY}muted text{/}'
|
|
555
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
556
|
+
expect(matches).toEqual(['{GRAY}', '{/}'])
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('matches {PRIMARY} theme token', () => {
|
|
560
|
+
const text = '{PRIMARY}main color{/}'
|
|
561
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
562
|
+
expect(matches).toEqual(['{PRIMARY}', '{/}'])
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('matches {SECONDARY} theme token', () => {
|
|
566
|
+
const text = '{SECONDARY}secondary color{/}'
|
|
567
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
568
|
+
expect(matches).toEqual(['{SECONDARY}', '{/}'])
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('matches {ACCENT} theme token', () => {
|
|
572
|
+
const text = '{ACCENT}accent color{/}'
|
|
573
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
574
|
+
expect(matches).toEqual(['{ACCENT}', '{/}'])
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('matches {MUTED} theme token', () => {
|
|
578
|
+
const text = '{MUTED}muted color{/}'
|
|
579
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
580
|
+
expect(matches).toEqual(['{MUTED}', '{/}'])
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('matches {TEXT} theme token', () => {
|
|
584
|
+
const text = '{TEXT}text color{/}'
|
|
585
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
586
|
+
expect(matches).toEqual(['{TEXT}', '{/}'])
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('matches {BACKGROUND} theme token', () => {
|
|
590
|
+
const text = '{BACKGROUND}bg color{/}'
|
|
591
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
592
|
+
expect(matches).toEqual(['{BACKGROUND}', '{/}'])
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('matches closing tag {/}', () => {
|
|
596
|
+
const text = '{GREEN}text{/}'
|
|
597
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
598
|
+
expect(matches).toContain('{/}')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('matches multiple tokens in same string', () => {
|
|
602
|
+
const text = '{GREEN}green{/} and {ORANGE}orange{/} text'
|
|
603
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
604
|
+
expect(matches).toEqual(['{GREEN}', '{/}', '{ORANGE}', '{/}'])
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
it('matches nested/adjacent tokens', () => {
|
|
608
|
+
const text = '{PRIMARY}{ACCENT}double styled{/}{/}'
|
|
609
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
610
|
+
expect(matches).toEqual(['{PRIMARY}', '{ACCENT}', '{/}', '{/}'])
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('does not match lowercase tokens', () => {
|
|
614
|
+
const text = '{green}text{/}'
|
|
615
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
616
|
+
expect(matches).toEqual(['{/}'])
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('does not match invalid tokens', () => {
|
|
620
|
+
const text = '{RED}text{/} and {BLUE}more{/}'
|
|
621
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
622
|
+
expect(matches).toEqual(['{/}', '{/}'])
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('does not match tokens without braces', () => {
|
|
626
|
+
const text = 'GREEN text ORANGE more'
|
|
627
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
628
|
+
expect(matches).toBeNull()
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('does not match partial braces', () => {
|
|
632
|
+
const text = '{GREEN text} and GREEN}'
|
|
633
|
+
const matches = text.match(COLOR_TOKEN_PATTERN)
|
|
634
|
+
expect(matches).toBeNull()
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('works with replaceAll for token processing', () => {
|
|
638
|
+
const text = '{GREEN}hello{/}'
|
|
639
|
+
const processed = text.replace(COLOR_TOKEN_PATTERN, (match, token) => {
|
|
640
|
+
if (token === '/') return '{/}'
|
|
641
|
+
return `{#00cc66-fg}`
|
|
642
|
+
})
|
|
643
|
+
expect(processed).toBe('{#00cc66-fg}hello{/}')
|
|
644
|
+
})
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
describe('PartialThemeSchema', () => {
|
|
648
|
+
describe('accepts partial theme objects', () => {
|
|
649
|
+
it('accepts empty object', () => {
|
|
650
|
+
const partial = {}
|
|
651
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('accepts only name field', () => {
|
|
655
|
+
const partial = { name: 'my-theme' }
|
|
656
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
it('accepts only colors field with partial colors', () => {
|
|
660
|
+
const partial = {
|
|
661
|
+
colors: {
|
|
662
|
+
primary: '#ff0066',
|
|
663
|
+
},
|
|
664
|
+
}
|
|
665
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('accepts only gradients field', () => {
|
|
669
|
+
const partial = {
|
|
670
|
+
gradients: {
|
|
671
|
+
fire: ['#ff0000', '#ff6600'],
|
|
672
|
+
},
|
|
673
|
+
}
|
|
674
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('accepts only animations field with partial values', () => {
|
|
678
|
+
const partial = {
|
|
679
|
+
animations: {
|
|
680
|
+
revealSpeed: 2.0,
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('accepts only window field with partial values', () => {
|
|
687
|
+
const partial = {
|
|
688
|
+
window: {
|
|
689
|
+
shadow: false,
|
|
690
|
+
},
|
|
691
|
+
}
|
|
692
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('accepts deeply nested partial values', () => {
|
|
696
|
+
const partial = {
|
|
697
|
+
window: {
|
|
698
|
+
padding: {
|
|
699
|
+
top: 3,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
}
|
|
703
|
+
expect(() => PartialThemeSchema.parse(partial)).not.toThrow()
|
|
704
|
+
})
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
describe('can be used for theme extension', () => {
|
|
708
|
+
it('allows overriding just primary color', () => {
|
|
709
|
+
const baseTheme: Theme = {
|
|
710
|
+
name: 'base',
|
|
711
|
+
colors: {
|
|
712
|
+
primary: '#000000',
|
|
713
|
+
accent: '#111111',
|
|
714
|
+
background: '#222222',
|
|
715
|
+
text: '#333333',
|
|
716
|
+
muted: '#444444',
|
|
717
|
+
},
|
|
718
|
+
gradients: { fire: ['#ff0000', '#ff6600'] },
|
|
719
|
+
glyphs: '0123456789abcdef',
|
|
720
|
+
animations: {
|
|
721
|
+
revealSpeed: 1.0,
|
|
722
|
+
matrixDensity: 50,
|
|
723
|
+
glitchIterations: 5,
|
|
724
|
+
lineDelay: 30,
|
|
725
|
+
matrixInterval: 80,
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const overrides: PartialTheme = {
|
|
730
|
+
colors: {
|
|
731
|
+
primary: '#ff0066',
|
|
732
|
+
},
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Verify overrides parse correctly
|
|
736
|
+
expect(() => PartialThemeSchema.parse(overrides)).not.toThrow()
|
|
737
|
+
|
|
738
|
+
// Simulate merge for theme extension
|
|
739
|
+
const merged = {
|
|
740
|
+
...baseTheme,
|
|
741
|
+
colors: {
|
|
742
|
+
...baseTheme.colors,
|
|
743
|
+
...overrides.colors,
|
|
744
|
+
},
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
expect(merged.colors.primary).toBe('#ff0066')
|
|
748
|
+
expect(merged.colors.accent).toBe('#111111') // Preserved from base
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('allows adding new gradient', () => {
|
|
752
|
+
const overrides: PartialTheme = {
|
|
753
|
+
gradients: {
|
|
754
|
+
cool: ['#0066ff', '#00ccff'],
|
|
755
|
+
},
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
expect(() => PartialThemeSchema.parse(overrides)).not.toThrow()
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('allows overriding animation settings', () => {
|
|
762
|
+
const overrides: PartialTheme = {
|
|
763
|
+
animations: {
|
|
764
|
+
revealSpeed: 2.5,
|
|
765
|
+
matrixDensity: 100,
|
|
766
|
+
},
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
expect(() => PartialThemeSchema.parse(overrides)).not.toThrow()
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('allows overriding window padding', () => {
|
|
773
|
+
const overrides: PartialTheme = {
|
|
774
|
+
window: {
|
|
775
|
+
padding: {
|
|
776
|
+
left: 4,
|
|
777
|
+
right: 4,
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
expect(() => PartialThemeSchema.parse(overrides)).not.toThrow()
|
|
783
|
+
})
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
describe('validates partial values against constraints', () => {
|
|
787
|
+
it('still validates hex colors in partial theme', () => {
|
|
788
|
+
const partial = {
|
|
789
|
+
colors: {
|
|
790
|
+
primary: 'red', // Invalid - should be hex
|
|
791
|
+
},
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const result = PartialThemeSchema.safeParse(partial)
|
|
795
|
+
expect(result.success).toBe(false)
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
it('still validates gradient array min length', () => {
|
|
799
|
+
const partial = {
|
|
800
|
+
gradients: {
|
|
801
|
+
fire: ['#ff0000'], // Invalid - needs 2+ colors
|
|
802
|
+
},
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const result = PartialThemeSchema.safeParse(partial)
|
|
806
|
+
expect(result.success).toBe(false)
|
|
807
|
+
})
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
describe('type compatibility', () => {
|
|
811
|
+
it('PartialTheme type allows all optional fields', () => {
|
|
812
|
+
const partial: PartialTheme = {}
|
|
813
|
+
expect(partial).toEqual({})
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
it('PartialTheme type allows nested optional fields', () => {
|
|
817
|
+
const partial: PartialTheme = {
|
|
818
|
+
colors: {
|
|
819
|
+
primary: '#ff0066',
|
|
820
|
+
},
|
|
821
|
+
animations: {
|
|
822
|
+
revealSpeed: 2.0,
|
|
823
|
+
},
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
expect(partial.colors?.primary).toBe('#ff0066')
|
|
827
|
+
expect(partial.animations?.revealSpeed).toBe(2.0)
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
it('DeepPartial type works with nested objects', () => {
|
|
831
|
+
type TestObj = { a: { b: { c: string } } }
|
|
832
|
+
type PartialTestObj = DeepPartial<TestObj>
|
|
833
|
+
|
|
834
|
+
const partial: PartialTestObj = { a: { b: {} } }
|
|
835
|
+
expect(partial.a?.b).toEqual({})
|
|
836
|
+
})
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
describe('DEFAULT_THEME', () => {
|
|
841
|
+
describe('passes ThemeSchema validation', () => {
|
|
842
|
+
it('passes ThemeSchema.parse without throwing', () => {
|
|
843
|
+
expect(() => ThemeSchema.parse(DEFAULT_THEME)).not.toThrow()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('is a valid Theme type', () => {
|
|
847
|
+
const theme: Theme = DEFAULT_THEME
|
|
848
|
+
expect(theme).toBeDefined()
|
|
849
|
+
})
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
describe('has correct name and metadata', () => {
|
|
853
|
+
it('has name "matrix"', () => {
|
|
854
|
+
expect(DEFAULT_THEME.name).toBe('matrix')
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
it('has description', () => {
|
|
858
|
+
expect(DEFAULT_THEME.description).toBe('Default cyberpunk/matrix theme')
|
|
859
|
+
})
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
describe('has correct colors', () => {
|
|
863
|
+
it('has primary color #00cc66', () => {
|
|
864
|
+
expect(DEFAULT_THEME.colors.primary).toBe('#00cc66')
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
it('has accent color #ff6600', () => {
|
|
868
|
+
expect(DEFAULT_THEME.colors.accent).toBe('#ff6600')
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('has background color #0a0a0a', () => {
|
|
872
|
+
expect(DEFAULT_THEME.colors.background).toBe('#0a0a0a')
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
it('has text color #ffffff', () => {
|
|
876
|
+
expect(DEFAULT_THEME.colors.text).toBe('#ffffff')
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
it('has muted color #666666', () => {
|
|
880
|
+
expect(DEFAULT_THEME.colors.muted).toBe('#666666')
|
|
881
|
+
})
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
describe('has correct gradients', () => {
|
|
885
|
+
it('has fire gradient', () => {
|
|
886
|
+
expect(DEFAULT_THEME.gradients.fire).toEqual(['#ff6600', '#ff3300', '#ff0066'])
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('has cool gradient', () => {
|
|
890
|
+
expect(DEFAULT_THEME.gradients.cool).toEqual(['#00ccff', '#0066ff', '#6600ff'])
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('has pink gradient', () => {
|
|
894
|
+
expect(DEFAULT_THEME.gradients.pink).toEqual(['#ff0066', '#ff0099', '#cc00ff'])
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
it('has hf gradient', () => {
|
|
898
|
+
expect(DEFAULT_THEME.gradients.hf).toEqual(['#99cc00', '#00cc66', '#00cccc'])
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
it('has 4 default gradients', () => {
|
|
902
|
+
expect(Object.keys(DEFAULT_THEME.gradients)).toHaveLength(4)
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
describe('has correct glyphs', () => {
|
|
907
|
+
it('includes katakana characters', () => {
|
|
908
|
+
expect(DEFAULT_THEME.glyphs).toContain('ア')
|
|
909
|
+
expect(DEFAULT_THEME.glyphs).toContain('イ')
|
|
910
|
+
expect(DEFAULT_THEME.glyphs).toContain('ウ')
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
it('includes digits', () => {
|
|
914
|
+
expect(DEFAULT_THEME.glyphs).toContain('0')
|
|
915
|
+
expect(DEFAULT_THEME.glyphs).toContain('9')
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
it('has at least 10 characters', () => {
|
|
919
|
+
expect(DEFAULT_THEME.glyphs.length).toBeGreaterThanOrEqual(10)
|
|
920
|
+
})
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
describe('has correct animation settings', () => {
|
|
924
|
+
it('has revealSpeed 1.0', () => {
|
|
925
|
+
expect(DEFAULT_THEME.animations.revealSpeed).toBe(1.0)
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
it('has matrixDensity 50', () => {
|
|
929
|
+
expect(DEFAULT_THEME.animations.matrixDensity).toBe(50)
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('has glitchIterations 5', () => {
|
|
933
|
+
expect(DEFAULT_THEME.animations.glitchIterations).toBe(5)
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
it('has lineDelay 30', () => {
|
|
937
|
+
expect(DEFAULT_THEME.animations.lineDelay).toBe(30)
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
it('has matrixInterval 80', () => {
|
|
941
|
+
expect(DEFAULT_THEME.animations.matrixInterval).toBe(80)
|
|
942
|
+
})
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
describe('has correct window settings', () => {
|
|
946
|
+
it('has borderStyle "line"', () => {
|
|
947
|
+
expect(DEFAULT_THEME.window?.borderStyle).toBe('line')
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('has shadow true', () => {
|
|
951
|
+
expect(DEFAULT_THEME.window?.shadow).toBe(true)
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
it('has padding top 1', () => {
|
|
955
|
+
expect(DEFAULT_THEME.window?.padding?.top).toBe(1)
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it('has padding bottom 1', () => {
|
|
959
|
+
expect(DEFAULT_THEME.window?.padding?.bottom).toBe(1)
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('has padding left 2', () => {
|
|
963
|
+
expect(DEFAULT_THEME.window?.padding?.left).toBe(2)
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it('has padding right 2', () => {
|
|
967
|
+
expect(DEFAULT_THEME.window?.padding?.right).toBe(2)
|
|
968
|
+
})
|
|
969
|
+
})
|
|
970
|
+
})
|