feat: teach Claude to use demowright createVideoScript

- Add createVideoScript API docs to qa-agent system prompt
- Claude now writes tests with native video scripts (title, segment, outro)
- Phase 2 detects createVideoScript in test code → skip regex injection
- Fallback: still injects annotate() for legacy tests without video scripts
This commit is contained in:
snomiao
2026-04-11 18:28:57 +00:00
parent 5228245561
commit 231878918d
2 changed files with 77 additions and 39 deletions

View File

@@ -503,6 +503,44 @@ export async function runResearchPhase(
- helpers/KeyboardHelper.ts, SettingsHelper.ts, SubgraphHelper.ts
- components/Topbar.ts, ContextMenu.ts, SettingDialog.ts, SidebarTab.ts
## Video Script (demowright createVideoScript)
After your test works, ALSO export a \`videoScript\` function that builds a demowright video script.
This will be used in Phase 2 to record a narrated demo video with subtitles, TTS, and chapter markers.
\`\`\`typescript
import { createVideoScript } from 'demowright/video-script'
// Export this alongside your test
export function videoScript() {
return createVideoScript()
.title('Bug Title Here', { subtitle: 'Issue #NNNN', durationMs: 4000 })
.segment('Step 1: description of what we do', async (pace) => {
// ... playwright actions ...
await pace() // wait for narration to finish
})
.segment('Step 2: description', async (pace) => {
// ... more actions ...
await pace()
})
.segment('Bug evidence: what we see that proves the bug', async (pace) => {
// ... final state showing the bug ...
await pace()
})
.outro({ text: 'Bug Reproduced', subtitle: 'Summary of the issue' })
}
\`\`\`
Key API:
- \`.title(text, {subtitle?, durationMs?})\` — full-screen title card
- \`.segment(narrationText, async (pace) => { ...actions...; await pace() })\` — narrated step with TTS
- \`.transition('fade' | 'crossfade', durationMs?)\` — visual transition
- \`.outro({text?, subtitle?, durationMs?})\` — ending card
- \`pace()\` — call after actions to wait for narration to finish before next segment
The videoScript function receives comfyPage as argument. Include it as a named export in the test file.
The test itself (for Phase 1 assertion) and the video script (for Phase 2 demo) should be in the SAME file.
## Current UI state (accessibility tree)
${initialA11y}

View File

@@ -1972,46 +1972,46 @@ async function main() {
const videoTestFile = `${projectRoot}/browser_tests/tests/qa-reproduce.spec.ts`
const testResultsDir = `${opts.outputDir}/test-results`
// Inject demowright annotate() call for TTS + subtitle intro
const issueTitle =
issueCtx.match(/Title:\s*(.+)/)?.[1]?.trim() ?? 'Bug Reproduction'
let testCode = research.testCode
const bodyMatch = testCode.match(
/async\s*\(\{\s*comfyPage\s*\}\)\s*=>\s*\{/
)
if (bodyMatch?.index !== undefined) {
const pos = bodyMatch.index + bodyMatch[0].length
const introInject = `
// demowright: announce issue title with subtitle + TTS
try {
const { annotate: _intro } = await import('demowright/helpers')
await _intro(comfyPage.page, ${JSON.stringify('Reproducing: ' + issueTitle)}, async () => {
await comfyPage.page.waitForTimeout(3000)
})
} catch (e) {
console.warn('[qa] intro annotate failed:', e instanceof Error ? e.message : e)
}
`
testCode =
testCode.slice(0, pos) + introInject + testCode.slice(pos)
}
// Inject step-by-step narrate() calls BEFORE each step comment
// narrate(page, text) speaks TTS + shows subtitle, then code runs normally
// This is safer than wrapping code in annotate() callbacks which can break syntax
testCode = testCode.replace(
/(\n\s*)(\/\/\s*(?:Step \d+|── Step \d+)[^\n]*)/g,
(_match, indent, comment) => {
const stepText = comment
.replace(/^\/\/\s*(?:──\s*)?/, '')
.replace(/\s*──+\s*$/, '')
.replace(/'/g, "\\'")
.trim()
if (!stepText) return `${indent}${comment}`
return `${indent}try { const { annotate: _a } = await import('demowright/helpers'); await _a(comfyPage.page, '${stepText}', async () => { await comfyPage.page.waitForTimeout(2000) }); } catch(e) { console.warn('[qa-narrate]', e); }\n${indent}${comment}`
}
)
const testCode = research.testCode
const hasVideoScript = testCode.includes('createVideoScript')
writeFileSync(videoTestFile, testCode)
if (hasVideoScript) {
// Test already uses createVideoScript — write as-is
console.warn(
'Phase 2: Test uses createVideoScript (native demowright)'
)
writeFileSync(videoTestFile, testCode)
} else {
// Fallback: inject basic narration before step comments
console.warn('Phase 2: Injecting step narration (fallback)')
const issueTitle =
issueCtx.match(/Title:\s*(.+)/)?.[1]?.trim() ?? 'Bug Reproduction'
let injectedCode = testCode
const bodyMatch = injectedCode.match(
/async\s*\(\{\s*comfyPage\s*\}\)\s*=>\s*\{/
)
if (bodyMatch?.index !== undefined) {
const pos = bodyMatch.index + bodyMatch[0].length
injectedCode =
injectedCode.slice(0, pos) +
`\n try { const { annotate: _intro } = await import('demowright/helpers'); await _intro(comfyPage.page, ${JSON.stringify('Reproducing: ' + issueTitle)}, async () => { await comfyPage.page.waitForTimeout(3000) }); } catch(e) { console.warn('[qa]', e); }\n` +
injectedCode.slice(pos)
}
// Insert narrate calls before step comments
injectedCode = injectedCode.replace(
/(\n\s*)(\/\/\s*(?:Step \d+|── Step \d+)[^\n]*)/g,
(_m, indent, comment) => {
const text = comment
.replace(/^\/\/\s*(?:──\s*)?/, '')
.replace(/\s*──+\s*$/, '')
.replace(/'/g, "\\'")
.trim()
if (!text) return `${indent}${comment}`
return `${indent}try { const { annotate: _a } = await import('demowright/helpers'); await _a(comfyPage.page, '${text}', async () => { await comfyPage.page.waitForTimeout(2000) }); } catch(e) { /* skip */ }\n${indent}${comment}`
}
)
writeFileSync(videoTestFile, injectedCode)
}
// Also save original test for the report
writeFileSync(