Create E2E Test
SkillWeb & browsingCreate end-to-end tests using Playwright with page objects and fixtures following project conventions. Use when the user asks to add e2e tests for a feature or user flow.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Create E2E Test skill
What this skill tells your AI
The instructions your AI receives, as published by incubateur-ademe/benefriches in .claude/skills/create-e2e-test/SKILL.md and read by ahel’s review.
Create end-to-end tests for the user flow described below.
Instructions
1. Understand the User Flow
First, analyze the User Flow to identify:
- Entry point: Which page/route does the flow start from?
- Steps: What user actions are involved (clicks, form fills, navigation)?
- Assertions: What should be verified at each step or at the end?
- Prerequisites: Does the user need to be authenticated? Any setup data needed?
2. Explore Existing Patterns
Before writing code, explore relevant existing files:
apps/e2e-tests/
├── pages/ # Page Objects - check for reusable pages
├── fixtures/ # Base fixtures (auth.fixtures.ts)
└── tests/
└── [feature]/ # Existing test patterns
├── [feature].fixtures.ts
└── [feature].spec.ts
Key files to reference:
apps/e2e-tests/fixtures/auth.fixtures.ts- Authentication fixturesapps/e2e-tests/pages/SiteCreationPage.ts- Page object pattern exampleapps/e2e-tests/tests/site-creation/site-creation.fixtures.ts- Fixture composition example
3. Create/Update Page Objects
For each page/screen in the flow, create or update a page object in apps/e2e-tests/pages/:
Page Object Pattern:
import { expect, type Page } from "@playwright/test";
export class FeaturePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Navigation
async goto(): Promise<void> {
await this.page.goto("/route-path");
}
// Assertions (prefix with expect*)
async expectStepVisible(): Promise<void> {
await expect(this.page.getByRole("heading", { name: "Step Title" })).toBeVisible();
}
// User Actions (verb-based names)
async clickButton(): Promise<void> {
await this.page.getByRole("button", { name: "Button Text" }).click();
}
async fillInput(value: string): Promise<void> {
await this.page.getByLabel("Input Label").fill(value);
}
async selectOption(value: string): Promise<void> {
await this.page.getByRole("radio", { name: value }).check({ force: true });
await this.submit();
}
// Private helpers
private async submit(): Promise<void> {
await this.page.getByRole("button", { name: /Valider|Suivant/ }).click();
}
}
Multi-step wizards: assert each step's title as you walk through it (expectStepTitle(title) — see apps/e2e-tests/pages/PhotovoltaicProjectCreationPage.ts for the pattern). Find each title's exact text with grep -rn "WizardFormLayout title" apps/web/src rather than guessing — many step components (e.g. create-mode selection, reinstatement question) are shared across wizards, so the same title string applies in multiple flows.
Selector Priority (accessibility-first):
getByRole()- buttons, links, headings, textboxesgetByLabel()- form inputs with labelsgetByText()- visible text contentlocator()- CSS selectors (last resort)
4. Create Feature Fixtures
Create apps/e2e-tests/tests/[feature]/[feature].fixtures.ts:
import { test as authTest } from "../../fixtures/auth.fixtures";
import { FeaturePage } from "../../pages/FeaturePage";
type FeatureFixtures = {
featurePage: FeaturePage;
};
export const test = authTest.extend<FeatureFixtures>({
featurePage: async ({ authenticatedPage }, use) => {
const featurePage = new FeaturePage(authenticatedPage);
await use(featurePage);
},
});
export { expect } from "@playwright/test";
If authentication is NOT needed, extend from base Playwright test instead:
import { test as base, expect } from "@playwright/test";
5. Write Test File
Create apps/e2e-tests/tests/[feature]/[feature].spec.ts:
import { test } from "./[feature].fixtures";
test.describe("Feature Name", () => {
test("describes what the user can do", async ({ featurePage }) => {
// Navigate to starting point
await featurePage.goto();
// Step 1: Action + optional assertion
await featurePage.expectStepVisible();
await featurePage.clickStart();
// Step 2: Another action
await featurePage.fillInput("value");
await featurePage.submit();
// Final assertions
await featurePage.expectSuccessMessage();
});
});
Test Naming: Use descriptive names that explain what the user can do:
"allows authenticated user to create a new project""shows error when required field is empty""redirects to login when not authenticated"
6. Run and Verify
# Start the e2e stack (builds fresh images and waits for readiness)
make e2e-up-build
# Run specific test file
pnpm --filter e2e-tests test:headless tests/[feature]/[feature].spec.ts
# Run with browser visible (for debugging)
pnpm --filter e2e-tests test:headed tests/[feature]/[feature].spec.ts
# Type check
pnpm --filter e2e-tests typecheck
File Checklist
Before completing, ensure you have created/updated:
- Page object(s) in
apps/e2e-tests/pages/[PageName].ts - Fixtures in
apps/e2e-tests/tests/[feature]/[feature].fixtures.ts - Test file in
apps/e2e-tests/tests/[feature]/[feature].spec.ts - Tests pass:
pnpm --filter e2e-tests test:headless tests/[feature]/ - Type check passes:
pnpm --filter e2e-tests typecheck
Patterns Reference
Using Shared Types
import type { SiteNature, FricheActivity } from "shared";
import { getLabelForSiteNature } from "shared";
Waiting for Async Content
// Wait for element to appear
await this.page.getByRole("option").first().waitFor({ state: "visible", timeout: 10000 });
// Wait for navigation
await expect(this.page).toHaveURL("/expected-path");
Form Autocomplete
async fillAutocomplete(searchText: string): Promise<void> {
const input = this.page.getByRole("searchbox", { name: /Label/i });
await input.pressSequentially(searchText, { delay: 50 });
const firstOption = this.page.getByRole("option").first();
await firstOption.waitFor({ state: "visible", timeout: 10000 });
await firstOption.click();
}
Data Verification
async expectDataInList(expectedData: [label: string, value: string][]): Promise<void> {
for (const [label, value] of expectedData) {
await expect(
this.page.locator("dl").filter({ hasText: label }).locator("dt")
).toHaveText(value);
}
}
User Flow
$ARGUMENTS
Signals
- GitHub stars
- 45
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
create-e2e-test- Source
- github.com/incubateur-ademe/benefriches