Create E2E Test

SkillWeb & browsing

Create 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.

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 fixtures
  • apps/e2e-tests/pages/SiteCreationPage.ts - Page object pattern example
  • apps/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):

  1. getByRole() - buttons, links, headings, textboxes
  2. getByLabel() - form inputs with labels
  3. getByText() - visible text content
  4. locator() - 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
Create E2E Test (create-e2e-test): Skill · ahel