--- name: 'create-run-e2e-tests' description: 'Guide for writing and running new Playwright end-to-end tests in the `apps/e2e-tests/` directory of the Packmind monorepo. Use this skill whenever you add or modify a spec that drives the real frontend and API — for example testing a user flow, a new page/route, a feature behind a flag, or a UI behavior end-to-end. Triggers on "write an e2e test", "add a Playwright test", "test this flow end-to-end", "cover this page with an e2e", "e2e for the frontend", or any work that lands a `*.spec.ts` under apps/e2e-tests/src/. Prefer this over hand-rolling raw Playwright `test()` calls — the codebase has mandatory fixtures and a Page Object Model you must follow.' --- # Authoring Packmind E2E Tests ## Overview `apps/e2e-tests/` runs Playwright against the **real** frontend (`http://localhost:4200`) and API. Tests drive the browser through a **Page Object Model** and seed irrelevant setup data through the **API**, not the UI. The dev stack must be running first (see `michel-run-local-dev-stack`). Two rules dominate everything here, and both come from the project's `.packmind` standards: 1. **Never use Playwright's raw `test`.** Always use one of the project fixtures. They handle user creation, sign-up, and API-key setup so each test starts from a clean, authenticated state. 2. **Drive the UI through Page Objects, never raw selectors in specs.** A spec should read like a user story; selectors live inside page objects so a markup change breaks one file, not twenty tests. ## Where files go | File type | Location | Naming | |-----------|----------|--------| | Spec | `src/features//` | `.spec.ts` — one spec per feature | | Page object interface | `src/domain/pages/index.ts` | `IXxxPage` | | Page object impl | `src/infra/pages/` | `XxxPage.ts` | | API gateway type | `src/domain/api/IPackmindApi.ts` | `Gateway` | | API gateway impl | `src/infra/api/PackmindApi.ts` | — | | API data factory | `src/domain/apiDataFactories/` | `apiXxxFactory.ts` | This mirrors the hexagonal split used across the repo: `domain/` holds interfaces, `infra/` holds implementations. ## Choosing a fixture The three fixtures form a chain — each extends the previous and adds one capability. Pick the **lowest** one that gives you what the test needs, so you don't pay for setup you won't use. | Fixture | Provides | Use when | |---------|----------|----------| | `testWithUserData` | `userData` (email/password), `page` | Testing sign-up / activation / trial itself — i.e. flows that run *before* a session exists. You drive the `PageFactory` yourself. | | `testWithUserSignedUp` | everything above + `dashboardPage` (already signed in) | Testing in-app UI where you don't need to seed API data. | | `testWithApi` | everything above + `packmindApi` | You must seed standards/packages/skills/etc. before exercising the UI. | All three live in `src/fixtures/packmindTest.ts`. Import the one you need: ```typescript import { testWithApi } from '../../fixtures/packmindTest'; ``` ### Example — UI-only test ```typescript import { testWithUserSignedUp } from '../../fixtures/packmindTest'; import { expect } from '@playwright/test'; testWithUserSignedUp('user sees an empty standards list', async ({ dashboardPage }) => { const standardsPage = await dashboardPage.openStandards(); // eslint-disable-next-line playwright/no-standalone-expect expect(await standardsPage.hasNoStandards()).toBe(true); }); ``` ### Example — seed via API, assert via UI Seed everything *not under test* through `packmindApi` (it's faster and less brittle than clicking through setup), then exercise the actual feature in the browser: ```typescript import { testWithApi } from '../../fixtures/packmindTest'; import { apiStandardFactory } from '../../domain/apiDataFactories/apiStandardFactory'; import { expect } from '@playwright/test'; testWithApi.describe('packages page', () => { testWithApi('lists a standard added to a package', async ({ packmindApi, dashboardPage }) => { const standard = await apiStandardFactory(packmindApi); // ...seed package referencing standard.id... const packagesPage = await dashboardPage.openPackages(); const packagePage = await packagesPage.openPackage('My package'); const standards = await packagePage.listStandardsInPackage(); // eslint-disable-next-line playwright/no-standalone-expect expect(standards).toEqual([{ name: standard.name }]); }); }); ``` > The `eslint-disable playwright/no-standalone-expect` line is required: the lint rule can't tell that a fixture-extended `testWithApi(...)` callback is a real test body. Add it on any `expect` that ESLint flags. ## Writing a Page Object A page object is the typed API a spec uses to talk to one route. Adding one is four mechanical steps — keep them in sync or TypeScript will complain. **1. Declare the interface** in `src/domain/pages/index.ts`. In-app pages extend `IPackmindAppPage` (gives `openStandards`, `openSettings`, etc. for free); pre-login pages extend `IPackmindPage`. ```typescript export interface IBillingPage extends IPackmindAppPage { listInvoices(): Promise<{ date: string; amount: string }[]>; } ``` **2. Implement it** in `src/infra/pages/BillingPage.ts`, extending the matching abstract base, and define `expectedUrl()` as a **RegExp** (the project standard — Playwright's glob matching is too loose): ```typescript import { IBillingPage } from '../../domain/pages'; import { AbstractPackmindAppPage } from './AbstractPackmindAppPage'; export class BillingPage extends AbstractPackmindAppPage implements IBillingPage { async listInvoices(): Promise<{ date: string; amount: string }[]> { await this.page.locator('table tbody tr').first().waitFor(); // ...read rows... } expectedUrl(): RegExp { return /.*\/billing$/; } } ``` **3. Register it in the factory** — add a getter to `IPageFactory` and `PageFactory`. Navigation methods that land on this page return the page object, so specs chain naturally (`dashboardPage.openBilling()` → `IBillingPage`). **4. Prefer `data-testid` over text/role selectors** for app chrome that's likely to be reworded. The codebase exports test-id enums from `@packmind/frontend` (e.g. `SidebarNavigationDataTestId`) — reuse them. ### Why navigation returns a page object After any navigation, the factory calls `waitForLoaded()` (which awaits `expectedUrl`) before handing back the typed page. That's the project's `this.pageFactory()`-after-navigation rule: it guarantees the URL actually changed before the next interaction runs, killing a whole class of race conditions. Never `page.goto` + interact directly in a spec — go through the factory. ## Seeding data through the API When a test needs a resource that already exists in the product, create it via the API rather than clicking through the UI. Add the capability bottom-up: 1. Add `myThing: Gateway;` to `IPackmindApi` (`src/domain/api/IPackmindApi.ts`). All gateway methods are typed with `Gateway` from `@packmind/types`. 2. Implement it in `PackmindApi` (`src/infra/api/PackmindApi.ts`) using the private `post`/`get` helpers — they inject the auth header and assert the status code. 3. Wrap it in an `apiXxxFactory` under `src/domain/apiDataFactories/`, reusing the shared `@packmind//test` factory for default field values (see `apiStandardFactory.ts` / `apiPackageFactory.ts` for the shape). This keeps specs declarative: `const standard = await apiStandardFactory(packmindApi)` instead of a paragraph of POST plumbing. ## Feature flags Before writing, ask: **is the feature under test gated by a feature flag?** If yes, the test user must have a `@packmind.com` email so the flag resolves to on. Flip the fixture option at the top of the file, before any `describe`/test: ```typescript testWithApi.use({ underFeatureFlag: true }); ``` Without it the fixture creates an `@example.com` user, the flag stays off, and the feature is invisible — the test fails for the wrong reason. (Implementation: see the `underFeatureFlag` option in `packmindTest.ts`.) ## Assertion style - Split distinct expectations into separate tests or `describe` blocks rather than one mega-test — a failure name then tells you exactly what broke. - Read state through a page-object method (`listInvoices()`, `listStandards()`) and assert on the returned plain object. Don't reach into the DOM from the spec. - For multi-step scenarios (sign up → create → verify), nest `describe` blocks with their own `beforeEach`, each building on the parent's state. See `CliInstallDistribution.spec.ts` for the pattern. ## Running the tests There are two ways to run, and they share the same entry point — `npm run e2e` (i.e. `npx playwright test`). Never run the suite through Nx; it isn't wired for it. ### Local iteration — fast feedback while writing a spec Bring the stack up first (`michel-run-local-dev-stack`) so `localhost:4200` serves the frontend. Then, **from `apps/e2e-tests/`**: ```bash npm run e2e # all specs (BASE_URL defaults to http://localhost:4200) npx playwright test .spec.ts # one file npx playwright test --headed # watch it run npx playwright test --debug # step through with the inspector npx playwright show-report # open the last HTML report ``` ### Full / CI run — containerized, the canonical way CI runs the suite inside the `run-e2e-tests` Docker Compose service (Playwright image), whose entrypoint is the same `npm run e2e` but with `BASE_URL=http://frontend:4200`. It lives behind the `e2e` profile, so it only starts when you ask for it. Launch it, then block on its exit code with the helper script — this is what CI does and what keeps the two consistent: ```bash # From the repo root, with PACKMIND_EDITION=oss already exported docker compose --profile=e2e up -d run-e2e-tests ./scripts/wait-for-e2e-tests.sh # waits for the container, returns its exit code ``` `wait-for-e2e-tests.sh` matches the container by the `run-e2e-tests` name pattern, waits for it to finish, prints its logs, and exits with the container's code — so a non-zero exit means a failing suite. Reports land in `apps/e2e-tests/playwright-report/` and `apps/e2e-tests/test-results/` via the volume mount, exactly as in local runs. Use local iteration while authoring a spec; use the containerized path to reproduce a CI result or run the whole suite the way the pipeline does. ## Checklist before you call it done - [ ] Spec uses a `testWith*` fixture, not raw `test`. - [ ] No raw selectors in the spec — all interaction goes through page objects. - [ ] New page object: interface in `domain/pages`, impl in `infra/pages`, registered in `PageFactory` + `IPageFactory`, `expectedUrl()` is a RegExp. - [ ] Setup data not under test is seeded via `packmindApi` / an `apiXxxFactory`. - [ ] `testWithApi.use({ underFeatureFlag: true })` added iff the feature is flagged. - [ ] `npx playwright test ` passes locally against a running stack. - [ ] `./node_modules/.bin/nx lint e2e-tests` is clean.