Initial commit: Data Analysis application with FastAPI backend and Next.js frontend
This commit is contained in:
280
_bmad/bmm/testarch/knowledge/intercept-network-call.md
Normal file
280
_bmad/bmm/testarch/knowledge/intercept-network-call.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Intercept Network Call Utility
|
||||
|
||||
## Principle
|
||||
|
||||
Intercept network requests with a single declarative call that returns a Promise. Automatically parse JSON responses, support both spy (observe) and stub (mock) patterns, and use powerful glob pattern matching for URL filtering.
|
||||
|
||||
## Rationale
|
||||
|
||||
Vanilla Playwright's network interception requires multiple steps:
|
||||
|
||||
- `page.route()` to setup, `page.waitForResponse()` to capture
|
||||
- Manual JSON parsing
|
||||
- Verbose syntax for conditional handling
|
||||
- Complex filter predicates
|
||||
|
||||
The `interceptNetworkCall` utility provides:
|
||||
|
||||
- **Single declarative call**: Setup and wait in one statement
|
||||
- **Automatic JSON parsing**: Response pre-parsed, strongly typed
|
||||
- **Flexible URL patterns**: Glob matching with picomatch
|
||||
- **Spy or stub modes**: Observe real traffic or mock responses
|
||||
- **Concise API**: Reduces boilerplate by 60-70%
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Spy on Network (Observe Real Traffic)
|
||||
|
||||
**Context**: Capture and inspect real API responses for validation.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
|
||||
|
||||
test('should spy on users API', async ({ page, interceptNetworkCall }) => {
|
||||
// Setup interception BEFORE navigation
|
||||
const usersCall = interceptNetworkCall({
|
||||
url: '**/api/users', // Glob pattern
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Wait for response and access parsed data
|
||||
const { responseJson, status } = await usersCall;
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(responseJson).toHaveLength(10);
|
||||
expect(responseJson[0]).toHaveProperty('name');
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Intercept before navigation (critical for race-free tests)
|
||||
- Returns Promise with `{ responseJson, status, requestBody }`
|
||||
- Glob patterns (`**` matches any path segment)
|
||||
- JSON automatically parsed
|
||||
|
||||
### Example 2: Stub Network (Mock Response)
|
||||
|
||||
**Context**: Mock API responses for testing UI behavior without backend.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test('should stub users API', async ({ page, interceptNetworkCall }) => {
|
||||
const mockUsers = [
|
||||
{ id: 1, name: 'Test User 1' },
|
||||
{ id: 2, name: 'Test User 2' },
|
||||
];
|
||||
|
||||
const usersCall = interceptNetworkCall({
|
||||
url: '**/api/users',
|
||||
fulfillResponse: {
|
||||
status: 200,
|
||||
body: mockUsers,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await usersCall;
|
||||
|
||||
// UI shows mocked data
|
||||
await expect(page.getByText('Test User 1')).toBeVisible();
|
||||
await expect(page.getByText('Test User 2')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- `fulfillResponse` mocks the API
|
||||
- No backend needed
|
||||
- Test UI logic in isolation
|
||||
- Status code and body fully controllable
|
||||
|
||||
### Example 3: Conditional Response Handling
|
||||
|
||||
**Context**: Different responses based on request method or parameters.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test('conditional mocking', async ({ page, interceptNetworkCall }) => {
|
||||
await interceptNetworkCall({
|
||||
url: '**/api/data',
|
||||
handler: async (route, request) => {
|
||||
if (request.method() === 'POST') {
|
||||
// Mock POST success
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
body: JSON.stringify({ id: 'new-id', success: true }),
|
||||
});
|
||||
} else if (request.method() === 'GET') {
|
||||
// Mock GET with data
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify([{ id: 1, name: 'Item' }]),
|
||||
});
|
||||
} else {
|
||||
// Let other methods through
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/data-page');
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- `handler` function for complex logic
|
||||
- Access full `route` and `request` objects
|
||||
- Can mock, continue, or abort
|
||||
- Flexible for advanced scenarios
|
||||
|
||||
### Example 4: Error Simulation
|
||||
|
||||
**Context**: Testing error handling in UI when API fails.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
|
||||
// Simulate 500 error
|
||||
const errorCall = interceptNetworkCall({
|
||||
url: '**/api/users',
|
||||
fulfillResponse: {
|
||||
status: 500,
|
||||
body: { error: 'Internal Server Error' },
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await errorCall;
|
||||
|
||||
// Verify UI shows error state
|
||||
await expect(page.getByText('Failed to load users')).toBeVisible();
|
||||
await expect(page.getByTestId('retry-button')).toBeVisible();
|
||||
});
|
||||
|
||||
// Simulate network timeout
|
||||
test('should handle timeout', async ({ page, interceptNetworkCall }) => {
|
||||
await interceptNetworkCall({
|
||||
url: '**/api/slow',
|
||||
handler: async (route) => {
|
||||
// Never respond - simulates timeout
|
||||
await new Promise(() => {});
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/slow-page');
|
||||
|
||||
// UI should show timeout error
|
||||
await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Mock error statuses (4xx, 5xx)
|
||||
- Test timeout scenarios
|
||||
- Validate error UI states
|
||||
- No real failures needed
|
||||
|
||||
### Example 5: Multiple Intercepts (Order Matters!)
|
||||
|
||||
**Context**: Intercepting different endpoints in same test - setup order is critical.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
|
||||
// ✅ CORRECT: Setup all intercepts BEFORE navigation
|
||||
const usersCall = interceptNetworkCall({ url: '**/api/users' });
|
||||
const productsCall = interceptNetworkCall({ url: '**/api/products' });
|
||||
const ordersCall = interceptNetworkCall({ url: '**/api/orders' });
|
||||
|
||||
// THEN navigate
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Wait for all (or specific ones)
|
||||
const [users, products] = await Promise.all([usersCall, productsCall]);
|
||||
|
||||
expect(users.responseJson).toHaveLength(10);
|
||||
expect(products.responseJson).toHaveLength(50);
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Setup all intercepts before triggering actions
|
||||
- Use `Promise.all()` to wait for multiple calls
|
||||
- Order: intercept → navigate → await
|
||||
- Prevents race conditions
|
||||
|
||||
## URL Pattern Matching
|
||||
|
||||
**Supported glob patterns:**
|
||||
|
||||
```typescript
|
||||
'**/api/users'; // Any path ending with /api/users
|
||||
'/api/users'; // Exact match
|
||||
'**/users/*'; // Any users sub-path
|
||||
'**/api/{users,products}'; // Either users or products
|
||||
'**/api/users?id=*'; // With query params
|
||||
```
|
||||
|
||||
**Uses picomatch library** - same pattern syntax as Playwright's `page.route()` but cleaner API.
|
||||
|
||||
## Comparison with Vanilla Playwright
|
||||
|
||||
| Vanilla Playwright | intercept-network-call |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `await page.route('/api/users', route => route.continue())` | `const call = interceptNetworkCall({ url: '**/api/users' })` |
|
||||
| `const resp = await page.waitForResponse('/api/users')` | (Combined in single statement) |
|
||||
| `const json = await resp.json()` | `const { responseJson } = await call` |
|
||||
| `const status = resp.status()` | `const { status } = await call` |
|
||||
| Complex filter predicates | Simple glob patterns |
|
||||
|
||||
**Reduction:** ~5-7 lines → ~2-3 lines per interception
|
||||
|
||||
## Related Fragments
|
||||
|
||||
- `network-first.md` - Core pattern: intercept before navigate
|
||||
- `network-recorder.md` - HAR-based offline testing
|
||||
- `overview.md` - Fixture composition basics
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
**❌ Intercepting after navigation:**
|
||||
|
||||
```typescript
|
||||
await page.goto('/dashboard'); // Navigation starts
|
||||
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
|
||||
```
|
||||
|
||||
**✅ Intercept before navigate:**
|
||||
|
||||
```typescript
|
||||
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
|
||||
await page.goto('/dashboard'); // Then navigate
|
||||
const { responseJson } = await usersCall; // Then await
|
||||
```
|
||||
|
||||
**❌ Ignoring the returned Promise:**
|
||||
|
||||
```typescript
|
||||
interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
|
||||
await page.goto('/dashboard');
|
||||
// No deterministic wait - race condition
|
||||
```
|
||||
|
||||
**✅ Always await the intercept:**
|
||||
|
||||
```typescript
|
||||
const usersCall = interceptNetworkCall({ url: '**/api/users' });
|
||||
await page.goto('/dashboard');
|
||||
await usersCall; // Deterministic wait
|
||||
```
|
||||
Reference in New Issue
Block a user