e2e-test
Write and validate Playwright E2E tests for Momentum CMS features. UI tests ALWAYS start from /admin dashboard and navigate via sidebar/dashboard — never go directly to deep URLs. Always starts the server and inspects the actual UI before writing assertions. Triggers include "write e2e tests for...", "add e2e tests", "test the admin UI for...", or "/e2e-test <feature>".
Best use case
e2e-test is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Write and validate Playwright E2E tests for Momentum CMS features. UI tests ALWAYS start from /admin dashboard and navigate via sidebar/dashboard — never go directly to deep URLs. Always starts the server and inspects the actual UI before writing assertions. Triggers include "write e2e tests for...", "add e2e tests", "test the admin UI for...", or "/e2e-test <feature>".
Teams using e2e-test should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/e2e-test/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How e2e-test Compares
| Feature / Agent | e2e-test | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Write and validate Playwright E2E tests for Momentum CMS features. UI tests ALWAYS start from /admin dashboard and navigate via sidebar/dashboard — never go directly to deep URLs. Always starts the server and inspects the actual UI before writing assertions. Triggers include "write e2e tests for...", "add e2e tests", "test the admin UI for...", or "/e2e-test <feature>".
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# E2E Test Writing Skill
Write Playwright E2E tests for Momentum CMS features. UI tests simulate real user journeys — they **always start from the admin dashboard** and navigate through the actual UI, never jump directly to deep URLs.
## RULE 0: TESTS ARE USER STORIES, NOT PLUMBING CHECKS
Every E2E test must be framed as a user story: "As a [role], I should be able to [action] so that [outcome]."
Tests that only check "endpoint returns 200" or "component renders" are banned. Those are health checks pretending to be tests. Every test must:
1. **Start with a real user intent** — what is the user trying to accomplish?
2. **Perform real actions** — create content, navigate UI, trigger workflows
3. **Assert real outcomes** — the data changed, the UI reflects it, the pipeline delivered end-to-end
4. **Prove the full pipeline** — not just one layer, but action → processing → visible result
### Bad: Plumbing check
- "Prometheus endpoint returns 200 with text/plain"
- "Dashboard component renders 4 cards"
- "API returns JSON with correct shape"
### Good: User story
- "As a DevOps engineer, after I create and delete articles, Prometheus shows those exact operation counts"
- "As an admin, after creating content, the observability dashboard shows that activity with real trace IDs"
- "As a non-admin, I'm blocked from the summary API but Prometheus scraping still works"
## RULE 1: DASHBOARD IS THE STARTING POINT
**Every admin UI test starts at `/admin` (the dashboard) and navigates to the feature via the sidebar or dashboard cards.** This is non-negotiable because:
- It validates that the feature is actually visible and reachable in the admin
- It catches missing sidebar links, broken navigation, missing dashboard cards
- It tests the real user journey, not just isolated pages
- If a feature doesn't appear on the dashboard/sidebar, the test fails — which is the correct outcome
### The Navigation Chain
Every UI test follows this chain:
```
/admin (dashboard) → sidebar click → list view → create/view/edit
```
**NEVER do this:**
```typescript
// BAD: Skipping to a deep URL means you never test if navigation works
await authenticatedPage.goto('/admin/collections/redirects/new');
```
**ALWAYS do this:**
```typescript
// GOOD: Start from dashboard, navigate like a real user
await authenticatedPage.goto('/admin');
await authenticatedPage.waitForLoadState('domcontentloaded');
const sidebar = authenticatedPage.getByLabel('Main navigation');
await sidebar.getByRole('link', { name: 'Redirects' }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/redirects$/);
// Now interact with the list page
await authenticatedPage.getByRole('button', { name: /Create Redirect/i }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/redirects\/new/);
```
### What each test should prove
| Test | Proves |
| --------------------------- | -------------------------------------------------- |
| Dashboard card visible | Collection is registered, appears in correct group |
| Sidebar link works | Navigation routing is wired |
| List view loads | Collection data loads, table renders |
| Create form → submit → view | Full create flow end-to-end |
| View page → Edit → Save | Full edit flow end-to-end |
## RULE 2: NEVER WRITE BLIND TESTS
**You MUST see the actual UI before writing any assertions.** Tests written from imagination are fiction.
### Workflow
1. Verify prerequisites (feature wired in, collection in generated config)
2. Run a probe test or start the server and inspect the real page
3. Write tests matching the actual DOM structure
4. Run all tests, confirm 100% pass
5. Only then declare done
### How to inspect the actual UI
Write a probe test that intentionally fails — Playwright's error context includes a YAML snapshot of the page DOM:
```typescript
test('probe: inspect dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin');
await authenticatedPage.waitForLoadState('domcontentloaded');
await expect(authenticatedPage.getByText('XYZZY_WILL_NOT_MATCH')).toBeVisible();
});
```
Run: `npx playwright test --grep "probe:" <spec-file>`
Read the error context file — it shows the exact page structure with element roles, text, and nesting.
## Arguments
- `$ARGUMENTS` - Feature/collection/plugin to test (e.g., "redirects", "analytics dashboard", "seo settings")
## Prerequisites Checklist
Before writing tests, verify:
### 1. Feature is wired into the example app
```bash
grep -n "<feature>" apps/example-angular/src/momentum.config.ts
```
### 2. Collection appears in generated admin config
```bash
grep -n "<slug>" apps/example-angular/src/generated/momentum.config.ts
```
If missing, the plugin needs a static `collections` property:
- **Static** `collections: [MyCollection]` on plugin object = admin UI sees it
- **Runtime** `collections.push(MyCollection)` in `onInit` = server-only, invisible to admin
- Both are needed. After fixing: `npx nx run example-angular:generate`
## Test Structure
```
libs/e2e-tests/src/specs/<feature>.spec.ts
```
### Imports and fixtures
```typescript
import { test, expect, TEST_CREDENTIALS } from '../fixtures';
```
- `authenticatedPage` — Browser page logged in as admin
- `request` — API context (needs manual sign-in via `beforeEach`)
- `baseURL`, `playwright` — For creating fresh request contexts
## Admin UI Test Patterns (Dashboard-First)
### Full Navigation Flow Test
```typescript
test.describe('Feature Admin UI', { tag: ['@feature', '@admin'] }, () => {
test('should navigate from dashboard to list via sidebar', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin');
await authenticatedPage.waitForLoadState('domcontentloaded');
// 1. Verify dashboard card in correct group
const section = authenticatedPage.getByRole('region', { name: 'GroupName' });
await expect(section).toBeVisible();
await expect(section.getByRole('heading', { name: 'FeatureLabel' })).toBeVisible();
// 2. Navigate via sidebar
const sidebar = authenticatedPage.getByLabel('Main navigation');
await sidebar.getByRole('link', { name: 'FeatureLabel' }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>$/);
// 3. Verify list page loaded
await expect(authenticatedPage.getByRole('heading', { name: 'PluralLabel' })).toBeVisible();
});
test('should create via UI from dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin');
await authenticatedPage.waitForLoadState('domcontentloaded');
// Navigate to list
const sidebar = authenticatedPage.getByLabel('Main navigation');
await sidebar.getByRole('link', { name: 'FeatureLabel' }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>$/);
// Click create
await authenticatedPage.getByRole('button', { name: /Create SingularLabel/i }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>\/new/);
// Wait for form
await expect(
authenticatedPage.getByRole('button', { name: 'Create', exact: true }),
).toBeVisible();
// Fill and submit
await authenticatedPage.locator('input#field-title').fill('Test Value');
await authenticatedPage.getByRole('button', { name: 'Create', exact: true }).click();
// After create: navigates to VIEW page (read-only, NOT edit form)
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>\/[^/]+$/);
await expect(authenticatedPage.getByText('Test Value')).toBeVisible();
});
test('should edit via UI from dashboard', async ({ authenticatedPage }) => {
// Create via API first
const createRes = await authenticatedPage.request.post('/api/<slug>', {
data: { title: 'Edit Me' },
});
expect(createRes.status()).toBe(201);
const { doc } = (await createRes.json()) as { doc: { id: string } };
try {
await authenticatedPage.goto('/admin');
await authenticatedPage.waitForLoadState('domcontentloaded');
// Navigate to list
const sidebar = authenticatedPage.getByLabel('Main navigation');
await sidebar.getByRole('link', { name: 'FeatureLabel' }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>$/);
// Click into the item (list row link)
// Click table row to navigate to view page
const row = authenticatedPage.locator('mcms-table-body mcms-table-row', {
hasText: 'Edit Me',
});
await expect(row).toBeVisible({ timeout: 10000 });
await row.click();
// View page: has Edit and Delete buttons
await expect(authenticatedPage.getByRole('button', { name: 'Edit' })).toBeVisible();
await authenticatedPage.getByRole('button', { name: 'Edit' }).click();
// Edit form: "Save Changes" button (NOT "Save")
await expect(authenticatedPage.getByRole('button', { name: 'Save Changes' })).toBeVisible({
timeout: 15000,
});
const input = authenticatedPage.locator('input#field-title');
await input.clear();
await input.fill('Updated Value');
await authenticatedPage.getByRole('button', { name: 'Save Changes' }).click();
// Returns to list
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/<slug>$/);
} finally {
await authenticatedPage.request.delete(`/api/<slug>/${doc.id}`);
}
});
});
```
### Key UI Facts (verified against real admin)
| Element | Selector |
| ---------------- | ------------------------------------------------------------------------------- |
| Sidebar nav | `getByLabel('Main navigation')` |
| Dashboard group | `getByRole('region', { name: 'GroupName' })` |
| Group headings | Appear as both group headers AND collection links — use `.first()` if ambiguous |
| Text inputs | `locator('input#field-{fieldName}')` |
| Select dropdowns | `locator('select#field-{fieldName}')` |
| Breadcrumbs | `locator('mcms-breadcrumbs')` |
| Create button | `getByRole('button', { name: 'Create', exact: true })` |
| Save button | `getByRole('button', { name: 'Save Changes' })` — NOT "Save" |
| View page | Shows read-only text values, "Edit" + "Delete" buttons |
| Edit URL | `/{id}/edit` — NOT `/{id}` (that's the view page) |
### Alternative: Dashboard "Create" shortcut
The dashboard cards have "Create" and "View all" links. You can also test via those:
```typescript
const section = authenticatedPage.getByRole('region', { name: 'Settings' });
await section.getByRole('link', { name: 'Create' }).click();
await expect(authenticatedPage).toHaveURL(/\/admin\/collections\/redirects\/new/);
```
## API Test Patterns
API tests complement UI tests. They test CRUD, middleware, access control.
```typescript
test.describe('Feature - API', { tag: ['@feature', '@api'] }, () => {
test.beforeEach(async ({ request }) => {
const signIn = await request.post('/api/auth/sign-in/email', {
headers: { 'Content-Type': 'application/json' },
data: { email: TEST_CREDENTIALS.email, password: TEST_CREDENTIALS.password },
});
expect(signIn.ok(), 'Admin sign-in must succeed').toBe(true);
});
test('CRUD', async ({ request }) => {
const create = await request.post('/api/<slug>', {
headers: { 'Content-Type': 'application/json' },
data: { ... },
});
expect(create.status()).toBe(201); // Exact status codes always
});
});
```
### Middleware/Redirect Testing
```typescript
const noRedirectCtx = await playwright.request.newContext({
baseURL: baseURL!,
maxRedirects: 0,
});
try {
const response = await noRedirectCtx.get('/old-path');
expect(response.status()).toBe(301);
expect(response.headers()['location']).toBe('/new-path');
} finally {
await noRedirectCtx.dispose();
}
```
## Banned Patterns (from CLAUDE.md)
1. **NO `.catch(() => false/null/{})` on Playwright calls**
2. **NO `waitForTimeout(N)`** — use `expect(locator).toBeVisible({ timeout: N })` or `expect.poll()`
3. **NO ambiguous OR-logic** like `.ok() || .status() === 201` — use exact assertions
4. **NO direct URL navigation for UI tests** — always start from `/admin` and click through
## Running Tests
```bash
# Run specific spec
npx playwright test --reporter=list libs/e2e-tests/src/specs/<feature>.spec.ts
# Run by name
npx playwright test --grep "test name" libs/e2e-tests/src/specs/<feature>.spec.ts
```
**ALL tests MUST pass. Do not declare done until you see 0 failures.**
## Reference Specs
- `admin-dashboard.spec.ts` — Dashboard regions, sidebar navigation
- `collection-edit.spec.ts` — Create form, field rendering, Cancel button
- `signal-forms.spec.ts` — Edit flow with "Save Changes"
- `collection-list.spec.ts` — List view patterns
- `redirects.spec.ts` — Full example: dashboard-first UI + API + middleware testsRelated Skills
test-all
Run the FULL Momentum CMS test suite — every single suite, no skips. Triggers on "test all", "test everything", "run all tests", "run the test all script", "test-all script", "run the full suite", "run every test", "test the whole thing", "make sure everything passes", "run test:all", or ANY variation asking to run all/every/full tests. Also triggers on typos like "test al", "tets all", "tes all". NEVER skip suites unless the user EXPLICITLY names suites to skip.
stroll-test
End-to-end CLI stroll test of npm-published Momentum CMS packages. Scaffolds a fresh project with create-momentum-app, adds all plugins, runs migrations, starts server, and verifies everything works. Triggers include "stroll test", "cli stroll", "test published packages", or "/stroll-test".
headless-ui
Use @momentumcms/headless inside generated Momentum apps. Use when building custom public UI, composing accessible primitives, configuring global styles for hdl-* elements, or adding app-level tests around headless interactions.
ui-audit
Comprehensive UI component audit for Momentum CMS. Use when asked to audit, review, check, or validate a UI component. Checks Storybook stories, interaction tests, variants, kitchen sink integration, admin dashboard usage, accessibility, and responsive design (mobile-first). AUTOMATICALLY FIXES issues found and verifies with visual inspection. Triggers include "audit button", "review the card component", "check accessibility of tabs", or "/ui-audit <component-name>".
skill-improve
Self-improving skill loop. Analyzes eval failures, rewrites the skill, re-evaluates, and repeats until convergence. Run after /skill-eval produces baseline results.
skill-eval
Run structured evaluations comparing skill vs no-skill performance. Measures assertion pass rates, timing, and output quality to systematically improve skills.
prepare-release
Prepare a patch/minor/major version release for all Momentum CMS packages. Bumps versions, generates changelogs, verifies builds/tests, adds new packages to Nx release config, and commits. Triggers include "prepare release", "bump version", "release patch", or "/prepare-release".
momentum-api
Work with Momentum API for data operations in Angular components
migrations
Run migrations, generate schemas, and manage code generation for Momentum CMS. Use when working with database migrations, Drizzle schema generation, type generation, or Angular schematics.
mcp-setup
Set up the Momentum CMS MCP server plugin and generate Claude Code MCP config for AI tool integration. Use when connecting Claude Code (or any MCP client) to a Momentum CMS instance.
SYSTEM ROLE & BEHAVIORAL PROTOCOLS
**ROLE:** Senior Frontend Architect & Avant-Garde UI Designer.
headless-primitive
Author, extend, or repair primitives in libs/headless. Use when adding a new headless primitive, changing its accessibility contract, updating slots/state attrs, wiring overlay behavior, or expanding the example styling lab and tests.