feat: integrate Playwright E2E test reports into GitHub Pages deployment

Add support for downloading and displaying Playwright E2E test reports
in the GitHub Pages deployment workflow. The reports are organized by
browser (chromium, chromium-2x, chromium-0.5x, mobile-chrome) and
presented via an interactive index page with test statistics.

Changes:
- Download Playwright reports from CI test runs (both triggered and latest)
- Organize reports into browser-specific directories
- Create interactive index page showing test stats (passed/failed/skipped/flaky)
- Integrate with existing .pages landing page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
snomiao
2025-10-20 18:49:37 +00:00
parent 89224e3e86
commit 152d456571
2 changed files with 336 additions and 0 deletions

View File

@@ -96,6 +96,52 @@ jobs:
workflow_conclusion: success
path: ./.pages/vitest-reports
- name: Download Playwright E2E reports (source run)
id: fetch_playwright_trigger
continue-on-error: true
if: github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Tests CI'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: ci-tests-e2e.yaml
name_is_regexp: true
name: playwright-report-.*
run_id: ${{ github.event.workflow_run.id }}
path: ./playwright-reports-temp
- name: Download Playwright E2E reports (latest successful run on main)
continue-on-error: true
if: steps.fetch_playwright_trigger.outcome != 'success'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: ci-tests-e2e.yaml
name_is_regexp: true
name: playwright-report-.*
branch: main
workflow_conclusion: success
path: ./playwright-reports-temp
- name: Organize Playwright reports by browser
if: always()
run: |
mkdir -p ./.pages/playwright-reports
# Move each browser report to its own directory
if [ -d "./playwright-reports-temp" ]; then
for dir in ./playwright-reports-temp/playwright-report-*; do
if [ -d "$dir" ]; then
browser_name=$(basename "$dir" | sed 's/playwright-report-//')
mkdir -p "./.pages/playwright-reports/${browser_name}"
cp -r "$dir"/* "./.pages/playwright-reports/${browser_name}/"
fi
done
fi
- name: Create Playwright reports index page
if: always()
run: node scripts/create-playwright-index.js
- name: Build static assets (with artifact reuse)
run: ./scripts/build-pages.sh

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env node
/**
* Creates an index page for Playwright test reports with test statistics
* Reads JSON reports from each browser and creates a landing page with cards
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const reportsDir = path.join(__dirname, '..', '.pages', 'playwright-reports')
function getTestStats(reportPath) {
try {
const reportJsonPath = path.join(reportPath, 'report.json')
if (!fs.existsSync(reportJsonPath)) {
console.warn(`No report.json found at ${reportJsonPath}`)
return null
}
const reportData = JSON.parse(fs.readFileSync(reportJsonPath, 'utf-8'))
let passed = 0
let failed = 0
let skipped = 0
let flaky = 0
// Parse Playwright JSON report format
if (reportData.suites) {
const countResults = (suites) => {
for (const suite of suites) {
if (suite.specs) {
for (const spec of suite.specs) {
if (!spec.tests || spec.tests.length === 0) continue
const test = spec.tests[0]
const results = test.results || []
// Check if test is flaky (has both pass and fail results)
const hasPass = results.some((r) => r.status === 'passed')
const hasFail = results.some((r) => r.status === 'failed')
if (hasPass && hasFail) {
flaky++
} else if (results.some((r) => r.status === 'passed')) {
passed++
} else if (results.some((r) => r.status === 'failed')) {
failed++
} else if (results.some((r) => r.status === 'skipped')) {
skipped++
}
}
}
if (suite.suites) {
countResults(suite.suites)
}
}
}
countResults(reportData.suites)
}
return { passed, failed, skipped, flaky }
} catch (error) {
console.error(`Error reading report at ${reportPath}:`, error.message)
return null
}
}
function generateIndexHtml(browsers) {
const cards = browsers
.map((browser) => {
const { name, stats } = browser
if (!stats) return ''
const total = stats.passed + stats.failed + stats.skipped + stats.flaky
const passRate = total > 0 ? ((stats.passed / total) * 100).toFixed(1) : 0
return `
<a href="./${name}/index.html" class="card">
<div class="card-header">
<h2>${name}</h2>
<span class="pass-rate ${stats.failed > 0 ? 'has-failures' : ''}">${passRate}%</span>
</div>
<div class="stats">
<div class="stat passed">
<span class="label">Passed</span>
<span class="value">${stats.passed}</span>
</div>
<div class="stat failed">
<span class="label">Failed</span>
<span class="value">${stats.failed}</span>
</div>
<div class="stat skipped">
<span class="label">Skipped</span>
<span class="value">${stats.skipped}</span>
</div>
<div class="stat flaky">
<span class="label">Flaky</span>
<span class="value">${stats.flaky}</span>
</div>
</div>
</a>
`
})
.join('')
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playwright E2E Test Reports</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.card h2 {
color: #333;
font-size: 1.5rem;
text-transform: capitalize;
}
.pass-rate {
background: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
font-size: 1.1rem;
}
.pass-rate.has-failures {
background: #ef4444;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat .label {
font-size: 0.875rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat .value {
font-size: 1.75rem;
font-weight: bold;
}
.stat.passed .value {
color: #10b981;
}
.stat.failed .value {
color: #ef4444;
}
.stat.skipped .value {
color: #f59e0b;
}
.stat.flaky .value {
color: #8b5cf6;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎭 Playwright E2E Test Reports</h1>
<div class="cards-grid">
${cards}
</div>
</div>
</body>
</html>`
}
function main() {
if (!fs.existsSync(reportsDir)) {
console.log(
'No playwright reports directory found, skipping index creation'
)
return
}
const browsers = []
const browserDirs = fs.readdirSync(reportsDir, { withFileTypes: true })
for (const dirent of browserDirs) {
if (dirent.isDirectory()) {
const browserName = dirent.name
const browserPath = path.join(reportsDir, browserName)
const stats = getTestStats(browserPath)
if (stats) {
browsers.push({ name: browserName, stats })
console.log(`✓ Found report for ${browserName}:`, stats)
}
}
}
if (browsers.length === 0) {
console.warn('No valid browser reports found')
return
}
const html = generateIndexHtml(browsers)
const indexPath = path.join(reportsDir, 'index.html')
fs.writeFileSync(indexPath, html, 'utf-8')
console.log(`✓ Created index page at ${indexPath}`)
}
main()