Files
dance-lessons-coach/frontend/scripts/generate-test-docs.mjs
Gabriel Radureau dc1d84666f feat(frontend): Storybook + auto-generated Playwright e2e docs with screenshots
Adds three things to make frontend PRs reviewable directly from Gitea web UI:

1. Storybook 8 for Vue 3 components
   - .storybook/main.ts + preview.ts
   - HealthDashboard.stories.ts (1 example story)
   - npm run storybook / npm run build-storybook

2. Playwright JSON reporter + screenshot on every test
   - playwright.config.ts: json reporter + screenshot: 'on'
   - health.spec.ts: explicit screenshot path

3. Auto-generated markdown docs with breadcrumbs
   - scripts/generate-test-docs.mjs: reads results.json -> generates docs/e2e/<test>.md per test
   - docs/README.md: top-level frontend docs index
   - docs/e2e/README.md: e2e index with link to each test
   - Each test markdown has breadcrumb [<- Back to index]
   - Screenshots embedded via relative path

Companion to PR #25 (Nuxt scaffold).

Out of scope (future PRs):
- Visual regression testing
- Storybook deployment / hosted preview
- More e2e test coverage

🤖 Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-03 16:39:54 +02:00

121 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const frontendDir = path.resolve(__dirname, '..')
const resultsPath = path.join(frontendDir, 'test-results', 'results.json')
const docsDir = path.join(frontendDir, 'docs', 'e2e')
const screenshotsDir = path.join(frontendDir, 'tests', 'e2e', 'screenshots')
async function main() {
// Read results
const resultsText = await fs.readFile(resultsPath, 'utf8')
const results = JSON.parse(resultsText)
// Create output directories
await fs.mkdir(docsDir, { recursive: true })
// Extract tests from suites
const testDocs = []
for (const suite of results.suites || []) {
for (const spec of suite.specs || []) {
for (const test of spec.tests || []) {
for (const result of test.results || []) {
const testInfo = {
title: spec.title,
specFile: spec.file || suite.file,
status: result.status,
duration: result.duration,
startTime: result.startTime,
attachments: result.attachments || [],
}
testDocs.push(testInfo)
}
}
}
}
// Generate individual test markdown files
for (const test of testDocs) {
const slug = slugify(test.title)
const mdPath = path.join(docsDir, `${slug}.md`)
// Use slug-based screenshot name (matches explicit screenshot in test)
let screenshotPath = `${slug}.png`
// Also try to find screenshot attachment and use its basename
if (test.attachments && test.attachments.length > 0) {
for (const attachment of test.attachments) {
if (attachment.contentType === 'image/png') {
const basename = path.basename(attachment.path)
// Prefer explicit screenshot name if it matches our pattern
if (basename !== 'test-finished-1.png' && basename !== 'test-finished-2.png') {
screenshotPath = basename
break
}
}
}
}
const absoluteScreenshotPath = path.join(screenshotsDir, screenshotPath)
const relativeScreenshotPath = path.relative(docsDir, absoluteScreenshotPath)
const mdContent = `# ${test.title}
[<- Back to index](./README.md) | [Top](../README.md)
**File**: \`tests/e2e/${test.specFile}\`
**Status**: ${test.status.toUpperCase()}
**Duration**: ${test.duration}ms
## Screenshot
![${test.title}](${relativeScreenshotPath})
## Test Details
- Start Time: ${test.startTime || 'N/A'}
- Spec File: ${test.specFile}
`
await fs.writeFile(mdPath, mdContent)
console.log(`Generated: ${path.relative(frontendDir, mdPath)}`)
}
// Generate index README
const indexContent = `# E2E Test Reports
[<- Up to docs](../README.md)
| Test | Status | Duration |
|------|--------|----------|
${testDocs.map(t => `| [${escapeMd(t.title)}](./${slugify(t.title)}.md) | ${t.status.toUpperCase()} | ${t.duration}ms |`).join('\n')}
`
await fs.writeFile(path.join(docsDir, 'README.md'), indexContent)
console.log(`Generated: ${path.relative(frontendDir, path.join(docsDir, 'README.md'))}`)
console.log(`\nGenerated ${testDocs.length} test docs`)
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '')
}
function escapeMd(str) {
return str.replace(/[|\\\[\]\{\}]/g, '\\$&')
}
main().catch(err => {
console.error('Error:', err)
process.exit(1)
})