test-proxy-recorder 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -524
- package/dist/index.cjs +95 -77
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.mjs +95 -77
- package/dist/playwright/index.cjs +7 -8
- package/dist/playwright/index.mjs +7 -8
- package/dist/proxy.js +114 -84
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -3,58 +3,36 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/test-proxy-recorder)
|
|
4
4
|
[](https://github.com/asmyshlyaev177/test-proxy-recorder/blob/master/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Fast, deterministic Playwright tests without maintaining manual mocks.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
An HTTP proxy that records real API responses during test runs and replays them on CI -- no backend required. Instead of hand-writing mock fixtures, just run your tests once against the real API and commit the recordings. Supports Next.js and SSR.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **Fast CI/CD Tests**: Record API responses once with real backend, replay them on CI/CD without backend
|
|
13
|
-
- **Fast Workflow**: Record real interactions with API instead of mocking every request manually
|
|
14
|
-
- **Server Side Rendering**: Can record SSR requests from JS frameworks like Next.js
|
|
15
|
-
- **Deterministic Tests**: Same responses every time, no flaky network issues, no need to wire up the whole Backend API for testing
|
|
16
|
-
- **WebSocket Support**: Records and replays WebSocket connections
|
|
17
|
-
|
|
18
|
-
## Table of Contents
|
|
19
|
-
|
|
20
|
-
- [How It Works](#how-it-works)
|
|
21
|
-
- [Complete Setup Guide](#complete-setup-guide)
|
|
22
|
-
- [CLI Usage](#cli-usage)
|
|
23
|
-
- [Playwright Integration](#playwright-integration)
|
|
24
|
-
- [Next.js Integration](#nextjs-integration)
|
|
25
|
-
- [Control Endpoint](#control-endpoint)
|
|
26
|
-
- [Typical Workflow](#typical-workflow)
|
|
27
|
-
- [Recording Format](#recording-format)
|
|
28
|
-
- [Troubleshooting](#troubleshooting)
|
|
29
|
-
- [API Reference](#api-reference)
|
|
30
|
-
|
|
31
|
-
## How It Works
|
|
32
|
-
|
|
33
|
-
The proxy server runs continuously and can switch between three modes per test:
|
|
34
|
-
|
|
35
|
-
### 1. Transparent Mode (Default)
|
|
36
|
-
|
|
37
|
-
Passes requests through to the backend without recording or replaying.
|
|
38
|
-
|
|
39
|
-
### 2. Record Mode
|
|
10
|
+
```
|
|
11
|
+
Record mode Replay mode
|
|
40
12
|
|
|
41
|
-
|
|
13
|
+
Browser/App ──> Proxy ──> Real API Browser/App ──> Proxy ──> Disk
|
|
14
|
+
│ │
|
|
15
|
+
└──> saves to disk └──> serves saved responses
|
|
16
|
+
(.mock.json) (.mock.json)
|
|
17
|
+
```
|
|
42
18
|
|
|
43
|
-
|
|
19
|
+
## Why
|
|
44
20
|
|
|
45
|
-
|
|
21
|
+
- **No backend on CI** -- record once against the real API, replay on every CI run
|
|
22
|
+
- **No manual mocks** -- capture real interactions instead of hand-writing fixtures
|
|
23
|
+
- **SSR support** -- records server-side requests from Next.js and similar frameworks
|
|
24
|
+
- **Deterministic** -- same responses every time, no flaky network
|
|
25
|
+
- **WebSocket support** -- records and replays WebSocket connections
|
|
46
26
|
|
|
47
|
-
##
|
|
27
|
+
## Quick Start
|
|
48
28
|
|
|
49
|
-
###
|
|
29
|
+
### 1. Install
|
|
50
30
|
|
|
51
31
|
```bash
|
|
52
32
|
npm install --save-dev test-proxy-recorder
|
|
53
33
|
```
|
|
54
34
|
|
|
55
|
-
###
|
|
56
|
-
|
|
57
|
-
Add to `package.json`:
|
|
35
|
+
### 2. Add scripts to `package.json`
|
|
58
36
|
|
|
59
37
|
```json
|
|
60
38
|
{
|
|
@@ -64,641 +42,319 @@ Add to `package.json`:
|
|
|
64
42
|
}
|
|
65
43
|
```
|
|
66
44
|
|
|
67
|
-
**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
**CRITICAL**: Recordings must be committed to git for CI/CD replay.
|
|
85
|
-
|
|
86
|
-
Create or update your `.gitattributes` file:
|
|
87
|
-
|
|
88
|
-
```gitattributes
|
|
89
|
-
/e2e/recordings/** binary
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
This marks recording files as binary, which causes long mock files to be collapsed/folded in Pull Request diffs for better readability.
|
|
93
|
-
|
|
94
|
-
**DO NOT** add `e2e/recordings` to `.gitignore`. Recordings need to be versioned in git for CI/CD to use them.
|
|
95
|
-
|
|
96
|
-
**Note**: The recordings directory will be created automatically when you first record a test - no need to create it manually.
|
|
97
|
-
|
|
98
|
-
### Step 4: Create Playwright Global Teardown (Recommended)
|
|
99
|
-
|
|
100
|
-
Create `e2e/global-teardown.ts`:
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { playwrightProxy } from 'test-proxy-recorder';
|
|
104
|
-
|
|
105
|
-
async function globalTeardown() {
|
|
106
|
-
await playwrightProxy.teardown();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export default globalTeardown;
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Update `playwright.config.ts`:
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { defineConfig } from '@playwright/test';
|
|
116
|
-
|
|
117
|
-
export default defineConfig({
|
|
118
|
-
testDir: './e2e',
|
|
119
|
-
globalTeardown: './e2e/global-teardown.ts',
|
|
120
|
-
// ... rest of config
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Step 5: Create Example Test
|
|
125
|
-
|
|
126
|
-
Create `e2e/example.spec.ts`:
|
|
45
|
+
> **Tip:** Use `concurrently` to run proxy + app together.
|
|
46
|
+
> `INTERNAL_API_URL` is the env var your app uses for the API base URL -- point it at the proxy instead of the real backend. Use proxy address for dev/test and real backend for production environment.
|
|
47
|
+
> Replace it with whatever env var your app uses (e.g. `API_URL`, `NEXT_PUBLIC_API_URL`).
|
|
48
|
+
>
|
|
49
|
+
> ```json
|
|
50
|
+
> {
|
|
51
|
+
> "scripts": {
|
|
52
|
+
> "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings",
|
|
53
|
+
> "dev:proxy": "concurrently \"npm run proxy\" \"INTERNAL_API_URL=http://localhost:8100 npm run dev\"",
|
|
54
|
+
> "serve:proxy": "concurrently \"npm run proxy\" \"INTERNAL_API_URL=http://localhost:8100 npm run serve\""
|
|
55
|
+
> }
|
|
56
|
+
> }
|
|
57
|
+
> ```
|
|
58
|
+
>
|
|
59
|
+
> **Next.js note:** Prefer `build` + `serve` over `dev` for recording/replaying tests. The Next.js dev server is slow and can cause timeouts or flaky recordings.
|
|
60
|
+
|
|
61
|
+
### 3. Write a test
|
|
127
62
|
|
|
128
63
|
```typescript
|
|
129
64
|
import { test, expect } from '@playwright/test';
|
|
130
65
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
131
66
|
|
|
132
|
-
test('
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
await playwrightProxy.before(page, testInfo, 'replay');
|
|
67
|
+
test('homepage loads', async ({ page }, testInfo) => {
|
|
68
|
+
await playwrightProxy.before(page, testInfo, 'record'); // first run: record
|
|
69
|
+
// await playwrightProxy.before(page, testInfo, 'replay'); // later: replay
|
|
136
70
|
|
|
137
71
|
await page.goto('/');
|
|
138
72
|
await expect(page.getByText('Welcome')).toBeVisible();
|
|
139
73
|
});
|
|
140
74
|
```
|
|
141
75
|
|
|
142
|
-
###
|
|
76
|
+
### 4. Run
|
|
143
77
|
|
|
144
|
-
|
|
78
|
+
Start the proxy + app first (e.g. `npm run serve:proxy`), then run tests in a separate terminal:
|
|
145
79
|
|
|
146
|
-
```
|
|
147
|
-
|
|
80
|
+
```bash
|
|
81
|
+
# Terminal 1 -- start proxy and app
|
|
82
|
+
npm run serve:proxy
|
|
83
|
+
|
|
84
|
+
# Terminal 2 -- run tests
|
|
85
|
+
npx playwright test
|
|
148
86
|
```
|
|
149
87
|
|
|
150
|
-
|
|
88
|
+
### 5. Commit recordings to git
|
|
151
89
|
|
|
152
|
-
```
|
|
153
|
-
|
|
90
|
+
```bash
|
|
91
|
+
# .gitattributes -- collapse long mock files in PR diffs
|
|
92
|
+
/e2e/recordings/** binary
|
|
154
93
|
```
|
|
155
94
|
|
|
156
|
-
|
|
95
|
+
> Do **not** add `e2e/recordings` to `.gitignore`. Recordings must be in git for CI replay.
|
|
157
96
|
|
|
158
|
-
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## CLI
|
|
159
100
|
|
|
160
101
|
```bash
|
|
161
102
|
test-proxy-recorder <target-url> [options]
|
|
162
103
|
```
|
|
163
104
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
- `--help, -h` - Show help
|
|
170
|
-
|
|
171
|
-
### Examples
|
|
105
|
+
| Option | Default | Description |
|
|
106
|
+
| -------------- | -------------- | ----------------------------- |
|
|
107
|
+
| `<target-url>` | *(required)* | Backend URL to proxy |
|
|
108
|
+
| `--port, -p` | `8080` | Proxy listen port |
|
|
109
|
+
| `--dir, -d` | `./recordings` | Directory for recording files |
|
|
172
110
|
|
|
173
111
|
```bash
|
|
174
|
-
#
|
|
112
|
+
# Examples
|
|
175
113
|
test-proxy-recorder http://localhost:8000
|
|
176
|
-
|
|
177
|
-
# Custom port and recordings directory
|
|
178
114
|
test-proxy-recorder http://localhost:8000 --port 8100 --dir ./mocks
|
|
179
|
-
|
|
180
|
-
# Multiple targets (experimental)
|
|
181
|
-
test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
|
|
182
115
|
```
|
|
183
116
|
|
|
184
117
|
## Playwright Integration
|
|
185
118
|
|
|
186
|
-
###
|
|
187
|
-
|
|
188
|
-
The proxy uses a **custom HTTP header** (`x-test-rcrd-id`) to identify recording sessions. This header is automatically set by the `playwrightProxy.before()` method and works seamlessly with Next.js and other server-side rendering frameworks.
|
|
189
|
-
|
|
190
|
-
**Cookie fallback**: For backward compatibility, the proxy also supports cookie-based session identification, but the custom header is preferred.
|
|
191
|
-
|
|
192
|
-
### Basic Test Structure
|
|
193
|
-
|
|
194
|
-
Every test using the proxy should follow this pattern:
|
|
119
|
+
### Basic pattern
|
|
195
120
|
|
|
196
121
|
```typescript
|
|
197
122
|
import { test } from '@playwright/test';
|
|
198
123
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
199
124
|
|
|
200
|
-
test('test
|
|
201
|
-
// Set mode BEFORE test actions
|
|
202
|
-
// This automatically sets the recording ID header and cleanup handler
|
|
125
|
+
test('my test', async ({ page }, testInfo) => {
|
|
203
126
|
await playwrightProxy.before(page, testInfo, 'replay');
|
|
204
|
-
|
|
205
|
-
// Test code
|
|
206
|
-
await page.goto('/page');
|
|
207
|
-
// Test assertions...
|
|
127
|
+
// ... test code
|
|
208
128
|
});
|
|
209
129
|
```
|
|
210
130
|
|
|
211
|
-
|
|
131
|
+
`playwrightProxy.before()` sets the proxy mode, attaches a session header (`x-test-rcrd-id`), and registers cleanup on page close. Recording filenames are derived from test names (`"create a user"` -> `create-a-user.mock.json`).
|
|
212
132
|
|
|
213
|
-
|
|
214
|
-
import { test } from '@playwright/test';
|
|
215
|
-
import { playwrightProxy } from 'test-proxy-recorder';
|
|
216
|
-
|
|
217
|
-
// Recording mode - captures API responses
|
|
218
|
-
test('create user', async ({ page }, testInfo) => {
|
|
219
|
-
await playwrightProxy.before(page, testInfo, 'record');
|
|
220
|
-
|
|
221
|
-
await page.goto('/users/new');
|
|
222
|
-
await page.fill('[name="username"]', 'testuser');
|
|
223
|
-
await page.click('button[type="submit"]');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Replay mode - uses recorded responses
|
|
227
|
-
test('create user', async ({ page }, testInfo) => {
|
|
228
|
-
await playwrightProxy.before(page, testInfo, 'replay');
|
|
229
|
-
|
|
230
|
-
await page.goto('/users/new');
|
|
231
|
-
await page.fill('[name="username"]', 'testuser');
|
|
232
|
-
await page.click('button[type="submit"]');
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### Test Naming
|
|
237
|
-
|
|
238
|
-
Recording files are auto-generated from test names:
|
|
239
|
-
|
|
240
|
-
- Test: `"create a user"`
|
|
241
|
-
- File: `create-a-user.mock.json`
|
|
242
|
-
|
|
243
|
-
**Important**: Keep test names stable for replay to work correctly.
|
|
244
|
-
|
|
245
|
-
### Global Teardown (Recommended)
|
|
246
|
-
|
|
247
|
-
Create `e2e/global-teardown.ts`:
|
|
133
|
+
### Global teardown (recommended)
|
|
248
134
|
|
|
249
135
|
```typescript
|
|
136
|
+
// e2e/global-teardown.ts
|
|
250
137
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
251
138
|
|
|
252
|
-
async function globalTeardown() {
|
|
139
|
+
export default async function globalTeardown() {
|
|
253
140
|
await playwrightProxy.teardown();
|
|
254
141
|
}
|
|
255
|
-
|
|
256
|
-
export default globalTeardown;
|
|
257
142
|
```
|
|
258
143
|
|
|
259
|
-
Update `playwright.config.ts`:
|
|
260
|
-
|
|
261
144
|
```typescript
|
|
262
|
-
|
|
263
|
-
|
|
145
|
+
// playwright.config.ts
|
|
264
146
|
export default defineConfig({
|
|
265
|
-
testDir: './e2e',
|
|
266
147
|
globalTeardown: './e2e/global-teardown.ts',
|
|
267
|
-
// ... rest of config
|
|
268
148
|
});
|
|
269
149
|
```
|
|
270
150
|
|
|
271
|
-
### Client-
|
|
272
|
-
|
|
273
|
-
For applications that make client-side requests to 3rd party services (e.g., AWS Cognito, Stream.io, analytics services), you can use client-side recording to capture these requests directly in the browser using Playwright's HAR (HTTP Archive) format.
|
|
151
|
+
### Client-side recording (3rd party APIs)
|
|
274
152
|
|
|
275
|
-
|
|
276
|
-
- Server-side proxy cannot intercept requests made directly from the browser to external services
|
|
277
|
-
- HAR files are a standard format supported by Playwright and browser dev tools
|
|
278
|
-
- Automatically handles CORS and other browser-specific request behaviors
|
|
279
|
-
|
|
280
|
-
**Example:**
|
|
153
|
+
For browser-side requests that don't go through the proxy (e.g. AWS Cognito, analytics), use HAR recording:
|
|
281
154
|
|
|
282
155
|
```typescript
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
test('authentication flow', async ({ page }, testInfo) => {
|
|
287
|
-
// Record both server-side (via proxy) and client-side (via HAR) requests
|
|
288
|
-
await playwrightProxy.before(
|
|
289
|
-
page,
|
|
290
|
-
testInfo,
|
|
291
|
-
'replay',
|
|
292
|
-
{
|
|
293
|
-
// Client-side URL pattern using Playwright's format
|
|
294
|
-
url: /cognito-.*amazonaws\.com|\.stream-io-api\.com/,
|
|
295
|
-
timeout: 60000 // Optional: custom timeout
|
|
296
|
-
}
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
await page.goto('/login');
|
|
300
|
-
// Cognito authentication requests are recorded to HAR files
|
|
301
|
-
await page.fill('[name="email"]', 'user@example.com');
|
|
302
|
-
await page.click('button[type="submit"]');
|
|
156
|
+
await playwrightProxy.before(page, testInfo, 'replay', {
|
|
157
|
+
url: /cognito-.*amazonaws\.com|\.stream-io-api\.com/,
|
|
303
158
|
});
|
|
304
159
|
```
|
|
305
160
|
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
// RegExp pattern (recommended for multiple domains)
|
|
309
|
-
{ url: /cognito-.*amazonaws\.com|\.stream-io-api\.com/ }
|
|
310
|
-
|
|
311
|
-
// String glob pattern
|
|
312
|
-
{ url: 'https://api.example.com/**' }
|
|
313
|
-
|
|
314
|
-
// Specific domain
|
|
315
|
-
{ url: /api\.external-service\.com/ }
|
|
316
|
-
```
|
|
161
|
+
Recordings are stored alongside server-side files:
|
|
317
162
|
|
|
318
|
-
**Storage:**
|
|
319
|
-
Client-side recordings are stored as HAR files alongside server-side recordings:
|
|
320
163
|
```
|
|
321
164
|
e2e/recordings/
|
|
322
|
-
|
|
323
|
-
|
|
165
|
+
my-test.mock.json # server-side (proxy)
|
|
166
|
+
my-test.har # client-side (HAR)
|
|
324
167
|
```
|
|
325
168
|
|
|
326
|
-
**Recording vs Replay:**
|
|
327
|
-
- **Record mode**: Creates/updates HAR file with actual responses from 3rd party services
|
|
328
|
-
- **Replay mode**: Uses recorded HAR file, no network requests made to 3rd party services
|
|
329
|
-
|
|
330
|
-
**Note:** The recordings directory is automatically retrieved from the proxy server, ensuring both server-side and client-side recordings are stored in the same location.
|
|
331
|
-
|
|
332
169
|
## Next.js Integration
|
|
333
170
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
### Option 1: Using Next.js Middleware (Recommended)
|
|
171
|
+
The proxy identifies sessions via a custom header. For SSR requests to carry this header, use one of:
|
|
337
172
|
|
|
338
|
-
|
|
173
|
+
### Middleware (recommended)
|
|
339
174
|
|
|
340
175
|
```typescript
|
|
176
|
+
// middleware.ts
|
|
341
177
|
import { NextResponse } from 'next/server';
|
|
342
178
|
import type { NextRequest } from 'next/server';
|
|
343
179
|
import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
|
|
344
180
|
|
|
345
181
|
export function middleware(request: NextRequest) {
|
|
346
182
|
const response = NextResponse.next();
|
|
347
|
-
|
|
348
|
-
// Forward the recording ID header during tests
|
|
349
|
-
// Only runs in non-production or when TEST_PROXY_RECORDER_ENABLED=true
|
|
350
|
-
setNextProxyHeaders(request, response);
|
|
351
|
-
|
|
183
|
+
setNextProxyHeaders(request, response); // no-op in production
|
|
352
184
|
return response;
|
|
353
185
|
}
|
|
354
186
|
```
|
|
355
187
|
|
|
356
|
-
|
|
357
|
-
- Automatically skipped when `NODE_ENV=production`
|
|
358
|
-
- Can be explicitly enabled in production with `TEST_PROXY_RECORDER_ENABLED=true`
|
|
359
|
-
|
|
360
|
-
### Option 2: Manual Header Forwarding in API Routes
|
|
361
|
-
|
|
362
|
-
For API routes or server components, manually include the header in fetch requests:
|
|
188
|
+
### Manual header forwarding
|
|
363
189
|
|
|
364
190
|
```typescript
|
|
365
|
-
// app/api/data/route.ts
|
|
366
191
|
import { headers } from 'next/headers';
|
|
367
192
|
import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
|
|
368
193
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
'Content-Type': 'application/json',
|
|
375
|
-
})
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
return Response.json(await response.json());
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### Option 3: Using getRecordingId Helper
|
|
383
|
-
|
|
384
|
-
For more control, extract the recording ID and use it manually:
|
|
385
|
-
|
|
386
|
-
```typescript
|
|
387
|
-
import { headers } from 'next/headers';
|
|
388
|
-
import { getRecordingId, RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
|
|
389
|
-
|
|
390
|
-
export async function GET() {
|
|
391
|
-
const recordingId = getRecordingId(await headers());
|
|
392
|
-
|
|
393
|
-
const response = await fetch('http://localhost:8100/api/data', {
|
|
394
|
-
headers: {
|
|
395
|
-
'Content-Type': 'application/json',
|
|
396
|
-
...(recordingId && { [RECORDING_ID_HEADER]: recordingId })
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
return Response.json(await response.json());
|
|
401
|
-
}
|
|
194
|
+
const res = await fetch('http://localhost:8100/api/data', {
|
|
195
|
+
headers: createHeadersWithRecordingId(await headers(), {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
402
199
|
```
|
|
403
200
|
|
|
404
201
|
## Control Endpoint
|
|
405
202
|
|
|
406
|
-
The proxy exposes
|
|
203
|
+
The proxy exposes `/__control` for programmatic mode switching.
|
|
407
204
|
|
|
408
|
-
### GET - Retrieve Proxy Configuration
|
|
409
|
-
|
|
410
|
-
Get the current proxy configuration including recordings directory, mode, and active session ID.
|
|
411
|
-
|
|
412
|
-
**Via HTTP:**
|
|
413
205
|
```bash
|
|
206
|
+
# Get current state
|
|
414
207
|
curl http://localhost:8100/__control
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
**Response:**
|
|
418
|
-
```json
|
|
419
|
-
{
|
|
420
|
-
"recordingsDir": "/path/to/e2e/recordings",
|
|
421
|
-
"mode": "replay",
|
|
422
|
-
"id": "my-test-1"
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
**Via JavaScript:**
|
|
427
|
-
```javascript
|
|
428
|
-
const config = await fetch('http://localhost:8100/__control').then(r => r.json());
|
|
429
|
-
console.log(config.recordingsDir); // "/path/to/e2e/recordings"
|
|
430
|
-
console.log(config.mode); // "replay"
|
|
431
|
-
console.log(config.id); // "my-test-1"
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### POST - Switch Proxy Mode
|
|
435
208
|
|
|
436
|
-
|
|
437
|
-
```bash
|
|
438
|
-
# Switch to record mode
|
|
439
|
-
curl -X POST http://localhost:8100/__control \
|
|
440
|
-
-H "Content-Type: application/json" \
|
|
441
|
-
-d '{"mode": "record", "id": "my-test-1", "timeout": 30000}'
|
|
442
|
-
|
|
443
|
-
# Switch to replay mode
|
|
444
|
-
curl -X POST http://localhost:8100/__control \
|
|
445
|
-
-H "Content-Type: application/json" \
|
|
446
|
-
-d '{"mode": "replay", "id": "my-test-1"}'
|
|
447
|
-
|
|
448
|
-
# Switch to transparent mode
|
|
209
|
+
# Switch modes
|
|
449
210
|
curl -X POST http://localhost:8100/__control \
|
|
450
211
|
-H "Content-Type: application/json" \
|
|
451
|
-
-d '{"mode": "
|
|
212
|
+
-d '{"mode": "record", "id": "my-test-1"}'
|
|
452
213
|
```
|
|
453
214
|
|
|
454
|
-
**Via JavaScript:**
|
|
455
|
-
```javascript
|
|
456
|
-
await fetch('http://localhost:8100/__control', {
|
|
457
|
-
method: 'POST',
|
|
458
|
-
headers: { 'Content-Type': 'application/json' },
|
|
459
|
-
body: JSON.stringify({
|
|
460
|
-
mode: 'record',
|
|
461
|
-
id: 'my-test-1',
|
|
462
|
-
timeout: 30000 // Optional: auto-reset after 30s
|
|
463
|
-
})
|
|
464
|
-
});
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Control Request Interface
|
|
468
|
-
|
|
469
215
|
```typescript
|
|
470
216
|
interface ControlRequest {
|
|
471
217
|
mode: 'transparent' | 'record' | 'replay';
|
|
472
|
-
id?: string;
|
|
473
|
-
timeout?: number;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
interface ControlResponse {
|
|
477
|
-
recordingsDir: string;
|
|
478
|
-
mode: string;
|
|
479
|
-
id?: string;
|
|
218
|
+
id?: string; // required for record/replay
|
|
219
|
+
timeout?: number; // auto-reset timeout in ms (default: 120000)
|
|
480
220
|
}
|
|
481
221
|
```
|
|
482
222
|
|
|
483
|
-
##
|
|
484
|
-
|
|
485
|
-
### Initial Recording
|
|
486
|
-
|
|
487
|
-
1. Start backend API: `npm run api`
|
|
488
|
-
2. Start proxy and app: `npm run dev:proxy`
|
|
489
|
-
3. Set test to `'record'` mode
|
|
490
|
-
4. Run test: Recordings saved to `./e2e/recordings/` (directory created automatically)
|
|
491
|
-
5. Commit `.mock.json` files to git
|
|
492
|
-
6. Change mode to `'replay'`
|
|
493
|
-
|
|
494
|
-
### Running with Replay
|
|
495
|
-
|
|
496
|
-
1. Start proxy and app: `npm run dev:proxy` (no backend needed!)
|
|
497
|
-
2. Set test to `'replay'` mode
|
|
498
|
-
3. Run test: Uses recorded responses
|
|
499
|
-
4. Tests run fast without backend
|
|
500
|
-
|
|
501
|
-
### Updating Recordings
|
|
502
|
-
|
|
503
|
-
1. Start backend API
|
|
504
|
-
2. Set test to `'record'` mode
|
|
505
|
-
3. Run test: Overwrites existing recording
|
|
506
|
-
4. Commit updated `.mock.json` file
|
|
507
|
-
|
|
508
|
-
## Recording Format
|
|
223
|
+
## API Reference
|
|
509
224
|
|
|
510
|
-
|
|
225
|
+
### `playwrightProxy`
|
|
511
226
|
|
|
512
|
-
|
|
513
|
-
|
|
227
|
+
```typescript
|
|
228
|
+
const playwrightProxy: {
|
|
229
|
+
before(
|
|
230
|
+
page: Page,
|
|
231
|
+
testInfo: TestInfo,
|
|
232
|
+
mode: 'record' | 'replay' | 'transparent',
|
|
233
|
+
options?: number | { url?: string | RegExp; timeout?: number }
|
|
234
|
+
): Promise<void>;
|
|
514
235
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
├── create-a-user.mock.json # Server-side API calls
|
|
518
|
-
├── create-a-user.har # Client-side 3rd party requests
|
|
519
|
-
├── fetch-users-list.mock.json
|
|
520
|
-
└── delete-user.mock.json
|
|
236
|
+
teardown(): Promise<void>;
|
|
237
|
+
};
|
|
521
238
|
```
|
|
522
239
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
## Troubleshooting
|
|
526
|
-
|
|
527
|
-
### Proxy not responding
|
|
240
|
+
### `setProxyMode`
|
|
528
241
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
242
|
+
```typescript
|
|
243
|
+
function setProxyMode(
|
|
244
|
+
mode: 'record' | 'replay' | 'transparent',
|
|
245
|
+
id?: string,
|
|
246
|
+
timeout?: number
|
|
247
|
+
): Promise<void>;
|
|
533
248
|
```
|
|
534
249
|
|
|
535
|
-
|
|
250
|
+
### Next.js helpers (`test-proxy-recorder/nextjs`)
|
|
536
251
|
|
|
537
|
-
```
|
|
538
|
-
|
|
252
|
+
```typescript
|
|
253
|
+
function setNextProxyHeaders(request: NextRequest, response: NextResponse): void;
|
|
254
|
+
function getRecordingId(headers: NextRequest | Headers): string | null;
|
|
255
|
+
function createHeadersWithRecordingId(
|
|
256
|
+
headers: NextRequest | Headers,
|
|
257
|
+
additional?: Record<string, string>
|
|
258
|
+
): Record<string, string>;
|
|
539
259
|
```
|
|
540
260
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
- Verify proxy mode is `'record'`
|
|
544
|
-
- Check app is using proxy URL (`http://localhost:8100`)
|
|
545
|
-
- Verify write permissions on recordings directory
|
|
546
|
-
- Check proxy server logs for errors
|
|
261
|
+
## Typical Workflow
|
|
547
262
|
|
|
548
|
-
|
|
263
|
+
```
|
|
264
|
+
1. Record start proxy + app + backend, run tests with 'record' mode
|
|
265
|
+
2. Commit git add e2e/recordings/
|
|
266
|
+
3. Replay start proxy + app (no backend), run tests with 'replay' mode
|
|
267
|
+
4. Update re-record when API changes, commit new recordings
|
|
268
|
+
```
|
|
549
269
|
|
|
550
|
-
|
|
551
|
-
- Check test name hasn't changed
|
|
552
|
-
- Verify recording file matches expected format
|
|
553
|
-
- Re-record if API responses changed
|
|
270
|
+
## Next.js 16
|
|
554
271
|
|
|
555
|
-
|
|
272
|
+
Next.js 16 uses `proxy.ts` as the middleware entry point (replaces `middleware.ts`). Place it at the project root alongside `next.config.ts`:
|
|
556
273
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
274
|
+
```typescript
|
|
275
|
+
// proxy.ts (Next.js 16 middleware convention)
|
|
276
|
+
import { NextResponse } from 'next/server';
|
|
277
|
+
import type { NextRequest } from 'next/server';
|
|
278
|
+
import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
|
|
561
279
|
|
|
280
|
+
export function middleware(request: NextRequest) {
|
|
281
|
+
const response = NextResponse.next();
|
|
282
|
+
setNextProxyHeaders(request, response);
|
|
283
|
+
return response;
|
|
284
|
+
}
|
|
562
285
|
|
|
563
|
-
|
|
286
|
+
export const config = {
|
|
287
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
288
|
+
};
|
|
289
|
+
```
|
|
564
290
|
|
|
565
|
-
|
|
291
|
+
**package.json scripts** — start services from scripts, not from `playwright.config.ts`:
|
|
566
292
|
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"scripts": {
|
|
296
|
+
"mock": "node mock-backend/server.mjs",
|
|
297
|
+
"proxy": "test-proxy-recorder http://localhost:3002 -p 8100 -d ./e2e/recordings",
|
|
298
|
+
"start:all": "concurrently \"pnpm mock\" \"pnpm proxy\" \"pnpm build && next start --port 3000\""
|
|
299
|
+
}
|
|
572
300
|
}
|
|
573
301
|
```
|
|
574
302
|
|
|
575
|
-
|
|
303
|
+
## Example App
|
|
576
304
|
|
|
577
|
-
|
|
578
|
-
import { playwrightProxy, setProxyMode, RECORDING_ID_HEADER } from 'test-proxy-recorder';
|
|
579
|
-
import type { Page } from '@playwright/test';
|
|
580
|
-
|
|
581
|
-
// Client-side recording options
|
|
582
|
-
interface ClientSideRecordingOptions {
|
|
583
|
-
/**
|
|
584
|
-
* URL pattern for client-side requests to record/replay
|
|
585
|
-
* Uses Playwright's native format (string or RegExp)
|
|
586
|
-
* Example: /cognito-.*amazonaws\.com|\.stream-io-api\.com/
|
|
587
|
-
* Example: 'https://api.example.com/**'
|
|
588
|
-
*/
|
|
589
|
-
url?: string | RegExp;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Main helper for Playwright tests
|
|
593
|
-
const playwrightProxy = {
|
|
594
|
-
// Set proxy mode before test and configure page with recording ID header
|
|
595
|
-
// Supports optional client-side recording for 3rd party APIs
|
|
596
|
-
async before(
|
|
597
|
-
page: Page,
|
|
598
|
-
testInfo: TestInfo,
|
|
599
|
-
mode: 'record' | 'replay' | 'transparent',
|
|
600
|
-
options?: number | (ClientSideRecordingOptions & { timeout?: number })
|
|
601
|
-
): Promise<void>;
|
|
305
|
+
[`apps/example-nextjs16`](apps/example-nextjs16) is a full working example: a Next.js 16 todo app wired up with a mock backend, proxy, and Playwright e2e tests in record/replay mode.
|
|
602
306
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
307
|
+
```text
|
|
308
|
+
apps/example-nextjs16/
|
|
309
|
+
app/ Next.js pages and components
|
|
310
|
+
mock-backend/ Standalone Node.js HTTP server (port 3002)
|
|
311
|
+
e2e/ Playwright tests + recordings
|
|
312
|
+
proxy.ts Next.js 16 middleware — forwards session headers to SSR fetches
|
|
313
|
+
```
|
|
607
314
|
|
|
608
|
-
|
|
609
|
-
async function setProxyMode(
|
|
610
|
-
mode: 'record' | 'replay' | 'transparent',
|
|
611
|
-
id?: string,
|
|
612
|
-
timeout?: number
|
|
613
|
-
): Promise<void>;
|
|
315
|
+
**Three-service architecture:**
|
|
614
316
|
|
|
615
|
-
|
|
616
|
-
|
|
317
|
+
```text
|
|
318
|
+
Browser ──> Proxy (8100) ──> Mock Backend (3002)
|
|
319
|
+
Next.js SSR ──> Proxy (8100) ──> Mock Backend (3002)
|
|
617
320
|
```
|
|
618
321
|
|
|
619
|
-
|
|
620
|
-
- `number` - Legacy format: timeout in milliseconds
|
|
621
|
-
- `ClientSideRecordingOptions & { timeout?: number }` - Object with optional client-side recording and timeout:
|
|
622
|
-
- `url?: string | RegExp` - URL pattern for client-side recording (uses Playwright's HAR format)
|
|
623
|
-
- `timeout?: number` - Auto-reset timeout in milliseconds
|
|
322
|
+
Start everything and run the record/replay cycle:
|
|
624
323
|
|
|
625
|
-
|
|
324
|
+
```bash
|
|
325
|
+
# Start all services (mock backend + proxy + Next.js)
|
|
326
|
+
pnpm --filter example-nextjs16 start:all
|
|
626
327
|
|
|
627
|
-
|
|
328
|
+
# Record tests (run against live services, save to e2e/recordings/)
|
|
329
|
+
pnpm --filter example-nextjs16 test:e2e:record
|
|
628
330
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
setNextProxyHeaders,
|
|
632
|
-
getRecordingId,
|
|
633
|
-
createHeadersWithRecordingId,
|
|
634
|
-
RECORDING_ID_HEADER
|
|
635
|
-
} from 'test-proxy-recorder/nextjs';
|
|
636
|
-
import type { NextRequest, NextResponse } from 'next/server';
|
|
637
|
-
|
|
638
|
-
// Forward recording ID header in Next.js middleware
|
|
639
|
-
// Automatically skipped in production unless TEST_PROXY_RECORDER_ENABLED=true
|
|
640
|
-
function setNextProxyHeaders(
|
|
641
|
-
request: NextRequest,
|
|
642
|
-
response: NextResponse
|
|
643
|
-
): void;
|
|
644
|
-
|
|
645
|
-
// Get recording ID from request headers
|
|
646
|
-
function getRecordingId(
|
|
647
|
-
requestHeaders: NextRequest | Headers
|
|
648
|
-
): string | null;
|
|
649
|
-
|
|
650
|
-
// Create headers object with recording ID for fetch requests
|
|
651
|
-
function createHeadersWithRecordingId(
|
|
652
|
-
requestHeaders: NextRequest | Headers,
|
|
653
|
-
additionalHeaders?: Record<string, string>
|
|
654
|
-
): Record<string, string>;
|
|
331
|
+
# Replay tests (no backend needed — served from recordings)
|
|
332
|
+
pnpm --filter example-nextjs16 test:e2e
|
|
655
333
|
```
|
|
656
334
|
|
|
657
|
-
|
|
335
|
+
## Parallel Replay: Do Not Call `teardown()` Per-Test
|
|
658
336
|
|
|
659
|
-
|
|
337
|
+
`playwrightProxy.teardown()` sets the **global** proxy mode to `transparent`. With `fullyParallel: true`, each Playwright worker runs its own `test.afterAll`. If a fast test completes and calls `teardown()` while a slower test (e.g., one with more interaction steps) is still running, the proxy switches to transparent mid-test. The remaining requests are forwarded to the real backend instead of being replayed, causing failures.
|
|
660
338
|
|
|
661
|
-
**
|
|
339
|
+
**Wrong:**
|
|
662
340
|
|
|
663
341
|
```typescript
|
|
664
|
-
//
|
|
665
|
-
{
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
id?: string; // Active recording/replay session ID
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**POST `/__control`** - Switch proxy mode:
|
|
673
|
-
|
|
674
|
-
```typescript
|
|
675
|
-
// Request Body
|
|
676
|
-
{
|
|
677
|
-
mode: 'transparent' | 'record' | 'replay';
|
|
678
|
-
id?: string; // Recording ID (required for record/replay)
|
|
679
|
-
timeout?: number; // Auto-reset timeout in ms (default: 120000)
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Response
|
|
683
|
-
{
|
|
684
|
-
success: boolean;
|
|
685
|
-
mode: string;
|
|
686
|
-
id: string | null;
|
|
687
|
-
timeout: number;
|
|
688
|
-
recordingsDir: string;
|
|
689
|
-
}
|
|
342
|
+
// ❌ breaks parallel replay — teardown() affects all sessions globally
|
|
343
|
+
test.afterAll(async () => {
|
|
344
|
+
await playwrightProxy.teardown();
|
|
345
|
+
});
|
|
690
346
|
```
|
|
691
347
|
|
|
692
|
-
**
|
|
348
|
+
**Correct:** omit `test.afterAll`. Session cleanup is automatic via `context.on('close')` → `cleanupSession()`. Use a [global teardown](https://playwright.dev/docs/test-global-setup-teardown) if you need to reset the proxy after a full test run.
|
|
693
349
|
|
|
694
350
|
## Requirements
|
|
695
351
|
|
|
696
352
|
- Node.js >= 22.0.0
|
|
697
|
-
- @playwright/test >= 1.0.0 (
|
|
353
|
+
- @playwright/test >= 1.0.0 (peer dependency)
|
|
698
354
|
|
|
699
355
|
## Contributing
|
|
700
356
|
|
|
701
|
-
Contributions
|
|
357
|
+
Contributions welcome! Please submit a Pull Request.
|
|
702
358
|
|
|
703
359
|
## License
|
|
704
360
|
|