@argo-video/cli 0.1.0 → 0.1.1

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/asset-server.d.ts +7 -0
  4. package/dist/asset-server.d.ts.map +1 -0
  5. package/dist/asset-server.js +66 -0
  6. package/dist/asset-server.js.map +1 -0
  7. package/dist/captions.d.ts +17 -0
  8. package/dist/captions.d.ts.map +1 -0
  9. package/dist/captions.js +23 -0
  10. package/dist/captions.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +87 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +44 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +74 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/export.d.ts +18 -0
  20. package/dist/export.d.ts.map +1 -0
  21. package/dist/export.js +64 -0
  22. package/dist/export.js.map +1 -0
  23. package/dist/fixtures.d.ts +13 -0
  24. package/dist/fixtures.d.ts.map +1 -0
  25. package/dist/fixtures.js +36 -0
  26. package/dist/fixtures.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/init.d.ts +2 -0
  32. package/dist/init.d.ts.map +1 -0
  33. package/{src/init.ts → dist/init.js} +39 -54
  34. package/dist/init.js.map +1 -0
  35. package/dist/narration.d.ts +9 -0
  36. package/dist/narration.d.ts.map +1 -0
  37. package/dist/narration.js +27 -0
  38. package/dist/narration.js.map +1 -0
  39. package/dist/overlays/index.d.ts +8 -0
  40. package/dist/overlays/index.d.ts.map +1 -0
  41. package/dist/overlays/index.js +34 -0
  42. package/dist/overlays/index.js.map +1 -0
  43. package/dist/overlays/manifest.d.ts +5 -0
  44. package/dist/overlays/manifest.d.ts.map +1 -0
  45. package/dist/overlays/manifest.js +52 -0
  46. package/dist/overlays/manifest.js.map +1 -0
  47. package/dist/overlays/motion.d.ts +4 -0
  48. package/dist/overlays/motion.d.ts.map +1 -0
  49. package/dist/overlays/motion.js +25 -0
  50. package/dist/overlays/motion.js.map +1 -0
  51. package/dist/overlays/templates.d.ts +7 -0
  52. package/dist/overlays/templates.d.ts.map +1 -0
  53. package/dist/overlays/templates.js +98 -0
  54. package/dist/overlays/templates.js.map +1 -0
  55. package/dist/overlays/types.d.ts +42 -0
  56. package/dist/overlays/types.d.ts.map +1 -0
  57. package/dist/overlays/types.js +25 -0
  58. package/dist/overlays/types.js.map +1 -0
  59. package/dist/overlays/zones.d.ts +15 -0
  60. package/dist/overlays/zones.d.ts.map +1 -0
  61. package/dist/overlays/zones.js +69 -0
  62. package/dist/overlays/zones.js.map +1 -0
  63. package/dist/pipeline.d.ts +3 -0
  64. package/dist/pipeline.d.ts.map +1 -0
  65. package/dist/pipeline.js +93 -0
  66. package/dist/pipeline.js.map +1 -0
  67. package/dist/record.d.ts +14 -0
  68. package/dist/record.d.ts.map +1 -0
  69. package/dist/record.js +100 -0
  70. package/dist/record.js.map +1 -0
  71. package/dist/tts/align.d.ts +17 -0
  72. package/dist/tts/align.d.ts.map +1 -0
  73. package/dist/tts/align.js +40 -0
  74. package/dist/tts/align.js.map +1 -0
  75. package/dist/tts/cache.d.ts +31 -0
  76. package/dist/tts/cache.d.ts.map +1 -0
  77. package/dist/tts/cache.js +51 -0
  78. package/dist/tts/cache.js.map +1 -0
  79. package/dist/tts/engine.d.ts +41 -0
  80. package/dist/tts/engine.d.ts.map +1 -0
  81. package/dist/tts/engine.js +108 -0
  82. package/dist/tts/engine.js.map +1 -0
  83. package/dist/tts/generate.d.ts +20 -0
  84. package/dist/tts/generate.d.ts.map +1 -0
  85. package/dist/tts/generate.js +58 -0
  86. package/dist/tts/generate.js.map +1 -0
  87. package/dist/tts/kokoro.d.ts +13 -0
  88. package/dist/tts/kokoro.d.ts.map +1 -0
  89. package/dist/tts/kokoro.js +46 -0
  90. package/dist/tts/kokoro.js.map +1 -0
  91. package/package.json +13 -1
  92. package/.claude/settings.local.json +0 -34
  93. package/DESIGN.md +0 -261
  94. package/docs/enhancement-proposal.md +0 -262
  95. package/docs/superpowers/plans/2026-03-12-argo.md +0 -208
  96. package/docs/superpowers/plans/2026-03-12-editorial-overlay-system.md +0 -1560
  97. package/docs/superpowers/plans/2026-03-13-npm-rename-skill-showcase.md +0 -499
  98. package/docs/superpowers/specs/2026-03-13-npm-rename-skill-showcase-design.md +0 -109
  99. package/skills/argo-demo-creator.md +0 -355
  100. package/src/asset-server.ts +0 -81
  101. package/src/captions.ts +0 -36
  102. package/src/cli.ts +0 -97
  103. package/src/config.ts +0 -125
  104. package/src/export.ts +0 -93
  105. package/src/fixtures.ts +0 -50
  106. package/src/index.ts +0 -41
  107. package/src/narration.ts +0 -31
  108. package/src/overlays/index.ts +0 -54
  109. package/src/overlays/manifest.ts +0 -68
  110. package/src/overlays/motion.ts +0 -27
  111. package/src/overlays/templates.ts +0 -121
  112. package/src/overlays/types.ts +0 -73
  113. package/src/overlays/zones.ts +0 -82
  114. package/src/pipeline.ts +0 -120
  115. package/src/record.ts +0 -123
  116. package/src/tts/align.ts +0 -75
  117. package/src/tts/cache.ts +0 -65
  118. package/src/tts/engine.ts +0 -147
  119. package/src/tts/generate.ts +0 -83
  120. package/src/tts/kokoro.ts +0 -51
  121. package/tests/asset-server.test.ts +0 -67
  122. package/tests/captions.test.ts +0 -76
  123. package/tests/cli.test.ts +0 -131
  124. package/tests/config.test.ts +0 -150
  125. package/tests/e2e/fake-server.ts +0 -45
  126. package/tests/e2e/record.e2e.test.ts +0 -131
  127. package/tests/export.test.ts +0 -155
  128. package/tests/fixtures.test.ts +0 -74
  129. package/tests/init.test.ts +0 -77
  130. package/tests/narration.test.ts +0 -120
  131. package/tests/overlays/index.test.ts +0 -73
  132. package/tests/overlays/manifest.test.ts +0 -120
  133. package/tests/overlays/motion.test.ts +0 -34
  134. package/tests/overlays/templates.test.ts +0 -69
  135. package/tests/overlays/types.test.ts +0 -36
  136. package/tests/overlays/zones.test.ts +0 -49
  137. package/tests/pipeline.test.ts +0 -177
  138. package/tests/record.test.ts +0 -87
  139. package/tests/tts/align.test.ts +0 -118
  140. package/tests/tts/cache.test.ts +0 -110
  141. package/tests/tts/engine.test.ts +0 -204
  142. package/tests/tts/generate.test.ts +0 -177
  143. package/tests/tts/kokoro.test.ts +0 -25
  144. package/tsconfig.json +0 -19
@@ -1,155 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- vi.mock('node:child_process', () => ({
4
- execFileSync: vi.fn(),
5
- spawnSync: vi.fn(),
6
- }));
7
-
8
- vi.mock('node:fs', () => ({
9
- existsSync: vi.fn(),
10
- mkdirSync: vi.fn(),
11
- }));
12
-
13
- import { execFileSync, spawnSync } from 'node:child_process';
14
- import { existsSync, mkdirSync } from 'node:fs';
15
- import { checkFfmpeg, exportVideo } from '../src/export.js';
16
-
17
- const mockedExecFileSync = vi.mocked(execFileSync);
18
- const mockedSpawnSync = vi.mocked(spawnSync);
19
- const mockedExistsSync = vi.mocked(existsSync);
20
- const mockedMkdirSync = vi.mocked(mkdirSync);
21
-
22
- beforeEach(() => {
23
- vi.resetAllMocks();
24
- });
25
-
26
- // ---------- checkFfmpeg ----------
27
- describe('checkFfmpeg', () => {
28
- it('returns true when ffmpeg is available', () => {
29
- mockedExecFileSync.mockReturnValue(Buffer.from('ffmpeg version 6.0'));
30
- expect(checkFfmpeg()).toBe(true);
31
- expect(mockedExecFileSync).toHaveBeenCalledWith('ffmpeg', ['-version'], { stdio: 'pipe' });
32
- });
33
-
34
- it('throws with install instructions when ffmpeg is missing', () => {
35
- mockedExecFileSync.mockImplementation(() => {
36
- throw new Error('command not found');
37
- });
38
- expect(() => checkFfmpeg()).toThrow(/ffmpeg is not installed/i);
39
- expect(() => checkFfmpeg()).toThrow(/brew install ffmpeg/);
40
- expect(() => checkFfmpeg()).toThrow(/apt install ffmpeg/);
41
- expect(() => checkFfmpeg()).toThrow(/choco install ffmpeg/);
42
- });
43
- });
44
-
45
- // ---------- exportVideo ----------
46
- describe('exportVideo', () => {
47
- function setupHappy() {
48
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
49
- mockedExistsSync.mockReturnValue(true);
50
- mockedSpawnSync.mockReturnValue({ status: 0 } as any);
51
- }
52
-
53
- it('builds correct default ffmpeg args', async () => {
54
- setupHappy();
55
- const result = await exportVideo({ demoName: 'my-demo', argoDir: '.argo', outputDir: 'videos' });
56
-
57
- expect(mockedSpawnSync).toHaveBeenCalledTimes(1);
58
- const [cmd, args] = mockedSpawnSync.mock.calls[0];
59
- expect(cmd).toBe('ffmpeg');
60
- expect(args).toEqual([
61
- '-i', '.argo/my-demo/video.webm',
62
- '-i', '.argo/my-demo/narration-aligned.wav',
63
- '-c:v', 'libx264',
64
- '-preset', 'slow',
65
- '-crf', '16',
66
- '-c:a', 'aac',
67
- '-b:a', '192k',
68
- '-shortest',
69
- '-y',
70
- 'videos/my-demo.mp4',
71
- ]);
72
- expect(result).toBe('videos/my-demo.mp4');
73
- });
74
-
75
- it('applies custom preset and crf', async () => {
76
- setupHappy();
77
- await exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out', preset: 'fast', crf: 23 });
78
-
79
- const [, args] = mockedSpawnSync.mock.calls[0];
80
- expect(args).toContain('fast');
81
- expect(args).toContain('23');
82
- });
83
-
84
- it('adds -r flag when fps is specified', async () => {
85
- setupHappy();
86
- await exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out', fps: 30 });
87
-
88
- const [, args] = mockedSpawnSync.mock.calls[0];
89
- const rIdx = (args as string[]).indexOf('-r');
90
- expect(rIdx).toBeGreaterThan(-1);
91
- expect((args as string[])[rIdx + 1]).toBe('30');
92
- });
93
-
94
- it('throws on missing video.webm', async () => {
95
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
96
- mockedExistsSync.mockImplementation((p) => {
97
- if (String(p).endsWith('video.webm')) return false;
98
- return true;
99
- });
100
-
101
- await expect(exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' }))
102
- .rejects.toThrow(/video\.webm/);
103
- });
104
-
105
- it('throws on missing narration-aligned.wav', async () => {
106
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
107
- mockedExistsSync.mockImplementation((p) => {
108
- if (String(p).endsWith('narration-aligned.wav')) return false;
109
- return true;
110
- });
111
-
112
- await expect(exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' }))
113
- .rejects.toThrow(/narration-aligned\.wav/);
114
- });
115
-
116
- it('creates output directory if it does not exist', async () => {
117
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
118
- mockedExistsSync.mockImplementation((p) => {
119
- if (String(p) === 'out') return false;
120
- return true;
121
- });
122
- mockedSpawnSync.mockReturnValue({ status: 0 } as any);
123
-
124
- await exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' });
125
-
126
- expect(mockedMkdirSync).toHaveBeenCalledWith('out', { recursive: true });
127
- });
128
-
129
- it('throws on non-zero exit code', async () => {
130
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
131
- mockedExistsSync.mockReturnValue(true);
132
- mockedSpawnSync.mockReturnValue({ status: 1 } as any);
133
-
134
- await expect(exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' }))
135
- .rejects.toThrow(/ffmpeg.*failed|exit code/i);
136
- });
137
-
138
- it('throws with signal info when ffmpeg is killed', async () => {
139
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
140
- mockedExistsSync.mockReturnValue(true);
141
- mockedSpawnSync.mockReturnValue({ status: null, signal: 'SIGKILL' } as any);
142
-
143
- await expect(exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' }))
144
- .rejects.toThrow(/killed by signal SIGKILL/);
145
- });
146
-
147
- it('throws with spawn error when ffmpeg cannot be launched', async () => {
148
- mockedExecFileSync.mockReturnValue(Buffer.from('ok'));
149
- mockedExistsSync.mockReturnValue(true);
150
- mockedSpawnSync.mockReturnValue({ status: null, error: new Error('ENOENT') } as any);
151
-
152
- await expect(exportVideo({ demoName: 'demo', argoDir: '.argo', outputDir: 'out' }))
153
- .rejects.toThrow(/Failed to launch ffmpeg/);
154
- });
155
- });
@@ -1,74 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { createNarrationFixture, demoType } from '../src/fixtures.js';
3
-
4
- // ---------- createNarrationFixture ----------
5
- describe('createNarrationFixture', () => {
6
- it('creates timeline, calls start, provides it via use, calls flush after', async () => {
7
- const mockTimeline = {
8
- start: vi.fn(),
9
- mark: vi.fn(),
10
- getTimings: vi.fn(),
11
- flush: vi.fn().mockResolvedValue(undefined),
12
- };
13
- const factory = vi.fn().mockReturnValue(mockTimeline);
14
-
15
- let provided: unknown;
16
- const use = vi.fn(async (value: unknown) => {
17
- provided = value;
18
- });
19
- const testInfo = { title: 'my test' };
20
-
21
- const fixture = createNarrationFixture(factory);
22
- await fixture({ /* placeholder */ } as any, use, testInfo as any);
23
-
24
- expect(factory).toHaveBeenCalledWith('my test');
25
- expect(mockTimeline.start).toHaveBeenCalled();
26
- expect(use).toHaveBeenCalledWith(mockTimeline);
27
- expect(provided).toBe(mockTimeline);
28
- expect(mockTimeline.flush).toHaveBeenCalled();
29
- });
30
-
31
- it('calls flush even if test (use callback) throws', async () => {
32
- const mockTimeline = {
33
- start: vi.fn(),
34
- mark: vi.fn(),
35
- getTimings: vi.fn(),
36
- flush: vi.fn().mockResolvedValue(undefined),
37
- };
38
- const factory = vi.fn().mockReturnValue(mockTimeline);
39
-
40
- const use = vi.fn(async () => {
41
- throw new Error('test failure');
42
- });
43
- const testInfo = { title: 'failing test' };
44
-
45
- const fixture = createNarrationFixture(factory);
46
- await expect(fixture({} as any, use, testInfo as any)).rejects.toThrow('test failure');
47
-
48
- expect(mockTimeline.flush).toHaveBeenCalled();
49
- });
50
- });
51
-
52
- // ---------- demoType ----------
53
- describe('demoType', () => {
54
- it('calls locator(selector).pressSequentially(text, {delay: 60})', async () => {
55
- const pressSequentially = vi.fn().mockResolvedValue(undefined);
56
- const locator = vi.fn().mockReturnValue({ pressSequentially });
57
- const page = { locator } as any;
58
-
59
- await demoType(page, '#input', 'hello');
60
-
61
- expect(locator).toHaveBeenCalledWith('#input');
62
- expect(pressSequentially).toHaveBeenCalledWith('hello', { delay: 60 });
63
- });
64
-
65
- it('accepts custom delay', async () => {
66
- const pressSequentially = vi.fn().mockResolvedValue(undefined);
67
- const locator = vi.fn().mockReturnValue({ pressSequentially });
68
- const page = { locator } as any;
69
-
70
- await demoType(page, '#input', 'hello', 120);
71
-
72
- expect(pressSequentially).toHaveBeenCalledWith('hello', { delay: 120 });
73
- });
74
- });
@@ -1,77 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mkdtemp, rm, readFile, writeFile, mkdir } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { init } from '../src/init.js';
6
-
7
- describe('init', () => {
8
- let dir: string;
9
-
10
- beforeEach(async () => {
11
- dir = await mkdtemp(join(tmpdir(), 'argo-init-'));
12
- });
13
-
14
- afterEach(async () => {
15
- await rm(dir, { recursive: true, force: true });
16
- });
17
-
18
- it('creates demos/ directory', async () => {
19
- await init(dir);
20
- const { stat } = await import('node:fs/promises');
21
- const s = await stat(join(dir, 'demos'));
22
- expect(s.isDirectory()).toBe(true);
23
- });
24
-
25
- it('creates example.demo.ts with correct content', async () => {
26
- await init(dir);
27
- const content = await readFile(join(dir, 'demos', 'example.demo.ts'), 'utf-8');
28
- expect(content).toContain("import { test } from '@argo-video/cli'");
29
- expect(content).toContain("import { showCaption, withCaption } from '@argo-video/cli'");
30
- expect(content).toContain("narration.mark('welcome')");
31
- expect(content).toContain('showCaption(page,');
32
- expect(content).toContain('withCaption(page,');
33
- });
34
-
35
- it('creates example.voiceover.json with valid JSON array', async () => {
36
- await init(dir);
37
- const content = await readFile(join(dir, 'demos', 'example.voiceover.json'), 'utf-8');
38
- const parsed = JSON.parse(content);
39
- expect(Array.isArray(parsed)).toBe(true);
40
- expect(parsed.length).toBe(3);
41
- expect(parsed[0]).toHaveProperty('scene');
42
- expect(parsed[0]).toHaveProperty('text');
43
- });
44
-
45
- it('creates example.overlays.json with valid JSON array', async () => {
46
- await init(dir);
47
- const content = await readFile(join(dir, 'demos', 'example.overlays.json'), 'utf-8');
48
- const parsed = JSON.parse(content);
49
- expect(Array.isArray(parsed)).toBe(true);
50
- expect(parsed.length).toBe(2);
51
- expect(parsed[0]).toHaveProperty('scene');
52
- expect(parsed[0]).toHaveProperty('type');
53
- expect(parsed[1].type).toBe('headline-card');
54
- expect(parsed[1].placement).toBe('top-left');
55
- });
56
-
57
- it('creates argo.config.js with config', async () => {
58
- await init(dir);
59
- const content = await readFile(join(dir, 'argo.config.js'), 'utf-8');
60
- expect(content).toContain('export default');
61
- expect(content).toContain('baseURL');
62
- });
63
-
64
- it('does NOT overwrite existing files', async () => {
65
- await mkdir(join(dir, 'demos'), { recursive: true });
66
- await writeFile(join(dir, 'demos', 'example.demo.ts'), 'existing content');
67
- await writeFile(join(dir, 'argo.config.js'), 'existing config');
68
-
69
- await init(dir);
70
-
71
- const demo = await readFile(join(dir, 'demos', 'example.demo.ts'), 'utf-8');
72
- expect(demo).toBe('existing content');
73
-
74
- const config = await readFile(join(dir, 'argo.config.js'), 'utf-8');
75
- expect(config).toBe('existing config');
76
- });
77
- });
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { join } from 'node:path';
3
- import { mkdtemp, rm, readFile, stat } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { NarrationTimeline } from '../src/narration.js';
6
-
7
- // ---------- start ----------
8
- describe('start', () => {
9
- it('sets t=0 — a mark immediately after should be <50ms', () => {
10
- const timeline = new NarrationTimeline();
11
- timeline.start();
12
- timeline.mark('intro');
13
- const timings = timeline.getTimings();
14
- expect(timings.intro).toBeGreaterThanOrEqual(0);
15
- expect(timings.intro).toBeLessThan(50);
16
- });
17
-
18
- it('clears existing timings on re-start', () => {
19
- const timeline = new NarrationTimeline();
20
- timeline.start();
21
- timeline.mark('scene1');
22
- timeline.start();
23
- expect(timeline.getTimings()).toEqual({});
24
- });
25
- });
26
-
27
- // ---------- mark ----------
28
- describe('mark', () => {
29
- it('throws if called before start()', () => {
30
- const timeline = new NarrationTimeline();
31
- expect(() => timeline.mark('intro')).toThrow();
32
- });
33
-
34
- it('records elapsed ms (~100ms delay)', async () => {
35
- const timeline = new NarrationTimeline();
36
- timeline.start();
37
- await new Promise((r) => setTimeout(r, 100));
38
- timeline.mark('after-delay');
39
- const timings = timeline.getTimings();
40
- expect(timings['after-delay']).toBeGreaterThanOrEqual(80);
41
- expect(timings['after-delay']).toBeLessThan(300);
42
- });
43
-
44
- it('throws on duplicate scene name', () => {
45
- const timeline = new NarrationTimeline();
46
- timeline.start();
47
- timeline.mark('scene1');
48
- expect(() => timeline.mark('scene1')).toThrow();
49
- });
50
- });
51
-
52
- // ---------- getTimings ----------
53
- describe('getTimings', () => {
54
- it('returns empty object before any marks', () => {
55
- const timeline = new NarrationTimeline();
56
- timeline.start();
57
- expect(timeline.getTimings()).toEqual({});
58
- });
59
-
60
- it('returns all recorded marks', () => {
61
- const timeline = new NarrationTimeline();
62
- timeline.start();
63
- timeline.mark('a');
64
- timeline.mark('b');
65
- timeline.mark('c');
66
- const timings = timeline.getTimings();
67
- expect(Object.keys(timings)).toEqual(['a', 'b', 'c']);
68
- });
69
-
70
- it('returns a copy — mutations do not affect internal state', () => {
71
- const timeline = new NarrationTimeline();
72
- timeline.start();
73
- timeline.mark('scene1');
74
- const timings = timeline.getTimings();
75
- timings.scene1 = 99999;
76
- timings.injected = 1;
77
- const fresh = timeline.getTimings();
78
- expect(fresh.scene1).not.toBe(99999);
79
- expect(fresh).not.toHaveProperty('injected');
80
- });
81
- });
82
-
83
- // ---------- flush ----------
84
- describe('flush', () => {
85
- let tmpDir: string;
86
-
87
- beforeEach(async () => {
88
- tmpDir = await mkdtemp(join(tmpdir(), 'argo-narration-test-'));
89
- });
90
-
91
- afterEach(async () => {
92
- await rm(tmpDir, { recursive: true, force: true });
93
- });
94
-
95
- it('writes a .timing.json file', async () => {
96
- const timeline = new NarrationTimeline();
97
- timeline.start();
98
- timeline.mark('intro');
99
- timeline.mark('outro');
100
- const outPath = join(tmpDir, 'demo.timing.json');
101
- await timeline.flush(outPath);
102
-
103
- const raw = await readFile(outPath, 'utf-8');
104
- const data = JSON.parse(raw);
105
- expect(data).toHaveProperty('intro');
106
- expect(data).toHaveProperty('outro');
107
- expect(typeof data.intro).toBe('number');
108
- });
109
-
110
- it('creates parent directories if they do not exist', async () => {
111
- const timeline = new NarrationTimeline();
112
- timeline.start();
113
- timeline.mark('scene1');
114
- const outPath = join(tmpDir, 'nested', 'deep', 'output.timing.json');
115
- await timeline.flush(outPath);
116
-
117
- const info = await stat(outPath);
118
- expect(info.isFile()).toBe(true);
119
- });
120
- });
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { showOverlay, hideOverlay, withOverlay } from '../../src/overlays/index.js';
3
- import type { Page } from '@playwright/test';
4
-
5
- function createMockPage() {
6
- return {
7
- evaluate: vi.fn(),
8
- waitForTimeout: vi.fn(),
9
- } as unknown as Page;
10
- }
11
-
12
- describe('showOverlay', () => {
13
- let page: Page;
14
- beforeEach(() => { page = createMockPage(); });
15
-
16
- it('injects overlay and removes after duration', async () => {
17
- await showOverlay(page, 'intro', { type: 'lower-third', text: 'Hello' }, 2000);
18
- expect(page.evaluate).toHaveBeenCalledTimes(2);
19
- expect(page.waitForTimeout).toHaveBeenCalledWith(2000);
20
- });
21
-
22
- it('defaults to bottom-center zone', async () => {
23
- await showOverlay(page, 'intro', { type: 'lower-third', text: 'Hi' }, 1000);
24
- const [, args] = (page.evaluate as any).mock.calls[0];
25
- expect(args[0]).toContain('bottom-center');
26
- });
27
-
28
- it('uses specified zone', async () => {
29
- await showOverlay(page, 'intro', {
30
- type: 'headline-card', title: 'Title', placement: 'top-left',
31
- }, 1000);
32
- const [, args] = (page.evaluate as any).mock.calls[0];
33
- expect(args[0]).toContain('top-left');
34
- });
35
- });
36
-
37
- describe('hideOverlay', () => {
38
- it('removes overlay from specified zone', async () => {
39
- const page = createMockPage();
40
- await hideOverlay(page, 'top-left');
41
- expect(page.evaluate).toHaveBeenCalledTimes(1);
42
- });
43
-
44
- it('defaults to bottom-center', async () => {
45
- const page = createMockPage();
46
- await hideOverlay(page);
47
- const [, arg] = (page.evaluate as any).mock.calls[0];
48
- expect(arg).toContain('bottom-center');
49
- });
50
- });
51
-
52
- describe('withOverlay', () => {
53
- let page: Page;
54
- beforeEach(() => { page = createMockPage(); });
55
-
56
- it('shows overlay during action and removes after', async () => {
57
- let actionRan = false;
58
- await withOverlay(page, 'demo', { type: 'callout', text: 'Watch' }, async () => {
59
- actionRan = true;
60
- });
61
- expect(actionRan).toBe(true);
62
- expect(page.evaluate).toHaveBeenCalledTimes(2);
63
- });
64
-
65
- it('removes overlay even if action throws', async () => {
66
- await expect(
67
- withOverlay(page, 'demo', { type: 'lower-third', text: 'Hi' }, async () => {
68
- throw new Error('boom');
69
- }),
70
- ).rejects.toThrow('boom');
71
- expect(page.evaluate).toHaveBeenCalledTimes(2);
72
- });
73
- });
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { loadOverlayManifest, hasImageAssets, resolveAssetURLs } from '../../src/overlays/manifest.js';
3
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
-
7
- describe('loadOverlayManifest', () => {
8
- let tmpDir: string;
9
-
10
- function setup() {
11
- tmpDir = mkdtempSync(join(tmpdir(), 'argo-manifest-'));
12
- }
13
-
14
- afterEach(() => {
15
- if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
16
- });
17
-
18
- it('returns null when file does not exist', async () => {
19
- setup();
20
- const result = await loadOverlayManifest(join(tmpDir, 'missing.overlays.json'));
21
- expect(result).toBeNull();
22
- });
23
-
24
- it('parses a valid manifest', async () => {
25
- setup();
26
- const manifestPath = join(tmpDir, 'demo.overlays.json');
27
- writeFileSync(manifestPath, JSON.stringify([
28
- { scene: 'intro', type: 'lower-third', text: 'Hello' },
29
- { scene: 'mid', type: 'headline-card', title: 'Title', placement: 'top-left' },
30
- ]));
31
- const result = await loadOverlayManifest(manifestPath);
32
- expect(result).toHaveLength(2);
33
- expect(result![0].scene).toBe('intro');
34
- expect(result![1].type).toBe('headline-card');
35
- });
36
-
37
- it('throws on invalid JSON', async () => {
38
- setup();
39
- const manifestPath = join(tmpDir, 'bad.overlays.json');
40
- writeFileSync(manifestPath, '{ nope }}}');
41
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('Failed to parse overlay manifest');
42
- });
43
-
44
- it('throws when manifest is not an array', async () => {
45
- setup();
46
- const manifestPath = join(tmpDir, 'obj.overlays.json');
47
- writeFileSync(manifestPath, JSON.stringify({ scene: 'x' }));
48
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('must contain a JSON array');
49
- });
50
-
51
- it('throws on entry missing scene', async () => {
52
- setup();
53
- const manifestPath = join(tmpDir, 'no-scene.overlays.json');
54
- writeFileSync(manifestPath, JSON.stringify([{ type: 'lower-third', text: 'Hi' }]));
55
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('missing required field "scene"');
56
- });
57
-
58
- it('throws on entry missing type', async () => {
59
- setup();
60
- const manifestPath = join(tmpDir, 'no-type.overlays.json');
61
- writeFileSync(manifestPath, JSON.stringify([{ scene: 'x', text: 'Hi' }]));
62
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('missing required field "type"');
63
- });
64
-
65
- it('throws on unknown template type', async () => {
66
- setup();
67
- const manifestPath = join(tmpDir, 'bad-type.overlays.json');
68
- writeFileSync(manifestPath, JSON.stringify([{ scene: 'x', type: 'banner', text: 'Hi' }]));
69
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('unknown overlay type "banner"');
70
- });
71
-
72
- it('throws on unknown zone', async () => {
73
- setup();
74
- const manifestPath = join(tmpDir, 'bad-zone.overlays.json');
75
- writeFileSync(manifestPath, JSON.stringify([
76
- { scene: 'x', type: 'lower-third', text: 'Hi', placement: 'middle' },
77
- ]));
78
- await expect(loadOverlayManifest(manifestPath)).rejects.toThrow('unknown placement "middle"');
79
- });
80
- });
81
-
82
- describe('hasImageAssets', () => {
83
- it('returns true when entries contain image-card', () => {
84
- expect(hasImageAssets([
85
- { scene: 'x', type: 'image-card', src: 'img.png' },
86
- ] as any)).toBe(true);
87
- });
88
-
89
- it('returns false when no image-card entries', () => {
90
- expect(hasImageAssets([
91
- { scene: 'x', type: 'lower-third', text: 'Hi' },
92
- ] as any)).toBe(false);
93
- });
94
- });
95
-
96
- describe('resolveAssetURLs', () => {
97
- it('prefixes relative image-card src with asset server URL', () => {
98
- const entries = [
99
- { scene: 'x', type: 'image-card' as const, src: 'assets/diagram.png' },
100
- ];
101
- const resolved = resolveAssetURLs(entries as any, 'http://127.0.0.1:9999');
102
- expect(resolved[0]).toHaveProperty('src', 'http://127.0.0.1:9999/diagram.png');
103
- });
104
-
105
- it('leaves absolute URLs unchanged', () => {
106
- const entries = [
107
- { scene: 'x', type: 'image-card' as const, src: 'http://example.com/img.png' },
108
- ];
109
- const resolved = resolveAssetURLs(entries as any, 'http://127.0.0.1:9999');
110
- expect(resolved[0]).toHaveProperty('src', 'http://example.com/img.png');
111
- });
112
-
113
- it('leaves non-image-card entries unchanged', () => {
114
- const entries = [
115
- { scene: 'x', type: 'lower-third' as const, text: 'Hello' },
116
- ];
117
- const resolved = resolveAssetURLs(entries as any, 'http://127.0.0.1:9999');
118
- expect(resolved[0]).toEqual(entries[0]);
119
- });
120
- });
@@ -1,34 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { getMotionCSS, getMotionStyles } from '../../src/overlays/motion.js';
3
-
4
- describe('getMotionCSS', () => {
5
- it('returns empty string for none', () => {
6
- expect(getMotionCSS('none', 'argo-overlay-top-left')).toBe('');
7
- });
8
- it('returns fade-in keyframes', () => {
9
- const css = getMotionCSS('fade-in', 'argo-overlay-top-left');
10
- expect(css).toContain('@keyframes');
11
- expect(css).toContain('opacity');
12
- expect(css).toContain('argo-overlay-top-left');
13
- });
14
- it('returns slide-in keyframes', () => {
15
- const css = getMotionCSS('slide-in', 'argo-overlay-top-left');
16
- expect(css).toContain('@keyframes');
17
- expect(css).toContain('translateX');
18
- expect(css).toContain('argo-overlay-top-left');
19
- });
20
- });
21
-
22
- describe('getMotionStyles', () => {
23
- it('returns empty object for none', () => {
24
- expect(getMotionStyles('none', 'test-id')).toEqual({});
25
- });
26
- it('returns animation property for fade-in', () => {
27
- const styles = getMotionStyles('fade-in', 'test-id');
28
- expect(styles.animation).toContain('300ms');
29
- });
30
- it('returns animation property for slide-in', () => {
31
- const styles = getMotionStyles('slide-in', 'test-id');
32
- expect(styles.animation).toContain('400ms');
33
- });
34
- });