Skip to content

Commit 7511011

Browse files
committed
feat: add test:e2e
1 parent d1e267c commit 7511011

File tree

7 files changed

+254
-26
lines changed

7 files changed

+254
-26
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ next-env.d.ts
3232
# content
3333
content2
3434
.vercel
35+
36+
playwright-report
37+
test-results

apps/docs/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
"start": "next start",
1414
"postinstall": "fumadocs-mdx",
1515
"translate": "node ../../packages/translate/dist/index.js",
16-
"check-types": "tsc --noEmit"
16+
"check-types": "tsc --noEmit",
17+
"test:e2e": "playwright test",
18+
"test:e2e:ui": "playwright test --ui",
19+
"test:e2e:headed": "playwright test --headed",
20+
"test:e2e:debug": "playwright test --debug",
21+
"playwright:install": "playwright install"
1722
},
1823
"dependencies": {
1924
"@fumadocs/mdx-remote": "^1.3.2",
@@ -39,6 +44,7 @@
3944
"@next/bundle-analyzer": "^15.3.3",
4045
"@next/env": "^15.3.3",
4146
"@oramacloud/client": "^2.1.4",
47+
"@playwright/test": "^1.52.0",
4248
"@tailwindcss/postcss": "^4.1.8",
4349
"@types/mdast": "^4.0.4",
4450
"@types/mdx": "^2.0.13",
@@ -51,6 +57,7 @@
5157
"github-slugger": "^2.0.0",
5258
"mdast": "^3.0.0",
5359
"mdast-util-mdx-jsx": "^3.2.0",
60+
"playwright": "^1.52.0",
5461
"postcss": "^8.5.4",
5562
"remark": "^15.0.1",
5663
"remark-gfm": "^4.0.1",

apps/docs/playwright.config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './tests/e2e',
5+
fullyParallel: true,
6+
forbidOnly: !!process.env.CI,
7+
retries: process.env.CI ? 2 : 0,
8+
workers: process.env.CI ? 1 : undefined,
9+
reporter: 'html',
10+
use: {
11+
baseURL: 'http://localhost:3000',
12+
trace: 'on-first-retry',
13+
},
14+
projects: [
15+
{
16+
name: 'chromium',
17+
use: { ...devices['Desktop Chrome'] },
18+
},
19+
],
20+
webServer: {
21+
command: 'pnpm dev',
22+
url: 'http://localhost:3000',
23+
reuseExistingServer: !process.env.CI,
24+
timeout: 120 * 1000,
25+
},
26+
});

apps/docs/tests/e2e/basic.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test.describe('Basic Route Tests', () => {
4+
test('homepage loads successfully', async ({ page }) => {
5+
const response = await page.goto('/');
6+
expect(response?.status()).toBe(200);
7+
await expect(page.locator('body')).toBeVisible();
8+
});
9+
10+
test('blog page loads successfully', async ({ page }) => {
11+
const response = await page.goto('/blog');
12+
expect(response?.status()).toBe(200);
13+
await expect(page.locator('body')).toBeVisible();
14+
});
15+
16+
test('learn page loads successfully', async ({ page }) => {
17+
const response = await page.goto('/learn');
18+
expect(response?.status()).toBe(200);
19+
await expect(page.locator('body')).toBeVisible();
20+
});
21+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, test } from '@playwright/test';
2+
import { getAllStaticRoutes } from './utils/static-routes';
3+
4+
test.describe('All routes should return 200', () => {
5+
const sampleRoutes = getAllStaticRoutes();
6+
7+
// biome-ignore lint/complexity/noForEach: <explanation>
8+
sampleRoutes.forEach((route) => {
9+
test(`${route.url}`, async ({ page }) => {
10+
const response = await page.goto(route.url);
11+
expect(response?.status()).toBe(200);
12+
await expect(page.locator('body')).toBeVisible();
13+
});
14+
});
15+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { blog, docs, learn } from '../../../src/lib/source';
2+
3+
export interface StaticRoute {
4+
url: string;
5+
params: Record<string, string | string[]>;
6+
type: 'docs' | 'blog' | 'learn';
7+
}
8+
9+
/**
10+
* Extract all static routes that should be generated by generateStaticParams
11+
*/
12+
export function getAllStaticRoutes(): StaticRoute[] {
13+
const routes: StaticRoute[] = [];
14+
15+
// Get docs routes - use the actual URLs from the source
16+
try {
17+
const docsPages = docs.getPages();
18+
for (const page of docsPages) {
19+
routes.push({
20+
url: page.url,
21+
params: { slug: page.slugs },
22+
type: 'docs',
23+
});
24+
}
25+
} catch (error) {
26+
console.error('Failed to get docs pages:', error);
27+
}
28+
29+
// Get blog routes
30+
try {
31+
const blogPages = blog.getPages();
32+
for (const page of blogPages) {
33+
routes.push({
34+
url: page.url,
35+
params: { slug: page.slugs },
36+
type: 'blog',
37+
});
38+
}
39+
} catch (error) {
40+
console.error('Failed to get blog pages:', error);
41+
}
42+
43+
// Get learn routes - use the actual URLs from the source
44+
try {
45+
const learnPages = learn.getPages();
46+
for (const page of learnPages) {
47+
routes.push({
48+
url: page.url,
49+
params: { slug: page.slugs },
50+
type: 'learn',
51+
});
52+
}
53+
} catch (error) {
54+
console.error('Failed to get learn pages:', error);
55+
}
56+
57+
return routes;
58+
}
59+
60+
/**
61+
* Group routes by type for easier testing
62+
*/
63+
export function getRoutesByType() {
64+
const allRoutes = getAllStaticRoutes();
65+
66+
return {
67+
docs: allRoutes.filter((route) => route.type === 'docs'),
68+
blog: allRoutes.filter((route) => route.type === 'blog'),
69+
learn: allRoutes.filter((route) => route.type === 'learn'),
70+
all: allRoutes,
71+
};
72+
}
73+
74+
/**
75+
* Get a sample of routes for quick testing
76+
*/
77+
export function getSampleRoutes(limit = 10): StaticRoute[] {
78+
const routes = getAllStaticRoutes();
79+
80+
// Get a balanced sample from each type
81+
const sampleSize = Math.ceil(limit / 3);
82+
const routesByType = getRoutesByType();
83+
84+
return [
85+
...routesByType.docs.slice(0, sampleSize),
86+
...routesByType.blog.slice(0, sampleSize),
87+
...routesByType.learn.slice(0, sampleSize),
88+
].slice(0, limit);
89+
}
90+
91+
/**
92+
* Clean URL by removing fragments that might cause navigation issues
93+
*/
94+
export function cleanUrl(url: string): string {
95+
// Remove URL fragments (#) that can cause timeouts in tests
96+
return url.split('#')[0];
97+
}
98+
99+
/**
100+
* Check if a URL has fragments that might cause navigation issues
101+
*/
102+
export function hasUrlFragments(url: string): boolean {
103+
return url.includes('#');
104+
}
105+
106+
/**
107+
* Get routes suitable for e2e testing (clean URLs without fragments)
108+
*/
109+
export function getTestableRoutes(): StaticRoute[] {
110+
return getAllStaticRoutes().map((route) => ({
111+
...route,
112+
url: cleanUrl(route.url),
113+
}));
114+
}

0 commit comments

Comments
 (0)