@opensassi/opencode 0.1.5 → 0.2.0
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/skills/demo-video/SKILL.md +264 -0
- package/skills/demo-video/scripts/assemble.cjs +152 -0
- package/skills/demo-video/scripts/capture-browser.sh +64 -0
- package/skills/demo-video/scripts/capture-html.sh +48 -0
- package/skills/demo-video/scripts/generate-subs.cjs +75 -0
- package/skills/demo-video/scripts/generate-tts.sh +28 -0
- package/skills/demo-video/scripts/render-slide.cjs +61 -0
- package/skills/demo-video/scripts/render-terminal.cjs +138 -0
- package/skills/demo-video/scripts/setup.sh +44 -0
- package/skills/demo-video/test/assemble.test.js +100 -0
- package/skills/demo-video/test/capture-browser.test.js +71 -0
- package/skills/demo-video/test/capture-html.test.js +72 -0
- package/skills/demo-video/test/e2e-test.sh +302 -0
- package/skills/demo-video/test/fixtures/demo-scenes.json +36 -0
- package/skills/demo-video/test/fixtures/hello.output +2 -0
- package/skills/demo-video/test/fixtures/hello.timing +13 -0
- package/skills/demo-video/test/generate-subs.test.js +67 -0
- package/skills/demo-video/test/generate-tts.test.js +58 -0
- package/skills/demo-video/test/helpers/run-script.js +33 -0
- package/skills/demo-video/test/integration.test.js +110 -0
- package/skills/demo-video/test/jest.config.cjs +6 -0
- package/skills/demo-video/test/render-slide.test.js +79 -0
- package/skills/demo-video/test/render-terminal.test.js +87 -0
- package/skills/demo-video/test/setup.test.js +55 -0
- package/skills/opensassi/SKILL.md +6 -1
- package/skills-index.json +5 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function parseArgs() {
|
|
6
|
+
const args = {};
|
|
7
|
+
for (let i = 2; i < process.argv.length; i += 2) {
|
|
8
|
+
const key = process.argv[i].replace(/^--/, '');
|
|
9
|
+
args[key] = process.argv[i + 1];
|
|
10
|
+
}
|
|
11
|
+
if (!args.title || !args.bullets || !args.output) {
|
|
12
|
+
console.error('Usage: render-slide.js --title <text> --bullets <json> [--background <color>] [--foreground <color>] --output <file>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapeHtml(s) {
|
|
19
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateHtml(title, bullets, bg, fg) {
|
|
23
|
+
const bulletHtml = bullets.map(b => `<li>${escapeHtml(b)}</li>`).join('\n');
|
|
24
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<style>
|
|
29
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
30
|
+
body { background:${bg}; color:${fg}; font-family:"Inter","Segoe UI",system-ui,sans-serif; display:flex; align-items:center; justify-content:center; min-height:100vh; }
|
|
31
|
+
.slide { max-width:900px; padding:64px; text-align:center; }
|
|
32
|
+
h1 { font-size:48px; font-weight:700; margin-bottom:48px; line-height:1.3; }
|
|
33
|
+
ul { list-style:none; text-align:left; display:inline-block; }
|
|
34
|
+
li { font-size:28px; line-height:1.8; margin:12px 0; padding-left:32px; position:relative; }
|
|
35
|
+
li::before { content:"▸"; position:absolute; left:0; color:#58a6ff; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div class="slide">
|
|
40
|
+
<h1>${escapeHtml(title)}</h1>
|
|
41
|
+
<ul>${bulletHtml}</ul>
|
|
42
|
+
</div>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const args = parseArgs();
|
|
49
|
+
const title = args.title;
|
|
50
|
+
const bullets = JSON.parse(args.bullets);
|
|
51
|
+
const bg = args.background || '#1e1e2e';
|
|
52
|
+
const fg = args.foreground || '#cdd6f4';
|
|
53
|
+
const outputPath = path.resolve(args.output);
|
|
54
|
+
|
|
55
|
+
const html = generateHtml(title, bullets, bg, fg);
|
|
56
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
58
|
+
console.log(`Wrote ${outputPath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function parseArgs() {
|
|
6
|
+
const args = {};
|
|
7
|
+
for (let i = 2; i < process.argv.length; i += 2) {
|
|
8
|
+
const key = process.argv[i].replace(/^--/, '');
|
|
9
|
+
args[key] = process.argv[i + 1];
|
|
10
|
+
}
|
|
11
|
+
if (!args.timing || !args.output || !args.html) {
|
|
12
|
+
console.error('Usage: render-terminal.cjs --timing <file> --output <file> --command <cmd> --speed <N> --html <out.html>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseScriptTiming(timingPath) {
|
|
19
|
+
const raw = fs.readFileSync(timingPath, 'utf-8');
|
|
20
|
+
const lines = raw.trim().split('\n');
|
|
21
|
+
const entries = [];
|
|
22
|
+
let absTime = 0;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const parts = line.trim().split(/\s+/);
|
|
25
|
+
if (parts.length < 2) continue;
|
|
26
|
+
const delay = parseFloat(parts[0]);
|
|
27
|
+
const bytes = parseInt(parts[1], 10);
|
|
28
|
+
absTime += delay;
|
|
29
|
+
entries.push({ absTime, delay, bytes });
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateHtml(segments, command) {
|
|
35
|
+
return `<!DOCTYPE html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="utf-8">
|
|
39
|
+
<style>
|
|
40
|
+
body { margin:0; background:#1e1e1e; display:flex; min-height:100vh; box-sizing:border-box; padding:20px; }
|
|
41
|
+
.terminal { background:#0d1117; color:#c9d1d9; font-family:"Cascadia Code","Fira Code","JetBrains Mono",monospace; font-size:14px; width:100%; height:100%; border-radius:8px; overflow:hidden; box-shadow:0 8px 32px rgba(0,0,0,0.5); display:flex; flex-direction:column; }
|
|
42
|
+
.titlebar { background:#161b22; padding:8px 16px; font-size:12px; color:#8b949e; display:flex; align-items:center; gap:8px; border-bottom:1px solid #30363d; flex-shrink:0; }
|
|
43
|
+
.titlebar .dot { width:12px; height:12px; border-radius:50%; display:inline-block; }
|
|
44
|
+
.titlebar .dot.r { background:#ff5f56; }
|
|
45
|
+
.titlebar .dot.y { background:#ffbd2e; }
|
|
46
|
+
.titlebar .dot.g { background:#27c93f; }
|
|
47
|
+
.titlebar .title { margin-left:8px; }
|
|
48
|
+
.screen { flex:1; padding:16px; overflow-y:auto; }
|
|
49
|
+
.screen pre { margin:0; white-space:pre-wrap; word-break:break-all; font-family:inherit; font-size:inherit; line-height:1.5; }
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="terminal">
|
|
54
|
+
<div class="titlebar">
|
|
55
|
+
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
|
56
|
+
<span class="title">bash</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="screen">
|
|
59
|
+
<pre id="output"><span id="cmd-line"></span></pre>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<script>
|
|
63
|
+
(function() {
|
|
64
|
+
const pre = document.getElementById('output');
|
|
65
|
+
const cmdLine = document.getElementById('cmd-line');
|
|
66
|
+
const command = ${JSON.stringify(command)};
|
|
67
|
+
const segs = ${JSON.stringify(segments)};
|
|
68
|
+
let ci = 0;
|
|
69
|
+
let si = 0;
|
|
70
|
+
let buf = '';
|
|
71
|
+
|
|
72
|
+
function next() {
|
|
73
|
+
if (ci < command.length) {
|
|
74
|
+
buf += command[ci];
|
|
75
|
+
cmdLine.textContent = buf;
|
|
76
|
+
ci++;
|
|
77
|
+
setTimeout(next, 20 + Math.random() * 40);
|
|
78
|
+
} else {
|
|
79
|
+
if (si === 0) { buf += '\\n'; si++; setTimeout(next, 200); return; }
|
|
80
|
+
if (si <= segs.length) {
|
|
81
|
+
buf += segs[si - 1];
|
|
82
|
+
cmdLine.textContent = '';
|
|
83
|
+
pre.textContent = buf;
|
|
84
|
+
si++;
|
|
85
|
+
setTimeout(next, 150);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
setTimeout(next, 200);
|
|
90
|
+
})();
|
|
91
|
+
</script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function main() {
|
|
97
|
+
const args = parseArgs();
|
|
98
|
+
const timingPath = path.resolve(args.timing);
|
|
99
|
+
const outputPath = path.resolve(args.output);
|
|
100
|
+
const htmlPath = path.resolve(args.html);
|
|
101
|
+
const command = args.command || '';
|
|
102
|
+
const speed = parseFloat(args.speed) || 3;
|
|
103
|
+
|
|
104
|
+
if (!fs.existsSync(timingPath)) {
|
|
105
|
+
console.error('Timing file not found:', timingPath);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (!fs.existsSync(outputPath)) {
|
|
109
|
+
console.error('Output file not found:', outputPath);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const outputContent = fs.readFileSync(outputPath, 'utf-8');
|
|
114
|
+
const timing = parseScriptTiming(timingPath);
|
|
115
|
+
|
|
116
|
+
// Find the "Script started on ..." banner length so we can skip those bytes
|
|
117
|
+
const bannerMatch = outputContent.match(/^Script started on[^\n]*\n/);
|
|
118
|
+
const bannerLen = bannerMatch ? bannerMatch[0].length : 0;
|
|
119
|
+
|
|
120
|
+
// Build content chunks, skipping bytes that belong to the banner
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let byteOffset = 0;
|
|
123
|
+
for (const entry of timing) {
|
|
124
|
+
const start = byteOffset;
|
|
125
|
+
const end = byteOffset + entry.bytes;
|
|
126
|
+
byteOffset = end;
|
|
127
|
+
if (end <= bannerLen) continue;
|
|
128
|
+
const chunk = outputContent.slice(Math.max(start, bannerLen), end);
|
|
129
|
+
chunks.push(chunk);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const html = generateHtml(chunks, command);
|
|
133
|
+
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(htmlPath, html, 'utf-8');
|
|
135
|
+
console.log(`Wrote ${htmlPath}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
MISSING=0
|
|
5
|
+
|
|
6
|
+
check_cmd() {
|
|
7
|
+
if ! command -v "$1" &>/dev/null; then
|
|
8
|
+
echo "[MISSING] $1 — $2"
|
|
9
|
+
MISSING=1
|
|
10
|
+
else
|
|
11
|
+
echo "[OK] $1 — $("$1" --version 2>&1 | head -1)"
|
|
12
|
+
fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
check_npx() {
|
|
16
|
+
if ! npx --yes playwright --version &>/dev/null; then
|
|
17
|
+
echo "[MISSING] playwright — npx playwright install chromium"
|
|
18
|
+
MISSING=1
|
|
19
|
+
else
|
|
20
|
+
echo "[OK] playwright — $(npx playwright --version 2>&1)"
|
|
21
|
+
fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
check_pip() {
|
|
25
|
+
if ! python3 -m edge_tts --version &>/dev/null; then
|
|
26
|
+
echo "[MISSING] edge-tts — pip install edge-tts"
|
|
27
|
+
MISSING=1
|
|
28
|
+
else
|
|
29
|
+
echo "[OK] edge-tts — $(python3 -m edge_tts --version 2>&1)"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
echo "=== demo-video dependency check ==="
|
|
34
|
+
check_cmd ffmpeg "apt/brew/choco install ffmpeg"
|
|
35
|
+
check_npx
|
|
36
|
+
check_pip
|
|
37
|
+
echo ""
|
|
38
|
+
if [ "$MISSING" -eq 1 ]; then
|
|
39
|
+
echo "Some dependencies are missing. Install them and re-run."
|
|
40
|
+
exit 1
|
|
41
|
+
else
|
|
42
|
+
echo "All dependencies satisfied."
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { assembleVideo, assembleAudio, assembleFinal, getDuration } = require('../scripts/assemble.cjs')
|
|
4
|
+
|
|
5
|
+
describe('assemble.cjs — ffmpeg assembly pipeline', () => {
|
|
6
|
+
const outDir = '/tmp/demo-test-assemble'
|
|
7
|
+
const clipDir = path.join(outDir, 'clips')
|
|
8
|
+
|
|
9
|
+
function generateTestClip(file, duration, color = 'black') {
|
|
10
|
+
const { execSync } = require('child_process')
|
|
11
|
+
execSync(
|
|
12
|
+
`ffmpeg -y -f lavfi -i "color=c=${color}:s=1920x1080:d=${duration}" -c:v libx264 -preset ultrafast -crf 28 "${file}"`,
|
|
13
|
+
{ stdio: 'ignore', timeout: 30000 }
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function generateTestAudio(file, duration) {
|
|
18
|
+
const { execSync } = require('child_process')
|
|
19
|
+
execSync(
|
|
20
|
+
`ffmpeg -y -f lavfi -i "sine=frequency=440:duration=${duration}" -c:a mp3 -b:a 128k "${file}"`,
|
|
21
|
+
{ stdio: 'ignore', timeout: 15000 }
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
fs.mkdirSync(clipDir, { recursive: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
fs.rmSync(outDir, { recursive: true, force: true })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('AS01: getDuration returns correct length', () => {
|
|
34
|
+
const clip = path.join(clipDir, 'duration_test.mp4')
|
|
35
|
+
generateTestClip(clip, 2)
|
|
36
|
+
const dur = getDuration(clip)
|
|
37
|
+
expect(dur).toBeGreaterThan(1.5)
|
|
38
|
+
expect(dur).toBeLessThan(2.5)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('AS02: assembleVideo two clips without audio', () => {
|
|
42
|
+
const c1 = path.join(clipDir, 'vid_clip1.mp4')
|
|
43
|
+
const c2 = path.join(clipDir, 'vid_clip2.mp4')
|
|
44
|
+
generateTestClip(c1, 1)
|
|
45
|
+
generateTestClip(c2, 1.5)
|
|
46
|
+
|
|
47
|
+
const output = path.join(outDir, 'video_master.mp4')
|
|
48
|
+
const manifest = {
|
|
49
|
+
scenes: [
|
|
50
|
+
{ clip: c1, duration: 1 },
|
|
51
|
+
{ clip: c2, duration: 1.5 },
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
const manifestPath = path.join(outDir, 'manifest_video.json')
|
|
55
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest))
|
|
56
|
+
|
|
57
|
+
assembleVideo(manifest, output, true)
|
|
58
|
+
expect(fs.existsSync(output)).toBe(true)
|
|
59
|
+
const finalDur = getDuration(output)
|
|
60
|
+
expect(finalDur).toBeGreaterThan(2)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('AS03: assembleAudio concatenates audio files', () => {
|
|
64
|
+
const a1 = path.join(clipDir, 'audio_a.mp3')
|
|
65
|
+
const a2 = path.join(clipDir, 'audio_b.mp3')
|
|
66
|
+
generateTestAudio(a1, 1)
|
|
67
|
+
generateTestAudio(a2, 1.5)
|
|
68
|
+
|
|
69
|
+
const output = path.join(outDir, 'audio_master.m4a')
|
|
70
|
+
const manifest = {
|
|
71
|
+
scenes: [
|
|
72
|
+
{ audio: a1 },
|
|
73
|
+
{ audio: a2 },
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
const manifestPath = path.join(outDir, 'manifest_audio.json')
|
|
77
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest))
|
|
78
|
+
|
|
79
|
+
assembleAudio(manifest, output, true)
|
|
80
|
+
expect(fs.existsSync(output)).toBe(true)
|
|
81
|
+
const dur = getDuration(output)
|
|
82
|
+
expect(dur).toBeGreaterThan(2)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('AS04: assembleFinal rejects mismatched lengths', () => {
|
|
86
|
+
const shortVid = path.join(clipDir, 'short_vid.mp4')
|
|
87
|
+
const longAud = path.join(clipDir, 'long_aud.mp3')
|
|
88
|
+
generateTestClip(shortVid, 1)
|
|
89
|
+
generateTestAudio(longAud, 5)
|
|
90
|
+
|
|
91
|
+
expect(() => {
|
|
92
|
+
assembleFinal({
|
|
93
|
+
video: shortVid,
|
|
94
|
+
audio: longAud,
|
|
95
|
+
output: path.join(outDir, 'mismatch.mp4'),
|
|
96
|
+
'keep-raw': 'true',
|
|
97
|
+
})
|
|
98
|
+
}).toThrow()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const { runScript } = require('./helpers/run-script')
|
|
5
|
+
|
|
6
|
+
const CAPTURE_SCRIPT = 'scripts/capture-browser.sh'
|
|
7
|
+
|
|
8
|
+
function havePlaywright() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('npx playwright --version', { stdio: 'ignore', timeout: 5000 })
|
|
11
|
+
return true
|
|
12
|
+
} catch { return false }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('capture-browser.sh — Playwright browser capture', () => {
|
|
16
|
+
const outDir = '/tmp/demo-test-capture-browser'
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
fs.rmSync(outDir, { recursive: true, force: true })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('CB01: valid URL produces MP4 when playwright available', () => {
|
|
27
|
+
const outFile = path.join(outDir, 'browser.mp4')
|
|
28
|
+
|
|
29
|
+
if (!havePlaywright()) {
|
|
30
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
31
|
+
'--url', 'https://example.com',
|
|
32
|
+
'--duration', '1',
|
|
33
|
+
'--output', outFile,
|
|
34
|
+
])
|
|
35
|
+
expect(result.exitCode).toBeGreaterThanOrEqual(1)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
40
|
+
'--url', 'https://example.com',
|
|
41
|
+
'--duration', '1',
|
|
42
|
+
'--output', outFile,
|
|
43
|
+
])
|
|
44
|
+
expect(result.exitCode).toBe(0)
|
|
45
|
+
expect(fs.existsSync(outFile)).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('CB02: with click action does not crash', () => {
|
|
49
|
+
const outFile = path.join(outDir, 'browser-click.mp4')
|
|
50
|
+
|
|
51
|
+
if (!havePlaywright()) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
56
|
+
'--url', 'https://example.com',
|
|
57
|
+
'--actions', '[{"type":"click","selector":"h1"}]',
|
|
58
|
+
'--duration', '1',
|
|
59
|
+
'--output', outFile,
|
|
60
|
+
])
|
|
61
|
+
expect(result.exitCode).toBe(0)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('CB03: missing --url exits with usage', () => {
|
|
65
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
66
|
+
'--duration', '1',
|
|
67
|
+
'--output', '/tmp/x.mp4',
|
|
68
|
+
])
|
|
69
|
+
expect(result.exitCode).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const { runScript } = require('./helpers/run-script')
|
|
5
|
+
|
|
6
|
+
const CAPTURE_SCRIPT = 'scripts/capture-html.sh'
|
|
7
|
+
|
|
8
|
+
function havePlaywright() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('npx playwright --version', { stdio: 'ignore', timeout: 5000 })
|
|
11
|
+
return true
|
|
12
|
+
} catch { return false }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('capture-html.sh — Playwright HTML capture', () => {
|
|
16
|
+
const outDir = '/tmp/demo-test-capture-html'
|
|
17
|
+
const testHtml = path.join(outDir, 'test.html')
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
21
|
+
fs.writeFileSync(testHtml, '<!DOCTYPE html><html><body><h1>Test</h1></body></html>')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
fs.rmSync(outDir, { recursive: true, force: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('CH01: valid HTML produces MP4', () => {
|
|
29
|
+
const outFile = path.join(outDir, 'clip.mp4')
|
|
30
|
+
|
|
31
|
+
if (!havePlaywright()) {
|
|
32
|
+
// Playwright not available — verify error handling
|
|
33
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
34
|
+
'--html', testHtml,
|
|
35
|
+
'--duration', '1',
|
|
36
|
+
'--output', outFile,
|
|
37
|
+
])
|
|
38
|
+
expect(result.exitCode).toBe(1)
|
|
39
|
+
expect(result.stderr).toMatch(/FAILED|Error|not found/i)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
44
|
+
'--html', testHtml,
|
|
45
|
+
'--duration', '1',
|
|
46
|
+
'--output', outFile,
|
|
47
|
+
])
|
|
48
|
+
expect(result.exitCode).toBe(0)
|
|
49
|
+
expect(fs.existsSync(outFile)).toBe(true)
|
|
50
|
+
const stat = fs.statSync(outFile)
|
|
51
|
+
expect(stat.size).toBeGreaterThan(1000)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('CH02: missing HTML file exits with error', () => {
|
|
55
|
+
const outFile = path.join(outDir, 'clip.mp4')
|
|
56
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
57
|
+
'--html', '/nonexistent.html',
|
|
58
|
+
'--duration', '1',
|
|
59
|
+
'--output', outFile,
|
|
60
|
+
])
|
|
61
|
+
expect(result.exitCode).toBe(1)
|
|
62
|
+
expect(result.stdout).toContain('not found')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('CH03: missing --output exits with usage', () => {
|
|
66
|
+
const result = runScript(CAPTURE_SCRIPT, [
|
|
67
|
+
'--html', testHtml,
|
|
68
|
+
'--duration', '1',
|
|
69
|
+
])
|
|
70
|
+
expect(result.exitCode).toBe(1)
|
|
71
|
+
})
|
|
72
|
+
})
|