e2e-test-narrative-builder
Assembles a multi-step end-to-end user-journey test from a list of high-level user intents - translates each intent ("user signs up", "user adds product to cart", "user completes checkout with promo code") into the corresponding test-runner step (Playwright / Cypress / Selenium / Karate), wires shared state across steps via test fixtures, and emits the resulting test as a single Scenario in the project's E2E framework. Use when scaffolding an E2E test that exercises a complete user flow rather than a single page.
e2e-test-narrative-builder
Overview
E2E tests model user journeys - sequences of actions across multiple pages / components that collectively exercise a business flow. Authoring them as a list of low-level Playwright / Cypress / Selenium calls is verbose and easy to break on UI refactors.
This skill takes a high-level intent list and assembles a single narrative test:
1. user_signs_up_with_email
2. user_confirms_email
3. user_creates_workspace
4. user_invites_teammate
5. user_completes_first_task→ One .spec.ts (or .feature) with the canonical sequence.
When to use
Step 1 - Capture the intent list
A typical user-journey test has 5-15 intents:
test:
name: New user onboards and completes first task
fixtures: [seed_workspace, seed_admin_user] # from seed-data-curator
steps:
- intent: user_visits_landing_page
page: /
- intent: user_clicks_signup
- intent: user_fills_signup_form
data:
email: '{{ faker.email }}'
password: '{{ faker.password }}'
- intent: user_submits_signup
- intent: user_confirms_email_via_inbox
- intent: user_creates_workspace
data:
name: 'Test Workspace'
- intent: user_invites_teammate
data:
teammate_email: 'teammate@example.com'
- intent: user_creates_first_task
data:
title: 'Hello world'
- intent: user_marks_task_complete
assertions:
- on_page: /workspace/{workspace_id}
- selector_visible: '[data-testid="task-complete-celebration"]'
- api_called: POST /api/tasks/completeIntents are named verbs in user-language. They abstract over "click the button with text 'Sign up'" - the matching helper function (in step 2) knows how.
Step 2 - Define the intent → step mapping
Once per project, a mapping file translates intents to framework- native code:
// e2e/intents/index.ts (Playwright example)
import { Page, expect } from '@playwright/test';
export const intents = {
user_visits_landing_page: async (page: Page, ctx: any) => {
await page.goto(ctx.baseUrl);
},
user_clicks_signup: async (page: Page) => {
await page.getByRole('link', { name: 'Sign up' }).click();
},
user_fills_signup_form: async (page: Page, ctx: any, data: any) => {
await page.getByLabel('Email').fill(data.email);
await page.getByLabel('Password').fill(data.password);
ctx.email = data.email; // share state with later intents
ctx.password = data.password;
},
user_submits_signup: async (page: Page) => {
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page).toHaveURL(/\/welcome/);
},
user_confirms_email_via_inbox: async (page: Page, ctx: any) => {
const link = await getEmailConfirmLink(ctx.email); // helper that reads from a Mailpit / Inbucket fixture
await page.goto(link);
},
// ... etc
};The mapping is the project's - it knows how the project's UI is laid out. Adding a new intent or refactoring an existing one is one edit; tests authored later automatically use the new shape.
Step 3 - Generate the test
The skill emits the matching .spec / .feature file.
Default: Playwright - first-party TypeScript types, built-in fixtures + parallelism, role/label-based selectors that survive UI refactors. Use Cypress when the project already standardizes on it; use Karate for pure-API journeys; use Selenium only for legacy suites already invested in it.
Playwright
// e2e/onboarding.spec.ts
import { test, expect } from '@playwright/test';
import { intents } from './intents';
test('new user onboards and completes first task', async ({ page }) => {
const ctx = { baseUrl: process.env.BASE_URL ?? 'http://localhost:3000' };
await intents.user_visits_landing_page(page, ctx);
await intents.user_clicks_signup(page, ctx);
await intents.user_fills_signup_form(page, ctx, {
email: 'newuser@example.com',
password: 'TestPass123!',
});
await intents.user_submits_signup(page, ctx);
await intents.user_confirms_email_via_inbox(page, ctx);
await intents.user_creates_workspace(page, ctx, { name: 'Test Workspace' });
await intents.user_invites_teammate(page, ctx, { teammate_email: 'teammate@example.com' });
await intents.user_creates_first_task(page, ctx, { title: 'Hello world' });
await intents.user_marks_task_complete(page, ctx);
// Assertions
await expect(page).toHaveURL(/\/workspace\/[a-z0-9-]+/);
await expect(page.locator('[data-testid="task-complete-celebration"]')).toBeVisible();
});Cypress
Similar structure with Cypress's chained command syntax; the intents map to cy.get(...) calls.
Karate
Feature: New user onboards and completes first task
Background:
* url 'http://localhost:3000'
Scenario: Onboarding flow
Given path '/'
When method GET
Then status 200
Given path '/signup'
And request { email: '#{newuser@example.com}', password: 'TestPass123!' }
When method POST
Then status 201
# ... more steps(Per karate-testing.)
Step 4 - Wire fixtures
Each intent may depend on fixtures generated by:
| Fixture source | Skill |
|---|---|
| Seed data (workspaces, admin users) | seed-data-curator. |
| Synthetic field values (emails, passwords) | faker-data etc. |
| Mock external services (email, payment) | wiremock-stubs, msw-handlers, mountebank-imposters. |
The intent file declares fixtures: [...] and the generated test imports them at the top.
Output format
## E2E Narrative Generated — `<test-name>`
**Framework:** Playwright | Cypress | Selenium | Karate
**Intents used:** N
**Test file:** `e2e/<slug>.spec.ts`
**Fixtures referenced:**
- seed_workspace (from seed-data-curator)
- mock_email_inbox (from wiremock-stubs)
**Output preview:**
```typescript
// (the generated test file)
Validation
Open intents (require new helpers)
## Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---------------------------------------------------------------|---------------------------------------------------------------------|-----|
| Inline Playwright calls instead of named intents | Tests break on UI refactors; can't migrate frameworks easily. | Always go through the intent mapping. |
| One mega-intent that does 5 things | Hard to compose with other tests; failure attribution unclear. | One intent per user-observable action. |
| Intent names that include implementation details (`user_clicks_xpath`) | Couples the intent to a selector strategy. | Intent names are user-language: `user_signs_up`, not `user_clicks_signup_button`. |
| Sharing context via globals | Cross-test pollution; flaky. | Pass `ctx` through every intent; per-test scope. |
| Authoring 50-step narrative tests | Single-test failure cascades; hard to debug. | Split into smaller stories; reuse intents across multiple smaller tests. |
## Limitations
- **One framework per project.** The intent mapping is per-
framework; sharing intents across Playwright + Cypress + Karate
in the same repo means maintaining three mappings.
- **UI changes still break.** Robust selectors (data-testid, role)
reduce churn but don't eliminate it; a UI overhaul still
requires updating the intent mapping.
- **Doesn't replace specific assertion logic.** The narrative
describes the journey; assertions per intent still need explicit
authoring.
## References
- [`seed-data-curator`](../seed-data-curator/SKILL.md),
[`faker-data`](../faker-data/SKILL.md),
[`wiremock-stubs`](../wiremock-stubs/SKILL.md),
[`msw-handlers`](../msw-handlers/SKILL.md),
[`mountebank-imposters`](../mountebank-imposters/SKILL.md) -
fixture sources.
- [`karate-testing`](../../../qa-api-testing/skills/karate-testing/SKILL.md) - Karate output format.
- [`spec-to-suite-orchestrator`](../../../qa-shift-left/agents/spec-to-suite-orchestrator.md) - upstream agent that hands off intent lists to this skill.