CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

High

Do not use without reviewing

Overview
Quality
Evals
Security
Files

SKILL.md

name:
e2e-test-narrative-builder
description:
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

This skill takes a high-level intent list and assembles a single narrative test - named user-language intents map to framework-native steps, so one UI refactor updates one mapping, not every 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

  • Scaffolding a new E2E test for a multi-step business flow.
  • Migrating from one E2E framework to another (Playwright → Cypress, etc.) - write the intents once, regenerate.
  • A PRD describes a user journey and the team wants the matching E2E test before implementation.

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/complete

Intents 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. Each intent is one async function that drives the UI and optionally shares state via ctx:

// e2e/intents/index.ts (Playwright)
export const intents = {
  user_clicks_signup: async (page: Page) =>
    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);
    ctx.email = data.email;   // share state with later intents
  },
};

The mapping is the project's - it knows how the UI is laid out; adding or refactoring an intent is one edit. Full mapping plus the Cypress and Karate variants: references/intent-mapping.md.

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. Cypress and Karate output shapes: references/intent-mapping.md.

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();
});

Step 4 - Wire fixtures

Each intent may depend on fixtures generated by:

Fixture sourceSkill
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

- All intents have matching entries in `e2e/intents/index.ts`.
- All referenced fixtures exist in the project.
- Test compiles (TypeScript / JS lint pass).
- (Optional) test runs once locally and passes against a seeded env.

### Open intents (require new helpers)

- `user_completes_payment_with_apple_pay` - not yet in the intent
  mapping. Add to `e2e/intents/index.ts` before this test will run.

Anti-patterns

Anti-patternWhy it failsFix
Inline Playwright calls instead of named intentsTests break on UI refactors; can't migrate frameworks easily.Always go through the intent mapping.
One mega-intent that does 5 thingsHard 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 globalsCross-test pollution; flaky.Pass ctx through every intent; per-test scope.
Authoring 50-step narrative testsSingle-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, faker-data, wiremock-stubs, msw-handlers, mountebank-imposters - fixture sources.
  • karate-testing (in the qa-api-testing plugin) - Karate output format.

SKILL.md

tile.json