| name | create-e2e-test |
| description | Guide for writing Playwright E2E tests with the project's auth, helpers, and isolation patterns |
Skill: Create E2E Test
Use this skill when adding a new Playwright E2E test spec. See examples/ for annotated reference tests.
Infrastructure
| File | Purpose |
|---|
playwright.config.ts | Serial execution, 1 worker, port 3001, auth project |
tests/global-setup.ts | Creates fresh ynab_test DB, seeds CSV data, 2 users |
tests/auth.setup.ts | Logs in as TEST_USER, saves session to .auth/user.json |
tests/test-constants.ts | Credentials, URLs, DB names |
tests/e2e-helpers.ts | Navigation helpers |
tests/i18n-helpers.ts | Locale-aware t() translation helper |
Steps
1. Create the spec file
tests/<feature-name>.spec.ts
Use the template from examples/example.spec.ts.
2. Import i18n helpers (mandatory)
All E2E tests MUST use the t() helper for any user-facing text. Never hardcode strings.
import { t, TEST_LOCALE } from "./i18n-helpers";
For specs that manage their own auth (no storageState), pin the test locale cookie:
test.beforeEach(async ({ page }) => {
await page.context().addCookies([
{
name: "NEXT_LOCALE",
value: TEST_LOCALE,
domain: "localhost",
path: "/",
},
]);
});
3. Use navigation helpers (mandatory)
import {
gotoBudgetPage,
gotoFirstAccount,
getTestBudgetId,
} from "./e2e-helpers";
| Helper | What it does |
|---|
gotoBudgetPage(page, request) | Navigate + wait for budget-table, returns budgetId |
gotoFirstAccount(page, request) | Navigate + click first sidebar account |
getTestBudgetId(request) | Fetch budgetId via API (cached) |
budgetUrl(budgetId) | Returns /budgets/{id}/budget |
accountUrl(budgetId, accountId) | Returns /budgets/{id}/accounts/{accountId} |
allAccountsUrl(budgetId) | Returns /budgets/{id}/accounts |
Never hardcode URLs — SaaS routes use /budgets/[id]/budget, not /budget.
4. Use data-testid and t() selectors
page.getByLabel(t("auth.email")).fill("user@test.com");
page.getByRole("button", { name: t("auth.login") }).click();
page.getByTestId("rta-amount");
page.locator('[data-testid^="category-row-"]');
page.getByLabel("Email").fill("user@test.com");
page.getByRole("button", { name: /Iniciar Sesión/i }).click();
5. Wait for server roundtrips after mutations
await page.getByTestId("save-button").click();
await page.waitForResponse(
(resp) => resp.url().includes("/api/budgets/") && resp.status() === 200,
);
await expect(page.getByTestId("value")).toHaveText("Updated");
6. Run
npm run test:e2e
npm run test:e2e -- tests/my-feature.spec.ts
npm run test:e2e -- --ui
Special Patterns
- Animated values (RTA): Poll for stability — see examples/example.spec.ts
- Tenant isolation: Login manually with locale cookie — see examples/isolation.spec.ts
- Auth is automatic:
auth.setup.ts runs first, all specs use TEST_USER session
- Locale override: Set
TEST_LOCALE=en npx playwright test to run tests in English
Common Pitfalls
| Pitfall | Fix |
|---|
| Hardcoded UI strings | Use t('key') from i18n-helpers.ts — never hardcode labels or button text |
| Wrong URL | Use helpers, not page.goto('/budget') |
| Intermittent assertion failure | Wait for server roundtrip before asserting |
RTA shows $ 0,00 | Poll for animated value stability |
| Login fails | Ensure locale cookie is set; use t('auth.*') for selectors |
storageState not found | auth-setup project runs as dependency (configured in playwright.config.ts) |
CI Guard
The check-locale-strings.sh CI guard script scans all tests/*.spec.ts files for hardcoded Spanish strings and fails the build if found. Always use t().
Examples Directory