Repository navigation
Frontend testing
Testing is a core part of our development workflow. This guide outlines how we test our application, the tools we use, and the structure we follow for different test types.
We rely on the following testing tools to cover unit, integration, and end-to-end needs:
-
vitest: Test runner with Vite integration -
vitest-browser-react: Enables browser-based testing with Vitest for React -
@testing-library/react: Utilities for testing React components by simulating user interactions -
@testing-library/user-event: Simulates complex user events -
@vitest/browser: Browser environment support for Vitest -
@vitest/coverage-v8: Coverage reporting powered by V8 -
playwright: End-to-end testing framework -
jsdom: Lightweight DOM implementation for testing in non-browser environments -
@axe-core/playwright: Integrates the Axe accessibility engine with Playwright, enabling automated a11y audits directly in browser contexts. -
axe-playwright: Provides a wrapper around Axe and Playwright to simplify accessibility checks and reporting. It supports detailed HTML reports and WCAG compliance targeting. -
@playwright/test: Playwright’s official test runner, used to define and execute accessibility tests. It enables browser automation and integrates with Axe for a11y validation. -
wiremock: Local HTTP stubbing server used by E2E and local integration workflows. -
dotenv-cli: Loads.envvariables for scripts such as WireMock startup.
All dependencies can be installed with the following command:
npm install --save-dev vitest vitest-browser-react @testing-library/react @testing-library/user-event @vitest/browser @vitest/coverage-v8 playwright jsdom @playwright/test axe-playwright @axe-core/playwrightWe have a couple of test categories, and each one relies in a particular combination of dependencies. Below is a description of each one.
Unit tests validate the correctness of individual components or utility functions in isolation. It relies on vitest and react testing library for most of the test and assertion.
-
File Naming Convention:
Test files follow the pattern:<component>.unit.test.<ext>
Example:index.unit.test.tsx -
Location:
Unit tests reside next to their corresponding source files within the same folder. This co-location helps maintain clarity and improves discoverability during development.
We use Vitest’s configuration capabilities within vite.config.ts to orchestrate our testing environment. The configuration supports different test types, enforces coverage standards, and wires up browser support via Playwright.
test: {
env: loadEnv(mode, process.cwd(), ''),
globals: true,
clearMocks: true,
exclude: [...configDefaults.exclude, 'dist/**', 'build/**'],
...
}- env: Loads environment variables based on the current mode.
- globals: Enables global variables in test files, simplifying setup.
- clearMocks: Automatically clears Vitest mocks between tests, reducing cross-test leakage.
- exclude: Prevents irrelevant files and folders (e.g., build output) from being included in test scans.
coverage: {
provider: 'v8',
reporter: ['lcov', 'cobertura', 'html', 'json', 'text'],
reportsDirectory: './coverage',
all: true,
exclude: [...],
include: ['src/**/*.ts', 'src/**/*.tsx'],
lines: 80,
functions: 80,
branches: 80,
statements: 80,
}- Uses V8 for instrumentation.
- Reports are generated in multiple formats (
lcov,html, etc.). - Enforces 80% coverage across all dimensions.
- Filters out non-source artifacts like style sheets and type definitions.
We segment test execution into projects, and currently we have just Node Unit Tests. This enables parallel strategies for isolated logic and full component rendering, and allow us to segment things in the future if we need to.
projects: [
{
name: 'node',
extends: true,
test: {
setupFiles: [
'./src/config/tests/setup-env.ts',
'./src/config/tests/custom-matchers.ts'
],
environment: 'jsdom',
include: ['src/**/*.unit.test.{ts,tsx}'],
},
},-
Environment: Uses
jsdomfor lightweight DOM simulation. - Setup Files: Initializes the test environment and custom matchers.
-
Inclusion Filter: Targets
.unit.test.tsx/tsfiles for execution.
To ensure consistency and reduce boilerplate across our test suites, each test "project" is configured with dedicated setupFiles. These files customize the runtime environment and extend the assertion capabilities before tests are executed.
The following setup files establish the base test conditions:
-
setup-env.ts:
Used in unit tests (happy-dom) to simulate browser behaviors not natively present.
Key responsibilities include:- Cleaning up the DOM after each test to prevent side effects
- Mocking
matchMedia,ResizeObserver, and severalSVGElementAPIs - Neutralizing
scrollIntoViewto prevent unhandled behavior
These setup files help normalize the environment across various browsers, simulators, and edge cases to make tests reliable and predictable.
-
custom-matchers.ts:
Globally extends Vitest’sexpectobject with reusable matchers. Here's the perfect place to add custom matchers and reduce boilerplate and repetition when testing. One example is:toContainText(received: HTMLElement, expected: string)
This matcher simplifies assertions like:
expect(element).toContainText('Submit')
Instead of checking
textContentmanually, it offers a clear, expressive syntax—improving readability and reducing test friction.
Accessibility testing ensures that web applications are usable by people with disabilities, including those who rely on assistive technologies like screen readers, keyboard navigation, and voice control.
-
File Naming Convention:
Test files follow the pattern:<component>.a11y.test.<ext>
Example:index.a11y.test.tsx -
Location:
Acessibility tests reside next to their corresponding source files within the same folder. This co-location helps maintain clarity and improves discoverability during development.
- Legal compliance: Many regions require digital accessibility by law (e.g., WCAG, ADA).
- Inclusive design: Improves usability for all users, not just those with disabilities.
- SEO & performance: Accessible sites often have better structure and semantics, which benefits search engines and load times.
- Semantic HTML structure
- ARIA roles and attributes
- Color contrast and visual cues
- Keyboard navigation and focus management
- Compatibility with screen readers
The tests in this PR specifically target WCAG 2.0 A and AA standards, using axe-core via the axe-playwright integration.
Some of the key configuration files required for the a11y tests are as follows:
| File | Purpose |
|---|---|
playwright.config.ts |
Defines test projects, match patterns, and reporters |
tsconfig.app.json |
Ensures TypeScript compatibility for test files |
This particular test relies on playwright, as it requires the application to be up and running in order to validate the final resulf of the code. Accessibility tags are usualy declared in different manners by the used components and can end up generating other tags and properties that can only be validated when acessing the final code content.
Inside playwright.config.ts, the following configurations define the a11y test behavior:
const a11yProject = {
name: 'a11y-chromium',
use: {
...commonSettings,
device: devices['Desktop Chrome'],
storageState: './src/config/tests/user.chromium.json',
},
testMatch: '**/*.a11y.test.{ts,tsx}',
}globalSetup: './src/config/tests/browser.setup.ts',
globalTeardown: './src/config/tests/browser.teardown.ts',These ensure the browser is prepped with Axe injected and local storage configured before tests run.
reporter: [
['list'],
['html', { outputFolder: 'test-results/report', open: 'never' }],
['junit', { outputFile: 'test-results/junit/report.xml' }],
]This enables human-readable HTML reports and CI-compatible JUnit XML output.
While most tests usualy generates coverage reports and such, a a11y test usually generate reports, that are used to improve acessibility support to the application. If a particular a11y rule is not being followed, it will generate an error during test execution, along with a report pointing out to the problem, but contrary to other tests, this will not end up with a line coverage number that tells you how close you are from achieving a fully accessible application.
While traditional coverage tools aren't used for a11y, confidence comes from:
-
Explicit test files: e.g.,
index.a11y.test.tsxtargets specific pages/components. -
Detailed Axe configuration:
axeOptions: { runOnly: ['wcag2a', 'wcag2aa'] }
-
Verbose output:
detailedReport: true, detailedReportOptions: { html: true }, verbose: true
-
HTML Report: Located at
test-results/report/index.html -
JUnit XML: For CI integration, found at
test-results/junit/report.xml -
GitHub Actions Artifacts: Uploaded via
upload-artifactsteps in.github/workflows/reusable-tests-fe-extra.yml
browser.setup.ts
- Launches browser and injects Axe
- Navigates to base URL and sets local storage
- Saves storage state to
user.chromium.json,user.firefox.json, etc. - Used for authentication and client pre-selection
a11y.helper.ts
- Wraps
checkA11yfromaxe-playwrightwith custom options - Provides reusable
runA11yAudit(page, selector?)function
auth.setup.ts
- Previously handled login flow and storage state
- Now merged into broader browser setup
browser.teardown.ts
- Placeholder for cleanup logic post-tests (currently empty)
Run all unit tests with:
npm run testGenerate coverage reports via:
npm run test:coverageFor full coverage on the CI platform:
npm run test:ci- Unit tests focus on logic and isolated behavior.
- Keeping tests co-located with components simplifies maintenance and encourages writing meaningful tests while developing.
End-to-end (E2E) tests validate complete user workflows in a real browser environment using Playwright. Unlike unit tests that focus on isolated components, E2E tests ensure the entire application works correctly from a user's perspective.
-
File Naming Convention:
Test files follow the pattern:<component>.e2e.test.<ext>
Example:index.e2e.test.tsx -
Location:
E2E tests reside next to their corresponding page/component files within the same folder. This co-location helps maintain clarity and ensures tests stay synchronized with the features they validate.
Our Playwright configuration supports multiple browsers, user types, and devices. Tests run against different authentication states (BCeID and IDIR users) to ensure proper access control and functionality across user roles.
const baseURL = process.env.BASE_URL ?? 'http://localhost:3000';
const commonSettings = {
headless: true,
baseURL,
viewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true,
video: { mode: 'retain-on-failure' as VideoMode },
contextOptions: {
recordVideo: {
dir: './test-results/videos',
},
},
};- baseURL: The application URL tests run against
- headless: Runs browsers without UI by default (can be changed for debugging)
- viewport: Default screen size for consistent test rendering
- video: Records videos only when tests fail, useful for debugging
Tests are organized into projects based on:
- User type (BCeID or IDIR)
- Browser (Chromium, Firefox, WebKit)
- Device category (Desktop, Mobile, Tablet)
Example project configuration:
{
name: 'bceid-chromium',
use: {
...commonSettings,
device: devices['Desktop Chrome'],
storageState: 'src/config/tests/user.bceid.json',
},
testMatch: '**/*.e2e.test.{ts,tsx}',
metadata: {
userType: 'bceid',
category: 'desktop',
browserName: 'chromium',
stateFile: 'user.bceid.json',
userPoolsClientId,
},
dependencies: ['bceid-setup'],
}E2E tests require authenticated sessions to access protected routes. Our authentication system uses Playwright's storage state to persist login sessions across tests, avoiding redundant logins and significantly improving test performance.
-
Setup Projects: Special projects (
bceid-setupandidir-setup) run before any E2E tests - Login Flow: Setup projects navigate to the login page, enter credentials, and complete authentication
-
Storage State: After successful login, the browser's local storage, session storage, and cookies are saved to JSON files (
user.bceid.json,user.idir.json) - State Reuse: Test projects load the saved storage state, so each test starts already authenticated
- Dependency Chain: E2E test projects depend on their respective setup projects, ensuring authentication completes first
Setup Projects (from playwright.config.ts):
const setupProjects = [
{
name: 'bceid-setup',
use: {
...commonSettings,
device: devices['Desktop Chrome'],
storageState: 'src/config/tests/user.bceid.json',
},
testMatch: '**/auth.setup.ts',
metadata: {
userType: 'bceid',
stateFile: 'user.bceid.json',
shouldMockAuthentication,
userPoolsClientId,
user: process.env.BCEID_USERNAME ?? '',
password: process.env.BCEID_PASSWORD ?? '',
},
},
{
name: 'idir-setup',
use: {
...commonSettings,
device: devices['Desktop Chrome'],
storageState: 'src/config/tests/user.idir.json',
},
testMatch: '**/auth.setup.ts',
metadata: {
userType: 'idir',
stateFile: 'user.idir.json',
shouldMockAuthentication,
userPoolsClientId,
user: process.env.IDIR_USERNAME ?? '',
password: process.env.IDIR_PASSWORD ?? '',
},
},
];Authentication Helper (auth.helper.ts):
The authenticate function handles the complete login flow:
export async function authenticate(page: Page, metadata: Record<string, any>): Promise<void> {
console.log(`Setup - Auth: ${metadata.user} via ${metadata.userType.toLowerCase()}`);
await page.goto('/landing');
// Check if already authenticated
const notFoundVisible = await page
.getByRole('heading', { name: 'Content Not Found' })
.isVisible({ timeout: 2000 });
if (notFoundVisible) {
console.log('Setup - Already authenticated, skipping login');
return;
}
// Click login button for user type
await Promise.all([
page.waitForNavigation({ waitUntil: 'load' }),
page.click(`[data-testid="landing-button__${metadata.userType.toLowerCase()}"]`),
]);
// Fill credentials
await page.fill('#user', metadata.user);
await page.fill('#password', metadata.password);
await page.click('input[name="btnSubmit"]');
// Wait for authentication
await page.waitForLoadState('networkidle');
// Save storage state
const authFile = path.resolve(process.cwd(), 'src/config/tests', metadata.stateFile);
await page.context().storageState({ path: authFile });
}Authentication Setup File (auth.setup.ts):
This file runs before E2E tests and prepares the test environment:
import { test as setup } from '@playwright/test';
setup.beforeEach(async ({ page }) => {
// Mock API responses needed for initial page load
await mockApiResponsesWithStub(page, 'users/preferences', 'users/preferences-GET-bceid.json');
await mockApiResponsesWithStub(
page,
'forest-clients/searchByNumbers**',
'forest-clients/searchByNumbers-pg0.json',
);
// ... more API mocks
await page.waitForLoadState('networkidle');
});
setup('authenticate', async ({ page }, testInfo) => {
const authFunction = testInfo.project.metadata.shouldMockAuthentication
? mockAuthenticate
: authenticate;
await authFunction(page, testInfo.project.metadata);
});Credentials are stored in environment variables for security:
# .env file (never commit this!)
BCEID_USERNAME=your_bceid_username
BCEID_PASSWORD=your_bceid_password
IDIR_USERNAME=your_idir_username
IDIR_PASSWORD=your_idir_password
VITE_MOCK_AUTH=true
VITE_USER_POOLS_WEB_CLIENT_ID=<cognito_app_client_id>Generated JSON files contain the authenticated session:
{
"cookies": [...],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{ "name": "auth_token", "value": "..." },
{ "name": "user_preferences", "value": "..." }
]
}
]
}These files are:
- Generated during setup projects
-
Reused by test projects via the
storageStateproperty - Gitignored to prevent committing sensitive data
- Ephemeral and recreated before each test run
E2E tests mock backend API responses to ensure consistent, fast, and reliable tests. We use helper functions that intercept network requests and return predefined responses.
Basic API Mocking:
export const mockApi = async (
page: Page,
routePath: string,
route: (route: any) => Promise<void>,
): Promise<void> => {
await page.route(`**/api/${routePath}`, route);
};Inline Response Mocking:
export const mockApiResponses = async (
page: Page,
routePath: string,
status: number = 200,
contentType: string = 'application/json',
content: Record<string, any> = {},
): Promise<void> => {
await mockApi(page, routePath, async (route) => {
await route.fulfill({
status,
contentType,
body: JSON.stringify(content),
});
});
};Stub File Response Mocking (Recommended):
export const mockApiResponsesWithStub = async (
page: Page,
routePath: string,
stubFileName: string,
status: number = 200,
contentType: string = 'application/json',
): Promise<void> => {
const stubPath = path.resolve(process.cwd(), 'stub/__files', stubFileName);
await mockApi(page, routePath, async (route) => {
await route.fulfill({
status,
contentType,
path: stubPath,
});
});
};Stub files are stored in the stub/__files directory, organized by API domain:
stub/
└── __files/
├── codes/
│ ├── districts.json
│ ├── samplings.json
│ └── assess-area-statuses.json
├── forest-clients/
│ ├── clients-pg0.json
│ ├── clients-OAK-pg0.json
│ └── searchByNumbers-pg0.json
├── search/
│ ├── reporting-units-pg0.json
│ ├── reporting-units-pg1.json
│ └── reporting-units-with-district.json
└── users/
├── preferences-GET.json
├── preferences-GET-bceid.json
└── preferences-GET-idir.json
test.beforeEach(async ({ page }) => {
// Mock with stub file (preferred)
await mockApiResponsesWithStub(
page,
'forest-clients/clients?page=0&size=10&value=',
'forest-clients/clients-pg0.json',
);
// Mock with inline content
await mockApiResponses(
page,
'search/reporting-units?mainSearchTerm=NONEXISTENT&size=10',
200,
'application/json',
{
content: [],
page: { number: 0, size: 10, totalElements: 0, totalPages: 0 },
},
);
// Mock error responses
await mockApiResponses(
page,
'search/reporting-units?mainSearchTerm=error&size=10',
500,
'application/problem+json',
{
detail: 'Internal server error',
status: 500,
title: 'Internal Server Error',
},
);
// Mock connection failures
await mockApi(page, 'search/reporting-units?mainSearchTerm=timeout&size=10', (route) =>
route.abort('connectionrefused'),
);
await page.goto('/search');
await page.waitForLoadState('networkidle');
});When adding new features or tests, you'll need to capture real API responses to create stub files.
-
Start the application with a real backend connection (no mocks)
-
Open browser DevTools:
- Navigate to the Network tab
- Enable Preserve log to keep requests between page navigations
- Filter by XHR/Fetch to show only API calls
[Screenshot placeholder: DevTools Network tab with filters enabled]
-
Perform the action in the application that triggers the API call
-
Locate the request in the Network tab
[Screenshot placeholder: API request selected in Network tab]
-
Copy the response:
- Click on the request
- Go to the Response tab
- Right-click and select Copy response
[Screenshot placeholder: Copy response context menu]
- Create a stub file:
- Create a new JSON file in
stub/__files/<domain>/ - Paste the response content
- Format the JSON for readability
# Example: Create a new stub file
mkdir -p stub/__files/codes
cat > stub/__files/codes/new-endpoint.json << 'EOF'
{
"data": [
{ "id": 1, "name": "Example" }
],
"metadata": {
"total": 1
}
}
EOFThis project supports two complementary stubbing approaches:
-
Playwright route interception via
mockApiResponsesWithStub(fast per-test control). -
WireMock server mappings via
stub/mappings/*.jsonandstub/__files/**(shared HTTP stubs across environments).
npm run stubUse the login-specific profile when needed:
npm run stub:login- Response bodies:
stub/__files/<domain>/<file>.json - WireMock mappings:
stub/mappings/<domain>.json - Playwright helper default path:
stub/__files
When VITE_MOCK_AUTH=true, tests can replace the active Cognito token with custom claims:
import { test, expect } from '@playwright/test';
import { mockJwt } from '@/config/tests/auth.helper';
test('overrides current mocked user token', async ({ page }, testInfo) => {
await mockJwt(page, testInfo.project.metadata, {
'custom:idp_username': 'notame',
});
await page.goto('/search');
await page.getByRole('button', { name: 'Profile settings' }).click();
await expect(page.getByText('BCEIDBUSINESS\\notame')).toBeVisible();
});Important: ensure userPoolsClientId is present in Playwright project metadata (or env fallback), otherwise the cookie key will not match Cognito key names.
- Use the stub in your test:
await mockApiResponsesWithStub(
page,
'codes/new-endpoint',
'codes/new-endpoint.json',
);- Remove sensitive data: Sanitize any PII, tokens, or sensitive information
- Use realistic data: Ensure the data represents typical production scenarios
- Keep it minimal: Only include necessary fields to reduce file size
- Document variations: Create separate stubs for different states (empty, full, error)
npm run test:e2eThis command:
- Installs Playwright browsers (if needed)
- Runs setup projects to authenticate
- Executes all
.e2e.test.tsfiles across all configured projects - Generates HTML and JUnit reports
npm run test:e2e:watchThis opens Playwright's UI mode where you can:
- See tests in a visual interface
- Watch browsers execute tests in real-time
- Debug interactively
- Re-run individual tests
[Screenshot placeholder: Playwright UI mode interface]
# Run only BCeID user tests
npx playwright test --project=bceid-chromium
# Run only IDIR user tests
npx playwright test --project=idir-chromium
# Run BCeID tests across all browsers
npx playwright test --project=bceid-*# Run specific test file
npx playwright test src/pages/WasteSearch/index.e2e.test.tsx
# Run with specific project
npx playwright test src/pages/WasteSearch/index.e2e.test.tsx --project=bceid-chromium# Run test by name (grep)
npx playwright test --grep "should display search results in table"
# Run test by line number
npx playwright test src/pages/WasteSearch/index.e2e.test.tsx:194# Run with inspector
npx playwright test --debug
# Run specific test with inspector
npx playwright test src/pages/WasteSearch/index.e2e.test.tsx --debugThe inspector allows you to:
- Step through test actions
- Pause execution
- Inspect page state
- View locators
- Take screenshots
[Screenshot placeholder: Playwright Inspector with step-through controls]
Add await page.pause() in your test to pause execution:
test('debugging example', async ({ page }) => {
await page.goto('/search');
await page.pause(); // Test pauses here
// Continue debugging in DevTools
});test('logging example', async ({ page }) => {
// Log page URL
console.log('Current URL:', page.url());
// Log element text
const heading = await page.locator('h1').textContent();
console.log('Heading:', heading);
// Log network requests
page.on('request', (request) => console.log('>>>', request.method(), request.url()));
page.on('response', (response) => console.log('<<<', response.status(), response.url()));
});When tests fail, videos are automatically saved to test-results/videos/:
# Open the latest test video
open test-results/videos/*.webmPlaywright provides multiple ways to locate elements on the page. Understanding which selectors exist is crucial for writing reliable tests.
-
Run test with inspector:
npx playwright test --debug -
Click the "Pick Locator" button (target icon)
- Hover over elements in the browser to see recommended selectors
- Copy the selector and use it in your test
You can test selectors directly in the browser console:
// Test if selector exists
document.querySelector('[data-testid="search-button"]')
// Test Playwright locator syntax
await page.locator('button:has-text("Search")').count()// By role (preferred for accessibility)
await page.getByRole('button', { name: 'Search' })
await page.getByRole('heading', { name: 'Waste search', level: 1 })
// By test ID
await page.getByTestId('search-button')
// By label (for form inputs)
await page.getByLabel('District')
// By text content
await page.getByText('Nothing to show yet!')
// By placeholder
await page.getByPlaceholder('Search...')
// CSS selector
await page.locator('.search-input')
await page.locator('#main-search')
// XPath (use sparingly)
await page.locator('xpath=//button[contains(text(), "Search")]')// Check if element exists
const exists = await page.locator('[data-testid="search-button"]').count() > 0;
// Check visibility
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();
// Wait for element to appear
await page.getByRole('button', { name: 'Search' }).waitFor({ state: 'visible' });Sometimes you need to verify that elements are NOT present on the page (e.g., after deletion, conditional rendering).
// Element should not be visible
await expect(page.getByText('Error message')).not.toBeVisible();
// Element should not exist in DOM
await expect(page.locator('[data-testid="deleted-item"]')).not.toBeAttached();
// Check element count is zero
await expect(page.locator('.error-message')).toHaveCount(0);test('filter removes unmatched results', async ({ page }) => {
// Initially visible
await expect(page.getByRole('cell', { name: 'CANADIAN SAMPLE CO.' })).toBeVisible();
// Apply filter
const searchBox = page.getByTestId('main-search');
await searchBox.fill('OAK');
await searchBox.press('Enter');
// Previously visible element should now be hidden
await expect(page.getByRole('cell', { name: 'CANADIAN SAMPLE CO.' })).not.toBeVisible();
// New filtered element should be visible
await expect(page.getByRole('cell', { name: 'OAK HERITAGE LTD.' })).toBeVisible();
});test('user type determines visible options', async ({ page }, testInfo) => {
// Skip test if wrong user type
test.skip(testInfo.project.metadata.userType === 'idir', 'Only runs for BCeID users');
// Verify BCeID-specific element exists
await expect(page.getByText('Select client')).toBeVisible();
// Verify IDIR-specific element does NOT exist
await expect(page.getByText('Select organization')).not.toBeVisible();
});test('should display page title', async ({ page }) => {
await page.goto('/search');
const title = page.getByRole('heading', { name: 'Waste search', level: 1 });
await expect(title).toBeVisible();
});test('should allow searching', async ({ page }) => {
const searchBox = page.getByTestId('main-search');
await searchBox.fill('12345');
await searchBox.press('Enter');
await page.waitForLoadState('networkidle');
await expect(page.getByRole('link', { name: '12345' })).toBeVisible();
});test('should navigate to client details', async ({ page, context }) => {
const clientLink = page.getByRole('link', { name: '90000001' }).first();
const [newPage] = await Promise.all([
context.waitForEvent('page'),
await clientLink.click(),
]);
await newPage.waitForLoadState();
await expect(newPage.getByRole('heading', { name: 'Client Details' })).toBeVisible();
});test('should submit form and show results', async ({ page }) => {
await page.getByLabel('District').selectOption('DSQ');
await page.getByLabel('Status').selectOption('Active');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByText('Results found')).toBeVisible();
});E2E tests can collect JavaScript coverage to measure how much of your code is executed during end-to-end testing. This complements unit test coverage.
The coverage setup extends Playwright's test fixture to capture V8 coverage:
export const test = base.extend<{
context: BrowserContext;
page: Page;
}>({
page: async ({ context }, provide, testInfo) => {
const isChromium = testInfo.project.metadata.browserName === 'chromium';
const page = await context.newPage();
// Start coverage collection (Chromium only)
if (isChromium) {
await page.coverage.startJSCoverage({ reportAnonymousScripts: true });
}
await provide(page);
// Stop and save coverage
if (isChromium) {
const jsCoverage = await page.coverage.stopJSCoverage();
// Convert to Istanbul format and save
// ...
}
},
});# Run all tests with coverage
npm run test:coverage
# This runs:
# 1. Unit tests with coverage
# 2. E2E tests with coverage
# 3. Merges coverage reportsCoverage reports are generated in the coverage/ directory and include both unit and E2E test coverage.
- Co-locate tests: Keep E2E tests next to the pages/components they test
-
Use semantic selectors: Prefer
getByRole,getByLabel,getByTestIdover CSS selectors - Mock API responses: Use stub files for consistent, fast tests
-
Wait for network: Use
await page.waitForLoadState('networkidle')after navigation - Test user workflows: Focus on complete user journeys, not implementation details
-
Skip when appropriate: Use
test.skip()to exclude tests from certain user types or browsers - Organize with describe blocks: Group related tests together
-
Clean up state: Use
beforeEachto set up a clean test environment -
Avoid hard-coded waits: Use
waitForconditions instead ofpage.waitForTimeout() - Test error states: Include tests for API failures, validation errors, and edge cases
- Check environment variables are set correctly
- Ensure you're running the same Node/npm version as CI
- Verify
playwright installhas been run
- Confirm credentials in
.envfile are correct - Check that setup projects run before test projects
- Verify storage state files are being generated
- Add explicit waits:
await page.waitForLoadState('networkidle') - Use
waitForinstead of fixed timeouts - Check for race conditions in async operations
- Enable video recording to debug:
video: { mode: 'on' }
- Use Playwright Inspector to verify the correct selector
- Check if element is in an iframe:
await page.frameLocator('iframe').locator(...) - Ensure the element is visible:
await element.waitFor({ state: 'visible' }) - Check if API mock is returning expected data
The current runner wiring (verified against playwright.config.ts and package.json):
| Test pattern | Runner wiring |
|---|---|
*.e2e.test.{ts,tsx} |
Playwright bceid-chromium and idir-chromium projects — storageState src/config/tests/user.bceid.json / user.idir.json, metadata.userType (bceid / idir), and setup-project dependencies (bceid-setup / idir-setup). testDir is ./src; the webServer command is npm run serve:e2e:test (builds with VITE_MOCK_AUTH=true, then serves via vite preview). |
*.a11y.test.{ts,tsx} |
Playwright a11y-chromium project, appended to the project list only when RUN_A11Y_TESTS=true. CI runs npm run test:a11y. |
Auth convention for page tests that render inside the authenticated layout:
-
setupAppShellMocks(page, testInfo.project.metadata.userType)from@/config/tests/app.setup— registers the API stubs required by the shell (preferences, clients, codes). -
mockJwt(page, testInfo.project.metadata)from@/config/tests/auth.helper— overrides the mocked Cognito token whenVITE_MOCK_AUTH=true.
Test helpers:
| Helper | Source | Purpose |
|---|---|---|
mockApiResponses, mockApiResponsesWithStub
|
@/config/tests/e2e.helper |
Inline and stub-file API mocking. |
runA11yAudit |
@/config/tests/a11y.helper |
Wraps checkA11y with WCAG 2.0 A/AA options and detailed HTML reporting. |
injectAxe, checkA11y
|
axe-playwright |
Axe injection and accessibility checks in page tests. |
Three idioms cover tests whose route sits behind a role guard or a feature flag that the workflow environment keeps off (established with issue #1369, 2026-10-05):
-
E2E BCeID skip — role-gated scenarios skip the
bceid-chromiumproject inside the test body:test.skip( test.info().project.metadata.userType === 'bceid', 'This scenario is validated on IDIR to avoid BCeID role-rule redirects.', );
ADMIN is the IDIR group (
WASTE_PLUS_ADMIN); BCeID mock users carry only client-scoped roles. Precedent:GlobalError/index.e2e.test.tsx,ReportingUnitBlockDetails/index.e2e.test.tsx. -
A11Y claim override — the
a11y-chromiumproject authenticates as BCeID, so grant the role instead of skipping, viamockJwt's third argument:await mockJwt(page, testInfo.project.metadata, { 'cognito:groups': ['WASTE_PLUS_ADMIN'] });
Precedent:
ReportingUnitBlockDetails/index.a11y.test.tsx. -
Runtime flag injection through
params.js— when the workflow keeps a flag off (for example the a11y workflow'sVITE_FEATURE_FLAGSholds onlyoffline-mode-enabled: false), set it before navigation:await page.route('**/data/params.js', (route) => route.fulfill({ status: 200, contentType: 'application/javascript', body: `window.config = { VITE_FEATURE_FLAGS: '{"reporting-unit-block-details-enabled":true}' };`, }), ); await page.goto(ROUTE_PATH);
params.jsloads withdeferbefore the main bundle, sowindow.configexists beforeenv.tsevaluatesfeatureFlags. Precedents:GlobalError/index.e2e.test.tsx,ReportingUnitBlockDetails/index.a11y.test.tsx.
Known environmental limitation (decision D7): direct-URL mock-auth E2E fails at clean HEAD in local sandboxes — the app renders the landing/login page instead of the authenticated route. CI with real secrets passes, so E2E and a11y coverage is validated via CI.
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository


