✨ 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>
This commit is contained in:
120
frontend/scripts/generate-test-docs.mjs
Normal file
120
frontend/scripts/generate-test-docs.mjs
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/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 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)
|
||||
})
|
||||
Reference in New Issue
Block a user