Pular para conteúdo

name: e2e-integration-flow description: 'Author and run full-stack E2E integration tests with Playwright — drive the UI through a complete feature flow and verify the effects at EVERY layer (UI, network/API, database, message queue, side effects). Triggers: "e2e", "teste e2e", "integration test", "teste de integração", "playwright", "validar fluxo completo", "end-to-end". Skip for a quick manual smoke check (use verify) or debugging one known bug (use bugfix).' argument-hint: "[feature/flow to test, or path to its spec/UC]" allowed-tools: - Read - Glob - Grep - Bash - Edit - Write - Agent - TaskCreate - TaskUpdate


E2E Integration Flow Skill

Author and run end-to-end integration tests that prove a feature actually works across the whole stack — not just that a button is clickable, but that every meaningful action produces the right effect at every layer it touches: UI → network/API → database → message queue → downstream side effects.

The driver is Playwright (browser automation for the frontend). The differentiator is that Playwright is only the entry point: each UI step is paired with assertions in the layers behind it. A test that fills a form and sees a toast is a UI test. A test that fills a form, sees the toast, confirms the row landed in the database with the right fields, and confirms the user.created event reached the queue — that is an integration flow test.

Scope & limits. This skill builds and runs automated functional integration tests. It is not a load/performance test, not a security pentest (use owasp-security), and not a substitute for unit tests. It assumes you can reach a running stack (frontend + backend + datastores) in a test/staging environment. Stack-agnostic core with concrete defaults — adapt commands and selectors to the project you are in.

Arguments

$ARGUMENTS is the feature/flow to test (e.g. "cadastro de cliente", "checkout com cupom") or a path to its spec/UC (e.g. docs/specs/cadastro/spec.md, docs/02-requisitos-casos-uso/UC-CAD-001.md). If empty, ask what flow to cover and whether a spec/UC exists.


The mental model: a flow is a chain of contracts

Most flaky, low-value e2e suites fail because they treat the UI as the whole system. The fix is to think of the feature as an ordered flow contract:

For each user action, there is an observable consequence in one or more layers. The test asserts that consequence — at the deepest layer that is practical — before moving to the next action.

A registration flow is not "click, type, click". It is:

# User action UI effect Network Database Queue / async Downstream
1 Log in redirect to dashboard POST /auth 200 + cookie session row (opt)
2 Open "New customer" form renders GET /form-meta 200
3 Submit invalid email inline error, no submit no POST fired no new row
4 Submit valid form success toast, redirect POST /customers 201 customers row w/ fields customer.created on exchange welcome email enqueued
5 (async) list shows new customer GET /customers includes it consumer ACKed email sent

Building this table first is the core of the skill. It turns a vague "test the registration" into a precise, layered set of assertions — and it exposes exactly which verifications are missing in a typical UI-only test.


Step 0 — Detect context & prerequisites

Before writing a line of test code, establish the ground truth. Do not assume.

  1. Find the flow's source of truth. If a spec/UC path was given, read it — user stories, FRs, business rules (RN), and especially the validation rules and the events. If none, look in docs/specs/*/spec.md, docs/02-requisitos-casos-uso/UC-*.md, or ask the user to describe the happy path and the rules.
  2. Locate the running stack & how tests reach it. Base URL of the frontend, API base URL, test database connection, message-broker access. Check for .env.test, docker-compose.*.yml, playwright.config.*, existing e2e//tests/e2e/ folders, CI workflow.
  3. Inventory what already exists. Is Playwright installed (npx playwright --version)? Is there a config, an auth-setup project, fixtures, a seeded test user? Reuse the project's conventions — do not reinvent a harness that already exists.
  4. Confirm verification access. Can the test environment query the DB directly? Reach the broker (AMQP port or RabbitMQ management API)? If a layer is unreachable, you will assert its observable proxy instead (e.g. assert the email was sent rather than the queue message) — note the gap explicitly.

Capture findings as a short context block. If a prerequisite is missing (no test DB, no broker access, frontend not buildable), surface it now — a flow test you cannot verify end-to-end is worth flagging before writing it.

For a multi-step flow, create tasks (TaskCreate) to track each segment.


Step 1 — Build the flow contract

Produce the table above for the target flow. One row per user action.

  • Derive actions from the happy path of the spec/UC.
  • For each action, fill every layer column that applies. Leave where a layer genuinely isn't touched (that is information, not laziness).
  • For the deepest practical layer of each action, mark it as the primary assertion — that is what makes the step trustworthy. UI assertions alone are necessary but never sufficient for the steps that mutate state.
  • Pull the field-level expectations from the spec: which fields persist, their transformed values (trimmed, lowercased, hashed), defaults, FKs.
  • Pull the events from the spec/code: exact routing key / topic, payload shape, which consumer reacts and its side effect.

This table is the test plan. Keep it in the test file as a comment or in a sibling FLOW.md so the coverage is auditable.


Step 2 — Build the test matrix (cases, not just the path)

A flow has more than its happy path. Expand each form/decision point into cases:

Category What to cover Example
Happy path The full flow end-to-end, all layers verified valid registration persists + emits event
Field validation Every rule from the spec: required, format, length, uniqueness, cross-field empty name, bad email, duplicate CPF, password mismatch
Boundary / edge Min/max, special chars, unicode, very long input, leading/trailing space 255-char name, emoji, " a@b.co " trims
Error paths Backend rejects (409/422/500), network failure, timeout duplicate → 409 surfaces a friendly error, no row, no event
Authz / state Wrong role, unauthenticated, already-done logged-out user redirected to login
Idempotency / async Double-submit, eventual consistency, retry double-click submits once; list reflects new row after consumer runs

For each validation case assert the negative space too: an invalid submit must produce no network call (or a 4xx that creates no DB row and no event). Forgetting the negative assertion is the most common hole — the UI shows an error and the bad data still got written.


Step 3 — Set up the harness (reuse first)

Goal: deterministic, isolated, fast-to-debug tests. Key pillars (deep patterns in references/playwright-patterns.md):

  • Auth once, reuse everywhere. A Playwright setup project logs in and saves storageState; flow tests start authenticated. Never log in inside every test unless login is the flow under test.
  • Data isolation. Generate unique test data per run (a run id + faker), so parallel/repeat runs never collide on unique fields. Tag created records so teardown can find them.
  • Deterministic environment. Pin the base URL, seed required reference data, and prefer a dedicated test DB/vhost you can safely write to and clean.
  • Backend assertion helpers. Thin fixtures that the test can call to query the DB and to assert/await queue messages — see Step 5. Keep DB/broker creds in test env, never hardcoded.
  • Trace on failure. Enable trace: 'on-first-retry', screenshots and video on failure — the trace viewer is how you triage which layer broke.

If the project already has a config/auth-setup/fixtures, extend them. Only scaffold from scratch when nothing exists.


Step 4 — Implement the UI steps (web-first, no sleeps)

Translate the flow contract into Playwright, one action → assertion pair at a time. The non-negotiables:

  • Semantic, user-facing locators. getByRole, getByLabel, getByPlaceholder, getByText — they survive refactors and assert accessibility. Use data-testid only as a last resort for ambiguous nodes.
  • Web-first assertions only. await expect(locator).toBeVisible(), toHaveText, toHaveURL — they auto-retry until the condition holds. Never waitForTimeout/fixed sleeps; they are the #1 source of flake.
  • Assert the network where it matters. waitForResponse/expect(response) to confirm the API was hit with the right status — bridges UI to backend. For the negative validation cases, assert the request was never sent.
  • One flow per test, readable as prose. Steps in order, each with its assertion. Use test.step() to label segments so failures point at the right action.
test('register customer — persists and emits customer.created', async ({ page, db, queue }) => {
  const email = uniq('e2e+%s@example.com');           // unique per run
  const listener = await queue.listen('customer.created'); // bind BEFORE acting

  await test.step('open form', async () => {
    await page.getByRole('link', { name: 'Novo cliente' }).click();
    await expect(page.getByRole('heading', { name: 'Novo cliente' })).toBeVisible();
  });

  await test.step('submit valid form', async () => {
    await page.getByLabel('Nome').fill('Ada Lovelace');
    await page.getByLabel('E-mail').fill(email);
    const [res] = await Promise.all([
      page.waitForResponse(r => r.url().endsWith('/customers') && r.request().method() === 'POST'),
      page.getByRole('button', { name: 'Salvar' }).click(),
    ]);
    expect(res.status()).toBe(201);
    await expect(page.getByText('Cliente criado')).toBeVisible();
  });

  await test.step('verify database', async () => {
    const row = await db.one('select * from customers where email = $1', [email]);
    expect(row.name).toBe('Ada Lovelace');     // assert the persisted, transformed fields
  });

  await test.step('verify event reached the queue', async () => {
    const msg = await listener.next({ timeoutMs: 10_000 }); // poll, never sleep
    expect(msg.routingKey).toBe('customer.created');
    expect(msg.payload.email).toBe(email);
  });
});

Step 5 — Verify the backend & async layers

This is what separates an integration flow test from a UI test. Deep recipes in references/backend-async-verification.md.

  • Database. Query directly (the project's client / psql / a pg fixture). Assert the row exists with the expected, transformed fields — not just a count. For negative cases assert it does not exist. Always clean up what the test created (afterEach/afterAll, or a tagged teardown).
  • Message queue (RabbitMQ et al.). Prefer asserting the observable side effect of the consumer (the truest end-to-end signal). When you need to assert the message itself, bind a temporary test queue to the exchange before triggering the action, then poll-consume with a timeout. The management HTTP API (message counts) is a coarser fallback. Never assert with a fixed sleep — poll until present or timeout.
  • Eventual consistency. Async effects (list updates, projections, emails) need a bounded retry: expect.poll(...) / a waitFor(predicate, timeout) helper. State the timeout; if it's exceeded, that's a real failure, not flake.
  • Isolation discipline. Unique data + teardown keeps the suite repeatable. A test that passes once and fails on re-run almost always leaked state.

Step 6 — Run, triage by layer, stabilize

npx playwright test            # full run
npx playwright test --ui       # watch/debug interactively
npx playwright show-trace ...  # open the trace of a failed run
npx playwright test -g "register customer"   # one flow

Triage failures by the layer that broke, using the flow contract as the map:

  • UI assertion failed but network 2xx + DB row present → selector/timing issue in the test, not the app.
  • Network 4xx/5xx → backend rejected; read the response body; is the test data or the app at fault?
  • UI + network fine, DB row missing → persistence bug (transaction rollback, wrong column) — a real find the UI alone would have hidden.
  • DB fine, no queue message / consumer side effect → publisher or binding bug — the highest-value catch of this whole skill.

Stabilize before declaring done: re-run the suite (--repeat-each=3 on the new specs) to flush flake. A test that isn't repeatable isn't a test.


Step 7 — Report coverage & gaps

Close with a concise report (not just "tests pass"):

  • Flow coverage map: the contract table with ✅/❌ per layer per step — what is actually asserted vs. left as a gap (e.g. "queue checked via side effect only; broker not directly reachable in CI").
  • Matrix coverage: which categories from Step 2 have cases; what's deferred.
  • Real findings: any layer mismatch the tests exposed (these are bugs, route them to bugfix).
  • Run command + CI note: how to run locally and whether it's wired into CI.

Golden rules

  1. The UI is the trigger, not the proof. Any step that mutates state earns a backend assertion. A green UI over a silent persistence/event bug is the exact failure mode this skill exists to prevent.
  2. Build the flow contract before the code. The table is the test plan and the coverage report.
  3. Assert the negative space. Invalid input must produce no write and no event — assert the absence, not only the error message.
  4. No fixed sleeps, ever. Web-first assertions and bounded polling. Sleeps are deferred flake.
  5. Isolated & repeatable. Unique data in, teardown out. If it can't run twice, it's not done.
  6. Bind listeners before acting. Subscribe to the queue / start waiting for the response before the click that produces it, or you race the event.
  7. Reuse the project's harness. Auth setup, fixtures, config conventions — extend, don't reinvent.

When NOT to use this skill

  • A quick "does it load / does this one change work" manual check → verify.
  • Investigating a single reported bug → bugfix.
  • Validating requirement/spec quality (not runtime behavior) → checklist / analyze.
  • Security review of the flow → owasp-security.

References