Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
architecture/test-architecture.md
1# Choosing Test Types: E2E, Component, or API23## Table of Contents451. [Decision Matrix](#decision-matrix)62. [API Tests](#api-tests)73. [Component Tests](#component-tests)84. [E2E Tests](#e2e-tests)95. [Layering Test Types](#layering-test-types)106. [Common Mistakes](#common-mistakes)117. [Related](#related)1213> **When to use**: Deciding which test type to write for a feature. Ask: "What's the cheapest test that gives confidence this works?"1415## Decision Matrix1617| Scenario | Recommended Type | Rationale |18| --------------------------- | ---------------- | --------------------------------------------- |19| Login / auth flow | E2E | Cross-page, cookies, redirects, session state |20| Form submission | Component | Isolated validation logic, error states |21| CRUD operations | API | Data integrity matters more than UI |22| Search with results UI | Component + API | API for query logic; component for rendering |23| Cross-page navigation | E2E | Routing, history, deep linking |24| API error handling | API | Status codes, error shapes, edge cases |25| UI error feedback | Component | Toast, banner, inline error rendering |26| Accessibility | Component | ARIA roles, keyboard nav per-component |27| Responsive layout | Component | Viewport-specific rendering without full app |28| API contract validation | API | Response shapes, headers, auth |29| WebSocket/real-time | E2E | Requires full browser environment |30| Payment / checkout | E2E | Multi-step, third-party iframes |31| Onboarding wizard | E2E | Multi-step, state persists across pages |32| Widget behavior | Component | Toggle, accordion, date picker, modal |33| Permissions / authorization | API | Role-based access is backend logic |3435## API Tests3637**Ideal for**:3839- CRUD operations (create, read, update, delete)40- Input validation and error responses (400, 422)41- Permission and authorization checks42- Data integrity and business rules43- API contract verification44- Edge cases expensive to reproduce through UI45- Test data setup/teardown for E2E tests4647**Avoid for**:4849- Testing how errors display to users50- Browser-specific behavior (cookies, redirects)51- Visual layout or responsive design52- Flows requiring JavaScript execution or DOM interaction53- Third-party iframe interactions5455```typescript56import { test, expect } from "@playwright/test";5758test.describe("Products API", () => {59let token: string;6061test.beforeAll(async ({ request }) => {62const res = await request.post("/api/auth/token", {63data: { email: "[email protected]", password: "mgr-secret" },64});65token = (await res.json()).accessToken;66});6768test("creates product with valid payload", async ({ request }) => {69const res = await request.post("/api/products", {70headers: { Authorization: `Bearer ${token}` },71data: { name: "Widget Pro", sku: "WGT-100", price: 29.99 },72});7374expect(res.status()).toBe(201);75const product = await res.json();76expect(product).toMatchObject({ name: "Widget Pro", sku: "WGT-100" });77expect(product).toHaveProperty("id");78});7980test("rejects duplicate SKU with 409", async ({ request }) => {81const res = await request.post("/api/products", {82headers: { Authorization: `Bearer ${token}` },83data: { name: "Duplicate", sku: "WGT-100", price: 19.99 },84});8586expect(res.status()).toBe(409);87expect((await res.json()).message).toContain("already exists");88});8990test("returns 422 for missing required fields", async ({ request }) => {91const res = await request.post("/api/products", {92headers: { Authorization: `Bearer ${token}` },93data: { name: "Incomplete" },94});9596expect(res.status()).toBe(422);97const err = await res.json();98expect(err.errors).toContainEqual(99expect.objectContaining({ field: "sku" })100);101});102103test("staff role cannot delete products", async ({ request }) => {104const staffLogin = await request.post("/api/auth/token", {105data: { email: "[email protected]", password: "staff-pass" },106});107const staffToken = (await staffLogin.json()).accessToken;108109const res = await request.delete("/api/products/123", {110headers: { Authorization: `Bearer ${staffToken}` },111});112113expect(res.status()).toBe(403);114});115116test("lists products with pagination", async ({ request }) => {117const res = await request.get("/api/products", {118headers: { Authorization: `Bearer ${token}` },119params: { page: "1", limit: "20" },120});121122expect(res.status()).toBe(200);123const body = await res.json();124expect(body.items).toBeInstanceOf(Array);125expect(body.items.length).toBeLessThanOrEqual(20);126expect(body).toHaveProperty("totalCount");127});128});129```130131## Component Tests132133**Ideal for**:134135- Form validation (required fields, format rules, error messages)136- Interactive widgets (modals, dropdowns, accordions, date pickers)137- Conditional rendering (show/hide, loading states, empty states)138- Accessibility per-component (ARIA attributes, keyboard navigation)139- Responsive layout at different viewports140- Visual states (hover, focus, disabled, selected)141142**Avoid for**:143144- Testing routing or navigation between pages145- Flows requiring real cookies, sessions, or server-side state146- Data persistence or API contract validation147- Third-party iframe interactions148- Anything requiring multiple pages or browser contexts149150```typescript151import { test, expect } from "@playwright/experimental-ct-react";152import { ContactForm } from "../src/components/ContactForm";153154test.describe("ContactForm component", () => {155test("displays validation errors on empty submit", async ({ mount }) => {156const component = await mount(<ContactForm onSubmit={() => {}} />);157158await component.getByRole("button", { name: "Send message" }).click();159160await expect(component.getByText("Name is required")).toBeVisible();161await expect(component.getByText("Email is required")).toBeVisible();162});163164test("rejects malformed email", async ({ mount }) => {165const component = await mount(<ContactForm onSubmit={() => {}} />);166167await component.getByLabel("Name").fill("Alex");168await component.getByLabel("Email").fill("invalid-email");169await component.getByLabel("Message").fill("Hello");170await component.getByRole("button", { name: "Send message" }).click();171172await expect(component.getByText("Enter a valid email")).toBeVisible();173});174175test("invokes onSubmit with form data", async ({ mount }) => {176const submissions: Array<{ name: string; email: string; message: string }> =177[];178const component = await mount(179<ContactForm onSubmit={(data) => submissions.push(data)} />180);181182await component.getByLabel("Name").fill("Alex");183await component.getByLabel("Email").fill("[email protected]");184await component.getByLabel("Message").fill("Inquiry about pricing");185await component.getByRole("button", { name: "Send message" }).click();186187expect(submissions).toHaveLength(1);188expect(submissions[0]).toEqual({189name: "Alex",190email: "[email protected]",191message: "Inquiry about pricing",192});193});194195test("disables button during submission", async ({ mount }) => {196const component = await mount(197<ContactForm onSubmit={() => {}} submitting={true} />198);199200await expect(201component.getByRole("button", { name: "Sending..." })202).toBeDisabled();203});204205test("associates labels with inputs for accessibility", async ({ mount }) => {206const component = await mount(<ContactForm onSubmit={() => {}} />);207208await expect(209component.getByRole("textbox", { name: "Name" })210).toBeVisible();211await expect(212component.getByRole("textbox", { name: "Email" })213).toBeVisible();214});215});216```217218## E2E Tests219220**Ideal for**:221222- Critical user flows that generate revenue (checkout, signup)223- Authentication flows (login, SSO, MFA, password reset)224- Multi-page workflows where state carries across navigation225- Flows involving third-party iframes (payment widgets)226- Smoke tests validating the entire stack227- Real-time collaboration requiring multiple browser contexts228229**Avoid for**:230231- Testing every form validation permutation232- CRUD operations where UI is a thin wrapper233- Verifying individual component states234- Testing API response shapes or error codes235- Responsive layout at every breakpoint236- Edge cases that only affect the backend237238```typescript239import { test, expect } from "@playwright/test";240241test.describe("subscription flow", () => {242test.beforeEach(async ({ page }) => {243await page.request.post("/api/test/seed-account", {244data: { plan: "free", email: "[email protected]" },245});246await page.goto("/account/upgrade");247});248249test("upgrades to premium plan", async ({ page }) => {250await test.step("select plan", async () => {251await expect(252page.getByRole("heading", { name: "Choose Your Plan" })253).toBeVisible();254await page.getByRole("button", { name: "Select Premium" }).click();255});256257await test.step("enter billing details", async () => {258await page.getByLabel("Cardholder name").fill("Sam Johnson");259await page.getByLabel("Billing address").fill("456 Oak Ave");260await page.getByLabel("City").fill("Seattle");261await page.getByRole("combobox", { name: "State" }).selectOption("WA");262await page.getByLabel("Postal code").fill("98101");263await page.getByRole("button", { name: "Continue" }).click();264});265266await test.step("complete payment", async () => {267const paymentFrame = page.frameLocator('iframe[title="Secure Payment"]');268await paymentFrame.getByLabel("Card number").fill("5555555555554444");269await paymentFrame.getByLabel("Expiry").fill("09/29");270await paymentFrame.getByLabel("CVV").fill("456");271await page.getByRole("button", { name: "Subscribe now" }).click();272});273274await test.step("verify success", async () => {275await page.waitForURL("**/account/subscription/success**");276await expect(277page.getByRole("heading", { name: "Welcome to Premium" })278).toBeVisible();279await expect(page.getByText(/Subscription #\d+/)).toBeVisible();280});281});282});283```284285## Layering Test Types286287Effective test suites combine all three types. Example for an "inventory management" feature:288289### API Layer (60% of tests)290291Cover every backend logic permutation. Cheap to run and maintain.292293```294tests/api/inventory.spec.ts295- creates item with valid data (201)296- rejects duplicate SKU (409)297- rejects invalid quantity format (422)298- rejects missing required fields (422)299- warehouse-staff cannot delete items (403)300- unauthenticated request returns 401301- lists items with pagination302- filters items by category303- updates item stock level304- archives an item305- prevents archiving items with pending orders306```307308### Component Layer (30% of tests)309310Cover every visual state and interaction.311312```313tests/components/InventoryForm.spec.tsx314- shows validation errors on empty submit315- shows inline error for invalid SKU format316- disables submit while saving317- calls onSubmit with form data318- resets form after successful save319320tests/components/InventoryTable.spec.tsx321- renders item rows from props322- shows empty state when no items323- handles archive confirmation modal324- sorts by column header click325- shows stock level badges with correct colors326```327328### E2E Layer (10% of tests)329330Cover only critical paths proving full stack works.331332```333tests/e2e/inventory.spec.ts334- manager creates item and sees it in list335- manager updates item stock level336- warehouse-staff cannot access admin settings337```338339### Execution Profile340341For this feature:342343- **11 API tests** — ~2 seconds total, no browser344- **10 component tests** — ~5 seconds total, real browser but no server345- **3 E2E tests** — ~15 seconds total, full stack346347Total: 24 tests, ~22 seconds. API tests catch most regressions. Component tests catch UI bugs. E2E tests prove wiring works. If E2E fails but API and component pass, the problem is in integration (routing, state management, API client).348349## Common Mistakes350351| Anti-Pattern | Problem | Better Approach |352| ----------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------- |353| E2E for every validation rule | 30-second browser test for something API covers in 200ms | API test for validation, one component test for error display |354| No API tests, all E2E | Slow suite, flaky from UI timing, hard to diagnose | API tests for data/logic, E2E for critical paths only |355| Component tests mocking everything | Tests pass but app broken because mocks drift | Mock only external boundaries; API tests verify real contracts |356| Same assertion in API, component, AND E2E | Triple maintenance cost | Each layer tests what it uniquely verifies |357| E2E creating test data via UI | 2-minute test where 90 seconds is setup | Seed via API in `beforeEach`, test actual flow |358| Testing third-party behavior | Testing that Stripe validates cards (Stripe's job) | Mock Stripe; trust their contract |359| Skipping API layer | Can't tell if bug is frontend or backend | API tests isolate backend; component tests isolate frontend |360| One giant E2E for entire feature | 5-minute test failing somewhere with no clear cause | Focused E2E per critical path; use `test.step()` |361362## Related363364- [test-suite-structure.md](../core/test-suite-structure.md) — file structure and naming365- [api-testing.md](../testing-patterns/api-testing.md) — Playwright's `request` API for HTTP testing366- [component-testing.md](../testing-patterns/component-testing.md) — setting up component tests367- [authentication.md](../advanced/authentication.md) — auth flow patterns with `storageState`368- [when-to-mock.md](when-to-mock.md) — when to mock vs hit real services369- [pom-vs-fixtures.md](pom-vs-fixtures.md) — organizing shared test logic370