@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,525 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
createScreen,
|
|
4
|
+
createRenderer,
|
|
5
|
+
destroyRenderer,
|
|
6
|
+
generateBigText,
|
|
7
|
+
generateMultiLineBigText,
|
|
8
|
+
lineByLineReveal,
|
|
9
|
+
glitchLine,
|
|
10
|
+
getWindowColor,
|
|
11
|
+
createWindow,
|
|
12
|
+
clearWindows,
|
|
13
|
+
renderMatrixRain,
|
|
14
|
+
initMatrixRain,
|
|
15
|
+
applyTransition,
|
|
16
|
+
renderSlide,
|
|
17
|
+
} from '../screen'
|
|
18
|
+
import { DEFAULT_THEME } from '../../schemas/theme'
|
|
19
|
+
import type { Slide } from '../../schemas/slide'
|
|
20
|
+
|
|
21
|
+
describe('createScreen', () => {
|
|
22
|
+
it('creates a blessed screen with correct configuration', () => {
|
|
23
|
+
const screen = createScreen()
|
|
24
|
+
|
|
25
|
+
expect(screen).toBeDefined()
|
|
26
|
+
// Clean up
|
|
27
|
+
screen.destroy()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('accepts custom title', () => {
|
|
31
|
+
const title = 'Custom Title'
|
|
32
|
+
const screen = createScreen(title)
|
|
33
|
+
|
|
34
|
+
expect(screen).toBeDefined()
|
|
35
|
+
// Clean up
|
|
36
|
+
screen.destroy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('uses default title when none provided', () => {
|
|
40
|
+
const screen = createScreen()
|
|
41
|
+
|
|
42
|
+
expect(screen).toBeDefined()
|
|
43
|
+
// Clean up
|
|
44
|
+
screen.destroy()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('createRenderer', () => {
|
|
49
|
+
it('initializes renderer with all components', () => {
|
|
50
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
51
|
+
|
|
52
|
+
expect(renderer).toBeDefined()
|
|
53
|
+
expect(renderer.screen).toBeDefined()
|
|
54
|
+
expect(renderer.matrixBox).toBeDefined()
|
|
55
|
+
expect(renderer.windowStack).toEqual([])
|
|
56
|
+
expect(renderer.theme).toBe(DEFAULT_THEME)
|
|
57
|
+
expect(Array.isArray(renderer.matrixDrops)).toBe(true)
|
|
58
|
+
expect(renderer.matrixDrops.length).toBeGreaterThan(0)
|
|
59
|
+
expect(renderer.matrixInterval).toBeDefined()
|
|
60
|
+
|
|
61
|
+
// Clean up
|
|
62
|
+
destroyRenderer(renderer)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('attaches matrix box to screen', () => {
|
|
66
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
67
|
+
|
|
68
|
+
expect(renderer.matrixBox.parent).toBe(renderer.screen)
|
|
69
|
+
|
|
70
|
+
// Clean up
|
|
71
|
+
destroyRenderer(renderer)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('initializes matrix rain on creation', () => {
|
|
75
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
76
|
+
|
|
77
|
+
// Matrix rain should be initialized (even if empty for now)
|
|
78
|
+
expect(renderer.matrixDrops).toBeDefined()
|
|
79
|
+
expect(Array.isArray(renderer.matrixDrops)).toBe(true)
|
|
80
|
+
|
|
81
|
+
// Clean up
|
|
82
|
+
destroyRenderer(renderer)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('destroyRenderer', () => {
|
|
87
|
+
it('clears matrix interval if running', () => {
|
|
88
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
89
|
+
|
|
90
|
+
// Set up a mock interval
|
|
91
|
+
renderer.matrixInterval = setInterval(() => {}, 100) as NodeJS.Timer
|
|
92
|
+
|
|
93
|
+
destroyRenderer(renderer)
|
|
94
|
+
|
|
95
|
+
expect(renderer.matrixInterval).toBeNull()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('handles null matrix interval gracefully', () => {
|
|
99
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
100
|
+
|
|
101
|
+
// Clear the interval so it becomes null
|
|
102
|
+
if (renderer.matrixInterval) {
|
|
103
|
+
clearInterval(renderer.matrixInterval)
|
|
104
|
+
renderer.matrixInterval = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Should not throw when interval is null
|
|
108
|
+
expect(() => destroyRenderer(renderer)).not.toThrow()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('destroys all windows in the stack', () => {
|
|
112
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
113
|
+
|
|
114
|
+
// Create mock windows
|
|
115
|
+
const mockWindow1 = {
|
|
116
|
+
destroy: mock(() => {}),
|
|
117
|
+
}
|
|
118
|
+
const mockWindow2 = {
|
|
119
|
+
destroy: mock(() => {}),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
renderer.windowStack = [mockWindow1 as any, mockWindow2 as any]
|
|
123
|
+
|
|
124
|
+
destroyRenderer(renderer)
|
|
125
|
+
|
|
126
|
+
expect(mockWindow1.destroy).toHaveBeenCalledTimes(1)
|
|
127
|
+
expect(mockWindow2.destroy).toHaveBeenCalledTimes(1)
|
|
128
|
+
expect(renderer.windowStack).toEqual([])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('empties the window stack after destroying windows', () => {
|
|
132
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
133
|
+
|
|
134
|
+
// Add mock windows
|
|
135
|
+
renderer.windowStack = [
|
|
136
|
+
{ destroy: mock(() => {}) } as any,
|
|
137
|
+
{ destroy: mock(() => {}) } as any,
|
|
138
|
+
{ destroy: mock(() => {}) } as any,
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
destroyRenderer(renderer)
|
|
142
|
+
|
|
143
|
+
expect(renderer.windowStack).toEqual([])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('destroys the blessed screen', () => {
|
|
147
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
148
|
+
|
|
149
|
+
// Mock the screen destroy method
|
|
150
|
+
const destroySpy = mock(() => {})
|
|
151
|
+
renderer.screen.destroy = destroySpy
|
|
152
|
+
|
|
153
|
+
destroyRenderer(renderer)
|
|
154
|
+
|
|
155
|
+
expect(destroySpy).toHaveBeenCalledTimes(1)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('performs full cleanup in correct order', () => {
|
|
159
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
160
|
+
|
|
161
|
+
// Set up interval and windows
|
|
162
|
+
renderer.matrixInterval = setInterval(() => {}, 100) as NodeJS.Timer
|
|
163
|
+
const mockWindow = { destroy: mock(() => {}) }
|
|
164
|
+
renderer.windowStack = [mockWindow as any]
|
|
165
|
+
const screenDestroySpy = mock(() => {})
|
|
166
|
+
renderer.screen.destroy = screenDestroySpy
|
|
167
|
+
|
|
168
|
+
destroyRenderer(renderer)
|
|
169
|
+
|
|
170
|
+
// Verify all cleanup steps occurred
|
|
171
|
+
expect(renderer.matrixInterval).toBeNull()
|
|
172
|
+
expect(mockWindow.destroy).toHaveBeenCalled()
|
|
173
|
+
expect(renderer.windowStack).toEqual([])
|
|
174
|
+
expect(screenDestroySpy).toHaveBeenCalled()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('handles empty window stack', () => {
|
|
178
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
179
|
+
|
|
180
|
+
expect(renderer.windowStack).toEqual([])
|
|
181
|
+
|
|
182
|
+
// Should not throw with empty stack
|
|
183
|
+
expect(() => destroyRenderer(renderer)).not.toThrow()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('generateBigText', () => {
|
|
188
|
+
it('generates ASCII art text successfully', async () => {
|
|
189
|
+
const text = 'HELLO'
|
|
190
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
191
|
+
|
|
192
|
+
const result = await generateBigText(text, gradientColors)
|
|
193
|
+
|
|
194
|
+
expect(result).toBeDefined()
|
|
195
|
+
expect(typeof result).toBe('string')
|
|
196
|
+
expect(result.length).toBeGreaterThan(0)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('applies gradient colors to ASCII art', async () => {
|
|
200
|
+
const text = 'TEST'
|
|
201
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
202
|
+
|
|
203
|
+
const result = await generateBigText(text, gradientColors)
|
|
204
|
+
|
|
205
|
+
// The gradient library adds ANSI color codes
|
|
206
|
+
expect(result).toBeDefined()
|
|
207
|
+
expect(result.length).toBeGreaterThan(text.length)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('uses Standard font by default', async () => {
|
|
211
|
+
const text = 'ABC'
|
|
212
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
213
|
+
|
|
214
|
+
const result = await generateBigText(text, gradientColors)
|
|
215
|
+
|
|
216
|
+
expect(result).toBeDefined()
|
|
217
|
+
// Standard font should produce multi-line ASCII art
|
|
218
|
+
expect(result.includes('\n')).toBe(true)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('accepts custom font parameter', async () => {
|
|
222
|
+
const text = 'XYZ'
|
|
223
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
224
|
+
const font = 'Small'
|
|
225
|
+
|
|
226
|
+
const result = await generateBigText(text, gradientColors, font)
|
|
227
|
+
|
|
228
|
+
expect(result).toBeDefined()
|
|
229
|
+
expect(typeof result).toBe('string')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('handles single character input', async () => {
|
|
233
|
+
const text = 'A'
|
|
234
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
235
|
+
|
|
236
|
+
const result = await generateBigText(text, gradientColors)
|
|
237
|
+
|
|
238
|
+
expect(result).toBeDefined()
|
|
239
|
+
expect(result.length).toBeGreaterThan(0)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('handles empty string gracefully', async () => {
|
|
243
|
+
const text = ''
|
|
244
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
245
|
+
|
|
246
|
+
const result = await generateBigText(text, gradientColors)
|
|
247
|
+
|
|
248
|
+
expect(result).toBeDefined()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('generateMultiLineBigText', () => {
|
|
253
|
+
it('generates ASCII art for multiple lines', async () => {
|
|
254
|
+
const lines = ['SPEC', 'MACHINE']
|
|
255
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
256
|
+
|
|
257
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
258
|
+
|
|
259
|
+
expect(result).toBeDefined()
|
|
260
|
+
expect(typeof result).toBe('string')
|
|
261
|
+
expect(result.length).toBeGreaterThan(0)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('joins lines with newlines', async () => {
|
|
265
|
+
const lines = ['LINE1', 'LINE2']
|
|
266
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
267
|
+
|
|
268
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
269
|
+
|
|
270
|
+
// Should have newlines between the ASCII art blocks
|
|
271
|
+
const newlineCount = (result.match(/\n/g) || []).length
|
|
272
|
+
expect(newlineCount).toBeGreaterThan(0)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('applies gradient to each line independently', async () => {
|
|
276
|
+
const lines = ['A', 'B', 'C']
|
|
277
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
278
|
+
|
|
279
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
280
|
+
|
|
281
|
+
expect(result).toBeDefined()
|
|
282
|
+
// Each line should be processed
|
|
283
|
+
expect(result.length).toBeGreaterThan(lines.length)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('handles empty array', async () => {
|
|
287
|
+
const lines: string[] = []
|
|
288
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
289
|
+
|
|
290
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
291
|
+
|
|
292
|
+
expect(result).toBeDefined()
|
|
293
|
+
expect(result).toBe('')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('handles single line array', async () => {
|
|
297
|
+
const lines = ['ONLY']
|
|
298
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
299
|
+
|
|
300
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
301
|
+
|
|
302
|
+
expect(result).toBeDefined()
|
|
303
|
+
expect(typeof result).toBe('string')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('uses custom font parameter', async () => {
|
|
307
|
+
const lines = ['TEST', 'FONT']
|
|
308
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
309
|
+
const font = 'Small'
|
|
310
|
+
|
|
311
|
+
const result = await generateMultiLineBigText(lines, gradientColors, font)
|
|
312
|
+
|
|
313
|
+
expect(result).toBeDefined()
|
|
314
|
+
expect(typeof result).toBe('string')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('processes all lines in parallel', async () => {
|
|
318
|
+
const lines = ['ONE', 'TWO', 'THREE']
|
|
319
|
+
const gradientColors = ['#00cc66', '#ff6600']
|
|
320
|
+
|
|
321
|
+
const startTime = Date.now()
|
|
322
|
+
const result = await generateMultiLineBigText(lines, gradientColors)
|
|
323
|
+
const duration = Date.now() - startTime
|
|
324
|
+
|
|
325
|
+
expect(result).toBeDefined()
|
|
326
|
+
// Processing should be relatively fast due to parallelization
|
|
327
|
+
// This is a loose check - mainly verifying it completes
|
|
328
|
+
expect(duration).toBeLessThan(5000)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe('glitchLine', () => {
|
|
333
|
+
it('animates a single line with glitch effect', async () => {
|
|
334
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
335
|
+
const box = renderer.windowStack[0] || {
|
|
336
|
+
setContent: mock(() => {}),
|
|
337
|
+
} as any
|
|
338
|
+
const screen = renderer.screen
|
|
339
|
+
const currentLines = ['Line 1', 'Line 2']
|
|
340
|
+
const newLine = 'Line 3'
|
|
341
|
+
|
|
342
|
+
await glitchLine(box, screen, currentLines, newLine, 3)
|
|
343
|
+
|
|
344
|
+
// Should have called setContent multiple times during animation
|
|
345
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
346
|
+
|
|
347
|
+
destroyRenderer(renderer)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('preserves protected characters during glitch', async () => {
|
|
351
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
352
|
+
const contentCalls: string[] = []
|
|
353
|
+
const box = {
|
|
354
|
+
setContent: mock((content: string) => {
|
|
355
|
+
contentCalls.push(content)
|
|
356
|
+
}),
|
|
357
|
+
} as any
|
|
358
|
+
const screen = renderer.screen
|
|
359
|
+
const currentLines: string[] = []
|
|
360
|
+
const newLine = 'Hello, World!'
|
|
361
|
+
|
|
362
|
+
await glitchLine(box, screen, currentLines, newLine, 5)
|
|
363
|
+
|
|
364
|
+
// Check that spaces and punctuation were preserved in all calls
|
|
365
|
+
for (const call of contentCalls) {
|
|
366
|
+
expect(call).toContain(',')
|
|
367
|
+
expect(call).toContain(' ')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
destroyRenderer(renderer)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('gradually reveals text over iterations', async () => {
|
|
374
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
375
|
+
const contentCalls: string[] = []
|
|
376
|
+
const box = {
|
|
377
|
+
setContent: mock((content: string) => {
|
|
378
|
+
contentCalls.push(content)
|
|
379
|
+
}),
|
|
380
|
+
} as any
|
|
381
|
+
const screen = renderer.screen
|
|
382
|
+
const currentLines: string[] = []
|
|
383
|
+
const newLine = 'Test'
|
|
384
|
+
|
|
385
|
+
await glitchLine(box, screen, currentLines, newLine, 3)
|
|
386
|
+
|
|
387
|
+
// Should have multiple calls showing progression
|
|
388
|
+
expect(contentCalls.length).toBeGreaterThan(1)
|
|
389
|
+
// Last call should have the final text
|
|
390
|
+
expect(contentCalls[contentCalls.length - 1]).toBe('Test')
|
|
391
|
+
|
|
392
|
+
destroyRenderer(renderer)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('handles empty lines', async () => {
|
|
396
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
397
|
+
const box = {
|
|
398
|
+
setContent: mock(() => {}),
|
|
399
|
+
} as any
|
|
400
|
+
const screen = renderer.screen
|
|
401
|
+
const currentLines: string[] = []
|
|
402
|
+
const newLine = ''
|
|
403
|
+
|
|
404
|
+
await glitchLine(box, screen, currentLines, newLine, 2)
|
|
405
|
+
|
|
406
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
407
|
+
|
|
408
|
+
destroyRenderer(renderer)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe('lineByLineReveal', () => {
|
|
413
|
+
it('reveals content line by line', async () => {
|
|
414
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
415
|
+
const box = {
|
|
416
|
+
setContent: mock(() => {}),
|
|
417
|
+
} as any
|
|
418
|
+
const screen = renderer.screen
|
|
419
|
+
const content = 'Line 1\nLine 2\nLine 3'
|
|
420
|
+
|
|
421
|
+
await lineByLineReveal(box, screen, content, DEFAULT_THEME)
|
|
422
|
+
|
|
423
|
+
// Should have called setContent multiple times
|
|
424
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
425
|
+
|
|
426
|
+
destroyRenderer(renderer)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('uses theme animations configuration', async () => {
|
|
430
|
+
const customTheme = {
|
|
431
|
+
...DEFAULT_THEME,
|
|
432
|
+
animations: {
|
|
433
|
+
...DEFAULT_THEME.animations,
|
|
434
|
+
lineDelay: 10,
|
|
435
|
+
glitchIterations: 2,
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const renderer = createRenderer(customTheme)
|
|
440
|
+
const box = {
|
|
441
|
+
setContent: mock(() => {}),
|
|
442
|
+
} as any
|
|
443
|
+
const screen = renderer.screen
|
|
444
|
+
const content = 'Line A\nLine B'
|
|
445
|
+
|
|
446
|
+
await lineByLineReveal(box, screen, content, customTheme)
|
|
447
|
+
|
|
448
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
449
|
+
|
|
450
|
+
destroyRenderer(renderer)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('handles single line content', async () => {
|
|
454
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
455
|
+
const box = {
|
|
456
|
+
setContent: mock(() => {}),
|
|
457
|
+
} as any
|
|
458
|
+
const screen = renderer.screen
|
|
459
|
+
const content = 'Single line'
|
|
460
|
+
|
|
461
|
+
await lineByLineReveal(box, screen, content, DEFAULT_THEME)
|
|
462
|
+
|
|
463
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
464
|
+
|
|
465
|
+
destroyRenderer(renderer)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('handles empty content', async () => {
|
|
469
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
470
|
+
const box = {
|
|
471
|
+
setContent: mock(() => {}),
|
|
472
|
+
} as any
|
|
473
|
+
const screen = renderer.screen
|
|
474
|
+
const content = ''
|
|
475
|
+
|
|
476
|
+
await lineByLineReveal(box, screen, content, DEFAULT_THEME)
|
|
477
|
+
|
|
478
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
479
|
+
|
|
480
|
+
destroyRenderer(renderer)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('skips delay for empty lines', async () => {
|
|
484
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
485
|
+
const box = {
|
|
486
|
+
setContent: mock(() => {}),
|
|
487
|
+
} as any
|
|
488
|
+
const screen = renderer.screen
|
|
489
|
+
const content = 'Line 1\n\nLine 3'
|
|
490
|
+
|
|
491
|
+
const startTime = Date.now()
|
|
492
|
+
await lineByLineReveal(box, screen, content, DEFAULT_THEME)
|
|
493
|
+
const duration = Date.now() - startTime
|
|
494
|
+
|
|
495
|
+
// Should complete relatively quickly since empty line doesn't add delay
|
|
496
|
+
expect(duration).toBeLessThan(1000)
|
|
497
|
+
expect(box.setContent).toHaveBeenCalled()
|
|
498
|
+
|
|
499
|
+
destroyRenderer(renderer)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('processes multi-line content sequentially', async () => {
|
|
503
|
+
const renderer = createRenderer(DEFAULT_THEME)
|
|
504
|
+
const contentCalls: string[] = []
|
|
505
|
+
const box = {
|
|
506
|
+
setContent: mock((content: string) => {
|
|
507
|
+
contentCalls.push(content)
|
|
508
|
+
}),
|
|
509
|
+
} as any
|
|
510
|
+
const screen = renderer.screen
|
|
511
|
+
const content = 'First\nSecond\nThird'
|
|
512
|
+
|
|
513
|
+
await lineByLineReveal(box, screen, content, DEFAULT_THEME)
|
|
514
|
+
|
|
515
|
+
// Should have accumulated lines progressively
|
|
516
|
+
expect(contentCalls.length).toBeGreaterThan(0)
|
|
517
|
+
// Final call should have all lines
|
|
518
|
+
const finalCall = contentCalls[contentCalls.length - 1]
|
|
519
|
+
expect(finalCall).toContain('First')
|
|
520
|
+
expect(finalCall).toContain('Second')
|
|
521
|
+
expect(finalCall).toContain('Third')
|
|
522
|
+
|
|
523
|
+
destroyRenderer(renderer)
|
|
524
|
+
})
|
|
525
|
+
})
|