Skip to content
Draft
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
45 changes: 45 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,51 @@ jobs:
done
if [ "$FAILED" = "1" ]; then exit 1; fi

visual-tests:
name: Visual regression
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.2-noble
options: --ipc=host

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install unzip
run: apt-get update && apt-get install -y unzip

- name: Set up Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build design system
run: bun run design-system:build

- name: Run visual tests
working-directory: ./app
run: npx playwright test --grep @visual
env:
HOME: /root

- name: Upload HTML report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: app/playwright-report/
retention-days: 14

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-test-results
path: app/test-results/
retention-days: 7

build:
name: Build
runs-on: ubuntu-latest
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,51 @@ jobs:
done
if [ "$FAILED" = "1" ]; then exit 1; fi

visual-tests:
name: Visual regression
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.2-noble
options: --ipc=host

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install unzip
run: apt-get update && apt-get install -y unzip

- name: Set up Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build design system
run: bun run design-system:build

- name: Run visual tests
working-directory: ./app
run: npx playwright test --grep @visual
env:
HOME: /root

- name: Upload HTML report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: app/playwright-report/
retention-days: 14

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-test-results
path: app/test-results/
retention-days: 7

build:
name: Build
runs-on: ubuntu-latest
Expand Down
84 changes: 84 additions & 0 deletions .github/workflows/update-visual-baselines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Update visual baselines

on:
# Pattern 1: Comment "/update-snapshots" on a PR
issue_comment:
types: [created]
# Pattern 2: Manual trigger from Actions tab (pick a branch)
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
update-baselines:
name: Update visual baselines
# For issue_comment: only run on PRs when comment is "/update-snapshots"
# For workflow_dispatch: always run
if: >
github.event_name == 'workflow_dispatch' ||
(
github.event.issue.pull_request &&
github.event.comment.body == '/update-snapshots'
)
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.2-noble
options: --ipc=host

steps:
# For issue_comment, we need to resolve the PR branch name
- name: Get PR branch
if: github.event_name == 'issue_comment'
id: pr
run: |
apt-get update && apt-get install -y jq curl
PR_DATA=$(curl -s \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}")
echo "branch=$(echo "$PR_DATA" | jq -r '.head.ref')" >> "$GITHUB_OUTPUT"

- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ steps.pr.outputs.branch || github.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install unzip
run: |
if ! command -v unzip &> /dev/null; then
apt-get update && apt-get install -y unzip
fi

- name: Set up Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build design system
run: bun run design-system:build

- name: Update visual baselines
working-directory: ./app
run: npx playwright test --grep @visual --update-snapshots --reporter=list
env:
HOME: /root

- name: Commit updated baselines
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update visual baselines"
file_pattern: "app/e2e/__screenshots__/**/*.png"

# Add a rocket reaction so the commenter knows it worked
- name: React to comment
if: github.event_name == 'issue_comment'
run: |
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-d '{"content": "rocket"}'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ coverage/
*.lcov
.nyc_output

# Playwright
app/test-results/
app/playwright-report/
app/blob-report/

# Production
dist/
dist-*/
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install dev build test lint format clean deploy
.PHONY: help install dev build test lint format clean deploy pw-test pw-update pw-report

help:
@echo "Available commands:"
Expand All @@ -11,6 +11,9 @@ help:
@echo " make format - Format code"
@echo " make clean - Clean build artifacts"
@echo " make deploy - Build and deploy to GitHub Pages"
@echo " make pw-test - Run Playwright visual tests"
@echo " make pw-update - Update visual test baselines"
@echo " make pw-report - Open Playwright HTML report"

install:
bun install
Expand Down Expand Up @@ -41,3 +44,12 @@ clean:

deploy: build
@echo "Build complete. GitHub Actions will handle deployment"

pw-test:
cd app && npx playwright test --grep @visual

pw-update:
cd app && npx playwright test --grep @visual --update-snapshots

pw-report:
cd app && bunx playwright show-report
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions app/e2e/visual/homepage.visual.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expect, test } from '@playwright/test';

test.describe('Homepage', () => {
test('renders correctly @visual', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot('homepage.png', {

Check failure on line 7 in app/e2e/visual/homepage.visual.spec.ts

View workflow job for this annotation

GitHub Actions / Visual regression

[chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual

1) [chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual ──── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(page).toHaveScreenshot(expected) failed Expected an image 1280px by 720px, received 1280px by 3493px. 956812 pixels (ratio 0.22 of all image pixels) are different. Snapshot: homepage.png Call log: - Expect "toHaveScreenshot(homepage.png)" with timeout 5000ms - verifying given screenshot expectation - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - Expected an image 1280px by 720px, received 1280px by 3493px. 956355 pixels (ratio 0.22 of all image pixels) are different. - waiting 100ms before taking screenshot - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - captured a stable screenshot - Expected an image 1280px by 720px, received 1280px by 3493px. 956812 pixels (ratio 0.22 of all image pixels) are different. 5 | await page.goto('/'); 6 | await page.waitForLoadState('networkidle'); > 7 | await expect(page).toHaveScreenshot('homepage.png', { | ^ 8 | fullPage: true, 9 | }); 10 | }); at /__w/policyengine-app-v2/policyengine-app-v2/app/e2e/visual/homepage.visual.spec.ts:7:24

Check failure on line 7 in app/e2e/visual/homepage.visual.spec.ts

View workflow job for this annotation

GitHub Actions / Visual regression

[chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual

1) [chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual ──── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(page).toHaveScreenshot(expected) failed Expected an image 1280px by 720px, received 1280px by 3493px. 953042 pixels (ratio 0.22 of all image pixels) are different. Snapshot: homepage.png Call log: - Expect "toHaveScreenshot(homepage.png)" with timeout 5000ms - verifying given screenshot expectation - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - Expected an image 1280px by 720px, received 1280px by 3493px. 952951 pixels (ratio 0.22 of all image pixels) are different. - waiting 100ms before taking screenshot - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - captured a stable screenshot - Expected an image 1280px by 720px, received 1280px by 3493px. 953042 pixels (ratio 0.22 of all image pixels) are different. 5 | await page.goto('/'); 6 | await page.waitForLoadState('networkidle'); > 7 | await expect(page).toHaveScreenshot('homepage.png', { | ^ 8 | fullPage: true, 9 | }); 10 | }); at /__w/policyengine-app-v2/policyengine-app-v2/app/e2e/visual/homepage.visual.spec.ts:7:24

Check failure on line 7 in app/e2e/visual/homepage.visual.spec.ts

View workflow job for this annotation

GitHub Actions / Visual regression

[chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual

1) [chromium] › e2e/visual/homepage.visual.spec.ts:4:3 › Homepage › renders correctly @visual ──── Error: expect(page).toHaveScreenshot(expected) failed Expected an image 1280px by 720px, received 1280px by 3493px. 954325 pixels (ratio 0.22 of all image pixels) are different. Snapshot: homepage.png Call log: - Expect "toHaveScreenshot(homepage.png)" with timeout 5000ms - verifying given screenshot expectation - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - Expected an image 1280px by 720px, received 1280px by 3493px. 954023 pixels (ratio 0.22 of all image pixels) are different. - waiting 100ms before taking screenshot - taking page screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - captured a stable screenshot - Expected an image 1280px by 720px, received 1280px by 3493px. 954325 pixels (ratio 0.22 of all image pixels) are different. 5 | await page.goto('/'); 6 | await page.waitForLoadState('networkidle'); > 7 | await expect(page).toHaveScreenshot('homepage.png', { | ^ 8 | fullPage: true, 9 | }); 10 | }); at /__w/policyengine-app-v2/policyengine-app-v2/app/e2e/visual/homepage.visual.spec.ts:7:24
fullPage: true,
});
});
});
11 changes: 11 additions & 0 deletions app/e2e/visual/screenshot.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* Applied during Playwright visual screenshots to eliminate flaky dynamic content. */

*,
*::before,
*::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
scroll-behavior: auto !important;
}
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"devDependencies": {
"@eslint/js": "^9.29.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
"@playwright/test": "^1.58.2",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
Expand Down
69 changes: 69 additions & 0 deletions app/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;
const PW_VERSION = '1.58.2';
const PW_IMAGE = `mcr.microsoft.com/playwright:v${PW_VERSION}-noble`;
const PW_SERVER_PORT = 3200;

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: isCI ? [['html', { open: 'never' }], ['github']] : [['html', { open: 'on-failure' }]],

snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}-{projectName}{ext}',

expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.005,
threshold: 0.2,
animations: 'disabled',
caret: 'hide',
scale: 'css',
stylePath: './e2e/visual/screenshot.css',
},
},

use: {
baseURL: isCI ? 'http://localhost:3000' : 'http://host.docker.internal:3000',
trace: 'on-first-retry',
timezoneId: 'America/New_York',
...(!isCI && {
connectOptions: {
wsEndpoint: `ws://127.0.0.1:${PW_SERVER_PORT}/`,
},
}),
},

webServer: [
{
command: 'VITE_APP_MODE=website npx vite',
url: 'http://localhost:3000',
reuseExistingServer: !isCI,
timeout: 120_000,
},
...(!isCI
? [
{
command: `docker run --rm --init -p ${PW_SERVER_PORT}:${PW_SERVER_PORT} ${PW_IMAGE} npx playwright run-server --port ${PW_SERVER_PORT} --host 0.0.0.0`,
url: `http://localhost:${PW_SERVER_PORT}`,
timeout: 120_000,
reuseExistingServer: !isCI,
gracefulShutdown: { signal: 'SIGTERM' as const, timeout: 10_000 },
},
]
: []),
],

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1280, height: 720 },
},
},
],
});
1 change: 1 addition & 0 deletions app/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,6 @@ export default defineConfig({
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.mjs',
exclude: ['e2e/**', 'node_modules/**'],
},
});
Loading
Loading