test-proxy-recorder 0.1.4 → 0.1.6

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 CHANGED
@@ -9,262 +9,455 @@ HTTP proxy server for recording and replaying network requests in testing. Works
9
9
 
10
10
  ## Features
11
11
 
12
- - **Fast CI/CD Tests**: Record API responses once with real backend, replay them on CI/CD
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 NextJS.
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
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
16
17
 
17
- ## Installation
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
+ - [Control Endpoint](#control-endpoint)
25
+ - [Typical Workflow](#typical-workflow)
26
+ - [Recording Format](#recording-format)
27
+ - [Troubleshooting](#troubleshooting)
28
+ - [API Reference](#api-reference)
29
+
30
+ ## How It Works
31
+
32
+ The proxy server runs continuously and can switch between three modes per test:
33
+
34
+ ### 1. Transparent Mode (Default)
35
+
36
+ Passes requests through to the backend without recording or replaying.
37
+
38
+ ### 2. Record Mode
39
+
40
+ Captures all HTTP requests/responses and WebSocket messages to disk. Each test gets its own recording file based on the test name.
41
+
42
+ ### 3. Replay Mode
43
+
44
+ Replays previously recorded responses from disk instead of hitting the real API. Perfect for fast, deterministic tests.
45
+
46
+ ## Complete Setup Guide
47
+
48
+ ### Step 1: Install Package
49
+
50
+ ```bash
51
+ npm install --save-dev test-proxy-recorder
52
+ ```
53
+
54
+ ### Step 2: Add NPM Scripts
55
+
56
+ Add to `package.json`:
57
+
58
+ ```json
59
+ {
60
+ "scripts": {
61
+ "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./e2e/recordings"
62
+ }
63
+ }
64
+ ```
65
+
66
+ **RECOMMENDED**: Use `concurrently` to run proxy and app together:
67
+
68
+ ```bash
69
+ npm install --save-dev concurrently
70
+ ```
71
+
72
+ ```json
73
+ {
74
+ "scripts": {
75
+ "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./e2e/recordings",
76
+ "dev:proxy": "concurrently -n \"proxy,app\" -c \"blue,green\" \"npm run proxy\" \"INTERNAL_API_URL=http://localhost:8100 npm run dev\""
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Step 3: Configure Git for Recordings
82
+
83
+ **CRITICAL**: Recordings must be committed to git for CI/CD replay.
84
+
85
+ Create or update your `.gitattributes` file:
86
+
87
+ ```gitattributes
88
+ /e2e/recordings/** binary
89
+ ```
90
+
91
+ This marks recording files as binary, which causes long mock files to be collapsed/folded in Pull Request diffs for better readability.
92
+
93
+ **DO NOT** add `e2e/recordings` to `.gitignore`. Recordings need to be versioned in git for CI/CD to use them.
94
+
95
+ **Note**: The recordings directory will be created automatically when you first record a test - no need to create it manually.
96
+
97
+ ### Step 4: Create Playwright Global Teardown (Recommended)
98
+
99
+ Create `e2e/global-teardown.ts`:
100
+
101
+ ```typescript
102
+ import { setProxyMode } from 'test-proxy-recorder';
103
+
104
+ async function globalTeardown() {
105
+ await setProxyMode('transparent');
106
+ }
107
+
108
+ export default globalTeardown;
109
+ ```
110
+
111
+ Update `playwright.config.ts`:
112
+
113
+ ```typescript
114
+ import { defineConfig } from '@playwright/test';
115
+
116
+ export default defineConfig({
117
+ testDir: './e2e',
118
+ globalTeardown: './e2e/global-teardown.ts',
119
+ // ... rest of config
120
+ });
121
+ ```
122
+
123
+ ### Step 5: Create Example Test
124
+
125
+ Create `e2e/example.spec.ts`:
126
+
127
+ ```typescript
128
+ import { test, expect } from '@playwright/test';
129
+ import { playwrightProxy } from 'test-proxy-recorder';
130
+
131
+ test('example test with proxy', async ({ page }, testInfo) => {
132
+ // Set proxy mode: 'record' to capture, 'replay' to use recordings
133
+ await playwrightProxy.before(testInfo, 'replay');
134
+
135
+ await page.goto('/');
136
+ await expect(page.getByText('Welcome')).toBeVisible();
137
+
138
+ // Always cleanup after test
139
+ await playwrightProxy.after(testInfo);
140
+ });
141
+ ```
142
+
143
+ ### Step 6: Run Tests
144
+
145
+ **First run (record mode)**:
146
+
147
+ ```typescript
148
+ await playwrightProxy.before(testInfo, 'record');
149
+ ```
150
+
151
+ **Subsequent runs (replay mode)**:
152
+
153
+ ```typescript
154
+ await playwrightProxy.before(testInfo, 'replay');
155
+ ```
156
+
157
+ ## CLI Usage
158
+
159
+ ### Basic Command
18
160
 
19
161
  ```bash
20
- npm install test-proxy-recorder
21
- # or
22
- pnpm add test-proxy-recorder
23
- # or
24
- yarn add test-proxy-recorder
162
+ test-proxy-recorder <target-url> [options]
25
163
  ```
26
164
 
27
- ## Quick Start
165
+ ### CLI Options
166
+
167
+ - `<target-url>` - Backend API URL (positional argument, required)
168
+ - `--port, -p <number>` - Port to listen on (default: 8080)
169
+ - `--recordings-dir, -r <path>` - Directory to store recordings (default: ./recordings)
170
+ - `--help, -h` - Show help
171
+
172
+ ### Examples
173
+
174
+ ```bash
175
+ # Basic usage
176
+ test-proxy-recorder http://localhost:8000
28
177
 
29
- 1. Run proxy with your backend API as a target `test-proxy-recorder --port 8100 --target http://localhost:8000 --recordings ./recordings`, here your backend on port 8000 as target, proxy on port 8100.
30
- 2. Point your Frontend app to proxy port, 8100 as example
31
- 3. The proxy runs continuously in the background. Tests control the recording/replay mode using `playwrightProxy.before()` and `playwrightProxy.after()`:
178
+ # Custom port and recordings directory
179
+ test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./mocks
180
+
181
+ # Multiple targets (experimental)
182
+ test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
183
+ ```
184
+
185
+ ## Playwright Integration
186
+
187
+ ### Basic Test Structure
188
+
189
+ Every test using the proxy should follow this pattern:
32
190
 
33
191
  ```typescript
34
192
  import { test } from '@playwright/test';
35
193
  import { playwrightProxy } from 'test-proxy-recorder';
36
194
 
37
- test('Test UI with API responses', async ({ page }, testInfo) => {
38
- // Set proxy to recording mode to record mocks, sanitized test title will be used as file name
195
+ test('test name', async ({ page }, testInfo) => {
196
+ // 1. Set mode BEFORE test actions
197
+ await playwrightProxy.before(testInfo, 'replay');
198
+
199
+ // 2. Test code
200
+ await page.goto('/page');
201
+
202
+ // 3. Reset mode AFTER test completes
203
+ await playwrightProxy.after(testInfo);
204
+ });
205
+ ```
206
+
207
+ ### Recording vs Replay
208
+
209
+ ```typescript
210
+ // Recording mode - captures API responses
211
+ test('create user', async ({ page }, testInfo) => {
39
212
  await playwrightProxy.before(testInfo, 'record');
40
213
 
41
- // Make requests - they will be recorded
42
- await page.goto('/myPage');
43
- /// ... test content ...
214
+ await page.goto('/users/new');
215
+ await page.fill('[name="username"]', 'testuser');
216
+ await page.click('button[type="submit"]');
44
217
 
45
- // Save mock and return to transparent mode
46
218
  await playwrightProxy.after(testInfo);
47
219
  });
48
220
 
49
- test('replay recorded responses', async ({ page }, testInfo) => {
50
- // Set proxy to replay mode - uses recording from test above
221
+ // Replay mode - uses recorded responses
222
+ test('create user', async ({ page }, testInfo) => {
51
223
  await playwrightProxy.before(testInfo, 'replay');
52
224
 
53
- await page.goto('/myPage');
54
- /// ... test content ...
225
+ await page.goto('/users/new');
226
+ await page.fill('[name="username"]', 'testuser');
227
+ await page.click('button[type="submit"]');
55
228
 
56
229
  await playwrightProxy.after(testInfo);
57
230
  });
58
231
  ```
59
232
 
60
- ## How It Works
233
+ ### Test Naming
61
234
 
62
- The proxy server runs continuously and can switch between three modes:
235
+ Recording files are auto-generated from test names:
63
236
 
64
- ### 1. Transparent Mode (Default)
237
+ - Test: `"create a user"`
238
+ - File: `create-a-user.mock.json`
65
239
 
66
- Simply proxies requests to the backend without recording or replaying.
240
+ **Important**: Keep test names stable for replay to work correctly.
67
241
 
68
- ### 2. Record Mode
242
+ ### Global Teardown (Recommended)
69
243
 
70
- Captures all HTTP requests/responses and WebSocket messages to disk. Each test gets its own recording file based on the test name.
244
+ Create `e2e/global-teardown.ts`:
71
245
 
72
- ### 3. Replay Mode
246
+ ```typescript
247
+ import { setProxyMode } from 'test-proxy-recorder';
73
248
 
74
- Replays previously recorded responses from disk instead of hitting the real API. Perfect for fast, deterministic tests.
249
+ async function globalTeardown() {
250
+ await setProxyMode('transparent');
251
+ }
75
252
 
76
- ## Modes Control
253
+ export default globalTeardown;
254
+ ```
77
255
 
78
- ### Via Playwright
256
+ Update `playwright.config.ts`:
79
257
 
80
258
  ```typescript
81
- // Recording mode
82
- await playwrightProxy.before(testInfo, 'recording');
83
- // ... test code ...
84
- await playwrightProxy.after(testInfo);
259
+ import { defineConfig } from '@playwright/test';
85
260
 
86
- // Replay mode
87
- await playwrightProxy.before(testInfo, 'replay');
88
- // ... test code ...
89
- await playwrightProxy.after(testInfo);
261
+ export default defineConfig({
262
+ testDir: './e2e',
263
+ globalTeardown: './e2e/global-teardown.ts',
264
+ // ... rest of config
265
+ });
90
266
  ```
91
267
 
92
- ### Via HTTP Control Endpoint
268
+ ## Control Endpoint
269
+
270
+ The proxy exposes a control endpoint at `/__control` for programmatic mode switching.
93
271
 
94
- Using curl:
272
+ ### Via HTTP
95
273
 
96
274
  ```bash
97
275
  # Switch to record mode
98
276
  curl -X POST http://localhost:8100/__control \
99
277
  -H "Content-Type: application/json" \
100
- -d '{"mode": "record", "id": "my-testfile-1", "timeout": 30000}'
278
+ -d '{"mode": "record", "id": "my-test-1", "timeout": 30000}'
101
279
 
102
280
  # Switch to replay mode
103
281
  curl -X POST http://localhost:8100/__control \
104
282
  -H "Content-Type: application/json" \
105
- -d '{"mode": "replay", "id": "my-testfile-1"}'
283
+ -d '{"mode": "replay", "id": "my-test-1"}'
106
284
 
107
285
  # Switch to transparent mode
108
286
  curl -X POST http://localhost:8100/__control \
109
287
  -H "Content-Type: application/json" \
110
- -d '{"mode": "transparent", "id": "my-testfile-1"}'
288
+ -d '{"mode": "transparent"}'
111
289
  ```
112
290
 
113
- Using JavaScript fetch:
291
+ ### Via JavaScript
114
292
 
115
293
  ```javascript
116
- // Switch to record mode
117
294
  await fetch('http://localhost:8100/__control', {
118
295
  method: 'POST',
119
- headers: {
120
- 'Content-Type': 'application/json',
121
- },
296
+ headers: { 'Content-Type': 'application/json' },
122
297
  body: JSON.stringify({
123
298
  mode: 'record',
124
- id: 'my-testfile-1',
125
- timeout: 30000
299
+ id: 'my-test-1',
300
+ timeout: 30000 // Optional: auto-reset after 30s
126
301
  })
127
302
  });
303
+ ```
128
304
 
129
- // Switch to replay mode
130
- await fetch('http://localhost:8100/__control', {
131
- method: 'POST',
132
- headers: {
133
- 'Content-Type': 'application/json',
134
- },
135
- body: JSON.stringify({
136
- mode: 'replay',
137
- id: 'my-testfile-1'
138
- })
139
- });
305
+ ### Control Request Interface
140
306
 
141
- // Switch to transparent mode
142
- await fetch('http://localhost:8100/__control', {
143
- method: 'POST',
144
- headers: {
145
- 'Content-Type': 'application/json',
146
- },
147
- body: JSON.stringify({
148
- mode: 'transparent',
149
- })
150
- });
307
+ ```typescript
308
+ interface ControlRequest {
309
+ mode: 'transparent' | 'record' | 'replay';
310
+ id?: string; // Recording ID, required for record/replay
311
+ timeout?: number; // Auto-reset timeout in ms (default: 120000)
312
+ }
151
313
  ```
152
314
 
153
- ## Usage
315
+ ## Typical Workflow
154
316
 
155
- ```bash
156
- # Start proxy server
157
- test-proxy-recorder --port 8100 --target http://localhost:8000 --recordings ./recordings
317
+ ### Initial Recording
318
+
319
+ 1. Start backend API: `npm run api`
320
+ 2. Start proxy and app: `npm run dev:proxy`
321
+ 3. Set test to `'record'` mode
322
+ 4. Run test: Recordings saved to `./e2e/recordings/` (directory created automatically)
323
+ 5. Commit `.mock.json` files to git
324
+ 6. Change mode to `'replay'`
325
+
326
+ ### Running with Replay
327
+
328
+ 1. Start proxy and app: `npm run dev:proxy` (no backend needed!)
329
+ 2. Set test to `'replay'` mode
330
+ 3. Run test: Uses recorded responses
331
+ 4. Tests run fast without backend
332
+
333
+ ### Updating Recordings
334
+
335
+ 1. Start backend API
336
+ 2. Set test to `'record'` mode
337
+ 3. Run test: Overwrites existing recording
338
+ 4. Commit updated `.mock.json` file
339
+
340
+ ## Recording Format
341
+
342
+ Recordings are stored as JSON files with `.mock.json` extension:
343
+
344
+ ```text
345
+ e2e/recordings/
346
+ ├── create-a-user.mock.json
347
+ ├── fetch-users-list.mock.json
348
+ └── delete-user.mock.json
158
349
  ```
159
350
 
160
- ### CLI Options
351
+ ## Troubleshooting
161
352
 
162
- - `--port, -p`: Port to listen on (default: 8080)
163
- - `--target, -t`: Backend target URL (can add multiple targets)
164
- - `--recordings, -r`: Directory to store recordings (default: ./recordings)
353
+ ### Proxy not responding
165
354
 
166
- ## API
355
+ **Check if proxy is running**:
167
356
 
168
- ### ProxyServer
357
+ ```bash
358
+ curl http://localhost:8100/__control
359
+ ```
169
360
 
170
- ```typescript
171
- class ProxyServer {
172
- constructor(targets: string[], recordingsDir: string);
361
+ **Check port availability**:
173
362
 
174
- async init(): Promise<void>;
175
- listen(port: number): http.Server;
176
- }
363
+ ```bash
364
+ lsof -i :8100
177
365
  ```
178
366
 
179
- ### Control Endpoint
367
+ ### No recordings saved
368
+
369
+ - Verify proxy mode is `'record'`
370
+ - Check app is using proxy URL (`http://localhost:8100`)
371
+ - Verify write permissions on recordings directory
372
+ - Check proxy server logs for errors
373
+
374
+ ### Test fails in replay mode
375
+
376
+ - Ensure recording exists for this test
377
+ - Check test name hasn't changed
378
+ - Verify recording file matches expected format
379
+ - Re-record if API responses changed
380
+
381
+ ### Recordings not matching requests
382
+
383
+ - Request URLs must match exactly
384
+ - Headers may affect matching (configurable)
385
+ - Query parameters must be in same order
386
+ - Re-record to capture current API behavior
387
+
388
+
389
+ ## API Reference
180
390
 
181
- Send POST requests to `/__control` with JSON body:
391
+ ### ProxyServer Class
182
392
 
183
393
  ```typescript
184
- interface ControlRequest {
185
- mode: 'transparent' | 'record' | 'replay';
186
- id?: string; // Will be used as file name for recordings, required for record/replay modes
187
- timeout?: number; // Auto-switch to transparent mode after timeout (ms), default 120 seconds
394
+ class ProxyServer {
395
+ constructor(targets: string[], recordingsDir: string);
396
+ async init(): Promise<void>;
397
+ listen(port: number): http.Server;
188
398
  }
189
399
  ```
190
400
 
191
- ### Playwright Integration API
401
+ ### Playwright Integration
192
402
 
193
403
  ```typescript
194
- import { playwrightProxy } from 'test-proxy-recorder';
404
+ import { playwrightProxy, setProxyMode } from 'test-proxy-recorder';
195
405
 
196
- // Main helper object for use with Playwright tests
406
+ // Main helper for Playwright tests
197
407
  const playwrightProxy = {
198
408
  // Set proxy mode before test
199
- async before(testInfo: PlaywrightTestInfo, mode: 'record' | 'replay' | 'transparent'): Promise<void>;
409
+ async before(
410
+ testInfo: TestInfo,
411
+ mode: 'record' | 'replay' | 'transparent',
412
+ timeout?: number
413
+ ): Promise<void>;
200
414
 
201
415
  // Reset to transparent mode after test
202
- async after(testInfo: PlaywrightTestInfo): Promise<void>;
416
+ async after(testInfo: TestInfo): Promise<void>;
203
417
  };
204
- ```
205
-
206
- ## Recording Format
207
-
208
- Recordings are stored as JSON files in the recordings directory:
209
418
 
419
+ // Direct mode control
420
+ async function setProxyMode(
421
+ mode: 'record' | 'replay' | 'transparent',
422
+ id?: string,
423
+ timeout?: number
424
+ ): Promise<void>;
210
425
  ```
211
- recordings/
212
- ├── test-session-1.json
213
- ├── test-session-2.json
214
- └── ...
215
- ```
216
-
217
- Each recording contains:
218
-
219
- - Request/response pairs with headers and bodies
220
- - WebSocket messages with timestamps
221
- - Unique keys for request matching during replay
222
-
223
- ## Typical Workflow
224
-
225
- 1. **Start the proxy server** (runs continuously):
226
426
 
227
- ```bash
228
- test-proxy-recorder http://localhost:8000 --port 8100
229
- ```
230
-
231
- 2. **Configure your app** to use the proxy:
427
+ ### Control Endpoint
232
428
 
233
- ```bash
234
- export EXTERNAL_API_URL=http://localhost:8100 yarn dev
235
- ```
429
+ **Endpoint**: `POST http://localhost:8100/__control`
236
430
 
237
- 3. **Record responses** (first run):
431
+ **Request Body**:
238
432
 
239
- ```typescript
240
- test('my test', async ({ page }, testInfo) => {
241
- await playwrightProxy.before(testInfo, 'record');
242
- // Test interacts with real API through proxy
243
- await page.goto('/my-page');
244
- await playwrightProxy.after(testInfo);
245
- });
246
- ```
433
+ ```typescript
434
+ {
435
+ mode: 'transparent' | 'record' | 'replay';
436
+ id?: string; // Recording ID (required for record/replay)
437
+ timeout?: number; // Auto-reset timeout in ms (default: 120000)
438
+ }
439
+ ```
247
440
 
248
- 4. **Replay responses** (subsequent runs):
441
+ **Response**:
249
442
 
250
- ```typescript
251
- test('my test', async ({ page }, testInfo) => {
252
- await playwrightProxy.before(testInfo, 'replay');
253
- // Test uses recorded responses - no real API calls
254
- await page.goto('/my-page');
255
- await playwrightProxy.after(testInfo);
256
- });
257
- ```
443
+ ```typescript
444
+ {
445
+ success: boolean;
446
+ mode: string;
447
+ id: string | null;
448
+ timeout: number;
449
+ }
450
+ ```
258
451
 
259
452
  ## Requirements
260
453
 
261
454
  - Node.js >= 22.0.0
262
- - @playwright/test >= 1.0.0
263
-
264
- ## License
265
-
266
- MIT
455
+ - @playwright/test >= 1.0.0 (for Playwright integration)
267
456
 
268
457
  ## Contributing
269
458
 
270
459
  Contributions are welcome! Please feel free to submit a Pull Request.
460
+
461
+ ## License
462
+
463
+ MIT
@@ -47,7 +47,7 @@ interface RecordingSession {
47
47
  websocketRecordings: WebSocketRecording[];
48
48
  }
49
49
 
50
- type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
50
+ type PlaywrightTestInfo = Pick<TestInfo, 'title' | 'titlePath'>;
51
51
  /**
52
52
  * Set the proxy mode for a given session
53
53
  * @param mode - The proxy mode to set (recording, replay, transparent)
@@ -57,6 +57,10 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
57
57
  declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
58
58
  /**
59
59
  * Generate a session ID from test info
60
+ * Uses titlePath to create folder structure with test file name
61
+ * Supports both .spec.ts and .test.ts extensions
62
+ * Example: ['jobs/Create.spec.ts', 'create a job'] becomes 'jobs/Create__create-a-job'
63
+ * Example: ['users/Auth.test.ts', 'login test'] becomes 'users/Auth__login-test'
60
64
  * @param testInfo - Playwright test info object
61
65
  */
62
66
  declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
@@ -47,7 +47,7 @@ interface RecordingSession {
47
47
  websocketRecordings: WebSocketRecording[];
48
48
  }
49
49
 
50
- type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
50
+ type PlaywrightTestInfo = Pick<TestInfo, 'title' | 'titlePath'>;
51
51
  /**
52
52
  * Set the proxy mode for a given session
53
53
  * @param mode - The proxy mode to set (recording, replay, transparent)
@@ -57,6 +57,10 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
57
57
  declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
58
58
  /**
59
59
  * Generate a session ID from test info
60
+ * Uses titlePath to create folder structure with test file name
61
+ * Supports both .spec.ts and .test.ts extensions
62
+ * Example: ['jobs/Create.spec.ts', 'create a job'] becomes 'jobs/Create__create-a-job'
63
+ * Example: ['users/Auth.test.ts', 'login test'] becomes 'users/Auth__login-test'
60
64
  * @param testInfo - Playwright test info object
61
65
  */
62
66
  declare function generateSessionId(testInfo: PlaywrightTestInfo): string;