@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,801 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
getWindowColor,
|
|
4
|
+
createWindow,
|
|
5
|
+
clearWindows,
|
|
6
|
+
renderMatrixRain,
|
|
7
|
+
initMatrixRain,
|
|
8
|
+
applyTransition,
|
|
9
|
+
renderSlide,
|
|
10
|
+
createRenderer,
|
|
11
|
+
destroyRenderer,
|
|
12
|
+
} from '../screen'
|
|
13
|
+
import { DEFAULT_THEME } from '../../schemas/theme'
|
|
14
|
+
import type { Slide } from '../../schemas/slide'
|
|
15
|
+
|
|
16
|
+
describe('getWindowColor', () => {
|
|
17
|
+
const mockTheme = {
|
|
18
|
+
...DEFAULT_THEME,
|
|
19
|
+
colors: {
|
|
20
|
+
...DEFAULT_THEME.colors,
|
|
21
|
+
primary: '#00cc66',
|
|
22
|
+
accent: '#ff6600',
|
|
23
|
+
secondary: '#0066ff',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('returns primary color for index 0', () => {
|
|
28
|
+
const color = getWindowColor(0, mockTheme)
|
|
29
|
+
expect(color).toBe('#00cc66')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns accent color for index 1', () => {
|
|
33
|
+
const color = getWindowColor(1, mockTheme)
|
|
34
|
+
expect(color).toBe('#ff6600')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns secondary color for index 2', () => {
|
|
38
|
+
const color = getWindowColor(2, mockTheme)
|
|
39
|
+
expect(color).toBe('#0066ff')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('cycles through extended color palette', () => {
|
|
43
|
+
const color3 = getWindowColor(3, mockTheme)
|
|
44
|
+
const color4 = getWindowColor(4, mockTheme)
|
|
45
|
+
const color5 = getWindowColor(5, mockTheme)
|
|
46
|
+
|
|
47
|
+
expect(color3).toBe('#ff0066') // pink
|
|
48
|
+
expect(color4).toBe('#9966ff') // purple
|
|
49
|
+
expect(color5).toBe('#ffcc00') // yellow
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('wraps around after all colors used', () => {
|
|
53
|
+
// Index 6 should wrap back to index 0
|
|
54
|
+
const color = getWindowColor(6, mockTheme)
|
|
55
|
+
expect(color).toBe('#00cc66')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('handles large indices correctly', () => {
|
|
59
|
+
const color = getWindowColor(12, mockTheme) // 12 % 6 = 0
|
|
60
|
+
expect(color).toBe('#00cc66')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('uses primary color as fallback when secondary is undefined', () => {
|
|
64
|
+
const themeNoSecondary = {
|
|
65
|
+
...mockTheme,
|
|
66
|
+
colors: {
|
|
67
|
+
...mockTheme.colors,
|
|
68
|
+
secondary: undefined,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const color = getWindowColor(2, themeNoSecondary as any)
|
|
73
|
+
expect(color).toBe('#00cc66') // falls back to primary
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('createWindow', () => {
|
|
78
|
+
it('creates a window with title', () => {
|
|
79
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
80
|
+
const window = createWindow(renderer, { title: 'Test Window' })
|
|
81
|
+
|
|
82
|
+
expect(window).toBeDefined()
|
|
83
|
+
expect(renderer.windowStack.length).toBe(1)
|
|
84
|
+
expect(renderer.windowStack[0]).toBe(window)
|
|
85
|
+
|
|
86
|
+
destroyRenderer(renderer)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('adds window to stack', () => {
|
|
90
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
91
|
+
|
|
92
|
+
expect(renderer.windowStack.length).toBe(0)
|
|
93
|
+
|
|
94
|
+
const window1 = createWindow(renderer, { title: 'Window 1' })
|
|
95
|
+
expect(renderer.windowStack.length).toBe(1)
|
|
96
|
+
|
|
97
|
+
const window2 = createWindow(renderer, { title: 'Window 2' })
|
|
98
|
+
expect(renderer.windowStack.length).toBe(2)
|
|
99
|
+
|
|
100
|
+
expect(renderer.windowStack[0]).toBe(window1)
|
|
101
|
+
expect(renderer.windowStack[1]).toBe(window2)
|
|
102
|
+
|
|
103
|
+
destroyRenderer(renderer)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('attaches window to screen', () => {
|
|
107
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
108
|
+
const window = createWindow(renderer, { title: 'Attached Window' })
|
|
109
|
+
|
|
110
|
+
expect(window.parent).toBe(renderer.screen)
|
|
111
|
+
|
|
112
|
+
destroyRenderer(renderer)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('uses custom color when provided', () => {
|
|
116
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
117
|
+
const customColor = '#ff00ff'
|
|
118
|
+
|
|
119
|
+
const window = createWindow(renderer, {
|
|
120
|
+
title: 'Custom Color',
|
|
121
|
+
color: customColor,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(window).toBeDefined()
|
|
125
|
+
|
|
126
|
+
destroyRenderer(renderer)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('uses theme-based color cycling when color not provided', () => {
|
|
130
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
131
|
+
|
|
132
|
+
// Create multiple windows without specifying color
|
|
133
|
+
createWindow(renderer, { title: 'Window 1' })
|
|
134
|
+
createWindow(renderer, { title: 'Window 2' })
|
|
135
|
+
createWindow(renderer, { title: 'Window 3' })
|
|
136
|
+
|
|
137
|
+
expect(renderer.windowStack.length).toBe(3)
|
|
138
|
+
|
|
139
|
+
destroyRenderer(renderer)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('accepts custom dimensions', () => {
|
|
143
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
144
|
+
|
|
145
|
+
const window = createWindow(renderer, {
|
|
146
|
+
title: 'Custom Size',
|
|
147
|
+
width: 50,
|
|
148
|
+
height: 20,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
expect(window).toBeDefined()
|
|
152
|
+
|
|
153
|
+
destroyRenderer(renderer)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('accepts custom position', () => {
|
|
157
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
158
|
+
|
|
159
|
+
const window = createWindow(renderer, {
|
|
160
|
+
title: 'Custom Position',
|
|
161
|
+
top: 5,
|
|
162
|
+
left: 10,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(window).toBeDefined()
|
|
166
|
+
|
|
167
|
+
destroyRenderer(renderer)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('uses default dimensions when not provided', () => {
|
|
171
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
172
|
+
|
|
173
|
+
const window = createWindow(renderer, {
|
|
174
|
+
title: 'Default Dimensions',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(window).toBeDefined()
|
|
178
|
+
|
|
179
|
+
destroyRenderer(renderer)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('creates multiple stacked windows', () => {
|
|
183
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < 5; i++) {
|
|
186
|
+
createWindow(renderer, { title: `Window ${i}` })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(renderer.windowStack.length).toBe(5)
|
|
190
|
+
|
|
191
|
+
destroyRenderer(renderer)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('clearWindows', () => {
|
|
196
|
+
it('destroys all windows in stack', () => {
|
|
197
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
198
|
+
|
|
199
|
+
const mockWindow1 = { destroy: mock(() => {}) }
|
|
200
|
+
const mockWindow2 = { destroy: mock(() => {}) }
|
|
201
|
+
const mockWindow3 = { destroy: mock(() => {}) }
|
|
202
|
+
|
|
203
|
+
renderer.windowStack = [mockWindow1 as any, mockWindow2 as any, mockWindow3 as any]
|
|
204
|
+
|
|
205
|
+
clearWindows(renderer)
|
|
206
|
+
|
|
207
|
+
expect(mockWindow1.destroy).toHaveBeenCalledTimes(1)
|
|
208
|
+
expect(mockWindow2.destroy).toHaveBeenCalledTimes(1)
|
|
209
|
+
expect(mockWindow3.destroy).toHaveBeenCalledTimes(1)
|
|
210
|
+
|
|
211
|
+
destroyRenderer(renderer)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('empties the window stack', () => {
|
|
215
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
216
|
+
|
|
217
|
+
createWindow(renderer, { title: 'Window 1' })
|
|
218
|
+
createWindow(renderer, { title: 'Window 2' })
|
|
219
|
+
createWindow(renderer, { title: 'Window 3' })
|
|
220
|
+
|
|
221
|
+
expect(renderer.windowStack.length).toBe(3)
|
|
222
|
+
|
|
223
|
+
clearWindows(renderer)
|
|
224
|
+
|
|
225
|
+
expect(renderer.windowStack.length).toBe(0)
|
|
226
|
+
|
|
227
|
+
destroyRenderer(renderer)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('handles empty window stack', () => {
|
|
231
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
232
|
+
|
|
233
|
+
expect(renderer.windowStack.length).toBe(0)
|
|
234
|
+
|
|
235
|
+
// Should not throw
|
|
236
|
+
expect(() => clearWindows(renderer)).not.toThrow()
|
|
237
|
+
|
|
238
|
+
destroyRenderer(renderer)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('can be called multiple times safely', () => {
|
|
242
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
243
|
+
|
|
244
|
+
createWindow(renderer, { title: 'Window' })
|
|
245
|
+
clearWindows(renderer)
|
|
246
|
+
|
|
247
|
+
expect(renderer.windowStack.length).toBe(0)
|
|
248
|
+
|
|
249
|
+
// Second call should not throw
|
|
250
|
+
expect(() => clearWindows(renderer)).not.toThrow()
|
|
251
|
+
|
|
252
|
+
destroyRenderer(renderer)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('renderMatrixRain', () => {
|
|
257
|
+
it('updates matrix box content', () => {
|
|
258
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
259
|
+
const initialContent = renderer.matrixBox.getContent()
|
|
260
|
+
|
|
261
|
+
renderMatrixRain(renderer)
|
|
262
|
+
|
|
263
|
+
const updatedContent = renderer.matrixBox.getContent()
|
|
264
|
+
|
|
265
|
+
// Content should be updated (may be empty or filled depending on drops)
|
|
266
|
+
expect(typeof updatedContent).toBe('string')
|
|
267
|
+
|
|
268
|
+
destroyRenderer(renderer)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('updates drop positions', () => {
|
|
272
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
273
|
+
|
|
274
|
+
// Get initial positions
|
|
275
|
+
const initialPositions = renderer.matrixDrops.map((d) => ({ x: d.x, y: d.y }))
|
|
276
|
+
|
|
277
|
+
// Render several frames
|
|
278
|
+
for (let i = 0; i < 5; i++) {
|
|
279
|
+
renderMatrixRain(renderer)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// At least some drops should have moved
|
|
283
|
+
const movedDrops = renderer.matrixDrops.filter(
|
|
284
|
+
(d, i) => d.y !== initialPositions[i].y
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
expect(movedDrops.length).toBeGreaterThan(0)
|
|
288
|
+
|
|
289
|
+
destroyRenderer(renderer)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('resets drops when they go off screen', () => {
|
|
293
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
294
|
+
const height = (renderer.screen.height as number) || 24
|
|
295
|
+
|
|
296
|
+
// Manually set a drop to be well off screen (beyond trail length)
|
|
297
|
+
const trailLength = renderer.matrixDrops[0].trail.length
|
|
298
|
+
renderer.matrixDrops[0].y = height + trailLength + 10
|
|
299
|
+
|
|
300
|
+
renderMatrixRain(renderer)
|
|
301
|
+
|
|
302
|
+
// Drop should be reset to top (negative y value)
|
|
303
|
+
expect(renderer.matrixDrops[0].y).toBeLessThan(0)
|
|
304
|
+
|
|
305
|
+
destroyRenderer(renderer)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('uses theme colors', () => {
|
|
309
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
310
|
+
|
|
311
|
+
renderMatrixRain(renderer)
|
|
312
|
+
|
|
313
|
+
const content = renderer.matrixBox.getContent()
|
|
314
|
+
|
|
315
|
+
// If there are drops, content should contain color tags
|
|
316
|
+
if (renderer.matrixDrops.length > 0 && content.trim() !== '') {
|
|
317
|
+
// Content may contain blessed color tags like {#00cc66-fg}
|
|
318
|
+
expect(typeof content).toBe('string')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
destroyRenderer(renderer)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('handles screen resize gracefully', () => {
|
|
325
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
326
|
+
|
|
327
|
+
// Should not throw even with different screen dimensions
|
|
328
|
+
expect(() => renderMatrixRain(renderer)).not.toThrow()
|
|
329
|
+
|
|
330
|
+
destroyRenderer(renderer)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('initMatrixRain', () => {
|
|
335
|
+
it('initializes matrix drops based on density', () => {
|
|
336
|
+
const customTheme = {
|
|
337
|
+
...DEFAULT_THEME,
|
|
338
|
+
animations: {
|
|
339
|
+
...DEFAULT_THEME.animations,
|
|
340
|
+
matrixDensity: 10,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const renderer = createRenderer(customTheme)
|
|
345
|
+
|
|
346
|
+
expect(renderer.matrixDrops.length).toBe(10)
|
|
347
|
+
|
|
348
|
+
destroyRenderer(renderer)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('starts animation interval', () => {
|
|
352
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
353
|
+
|
|
354
|
+
expect(renderer.matrixInterval).not.toBeNull()
|
|
355
|
+
|
|
356
|
+
destroyRenderer(renderer)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('creates drops with random positions', () => {
|
|
360
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
361
|
+
|
|
362
|
+
const xPositions = renderer.matrixDrops.map((d) => d.x)
|
|
363
|
+
const yPositions = renderer.matrixDrops.map((d) => d.y)
|
|
364
|
+
|
|
365
|
+
// With default density, positions should vary (not all the same)
|
|
366
|
+
// Note: With small screen dimensions, it's possible (but unlikely) for some
|
|
367
|
+
// coordinates to overlap, so we just check that drops are created
|
|
368
|
+
const uniqueX = new Set(xPositions)
|
|
369
|
+
const uniqueY = new Set(yPositions)
|
|
370
|
+
|
|
371
|
+
// Should have at least one position defined
|
|
372
|
+
expect(xPositions.length).toBeGreaterThan(0)
|
|
373
|
+
expect(yPositions.length).toBeGreaterThan(0)
|
|
374
|
+
|
|
375
|
+
// With default density > 1, we expect some variation
|
|
376
|
+
// (though with very small terminals, x positions might coincide)
|
|
377
|
+
if (renderer.matrixDrops.length > 1) {
|
|
378
|
+
// At least the y positions should vary since they're random over screen height
|
|
379
|
+
expect(uniqueY.size).toBeGreaterThanOrEqual(1)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
destroyRenderer(renderer)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('creates drops with random speeds', () => {
|
|
386
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
387
|
+
|
|
388
|
+
const speeds = renderer.matrixDrops.map((d) => d.speed)
|
|
389
|
+
|
|
390
|
+
// All speeds should be within range [0.3, 1.0]
|
|
391
|
+
for (const speed of speeds) {
|
|
392
|
+
expect(speed).toBeGreaterThanOrEqual(0.3)
|
|
393
|
+
expect(speed).toBeLessThanOrEqual(1.0)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
destroyRenderer(renderer)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('creates drops with trails', () => {
|
|
400
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
401
|
+
|
|
402
|
+
for (const drop of renderer.matrixDrops) {
|
|
403
|
+
expect(Array.isArray(drop.trail)).toBe(true)
|
|
404
|
+
expect(drop.trail.length).toBeGreaterThanOrEqual(5)
|
|
405
|
+
expect(drop.trail.length).toBeLessThanOrEqual(15)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
destroyRenderer(renderer)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('uses theme glyphs for trails', () => {
|
|
412
|
+
const customTheme = {
|
|
413
|
+
...DEFAULT_THEME,
|
|
414
|
+
glyphs: 'ABC123',
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const renderer = createRenderer(customTheme)
|
|
418
|
+
|
|
419
|
+
for (const drop of renderer.matrixDrops) {
|
|
420
|
+
for (const char of drop.trail) {
|
|
421
|
+
expect(customTheme.glyphs).toContain(char)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
destroyRenderer(renderer)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('respects theme matrix interval', () => {
|
|
429
|
+
const customTheme = {
|
|
430
|
+
...DEFAULT_THEME,
|
|
431
|
+
animations: {
|
|
432
|
+
...DEFAULT_THEME.animations,
|
|
433
|
+
matrixInterval: 50,
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const renderer = createRenderer(customTheme)
|
|
438
|
+
|
|
439
|
+
expect(renderer.matrixInterval).not.toBeNull()
|
|
440
|
+
|
|
441
|
+
destroyRenderer(renderer)
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
describe('applyTransition', () => {
|
|
446
|
+
it('applies instant transition', async () => {
|
|
447
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
448
|
+
const box = {
|
|
449
|
+
setContent: mock(() => {}),
|
|
450
|
+
} as any
|
|
451
|
+
const screen = renderer.screen
|
|
452
|
+
const content = 'Test content'
|
|
453
|
+
|
|
454
|
+
await applyTransition(box, screen, content, 'instant', DEFAULT_THEME)
|
|
455
|
+
|
|
456
|
+
expect(box.setContent).toHaveBeenCalledWith(content)
|
|
457
|
+
|
|
458
|
+
destroyRenderer(renderer)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('applies glitch transition', async () => {
|
|
462
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
463
|
+
const box = {
|
|
464
|
+
setContent: mock(() => {}),
|
|
465
|
+
} as any
|
|
466
|
+
const screen = renderer.screen
|
|
467
|
+
const content = 'Test glitch'
|
|
468
|
+
|
|
469
|
+
await applyTransition(box, screen, content, 'glitch', DEFAULT_THEME)
|
|
470
|
+
|
|
471
|
+
// Should have called setContent multiple times during animation
|
|
472
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
473
|
+
|
|
474
|
+
destroyRenderer(renderer)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('applies fade transition', async () => {
|
|
478
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
479
|
+
const box = {
|
|
480
|
+
setContent: mock(() => {}),
|
|
481
|
+
} as any
|
|
482
|
+
const screen = renderer.screen
|
|
483
|
+
const content = 'Test fade'
|
|
484
|
+
|
|
485
|
+
await applyTransition(box, screen, content, 'fade', DEFAULT_THEME)
|
|
486
|
+
|
|
487
|
+
// Should have called setContent multiple times
|
|
488
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
489
|
+
|
|
490
|
+
destroyRenderer(renderer)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('applies typewriter transition', async () => {
|
|
494
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
495
|
+
const box = {
|
|
496
|
+
setContent: mock(() => {}),
|
|
497
|
+
} as any
|
|
498
|
+
const screen = renderer.screen
|
|
499
|
+
const content = 'Test typewriter'
|
|
500
|
+
|
|
501
|
+
await applyTransition(box, screen, content, 'typewriter', DEFAULT_THEME)
|
|
502
|
+
|
|
503
|
+
// Should have called setContent multiple times
|
|
504
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
505
|
+
|
|
506
|
+
destroyRenderer(renderer)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('defaults to instant for unknown transition type', async () => {
|
|
510
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
511
|
+
const box = {
|
|
512
|
+
setContent: mock(() => {}),
|
|
513
|
+
} as any
|
|
514
|
+
const screen = renderer.screen
|
|
515
|
+
const content = 'Test default'
|
|
516
|
+
|
|
517
|
+
await applyTransition(box, screen, content, 'unknown' as any, DEFAULT_THEME)
|
|
518
|
+
|
|
519
|
+
expect(box.setContent).toHaveBeenCalledWith(content)
|
|
520
|
+
|
|
521
|
+
destroyRenderer(renderer)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('handles empty content', async () => {
|
|
525
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
526
|
+
const box = {
|
|
527
|
+
setContent: mock(() => {}),
|
|
528
|
+
} as any
|
|
529
|
+
const screen = renderer.screen
|
|
530
|
+
|
|
531
|
+
await applyTransition(box, screen, '', 'instant', DEFAULT_THEME)
|
|
532
|
+
|
|
533
|
+
expect(box.setContent).toHaveBeenCalledWith('')
|
|
534
|
+
|
|
535
|
+
destroyRenderer(renderer)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('handles multi-line content', async () => {
|
|
539
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
540
|
+
const box = {
|
|
541
|
+
setContent: mock(() => {}),
|
|
542
|
+
} as any
|
|
543
|
+
const screen = renderer.screen
|
|
544
|
+
const content = 'Line 1\nLine 2\nLine 3'
|
|
545
|
+
|
|
546
|
+
await applyTransition(box, screen, content, 'instant', DEFAULT_THEME)
|
|
547
|
+
|
|
548
|
+
expect(box.setContent).toHaveBeenCalledWith(content)
|
|
549
|
+
|
|
550
|
+
destroyRenderer(renderer)
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
describe('renderSlide', () => {
|
|
555
|
+
it('creates a window for the slide', async () => {
|
|
556
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
557
|
+
|
|
558
|
+
const slide: Slide = {
|
|
559
|
+
frontmatter: {
|
|
560
|
+
title: 'Test Slide',
|
|
561
|
+
},
|
|
562
|
+
body: 'Test content',
|
|
563
|
+
notes: '',
|
|
564
|
+
sourcePath: 'test.md',
|
|
565
|
+
index: 0,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const window = await renderSlide(renderer, slide)
|
|
569
|
+
|
|
570
|
+
expect(window).toBeDefined()
|
|
571
|
+
expect(renderer.windowStack.length).toBe(1)
|
|
572
|
+
|
|
573
|
+
destroyRenderer(renderer)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('renders slide with title', async () => {
|
|
577
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
578
|
+
|
|
579
|
+
const slide: Slide = {
|
|
580
|
+
frontmatter: {
|
|
581
|
+
title: 'My Slide Title',
|
|
582
|
+
},
|
|
583
|
+
body: 'Slide body content',
|
|
584
|
+
notes: '',
|
|
585
|
+
sourcePath: 'slide.md',
|
|
586
|
+
index: 0,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const window = await renderSlide(renderer, slide)
|
|
590
|
+
|
|
591
|
+
expect(window).toBeDefined()
|
|
592
|
+
|
|
593
|
+
destroyRenderer(renderer)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('renders slide with body content', async () => {
|
|
597
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
598
|
+
|
|
599
|
+
const slide: Slide = {
|
|
600
|
+
frontmatter: {
|
|
601
|
+
title: 'Content Slide',
|
|
602
|
+
},
|
|
603
|
+
body: 'This is the slide content',
|
|
604
|
+
notes: '',
|
|
605
|
+
sourcePath: 'content.md',
|
|
606
|
+
index: 0,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const window = await renderSlide(renderer, slide)
|
|
610
|
+
|
|
611
|
+
expect(window).toBeDefined()
|
|
612
|
+
|
|
613
|
+
destroyRenderer(renderer)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('renders slide with bigText', async () => {
|
|
617
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
618
|
+
|
|
619
|
+
const slide: Slide = {
|
|
620
|
+
frontmatter: {
|
|
621
|
+
title: 'Big Text Slide',
|
|
622
|
+
bigText: 'HELLO',
|
|
623
|
+
},
|
|
624
|
+
body: 'Additional content',
|
|
625
|
+
notes: '',
|
|
626
|
+
sourcePath: 'big.md',
|
|
627
|
+
index: 0,
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const window = await renderSlide(renderer, slide)
|
|
631
|
+
|
|
632
|
+
expect(window).toBeDefined()
|
|
633
|
+
|
|
634
|
+
destroyRenderer(renderer)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('renders slide with multi-line bigText', async () => {
|
|
638
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
639
|
+
|
|
640
|
+
const slide: Slide = {
|
|
641
|
+
frontmatter: {
|
|
642
|
+
title: 'Multi-line Big Text',
|
|
643
|
+
bigText: ['SPEC', 'MACHINE'],
|
|
644
|
+
},
|
|
645
|
+
body: 'Body text',
|
|
646
|
+
notes: '',
|
|
647
|
+
sourcePath: 'multi.md',
|
|
648
|
+
index: 0,
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const window = await renderSlide(renderer, slide)
|
|
652
|
+
|
|
653
|
+
expect(window).toBeDefined()
|
|
654
|
+
|
|
655
|
+
destroyRenderer(renderer)
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('uses gradient from frontmatter', async () => {
|
|
659
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
660
|
+
|
|
661
|
+
const slide: Slide = {
|
|
662
|
+
frontmatter: {
|
|
663
|
+
title: 'Gradient Slide',
|
|
664
|
+
bigText: 'TEST',
|
|
665
|
+
gradient: 'matrix',
|
|
666
|
+
},
|
|
667
|
+
body: '',
|
|
668
|
+
notes: '',
|
|
669
|
+
sourcePath: 'gradient.md',
|
|
670
|
+
index: 0,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const window = await renderSlide(renderer, slide)
|
|
674
|
+
|
|
675
|
+
expect(window).toBeDefined()
|
|
676
|
+
|
|
677
|
+
destroyRenderer(renderer)
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('uses default gradient when not specified', async () => {
|
|
681
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
682
|
+
|
|
683
|
+
const slide: Slide = {
|
|
684
|
+
frontmatter: {
|
|
685
|
+
title: 'Default Gradient',
|
|
686
|
+
bigText: 'TEST',
|
|
687
|
+
},
|
|
688
|
+
body: '',
|
|
689
|
+
notes: '',
|
|
690
|
+
sourcePath: 'default.md',
|
|
691
|
+
index: 0,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const window = await renderSlide(renderer, slide)
|
|
695
|
+
|
|
696
|
+
expect(window).toBeDefined()
|
|
697
|
+
|
|
698
|
+
destroyRenderer(renderer)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('applies transition from frontmatter', async () => {
|
|
702
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
703
|
+
|
|
704
|
+
const slide: Slide = {
|
|
705
|
+
frontmatter: {
|
|
706
|
+
title: 'Transition Slide',
|
|
707
|
+
transition: 'instant',
|
|
708
|
+
},
|
|
709
|
+
body: 'Content with instant transition',
|
|
710
|
+
notes: '',
|
|
711
|
+
sourcePath: 'transition.md',
|
|
712
|
+
index: 0,
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const window = await renderSlide(renderer, slide)
|
|
716
|
+
|
|
717
|
+
expect(window).toBeDefined()
|
|
718
|
+
|
|
719
|
+
destroyRenderer(renderer)
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
it('uses default glitch transition when not specified', async () => {
|
|
723
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
724
|
+
|
|
725
|
+
const slide: Slide = {
|
|
726
|
+
frontmatter: {
|
|
727
|
+
title: 'Default Transition',
|
|
728
|
+
},
|
|
729
|
+
body: 'Content',
|
|
730
|
+
notes: '',
|
|
731
|
+
sourcePath: 'default-trans.md',
|
|
732
|
+
index: 0,
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const window = await renderSlide(renderer, slide)
|
|
736
|
+
|
|
737
|
+
expect(window).toBeDefined()
|
|
738
|
+
|
|
739
|
+
destroyRenderer(renderer)
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('handles slide with notes (notes not rendered)', async () => {
|
|
743
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
744
|
+
|
|
745
|
+
const slide: Slide = {
|
|
746
|
+
frontmatter: {
|
|
747
|
+
title: 'Slide with Notes',
|
|
748
|
+
},
|
|
749
|
+
body: 'Visible content',
|
|
750
|
+
notes: 'These are presenter notes',
|
|
751
|
+
sourcePath: 'notes.md',
|
|
752
|
+
index: 0,
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const window = await renderSlide(renderer, slide)
|
|
756
|
+
|
|
757
|
+
expect(window).toBeDefined()
|
|
758
|
+
|
|
759
|
+
destroyRenderer(renderer)
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('handles slide with color tokens in body', async () => {
|
|
763
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
764
|
+
|
|
765
|
+
const slide: Slide = {
|
|
766
|
+
frontmatter: {
|
|
767
|
+
title: 'Color Token Slide',
|
|
768
|
+
},
|
|
769
|
+
body: 'Text with {GREEN} color',
|
|
770
|
+
notes: '',
|
|
771
|
+
sourcePath: 'token.md',
|
|
772
|
+
index: 0,
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const window = await renderSlide(renderer, slide)
|
|
776
|
+
|
|
777
|
+
expect(window).toBeDefined()
|
|
778
|
+
|
|
779
|
+
destroyRenderer(renderer)
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('processes slide content through processSlideContent', async () => {
|
|
783
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
784
|
+
|
|
785
|
+
const slide: Slide = {
|
|
786
|
+
frontmatter: {
|
|
787
|
+
title: 'Processed Slide',
|
|
788
|
+
},
|
|
789
|
+
body: 'Content with {PRIMARY} token',
|
|
790
|
+
notes: '',
|
|
791
|
+
sourcePath: 'processed.md',
|
|
792
|
+
index: 0,
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const window = await renderSlide(renderer, slide)
|
|
796
|
+
|
|
797
|
+
expect(window).toBeDefined()
|
|
798
|
+
|
|
799
|
+
destroyRenderer(renderer)
|
|
800
|
+
})
|
|
801
|
+
})
|