854 lines
24 KiB
Markdown
854 lines
24 KiB
Markdown
---
|
|
title: "Network-First Patterns Explained"
|
|
description: Understanding how TEA eliminates test flakiness by waiting for actual network responses
|
|
---
|
|
|
|
# Network-First Patterns Explained
|
|
|
|
Network-first patterns are TEA's solution to test flakiness. Instead of guessing how long to wait with fixed timeouts, wait for the actual network event that causes UI changes.
|
|
|
|
## Overview
|
|
|
|
**The Core Principle:**
|
|
UI changes because APIs respond. Wait for the API response, not an arbitrary timeout.
|
|
|
|
**Traditional approach:**
|
|
```typescript
|
|
await page.click('button');
|
|
await page.waitForTimeout(3000); // Hope 3 seconds is enough
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
```
|
|
|
|
**Network-first approach:**
|
|
```typescript
|
|
const responsePromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/submit') && resp.ok()
|
|
);
|
|
await page.click('button');
|
|
await responsePromise; // Wait for actual response
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
```
|
|
|
|
**Result:** Deterministic tests that wait exactly as long as needed.
|
|
|
|
## The Problem
|
|
|
|
### Hard Waits Create Flakiness
|
|
|
|
```typescript
|
|
// ❌ The flaky test pattern
|
|
test('should submit form', async ({ page }) => {
|
|
await page.fill('#name', 'Test User');
|
|
await page.click('button[type="submit"]');
|
|
|
|
await page.waitForTimeout(2000); // Wait 2 seconds
|
|
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
**Why this fails:**
|
|
- **Fast network:** Wastes 1.5 seconds waiting
|
|
- **Slow network:** Not enough time, test fails
|
|
- **CI environment:** Slower than local, fails randomly
|
|
- **Under load:** API takes 3 seconds, test fails
|
|
|
|
**Result:** "Works on my machine" syndrome, flaky CI.
|
|
|
|
### The Timeout Escalation Trap
|
|
|
|
```typescript
|
|
// Developer sees flaky test
|
|
await page.waitForTimeout(2000); // Failed in CI
|
|
|
|
// Increases timeout
|
|
await page.waitForTimeout(5000); // Still fails sometimes
|
|
|
|
// Increases again
|
|
await page.waitForTimeout(10000); // Now it passes... slowly
|
|
|
|
// Problem: Now EVERY test waits 10 seconds
|
|
// Suite that took 5 minutes now takes 30 minutes
|
|
```
|
|
|
|
**Result:** Slow, still-flaky tests.
|
|
|
|
### Race Conditions
|
|
|
|
```typescript
|
|
// ❌ Navigate-then-wait race condition
|
|
test('should load dashboard data', async ({ page }) => {
|
|
await page.goto('/dashboard'); // Navigation starts
|
|
|
|
// Race condition! API might not have responded yet
|
|
await expect(page.locator('.data-table')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
**What happens:**
|
|
1. `goto()` starts navigation
|
|
2. Page loads HTML
|
|
3. JavaScript requests `/api/dashboard`
|
|
4. Test checks for `.data-table` BEFORE API responds
|
|
5. Test fails intermittently
|
|
|
|
**Result:** "Sometimes it works, sometimes it doesn't."
|
|
|
|
## The Solution: Intercept-Before-Navigate
|
|
|
|
### Wait for Response Before Asserting
|
|
|
|
```typescript
|
|
// ✅ Good: Network-first pattern
|
|
test('should load dashboard data', async ({ page }) => {
|
|
// Set up promise BEFORE navigation
|
|
const dashboardPromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/dashboard') && resp.ok()
|
|
);
|
|
|
|
// Navigate
|
|
await page.goto('/dashboard');
|
|
|
|
// Wait for API response
|
|
const response = await dashboardPromise;
|
|
const data = await response.json();
|
|
|
|
// Now assert UI
|
|
await expect(page.locator('.data-table')).toBeVisible();
|
|
await expect(page.locator('.data-table tr')).toHaveCount(data.items.length);
|
|
});
|
|
```
|
|
|
|
**Why this works:**
|
|
- Wait set up BEFORE navigation (no race)
|
|
- Wait for actual API response (deterministic)
|
|
- No fixed timeout (fast when API is fast)
|
|
- Validates API response (catch backend errors)
|
|
|
|
**With Playwright Utils (Even Cleaner):**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
import { expect } from '@playwright/test';
|
|
|
|
test('should load dashboard data', async ({ page, interceptNetworkCall }) => {
|
|
// Set up interception BEFORE navigation
|
|
const dashboardCall = interceptNetworkCall({
|
|
method: 'GET',
|
|
url: '**/api/dashboard'
|
|
});
|
|
|
|
// Navigate
|
|
await page.goto('/dashboard');
|
|
|
|
// Wait for API response (automatic JSON parsing)
|
|
const { status, responseJson: data } = await dashboardCall;
|
|
|
|
// Validate API response
|
|
expect(status).toBe(200);
|
|
expect(data.items).toBeDefined();
|
|
|
|
// Assert UI matches API data
|
|
await expect(page.locator('.data-table')).toBeVisible();
|
|
await expect(page.locator('.data-table tr')).toHaveCount(data.items.length);
|
|
});
|
|
```
|
|
|
|
**Playwright Utils Benefits:**
|
|
- Automatic JSON parsing (no `await response.json()`)
|
|
- Returns `{ status, responseJson, requestJson }` structure
|
|
- Cleaner API (no need to check `resp.ok()`)
|
|
- Same intercept-before-navigate pattern
|
|
|
|
### Intercept-Before-Navigate Pattern
|
|
|
|
**Key insight:** Set up wait BEFORE triggering the action.
|
|
|
|
```typescript
|
|
// ✅ Pattern: Intercept → Action → Await
|
|
|
|
// 1. Intercept (set up wait)
|
|
const promise = page.waitForResponse(matcher);
|
|
|
|
// 2. Action (trigger request)
|
|
await page.click('button');
|
|
|
|
// 3. Await (wait for actual response)
|
|
await promise;
|
|
```
|
|
|
|
**Why this order:**
|
|
- `waitForResponse()` starts listening immediately
|
|
- Then trigger the action that makes the request
|
|
- Then wait for the promise to resolve
|
|
- No race condition possible
|
|
|
|
#### Intercept-Before-Navigate Flow
|
|
|
|
```mermaid
|
|
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
|
|
sequenceDiagram
|
|
participant Test
|
|
participant Playwright
|
|
participant Browser
|
|
participant API
|
|
|
|
rect rgb(200, 230, 201)
|
|
Note over Test,Playwright: ✅ CORRECT: Intercept First
|
|
Test->>Playwright: 1. waitForResponse(matcher)
|
|
Note over Playwright: Starts listening for response
|
|
Test->>Browser: 2. click('button')
|
|
Browser->>API: 3. POST /api/submit
|
|
API-->>Browser: 4. 200 OK {success: true}
|
|
Browser-->>Playwright: 5. Response captured
|
|
Test->>Playwright: 6. await promise
|
|
Playwright-->>Test: 7. Returns response
|
|
Note over Test: No race condition!
|
|
end
|
|
|
|
rect rgb(255, 205, 210)
|
|
Note over Test,API: ❌ WRONG: Action First
|
|
Test->>Browser: 1. click('button')
|
|
Browser->>API: 2. POST /api/submit
|
|
API-->>Browser: 3. 200 OK (already happened!)
|
|
Test->>Playwright: 4. waitForResponse(matcher)
|
|
Note over Test,Playwright: Too late - response already occurred
|
|
Note over Test: Race condition! Test hangs or fails
|
|
end
|
|
```
|
|
|
|
**Correct Order (Green):**
|
|
1. Set up listener (`waitForResponse`)
|
|
2. Trigger action (`click`)
|
|
3. Wait for response (`await promise`)
|
|
|
|
**Wrong Order (Red):**
|
|
1. Trigger action first
|
|
2. Set up listener too late
|
|
3. Response already happened - missed!
|
|
|
|
## How It Works in TEA
|
|
|
|
### TEA Generates Network-First Tests
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// When you run *atdd or *automate, TEA generates:
|
|
|
|
test('should create user', async ({ page }) => {
|
|
// TEA automatically includes network wait
|
|
const createUserPromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/users') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.ok()
|
|
);
|
|
|
|
await page.fill('#name', 'Test User');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const response = await createUserPromise;
|
|
const user = await response.json();
|
|
|
|
// Validate both API and UI
|
|
expect(user.id).toBeDefined();
|
|
await expect(page.locator('.success')).toContainText(user.name);
|
|
});
|
|
```
|
|
|
|
**With Playwright Utils (if `tea_use_playwright_utils: true`):**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
import { expect } from '@playwright/test';
|
|
|
|
test('should create user', async ({ page, interceptNetworkCall }) => {
|
|
// TEA uses interceptNetworkCall for cleaner interception
|
|
const createUserCall = interceptNetworkCall({
|
|
method: 'POST',
|
|
url: '**/api/users'
|
|
});
|
|
|
|
await page.getByLabel('Name').fill('Test User');
|
|
await page.getByRole('button', { name: 'Submit' }).click();
|
|
|
|
// Wait for response (automatic JSON parsing)
|
|
const { status, responseJson: user } = await createUserCall;
|
|
|
|
// Validate both API and UI
|
|
expect(status).toBe(201);
|
|
expect(user.id).toBeDefined();
|
|
await expect(page.locator('.success')).toContainText(user.name);
|
|
});
|
|
```
|
|
|
|
**Playwright Utils Benefits:**
|
|
- Automatic JSON parsing (`responseJson` ready to use)
|
|
- No manual `await response.json()`
|
|
- Returns `{ status, responseJson }` structure
|
|
- Cleaner, more readable code
|
|
|
|
### TEA Reviews for Hard Waits
|
|
|
|
When you run `*test-review`:
|
|
|
|
```markdown
|
|
## Critical Issue: Hard Wait Detected
|
|
|
|
**File:** tests/e2e/submit.spec.ts:45
|
|
**Issue:** Using `page.waitForTimeout(3000)`
|
|
**Severity:** Critical (causes flakiness)
|
|
|
|
**Current Code:**
|
|
```typescript
|
|
await page.click('button');
|
|
await page.waitForTimeout(3000); // ❌
|
|
```
|
|
|
|
**Fix:**
|
|
```typescript
|
|
const responsePromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/submit') && resp.ok()
|
|
);
|
|
await page.click('button');
|
|
await responsePromise; // ✅
|
|
```
|
|
|
|
**Why:** Hard waits are non-deterministic. Use network-first patterns.
|
|
```
|
|
|
|
## Pattern Variations
|
|
|
|
### Basic Response Wait
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// Wait for any successful response
|
|
const promise = page.waitForResponse(resp => resp.ok());
|
|
await page.click('button');
|
|
await promise;
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
|
|
test('basic wait', async ({ page, interceptNetworkCall }) => {
|
|
const responseCall = interceptNetworkCall({ url: '**' }); // Match any
|
|
await page.click('button');
|
|
const { status } = await responseCall;
|
|
expect(status).toBe(200);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Specific URL Match
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// Wait for specific endpoint
|
|
const promise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/users/123')
|
|
);
|
|
await page.goto('/user/123');
|
|
await promise;
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
test('specific URL', async ({ page, interceptNetworkCall }) => {
|
|
const userCall = interceptNetworkCall({ url: '**/api/users/123' });
|
|
await page.goto('/user/123');
|
|
const { status, responseJson } = await userCall;
|
|
expect(status).toBe(200);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Method + Status Match
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// Wait for POST that returns 201
|
|
const promise = page.waitForResponse(
|
|
resp =>
|
|
resp.url().includes('/api/users') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.status() === 201
|
|
);
|
|
await page.click('button[type="submit"]');
|
|
await promise;
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
test('method and status', async ({ page, interceptNetworkCall }) => {
|
|
const createCall = interceptNetworkCall({
|
|
method: 'POST',
|
|
url: '**/api/users'
|
|
});
|
|
await page.click('button[type="submit"]');
|
|
const { status, responseJson } = await createCall;
|
|
expect(status).toBe(201); // Explicit status check
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Multiple Responses
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// Wait for multiple API calls
|
|
const [usersResp, postsResp] = await Promise.all([
|
|
page.waitForResponse(resp => resp.url().includes('/api/users')),
|
|
page.waitForResponse(resp => resp.url().includes('/api/posts')),
|
|
page.goto('/dashboard') // Triggers both requests
|
|
]);
|
|
|
|
const users = await usersResp.json();
|
|
const posts = await postsResp.json();
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
test('multiple responses', async ({ page, interceptNetworkCall }) => {
|
|
const usersCall = interceptNetworkCall({ url: '**/api/users' });
|
|
const postsCall = interceptNetworkCall({ url: '**/api/posts' });
|
|
|
|
await page.goto('/dashboard'); // Triggers both
|
|
|
|
const [{ responseJson: users }, { responseJson: posts }] = await Promise.all([
|
|
usersCall,
|
|
postsCall
|
|
]);
|
|
|
|
expect(users).toBeInstanceOf(Array);
|
|
expect(posts).toBeInstanceOf(Array);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### Validate Response Data
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
// Verify API response before asserting UI
|
|
const promise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/checkout') && resp.ok()
|
|
);
|
|
|
|
await page.click('button:has-text("Complete Order")');
|
|
|
|
const response = await promise;
|
|
const order = await response.json();
|
|
|
|
// Response validation
|
|
expect(order.status).toBe('confirmed');
|
|
expect(order.total).toBeGreaterThan(0);
|
|
|
|
// UI validation
|
|
await expect(page.locator('.order-confirmation')).toContainText(order.id);
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
test('validate response data', async ({ page, interceptNetworkCall }) => {
|
|
const checkoutCall = interceptNetworkCall({
|
|
method: 'POST',
|
|
url: '**/api/checkout'
|
|
});
|
|
|
|
await page.click('button:has-text("Complete Order")');
|
|
|
|
const { status, responseJson: order } = await checkoutCall;
|
|
|
|
// Response validation (automatic JSON parsing)
|
|
expect(status).toBe(200);
|
|
expect(order.status).toBe('confirmed');
|
|
expect(order.total).toBeGreaterThan(0);
|
|
|
|
// UI validation
|
|
await expect(page.locator('.order-confirmation')).toContainText(order.id);
|
|
});
|
|
```
|
|
|
|
## Advanced Patterns
|
|
|
|
### HAR Recording for Offline Testing
|
|
|
|
**Vanilla Playwright (Manual HAR Handling):**
|
|
|
|
```typescript
|
|
// First run: Record mode (saves HAR file)
|
|
test('offline testing - RECORD', async ({ page, context }) => {
|
|
// Record mode: Save network traffic to HAR
|
|
await context.routeFromHAR('./hars/dashboard.har', {
|
|
url: '**/api/**',
|
|
update: true // Update HAR file
|
|
});
|
|
|
|
await page.goto('/dashboard');
|
|
// All network traffic saved to dashboard.har
|
|
});
|
|
|
|
// Subsequent runs: Playback mode (uses saved HAR)
|
|
test('offline testing - PLAYBACK', async ({ page, context }) => {
|
|
// Playback mode: Use saved network traffic
|
|
await context.routeFromHAR('./hars/dashboard.har', {
|
|
url: '**/api/**',
|
|
update: false // Use existing HAR, no network calls
|
|
});
|
|
|
|
await page.goto('/dashboard');
|
|
// Uses recorded responses, no backend needed
|
|
});
|
|
```
|
|
|
|
**With Playwright Utils (Automatic HAR Management):**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
|
|
|
|
// Record mode: Set environment variable
|
|
process.env.PW_NET_MODE = 'record';
|
|
|
|
test('should work offline', async ({ page, context, networkRecorder }) => {
|
|
await networkRecorder.setup(context); // Handles HAR automatically
|
|
|
|
await page.goto('/dashboard');
|
|
await page.click('#add-item');
|
|
// All network traffic recorded, CRUD operations detected
|
|
});
|
|
```
|
|
|
|
**Switch to playback:**
|
|
```bash
|
|
# Playback mode (offline)
|
|
PW_NET_MODE=playback npx playwright test
|
|
# Uses HAR file, no backend needed!
|
|
```
|
|
|
|
**Playwright Utils Benefits:**
|
|
- Automatic HAR file management (naming, paths)
|
|
- CRUD operation detection (stateful mocking)
|
|
- Environment variable control (easy switching)
|
|
- Works for complex interactions (create, update, delete)
|
|
- No manual route configuration
|
|
|
|
### Network Request Interception
|
|
|
|
**Vanilla Playwright:**
|
|
```typescript
|
|
test('should handle API error', async ({ page }) => {
|
|
// Manual route setup
|
|
await page.route('**/api/users', (route) => {
|
|
route.fulfill({
|
|
status: 500,
|
|
body: JSON.stringify({ error: 'Internal server error' })
|
|
});
|
|
});
|
|
|
|
await page.goto('/users');
|
|
|
|
const response = await page.waitForResponse('**/api/users');
|
|
const error = await response.json();
|
|
|
|
expect(error.error).toContain('Internal server');
|
|
await expect(page.locator('.error-message')).toContainText('Server error');
|
|
});
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
|
|
test('should handle API error', async ({ page, interceptNetworkCall }) => {
|
|
// Stub API to return error (set up BEFORE navigation)
|
|
const usersCall = interceptNetworkCall({
|
|
method: 'GET',
|
|
url: '**/api/users',
|
|
fulfillResponse: {
|
|
status: 500,
|
|
body: { error: 'Internal server error' }
|
|
}
|
|
});
|
|
|
|
await page.goto('/users');
|
|
|
|
// Wait for mocked response and access parsed data
|
|
const { status, responseJson } = await usersCall;
|
|
|
|
expect(status).toBe(500);
|
|
expect(responseJson.error).toContain('Internal server');
|
|
await expect(page.locator('.error-message')).toContainText('Server error');
|
|
});
|
|
```
|
|
|
|
**Playwright Utils Benefits:**
|
|
- Automatic JSON parsing (`responseJson` ready to use)
|
|
- Returns promise with `{ status, responseJson, requestJson }`
|
|
- No need to pass `page` (auto-injected by fixture)
|
|
- Glob pattern matching (simpler than regex)
|
|
- Single declarative call (setup + wait in one)
|
|
|
|
## Comparison: Traditional vs Network-First
|
|
|
|
### Loading Dashboard Data
|
|
|
|
**Traditional (Flaky):**
|
|
```typescript
|
|
test('dashboard loads data', async ({ page }) => {
|
|
await page.goto('/dashboard');
|
|
await page.waitForTimeout(2000); // ❌ Magic number
|
|
await expect(page.locator('table tr')).toHaveCount(5);
|
|
});
|
|
```
|
|
|
|
**Failure modes:**
|
|
- API takes 2.5s → test fails
|
|
- API returns 3 items not 5 → hard to debug (which issue?)
|
|
- CI slower than local → fails in CI only
|
|
|
|
**Network-First (Deterministic):**
|
|
```typescript
|
|
test('dashboard loads data', async ({ page }) => {
|
|
const apiPromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/dashboard') && resp.ok()
|
|
);
|
|
|
|
await page.goto('/dashboard');
|
|
|
|
const response = await apiPromise;
|
|
const { items } = await response.json();
|
|
|
|
// Validate API response
|
|
expect(items).toHaveLength(5);
|
|
|
|
// Validate UI matches API
|
|
await expect(page.locator('table tr')).toHaveCount(items.length);
|
|
});
|
|
```
|
|
|
|
**Benefits:**
|
|
- Waits exactly as long as needed (100ms or 5s, doesn't matter)
|
|
- Validates API response (catch backend errors)
|
|
- Validates UI matches API (catch frontend bugs)
|
|
- Works in any environment (local, CI, staging)
|
|
|
|
**With Playwright Utils (Even Better):**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
|
|
test('dashboard loads data', async ({ page, interceptNetworkCall }) => {
|
|
const dashboardCall = interceptNetworkCall({
|
|
method: 'GET',
|
|
url: '**/api/dashboard'
|
|
});
|
|
|
|
await page.goto('/dashboard');
|
|
|
|
const { status, responseJson: { items } } = await dashboardCall;
|
|
|
|
// Validate API response (automatic JSON parsing)
|
|
expect(status).toBe(200);
|
|
expect(items).toHaveLength(5);
|
|
|
|
// Validate UI matches API
|
|
await expect(page.locator('table tr')).toHaveCount(items.length);
|
|
});
|
|
```
|
|
|
|
**Additional Benefits:**
|
|
- No manual `await response.json()` (automatic parsing)
|
|
- Cleaner destructuring of nested data
|
|
- Consistent API across all network calls
|
|
|
|
---
|
|
|
|
### Form Submission
|
|
|
|
**Traditional (Flaky):**
|
|
```typescript
|
|
test('form submission', async ({ page }) => {
|
|
await page.fill('#email', 'test@example.com');
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForTimeout(3000); // ❌ Hope it's enough
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
**Network-First (Deterministic):**
|
|
```typescript
|
|
test('form submission', async ({ page }) => {
|
|
const submitPromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/submit') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.ok()
|
|
);
|
|
|
|
await page.fill('#email', 'test@example.com');
|
|
await page.click('button[type="submit"]');
|
|
|
|
const response = await submitPromise;
|
|
const result = await response.json();
|
|
|
|
expect(result.success).toBe(true);
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
**With Playwright Utils:**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
|
|
test('form submission', async ({ page, interceptNetworkCall }) => {
|
|
const submitCall = interceptNetworkCall({
|
|
method: 'POST',
|
|
url: '**/api/submit'
|
|
});
|
|
|
|
await page.getByLabel('Email').fill('test@example.com');
|
|
await page.getByRole('button', { name: 'Submit' }).click();
|
|
|
|
const { status, responseJson: result } = await submitCall;
|
|
|
|
// Automatic JSON parsing, no manual await
|
|
expect(status).toBe(200);
|
|
expect(result.success).toBe(true);
|
|
await expect(page.locator('.success')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
**Progression:**
|
|
- Traditional: Hard waits (flaky)
|
|
- Network-First (Vanilla): waitForResponse (deterministic)
|
|
- Network-First (PW-Utils): interceptNetworkCall (deterministic + cleaner API)
|
|
|
|
---
|
|
|
|
## Common Misconceptions
|
|
|
|
### "I Already Use waitForSelector"
|
|
|
|
```typescript
|
|
// This is still a hard wait in disguise
|
|
await page.click('button');
|
|
await page.waitForSelector('.success', { timeout: 5000 });
|
|
```
|
|
|
|
**Problem:** Waiting for DOM, not for the API that caused DOM change.
|
|
|
|
**Better:**
|
|
```typescript
|
|
await page.waitForResponse(matcher); // Wait for root cause
|
|
await page.waitForSelector('.success'); // Then validate UI
|
|
```
|
|
|
|
### "My Tests Are Fast, Why Add Complexity?"
|
|
|
|
**Short-term:** Tests are fast locally
|
|
|
|
**Long-term problems:**
|
|
- Different environments (CI slower)
|
|
- Under load (API slower)
|
|
- Network variability (random)
|
|
- Scaling test suite (100 → 1000 tests)
|
|
|
|
**Network-first prevents these issues before they appear.**
|
|
|
|
### "Too Much Boilerplate"
|
|
|
|
**Problem:** `waitForResponse` is verbose, repeated in every test.
|
|
|
|
**Solution:** Use Playwright Utils `interceptNetworkCall` - built-in fixture that reduces boilerplate.
|
|
|
|
**Vanilla Playwright (Repetitive):**
|
|
```typescript
|
|
test('test 1', async ({ page }) => {
|
|
const promise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/submit') && resp.ok()
|
|
);
|
|
await page.click('button');
|
|
await promise;
|
|
});
|
|
|
|
test('test 2', async ({ page }) => {
|
|
const promise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/load') && resp.ok()
|
|
);
|
|
await page.click('button');
|
|
await promise;
|
|
});
|
|
// Repeated pattern in every test
|
|
```
|
|
|
|
**With Playwright Utils (Cleaner):**
|
|
```typescript
|
|
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
|
|
|
test('test 1', async ({ page, interceptNetworkCall }) => {
|
|
const submitCall = interceptNetworkCall({ url: '**/api/submit' });
|
|
await page.click('button');
|
|
const { status, responseJson } = await submitCall;
|
|
expect(status).toBe(200);
|
|
});
|
|
|
|
test('test 2', async ({ page, interceptNetworkCall }) => {
|
|
const loadCall = interceptNetworkCall({ url: '**/api/load' });
|
|
await page.click('button');
|
|
const { responseJson } = await loadCall;
|
|
// Automatic JSON parsing, cleaner API
|
|
});
|
|
```
|
|
|
|
**Benefits:**
|
|
- Less boilerplate (fixture handles complexity)
|
|
- Automatic JSON parsing
|
|
- Glob pattern matching (`**/api/**`)
|
|
- Consistent API across all tests
|
|
|
|
See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#intercept-network-call) for setup.
|
|
|
|
## Technical Implementation
|
|
|
|
For detailed network-first patterns, see the knowledge base:
|
|
- [Knowledge Base Index - Network & Reliability](/docs/reference/tea/knowledge-base.md)
|
|
- [Complete Knowledge Base Index](/docs/reference/tea/knowledge-base.md)
|
|
|
|
## Related Concepts
|
|
|
|
**Core TEA Concepts:**
|
|
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - Determinism requires network-first
|
|
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - High-risk features need reliable tests
|
|
|
|
**Technical Patterns:**
|
|
- [Fixture Architecture](/docs/explanation/tea/fixture-architecture.md) - Network utilities as fixtures
|
|
- [Knowledge Base System](/docs/explanation/tea/knowledge-base-system.md) - Network patterns in knowledge base
|
|
|
|
**Overview:**
|
|
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Network-first in workflows
|
|
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Why flakiness matters
|
|
|
|
## Practical Guides
|
|
|
|
**Workflow Guides:**
|
|
- [How to Run Test Review](/docs/how-to/workflows/run-test-review.md) - Review for hard waits
|
|
- [How to Run ATDD](/docs/how-to/workflows/run-atdd.md) - Generate network-first tests
|
|
- [How to Run Automate](/docs/how-to/workflows/run-automate.md) - Expand with network patterns
|
|
|
|
**Use-Case Guides:**
|
|
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Fix flaky legacy tests
|
|
|
|
**Customization:**
|
|
- [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md) - Network utilities (recorder, interceptor, error monitor)
|
|
|
|
## Reference
|
|
|
|
- [TEA Command Reference](/docs/reference/tea/commands.md) - All workflows use network-first
|
|
- [Knowledge Base Index](/docs/reference/tea/knowledge-base.md) - Network-first fragment
|
|
- [Glossary](/docs/reference/glossary/index.md#test-architect-tea-concepts) - Network-first pattern term
|
|
|
|
---
|
|
|
|
Generated with [BMad Method](https://bmad-method.org) - TEA (Test Architect)
|