Skip to content

Commit 672e298

Browse files
committed
Add e2e tests
1 parent fd1cd30 commit 672e298

7 files changed

Lines changed: 303 additions & 2 deletions

File tree

.github/workflows/run-tests.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,11 @@ jobs:
1616
- name: Install Dependencies
1717
run: npm ci
1818

19-
- name: Run Tests
19+
- name: Run Unit Tests
2020
run: npx vitest run
21+
22+
- name: Install Playwright Browsers
23+
run: npx playwright install chromium --with-deps
24+
25+
- name: Run E2E Tests
26+
run: npx playwright test

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"generate": "nuxt generate",
1616
"preview": "nuxt preview",
1717
"format": "prettier --write 'app/**/*.{vue,js,ts}'",
18-
"test": "vitest"
18+
"test": "vitest",
19+
"test:e2e": "playwright test"
1920
},
2021
"devDependencies": {
2122
"@formkit/auto-animate": "^0.9.0",
@@ -24,6 +25,7 @@
2425
"@nuxtjs/tailwindcss": "^6.14.0",
2526
"@panzoom/panzoom": "^4.6.1",
2627
"@pinia/nuxt": "^0.11.3",
28+
"@playwright/test": "^1.58.2",
2729
"@tailwindcss/forms": "^0.5.11",
2830
"@vue/test-utils": "^2.4.6",
2931
"@vueuse/core": "^14.2.1",

playwright.config.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './tests/e2e',
5+
timeout: 30000,
6+
retries: 0,
7+
use: {
8+
baseURL: 'http://localhost:3000',
9+
headless: true,
10+
},
11+
projects: [
12+
{
13+
name: 'chromium',
14+
use: { browserName: 'chromium' },
15+
},
16+
],
17+
webServer: {
18+
command: 'npm run dev',
19+
url: 'http://localhost:3000',
20+
reuseExistingServer: true,
21+
timeout: 120000,
22+
},
23+
});
24+

test-results/.last-run.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": "passed",
3+
"failedTests": []
4+
}

tests/e2e/tabs.spec.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const selectors = {
4+
tab: '[data-tab-id]',
5+
project: '[data-project-id]',
6+
};
7+
8+
/**
9+
* Get all visible tabs from the page.
10+
*/
11+
function getTabs(page) {
12+
return page.evaluate((sel) => {
13+
return Array.from(document.querySelectorAll(sel)).map((tab) => ({
14+
id: tab.dataset.tabId,
15+
text: tab.textContent?.trim(),
16+
width: tab.offsetWidth,
17+
height: tab.offsetHeight,
18+
opacity: window.getComputedStyle(tab).opacity,
19+
}));
20+
}, selectors.tab);
21+
}
22+
23+
/**
24+
* Get all visible projects from the page.
25+
*/
26+
function getProjects(page) {
27+
return page.evaluate((sel) => {
28+
return Array.from(document.querySelectorAll(sel)).map((project) => ({
29+
id: project.dataset.projectId,
30+
display: window.getComputedStyle(project).display,
31+
width: project.offsetWidth,
32+
height: project.offsetHeight,
33+
}));
34+
}, selectors.project);
35+
}
36+
37+
/**
38+
* Click the add new tab button.
39+
*/
40+
async function addTab(page) {
41+
const addButton = page.locator('button:has(svg)', { hasText: '' }).filter({
42+
has: page.locator('.h-3\\.5.w-3\\.5'),
43+
});
44+
45+
// Fallback: find the button next to the draggable container
46+
const button = page.locator('.flex.items-center.gap-0\\.5 > button').last();
47+
48+
await button.click();
49+
50+
await page.waitForTimeout(300);
51+
}
52+
53+
/**
54+
* Close a tab by its index.
55+
*/
56+
async function closeTab(page, index) {
57+
const tab = page.locator(selectors.tab).nth(index);
58+
59+
await tab.hover();
60+
61+
// The close button has the `shrink-0 rounded-full` classes.
62+
const closeButton = tab.locator('button.shrink-0');
63+
64+
await closeButton.click();
65+
66+
await page.waitForTimeout(300);
67+
}
68+
69+
test.beforeEach(async ({ page }) => {
70+
// Clear localStorage to start fresh.
71+
await page.goto('/');
72+
await page.evaluate(() => localStorage.clear());
73+
await page.reload();
74+
await page.waitForTimeout(1000);
75+
});
76+
77+
test('displays one tab by default', async ({ page }) => {
78+
const tabs = await getTabs(page);
79+
80+
expect(tabs).toHaveLength(1);
81+
expect(Number(tabs[0].opacity)).toBe(1);
82+
expect(tabs[0].width).toBeGreaterThan(0);
83+
});
84+
85+
test('displays one project by default', async ({ page }) => {
86+
const projects = await getProjects(page);
87+
88+
expect(projects).toHaveLength(1);
89+
expect(projects[0].width).toBeGreaterThan(0);
90+
expect(projects[0].height).toBeGreaterThan(0);
91+
});
92+
93+
test('adds a new tab', async ({ page }) => {
94+
await addTab(page);
95+
96+
const tabs = await getTabs(page);
97+
98+
expect(tabs).toHaveLength(2);
99+
expect(tabs[0].id).not.toBe(tabs[1].id);
100+
});
101+
102+
test('closes a tab and keeps the remaining tab visible', async ({ page }) => {
103+
await addTab(page);
104+
105+
const tabsBefore = await getTabs(page);
106+
107+
expect(tabsBefore).toHaveLength(2);
108+
109+
await closeTab(page, 0);
110+
111+
const tabsAfter = await getTabs(page);
112+
113+
expect(tabsAfter).toHaveLength(1);
114+
expect(tabsAfter[0].id).toBe(tabsBefore[1].id);
115+
expect(Number(tabsAfter[0].opacity)).toBe(1);
116+
expect(tabsAfter[0].width).toBeGreaterThan(0);
117+
});
118+
119+
test('keeps project content visible after closing a tab', async ({ page }) => {
120+
await addTab(page);
121+
122+
await closeTab(page, 0);
123+
124+
const projects = await getProjects(page);
125+
126+
expect(projects).toHaveLength(1);
127+
expect(projects[0].width).toBeGreaterThan(0);
128+
expect(projects[0].height).toBeGreaterThan(0);
129+
});
130+
131+
test('auto-creates a new tab when closing the only tab', async ({ page }) => {
132+
const tabsBefore = await getTabs(page);
133+
134+
expect(tabsBefore).toHaveLength(1);
135+
136+
await closeTab(page, 0);
137+
138+
const tabsAfter = await getTabs(page);
139+
140+
expect(tabsAfter).toHaveLength(1);
141+
expect(tabsAfter[0].id).not.toBe(tabsBefore[0].id);
142+
expect(Number(tabsAfter[0].opacity)).toBe(1);
143+
});
144+
145+
test('switches to the next tab when closing the active tab', async ({ page }) => {
146+
await addTab(page);
147+
148+
const tabs = await getTabs(page);
149+
150+
// Click the first tab to make it active.
151+
await page.locator(selectors.tab).first().click();
152+
await page.waitForTimeout(300);
153+
154+
await closeTab(page, 0);
155+
156+
const tabsAfter = await getTabs(page);
157+
158+
expect(tabsAfter).toHaveLength(1);
159+
expect(tabsAfter[0].id).toBe(tabs[1].id);
160+
});
161+
162+
test('switches between tabs', async ({ page }) => {
163+
await addTab(page);
164+
165+
const tabs = await getTabs(page);
166+
167+
// Click the first tab.
168+
await page.locator(selectors.tab).first().click();
169+
await page.waitForTimeout(300);
170+
171+
const activeProject = await page.evaluate((sel) => {
172+
const projects = document.querySelectorAll(sel);
173+
for (const p of projects) {
174+
if (window.getComputedStyle(p).display !== 'none' && p.offsetHeight > 0) {
175+
return p.dataset.projectId;
176+
}
177+
}
178+
return null;
179+
}, selectors.project);
180+
181+
expect(activeProject).toBeTruthy();
182+
183+
// Click the second tab.
184+
await page.locator(selectors.tab).nth(1).click();
185+
await page.waitForTimeout(300);
186+
187+
const activeProjectAfter = await page.evaluate((sel) => {
188+
const projects = document.querySelectorAll(sel);
189+
for (const p of projects) {
190+
if (window.getComputedStyle(p).display !== 'none' && p.offsetHeight > 0) {
191+
return p.dataset.projectId;
192+
}
193+
}
194+
return null;
195+
}, selectors.project);
196+
197+
expect(activeProjectAfter).toBeTruthy();
198+
expect(activeProjectAfter).not.toBe(activeProject);
199+
});
200+

vitest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineVitestConfig } from '@nuxt/test-utils/config';
22

33
export default defineVitestConfig({
44
test: {
5+
exclude: ['tests/e2e/**', 'node_modules/**'],
56
environment: 'nuxt',
67
environmentOptions: {
78
nuxt: {

0 commit comments

Comments
 (0)