Skip to content

Commit a477bfd

Browse files
test(e2e): Implement playwright testing for redesign (#2288)
1 parent ffa61a5 commit a477bfd

23 files changed

Lines changed: 575 additions & 52 deletions

File tree

.github/workflows/playwright.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Playwright Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- redesign
7+
- gh-pages
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
playwright:
18+
if: github.actor != 'dependabot[bot]'
19+
name: Playwright Tests
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v6
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v6
28+
with:
29+
node-version: '24.13'
30+
cache: 'npm'
31+
32+
- name: Wait for Netlify preview
33+
id: wait-for-preview
34+
run: |
35+
# Calculate the Netlify preview URL based on the PR number
36+
PREVIEW_URL="https://deploy-preview-${{ github.event.pull_request.number }}--expressjscom-preview.netlify.app"
37+
echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_ENV"
38+
39+
MAX_RETRIES=10
40+
DELAY=20
41+
echo "Checking Netlify preview: $PREVIEW_URL"
42+
43+
for i in $(seq 1 $MAX_RETRIES); do
44+
# Check if the URL returns a 200 OK
45+
if curl -s -I "$PREVIEW_URL" | grep -q "HTTP/.* 200"; then
46+
echo "✅ Preview is live at $PREVIEW_URL"
47+
exit 0
48+
fi
49+
echo "⏳ Waiting for Netlify to deploy... ($i/$MAX_RETRIES)"
50+
sleep $DELAY
51+
done
52+
53+
echo "❌ Preview not live after $((MAX_RETRIES*DELAY)) seconds."
54+
exit 1
55+
56+
- name: Install dependencies
57+
run: npm ci
58+
59+
- name: Get Playwright version
60+
id: playwright-version
61+
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
62+
63+
- name: Cache Playwright browsers
64+
uses: actions/cache@v4
65+
with:
66+
path: ~/.cache/ms-playwright
67+
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
68+
restore-keys: |
69+
playwright-${{ runner.os }}-
70+
71+
- name: Install Playwright Browsers
72+
if: steps.playwright-cache.outputs.cache-hit != 'true'
73+
run: npx playwright install --with-deps
74+
75+
- name: Run Playwright tests
76+
run: npm run test:e2e
77+
env:
78+
PLAYWRIGHT_BASE_URL: ${{ env.PREVIEW_URL }}
79+
80+
- name: Upload Playwright test results
81+
if: always()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: playwright-report
85+
path: playwright-report/
86+
retention-days: 30

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ pnpm-debug.log*
2222

2323
# jetbrains setting folder
2424
.idea/
25+
26+
playwright-report/
27+
test-results/

CONTRIBUTING.md

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,65 @@ Tooling required:
7171

7272
### Available Scripts
7373

74-
| Command | Description |
75-
| ----------------- | ----------------------------------------- |
76-
| `npm run dev` | Start development server with hot reload |
77-
| `npm run build` | Build production site to `./dist` |
78-
| `npm run preview` | Preview production build locally |
79-
| `npm run lint` | Run ESLint to check for issues |
80-
| `npm run check` | Run type checking and format verification |
74+
| Command | Description |
75+
| ------------------ | ----------------------------------------- |
76+
| `npm run dev` | Start development server with hot reload |
77+
| `npm run build` | Build production site to `./dist` |
78+
| `npm run preview` | Preview production build locally |
79+
| `npm run lint` | Run ESLint to check for issues |
80+
| `npm run check` | Run type checking and format verification |
81+
| `npm run test:e2e` | Run Playwright E2E tests |
8182

8283
## Submitting a Pull Request
8384

84-
1. Create a new branch from `main`
85-
2. Make your changes
86-
3. Run `npm run check` to verify code style and types
87-
4. Commit with a clear message
88-
5. Push to your fork
89-
6. Open a PR against `main`
85+
1. Create a new branch from `main`.
86+
2. Make your changes.
87+
3. Run `npm run check` to verify code style and types.
88+
4. Run `npm run test:e2e` to ensure your changes don't break existing functionality.
89+
5. Commit with a clear message.
90+
6. Push to your fork.
91+
7. Open a PR against `main`.
9092

9193
> Ensure all checks pass and your branch is up to date with `main` before opening a PR.
9294
95+
## Testing
96+
97+
We use **Playwright** for End-to-End (E2E) testing. All PRs are automatically tested against a Netlify Preview deployment before they can be merged.
98+
99+
### Prerequisites
100+
101+
Before running E2E tests for the first time, you need to install the browser binaries:
102+
103+
```bash
104+
npx playwright install --with-deps
105+
```
106+
107+
### Running Tests Locally
108+
109+
You can run the full test suite against your local development server:
110+
111+
1. In one terminal, start the site: `npm run dev`
112+
2. In another terminal, run the tests: `npm run test:e2e`
113+
114+
### Writing Stable Tests
115+
116+
When adding new tests or modifying components, please follow these stability guidelines:
117+
118+
1. **Avoid CSS Classes**: Do not use CSS classes (e.g., `.hero__content`) for locators, as they are fragile and change during refactoring.
119+
2. **Use data-testid**: Add `data-testid` attributes to components for stable targeting (e.g., `<div data-testid="my-component">`).
120+
3. **User-Visible Locators**: Prefer semantic locators like `getByRole`, `getByText`, or `getByAltText` over IDs when possible.
121+
122+
Example:
123+
124+
```typescript
125+
// Good: Stable and accessible
126+
const logo = page.getByAltText('Express.js logo');
127+
const section = page.getByTestId('features-section');
128+
129+
// Bad: Fragile
130+
const logo = page.locator('.hero__logo');
131+
```
132+
93133
## Further Documentation
94134

95135
For more detailed documentation about the project, see the [`docs/`](docs/) folder:

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
@@ -23,7 +23,8 @@
2323
"check": "npm run format:check && npm run lint",
2424
"fix": "npm run format && npm run lint:fix",
2525
"prepare": "husky",
26-
"lint-staged": "lint-staged"
26+
"lint-staged": "lint-staged",
27+
"test:e2e": "playwright test"
2728
},
2829
"dependencies": {
2930
"@astrojs/mdx": "^5.0.3",
@@ -62,6 +63,7 @@
6263
"@csstools/postcss-global-data": "^4.0.0",
6364
"@eslint/js": "^9.39.2",
6465
"@iconify-json/fluent": "^1.2.41",
66+
"@playwright/test": "^1.59.1",
6567
"@types/node": "^25.3.2",
6668
"@types/react": "^19.2.14",
6769
"@types/react-dom": "^19.2.3",

playwright.config.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
const isCI = !!process.env.CI;
4+
5+
/**
6+
* See https://playwright.dev/docs/test-configuration.
7+
*/
8+
export default defineConfig({
9+
testDir: './tests/e2e',
10+
fullyParallel: true,
11+
forbidOnly: isCI,
12+
retries: isCI ? 2 : 0,
13+
workers: isCI ? 1 : undefined,
14+
reporter: isCI ? [['html'], ['github']] : [['html']],
15+
16+
use: {
17+
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4321',
18+
trace: 'on-first-retry',
19+
},
20+
projects: [
21+
{
22+
name: 'chromium',
23+
use: { ...devices['Desktop Chrome'] },
24+
},
25+
{
26+
name: 'firefox',
27+
use: { ...devices['Desktop Firefox'] },
28+
},
29+
{
30+
name: 'webkit',
31+
use: { ...devices['Desktop Safari'] },
32+
},
33+
],
34+
});

src/components/patterns/Features/Features.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface Props extends HTMLAttributes<'section'> {
2626
const { title, class: className, ...rest } = Astro.props;
2727
---
2828

29-
<section class:list={['features', className]} {...rest}>
29+
<section class:list={['features', className]} data-testid="features-section" {...rest}>
3030
<Container>
3131
<Grid>
3232
<Col xs={12} lg={4} class="features__header">

src/components/patterns/Footer/Footer.astro

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Icon } from 'astro-icon/components';
99
import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
1010
---
1111

12-
<footer class="footer">
12+
<footer class="footer" data-testid="footer">
1313
<div class="footer-top">
1414
<Container>
1515
<BodyMd color="secondary" center as="p">
@@ -98,6 +98,7 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
9898
variant="secondary"
9999
size="xs"
100100
ghost
101+
data-testid="kawaii-toggle"
101102
>
102103
Kawaii?
103104
</Button>
@@ -161,26 +162,30 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
161162
</div>
162163
</footer>
163164

164-
<script>
165-
const toggle = document.getElementById('kawaiiToggle');
166-
const isKawaii = localStorage.getItem('kawaii') === 'true';
165+
<script is:inline>
166+
const initKawaii = () => {
167+
const toggle = document.getElementById('kawaiiToggle');
168+
if (!toggle) return;
167169

168-
if (isKawaii) {
169-
document.documentElement.setAttribute('data-kawaii', '');
170-
toggle?.setAttribute('aria-pressed', 'true');
171-
}
170+
const getState = () => localStorage.getItem('kawaii') === 'true';
172171

173-
toggle?.addEventListener('click', () => {
174-
const active = document.documentElement.hasAttribute('data-kawaii');
172+
const applyState = (enabled) => {
173+
document.documentElement.toggleAttribute('data-kawaii', enabled);
174+
toggle.setAttribute('aria-pressed', String(enabled));
175175

176-
if (active) {
177-
document.documentElement.removeAttribute('data-kawaii');
178-
localStorage.removeItem('kawaii');
179-
toggle.setAttribute('aria-pressed', 'false');
180-
} else {
181-
document.documentElement.setAttribute('data-kawaii', '');
182-
localStorage.setItem('kawaii', 'true');
183-
toggle.setAttribute('aria-pressed', 'true');
184-
}
185-
});
176+
if (enabled) {
177+
localStorage.setItem('kawaii', 'true');
178+
} else {
179+
localStorage.removeItem('kawaii');
180+
}
181+
};
182+
183+
applyState(getState());
184+
185+
toggle.addEventListener('click', () => {
186+
applyState(!getState());
187+
});
188+
};
189+
190+
initKawaii();
186191
</script>

0 commit comments

Comments
 (0)