Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Playwright Tests

on:
pull_request:
branches:
- redesign
- gh-pages

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
playwright:
if: github.actor != 'dependabot[bot]'
name: Playwright Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.13'
cache: 'npm'

- name: Wait for Netlify preview
id: wait-for-preview
run: |
# Calculate the Netlify preview URL based on the PR number
PREVIEW_URL="https://deploy-preview-${{ github.event.pull_request.number }}--expressjscom-preview.netlify.app"
echo "PREVIEW_URL=$PREVIEW_URL" >> "$GITHUB_ENV"

MAX_RETRIES=10
DELAY=20
echo "Checking Netlify preview: $PREVIEW_URL"

for i in $(seq 1 $MAX_RETRIES); do
# Check if the URL returns a 200 OK
if curl -s -I "$PREVIEW_URL" | grep -q "HTTP/.* 200"; then
echo "✅ Preview is live at $PREVIEW_URL"
exit 0
fi
echo "⏳ Waiting for Netlify to deploy... ($i/$MAX_RETRIES)"
sleep $DELAY
done

echo "❌ Preview not live after $((MAX_RETRIES*DELAY)) seconds."
exit 1

- name: Install dependencies
run: npm ci

- name: Get Playwright version
id: playwright-version
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
restore-keys: |
playwright-${{ runner.os }}-

- name: Install Playwright Browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps

- name: Run Playwright tests
run: npm run test:e2e
env:
PLAYWRIGHT_BASE_URL: ${{ env.PREVIEW_URL }}

- name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ pnpm-debug.log*

# jetbrains setting folder
.idea/

playwright-report/
test-results/
66 changes: 53 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,65 @@ Tooling required:

### Available Scripts

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

## Submitting a Pull Request

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

> Ensure all checks pass and your branch is up to date with `main` before opening a PR.

## Testing

We use **Playwright** for End-to-End (E2E) testing. All PRs are automatically tested against a Netlify Preview deployment before they can be merged.

### Prerequisites

Before running E2E tests for the first time, you need to install the browser binaries:

```bash
npx playwright install --with-deps
```

### Running Tests Locally

You can run the full test suite against your local development server:

1. In one terminal, start the site: `npm run dev`
2. In another terminal, run the tests: `npm run test:e2e`

### Writing Stable Tests

When adding new tests or modifying components, please follow these stability guidelines:

1. **Avoid CSS Classes**: Do not use CSS classes (e.g., `.hero__content`) for locators, as they are fragile and change during refactoring.
2. **Use data-testid**: Add `data-testid` attributes to components for stable targeting (e.g., `<div data-testid="my-component">`).
3. **User-Visible Locators**: Prefer semantic locators like `getByRole`, `getByText`, or `getByAltText` over IDs when possible.

Example:

```typescript
// Good: Stable and accessible
const logo = page.getByAltText('Express.js logo');
const section = page.getByTestId('features-section');

// Bad: Fragile
const logo = page.locator('.hero__logo');
```

## Further Documentation

For more detailed documentation about the project, see the [`docs/`](docs/) folder:
Expand Down
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"check": "npm run format:check && npm run lint",
"fix": "npm run format && npm run lint:fix",
"prepare": "husky",
"lint-staged": "lint-staged"
"lint-staged": "lint-staged",
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/mdx": "^5.0.3",
Expand Down Expand Up @@ -62,6 +63,7 @@
"@csstools/postcss-global-data": "^4.0.0",
"@eslint/js": "^9.39.2",
"@iconify-json/fluent": "^1.2.41",
"@playwright/test": "^1.59.1",
"@types/node": "^25.3.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
34 changes: 34 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: isCI ? [['html'], ['github']] : [['html']],

use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4321',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
2 changes: 1 addition & 1 deletion src/components/patterns/Features/Features.astro
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface Props extends HTMLAttributes<'section'> {
const { title, class: className, ...rest } = Astro.props;
---

<section class:list={['features', className]} {...rest}>
<section class:list={['features', className]} data-testid="features-section" {...rest}>
<Container>
<Grid>
<Col xs={12} lg={4} class="features__header">
Expand Down
45 changes: 25 additions & 20 deletions src/components/patterns/Footer/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Icon } from 'astro-icon/components';
import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
---

<footer class="footer">
<footer class="footer" data-testid="footer">
<div class="footer-top">
<Container>
<BodyMd color="secondary" center as="p">
Expand Down Expand Up @@ -98,6 +98,7 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
variant="secondary"
size="xs"
ghost
data-testid="kawaii-toggle"
>
Kawaii?
</Button>
Expand Down Expand Up @@ -161,26 +162,30 @@ import { BodyMd, Button, Col, Container, Grid } from '@/components/primitives';
</div>
</footer>

<script>
const toggle = document.getElementById('kawaiiToggle');
const isKawaii = localStorage.getItem('kawaii') === 'true';
<script is:inline>
const initKawaii = () => {
const toggle = document.getElementById('kawaiiToggle');
if (!toggle) return;

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

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

if (active) {
document.documentElement.removeAttribute('data-kawaii');
localStorage.removeItem('kawaii');
toggle.setAttribute('aria-pressed', 'false');
} else {
document.documentElement.setAttribute('data-kawaii', '');
localStorage.setItem('kawaii', 'true');
toggle.setAttribute('aria-pressed', 'true');
}
});
if (enabled) {
localStorage.setItem('kawaii', 'true');
} else {
localStorage.removeItem('kawaii');
}
};

applyState(getState());

toggle.addEventListener('click', () => {
applyState(!getState());
});
};

initKawaii();
</script>
Loading
Loading