Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Visual Regression

on:
pull_request:
branches: [main]
paths:
- "src/**"
- "tests/visual/**"
- ".github/workflows/visual-regression.yml"

env:
NODE_VERSION: 20

jobs:
visual-regression:
name: Visual Regression Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install chromium --with-deps

- name: Build app
run: npm run build

- name: Start dev server
run: npm run dev &
env:
CI: true

- name: Wait for server
run: npx wait-on http://localhost:3000 --timeout 120000

- name: Run visual regression tests
id: visual
run: npx playwright test --config=playwright.visual.config.ts
continue-on-error: true

- name: Generate visual report
if: always()
run: |
npx tsx -e "
const { collectEntries, generateReport } = require('./tests/visual/reportGenerator');
const fs = require('node:fs');
const entries = collectEntries();
const html = generateReport(entries);
fs.writeFileSync('visual-report.html', html);

// Write a machine-readable summary to a temp file for the PR comment step
const summary = { pass: 0, fail: 0, skip: 0, entries: [] as Record<string,unknown>[] };
for (const e of entries) {
if (e.status === 'fail') summary.fail++;
else if (e.status === 'pass') summary.pass++;
else if (e.status === 'skip') summary.skip++;
summary.entries.push({ route: e.route, viewport: e.viewport, diffRatio: (e.diffRatio * 100).toFixed(3), status: e.status });
}
fs.writeFileSync('visual-summary.json', JSON.stringify(summary));
console.log('Report and summary generated.');
"

- name: Upload diff artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: tests/visual/diffs/
retention-days: 30

- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-screenshots
path: tests/visual/results/
retention-days: 30

- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-report
path: visual-report.html
retention-days: 30

- name: Post PR comment
if: github.event_name == 'pull_request' && always()
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ ! -f visual-summary.json ]; then
echo "No visual-summary.json found — skipping PR comment."
exit 0
fi

SUMMARY_FILE="visual-pr-comment.md"

echo "## 🖼️ Visual Regression Results" > $SUMMARY_FILE
echo "" >> $SUMMARY_FILE
echo "| Route / Component | Viewport | Diff Ratio | Status |" >> $SUMMARY_FILE
echo "|---|---|---|---|" >> $SUMMARY_FILE

npx tsx -e "
const fs = require('node:fs');
const data = JSON.parse(fs.readFileSync('visual-summary.json', 'utf8'));
for (const e of data.entries) {
const icon = e.status === 'pass' ? '✅' : e.status === 'fail' ? '❌' : '⚠️';
console.log('| ' + e.route + ' | ' + e.viewport + ' | ' + e.diffRatio + '% | ' + icon + ' ' + e.status.toUpperCase() + ' |');
}
// Output totals as a special key-value format
console.log('__TOTALS__pass=' + data.pass + ',fail=' + data.fail + ',skip=' + data.skip);
" > /tmp/visual-table.md

# Extract totals line and table content
TOTALS=$(grep '__TOTALS__' /tmp/visual-table.md | head -1)
grep -v '__TOTALS__' /tmp/visual-table.md >> $SUMMARY_FILE

# Parse totals
PASS=$(echo "$TOTALS" | sed -n 's/.*pass=\([0-9]*\).*/\1/p')
FAIL=$(echo "$TOTALS" | sed -n 's/.*fail=\([0-9]*\).*/\1/p')
SKIP=$(echo "$TOTALS" | sed -n 's/.*skip=\([0-9]*\).*/\1/p')

echo "" >> $SUMMARY_FILE
echo "📊 **Summary:** ${PASS:-0} passed, ${FAIL:-0} failed, ${SKIP:-0} skipped" >> $SUMMARY_FILE
echo "" >> $SUMMARY_FILE
echo "📎 [View workflow artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $SUMMARY_FILE

gh pr comment ${{ github.event.pull_request.number }} --body-file $SUMMARY_FILE || echo "Could not post PR comment (may be a fork PR)."

- name: Stop dev server
if: always()
run: kill $(lsof -t -i:3000) 2>/dev/null || true

- name: Fail on visual regressions
if: steps.visual.outcome == 'failure'
run: exit 1
Loading
Loading