test-proxy-recorder 0.1.10 → 0.2.0
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 +32 -16
- package/dist/{index-CjM3evKb.d.cts → index-nmNRt1WE.d.cts} +10 -2
- package/dist/{index-CjM3evKb.d.ts → index-nmNRt1WE.d.ts} +10 -2
- package/dist/index.cjs +61 -19
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +59 -17
- package/dist/playwright/index.cjs +13 -5
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.mjs +13 -5
- package/dist/proxy.js +47 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,10 +99,10 @@ This marks recording files as binary, which causes long mock files to be collaps
|
|
|
99
99
|
Create `e2e/global-teardown.ts`:
|
|
100
100
|
|
|
101
101
|
```typescript
|
|
102
|
-
import {
|
|
102
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
103
103
|
|
|
104
104
|
async function globalTeardown() {
|
|
105
|
-
await
|
|
105
|
+
await playwrightProxy.teardown();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export default globalTeardown;
|
|
@@ -128,15 +128,17 @@ Create `e2e/example.spec.ts`:
|
|
|
128
128
|
import { test, expect } from '@playwright/test';
|
|
129
129
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
130
130
|
|
|
131
|
+
// Setup afterEach hook to reset proxy after each test
|
|
132
|
+
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
133
|
+
await playwrightProxy.after(testInfo);
|
|
134
|
+
});
|
|
135
|
+
|
|
131
136
|
test('example test with proxy', async ({ page }, testInfo) => {
|
|
132
137
|
// Set proxy mode: 'record' to capture, 'replay' to use recordings
|
|
133
138
|
await playwrightProxy.before(testInfo, 'replay');
|
|
134
139
|
|
|
135
140
|
await page.goto('/');
|
|
136
141
|
await expect(page.getByText('Welcome')).toBeVisible();
|
|
137
|
-
|
|
138
|
-
// Always cleanup after test
|
|
139
|
-
await playwrightProxy.after(testInfo);
|
|
140
142
|
});
|
|
141
143
|
```
|
|
142
144
|
|
|
@@ -186,27 +188,38 @@ test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
|
|
|
186
188
|
|
|
187
189
|
### Basic Test Structure
|
|
188
190
|
|
|
189
|
-
Every test using the proxy should follow this pattern:
|
|
191
|
+
Every test file using the proxy should follow this pattern:
|
|
190
192
|
|
|
191
193
|
```typescript
|
|
192
194
|
import { test } from '@playwright/test';
|
|
193
195
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
194
196
|
|
|
197
|
+
// Setup afterEach hook once per test file
|
|
198
|
+
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
199
|
+
await playwrightProxy.after(testInfo);
|
|
200
|
+
});
|
|
201
|
+
|
|
195
202
|
test('test name', async ({ page }, testInfo) => {
|
|
196
203
|
// 1. Set mode BEFORE test actions
|
|
197
204
|
await playwrightProxy.before(testInfo, 'replay');
|
|
198
205
|
|
|
199
206
|
// 2. Test code
|
|
200
207
|
await page.goto('/page');
|
|
201
|
-
|
|
202
|
-
// 3. Reset mode AFTER test completes
|
|
203
|
-
await playwrightProxy.after(testInfo);
|
|
208
|
+
// Test assertions...
|
|
204
209
|
});
|
|
205
210
|
```
|
|
206
211
|
|
|
207
212
|
### Recording vs Replay
|
|
208
213
|
|
|
209
214
|
```typescript
|
|
215
|
+
import { test } from '@playwright/test';
|
|
216
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
217
|
+
|
|
218
|
+
// Setup afterEach hook to automatically cleanup after each test
|
|
219
|
+
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
220
|
+
await playwrightProxy.after(testInfo);
|
|
221
|
+
});
|
|
222
|
+
|
|
210
223
|
// Recording mode - captures API responses
|
|
211
224
|
test('create user', async ({ page }, testInfo) => {
|
|
212
225
|
await playwrightProxy.before(testInfo, 'record');
|
|
@@ -214,8 +227,6 @@ test('create user', async ({ page }, testInfo) => {
|
|
|
214
227
|
await page.goto('/users/new');
|
|
215
228
|
await page.fill('[name="username"]', 'testuser');
|
|
216
229
|
await page.click('button[type="submit"]');
|
|
217
|
-
|
|
218
|
-
await playwrightProxy.after(testInfo);
|
|
219
230
|
});
|
|
220
231
|
|
|
221
232
|
// Replay mode - uses recorded responses
|
|
@@ -225,8 +236,6 @@ test('create user', async ({ page }, testInfo) => {
|
|
|
225
236
|
await page.goto('/users/new');
|
|
226
237
|
await page.fill('[name="username"]', 'testuser');
|
|
227
238
|
await page.click('button[type="submit"]');
|
|
228
|
-
|
|
229
|
-
await playwrightProxy.after(testInfo);
|
|
230
239
|
});
|
|
231
240
|
```
|
|
232
241
|
|
|
@@ -244,10 +253,10 @@ Recording files are auto-generated from test names:
|
|
|
244
253
|
Create `e2e/global-teardown.ts`:
|
|
245
254
|
|
|
246
255
|
```typescript
|
|
247
|
-
import {
|
|
256
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
248
257
|
|
|
249
258
|
async function globalTeardown() {
|
|
250
|
-
await
|
|
259
|
+
await playwrightProxy.teardown();
|
|
251
260
|
}
|
|
252
261
|
|
|
253
262
|
export default globalTeardown;
|
|
@@ -412,8 +421,13 @@ const playwrightProxy = {
|
|
|
412
421
|
timeout?: number
|
|
413
422
|
): Promise<void>;
|
|
414
423
|
|
|
415
|
-
// Reset to transparent mode after test
|
|
424
|
+
// Reset replay session and return to transparent mode after test
|
|
425
|
+
// Resets sequence counters to ensure next replay starts fresh
|
|
416
426
|
async after(testInfo: TestInfo): Promise<void>;
|
|
427
|
+
|
|
428
|
+
// Global teardown - switches proxy to transparent mode
|
|
429
|
+
// Use in Playwright's globalTeardown configuration
|
|
430
|
+
async teardown(): Promise<void>;
|
|
417
431
|
};
|
|
418
432
|
|
|
419
433
|
// Direct mode control
|
|
@@ -438,6 +452,8 @@ async function setProxyMode(
|
|
|
438
452
|
}
|
|
439
453
|
```
|
|
440
454
|
|
|
455
|
+
**Note**: Switching to replay mode automatically resets session counters (clears served recordings tracker), allowing replay from the beginning.
|
|
456
|
+
|
|
441
457
|
**Response**:
|
|
442
458
|
|
|
443
459
|
```typescript
|
|
@@ -28,6 +28,7 @@ interface Recording {
|
|
|
28
28
|
response?: RecordedResponse;
|
|
29
29
|
timestamp: string;
|
|
30
30
|
key: string;
|
|
31
|
+
sequence?: number;
|
|
31
32
|
recordingId: number;
|
|
32
33
|
}
|
|
33
34
|
interface WebSocketMessage {
|
|
@@ -88,13 +89,20 @@ declare const playwrightProxy: {
|
|
|
88
89
|
* Setup before test - sets the proxy mode
|
|
89
90
|
* @param testInfo - Playwright test info object
|
|
90
91
|
* @param mode - The proxy mode to use for this test
|
|
92
|
+
* @param timeout - Optional timeout in milliseconds
|
|
91
93
|
*/
|
|
92
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
94
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
93
95
|
/**
|
|
94
|
-
* Cleanup after test -
|
|
96
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
97
|
+
* switchToReplayMode automatically clears sequence counters
|
|
95
98
|
* @param testInfo - Playwright test info object
|
|
96
99
|
*/
|
|
97
100
|
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Global teardown - switches proxy to transparent mode
|
|
103
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
104
|
+
*/
|
|
105
|
+
teardown(): Promise<void>;
|
|
98
106
|
};
|
|
99
107
|
|
|
100
108
|
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 };
|
|
@@ -28,6 +28,7 @@ interface Recording {
|
|
|
28
28
|
response?: RecordedResponse;
|
|
29
29
|
timestamp: string;
|
|
30
30
|
key: string;
|
|
31
|
+
sequence?: number;
|
|
31
32
|
recordingId: number;
|
|
32
33
|
}
|
|
33
34
|
interface WebSocketMessage {
|
|
@@ -88,13 +89,20 @@ declare const playwrightProxy: {
|
|
|
88
89
|
* Setup before test - sets the proxy mode
|
|
89
90
|
* @param testInfo - Playwright test info object
|
|
90
91
|
* @param mode - The proxy mode to use for this test
|
|
92
|
+
* @param timeout - Optional timeout in milliseconds
|
|
91
93
|
*/
|
|
92
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
94
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
93
95
|
/**
|
|
94
|
-
* Cleanup after test -
|
|
96
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
97
|
+
* switchToReplayMode automatically clears sequence counters
|
|
95
98
|
* @param testInfo - Playwright test info object
|
|
96
99
|
*/
|
|
97
100
|
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Global teardown - switches proxy to transparent mode
|
|
103
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
104
|
+
*/
|
|
105
|
+
teardown(): Promise<void>;
|
|
98
106
|
};
|
|
99
107
|
|
|
100
108
|
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,9 +5,9 @@ var http = require('http');
|
|
|
5
5
|
var https = require('https');
|
|
6
6
|
var httpProxy = require('http-proxy');
|
|
7
7
|
var ws = require('ws');
|
|
8
|
-
var path = require('path');
|
|
9
8
|
var crypto = require('crypto');
|
|
10
|
-
var
|
|
9
|
+
var path = require('path');
|
|
10
|
+
var filenamify2 = require('filenamify');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
@@ -15,9 +15,9 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
|
15
15
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
16
16
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
17
17
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
18
|
-
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
19
18
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
20
|
-
var
|
|
19
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
|
+
var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
|
|
21
21
|
|
|
22
22
|
// src/ProxyServer.ts
|
|
23
23
|
|
|
@@ -36,23 +36,53 @@ var Modes = {
|
|
|
36
36
|
replay: "replay"
|
|
37
37
|
};
|
|
38
38
|
var JSON_INDENT_SPACES = 2;
|
|
39
|
+
var EXTENSION = ".mock.json";
|
|
40
|
+
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
41
|
+
var HASH_LENGTH = 8;
|
|
42
|
+
function generateHash(str) {
|
|
43
|
+
return crypto__default.default.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
|
|
44
|
+
}
|
|
39
45
|
function getRecordingPath(recordingsDir, id) {
|
|
40
|
-
|
|
46
|
+
let processedId = id.replaceAll("/", "__");
|
|
47
|
+
if (processedId.length > MAX_FILENAME_LENGTH) {
|
|
48
|
+
const hash = generateHash(id);
|
|
49
|
+
const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
|
|
50
|
+
processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
|
|
51
|
+
}
|
|
52
|
+
const sanitizedId = filenamify2__default.default(processedId, {
|
|
53
|
+
replacement: "_",
|
|
54
|
+
maxLength: 255
|
|
55
|
+
// Set explicit max to prevent filenamify's default truncation
|
|
56
|
+
});
|
|
57
|
+
return path__default.default.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
41
58
|
}
|
|
42
59
|
async function loadRecordingSession(filePath) {
|
|
43
60
|
const fileContent = await fs__default.default.readFile(filePath, "utf8");
|
|
44
61
|
return JSON.parse(fileContent);
|
|
45
62
|
}
|
|
63
|
+
function processRecordings(recordings) {
|
|
64
|
+
const keySequenceMap = /* @__PURE__ */ new Map();
|
|
65
|
+
return recordings.map((recording) => {
|
|
66
|
+
const key = recording.key;
|
|
67
|
+
const currentSeq = keySequenceMap.get(key) || 0;
|
|
68
|
+
keySequenceMap.set(key, currentSeq + 1);
|
|
69
|
+
return { ...recording, sequence: currentSeq };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
46
72
|
async function saveRecordingSession(recordingsDir, session) {
|
|
47
73
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
await fs__default.default.mkdir(recordingsDir, { recursive: true });
|
|
75
|
+
const processedRecordings = processRecordings(session.recordings);
|
|
76
|
+
const processedSession = {
|
|
77
|
+
...session,
|
|
78
|
+
recordings: processedRecordings
|
|
79
|
+
};
|
|
50
80
|
await fs__default.default.writeFile(
|
|
51
81
|
filePath,
|
|
52
|
-
JSON.stringify(
|
|
82
|
+
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
53
83
|
);
|
|
54
84
|
console.log(
|
|
55
|
-
`Saved ${
|
|
85
|
+
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
56
86
|
);
|
|
57
87
|
}
|
|
58
88
|
function getReqID(req) {
|
|
@@ -60,10 +90,10 @@ function getReqID(req) {
|
|
|
60
90
|
const pathname = urlParts[0];
|
|
61
91
|
const query = urlParts[1] || "";
|
|
62
92
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
63
|
-
const normalizedPath =
|
|
93
|
+
const normalizedPath = filenamify2__default.default(pathPart, { replacement: "_" });
|
|
64
94
|
const queryHash = generateQueryHash(query);
|
|
65
95
|
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
66
|
-
return
|
|
96
|
+
return filenamify2__default.default(filename, { replacement: "_" });
|
|
67
97
|
}
|
|
68
98
|
function generateQueryHash(query) {
|
|
69
99
|
if (!query) {
|
|
@@ -499,7 +529,11 @@ var ProxyServer = class {
|
|
|
499
529
|
}
|
|
500
530
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
501
531
|
const host = req.headers.host || "unknown";
|
|
502
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
532
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
533
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
534
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
535
|
+
return aSeq - bSeq;
|
|
536
|
+
});
|
|
503
537
|
if (recordsWithKey.length === 0) {
|
|
504
538
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
505
539
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -522,7 +556,7 @@ var ProxyServer = class {
|
|
|
522
556
|
}
|
|
523
557
|
const requestCount = servedForThisKey.size + 1;
|
|
524
558
|
console.log(
|
|
525
|
-
`[
|
|
559
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
526
560
|
);
|
|
527
561
|
let record;
|
|
528
562
|
for (const rec of recordsWithKey) {
|
|
@@ -539,7 +573,7 @@ var ProxyServer = class {
|
|
|
539
573
|
}
|
|
540
574
|
servedForThisKey.add(record.recordingId);
|
|
541
575
|
console.log(
|
|
542
|
-
`[
|
|
576
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
543
577
|
);
|
|
544
578
|
if (!record.response) {
|
|
545
579
|
throw new Error(
|
|
@@ -923,19 +957,27 @@ var playwrightProxy = {
|
|
|
923
957
|
* Setup before test - sets the proxy mode
|
|
924
958
|
* @param testInfo - Playwright test info object
|
|
925
959
|
* @param mode - The proxy mode to use for this test
|
|
960
|
+
* @param timeout - Optional timeout in milliseconds
|
|
926
961
|
*/
|
|
927
|
-
async before(testInfo, mode) {
|
|
962
|
+
async before(testInfo, mode, timeout) {
|
|
928
963
|
const sessionId = generateSessionId(testInfo);
|
|
929
|
-
|
|
930
|
-
await setProxyMode(mode, sessionId);
|
|
964
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
931
965
|
},
|
|
932
966
|
/**
|
|
933
|
-
* Cleanup after test -
|
|
967
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
968
|
+
* switchToReplayMode automatically clears sequence counters
|
|
934
969
|
* @param testInfo - Playwright test info object
|
|
935
970
|
*/
|
|
936
971
|
async after(testInfo) {
|
|
937
972
|
const sessionId = generateSessionId(testInfo);
|
|
938
|
-
await setProxyMode(Modes.
|
|
973
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
974
|
+
},
|
|
975
|
+
/**
|
|
976
|
+
* Global teardown - switches proxy to transparent mode
|
|
977
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
978
|
+
*/
|
|
979
|
+
async teardown() {
|
|
980
|
+
await setProxyMode(Modes.transparent);
|
|
939
981
|
}
|
|
940
982
|
};
|
|
941
983
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
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-
|
|
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-nmNRt1WE.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
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-
|
|
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-nmNRt1WE.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import http from 'http';
|
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import httpProxy from 'http-proxy';
|
|
5
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
|
-
import path from 'path';
|
|
7
6
|
import crypto from 'crypto';
|
|
8
|
-
import
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import filenamify2 from 'filenamify';
|
|
9
9
|
|
|
10
10
|
// src/ProxyServer.ts
|
|
11
11
|
|
|
@@ -24,23 +24,53 @@ var Modes = {
|
|
|
24
24
|
replay: "replay"
|
|
25
25
|
};
|
|
26
26
|
var JSON_INDENT_SPACES = 2;
|
|
27
|
+
var EXTENSION = ".mock.json";
|
|
28
|
+
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
29
|
+
var HASH_LENGTH = 8;
|
|
30
|
+
function generateHash(str) {
|
|
31
|
+
return crypto.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
|
|
32
|
+
}
|
|
27
33
|
function getRecordingPath(recordingsDir, id) {
|
|
28
|
-
|
|
34
|
+
let processedId = id.replaceAll("/", "__");
|
|
35
|
+
if (processedId.length > MAX_FILENAME_LENGTH) {
|
|
36
|
+
const hash = generateHash(id);
|
|
37
|
+
const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
|
|
38
|
+
processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
|
|
39
|
+
}
|
|
40
|
+
const sanitizedId = filenamify2(processedId, {
|
|
41
|
+
replacement: "_",
|
|
42
|
+
maxLength: 255
|
|
43
|
+
// Set explicit max to prevent filenamify's default truncation
|
|
44
|
+
});
|
|
45
|
+
return path.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
29
46
|
}
|
|
30
47
|
async function loadRecordingSession(filePath) {
|
|
31
48
|
const fileContent = await fs.readFile(filePath, "utf8");
|
|
32
49
|
return JSON.parse(fileContent);
|
|
33
50
|
}
|
|
51
|
+
function processRecordings(recordings) {
|
|
52
|
+
const keySequenceMap = /* @__PURE__ */ new Map();
|
|
53
|
+
return recordings.map((recording) => {
|
|
54
|
+
const key = recording.key;
|
|
55
|
+
const currentSeq = keySequenceMap.get(key) || 0;
|
|
56
|
+
keySequenceMap.set(key, currentSeq + 1);
|
|
57
|
+
return { ...recording, sequence: currentSeq };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
34
60
|
async function saveRecordingSession(recordingsDir, session) {
|
|
35
61
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
await fs.mkdir(recordingsDir, { recursive: true });
|
|
63
|
+
const processedRecordings = processRecordings(session.recordings);
|
|
64
|
+
const processedSession = {
|
|
65
|
+
...session,
|
|
66
|
+
recordings: processedRecordings
|
|
67
|
+
};
|
|
38
68
|
await fs.writeFile(
|
|
39
69
|
filePath,
|
|
40
|
-
JSON.stringify(
|
|
70
|
+
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
41
71
|
);
|
|
42
72
|
console.log(
|
|
43
|
-
`Saved ${
|
|
73
|
+
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
44
74
|
);
|
|
45
75
|
}
|
|
46
76
|
function getReqID(req) {
|
|
@@ -48,10 +78,10 @@ function getReqID(req) {
|
|
|
48
78
|
const pathname = urlParts[0];
|
|
49
79
|
const query = urlParts[1] || "";
|
|
50
80
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
51
|
-
const normalizedPath =
|
|
81
|
+
const normalizedPath = filenamify2(pathPart, { replacement: "_" });
|
|
52
82
|
const queryHash = generateQueryHash(query);
|
|
53
83
|
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
54
|
-
return
|
|
84
|
+
return filenamify2(filename, { replacement: "_" });
|
|
55
85
|
}
|
|
56
86
|
function generateQueryHash(query) {
|
|
57
87
|
if (!query) {
|
|
@@ -487,7 +517,11 @@ var ProxyServer = class {
|
|
|
487
517
|
}
|
|
488
518
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
489
519
|
const host = req.headers.host || "unknown";
|
|
490
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
520
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
521
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
522
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
523
|
+
return aSeq - bSeq;
|
|
524
|
+
});
|
|
491
525
|
if (recordsWithKey.length === 0) {
|
|
492
526
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
493
527
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -510,7 +544,7 @@ var ProxyServer = class {
|
|
|
510
544
|
}
|
|
511
545
|
const requestCount = servedForThisKey.size + 1;
|
|
512
546
|
console.log(
|
|
513
|
-
`[
|
|
547
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
514
548
|
);
|
|
515
549
|
let record;
|
|
516
550
|
for (const rec of recordsWithKey) {
|
|
@@ -527,7 +561,7 @@ var ProxyServer = class {
|
|
|
527
561
|
}
|
|
528
562
|
servedForThisKey.add(record.recordingId);
|
|
529
563
|
console.log(
|
|
530
|
-
`[
|
|
564
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
531
565
|
);
|
|
532
566
|
if (!record.response) {
|
|
533
567
|
throw new Error(
|
|
@@ -911,19 +945,27 @@ var playwrightProxy = {
|
|
|
911
945
|
* Setup before test - sets the proxy mode
|
|
912
946
|
* @param testInfo - Playwright test info object
|
|
913
947
|
* @param mode - The proxy mode to use for this test
|
|
948
|
+
* @param timeout - Optional timeout in milliseconds
|
|
914
949
|
*/
|
|
915
|
-
async before(testInfo, mode) {
|
|
950
|
+
async before(testInfo, mode, timeout) {
|
|
916
951
|
const sessionId = generateSessionId(testInfo);
|
|
917
|
-
|
|
918
|
-
await setProxyMode(mode, sessionId);
|
|
952
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
919
953
|
},
|
|
920
954
|
/**
|
|
921
|
-
* Cleanup after test -
|
|
955
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
956
|
+
* switchToReplayMode automatically clears sequence counters
|
|
922
957
|
* @param testInfo - Playwright test info object
|
|
923
958
|
*/
|
|
924
959
|
async after(testInfo) {
|
|
925
960
|
const sessionId = generateSessionId(testInfo);
|
|
926
|
-
await setProxyMode(Modes.
|
|
961
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
962
|
+
},
|
|
963
|
+
/**
|
|
964
|
+
* Global teardown - switches proxy to transparent mode
|
|
965
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
966
|
+
*/
|
|
967
|
+
async teardown() {
|
|
968
|
+
await setProxyMode(Modes.transparent);
|
|
927
969
|
}
|
|
928
970
|
};
|
|
929
971
|
|
|
@@ -89,19 +89,27 @@ var playwrightProxy = {
|
|
|
89
89
|
* Setup before test - sets the proxy mode
|
|
90
90
|
* @param testInfo - Playwright test info object
|
|
91
91
|
* @param mode - The proxy mode to use for this test
|
|
92
|
+
* @param timeout - Optional timeout in milliseconds
|
|
92
93
|
*/
|
|
93
|
-
async before(testInfo, mode) {
|
|
94
|
+
async before(testInfo, mode, timeout) {
|
|
94
95
|
const sessionId = generateSessionId(testInfo);
|
|
95
|
-
|
|
96
|
-
await setProxyMode(mode, sessionId);
|
|
96
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
97
97
|
},
|
|
98
98
|
/**
|
|
99
|
-
* Cleanup after test -
|
|
99
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
100
|
+
* switchToReplayMode automatically clears sequence counters
|
|
100
101
|
* @param testInfo - Playwright test info object
|
|
101
102
|
*/
|
|
102
103
|
async after(testInfo) {
|
|
103
104
|
const sessionId = generateSessionId(testInfo);
|
|
104
|
-
await setProxyMode(Modes.
|
|
105
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Global teardown - switches proxy to transparent mode
|
|
109
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
110
|
+
*/
|
|
111
|
+
async teardown() {
|
|
112
|
+
await setProxyMode(Modes.transparent);
|
|
105
113
|
}
|
|
106
114
|
};
|
|
107
115
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
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-
|
|
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-nmNRt1WE.cjs';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
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-
|
|
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-nmNRt1WE.js';
|
|
3
3
|
import 'node:http';
|
|
@@ -87,19 +87,27 @@ var playwrightProxy = {
|
|
|
87
87
|
* Setup before test - sets the proxy mode
|
|
88
88
|
* @param testInfo - Playwright test info object
|
|
89
89
|
* @param mode - The proxy mode to use for this test
|
|
90
|
+
* @param timeout - Optional timeout in milliseconds
|
|
90
91
|
*/
|
|
91
|
-
async before(testInfo, mode) {
|
|
92
|
+
async before(testInfo, mode, timeout) {
|
|
92
93
|
const sessionId = generateSessionId(testInfo);
|
|
93
|
-
|
|
94
|
-
await setProxyMode(mode, sessionId);
|
|
94
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
95
95
|
},
|
|
96
96
|
/**
|
|
97
|
-
* Cleanup after test -
|
|
97
|
+
* Cleanup after test - resets replay session by re-entering replay mode
|
|
98
|
+
* switchToReplayMode automatically clears sequence counters
|
|
98
99
|
* @param testInfo - Playwright test info object
|
|
99
100
|
*/
|
|
100
101
|
async after(testInfo) {
|
|
101
102
|
const sessionId = generateSessionId(testInfo);
|
|
102
|
-
await setProxyMode(Modes.
|
|
103
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Global teardown - switches proxy to transparent mode
|
|
107
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
108
|
+
*/
|
|
109
|
+
async teardown() {
|
|
110
|
+
await setProxyMode(Modes.transparent);
|
|
103
111
|
}
|
|
104
112
|
};
|
|
105
113
|
|
package/dist/proxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'path';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import http from 'http';
|
|
@@ -6,7 +6,7 @@ import https from 'https';
|
|
|
6
6
|
import httpProxy from 'http-proxy';
|
|
7
7
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
|
-
import
|
|
9
|
+
import filenamify2 from 'filenamify';
|
|
10
10
|
|
|
11
11
|
// src/cli.ts
|
|
12
12
|
var DEFAULT_PORT = 8e3;
|
|
@@ -39,7 +39,7 @@ function parseCliArgs() {
|
|
|
39
39
|
if (targets2.length === 0) {
|
|
40
40
|
program.help();
|
|
41
41
|
}
|
|
42
|
-
const recordingsDir2 =
|
|
42
|
+
const recordingsDir2 = path.resolve(process.cwd(), options.recordingsDir);
|
|
43
43
|
return { targets: targets2, port: port2, recordingsDir: recordingsDir2 };
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -58,23 +58,53 @@ var Modes = {
|
|
|
58
58
|
replay: "replay"
|
|
59
59
|
};
|
|
60
60
|
var JSON_INDENT_SPACES = 2;
|
|
61
|
+
var EXTENSION = ".mock.json";
|
|
62
|
+
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
63
|
+
var HASH_LENGTH = 8;
|
|
64
|
+
function generateHash(str) {
|
|
65
|
+
return crypto.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
|
|
66
|
+
}
|
|
61
67
|
function getRecordingPath(recordingsDir2, id) {
|
|
62
|
-
|
|
68
|
+
let processedId = id.replaceAll("/", "__");
|
|
69
|
+
if (processedId.length > MAX_FILENAME_LENGTH) {
|
|
70
|
+
const hash = generateHash(id);
|
|
71
|
+
const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
|
|
72
|
+
processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
|
|
73
|
+
}
|
|
74
|
+
const sanitizedId = filenamify2(processedId, {
|
|
75
|
+
replacement: "_",
|
|
76
|
+
maxLength: 255
|
|
77
|
+
// Set explicit max to prevent filenamify's default truncation
|
|
78
|
+
});
|
|
79
|
+
return path.join(recordingsDir2, `${sanitizedId}${EXTENSION}`);
|
|
63
80
|
}
|
|
64
81
|
async function loadRecordingSession(filePath) {
|
|
65
82
|
const fileContent = await fs.readFile(filePath, "utf8");
|
|
66
83
|
return JSON.parse(fileContent);
|
|
67
84
|
}
|
|
85
|
+
function processRecordings(recordings) {
|
|
86
|
+
const keySequenceMap = /* @__PURE__ */ new Map();
|
|
87
|
+
return recordings.map((recording) => {
|
|
88
|
+
const key = recording.key;
|
|
89
|
+
const currentSeq = keySequenceMap.get(key) || 0;
|
|
90
|
+
keySequenceMap.set(key, currentSeq + 1);
|
|
91
|
+
return { ...recording, sequence: currentSeq };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
68
94
|
async function saveRecordingSession(recordingsDir2, session) {
|
|
69
95
|
const filePath = getRecordingPath(recordingsDir2, session.id);
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
await fs.mkdir(recordingsDir2, { recursive: true });
|
|
97
|
+
const processedRecordings = processRecordings(session.recordings);
|
|
98
|
+
const processedSession = {
|
|
99
|
+
...session,
|
|
100
|
+
recordings: processedRecordings
|
|
101
|
+
};
|
|
72
102
|
await fs.writeFile(
|
|
73
103
|
filePath,
|
|
74
|
-
JSON.stringify(
|
|
104
|
+
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
75
105
|
);
|
|
76
106
|
console.log(
|
|
77
|
-
`Saved ${
|
|
107
|
+
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
78
108
|
);
|
|
79
109
|
}
|
|
80
110
|
function getReqID(req) {
|
|
@@ -82,10 +112,10 @@ function getReqID(req) {
|
|
|
82
112
|
const pathname = urlParts[0];
|
|
83
113
|
const query = urlParts[1] || "";
|
|
84
114
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
85
|
-
const normalizedPath =
|
|
115
|
+
const normalizedPath = filenamify2(pathPart, { replacement: "_" });
|
|
86
116
|
const queryHash = generateQueryHash(query);
|
|
87
117
|
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
88
|
-
return
|
|
118
|
+
return filenamify2(filename, { replacement: "_" });
|
|
89
119
|
}
|
|
90
120
|
function generateQueryHash(query) {
|
|
91
121
|
if (!query) {
|
|
@@ -521,7 +551,11 @@ var ProxyServer = class {
|
|
|
521
551
|
}
|
|
522
552
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
523
553
|
const host = req.headers.host || "unknown";
|
|
524
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
554
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
555
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
556
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
557
|
+
return aSeq - bSeq;
|
|
558
|
+
});
|
|
525
559
|
if (recordsWithKey.length === 0) {
|
|
526
560
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
527
561
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -544,7 +578,7 @@ var ProxyServer = class {
|
|
|
544
578
|
}
|
|
545
579
|
const requestCount = servedForThisKey.size + 1;
|
|
546
580
|
console.log(
|
|
547
|
-
`[
|
|
581
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
548
582
|
);
|
|
549
583
|
let record;
|
|
550
584
|
for (const rec of recordsWithKey) {
|
|
@@ -561,7 +595,7 @@ var ProxyServer = class {
|
|
|
561
595
|
}
|
|
562
596
|
servedForThisKey.add(record.recordingId);
|
|
563
597
|
console.log(
|
|
564
|
-
`[
|
|
598
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
565
599
|
);
|
|
566
600
|
if (!record.response) {
|
|
567
601
|
throw new Error(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|