@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,566 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
|
|
2
|
+
import { VirtualTerminal, captureScreen, createRecordingSession, saveFrame, cleanupSession, checkFfmpeg, detectFormat, exportPresentation, ansi256ToHex } from '../recorder'
|
|
3
|
+
import type { RecordingSession } from '../recorder'
|
|
4
|
+
import { readdir, stat, rm } from 'fs/promises'
|
|
5
|
+
|
|
6
|
+
describe('VirtualTerminal', () => {
|
|
7
|
+
it('creates buffer with correct dimensions', () => {
|
|
8
|
+
const vt = new VirtualTerminal(80, 24)
|
|
9
|
+
expect(vt.width).toBe(80)
|
|
10
|
+
expect(vt.height).toBe(24)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('sets characters at valid positions', () => {
|
|
14
|
+
const vt = new VirtualTerminal(10, 5)
|
|
15
|
+
vt.setChar(5, 2, 'X', '#ff0000')
|
|
16
|
+
|
|
17
|
+
const str = vt.toString()
|
|
18
|
+
const lines = str.split('\n')
|
|
19
|
+
expect(lines[2][5]).toBe('X')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('ignores out-of-bounds positions', () => {
|
|
23
|
+
const vt = new VirtualTerminal(10, 5)
|
|
24
|
+
vt.setChar(-1, 0, 'X') // Should not crash
|
|
25
|
+
vt.setChar(0, -1, 'X') // Should not crash
|
|
26
|
+
vt.setChar(100, 0, 'X') // Should not crash
|
|
27
|
+
vt.setChar(0, 100, 'X') // Should not crash
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('clears the buffer', () => {
|
|
31
|
+
const vt = new VirtualTerminal(10, 5)
|
|
32
|
+
vt.setChar(5, 2, 'X', '#ff0000')
|
|
33
|
+
vt.clear()
|
|
34
|
+
|
|
35
|
+
const str = vt.toString()
|
|
36
|
+
expect(str).toBe(' \n \n \n \n ')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('captureScreen', () => {
|
|
41
|
+
it('captures screen content from blessed', () => {
|
|
42
|
+
const vt = new VirtualTerminal(10, 5)
|
|
43
|
+
|
|
44
|
+
// Mock blessed screen with lines
|
|
45
|
+
const mockScreen = {
|
|
46
|
+
lines: [
|
|
47
|
+
[
|
|
48
|
+
['H', { fg: 15 }], // White 'H'
|
|
49
|
+
['e', { fg: 10 }], // Green 'e'
|
|
50
|
+
['l', { fg: 10 }],
|
|
51
|
+
['l', { fg: 10 }],
|
|
52
|
+
['o', { fg: 10 }],
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
['W', { fg: 15 }],
|
|
56
|
+
['o', { fg: 9 }], // Red 'o'
|
|
57
|
+
['r', { fg: 9 }],
|
|
58
|
+
['l', { fg: 9 }],
|
|
59
|
+
['d', { fg: 9 }],
|
|
60
|
+
],
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
captureScreen(mockScreen, vt)
|
|
65
|
+
|
|
66
|
+
const str = vt.toString()
|
|
67
|
+
const lines = str.split('\n')
|
|
68
|
+
|
|
69
|
+
// Check first line contains "Hello"
|
|
70
|
+
expect(lines[0].substring(0, 5)).toBe('Hello')
|
|
71
|
+
|
|
72
|
+
// Check second line contains "World"
|
|
73
|
+
expect(lines[1].substring(0, 5)).toBe('World')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('handles empty screen', () => {
|
|
77
|
+
const vt = new VirtualTerminal(10, 5)
|
|
78
|
+
const mockScreen = {
|
|
79
|
+
lines: [],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
captureScreen(mockScreen, vt)
|
|
83
|
+
|
|
84
|
+
const str = vt.toString()
|
|
85
|
+
expect(str).toBe(' \n \n \n \n ')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles missing lines', () => {
|
|
89
|
+
const vt = new VirtualTerminal(10, 5)
|
|
90
|
+
const mockScreen = {} // No lines property
|
|
91
|
+
|
|
92
|
+
captureScreen(mockScreen, vt)
|
|
93
|
+
|
|
94
|
+
const str = vt.toString()
|
|
95
|
+
expect(str).toBe(' \n \n \n \n ')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('extracts colors from cells with fg attribute', () => {
|
|
99
|
+
const vt = new VirtualTerminal(10, 5)
|
|
100
|
+
|
|
101
|
+
// Mock screen with different color types
|
|
102
|
+
const mockScreen = {
|
|
103
|
+
lines: [
|
|
104
|
+
[
|
|
105
|
+
['R', { fg: 9 }], // ANSI red
|
|
106
|
+
['G', { fg: '#00ff00' }], // Hex green
|
|
107
|
+
['B', { fg: 12 }], // ANSI blue
|
|
108
|
+
],
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
captureScreen(mockScreen, vt)
|
|
113
|
+
|
|
114
|
+
// Verify it doesn't crash and captures the characters
|
|
115
|
+
const str = vt.toString()
|
|
116
|
+
expect(str.startsWith('RGB')).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('handles cells without attributes', () => {
|
|
120
|
+
const vt = new VirtualTerminal(10, 5)
|
|
121
|
+
|
|
122
|
+
// Mock screen with plain characters (no attributes)
|
|
123
|
+
const mockScreen = {
|
|
124
|
+
lines: [
|
|
125
|
+
[
|
|
126
|
+
'A', // Plain char without attr
|
|
127
|
+
'B',
|
|
128
|
+
'C',
|
|
129
|
+
],
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
captureScreen(mockScreen, vt)
|
|
134
|
+
|
|
135
|
+
const str = vt.toString()
|
|
136
|
+
expect(str.startsWith('ABC')).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('respects virtual terminal dimensions', () => {
|
|
140
|
+
const vt = new VirtualTerminal(3, 2) // Small virtual terminal
|
|
141
|
+
|
|
142
|
+
// Mock screen with more content than vt can hold
|
|
143
|
+
const mockScreen = {
|
|
144
|
+
lines: [
|
|
145
|
+
[['A', {}], ['B', {}], ['C', {}], ['D', {}], ['E', {}]],
|
|
146
|
+
[['F', {}], ['G', {}], ['H', {}], ['I', {}], ['J', {}]],
|
|
147
|
+
[['K', {}], ['L', {}], ['M', {}], ['N', {}], ['O', {}]],
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
captureScreen(mockScreen, vt)
|
|
152
|
+
|
|
153
|
+
const str = vt.toString()
|
|
154
|
+
const lines = str.split('\n')
|
|
155
|
+
|
|
156
|
+
// Should only capture 3x2 area
|
|
157
|
+
expect(lines.length).toBe(2)
|
|
158
|
+
expect(lines[0]).toBe('ABC')
|
|
159
|
+
expect(lines[1]).toBe('FGH')
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('Recording Session', () => {
|
|
164
|
+
let session: RecordingSession
|
|
165
|
+
|
|
166
|
+
afterEach(async () => {
|
|
167
|
+
if (session) {
|
|
168
|
+
await cleanupSession(session)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('creates temp directory', async () => {
|
|
173
|
+
session = await createRecordingSession({
|
|
174
|
+
output: 'test.mp4',
|
|
175
|
+
width: 80,
|
|
176
|
+
height: 24,
|
|
177
|
+
fps: 10,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// Verify temp directory was created
|
|
181
|
+
const statResult = await stat(session.tempDir)
|
|
182
|
+
expect(statResult.isDirectory()).toBe(true)
|
|
183
|
+
|
|
184
|
+
// Verify session has correct defaults
|
|
185
|
+
expect(session.frameCount).toBe(0)
|
|
186
|
+
expect(session.width).toBe(80)
|
|
187
|
+
expect(session.height).toBe(24)
|
|
188
|
+
expect(session.fps).toBe(10)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('uses default dimensions when not provided', async () => {
|
|
192
|
+
session = await createRecordingSession({
|
|
193
|
+
output: 'test.mp4',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(session.width).toBe(120)
|
|
197
|
+
expect(session.height).toBe(40)
|
|
198
|
+
expect(session.fps).toBe(30)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('saves frames with padded numbering', async () => {
|
|
202
|
+
session = await createRecordingSession({
|
|
203
|
+
output: 'test.mp4',
|
|
204
|
+
width: 80,
|
|
205
|
+
height: 24,
|
|
206
|
+
fps: 10,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Create dummy PNG data (PNG magic number)
|
|
210
|
+
const dummyPng = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
|
|
211
|
+
|
|
212
|
+
await saveFrame(session, dummyPng)
|
|
213
|
+
await saveFrame(session, dummyPng)
|
|
214
|
+
await saveFrame(session, dummyPng)
|
|
215
|
+
|
|
216
|
+
// Check that files were created with correct names
|
|
217
|
+
const files = await readdir(session.tempDir)
|
|
218
|
+
expect(files).toContain('frame_000000.png')
|
|
219
|
+
expect(files).toContain('frame_000001.png')
|
|
220
|
+
expect(files).toContain('frame_000002.png')
|
|
221
|
+
expect(session.frameCount).toBe(3)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('cleanup removes directory', async () => {
|
|
225
|
+
session = await createRecordingSession({
|
|
226
|
+
output: 'test.mp4',
|
|
227
|
+
width: 80,
|
|
228
|
+
height: 24,
|
|
229
|
+
fps: 10,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const tempDir = session.tempDir
|
|
233
|
+
|
|
234
|
+
// Verify directory exists
|
|
235
|
+
const statBefore = await stat(tempDir)
|
|
236
|
+
expect(statBefore.isDirectory()).toBe(true)
|
|
237
|
+
|
|
238
|
+
// Cleanup
|
|
239
|
+
await cleanupSession(session)
|
|
240
|
+
|
|
241
|
+
// Verify directory no longer exists
|
|
242
|
+
try {
|
|
243
|
+
await stat(tempDir)
|
|
244
|
+
expect(true).toBe(false) // Should not reach here
|
|
245
|
+
} catch (error) {
|
|
246
|
+
expect((error as any).code).toBe('ENOENT')
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('frame count increments correctly', async () => {
|
|
251
|
+
session = await createRecordingSession({
|
|
252
|
+
output: 'test.mp4',
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(session.frameCount).toBe(0)
|
|
256
|
+
|
|
257
|
+
const dummyPng = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
|
|
258
|
+
|
|
259
|
+
await saveFrame(session, dummyPng)
|
|
260
|
+
expect(session.frameCount).toBe(1)
|
|
261
|
+
|
|
262
|
+
await saveFrame(session, dummyPng)
|
|
263
|
+
expect(session.frameCount).toBe(2)
|
|
264
|
+
|
|
265
|
+
await saveFrame(session, dummyPng)
|
|
266
|
+
expect(session.frameCount).toBe(3)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('checkFfmpeg', () => {
|
|
271
|
+
it('succeeds when ffmpeg is available', async () => {
|
|
272
|
+
// This test will pass if ffmpeg is installed on the system
|
|
273
|
+
// If ffmpeg is not installed, this test will fail
|
|
274
|
+
// The checkFfmpeg function should not throw if ffmpeg exists
|
|
275
|
+
try {
|
|
276
|
+
await checkFfmpeg()
|
|
277
|
+
// If we reach here, ffmpeg is installed
|
|
278
|
+
expect(true).toBe(true)
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// If we catch an error, ffmpeg is not installed
|
|
281
|
+
// Check that the error message contains installation instructions
|
|
282
|
+
expect((error as Error).message).toContain('ffmpeg not found')
|
|
283
|
+
expect((error as Error).message).toContain('brew install ffmpeg')
|
|
284
|
+
expect((error as Error).message).toContain('sudo apt install ffmpeg')
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('throws error with install instructions when ffmpeg is missing', async () => {
|
|
289
|
+
// This test verifies the error message format
|
|
290
|
+
// We can't easily mock the `which` command, so we'll test the error format indirectly
|
|
291
|
+
// by checking that the error message contains the expected text
|
|
292
|
+
try {
|
|
293
|
+
await checkFfmpeg()
|
|
294
|
+
// If ffmpeg is installed, we can't test the error case
|
|
295
|
+
// This is acceptable - the test above covers the success case
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// Verify error message format
|
|
298
|
+
expect(error).toBeInstanceOf(Error)
|
|
299
|
+
const errorMessage = (error as Error).message
|
|
300
|
+
expect(errorMessage).toContain('ffmpeg not found')
|
|
301
|
+
expect(errorMessage).toContain('Install it with:')
|
|
302
|
+
expect(errorMessage).toContain('macOS: brew install ffmpeg')
|
|
303
|
+
expect(errorMessage).toContain('Ubuntu: sudo apt install ffmpeg')
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('detectFormat', () => {
|
|
309
|
+
it('detects mp4 format', () => {
|
|
310
|
+
expect(detectFormat('output.mp4')).toBe('mp4')
|
|
311
|
+
expect(detectFormat('presentation.mp4')).toBe('mp4')
|
|
312
|
+
expect(detectFormat('/path/to/video.mp4')).toBe('mp4')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('detects gif format', () => {
|
|
316
|
+
expect(detectFormat('output.gif')).toBe('gif')
|
|
317
|
+
expect(detectFormat('presentation.gif')).toBe('gif')
|
|
318
|
+
expect(detectFormat('/path/to/animation.gif')).toBe('gif')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('throws error for unknown format', () => {
|
|
322
|
+
expect(() => detectFormat('output.avi')).toThrow('Unknown output format for output.avi. Use .mp4 or .gif extension.')
|
|
323
|
+
expect(() => detectFormat('output.mov')).toThrow('Unknown output format for output.mov. Use .mp4 or .gif extension.')
|
|
324
|
+
expect(() => detectFormat('output.webm')).toThrow('Unknown output format for output.webm. Use .mp4 or .gif extension.')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('throws error for missing extension', () => {
|
|
328
|
+
expect(() => detectFormat('output')).toThrow('Unknown output format for output. Use .mp4 or .gif extension.')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('handles case-sensitive extensions', () => {
|
|
332
|
+
// Extensions should be case-sensitive (lowercase only)
|
|
333
|
+
expect(() => detectFormat('output.MP4')).toThrow()
|
|
334
|
+
expect(() => detectFormat('output.GIF')).toThrow()
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
describe('exportPresentation', () => {
|
|
339
|
+
it('validates that slides directory exists', async () => {
|
|
340
|
+
const options = {
|
|
341
|
+
output: 'test.mp4',
|
|
342
|
+
width: 80,
|
|
343
|
+
height: 24,
|
|
344
|
+
fps: 10,
|
|
345
|
+
slideTime: 1,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Test with non-existent directory
|
|
349
|
+
try {
|
|
350
|
+
await exportPresentation('/non/existent/path', options)
|
|
351
|
+
expect(true).toBe(false) // Should not reach here
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// Should throw an error about missing directory or no slides
|
|
354
|
+
expect(error).toBeDefined()
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('throws error when no slides found', async () => {
|
|
359
|
+
// Create a temp directory with no slides
|
|
360
|
+
const { tmpdir } = await import('os')
|
|
361
|
+
const { join } = await import('path')
|
|
362
|
+
const { mkdir } = await import('fs/promises')
|
|
363
|
+
|
|
364
|
+
const testDir = join(tmpdir(), `test-export-${Date.now()}`)
|
|
365
|
+
await mkdir(testDir, { recursive: true })
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const options = {
|
|
369
|
+
output: 'test.mp4',
|
|
370
|
+
width: 80,
|
|
371
|
+
height: 24,
|
|
372
|
+
fps: 10,
|
|
373
|
+
slideTime: 1,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await exportPresentation(testDir, options)
|
|
377
|
+
expect(true).toBe(false) // Should not reach here
|
|
378
|
+
} catch (error) {
|
|
379
|
+
expect((error as Error).message).toContain('No slides found')
|
|
380
|
+
} finally {
|
|
381
|
+
// Cleanup
|
|
382
|
+
await rm(testDir, { recursive: true, force: true })
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('detects format from output filename', async () => {
|
|
387
|
+
const optionsMp4 = {
|
|
388
|
+
output: 'presentation.mp4',
|
|
389
|
+
width: 80,
|
|
390
|
+
height: 24,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const optionsGif = {
|
|
394
|
+
output: 'presentation.gif',
|
|
395
|
+
width: 80,
|
|
396
|
+
height: 24,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Verify detectFormat is called correctly (indirectly tested by not throwing on valid extensions)
|
|
400
|
+
expect(detectFormat(optionsMp4.output)).toBe('mp4')
|
|
401
|
+
expect(detectFormat(optionsGif.output)).toBe('gif')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('throws error for invalid output format', async () => {
|
|
405
|
+
const options = {
|
|
406
|
+
output: 'presentation.avi',
|
|
407
|
+
width: 80,
|
|
408
|
+
height: 24,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await exportPresentation('/any/path', options)
|
|
413
|
+
expect(true).toBe(false) // Should not reach here
|
|
414
|
+
} catch (error) {
|
|
415
|
+
expect((error as Error).message).toContain('Unknown output format')
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
describe('ansi256ToHex', () => {
|
|
421
|
+
describe('Standard 16 colors', () => {
|
|
422
|
+
it('converts black (0)', () => {
|
|
423
|
+
expect(ansi256ToHex(0)).toBe('#000000')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('converts maroon (1)', () => {
|
|
427
|
+
expect(ansi256ToHex(1)).toBe('#800000')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('converts green (2)', () => {
|
|
431
|
+
expect(ansi256ToHex(2)).toBe('#008000')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('converts olive (3)', () => {
|
|
435
|
+
expect(ansi256ToHex(3)).toBe('#808000')
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('converts navy (4)', () => {
|
|
439
|
+
expect(ansi256ToHex(4)).toBe('#000080')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('converts purple (5)', () => {
|
|
443
|
+
expect(ansi256ToHex(5)).toBe('#800080')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('converts teal (6)', () => {
|
|
447
|
+
expect(ansi256ToHex(6)).toBe('#008080')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('converts silver (7)', () => {
|
|
451
|
+
expect(ansi256ToHex(7)).toBe('#c0c0c0')
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('converts gray (8)', () => {
|
|
455
|
+
expect(ansi256ToHex(8)).toBe('#808080')
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('converts red (9)', () => {
|
|
459
|
+
expect(ansi256ToHex(9)).toBe('#ff0000')
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('converts lime (10)', () => {
|
|
463
|
+
expect(ansi256ToHex(10)).toBe('#00ff00')
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('converts yellow (11)', () => {
|
|
467
|
+
expect(ansi256ToHex(11)).toBe('#ffff00')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('converts blue (12)', () => {
|
|
471
|
+
expect(ansi256ToHex(12)).toBe('#0000ff')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('converts fuchsia (13)', () => {
|
|
475
|
+
expect(ansi256ToHex(13)).toBe('#ff00ff')
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('converts aqua (14)', () => {
|
|
479
|
+
expect(ansi256ToHex(14)).toBe('#00ffff')
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('converts white (15)', () => {
|
|
483
|
+
expect(ansi256ToHex(15)).toBe('#ffffff')
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
describe('216 color cube (16-231)', () => {
|
|
488
|
+
it('converts first color in cube (16)', () => {
|
|
489
|
+
expect(ansi256ToHex(16)).toBe('#000000')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('converts a mid-range color in cube (100)', () => {
|
|
493
|
+
// Code 100: n = 84
|
|
494
|
+
// r = floor(84/36) * 51 = 2 * 51 = 102 = 0x66
|
|
495
|
+
// g = floor((84 % 36)/6) * 51 = floor(12/6) * 51 = 2 * 51 = 102 = 0x66
|
|
496
|
+
// b = (84 % 6) * 51 = 0 * 51 = 0 = 0x00
|
|
497
|
+
expect(ansi256ToHex(100)).toBe('#666600')
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('converts another color in cube (196)', () => {
|
|
501
|
+
// Code 196: n = 180
|
|
502
|
+
// r = floor(180/36) * 51 = 5 * 51 = 255 = 0xff
|
|
503
|
+
// g = floor(0/6) * 51 = 0 * 51 = 0 = 0x00
|
|
504
|
+
// b = 0 * 51 = 0 = 0x00
|
|
505
|
+
expect(ansi256ToHex(196)).toBe('#ff0000')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('converts last color in cube (231)', () => {
|
|
509
|
+
// Code 231: n = 215
|
|
510
|
+
// r = floor(215/36) * 51 = 5 * 51 = 255 = 0xff
|
|
511
|
+
// g = floor(35/6) * 51 = 5 * 51 = 255 = 0xff
|
|
512
|
+
// b = 5 * 51 = 255 = 0xff
|
|
513
|
+
expect(ansi256ToHex(231)).toBe('#ffffff')
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('converts green-ish color (34)', () => {
|
|
517
|
+
// Code 34: n = 18
|
|
518
|
+
// r = floor(18/36) * 51 = 0 * 51 = 0 = 0x00
|
|
519
|
+
// g = floor(18/6) * 51 = 3 * 51 = 153 = 0x99
|
|
520
|
+
// b = 0 * 51 = 0 = 0x00
|
|
521
|
+
expect(ansi256ToHex(34)).toBe('#009900')
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('converts blue-ish color (21)', () => {
|
|
525
|
+
// Code 21: n = 5
|
|
526
|
+
// r = floor(5/36) * 51 = 0 * 51 = 0 = 0x00
|
|
527
|
+
// g = floor(5/6) * 51 = 0 * 51 = 0 = 0x00
|
|
528
|
+
// b = 5 * 51 = 255 = 0xff
|
|
529
|
+
expect(ansi256ToHex(21)).toBe('#0000ff')
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
describe('Grayscale (232-255)', () => {
|
|
534
|
+
it('converts darkest gray (232)', () => {
|
|
535
|
+
// gray = (232 - 232) * 10 + 8 = 8 = 0x08
|
|
536
|
+
expect(ansi256ToHex(232)).toBe('#080808')
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('converts mid gray (244)', () => {
|
|
540
|
+
// gray = (244 - 232) * 10 + 8 = 120 + 8 = 128 = 0x80
|
|
541
|
+
expect(ansi256ToHex(244)).toBe('#808080')
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('converts light gray (250)', () => {
|
|
545
|
+
// gray = (250 - 232) * 10 + 8 = 180 + 8 = 188 = 0xbc
|
|
546
|
+
expect(ansi256ToHex(250)).toBe('#bcbcbc')
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('converts brightest gray (255)', () => {
|
|
550
|
+
// gray = (255 - 232) * 10 + 8 = 230 + 8 = 238 = 0xee
|
|
551
|
+
expect(ansi256ToHex(255)).toBe('#eeeeee')
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
describe('Edge cases', () => {
|
|
556
|
+
it('handles boundary between standard and cube (15 and 16)', () => {
|
|
557
|
+
expect(ansi256ToHex(15)).toBe('#ffffff') // Last standard color
|
|
558
|
+
expect(ansi256ToHex(16)).toBe('#000000') // First cube color
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('handles boundary between cube and grayscale (231 and 232)', () => {
|
|
562
|
+
expect(ansi256ToHex(231)).toBe('#ffffff') // Last cube color
|
|
563
|
+
expect(ansi256ToHex(232)).toBe('#080808') // First grayscale
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
})
|