End-to-end tests use Playwright. There are two modes: full browser E2E across 5 device configurations, and fast integration tests that run schema/content validation without launching a browser.
Running E2E tests
# Full suite — all 5 browser projects (builds Astro first)
npm run test:e2e
# Chromium only — fastest feedback
npm run test:chromium
# Headed mode — watch the browser
npm run test:headed
# Interactive Playwright UI explorer
npm run test:ui
# Specific test file
npx playwright test tests/e2e/smoke.spec.ts
# Specific browser project
npx playwright test --project=mobile-safari
Integration tests (no browser)
Integration tests use the same Playwright/Vitest runner but skip browser launch entirely. They validate schema structure, module imports, Storybook configuration, and story file conventions:
npx playwright test tests/integration
Integration test directories in tests/integration/:
blocks-2-1/ — Block registry and component conventions
schema-1-3/ — Sanity schema validation
storybook-1-4.test.ts — Storybook config, story files, and build verification
site-settings-2-3/ — Site settings schema
sponsor-3-1/ — Sponsor document schema
template-2-0/, variant-2-4/ — Template block wiring
Browser matrix
Five browser projects cover the primary desktop and mobile combinations:
| Project | Device | Use case |
|---|
chromium | Desktop Chrome | Primary desktop browser |
firefox | Desktop Firefox | Cross-browser coverage |
webkit | Desktop Safari | macOS/iOS engine |
mobile-chrome | Pixel 7 | Android responsive |
mobile-safari | iPhone 14 | iOS responsive |
playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
const BASE_URL = process.env.BASE_URL || 'http://localhost:4321'
export default defineConfig({
testDir: './tests',
outputDir: './test-results',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['junit', { outputFile: 'test-results/results.xml' }],
['list'],
],
timeout: 60_000,
expect: { timeout: 10_000 },
use: {
baseURL: BASE_URL,
actionTimeout: 15_000,
navigationTimeout: 30_000,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
webServer: {
command: 'sh -c "npm run build --workspace=astro-app && npm run preview --workspace=astro-app"',
url: BASE_URL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
],
})
CI behavior
forbidOnly: true — .only() in test files blocks the pipeline.
retries: 2 — Each test gets two retries before failing.
workers: 1 — Serial execution for stability (prevents port conflicts).
- Traces captured on first retry, screenshots and videos on failure.
Test file locations
tests/
├── e2e/
│ ├── smoke.spec.ts # Homepage smoke + a11y + perf baseline
│ ├── navigation.spec.ts # Header/footer links and routing
│ ├── homepage-2-2.spec.ts # Homepage block rendering
│ ├── pages-1-2.spec.ts # Dynamic page routes
│ ├── dynamic-pages.spec.ts # Sanity-driven page rendering
│ ├── sponsors.spec.ts # Sponsor directory pages
│ ├── projects.spec.ts # Project cards and listings
│ ├── events-calendar.spec.ts # Event calendar block
│ ├── seo-structured-data.spec.ts # JSON-LD and meta tags
│ ├── site-settings-2-3.spec.ts # Global site settings
│ ├── gtm-datalayer.spec.ts # GA4 / GTM data layer
│ ├── portal-auth.spec.ts # Sponsor portal auth flows
│ ├── portal-events.spec.ts # Portal event management
│ ├── portal-progress.spec.ts # Portal progress tracking
│ └── error-pages.spec.ts # 404 and error handling
├── integration/ # Schema/config validation (no browser)
└── support/
├── fixtures/
│ └── index.ts # Merged test fixtures
└── helpers/
└── a11y.ts # axe-core WCAG 2.1 AA helper
Fixtures
All test files import from the shared fixtures index instead of importing directly from @playwright/test:
import { test, expect } from '../support/fixtures'
The fixture layer provides two automatic behaviors:
- network-error-monitor — Fails the test if any HTTP 4xx/5xx responses are detected during page navigation. Opt out per-test with
{ annotation: [{ type: 'skipNetworkMonitoring' }] }.
- log — Structured logging attached to Playwright HTML reports.
Writing a new E2E test
Create the spec file
Add a .spec.ts file in tests/e2e/. Name it after the feature (e.g., contact-form.spec.ts).
Import from fixtures
import { test, expect } from '../support/fixtures'
import { expectAccessible } from '../support/helpers/a11y'
Write tests with semantic locators
Prefer getByRole, getByText, and locator with semantic selectors. Use data-testid for elements with no semantic role:test('page loads', async ({ page }) => {
await page.goto('/sponsors')
await expect(page.getByRole('heading', { name: /sponsors/i })).toBeVisible()
})
Include an accessibility assertion
Every new page or block test must call expectAccessible:test('no a11y violations', async ({ page }) => {
await page.goto('/sponsors')
await expectAccessible(page)
})
Setting BASE_URL
By default, tests run against http://localhost:4321. Override with the BASE_URL environment variable to test a deployed preview URL:
BASE_URL=https://preview.ywcccapstone1.com npm run test:e2e
Install browser binaries once after cloning: npx playwright install --with-deps
Dependencies
| Package | Purpose |
|---|
@playwright/test | Test runner and browser automation |
@axe-core/playwright | WCAG accessibility auditing |
@seontechnologies/playwright-utils | Network monitor and logging fixtures |