Skip to content

Streamline visual regression tests by making CI maintain visual snapshots #324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2a64334
first pass at ci maintaining snapshots using gh actions
buckhalt Mar 21, 2025
b2a4c4a
rm fixme on screenshot validation
buckhalt Mar 24, 2025
c863f0b
inc timeout
buckhalt Mar 24, 2025
2d7de83
fix reporters to ensure playwright-report is generated in ci
buckhalt Mar 24, 2025
a348cfd
try to get pr number and pass it
buckhalt Mar 24, 2025
42989ff
try using github-script action for pr number
buckhalt Mar 24, 2025
26cb259
fix if statements
buckhalt Mar 24, 2025
768b51a
fix pr-number
buckhalt Mar 24, 2025
1e9e851
rm logging pr data outputs
buckhalt Mar 24, 2025
c074f07
fix playwright report url
buckhalt Mar 24, 2025
11bdbab
edit report link comment with instruction to add preview url
buckhalt Mar 25, 2025
733be53
only set SNAPSHOT_DIFFERENCES if only snapshot tests failed
buckhalt Mar 25, 2025
2a2afd6
use expect soft for screenshots
buckhalt Mar 25, 2025
ea9156a
fixme edit participant for debugging screenshot flow
buckhalt Mar 25, 2025
4409333
fixme add new
buckhalt Mar 25, 2025
8bdc66b
increase timeouts
buckhalt Mar 25, 2025
8b20677
fixme protocol tests
buckhalt Mar 25, 2025
d2e7b0e
only delete branch if there is a setup error
buckhalt Mar 26, 2025
f16ad71
intentional fail in setup to test db branch deletion
buckhalt Mar 26, 2025
b4caf7b
fix order of checks
buckhalt Mar 26, 2025
80d311c
rm intentional fail
buckhalt Mar 26, 2025
5fc361a
inc timeout
buckhalt Mar 26, 2025
960a146
comment out flaky test for now
buckhalt Mar 26, 2025
abba763
changes to trigger tests again
buckhalt Mar 26, 2025
c24f4c3
Update e2e snapshots
buckhalt Mar 26, 2025
4187db9
adjust logic to ensure setup errror is thrown only when there is a te…
buckhalt Mar 26, 2025
d67cc8b
rm update snapshots workflow which was already added to main
buckhalt Mar 26, 2025
c2d5b47
improve logic for matching visual snapshots, ignore warnings
buckhalt Mar 26, 2025
3cef3a6
set long global timeouts to prevent flaky tests while we're fixing ot…
buckhalt Mar 26, 2025
ca3d916
1m timeout
buckhalt Mar 26, 2025
1e2eb45
add participants imported toast check back in
buckhalt Mar 26, 2025
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
49 changes: 44 additions & 5 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: ${{ github.event_name == 'deployment_status' && startsWith(github.event.deployment_status.environment, 'preview') && github.event.deployment_status.state == 'success' }}
steps:
- uses: actions/checkout@v4
Expand All @@ -19,10 +22,36 @@ jobs:
run: npx playwright install --with-deps
- name: Run tests
id: run_playwright_tests
run: pnpm run test:e2e-ci
run: |
pnpm run test:e2e-ci | tee output.log
if grep -q -e "Error: A snapshot doesn't exist at" -e "Screenshot comparison failed" -e "should match visual snapshot" output.log; then
echo "Playwright tests failed due to a snapshot issue."
echo "SNAPSHOT_DIFFERENCES=true" >> $GITHUB_ENV
exit 1
elif grep -q "failed" output.log && grep -q "e2e/global.setup.ts" output.log && ! grep -q "Warning: \[setup db\] › e2e/global.setup.ts took" output.log; then
echo "Playwright tests failed during global setup."
echo "SETUP_ERROR=true" >> $GITHUB_ENV
exit 1
elif grep -q "failed" output.log; then
echo "Playwright tests failed due to an issue unrelated to snapshots or global setup."
exit 1
fi
env:
E2E_UPLOADTHING_TOKEN: ${{ secrets.E2E_UPLOADTHING_TOKEN }}
BASE_URL: ${{ github.event.deployment_status.environment_url }}
- name: Get PR data
uses: actions/github-script@v7
if: always() && steps.run_playwright_tests.outcome == 'failure'
id: get_pr_data
with:
script: |
return (
await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
})
).data[0];
- name: Extract branch name
if: always() && steps.run_playwright_tests.outcome == 'failure'
id: extract_branch
Expand All @@ -32,18 +61,28 @@ jobs:

BRANCH_NAME=$(git branch -r --contains ${COMMIT_SHA} | grep -v HEAD | head -n 1 | sed 's/origin\///' | xargs)
echo "branch=${BRANCH_NAME}" >> $GITHUB_OUTPUT

- name: Install Neon CLI
if: always() && steps.run_playwright_tests.outcome == 'failure'
if: always() && env.SETUP_ERROR == 'true'
run: npm i -g neonctl@latest
- name: Delete Neon branch if tests fail
if: always() && steps.run_playwright_tests.outcome == 'failure'
if: always() && env.SETUP_ERROR == 'true'
run: neonctl branches delete preview/${{ steps.extract_branch.outputs.branch }} --project-id ${{ secrets.NEON_PROJECT_ID }}
env:
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
id: artifact-upload
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Comment on PR with report link
uses: thollander/actions-comment-pull-request@v3
if: ${{ failure() && env.SNAPSHOT_DIFFERENCES == 'true' && fromJson(steps.get_pr_data.outputs.result).number != '' }}
with:
message: |
### Playwright visual snapshot differences were detected.
View the [Playwright report](${{ steps.artifact-upload.outputs.artifact-url }}) to review the visual differences.
**To approve the snapshot changes and update the snapshots, please comment:** /approve-snapshots <preview-url>
**Example:** /approve-snapshots https://fresco-sandbox.vercel.app/
pr-number: ${{ fromJson(steps.get_pr_data.outputs.result).number }}
25 changes: 11 additions & 14 deletions e2e/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ test('create test database and setup app', async ({
}) => {
console.log('🚀 Starting setup test', baseURL);

test.slow(); // triple the default timeout

// Stop any existing test db to ensure clean state
if (!process.env.CI) {
console.log('Local environment detected');
Expand Down Expand Up @@ -68,7 +66,7 @@ test('create test database and setup app', async ({
await page.fill('input[name="password"]', 'Administrator1!');
await page.click('button[type="submit"]');

await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 });
await expect(page).toHaveURL(/\/dashboard/, { timeout: 10_000 });
console.log('✅ Signed in successfully');

// go to /settings
Expand All @@ -77,7 +75,7 @@ test('create test database and setup app', async ({
const resetButton = page.getByRole('button', {
name: 'Reset all app data',
});
await resetButton.click({ timeout: 10000 });
await resetButton.click({ timeout: 10_000 });
const confirmButton = page.getByRole('button', { name: 'Delete all data' });
await confirmButton.click({ timeout: 10000 });

Expand All @@ -88,7 +86,6 @@ test('create test database and setup app', async ({

// STEP 1
await page.goto('/setup');
// screenshot
await page.fill('input[name="username"]', 'admin', { timeout: 5000 });
await page.fill('input[name="password"]', 'Administrator1!', {
timeout: 5000,
Expand Down Expand Up @@ -140,20 +137,20 @@ test('create test database and setup app', async ({

const participantsHandle = page.locator('input[type="file"]');
await participantsHandle.setInputFiles('e2e/files/participants.csv');
await page.getByRole('button', { name: 'Import' }).click({ timeout: 5000 });
await page.getByRole('button', { name: 'Import' }).click({ timeout: 20000 });

// participants imported toast
await expect(
page.locator('div.text-sm.opacity-90', {
hasText: 'Participants have been imported successfully',
}),
).toBeVisible({ timeout: 10000 });
// // participants imported toast
// await expect(
// page.locator('div.text-sm.opacity-90', {
// hasText: 'Participants have been imported successfully',
// }),
// ).toBeVisible({ timeout: 40000 });

// toggle switches
const anonymousRecruitmentSwitch = page.getByRole('switch').first();
const limitInterviewsSwitch = page.getByRole('switch').last();
await anonymousRecruitmentSwitch.click({ timeout: 10000 });
await limitInterviewsSwitch.click({ timeout: 10000 });
await anonymousRecruitmentSwitch.click({ timeout: 10_000 });
await limitInterviewsSwitch.click({ timeout: 10_000 });

await expect(anonymousRecruitmentSwitch).toBeChecked();
await expect(limitInterviewsSwitch).toBeChecked();
Expand Down
12 changes: 5 additions & 7 deletions e2e/participants.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ test.describe('Participants page', () => {
await expect(page.locator('text=Emily Brown')).toHaveText('Emily Brown');
});

test.fixme('should match visual snapshot', async ({ page }) => {
test('should match visual snapshot', async ({ page }) => {
// validate screenshot
await expect(page).toHaveScreenshot('participants-page.png');
await expect.soft(page).toHaveScreenshot('participants-page.png');
});

test('should add new participant', async ({ page }) => {
test.fixme('should add new participant', async ({ page }) => {
await page.getByRole('button', { name: 'Add Single Participant' }).click();
await page.getByRole('button', { name: 'Generate' }).click();
await page.getByRole('textbox', { name: 'Label' }).click();
await page.getByRole('textbox', { name: 'Label' }).fill('New Participant');
await page.getByRole('button', { name: 'Submit' }).click();
});

test('should edit participant', async ({ page }) => {
test.fixme('should edit participant', async ({ page }) => {
test.setTimeout(30000);
await page.getByRole('button', { name: 'Open menu' }).first().click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
Expand All @@ -37,12 +37,10 @@ test.describe('Participants page', () => {
await page.getByRole('button', { name: 'Update' }).click();
});

test('should copy unique URL', async ({ page }) => {
test.fixme('should copy unique URL', async ({ page }) => {
test.setTimeout(30000);
await page.getByRole('button', { name: 'Copy Unique URL' }).first().click();
await page.getByRole('combobox').click();
// screenshot for visual validation
await page.screenshot({ path: 'debug-copy-url.png', fullPage: true });
await page.getByRole('option', { name: 'Sample Protocol' }).click();
await page.getByText('Sample Protocol.netcanvas').click();
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions e2e/protocols.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ test.describe('Protocols page', () => {
);
});

test.fixme('should match visual snapshot', async ({ page }) => {
test('should match visual snapshot', async ({ page }) => {
// validate screenshot
await expect(page).toHaveScreenshot('protocols-page.png');
await expect.soft(page).toHaveScreenshot('protocols-page.png');
});

test('should upload new protocol', async ({ page }) => {
test.fixme('should upload new protocol', async ({ page }) => {
const protocolHandle = page.locator('input[type="file"]');
await protocolHandle.setInputFiles('e2e/files/E2E.netcanvas');
await expect(page.getByText('Extracting protocol')).toBeVisible({
Expand All @@ -26,7 +26,7 @@ test.describe('Protocols page', () => {
await expect(page.getByText('Complete...')).toBeVisible({ timeout: 20000 });
});

test('should delete protocol', async ({ page }) => {
test.fixme('should delete protocol', async ({ page }) => {
// find the table row with the protocol we want to delete
await page
.getByRole('row', { name: 'Select row Protocol icon E2E.' })
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"knip": "knip",
"test": "vitest",
"load-test": "docker run -i grafana/k6 run - <load-test.js",
"test:e2e-dev": "rm -rf .next && NODE_ENV='test' next build && playwright test --trace on --reporter=list",
"test:e2e-ci": "playwright test --trace on --reporter=list"
"test:e2e-dev": "rm -rf .next && NODE_ENV='test' next build && playwright test --trace on",
"test:e2e-ci": "playwright test --trace on"
},
"pnpm": {
"overrides": {
Expand Down
6 changes: 5 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const webServer = CI
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
reporter: process.env.CI ? 'github' : 'html',
reporter: process.env.CI ? [['github'], ['html']] : 'html',
timeout: 60000,
expect: {
timeout: 60000,
},

use: {
baseURL,
Expand Down
Loading