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 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 { setProxyMode } from 'test-proxy-recorder';
102
+ import { playwrightProxy } from 'test-proxy-recorder';
103
103
 
104
104
  async function globalTeardown() {
105
- await setProxyMode('transparent').catch(err => { console.error(err) });
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 { setProxyMode } from 'test-proxy-recorder';
256
+ import { playwrightProxy } from 'test-proxy-recorder';
248
257
 
249
258
  async function globalTeardown() {
250
- await setProxyMode('transparent').catch(err => { console.error(err) });
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 - returns to transparent mode
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 - returns to transparent mode
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 filenamify = require('filenamify');
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 filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
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
- return path__default.default.join(recordingsDir, `${id}.mock.json`);
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
- const dirPath = path__default.default.dirname(filePath);
49
- await fs__default.default.mkdir(dirPath, { recursive: true });
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(session, null, JSON_INDENT_SPACES)
82
+ JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
53
83
  );
54
84
  console.log(
55
- `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
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 = filenamify__default.default(pathPart, { replacement: "_" });
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 filenamify__default.default(filename, { replacement: "_" });
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) => a.recordingId - b.recordingId);
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
- `[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
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
- `[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
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
- console.log("Proxy setup:", { mode, sessionId });
930
- await setProxyMode(mode, sessionId);
964
+ await setProxyMode(mode, sessionId, timeout);
931
965
  },
932
966
  /**
933
- * Cleanup after test - returns to transparent mode
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.transparent, sessionId);
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-CjM3evKb.cjs';
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-CjM3evKb.js';
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 filenamify from 'filenamify';
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
- return path.join(recordingsDir, `${id}.mock.json`);
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
- const dirPath = path.dirname(filePath);
37
- await fs.mkdir(dirPath, { recursive: true });
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(session, null, JSON_INDENT_SPACES)
70
+ JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
41
71
  );
42
72
  console.log(
43
- `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
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 = filenamify(pathPart, { replacement: "_" });
81
+ const normalizedPath = filenamify2(pathPart, { replacement: "_" });
52
82
  const queryHash = generateQueryHash(query);
53
83
  const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
54
- return filenamify(filename, { replacement: "_" });
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) => a.recordingId - b.recordingId);
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
- `[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
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
- `[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
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
- console.log("Proxy setup:", { mode, sessionId });
918
- await setProxyMode(mode, sessionId);
952
+ await setProxyMode(mode, sessionId, timeout);
919
953
  },
920
954
  /**
921
- * Cleanup after test - returns to transparent mode
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.transparent, sessionId);
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
- console.log("Proxy setup:", { mode, sessionId });
96
- await setProxyMode(mode, sessionId);
96
+ await setProxyMode(mode, sessionId, timeout);
97
97
  },
98
98
  /**
99
- * Cleanup after test - returns to transparent mode
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.transparent, sessionId);
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-CjM3evKb.cjs';
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-CjM3evKb.js';
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
- console.log("Proxy setup:", { mode, sessionId });
94
- await setProxyMode(mode, sessionId);
94
+ await setProxyMode(mode, sessionId, timeout);
95
95
  },
96
96
  /**
97
- * Cleanup after test - returns to transparent mode
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.transparent, sessionId);
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 path2 from 'path';
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 filenamify from 'filenamify';
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 = path2.resolve(process.cwd(), options.recordingsDir);
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
- return path2.join(recordingsDir2, `${id}.mock.json`);
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
- const dirPath = path2.dirname(filePath);
71
- await fs.mkdir(dirPath, { recursive: true });
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(session, null, JSON_INDENT_SPACES)
104
+ JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
75
105
  );
76
106
  console.log(
77
- `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
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 = filenamify(pathPart, { replacement: "_" });
115
+ const normalizedPath = filenamify2(pathPart, { replacement: "_" });
86
116
  const queryHash = generateQueryHash(query);
87
117
  const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
88
- return filenamify(filename, { replacement: "_" });
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) => a.recordingId - b.recordingId);
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
- `[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
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
- `[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
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.1.10",
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",