Skip to content

Commit c2390a4

Browse files
add tests
1 parent 17626f3 commit c2390a4

File tree

11 files changed

+367
-61
lines changed

11 files changed

+367
-61
lines changed

examples/custom-node-server/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"dev": "RSDOCTOR=false node server.js",
99
"start": "NODE_ENV=production node server.js",
1010
"build": "rsbuild build",
11-
"typecheck": "react-router typegen && tsc"
11+
"typecheck": "react-router typegen && tsc",
12+
"test:e2e": "pnpm run dev & sleep 5 && playwright test",
13+
"test:e2e:debug": "playwright test --debug",
14+
"test:e2e:ui": "playwright test --ui"
1215
},
1316
"keywords": [],
1417
"author": "",
@@ -23,6 +26,7 @@
2326
"react-router": "^7.4.0"
2427
},
2528
"devDependencies": {
29+
"@playwright/test": "^1.50.1",
2630
"@react-router/dev": "^7.4.0",
2731
"@rsbuild/core": "^1.2.19",
2832
"@rsbuild/plugin-react": "^1.1.1",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './tests/e2e',
5+
// Maximum time one test can run for
6+
timeout: 30 * 1000,
7+
expect: {
8+
timeout: 5000
9+
},
10+
// Run tests in files in parallel
11+
fullyParallel: false,
12+
// Fail the build on CI if you accidentally left test.only in the source code
13+
forbidOnly: !!process.env.CI,
14+
// Retry on CI only
15+
retries: process.env.CI ? 2 : 0,
16+
17+
// Shared settings for all the projects below
18+
use: {
19+
// Base URL to use in actions like `await page.goto('/')`
20+
baseURL: 'http://localhost:3000',
21+
22+
// Collect trace when retrying the failed test
23+
trace: 'on-first-retry',
24+
25+
// Take screenshot on test failure
26+
screenshot: 'only-on-failure',
27+
},
28+
29+
// Configure only Chrome desktop browser
30+
projects: [
31+
{
32+
name: 'chromium',
33+
use: { ...devices['Desktop Chrome'] },
34+
},
35+
]
36+
});
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+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# End-to-End Tests
2+
3+
This directory contains end-to-end tests for the React Router default template application using Playwright.
4+
5+
## Test Structure
6+
7+
The tests are organized by feature area:
8+
9+
- `home.test.ts` - Tests for the home page and welcome component
10+
- `about.test.ts` - Tests for the about page
11+
- `docs.test.ts` - Tests for the docs section with nested routes
12+
- `projects.test.ts` - Tests for the projects section with dynamic routes
13+
- `navigation.test.ts` - General navigation flows across the application
14+
15+
## Running Tests
16+
17+
You can run the tests using the following npm scripts:
18+
19+
```bash
20+
# Run all tests
21+
npm run test:e2e
22+
23+
# Run tests with the Playwright UI
24+
npm run test:e2e:ui
25+
26+
# Run tests in debug mode
27+
npm run test:e2e:debug
28+
```
29+
30+
## Test Configuration
31+
32+
Test configuration is defined in `playwright.config.ts` in the project root. The configuration:
33+
34+
- Runs tests in the `tests/e2e` directory
35+
- Tests across multiple browsers (Chrome, Firefox, Safari)
36+
- Tests across desktop and mobile viewports
37+
- Automatically starts the development server before running tests
38+
- Takes screenshots on test failures
39+
- Generates HTML reports
40+
41+
## Adding New Tests
42+
43+
To add new tests:
44+
45+
1. Create a new file in the `tests/e2e` directory with the `.test.ts` extension
46+
2. Import the required Playwright utilities:
47+
```typescript
48+
import { test, expect } from '@playwright/test';
49+
```
50+
3. Write your tests using the Playwright API
51+
4. Run your tests with `npm run test:e2e`
52+
53+
## Generating Base Screenshots
54+
55+
If you need to generate baseline screenshots for visual comparison:
56+
57+
```bash
58+
npx playwright test --update-snapshots
59+
```
60+
61+
## CI Integration
62+
63+
These tests can be integrated into CI pipelines. The configuration includes special settings for CI environments:
64+
65+
- More retries on CI
66+
- Forbidding `.only` tests on CI
67+
- Not reusing existing servers on CI
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('About Page', () => {
4+
test('should display about page content and team members', async ({ page }) => {
5+
// Navigate to about page
6+
await page.goto('/about');
7+
8+
// Check page heading
9+
const heading = page.locator('h1:has-text("About This Demo")');
10+
await expect(heading).toBeVisible();
11+
12+
// Check team member cards
13+
const teamCards = page.locator('.card');
14+
await expect(teamCards).toHaveCount(3);
15+
16+
// Verify each team member
17+
const expectedMembers = ['React Router', 'Tailwind CSS', 'TypeScript'];
18+
for (let i = 0; i < expectedMembers.length; i++) {
19+
const memberName = expectedMembers[i];
20+
await expect(teamCards.nth(i).locator('h2')).toContainText(memberName);
21+
}
22+
23+
// Check that back to home link works
24+
const backLink = page.locator('a:has-text("← Back to Home")');
25+
await expect(backLink).toBeVisible();
26+
await backLink.click();
27+
28+
// Verify navigation back to home page
29+
await expect(page).toHaveURL(/\/$/);
30+
await expect(page.locator('h1:has-text("Welcome to React Router")')).toBeVisible();
31+
});
32+
33+
test('should have working external links', async ({ page }) => {
34+
// Navigate to about page
35+
await page.goto('/about');
36+
37+
// Get all external links
38+
const externalLinks = page.locator('.card a[target="_blank"]');
39+
40+
// Verify each link has correct attributes
41+
for (const link of await externalLinks.all()) {
42+
await expect(link).toHaveAttribute('rel', 'noopener noreferrer');
43+
await expect(link).toHaveText('Learn more →');
44+
}
45+
});
46+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Docs Section', () => {
4+
test('should navigate through docs section with nested routes', async ({ page }) => {
5+
// Navigate to docs index
6+
await page.goto('/docs');
7+
8+
// Verify the docs index page is shown
9+
await expect(page).toHaveURL('/docs');
10+
11+
// Navigate to getting-started page
12+
await page.goto('/docs/getting-started');
13+
await expect(page).toHaveURL('/docs/getting-started');
14+
15+
// Navigate to advanced page
16+
await page.goto('/docs/advanced');
17+
await expect(page).toHaveURL('/docs/advanced');
18+
19+
// Verify layouts are preserved during navigation
20+
await page.goto('/docs');
21+
22+
// Check for the main navigation menu
23+
const mainNav = page.locator('header nav');
24+
await expect(mainNav).toBeVisible();
25+
await expect(mainNav.locator('a[href="/docs"]')).toBeVisible();
26+
});
27+
28+
test('should preserve layout when navigating between nested routes', async ({ page }) => {
29+
// Start at docs index
30+
await page.goto('/docs');
31+
32+
// Click on the Documentation link in the main nav
33+
const mainNav = page.locator('header nav');
34+
const docsLink = mainNav.locator('a[href="/docs"]');
35+
await expect(docsLink).toBeVisible();
36+
await expect(docsLink).toHaveAttribute('aria-current', 'page');
37+
38+
// Navigate to getting-started
39+
await page.goto('/docs/getting-started');
40+
await expect(page).toHaveURL('/docs/getting-started');
41+
42+
// The main navigation should still be visible
43+
await expect(mainNav).toBeVisible();
44+
await expect(docsLink).toBeVisible();
45+
46+
// Navigate to advanced
47+
await page.goto('/docs/advanced');
48+
await expect(page).toHaveURL('/docs/advanced');
49+
50+
// Navigation should still be preserved
51+
await expect(mainNav).toBeVisible();
52+
await expect(docsLink).toBeVisible();
53+
});
54+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Home Page', () => {
4+
test('should display welcome message and feature cards', async ({ page }) => {
5+
// Navigate to home page
6+
await page.goto('/');
7+
8+
// Check page title
9+
await expect(page).toHaveTitle(/React Router Demo/);
10+
11+
// Check welcome message
12+
const welcomeHeading = page.locator('h1:has-text("Welcome to React Router")');
13+
await expect(welcomeHeading).toBeVisible();
14+
15+
// Check feature cards (there should be 3)
16+
const featureCards = page.locator('.card h2').filter({
17+
hasText: /Dynamic Routing|Nested Routes|Route Protection/
18+
});
19+
await expect(featureCards).toHaveCount(3);
20+
21+
// Test hover state on a feature card's parent
22+
const firstFeatureCard = featureCards.first().locator('..').first();
23+
await firstFeatureCard.hover();
24+
await expect(firstFeatureCard).toHaveClass(/card.*cursor-pointer/);
25+
26+
// Test navigation to about page
27+
const aboutPageLinks = page.locator('a[href="/about"]').filter({ hasText: 'View About Page' });
28+
await expect(aboutPageLinks.first()).toBeVisible();
29+
await aboutPageLinks.first().click();
30+
31+
// Verify navigation to about page
32+
await expect(page).toHaveURL('/about');
33+
await expect(page.locator('h1:has-text("About This Demo")')).toBeVisible();
34+
});
35+
36+
test('should have working resource links', async ({ page }) => {
37+
// Navigate to home page
38+
await page.goto('/');
39+
40+
// Check resource cards
41+
const resourceLinks = page.locator('a.card[target="_blank"]').filter({
42+
hasText: /React Router Documentation|GitHub Repository|React Router Blog/
43+
});
44+
await expect(resourceLinks).toHaveCount(3);
45+
46+
// Test that links have proper attributes
47+
for (const link of await resourceLinks.all()) {
48+
await expect(link).toHaveAttribute('target', '_blank');
49+
await expect(link).toHaveAttribute('rel', 'noreferrer');
50+
}
51+
});
52+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Navigation Flow', () => {
4+
test('should navigate through all major sections of the app', async ({ page }) => {
5+
// Start at the home page
6+
await page.goto('/');
7+
await expect(page).toHaveURL('/');
8+
9+
// Navigate to about page
10+
await page.goto('/about');
11+
await expect(page).toHaveURL('/about');
12+
13+
// Navigate to docs section
14+
await page.goto('/docs');
15+
await expect(page).toHaveURL('/docs');
16+
17+
// Navigate to projects section
18+
await page.goto('/projects');
19+
await expect(page).toHaveURL('/projects');
20+
21+
// Navigate to a specific project
22+
const projectId = 'react-router';
23+
await page.goto(`/projects/${projectId}`);
24+
await expect(page).toHaveURL(`/projects/${projectId}`);
25+
});
26+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Projects Section', () => {
4+
test('should display projects listing', async ({ page }) => {
5+
// Navigate to projects index
6+
await page.goto('/projects');
7+
8+
// Verify the projects index page URL
9+
await expect(page).toHaveURL('/projects');
10+
11+
// Verify the Projects link in nav is active
12+
const projectsLink = page.locator('a.nav-link[href="/projects"]');
13+
await expect(projectsLink).toBeVisible();
14+
await expect(projectsLink).toHaveAttribute('aria-current', 'page');
15+
});
16+
17+
test('should navigate to project detail page', async ({ page }) => {
18+
const projectId = 'react-router';
19+
20+
// Go directly to the project page
21+
await page.goto(`/projects/${projectId}`);
22+
23+
// Verify we're on the correct page
24+
await expect(page).toHaveURL(`/projects/${projectId}`);
25+
26+
// Check project name is displayed
27+
const projectName = page.locator('h1').first();
28+
await expect(projectName).toBeVisible();
29+
30+
// Check edit and settings links in the navigation
31+
const editLink = page.locator(`a[href="/projects/${projectId}/edit"]`);
32+
await expect(editLink).toBeVisible();
33+
34+
const settingsLink = page.locator(`a[href="/projects/${projectId}/settings"]`);
35+
await expect(settingsLink).toBeVisible();
36+
37+
// Check sections
38+
const sections = page.locator('.card h2').filter({
39+
hasText: /Progress|Team|Recent Activity/
40+
});
41+
await expect(sections).toHaveCount(3);
42+
});
43+
44+
test('should navigate to project edit page', async ({ page }) => {
45+
const projectId = 'react-router';
46+
47+
// Go to the project detail page
48+
await page.goto(`/projects/${projectId}`);
49+
50+
// Click the edit link
51+
const editLink = page.locator(`a[href="/projects/${projectId}/edit"]`);
52+
await editLink.click();
53+
54+
// Verify we're on the edit page
55+
await expect(page).toHaveURL(`/projects/${projectId}/edit`);
56+
});
57+
58+
test('should navigate to project settings page', async ({ page }) => {
59+
const projectId = 'react-router';
60+
61+
// Go to the project detail page
62+
await page.goto(`/projects/${projectId}`);
63+
64+
// Click the settings link
65+
const settingsLink = page.locator(`a[href="/projects/${projectId}/settings"]`);
66+
await settingsLink.click();
67+
68+
// Verify we're on the settings page
69+
await expect(page).toHaveURL(`/projects/${projectId}/settings`);
70+
});
71+
});

0 commit comments

Comments
 (0)