Skip to content

Commit 20cafed

Browse files
committed
e2e : add general assertions and snapshot tests
Ensure consistent results between local and CI by running E2E tests inside a Playwright Docker container.
1 parent efa077d commit 20cafed

19 files changed

Lines changed: 231 additions & 58 deletions

.github/workflows/e2e.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
node-version: lts/*
1616
- name: Install dependencies
1717
run: npm ci
18-
- name: Install Playwright Browsers
19-
run: npx playwright install --with-deps
18+
- name: Build Docker image
19+
run: docker build -t playwright-tests .
2020
- name: Run Playwright tests
21-
run: npx playwright test
21+
run: npm run test:docker
2222
- uses: actions/upload-artifact@v4
2323
if: ${{ !cancelled() }}
2424
with:

CONTRIBUTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,21 @@ npm run lint
3737
```sh
3838
npm run test
3939
```
40+
41+
42+
## Run E2E tests
43+
44+
```sh
45+
npm run test:docker:build
46+
npm run test:docker
47+
```
48+
49+
E2E tests are executed inside a Docker container using Playwright, ensuring consistent results between local and CI environments. The Docker image only needs to be built once before running the tests, unless dependencies change.
50+
51+
### Update snapshots
52+
53+
```sh
54+
npm run test:docker:update
55+
```
56+
57+
Visual testing via snapshots is supported by Playwright. To update or create new snapshots, run this command.

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM mcr.microsoft.com/playwright:v1.61.0-noble
2+
3+
WORKDIR /app
4+
COPY package*.json ./
5+
RUN npm ci
6+
COPY . .
7+
RUN npx playwright install
8+
9+
RUN apt-get update && apt-get install -y \
10+
fonts-noto-core \
11+
fonts-noto-extra \
12+
&& rm -rf /var/lib/apt/lists/* \
13+
&& fc-cache -fv
14+
15+
CMD ["npx", "playwright", "test"]

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
"typecheck": "vue-tsc --noEmit",
2727
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-pattern .gitignore",
2828
"prepare": "husky",
29-
"test": "LANG=en_US.UTF-8 vitest --run"
29+
"test": "LANG=en_US.UTF-8 vitest --run",
30+
"test:docker:build": "docker build -t playwright-tests .",
31+
"test:docker": "docker run --rm --ipc=host -v $PWD:$PWD -w $PWD playwright-tests npx playwright test",
32+
"test:docker:update": "docker run --rm --ipc=host -v $PWD:$PWD -w $PWD playwright-tests npx playwright test --update-snapshots"
3033
},
3134
"dependencies": {
3235
"@fortawesome/fontawesome-svg-core": "^6.7.2",

playwright.config.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,46 +34,16 @@ export default defineConfig({
3434

3535
/* Configure projects for major browsers */
3636
projects: [
37-
{
38-
name: 'chromium',
39-
use: { ...devices['Desktop Chrome'] },
40-
},
41-
4237
{
4338
name: 'firefox',
4439
use: { ...devices['Desktop Firefox'] },
4540
},
46-
47-
{
48-
name: 'webkit',
49-
use: { ...devices['Desktop Safari'] },
50-
},
51-
52-
/* Test against mobile viewports. */
53-
// {
54-
// name: 'Mobile Chrome',
55-
// use: { ...devices['Pixel 5'] },
56-
// },
57-
// {
58-
// name: 'Mobile Safari',
59-
// use: { ...devices['iPhone 12'] },
60-
// },
61-
62-
/* Test against branded browsers. */
63-
// {
64-
// name: 'Microsoft Edge',
65-
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
66-
// },
67-
// {
68-
// name: 'Google Chrome',
69-
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
70-
// },
7141
],
7242

7343
/* Run your local dev server before starting the tests */
74-
// webServer: {
75-
// command: 'npm run start',
76-
// url: 'http://localhost:3000',
77-
// reuseExistingServer: !process.env.CI,
78-
// },
44+
webServer: {
45+
command: 'npm run dev',
46+
url: 'http://localhost:5173',
47+
reuseExistingServer: !process.env.CI,
48+
},
7949
});

src/components/Plan.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ function updateNodeSize(node: Node, size: [number, number]) {
502502
</li>
503503
</ul>
504504
<div class="ms-auto me-2 small">
505-
<a href="https://github.com/dalibo/pev2" target="_blank">
505+
<a href="https://github.com/dalibo/pev2" target="_blank" data-testid="version">
506506
<LogoImage />
507507
{{ version }}
508508
</a>

tests/e2e/example.spec.ts

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

tests/e2e/plan.spec.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.goto('http://localhost:5173/');
5+
await page.getByRole('button', { name: 'Sample Plans' }).click();
6+
await page.getByRole('link', { name: 'Simple join (JSON format)' }).click();
7+
await page.getByRole('button', { name: 'Submit' }).click();
8+
});
9+
10+
test('verify plan has title', async ({ page }) => {
11+
await expect(page.getByText('Simple join (JSON format)')).toBeVisible();
12+
});
13+
14+
test('redirects to home when clicking on New Plan link', async ({ page }) => {
15+
await page.getByText('New Plan').click();
16+
await expect(page).toHaveURL('http://localhost:5173/');
17+
});
18+
19+
test('redirects to home when clicking pev2 logo', async ({ page }) => {
20+
await page.getByText('New Plan').click();
21+
await expect(page).toHaveURL('http://localhost:5173/');
22+
});
23+
24+
test('switches to dark mode', async ({ page }) => {
25+
await page.getByTitle('Switch to dark mode').click();
26+
await expect(page).toHaveScreenshot('plan_darkMode.png');
27+
});
28+
29+
test('redirects to About page', async ({ page }) => {
30+
await page.getByText('About').click();
31+
await expect(page.getByText('Disclaimer')).toBeVisible();
32+
});
33+
34+
test('opens pev2 GitHub link in a new tab', async ({ page }) => {
35+
await page.getByTestId('version').click();
36+
const popupPromise = page.waitForEvent('popup');
37+
const popup = await popupPromise;
38+
39+
await expect(popup).toHaveURL('https://github.com/dalibo/pev2');
40+
});
41+
42+
test.describe('node', () => {
43+
test('node box toggle shows and hides tabs', async ({ page }) => {
44+
const node = page.locator('.plan-node', { hasText: 'Nested Loop' });
45+
46+
const toggle = node.getByRole('heading', { name: 'Nested Loop' })
47+
const tabs = node.locator('li.nav-item a');
48+
49+
//open
50+
await toggle.click();
51+
await expect(node).toHaveClass(/detailed/);
52+
await expect(tabs).toContainText([
53+
'General',
54+
'IO & Buffers',
55+
'Output',
56+
'Workers',
57+
'Misc',
58+
]);
59+
await expect(node.getByRole('link', { name: 'General' })).toHaveClass(/active/);
60+
await expect(node.getByRole('link', { name: 'IO & Buffers' })).toHaveClass(/disabled/);
61+
62+
//close
63+
await toggle.click();
64+
await expect(node).not.toHaveClass('detailed');
65+
});
66+
});
67+
68+
test.describe('plan style', () => {
69+
let rightPanel;
70+
71+
test.beforeEach(async ({ page }) => {
72+
rightPanel = page.locator('.splitpanes__pane.plan');
73+
});
74+
75+
76+
test('displays plan', async ({ page }) => {
77+
const noneButton = rightPanel.getByRole('button', { name: 'none' });
78+
79+
await expect(noneButton).toHaveClass(/active/)
80+
await expect(page).toHaveScreenshot('plan_none.png');
81+
});
82+
83+
test('displays plan with duration', async ({ page }) => {
84+
const durationButton = rightPanel.getByRole('button', { name: 'duration' });
85+
await durationButton.click()
86+
87+
await expect(durationButton).toHaveClass(/active/)
88+
await expect(page).toHaveScreenshot('plan_duration.png');
89+
});
90+
91+
test('displays plan with rows', async ({ page }) => {
92+
const rowsButton = rightPanel.getByRole('button', { name: 'rows' });
93+
await rowsButton.click()
94+
95+
await expect(rowsButton).toHaveClass(/active/)
96+
await expect(page).toHaveScreenshot('plan_rows.png');
97+
});
98+
99+
test('displays plan with cost', async ({ page }) => {
100+
const costButton = rightPanel.getByRole('button', { name: 'duration' });
101+
await costButton.click()
102+
103+
await expect(costButton).toHaveClass(/active/)
104+
await expect(page).toHaveScreenshot('plan_cost.png');
105+
});
106+
});
107+
108+
test.describe('diagram view', () => {
109+
let leftPanel;
110+
111+
test.beforeEach(async ({ page }) => {
112+
leftPanel = page.locator('.diagram');
113+
});
114+
115+
test('selects node', async ({ page }) => {
116+
const row = leftPanel.getByRole('row', { name: /Nested Loop/ })
117+
const node = page.locator('.plan-node', { hasText: 'Nested Loop' });
118+
await row.click();
119+
120+
await expect(row).toHaveClass(/selected/)
121+
await expect(node).toHaveClass(/detailed/)
122+
await expect(node).toHaveClass(/selected/);
123+
await expect(page).toHaveScreenshot('plan_diagram.png');
124+
});
125+
126+
test('selects buffers button in diagram option', async ({ page }) => {
127+
const bufferButton = leftPanel.getByRole('button', { name: 'buffers' });
128+
await bufferButton.click();
129+
130+
await expect(bufferButton).toHaveClass(/active/);
131+
await expect(page).toHaveScreenshot('plan_buffers_diagram.png');
132+
});
133+
});
134+
135+
test.describe('plan tabs', () => {
136+
let navPills;
137+
138+
test.beforeEach(async ({ page }) => {
139+
navPills = page.locator('.nav.nav-pills');
140+
});
141+
142+
test('redirects to Plan view', async ({ page }) => {
143+
const planLink = navPills.getByRole('link', { name: 'Plan' });
144+
145+
await planLink.click();
146+
await expect(planLink).toHaveClass(/active/)
147+
await expect(page).toHaveURL(url => url.hash === '#plan')
148+
});
149+
150+
test('redirects to Grid view', async ({ page }) => {
151+
const gridLink = navPills.getByRole('link', { name: 'Grid' });
152+
153+
await gridLink.click();
154+
await expect(gridLink).toHaveClass(/active/)
155+
await expect(page).toHaveURL(url => url.hash === '#grid');
156+
await expect(page).toHaveScreenshot('plan_gridView.png');
157+
});
158+
159+
test('redirects to Raw view', async ({ page }) => {
160+
const rawLink = navPills.getByRole('link', { name: 'Raw' });
161+
162+
await rawLink.click();
163+
await expect(rawLink).toHaveClass(/active/)
164+
await expect(page).toHaveURL(url => url.hash === '#raw')
165+
await expect(page).toHaveScreenshot('plan_rawView.png');
166+
});
167+
168+
test('redirects to Query view', async ({ page }) => {
169+
const queryLink = navPills.getByRole('link', { name: 'Query' });
170+
171+
await queryLink.click();
172+
await expect(queryLink).toHaveClass(/active/)
173+
await expect(page).toHaveURL(url => url.hash === '#query')
174+
await expect(page).toHaveScreenshot('plan_queryView.png');
175+
});
176+
177+
test('redirects to Stats view', async ({ page }) => {
178+
const statsLink = navPills.getByRole('link', { name: 'Stats' });
179+
180+
await statsLink.click();
181+
await expect(statsLink).toHaveClass(/active/)
182+
await expect(page).toHaveURL(url => url.hash === '#stats')
183+
await expect(page).toHaveScreenshot('plan_statsView.png');
184+
});
185+
});
85.2 KB
Loading
92.2 KB
Loading

0 commit comments

Comments
 (0)