Skip to content

Commit 9d8156a

Browse files
authored
Visual regression test cleanup (#2933)
* clean up visual regression stuff * use a worktree for visual baseline, no stashing * same treatment on compare script * actually just delete the compare script * fix spurious time differences by setting fixed time * update safety test about e2e test file locations
1 parent 6e61797 commit 9d8156a

File tree

10 files changed

+84
-251
lines changed

10 files changed

+84
-251
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ playwright-report/
3333
.vercel
3434

3535
# Visual regression snapshots
36-
test/e2e/**/*-snapshots/
37-
test/e2e/**/*.png
36+
test/visual/**/*-snapshots/
37+
test/visual/**/*.png

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
- Cover role-gated flows by logging in with `getPageAsUser`; exercise negative paths (e.g., forbidden actions) alongside happy paths as shown in `test/e2e/system-update.e2e.ts`.
2828
- When UI needs new mock behavior, extend the MSW handlers/db minimally so E2E tests stay deterministic; prefer storing full API responses so subsequent calls see the updated state (`mock-api/msw/db.ts`, `mock-api/msw/handlers.ts`).
2929
- Co-locate Vitest specs next to the code they cover; use Testing Library utilities (`render`, `renderHook`, `fireEvent`, fake timers) to assert observable output rather than implementation details (`app/ui/lib/FileInput.spec.tsx`, `app/hooks/use-pagination.spec.ts`).
30-
- For sweeping styling changes, coordinate with the visual regression harness and follow `test/e2e/VISUAL-REGRESSION.md` for the workflow.
30+
- For sweeping styling changes, coordinate with the visual regression harness and follow `test/visual/README.md` for the workflow.
3131

3232
# Data fetching pattern
3333

app/api/__tests__/safety.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ const listFiles = (s: string) =>
7474
execSync(`git ls-files | grep "${s}"`).toString().trim().split('\n')
7575

7676
// avoid accidentally making an e2e file in the wrong place
77-
it('e2e tests are only in test/e2e', () => {
77+
it('e2e tests are only in test/e2e or test/visual', () => {
7878
for (const file of listFiles('\\.e2e\\.')) {
79-
expect(file).toMatch(/^test\/e2e/)
79+
expect(file).toMatch(/^test\/(e2e|visual)/)
8080
}
8181
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"e2e": "playwright test",
2424
"e2ec": "playwright test --project=chrome",
2525
"visual:baseline": "./tools/generate-visual-baseline.sh",
26-
"visual:compare": "./tools/compare-visual-changes.sh",
26+
"visual:compare": "playwright test -c playwright.visual.config.ts",
2727
"lint": "oxlint --type-aware && eslint --ext .js,.ts,.tsx app test mock-api",
2828
"oxlint": "oxlint --type-aware",
2929
"fmt": "prettier --cache --write . && npm run lint -- --fix",

playwright.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ export default {
2424
// default is 5 seconds. somehow playwright really hates async route modules,
2525
// takes a long time to load them. https://playwright.dev/docs/test-timeouts
2626
expect: { timeout: 10_000 },
27-
// Skip visual regression tests in CI
28-
grep: process.env.CI ? /^(?!.*@visual)/ : undefined,
2927
use: {
3028
trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
3129
baseURL: 'http://localhost:4009',

playwright.visual.config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import { devices, type PlaywrightTestConfig } from '@playwright/test'
10+
11+
// the regression test stuff is not run in CI and only needs one browser
12+
13+
export default {
14+
testDir: './test/visual',
15+
testMatch: /\.e2e\.ts/,
16+
fullyParallel: true,
17+
workers: '66%',
18+
use: {
19+
baseURL: 'http://localhost:4009',
20+
},
21+
projects: [
22+
{
23+
name: 'chrome',
24+
use: { ...devices['Desktop Chrome'] },
25+
},
26+
],
27+
// use different port so it doesn't conflict with local dev server
28+
webServer: {
29+
command: 'npm run start:msw -- --port 4009',
30+
port: 4009,
31+
},
32+
} satisfies PlaywrightTestConfig

test/e2e/VISUAL-REGRESSION.md renamed to test/visual/README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,7 @@ npm run visual:baseline -- abc123
1717
npm run visual:baseline -- HEAD~1
1818
```
1919

20-
This will:
21-
22-
1. Fetch the latest main branch (if using `--main`)
23-
2. Temporarily checkout the specified commit
24-
3. Install dependencies
25-
4. Run all E2E tests with `--update-snapshots` to generate baseline images
26-
5. Restore your working directory to its previous state
20+
This will check out that revision in a git work tree, run the visual snapshot tests, and copy the snapshots back into your current working directory.
2721

2822
### 2. Compare Current Changes
2923

@@ -60,7 +54,7 @@ If differences are detected:
6054
If the visual changes are intentional and correct:
6155

6256
```bash
63-
npm run visual:compare -- --update
57+
npm run visual:compare -- --update-snapshots
6458
```
6559

6660
This will update the baseline snapshots to match the current state.

test/e2e/visual-regression.e2e.ts renamed to test/visual/regression.e2e.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,92 +14,99 @@
1414
* CSS frameworks, or making broad styling changes.
1515
*/
1616

17-
import { expect, test } from './utils'
17+
import { expect, test } from '../e2e/utils'
18+
19+
// set a fixed time to avoid diffs due to irrelevant time differences
20+
test.beforeEach(async ({ page }) => {
21+
await page.clock.setFixedTime(new Date('2025-10-23T12:34:56.000Z'))
22+
})
1823

1924
test.describe('Visual Regression', { tag: '@visual' }, () => {
2025
test('projects list', async ({ page }) => {
2126
await page.goto('/projects')
22-
await page.getByRole('heading', { name: 'Projects' }).waitFor()
27+
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
2328
await expect(page).toHaveScreenshot('projects-list.png', { fullPage: true })
2429
})
2530

2631
test('instances list', async ({ page }) => {
2732
await page.goto('/projects/mock-project/instances')
28-
await page.getByRole('heading', { name: 'Instances' }).waitFor()
33+
await expect(page.getByRole('heading', { name: 'Instances' })).toBeVisible()
2934
await expect(page).toHaveScreenshot('instances-list.png', { fullPage: true })
3035
})
3136

3237
test('instance detail', async ({ page }) => {
3338
await page.goto('/projects/mock-project/instances/db1')
34-
await page.getByRole('heading', { name: 'db1' }).waitFor()
39+
await expect(page.getByRole('heading', { name: 'db1' })).toBeVisible()
3540
await expect(page).toHaveScreenshot('instance-detail.png', { fullPage: true })
3641
})
3742

3843
test('create disk', async ({ page }) => {
3944
await page.goto('/projects/mock-project/disks-new')
40-
await page.getByRole('heading', { name: 'Create disk' }).waitFor()
45+
await expect(page.getByRole('heading', { name: 'Create disk' })).toBeVisible()
4146
await expect(page).toHaveScreenshot('disks-new.png', { fullPage: true })
4247
})
4348

4449
test('disks list', async ({ page }) => {
4550
await page.goto('/projects/mock-project/disks')
46-
await page.getByRole('heading', { name: 'Disks' }).waitFor()
51+
await expect(page.getByRole('heading', { name: 'Disks' })).toBeVisible()
4752
await expect(page).toHaveScreenshot('disks-list.png', { fullPage: true })
4853
})
4954

5055
test('vpcs list', async ({ page }) => {
5156
await page.goto('/projects/mock-project/vpcs')
52-
await page.getByRole('heading', { name: 'VPCs' }).waitFor()
57+
await expect(page.getByRole('heading', { name: 'VPCs' })).toBeVisible()
5358
await expect(page).toHaveScreenshot('vpcs-list.png', { fullPage: true })
5459
})
5560

5661
test('snapshots list', async ({ page }) => {
5762
await page.goto('/projects/mock-project/snapshots')
58-
await page.getByRole('heading', { name: 'Snapshots' }).waitFor()
63+
await expect(page.getByRole('heading', { name: 'Snapshots' })).toBeVisible()
5964
await expect(page).toHaveScreenshot('snapshots-list.png', { fullPage: true })
6065
})
6166

6267
test('images list', async ({ page }) => {
6368
await page.goto('/projects/mock-project/images')
64-
await page.getByRole('heading', { name: 'Images' }).waitFor()
69+
await expect(page.getByRole('heading', { name: 'Images' })).toBeVisible()
6570
await expect(page).toHaveScreenshot('images-list.png', { fullPage: true })
6671
})
6772

6873
test('silo images list', async ({ page }) => {
6974
await page.goto('/images')
70-
await page.getByRole('heading', { name: 'Silo Images' }).waitFor()
75+
await expect(page.getByRole('heading', { name: 'Silo Images' })).toBeVisible()
7176
await expect(page).toHaveScreenshot('silo-images.png', { fullPage: true })
7277
await page.click('role=button[name="Promote image"]')
7378
await expect(page).toHaveScreenshot('silo-images-promote.png', { fullPage: true })
7479
})
7580

7681
test('silo image', async ({ page }) => {
7782
await page.goto('/images/arch-2022-06-01/edit')
78-
await page.getByRole('heading', { name: 'Silo image', exact: true }).waitFor()
83+
await expect(
84+
page.getByRole('heading', { name: 'Silo image', exact: true })
85+
).toBeVisible()
7986
await expect(page).toHaveScreenshot('silo-image.png', { fullPage: true })
8087
})
8188

8289
test('system utilization', async ({ page }) => {
8390
await page.goto('/utilization')
84-
await page.getByRole('heading', { name: 'Utilization' }).waitFor()
91+
await expect(page.getByRole('heading', { name: 'Utilization' })).toBeVisible()
8592
await expect(page).toHaveScreenshot('system-utilization.png', { fullPage: true })
8693
})
8794

8895
test('system silos list', async ({ page }) => {
8996
await page.goto('/system/silos')
90-
await page.getByRole('heading', { name: 'Silos' }).waitFor()
97+
await expect(page.getByRole('heading', { name: 'Silos' })).toBeVisible()
9198
await expect(page).toHaveScreenshot('system-silos.png', { fullPage: true })
9299
})
93100

94101
test('system networking ip pools', async ({ page }) => {
95102
await page.goto('/system/networking/ip-pools')
96-
await page.getByRole('heading', { name: 'IP Pools' }).waitFor()
103+
await expect(page.getByRole('heading', { name: 'IP Pools' })).toBeVisible()
97104
await expect(page).toHaveScreenshot('system-ip-pools.png', { fullPage: true })
98105
})
99106

100107
test('settings profile', async ({ page }) => {
101108
await page.goto('/settings/profile')
102-
await page.getByRole('heading', { name: 'Profile' }).waitFor()
109+
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible()
103110
await expect(page).toHaveScreenshot('settings-profile.png', { fullPage: true })
104111
})
105112

tools/compare-visual-changes.sh

Lines changed: 0 additions & 112 deletions
This file was deleted.

0 commit comments

Comments
 (0)