test-proxy-recorder 0.1.1 → 0.1.3
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 +79 -172
- package/dist/index-DfFpm8mB.d.cts +95 -0
- package/dist/index-DfFpm8mB.d.ts +95 -0
- package/dist/index.cjs +28 -29
- package/dist/index.d.cts +2 -47
- package/dist/index.d.ts +2 -47
- package/dist/index.mjs +27 -29
- package/dist/playwright/index.cjs +11 -4
- package/dist/playwright/index.d.cts +3 -50
- package/dist/playwright/index.d.ts +3 -50
- package/dist/playwright/index.mjs +11 -4
- package/dist/proxy.js +23 -25
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# test-proxy-recorder
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/test-proxy-recorder)
|
|
4
|
+
[](https://github.com/asmyshlyaev177/test-proxy-recorder/blob/master/LICENSE)
|
|
5
|
+
|
|
3
6
|
HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright and other testing frameworks.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
## BETA VERSION
|
|
6
9
|
|
|
7
10
|
## Features
|
|
8
11
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **WebSocket Support**: Full support for recording and replaying WebSocket connections
|
|
14
|
-
- **Multiple Targets**: Load balance between multiple backend targets
|
|
15
|
-
- **Timeout Control**: Automatic mode switching after configurable timeouts
|
|
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.
|
|
15
|
+
- **Deterministic Tests**: Same responses every time, no flaky network issues, no need to wire up the whole Backend API for testing
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -26,40 +26,23 @@ yarn add test-proxy-recorder
|
|
|
26
26
|
|
|
27
27
|
## Quick Start
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import { ProxyServer } from 'test-proxy-recorder';
|
|
33
|
-
|
|
34
|
-
const proxy = new ProxyServer(
|
|
35
|
-
['http://localhost:3000'], // backend targets
|
|
36
|
-
'./recordings' // directory to store recordings
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
await proxy.init();
|
|
40
|
-
const server = proxy.listen(8080);
|
|
41
|
-
console.log('Proxy running on http://localhost:8080');
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### With Playwright
|
|
45
|
-
|
|
46
|
-
The proxy runs continuously in the background. Tests control the recording/replay mode using `playwrightProxy.before()` and `playwrightProxy.after()`:
|
|
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()`:
|
|
47
32
|
|
|
48
33
|
```typescript
|
|
49
34
|
import { test } from '@playwright/test';
|
|
50
35
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
51
36
|
|
|
52
|
-
test('
|
|
53
|
-
// Set proxy to recording mode
|
|
54
|
-
await playwrightProxy.before(testInfo, '
|
|
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
|
|
39
|
+
await playwrightProxy.before(testInfo, 'record');
|
|
55
40
|
|
|
56
41
|
// Make requests - they will be recorded
|
|
57
|
-
await page.goto('
|
|
58
|
-
|
|
59
|
-
// Your test assertions here
|
|
60
|
-
await expect(page.getByText('Data loaded')).toBeVisible();
|
|
42
|
+
await page.goto('/myPage');
|
|
43
|
+
/// ... test content ...
|
|
61
44
|
|
|
62
|
-
//
|
|
45
|
+
// Save mock and return to transparent mode
|
|
63
46
|
await playwrightProxy.after(testInfo);
|
|
64
47
|
});
|
|
65
48
|
|
|
@@ -67,34 +50,13 @@ test('replay recorded responses', async ({ page }, testInfo) => {
|
|
|
67
50
|
// Set proxy to replay mode - uses recording from test above
|
|
68
51
|
await playwrightProxy.before(testInfo, 'replay');
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await expect(page.getByText('Data loaded')).toBeVisible();
|
|
53
|
+
await page.goto('/myPage');
|
|
54
|
+
/// ... test content ...
|
|
74
55
|
|
|
75
56
|
await playwrightProxy.after(testInfo);
|
|
76
57
|
});
|
|
77
58
|
```
|
|
78
59
|
|
|
79
|
-
You can also use standalone functions for more control:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import { test } from '@playwright/test';
|
|
83
|
-
import { setProxyMode, generateSessionId } from 'test-proxy-recorder';
|
|
84
|
-
|
|
85
|
-
test('custom proxy control', async ({ page }, testInfo) => {
|
|
86
|
-
const sessionId = generateSessionId(testInfo);
|
|
87
|
-
|
|
88
|
-
// Start recording with a 30s timeout
|
|
89
|
-
await setProxyMode('recording', sessionId, 30000);
|
|
90
|
-
|
|
91
|
-
await page.goto('http://localhost:8080/api/data');
|
|
92
|
-
|
|
93
|
-
// Switch to transparent mode
|
|
94
|
-
await setProxyMode('transparent', sessionId);
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
60
|
## How It Works
|
|
99
61
|
|
|
100
62
|
The proxy server runs continuously and can switch between three modes:
|
|
@@ -113,7 +75,7 @@ Replays previously recorded responses from disk instead of hitting the real API.
|
|
|
113
75
|
|
|
114
76
|
## Modes Control
|
|
115
77
|
|
|
116
|
-
### Via Playwright
|
|
78
|
+
### Via Playwright
|
|
117
79
|
|
|
118
80
|
```typescript
|
|
119
81
|
// Recording mode
|
|
@@ -129,40 +91,76 @@ await playwrightProxy.after(testInfo);
|
|
|
129
91
|
|
|
130
92
|
### Via HTTP Control Endpoint
|
|
131
93
|
|
|
94
|
+
Using curl:
|
|
95
|
+
|
|
132
96
|
```bash
|
|
133
97
|
# Switch to record mode
|
|
134
|
-
curl -X POST http://localhost:
|
|
98
|
+
curl -X POST http://localhost:8100/__control \
|
|
135
99
|
-H "Content-Type: application/json" \
|
|
136
|
-
-d '{"mode": "record", "id": "
|
|
100
|
+
-d '{"mode": "record", "id": "my-testfile-1", "timeout": 30000}'
|
|
137
101
|
|
|
138
102
|
# Switch to replay mode
|
|
139
|
-
curl -X POST http://localhost:
|
|
103
|
+
curl -X POST http://localhost:8100/__control \
|
|
140
104
|
-H "Content-Type: application/json" \
|
|
141
|
-
-d '{"mode": "replay", "id": "
|
|
105
|
+
-d '{"mode": "replay", "id": "my-testfile-1"}'
|
|
142
106
|
|
|
143
107
|
# Switch to transparent mode
|
|
144
|
-
curl -X POST http://localhost:
|
|
108
|
+
curl -X POST http://localhost:8100/__control \
|
|
145
109
|
-H "Content-Type: application/json" \
|
|
146
|
-
-d '{"mode": "transparent", "id": "
|
|
110
|
+
-d '{"mode": "transparent", "id": "my-testfile-1"}'
|
|
147
111
|
```
|
|
148
112
|
|
|
149
|
-
|
|
113
|
+
Using JavaScript fetch:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// Switch to record mode
|
|
117
|
+
await fetch('http://localhost:8100/__control', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
mode: 'record',
|
|
124
|
+
id: 'my-testfile-1',
|
|
125
|
+
timeout: 30000
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
|
|
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
|
+
});
|
|
140
|
+
|
|
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
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Usage
|
|
150
154
|
|
|
151
155
|
```bash
|
|
152
156
|
# Start proxy server
|
|
153
|
-
test-proxy-recorder --port
|
|
154
|
-
|
|
155
|
-
# With multiple targets for load balancing
|
|
156
|
-
test-proxy-recorder --port 8080 \
|
|
157
|
-
--target http://localhost:3000 \
|
|
158
|
-
--target http://localhost:3001 \
|
|
159
|
-
--recordings ./recordings
|
|
157
|
+
test-proxy-recorder --port 8100 --target http://localhost:8000 --recordings ./recordings
|
|
160
158
|
```
|
|
161
159
|
|
|
162
160
|
### CLI Options
|
|
163
161
|
|
|
164
162
|
- `--port, -p`: Port to listen on (default: 8080)
|
|
165
|
-
- `--target, -t`: Backend target URL (can
|
|
163
|
+
- `--target, -t`: Backend target URL (can add multiple targets)
|
|
166
164
|
- `--recordings, -r`: Directory to store recordings (default: ./recordings)
|
|
167
165
|
|
|
168
166
|
## API
|
|
@@ -180,13 +178,13 @@ class ProxyServer {
|
|
|
180
178
|
|
|
181
179
|
### Control Endpoint
|
|
182
180
|
|
|
183
|
-
Send POST requests to `/
|
|
181
|
+
Send POST requests to `/__control` with JSON body:
|
|
184
182
|
|
|
185
183
|
```typescript
|
|
186
184
|
interface ControlRequest {
|
|
187
185
|
mode: 'transparent' | 'record' | 'replay';
|
|
188
|
-
id?: string; //
|
|
189
|
-
timeout?: number; // Auto-switch to transparent mode after timeout (ms)
|
|
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
|
|
190
188
|
}
|
|
191
189
|
```
|
|
192
190
|
|
|
@@ -198,36 +196,11 @@ import { playwrightProxy } from 'test-proxy-recorder';
|
|
|
198
196
|
// Main helper object for use with Playwright tests
|
|
199
197
|
const playwrightProxy = {
|
|
200
198
|
// Set proxy mode before test
|
|
201
|
-
async before(testInfo: PlaywrightTestInfo, mode: '
|
|
199
|
+
async before(testInfo: PlaywrightTestInfo, mode: 'record' | 'replay' | 'transparent'): Promise<void>;
|
|
202
200
|
|
|
203
201
|
// Reset to transparent mode after test
|
|
204
202
|
async after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
205
203
|
};
|
|
206
|
-
|
|
207
|
-
// Standalone functions for custom control:
|
|
208
|
-
import {
|
|
209
|
-
setProxyMode, // Set mode with custom session ID
|
|
210
|
-
generateSessionId, // Generate session ID from test info
|
|
211
|
-
startRecording, // Helper to start recording
|
|
212
|
-
startReplay, // Helper to start replay
|
|
213
|
-
stopProxy // Helper to stop recording/replay
|
|
214
|
-
} from 'test-proxy-recorder';
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Usage Pattern
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
test('your test', async ({ page }, testInfo) => {
|
|
221
|
-
// Setup: Set proxy mode at start of test
|
|
222
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
223
|
-
|
|
224
|
-
// Your test code here
|
|
225
|
-
await page.goto('/your-page');
|
|
226
|
-
await expect(page.getByText('Something')).toBeVisible();
|
|
227
|
-
|
|
228
|
-
// Cleanup: Return to transparent mode
|
|
229
|
-
await playwrightProxy.after(testInfo);
|
|
230
|
-
});
|
|
231
204
|
```
|
|
232
205
|
|
|
233
206
|
## Recording Format
|
|
@@ -252,20 +225,20 @@ Each recording contains:
|
|
|
252
225
|
1. **Start the proxy server** (runs continuously):
|
|
253
226
|
|
|
254
227
|
```bash
|
|
255
|
-
test-proxy-recorder http://localhost:
|
|
228
|
+
test-proxy-recorder http://localhost:8000 --port 8100
|
|
256
229
|
```
|
|
257
230
|
|
|
258
231
|
2. **Configure your app** to use the proxy:
|
|
259
232
|
|
|
260
233
|
```bash
|
|
261
|
-
export EXTERNAL_API_URL=http://localhost:
|
|
234
|
+
export EXTERNAL_API_URL=http://localhost:8100 yarn dev
|
|
262
235
|
```
|
|
263
236
|
|
|
264
237
|
3. **Record responses** (first run):
|
|
265
238
|
|
|
266
239
|
```typescript
|
|
267
240
|
test('my test', async ({ page }, testInfo) => {
|
|
268
|
-
await playwrightProxy.before(testInfo, '
|
|
241
|
+
await playwrightProxy.before(testInfo, 'record');
|
|
269
242
|
// Test interacts with real API through proxy
|
|
270
243
|
await page.goto('/my-page');
|
|
271
244
|
await playwrightProxy.after(testInfo);
|
|
@@ -283,68 +256,10 @@ Each recording contains:
|
|
|
283
256
|
});
|
|
284
257
|
```
|
|
285
258
|
|
|
286
|
-
## Use Cases
|
|
287
|
-
|
|
288
|
-
- **Fast CI/CD Tests**: Record API responses once, replay them for instant test execution
|
|
289
|
-
- **Deterministic Tests**: Same responses every time, no flaky network issues
|
|
290
|
-
- **API Contract Testing**: Verify frontend handles all API scenarios
|
|
291
|
-
- **WebSocket Testing**: Record and replay complex WebSocket message sequences
|
|
292
|
-
|
|
293
|
-
## Configuration Examples
|
|
294
|
-
|
|
295
|
-
### With Environment Variables
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
const proxy = new ProxyServer(
|
|
299
|
-
[process.env.BACKEND_URL || 'http://localhost:3000'],
|
|
300
|
-
process.env.RECORDINGS_DIR || './recordings'
|
|
301
|
-
);
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Multiple Backend Targets
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
// Round-robin load balancing between targets
|
|
308
|
-
const proxy = new ProxyServer(
|
|
309
|
-
[
|
|
310
|
-
'http://backend-1:3000',
|
|
311
|
-
'http://backend-2:3000',
|
|
312
|
-
'http://backend-3:3000'
|
|
313
|
-
],
|
|
314
|
-
'./recordings'
|
|
315
|
-
);
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
## WebSocket Support
|
|
319
|
-
|
|
320
|
-
WebSocket connections are automatically detected and recorded/replayed:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
test('websocket test', async ({ page }, testInfo) => {
|
|
324
|
-
// Recording WebSocket messages
|
|
325
|
-
await playwrightProxy.before(testInfo, 'recording');
|
|
326
|
-
|
|
327
|
-
await page.goto('/websocket-page');
|
|
328
|
-
// WebSocket connections and messages are recorded
|
|
329
|
-
|
|
330
|
-
await playwrightProxy.after(testInfo);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('websocket replay', async ({ page }, testInfo) => {
|
|
334
|
-
// Replay WebSocket messages
|
|
335
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
336
|
-
|
|
337
|
-
await page.goto('/websocket-page');
|
|
338
|
-
// Previously recorded WebSocket messages are replayed
|
|
339
|
-
|
|
340
|
-
await playwrightProxy.after(testInfo);
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
259
|
## Requirements
|
|
345
260
|
|
|
346
261
|
- Node.js >= 22.0.0
|
|
347
|
-
-
|
|
262
|
+
- @playwright/test >= 1.0.0
|
|
348
263
|
|
|
349
264
|
## License
|
|
350
265
|
|
|
@@ -353,11 +268,3 @@ MIT
|
|
|
353
268
|
## Contributing
|
|
354
269
|
|
|
355
270
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
356
|
-
|
|
357
|
-
## Author
|
|
358
|
-
|
|
359
|
-
asmyshlyaev177
|
|
360
|
-
|
|
361
|
-
## Repository
|
|
362
|
-
|
|
363
|
-
[https://github.com/asmyshlyaev177/test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { TestInfo } from '@playwright/test';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
|
|
4
|
+
declare const Modes: {
|
|
5
|
+
readonly transparent: "transparent";
|
|
6
|
+
readonly record: "record";
|
|
7
|
+
readonly replay: "replay";
|
|
8
|
+
};
|
|
9
|
+
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
10
|
+
interface ControlRequest {
|
|
11
|
+
mode: Mode;
|
|
12
|
+
id?: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
15
|
+
interface RecordedRequest {
|
|
16
|
+
method: string;
|
|
17
|
+
url: string;
|
|
18
|
+
headers: http.IncomingHttpHeaders;
|
|
19
|
+
body: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface RecordedResponse {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
headers: http.IncomingHttpHeaders;
|
|
24
|
+
body: string | null;
|
|
25
|
+
}
|
|
26
|
+
interface Recording {
|
|
27
|
+
request: RecordedRequest;
|
|
28
|
+
response?: RecordedResponse;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
interface WebSocketMessage {
|
|
33
|
+
direction: 'client-to-server' | 'server-to-client';
|
|
34
|
+
data: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
interface WebSocketRecording {
|
|
38
|
+
url: string;
|
|
39
|
+
messages: WebSocketMessage[];
|
|
40
|
+
timestamp: string;
|
|
41
|
+
key: string;
|
|
42
|
+
}
|
|
43
|
+
interface RecordingSession {
|
|
44
|
+
id: string;
|
|
45
|
+
recordings: Recording[];
|
|
46
|
+
websocketRecordings: WebSocketRecording[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
50
|
+
/**
|
|
51
|
+
* Set the proxy mode for a given session
|
|
52
|
+
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
53
|
+
* @param sessionId - Unique identifier for the session
|
|
54
|
+
* @param timeout - Optional timeout in milliseconds
|
|
55
|
+
*/
|
|
56
|
+
declare function setProxyMode(mode: Mode, sessionId: string, timeout?: number): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Generate a session ID from test info
|
|
59
|
+
* @param testInfo - Playwright test info object
|
|
60
|
+
*/
|
|
61
|
+
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
62
|
+
/**
|
|
63
|
+
* Start recording for a test
|
|
64
|
+
* @param testInfo - Playwright test info object
|
|
65
|
+
*/
|
|
66
|
+
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Start replay for a test
|
|
69
|
+
* @param testInfo - Playwright test info object
|
|
70
|
+
*/
|
|
71
|
+
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Stop recording/replay and return to transparent mode
|
|
74
|
+
* @param testInfo - Playwright test info object
|
|
75
|
+
*/
|
|
76
|
+
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Playwright test fixture helper for managing proxy mode
|
|
79
|
+
* Use this in beforeEach/afterEach hooks
|
|
80
|
+
*/
|
|
81
|
+
declare const playwrightProxy: {
|
|
82
|
+
/**
|
|
83
|
+
* Setup before test - sets the proxy mode
|
|
84
|
+
* @param testInfo - Playwright test info object
|
|
85
|
+
* @param mode - The proxy mode to use for this test
|
|
86
|
+
*/
|
|
87
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup after test - returns to transparent mode
|
|
90
|
+
* @param testInfo - Playwright test info object
|
|
91
|
+
*/
|
|
92
|
+
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type Recording as R, type WebSocketRecording as W, type RecordingSession as a, startRecording as b, startReplay as c, stopProxy as d, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { TestInfo } from '@playwright/test';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
|
|
4
|
+
declare const Modes: {
|
|
5
|
+
readonly transparent: "transparent";
|
|
6
|
+
readonly record: "record";
|
|
7
|
+
readonly replay: "replay";
|
|
8
|
+
};
|
|
9
|
+
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
10
|
+
interface ControlRequest {
|
|
11
|
+
mode: Mode;
|
|
12
|
+
id?: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
15
|
+
interface RecordedRequest {
|
|
16
|
+
method: string;
|
|
17
|
+
url: string;
|
|
18
|
+
headers: http.IncomingHttpHeaders;
|
|
19
|
+
body: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface RecordedResponse {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
headers: http.IncomingHttpHeaders;
|
|
24
|
+
body: string | null;
|
|
25
|
+
}
|
|
26
|
+
interface Recording {
|
|
27
|
+
request: RecordedRequest;
|
|
28
|
+
response?: RecordedResponse;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
interface WebSocketMessage {
|
|
33
|
+
direction: 'client-to-server' | 'server-to-client';
|
|
34
|
+
data: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
interface WebSocketRecording {
|
|
38
|
+
url: string;
|
|
39
|
+
messages: WebSocketMessage[];
|
|
40
|
+
timestamp: string;
|
|
41
|
+
key: string;
|
|
42
|
+
}
|
|
43
|
+
interface RecordingSession {
|
|
44
|
+
id: string;
|
|
45
|
+
recordings: Recording[];
|
|
46
|
+
websocketRecordings: WebSocketRecording[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
50
|
+
/**
|
|
51
|
+
* Set the proxy mode for a given session
|
|
52
|
+
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
53
|
+
* @param sessionId - Unique identifier for the session
|
|
54
|
+
* @param timeout - Optional timeout in milliseconds
|
|
55
|
+
*/
|
|
56
|
+
declare function setProxyMode(mode: Mode, sessionId: string, timeout?: number): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Generate a session ID from test info
|
|
59
|
+
* @param testInfo - Playwright test info object
|
|
60
|
+
*/
|
|
61
|
+
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
62
|
+
/**
|
|
63
|
+
* Start recording for a test
|
|
64
|
+
* @param testInfo - Playwright test info object
|
|
65
|
+
*/
|
|
66
|
+
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Start replay for a test
|
|
69
|
+
* @param testInfo - Playwright test info object
|
|
70
|
+
*/
|
|
71
|
+
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Stop recording/replay and return to transparent mode
|
|
74
|
+
* @param testInfo - Playwright test info object
|
|
75
|
+
*/
|
|
76
|
+
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Playwright test fixture helper for managing proxy mode
|
|
79
|
+
* Use this in beforeEach/afterEach hooks
|
|
80
|
+
*/
|
|
81
|
+
declare const playwrightProxy: {
|
|
82
|
+
/**
|
|
83
|
+
* Setup before test - sets the proxy mode
|
|
84
|
+
* @param testInfo - Playwright test info object
|
|
85
|
+
* @param mode - The proxy mode to use for this test
|
|
86
|
+
*/
|
|
87
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup after test - returns to transparent mode
|
|
90
|
+
* @param testInfo - Playwright test info object
|
|
91
|
+
*/
|
|
92
|
+
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type Recording as R, type WebSocketRecording as W, type RecordingSession as a, startRecording as b, startReplay as c, stopProxy as d, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ var http = require('http');
|
|
|
5
5
|
var httpProxy = require('http-proxy');
|
|
6
6
|
var ws = require('ws');
|
|
7
7
|
var path = require('path');
|
|
8
|
+
var filenamify = require('filenamify');
|
|
8
9
|
|
|
9
10
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
|
12
13
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
13
14
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
14
15
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
16
|
+
var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
|
|
15
17
|
|
|
16
18
|
// src/ProxyServer.ts
|
|
17
19
|
|
|
@@ -47,6 +49,24 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
47
49
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
48
50
|
);
|
|
49
51
|
}
|
|
52
|
+
var QUERY_HASH_LENGTH = 8;
|
|
53
|
+
function getReqID(req) {
|
|
54
|
+
const urlParts = req.url.split("?");
|
|
55
|
+
const pathname = urlParts[0];
|
|
56
|
+
const query = urlParts[1] || "";
|
|
57
|
+
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
58
|
+
const normalizedPath = filenamify__default.default(pathPart, { replacement: "_" });
|
|
59
|
+
const queryHash = generateQueryHash(query);
|
|
60
|
+
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
61
|
+
return filenamify__default.default(filename, { replacement: "_" });
|
|
62
|
+
}
|
|
63
|
+
function generateQueryHash(query) {
|
|
64
|
+
if (!query) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
68
|
+
return `_${hash}`;
|
|
69
|
+
}
|
|
50
70
|
|
|
51
71
|
// src/utils/httpHelpers.ts
|
|
52
72
|
var CONTENT_TYPE_JSON = "application/json";
|
|
@@ -62,28 +82,6 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
62
82
|
res.end(JSON.stringify(data));
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
// src/utils/requestKeyGenerator.ts
|
|
66
|
-
var QUERY_HASH_LENGTH = 8;
|
|
67
|
-
function generateRequestKey(req) {
|
|
68
|
-
const urlParts = req.url.split("?");
|
|
69
|
-
const pathname = urlParts[0];
|
|
70
|
-
const query = urlParts[1] || "";
|
|
71
|
-
const normalizedPath = normalizePathname(pathname);
|
|
72
|
-
const queryHash = generateQueryHash(query);
|
|
73
|
-
return `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
74
|
-
}
|
|
75
|
-
function normalizePathname(pathname) {
|
|
76
|
-
const normalized = pathname.replaceAll("/", "_").replace(/^_/, "");
|
|
77
|
-
return normalized || "root";
|
|
78
|
-
}
|
|
79
|
-
function generateQueryHash(query) {
|
|
80
|
-
if (!query) {
|
|
81
|
-
return "";
|
|
82
|
-
}
|
|
83
|
-
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
84
|
-
return `_${hash}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
85
|
// src/ProxyServer.ts
|
|
88
86
|
var ProxyServer = class {
|
|
89
87
|
targets;
|
|
@@ -209,6 +207,7 @@ var ProxyServer = class {
|
|
|
209
207
|
this.recordingId = null;
|
|
210
208
|
this.replayId = null;
|
|
211
209
|
this.currentSession = null;
|
|
210
|
+
clearTimeout(this.modeTimeout || 0);
|
|
212
211
|
console.log("Switched to transparent mode");
|
|
213
212
|
}
|
|
214
213
|
switchToRecordMode(id) {
|
|
@@ -259,7 +258,7 @@ var ProxyServer = class {
|
|
|
259
258
|
if (!this.currentSession) {
|
|
260
259
|
return;
|
|
261
260
|
}
|
|
262
|
-
const key =
|
|
261
|
+
const key = getReqID(req);
|
|
263
262
|
const record = {
|
|
264
263
|
request: {
|
|
265
264
|
method: req.method,
|
|
@@ -276,7 +275,7 @@ var ProxyServer = class {
|
|
|
276
275
|
if (!this.currentSession) {
|
|
277
276
|
return;
|
|
278
277
|
}
|
|
279
|
-
const key =
|
|
278
|
+
const key = getReqID(req);
|
|
280
279
|
const record = this.currentSession.recordings.find((r) => r.key === key);
|
|
281
280
|
if (!record) {
|
|
282
281
|
console.error("Request record not found for response:", key);
|
|
@@ -298,7 +297,7 @@ var ProxyServer = class {
|
|
|
298
297
|
});
|
|
299
298
|
}
|
|
300
299
|
async handleReplayRequest(req, res) {
|
|
301
|
-
const key =
|
|
300
|
+
const key = getReqID(req);
|
|
302
301
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
303
302
|
try {
|
|
304
303
|
const session = await loadRecordingSession(filePath);
|
|
@@ -555,15 +554,15 @@ function generateSessionId(testInfo) {
|
|
|
555
554
|
}
|
|
556
555
|
async function startRecording(testInfo) {
|
|
557
556
|
const sessionId = generateSessionId(testInfo);
|
|
558
|
-
await setProxyMode(
|
|
557
|
+
await setProxyMode(Modes.record, sessionId);
|
|
559
558
|
}
|
|
560
559
|
async function startReplay(testInfo) {
|
|
561
560
|
const sessionId = generateSessionId(testInfo);
|
|
562
|
-
await setProxyMode(
|
|
561
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
563
562
|
}
|
|
564
563
|
async function stopProxy(testInfo) {
|
|
565
564
|
const sessionId = generateSessionId(testInfo);
|
|
566
|
-
await setProxyMode(
|
|
565
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
567
566
|
}
|
|
568
567
|
var playwrightProxy = {
|
|
569
568
|
/**
|
|
@@ -582,7 +581,7 @@ var playwrightProxy = {
|
|
|
582
581
|
*/
|
|
583
582
|
async after(testInfo) {
|
|
584
583
|
const sessionId = generateSessionId(testInfo);
|
|
585
|
-
await setProxyMode(
|
|
584
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
586
585
|
}
|
|
587
586
|
};
|
|
588
587
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { PlaywrightTestInfo,
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-DfFpm8mB.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -40,49 +40,4 @@ declare class ProxyServer {
|
|
|
40
40
|
private logServerStartup;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
readonly transparent: "transparent";
|
|
45
|
-
readonly record: "record";
|
|
46
|
-
readonly replay: "replay";
|
|
47
|
-
};
|
|
48
|
-
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
49
|
-
interface ControlRequest {
|
|
50
|
-
mode: Mode;
|
|
51
|
-
id?: string;
|
|
52
|
-
timeout?: number;
|
|
53
|
-
}
|
|
54
|
-
interface RecordedRequest {
|
|
55
|
-
method: string;
|
|
56
|
-
url: string;
|
|
57
|
-
headers: http.IncomingHttpHeaders;
|
|
58
|
-
body: string | null;
|
|
59
|
-
}
|
|
60
|
-
interface RecordedResponse {
|
|
61
|
-
statusCode: number;
|
|
62
|
-
headers: http.IncomingHttpHeaders;
|
|
63
|
-
body: string | null;
|
|
64
|
-
}
|
|
65
|
-
interface Recording {
|
|
66
|
-
request: RecordedRequest;
|
|
67
|
-
response?: RecordedResponse;
|
|
68
|
-
timestamp: string;
|
|
69
|
-
key: string;
|
|
70
|
-
}
|
|
71
|
-
interface WebSocketMessage {
|
|
72
|
-
direction: 'client-to-server' | 'server-to-client';
|
|
73
|
-
data: string;
|
|
74
|
-
timestamp: string;
|
|
75
|
-
}
|
|
76
|
-
interface WebSocketRecording {
|
|
77
|
-
url: string;
|
|
78
|
-
messages: WebSocketMessage[];
|
|
79
|
-
timestamp: string;
|
|
80
|
-
key: string;
|
|
81
|
-
}
|
|
82
|
-
interface RecordingSession {
|
|
83
|
-
id: string;
|
|
84
|
-
recordings: Recording[];
|
|
85
|
-
websocketRecordings: WebSocketRecording[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { type ControlRequest, type Mode, ProxyServer, type Recording, type RecordingSession, type WebSocketRecording };
|
|
43
|
+
export { ProxyServer };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { PlaywrightTestInfo,
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-DfFpm8mB.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -40,49 +40,4 @@ declare class ProxyServer {
|
|
|
40
40
|
private logServerStartup;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
readonly transparent: "transparent";
|
|
45
|
-
readonly record: "record";
|
|
46
|
-
readonly replay: "replay";
|
|
47
|
-
};
|
|
48
|
-
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
49
|
-
interface ControlRequest {
|
|
50
|
-
mode: Mode;
|
|
51
|
-
id?: string;
|
|
52
|
-
timeout?: number;
|
|
53
|
-
}
|
|
54
|
-
interface RecordedRequest {
|
|
55
|
-
method: string;
|
|
56
|
-
url: string;
|
|
57
|
-
headers: http.IncomingHttpHeaders;
|
|
58
|
-
body: string | null;
|
|
59
|
-
}
|
|
60
|
-
interface RecordedResponse {
|
|
61
|
-
statusCode: number;
|
|
62
|
-
headers: http.IncomingHttpHeaders;
|
|
63
|
-
body: string | null;
|
|
64
|
-
}
|
|
65
|
-
interface Recording {
|
|
66
|
-
request: RecordedRequest;
|
|
67
|
-
response?: RecordedResponse;
|
|
68
|
-
timestamp: string;
|
|
69
|
-
key: string;
|
|
70
|
-
}
|
|
71
|
-
interface WebSocketMessage {
|
|
72
|
-
direction: 'client-to-server' | 'server-to-client';
|
|
73
|
-
data: string;
|
|
74
|
-
timestamp: string;
|
|
75
|
-
}
|
|
76
|
-
interface WebSocketRecording {
|
|
77
|
-
url: string;
|
|
78
|
-
messages: WebSocketMessage[];
|
|
79
|
-
timestamp: string;
|
|
80
|
-
key: string;
|
|
81
|
-
}
|
|
82
|
-
interface RecordingSession {
|
|
83
|
-
id: string;
|
|
84
|
-
recordings: Recording[];
|
|
85
|
-
websocketRecordings: WebSocketRecording[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { type ControlRequest, type Mode, ProxyServer, type Recording, type RecordingSession, type WebSocketRecording };
|
|
43
|
+
export { ProxyServer };
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import http from 'http';
|
|
|
3
3
|
import httpProxy from 'http-proxy';
|
|
4
4
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import filenamify from 'filenamify';
|
|
6
7
|
|
|
7
8
|
// src/ProxyServer.ts
|
|
8
9
|
|
|
@@ -38,6 +39,24 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
38
39
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
39
40
|
);
|
|
40
41
|
}
|
|
42
|
+
var QUERY_HASH_LENGTH = 8;
|
|
43
|
+
function getReqID(req) {
|
|
44
|
+
const urlParts = req.url.split("?");
|
|
45
|
+
const pathname = urlParts[0];
|
|
46
|
+
const query = urlParts[1] || "";
|
|
47
|
+
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
48
|
+
const normalizedPath = filenamify(pathPart, { replacement: "_" });
|
|
49
|
+
const queryHash = generateQueryHash(query);
|
|
50
|
+
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
51
|
+
return filenamify(filename, { replacement: "_" });
|
|
52
|
+
}
|
|
53
|
+
function generateQueryHash(query) {
|
|
54
|
+
if (!query) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
58
|
+
return `_${hash}`;
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
// src/utils/httpHelpers.ts
|
|
43
62
|
var CONTENT_TYPE_JSON = "application/json";
|
|
@@ -53,28 +72,6 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
53
72
|
res.end(JSON.stringify(data));
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
// src/utils/requestKeyGenerator.ts
|
|
57
|
-
var QUERY_HASH_LENGTH = 8;
|
|
58
|
-
function generateRequestKey(req) {
|
|
59
|
-
const urlParts = req.url.split("?");
|
|
60
|
-
const pathname = urlParts[0];
|
|
61
|
-
const query = urlParts[1] || "";
|
|
62
|
-
const normalizedPath = normalizePathname(pathname);
|
|
63
|
-
const queryHash = generateQueryHash(query);
|
|
64
|
-
return `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
65
|
-
}
|
|
66
|
-
function normalizePathname(pathname) {
|
|
67
|
-
const normalized = pathname.replaceAll("/", "_").replace(/^_/, "");
|
|
68
|
-
return normalized || "root";
|
|
69
|
-
}
|
|
70
|
-
function generateQueryHash(query) {
|
|
71
|
-
if (!query) {
|
|
72
|
-
return "";
|
|
73
|
-
}
|
|
74
|
-
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
75
|
-
return `_${hash}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
75
|
// src/ProxyServer.ts
|
|
79
76
|
var ProxyServer = class {
|
|
80
77
|
targets;
|
|
@@ -200,6 +197,7 @@ var ProxyServer = class {
|
|
|
200
197
|
this.recordingId = null;
|
|
201
198
|
this.replayId = null;
|
|
202
199
|
this.currentSession = null;
|
|
200
|
+
clearTimeout(this.modeTimeout || 0);
|
|
203
201
|
console.log("Switched to transparent mode");
|
|
204
202
|
}
|
|
205
203
|
switchToRecordMode(id) {
|
|
@@ -250,7 +248,7 @@ var ProxyServer = class {
|
|
|
250
248
|
if (!this.currentSession) {
|
|
251
249
|
return;
|
|
252
250
|
}
|
|
253
|
-
const key =
|
|
251
|
+
const key = getReqID(req);
|
|
254
252
|
const record = {
|
|
255
253
|
request: {
|
|
256
254
|
method: req.method,
|
|
@@ -267,7 +265,7 @@ var ProxyServer = class {
|
|
|
267
265
|
if (!this.currentSession) {
|
|
268
266
|
return;
|
|
269
267
|
}
|
|
270
|
-
const key =
|
|
268
|
+
const key = getReqID(req);
|
|
271
269
|
const record = this.currentSession.recordings.find((r) => r.key === key);
|
|
272
270
|
if (!record) {
|
|
273
271
|
console.error("Request record not found for response:", key);
|
|
@@ -289,7 +287,7 @@ var ProxyServer = class {
|
|
|
289
287
|
});
|
|
290
288
|
}
|
|
291
289
|
async handleReplayRequest(req, res) {
|
|
292
|
-
const key =
|
|
290
|
+
const key = getReqID(req);
|
|
293
291
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
294
292
|
try {
|
|
295
293
|
const session = await loadRecordingSession(filePath);
|
|
@@ -546,15 +544,15 @@ function generateSessionId(testInfo) {
|
|
|
546
544
|
}
|
|
547
545
|
async function startRecording(testInfo) {
|
|
548
546
|
const sessionId = generateSessionId(testInfo);
|
|
549
|
-
await setProxyMode(
|
|
547
|
+
await setProxyMode(Modes.record, sessionId);
|
|
550
548
|
}
|
|
551
549
|
async function startReplay(testInfo) {
|
|
552
550
|
const sessionId = generateSessionId(testInfo);
|
|
553
|
-
await setProxyMode(
|
|
551
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
554
552
|
}
|
|
555
553
|
async function stopProxy(testInfo) {
|
|
556
554
|
const sessionId = generateSessionId(testInfo);
|
|
557
|
-
await setProxyMode(
|
|
555
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
558
556
|
}
|
|
559
557
|
var playwrightProxy = {
|
|
560
558
|
/**
|
|
@@ -573,7 +571,7 @@ var playwrightProxy = {
|
|
|
573
571
|
*/
|
|
574
572
|
async after(testInfo) {
|
|
575
573
|
const sessionId = generateSessionId(testInfo);
|
|
576
|
-
await setProxyMode(
|
|
574
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
577
575
|
}
|
|
578
576
|
};
|
|
579
577
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var Modes = {
|
|
5
|
+
transparent: "transparent",
|
|
6
|
+
record: "record",
|
|
7
|
+
replay: "replay"
|
|
8
|
+
};
|
|
9
|
+
|
|
3
10
|
// src/playwright/index.ts
|
|
4
11
|
var INTERNAL_API_URL = process.env.INTERNAL_API_URL || "http://localhost:8100";
|
|
5
12
|
async function setProxyMode(mode, sessionId, timeout) {
|
|
@@ -31,15 +38,15 @@ function generateSessionId(testInfo) {
|
|
|
31
38
|
}
|
|
32
39
|
async function startRecording(testInfo) {
|
|
33
40
|
const sessionId = generateSessionId(testInfo);
|
|
34
|
-
await setProxyMode(
|
|
41
|
+
await setProxyMode(Modes.record, sessionId);
|
|
35
42
|
}
|
|
36
43
|
async function startReplay(testInfo) {
|
|
37
44
|
const sessionId = generateSessionId(testInfo);
|
|
38
|
-
await setProxyMode(
|
|
45
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
39
46
|
}
|
|
40
47
|
async function stopProxy(testInfo) {
|
|
41
48
|
const sessionId = generateSessionId(testInfo);
|
|
42
|
-
await setProxyMode(
|
|
49
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
43
50
|
}
|
|
44
51
|
var playwrightProxy = {
|
|
45
52
|
/**
|
|
@@ -58,7 +65,7 @@ var playwrightProxy = {
|
|
|
58
65
|
*/
|
|
59
66
|
async after(testInfo) {
|
|
60
67
|
const sessionId = generateSessionId(testInfo);
|
|
61
|
-
await setProxyMode(
|
|
68
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
62
69
|
}
|
|
63
70
|
};
|
|
64
71
|
|
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
5
|
-
/**
|
|
6
|
-
* Set the proxy mode for a given session
|
|
7
|
-
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
8
|
-
* @param sessionId - Unique identifier for the session
|
|
9
|
-
* @param timeout - Optional timeout in milliseconds
|
|
10
|
-
*/
|
|
11
|
-
declare function setProxyMode(mode: ProxyMode, sessionId: string, timeout?: number): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
-
* Generate a session ID from test info
|
|
14
|
-
* @param testInfo - Playwright test info object
|
|
15
|
-
*/
|
|
16
|
-
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
17
|
-
/**
|
|
18
|
-
* Start recording for a test
|
|
19
|
-
* @param testInfo - Playwright test info object
|
|
20
|
-
*/
|
|
21
|
-
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Start replay for a test
|
|
24
|
-
* @param testInfo - Playwright test info object
|
|
25
|
-
*/
|
|
26
|
-
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Stop recording/replay and return to transparent mode
|
|
29
|
-
* @param testInfo - Playwright test info object
|
|
30
|
-
*/
|
|
31
|
-
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Playwright test fixture helper for managing proxy mode
|
|
34
|
-
* Use this in beforeEach/afterEach hooks
|
|
35
|
-
*/
|
|
36
|
-
declare const playwrightProxy: {
|
|
37
|
-
/**
|
|
38
|
-
* Setup before test - sets the proxy mode
|
|
39
|
-
* @param testInfo - Playwright test info object
|
|
40
|
-
* @param mode - The proxy mode to use for this test
|
|
41
|
-
*/
|
|
42
|
-
before(testInfo: PlaywrightTestInfo, mode: ProxyMode): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* Cleanup after test - returns to transparent mode
|
|
45
|
-
* @param testInfo - Playwright test info object
|
|
46
|
-
*/
|
|
47
|
-
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export { type PlaywrightTestInfo, type ProxyMode, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
|
|
1
|
+
import '@playwright/test';
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-DfFpm8mB.cjs';
|
|
3
|
+
import 'node:http';
|
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
5
|
-
/**
|
|
6
|
-
* Set the proxy mode for a given session
|
|
7
|
-
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
8
|
-
* @param sessionId - Unique identifier for the session
|
|
9
|
-
* @param timeout - Optional timeout in milliseconds
|
|
10
|
-
*/
|
|
11
|
-
declare function setProxyMode(mode: ProxyMode, sessionId: string, timeout?: number): Promise<void>;
|
|
12
|
-
/**
|
|
13
|
-
* Generate a session ID from test info
|
|
14
|
-
* @param testInfo - Playwright test info object
|
|
15
|
-
*/
|
|
16
|
-
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
17
|
-
/**
|
|
18
|
-
* Start recording for a test
|
|
19
|
-
* @param testInfo - Playwright test info object
|
|
20
|
-
*/
|
|
21
|
-
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Start replay for a test
|
|
24
|
-
* @param testInfo - Playwright test info object
|
|
25
|
-
*/
|
|
26
|
-
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Stop recording/replay and return to transparent mode
|
|
29
|
-
* @param testInfo - Playwright test info object
|
|
30
|
-
*/
|
|
31
|
-
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Playwright test fixture helper for managing proxy mode
|
|
34
|
-
* Use this in beforeEach/afterEach hooks
|
|
35
|
-
*/
|
|
36
|
-
declare const playwrightProxy: {
|
|
37
|
-
/**
|
|
38
|
-
* Setup before test - sets the proxy mode
|
|
39
|
-
* @param testInfo - Playwright test info object
|
|
40
|
-
* @param mode - The proxy mode to use for this test
|
|
41
|
-
*/
|
|
42
|
-
before(testInfo: PlaywrightTestInfo, mode: ProxyMode): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* Cleanup after test - returns to transparent mode
|
|
45
|
-
* @param testInfo - Playwright test info object
|
|
46
|
-
*/
|
|
47
|
-
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export { type PlaywrightTestInfo, type ProxyMode, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
|
|
1
|
+
import '@playwright/test';
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-DfFpm8mB.js';
|
|
3
|
+
import 'node:http';
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var Modes = {
|
|
3
|
+
transparent: "transparent",
|
|
4
|
+
record: "record",
|
|
5
|
+
replay: "replay"
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
// src/playwright/index.ts
|
|
2
9
|
var INTERNAL_API_URL = process.env.INTERNAL_API_URL || "http://localhost:8100";
|
|
3
10
|
async function setProxyMode(mode, sessionId, timeout) {
|
|
@@ -29,15 +36,15 @@ function generateSessionId(testInfo) {
|
|
|
29
36
|
}
|
|
30
37
|
async function startRecording(testInfo) {
|
|
31
38
|
const sessionId = generateSessionId(testInfo);
|
|
32
|
-
await setProxyMode(
|
|
39
|
+
await setProxyMode(Modes.record, sessionId);
|
|
33
40
|
}
|
|
34
41
|
async function startReplay(testInfo) {
|
|
35
42
|
const sessionId = generateSessionId(testInfo);
|
|
36
|
-
await setProxyMode(
|
|
43
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
37
44
|
}
|
|
38
45
|
async function stopProxy(testInfo) {
|
|
39
46
|
const sessionId = generateSessionId(testInfo);
|
|
40
|
-
await setProxyMode(
|
|
47
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
41
48
|
}
|
|
42
49
|
var playwrightProxy = {
|
|
43
50
|
/**
|
|
@@ -56,7 +63,7 @@ var playwrightProxy = {
|
|
|
56
63
|
*/
|
|
57
64
|
async after(testInfo) {
|
|
58
65
|
const sessionId = generateSessionId(testInfo);
|
|
59
|
-
await setProxyMode(
|
|
66
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
60
67
|
}
|
|
61
68
|
};
|
|
62
69
|
|
package/dist/proxy.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import httpProxy from 'http-proxy';
|
|
6
6
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
|
+
import filenamify from 'filenamify';
|
|
7
8
|
|
|
8
9
|
// src/cli.ts
|
|
9
10
|
var DEFAULT_PORT = 8e3;
|
|
@@ -72,6 +73,24 @@ async function saveRecordingSession(recordingsDir2, session) {
|
|
|
72
73
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
73
74
|
);
|
|
74
75
|
}
|
|
76
|
+
var QUERY_HASH_LENGTH = 8;
|
|
77
|
+
function getReqID(req) {
|
|
78
|
+
const urlParts = req.url.split("?");
|
|
79
|
+
const pathname = urlParts[0];
|
|
80
|
+
const query = urlParts[1] || "";
|
|
81
|
+
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
82
|
+
const normalizedPath = filenamify(pathPart, { replacement: "_" });
|
|
83
|
+
const queryHash = generateQueryHash(query);
|
|
84
|
+
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
85
|
+
return filenamify(filename, { replacement: "_" });
|
|
86
|
+
}
|
|
87
|
+
function generateQueryHash(query) {
|
|
88
|
+
if (!query) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
92
|
+
return `_${hash}`;
|
|
93
|
+
}
|
|
75
94
|
|
|
76
95
|
// src/utils/httpHelpers.ts
|
|
77
96
|
var CONTENT_TYPE_JSON = "application/json";
|
|
@@ -87,28 +106,6 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
87
106
|
res.end(JSON.stringify(data));
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
// src/utils/requestKeyGenerator.ts
|
|
91
|
-
var QUERY_HASH_LENGTH = 8;
|
|
92
|
-
function generateRequestKey(req) {
|
|
93
|
-
const urlParts = req.url.split("?");
|
|
94
|
-
const pathname = urlParts[0];
|
|
95
|
-
const query = urlParts[1] || "";
|
|
96
|
-
const normalizedPath = normalizePathname(pathname);
|
|
97
|
-
const queryHash = generateQueryHash(query);
|
|
98
|
-
return `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
99
|
-
}
|
|
100
|
-
function normalizePathname(pathname) {
|
|
101
|
-
const normalized = pathname.replaceAll("/", "_").replace(/^_/, "");
|
|
102
|
-
return normalized || "root";
|
|
103
|
-
}
|
|
104
|
-
function generateQueryHash(query) {
|
|
105
|
-
if (!query) {
|
|
106
|
-
return "";
|
|
107
|
-
}
|
|
108
|
-
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
109
|
-
return `_${hash}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
109
|
// src/ProxyServer.ts
|
|
113
110
|
var ProxyServer = class {
|
|
114
111
|
targets;
|
|
@@ -234,6 +231,7 @@ var ProxyServer = class {
|
|
|
234
231
|
this.recordingId = null;
|
|
235
232
|
this.replayId = null;
|
|
236
233
|
this.currentSession = null;
|
|
234
|
+
clearTimeout(this.modeTimeout || 0);
|
|
237
235
|
console.log("Switched to transparent mode");
|
|
238
236
|
}
|
|
239
237
|
switchToRecordMode(id) {
|
|
@@ -284,7 +282,7 @@ var ProxyServer = class {
|
|
|
284
282
|
if (!this.currentSession) {
|
|
285
283
|
return;
|
|
286
284
|
}
|
|
287
|
-
const key =
|
|
285
|
+
const key = getReqID(req);
|
|
288
286
|
const record = {
|
|
289
287
|
request: {
|
|
290
288
|
method: req.method,
|
|
@@ -301,7 +299,7 @@ var ProxyServer = class {
|
|
|
301
299
|
if (!this.currentSession) {
|
|
302
300
|
return;
|
|
303
301
|
}
|
|
304
|
-
const key =
|
|
302
|
+
const key = getReqID(req);
|
|
305
303
|
const record = this.currentSession.recordings.find((r) => r.key === key);
|
|
306
304
|
if (!record) {
|
|
307
305
|
console.error("Request record not found for response:", key);
|
|
@@ -323,7 +321,7 @@ var ProxyServer = class {
|
|
|
323
321
|
});
|
|
324
322
|
}
|
|
325
323
|
async handleReplayRequest(req, res) {
|
|
326
|
-
const key =
|
|
324
|
+
const key = getReqID(req);
|
|
327
325
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
328
326
|
try {
|
|
329
327
|
const session = await loadRecordingSession(filePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"commander": "^12.0.0",
|
|
82
|
+
"filenamify": "^7.0.1",
|
|
82
83
|
"http-proxy": "^1.18.1"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|