test-proxy-recorder 0.3.1 → 0.3.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 +142 -17
- package/dist/{index-CVuiglPk.d.cts → index-BlBWqSE4.d.cts} +21 -4
- package/dist/{index-CVuiglPk.d.ts → index-BlBWqSE4.d.ts} +21 -4
- package/dist/index.cjs +269 -63
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.mjs +268 -62
- package/dist/playwright/index.cjs +129 -14
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.mjs +125 -15
- package/dist/proxy.js +145 -45
- package/package.json +1 -1
|
@@ -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 { f as ClientSideRecordingOptions, P as PlaywrightTestInfo, e as cleanupSession, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-BlBWqSE4.js';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
// src/playwright/index.ts
|
|
2
4
|
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
3
5
|
|
|
4
6
|
// src/types.ts
|
|
@@ -38,12 +40,34 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
38
40
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
39
41
|
}
|
|
40
42
|
await response.json();
|
|
41
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
42
43
|
} catch (error) {
|
|
43
44
|
console.error(`Error setting proxy mode:`, error);
|
|
44
45
|
throw error;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
async function cleanupSession(sessionId) {
|
|
49
|
+
const proxyPort = getProxyPort();
|
|
50
|
+
try {
|
|
51
|
+
const body = {
|
|
52
|
+
cleanup: true,
|
|
53
|
+
id: sessionId
|
|
54
|
+
};
|
|
55
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/json" },
|
|
58
|
+
body: JSON.stringify(body)
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
console.error(`Failed to cleanup session ${sessionId}:`, text);
|
|
63
|
+
throw new Error(`Failed to cleanup session: ${text}`);
|
|
64
|
+
}
|
|
65
|
+
await response.json();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
47
71
|
function parseSpecFilePath(specPath) {
|
|
48
72
|
const folderMatch = specPath.match(/^(.+?)\/([^/]+)\.(spec|test)\.ts$/);
|
|
49
73
|
if (folderMatch) {
|
|
@@ -85,6 +109,50 @@ async function stopProxy(testInfo) {
|
|
|
85
109
|
const sessionId = generateSessionId(testInfo);
|
|
86
110
|
await setProxyMode(Modes.transparent, sessionId);
|
|
87
111
|
}
|
|
112
|
+
var cachedRecordingsDir = null;
|
|
113
|
+
async function getRecordingsDir() {
|
|
114
|
+
if (cachedRecordingsDir) {
|
|
115
|
+
return cachedRecordingsDir;
|
|
116
|
+
}
|
|
117
|
+
const proxyPort = getProxyPort();
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`);
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
if (data.recordingsDir) {
|
|
123
|
+
cachedRecordingsDir = data.recordingsDir;
|
|
124
|
+
return cachedRecordingsDir;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(
|
|
129
|
+
"Failed to get recordings directory from proxy, using default:",
|
|
130
|
+
error
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
cachedRecordingsDir = path.join(process.cwd(), "e2e", "recordings");
|
|
134
|
+
return cachedRecordingsDir;
|
|
135
|
+
}
|
|
136
|
+
async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
137
|
+
const harFileName = sessionId.replaceAll("/", "__");
|
|
138
|
+
const recordingsDir = await getRecordingsDir();
|
|
139
|
+
const harPath = path.join(recordingsDir, `${harFileName}.har`);
|
|
140
|
+
try {
|
|
141
|
+
await page.routeFromHAR(harPath, {
|
|
142
|
+
url,
|
|
143
|
+
update: mode === Modes.record,
|
|
144
|
+
updateContent: "embed"
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (mode === Modes.replay) {
|
|
148
|
+
console.error(
|
|
149
|
+
`[Client-Side Replay] Failed to load HAR file. Run tests in record mode first.`,
|
|
150
|
+
error
|
|
151
|
+
);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
88
156
|
var playwrightProxy = {
|
|
89
157
|
/**
|
|
90
158
|
* Setup before test - sets the proxy mode and configures page with custom header
|
|
@@ -92,24 +160,66 @@ var playwrightProxy = {
|
|
|
92
160
|
* @param page - Playwright page object
|
|
93
161
|
* @param testInfo - Playwright test info object
|
|
94
162
|
* @param mode - The proxy mode to use for this test
|
|
95
|
-
* @param
|
|
163
|
+
* @param options - Optional configuration including timeout and client-side recording patterns
|
|
96
164
|
*/
|
|
97
|
-
async before(page, testInfo, mode,
|
|
165
|
+
async before(page, testInfo, mode, options) {
|
|
166
|
+
const timeout = typeof options === "number" ? options : options?.timeout;
|
|
167
|
+
const clientSideOptions = typeof options === "object" && options !== null ? options : void 0;
|
|
98
168
|
const sessionId = generateSessionId(testInfo);
|
|
99
169
|
await page.setExtraHTTPHeaders({
|
|
100
170
|
[RECORDING_ID_HEADER]: sessionId
|
|
101
171
|
});
|
|
102
172
|
await setProxyMode(mode, sessionId, timeout);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
173
|
+
if (clientSideOptions?.url) {
|
|
174
|
+
await setupClientSideRecording(
|
|
175
|
+
page,
|
|
176
|
+
sessionId,
|
|
177
|
+
mode,
|
|
178
|
+
clientSideOptions.url
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
182
|
+
const proxyUrl = `localhost:${proxyPort}`;
|
|
183
|
+
await page.route(
|
|
184
|
+
(url) => {
|
|
185
|
+
const urlStr = url.toString();
|
|
186
|
+
const matches = urlStr.includes(proxyUrl);
|
|
187
|
+
return matches;
|
|
188
|
+
},
|
|
189
|
+
async (route) => {
|
|
190
|
+
try {
|
|
191
|
+
const headers = route.request().headers();
|
|
192
|
+
headers[RECORDING_ID_HEADER] = sessionId;
|
|
193
|
+
await route.continue({ headers });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(
|
|
196
|
+
`[Route Handler Error] Failed to add ${RECORDING_ID_HEADER} header:`,
|
|
197
|
+
error
|
|
198
|
+
);
|
|
199
|
+
await route.fallback();
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{ times: Infinity }
|
|
203
|
+
// Ensure the handler applies to all matching requests
|
|
204
|
+
);
|
|
205
|
+
const context = page.context();
|
|
206
|
+
const contextId = context._guid || "default";
|
|
207
|
+
const handlerKey = `cleanup_${contextId}`;
|
|
208
|
+
if (!globalThis[handlerKey]) {
|
|
209
|
+
globalThis[handlerKey] = true;
|
|
210
|
+
context.on("close", async () => {
|
|
211
|
+
try {
|
|
212
|
+
await cleanupSession(sessionId);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn(
|
|
215
|
+
`[Cleanup] Failed to cleanup session ${sessionId}:`,
|
|
216
|
+
error
|
|
217
|
+
);
|
|
218
|
+
} finally {
|
|
219
|
+
delete globalThis[handlerKey];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
113
223
|
},
|
|
114
224
|
/**
|
|
115
225
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -120,6 +230,6 @@ var playwrightProxy = {
|
|
|
120
230
|
}
|
|
121
231
|
};
|
|
122
232
|
|
|
123
|
-
export { generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
|
|
233
|
+
export { cleanupSession, generateSessionId, playwrightProxy, setProxyMode, startRecording, startReplay, stopProxy };
|
|
124
234
|
//# sourceMappingURL=index.mjs.map
|
|
125
235
|
//# sourceMappingURL=index.mjs.map
|
package/dist/proxy.js
CHANGED
|
@@ -118,16 +118,19 @@ async function saveRecordingSession(recordingsDir2, session) {
|
|
|
118
118
|
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
|
-
function
|
|
122
|
-
const urlParts = req.url.split("?");
|
|
123
|
-
const pathname = urlParts[0];
|
|
124
|
-
const query = urlParts[1] || "";
|
|
121
|
+
function generateRecordingKey(pathname, query, method) {
|
|
125
122
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
126
123
|
const normalizedPath = filenamify2(pathPart, { replacement: "_" });
|
|
127
124
|
const queryHash = generateQueryHash(query);
|
|
128
|
-
const filename = `${
|
|
125
|
+
const filename = `${method}_${normalizedPath}${queryHash}.json`;
|
|
129
126
|
return filenamify2(filename, { replacement: "_" });
|
|
130
127
|
}
|
|
128
|
+
function getReqID(req) {
|
|
129
|
+
const urlParts = req.url.split("?");
|
|
130
|
+
const pathname = urlParts[0];
|
|
131
|
+
const query = urlParts[1] || "";
|
|
132
|
+
return generateRecordingKey(pathname, query, req.method);
|
|
133
|
+
}
|
|
131
134
|
function generateQueryHash(query) {
|
|
132
135
|
if (!query) {
|
|
133
136
|
return "";
|
|
@@ -169,6 +172,8 @@ var ProxyServer = class {
|
|
|
169
172
|
// Track multiple concurrent replay sessions by recording ID
|
|
170
173
|
recordingPromises;
|
|
171
174
|
// Stack of promises that resolve to completed recordings
|
|
175
|
+
flushPromise;
|
|
176
|
+
// Promise for in-progress flush operation
|
|
172
177
|
constructor(targets2, recordingsDir2) {
|
|
173
178
|
this.targets = targets2;
|
|
174
179
|
this.currentTargetIndex = 0;
|
|
@@ -182,6 +187,7 @@ var ProxyServer = class {
|
|
|
182
187
|
this.recordingsDir = recordingsDir2;
|
|
183
188
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
184
189
|
this.recordingPromises = [];
|
|
190
|
+
this.flushPromise = null;
|
|
185
191
|
this.proxy = httpProxy.createProxyServer({
|
|
186
192
|
secure: false,
|
|
187
193
|
changeOrigin: true,
|
|
@@ -280,7 +286,15 @@ var ProxyServer = class {
|
|
|
280
286
|
* @returns The recording ID, or null if not found
|
|
281
287
|
*/
|
|
282
288
|
getRecordingIdFromRequest(req) {
|
|
283
|
-
|
|
289
|
+
const fromHeader = this.getRecordingIdFromHeader(req);
|
|
290
|
+
const fromCookie = this.getRecordingIdFromCookie(req);
|
|
291
|
+
if (fromHeader) {
|
|
292
|
+
return fromHeader;
|
|
293
|
+
}
|
|
294
|
+
if (fromCookie) {
|
|
295
|
+
return fromCookie;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
284
298
|
}
|
|
285
299
|
/**
|
|
286
300
|
* Get or create a replay session state for a given recording ID
|
|
@@ -296,7 +310,8 @@ var ProxyServer = class {
|
|
|
296
310
|
recordingId,
|
|
297
311
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
298
312
|
loadedSession: null,
|
|
299
|
-
lastAccessTime: Date.now()
|
|
313
|
+
lastAccessTime: Date.now(),
|
|
314
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
300
315
|
};
|
|
301
316
|
this.replaySessions.set(recordingId, session);
|
|
302
317
|
console.log(
|
|
@@ -305,6 +320,24 @@ var ProxyServer = class {
|
|
|
305
320
|
}
|
|
306
321
|
return session;
|
|
307
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Clean up a session - removes it from memory and resets counters
|
|
325
|
+
* @param sessionId The session ID to clean up
|
|
326
|
+
*/
|
|
327
|
+
async cleanupSession(sessionId) {
|
|
328
|
+
if (this.replaySessions.has(sessionId)) {
|
|
329
|
+
this.replaySessions.delete(sessionId);
|
|
330
|
+
}
|
|
331
|
+
if (this.recordingId === sessionId) {
|
|
332
|
+
await this.saveCurrentSession();
|
|
333
|
+
this.currentSession = null;
|
|
334
|
+
this.recordingId = null;
|
|
335
|
+
}
|
|
336
|
+
if (this.replayId === sessionId) {
|
|
337
|
+
this.replayId = null;
|
|
338
|
+
}
|
|
339
|
+
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
340
|
+
}
|
|
308
341
|
parseGetParams(req) {
|
|
309
342
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
310
343
|
const mode = url.searchParams.get("mode");
|
|
@@ -328,9 +361,31 @@ var ProxyServer = class {
|
|
|
328
361
|
throw new Error("Unsupported control method");
|
|
329
362
|
}
|
|
330
363
|
async handleControlRequest(req, res) {
|
|
364
|
+
if (req.method === "GET") {
|
|
365
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
366
|
+
recordingsDir: this.recordingsDir,
|
|
367
|
+
mode: this.mode,
|
|
368
|
+
id: this.recordingId || this.replayId
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
331
372
|
try {
|
|
332
373
|
const data = await this.parseControlRequest(req);
|
|
333
|
-
const { mode, id, timeout: requestTimeout } = data;
|
|
374
|
+
const { mode, id, timeout: requestTimeout, cleanup } = data;
|
|
375
|
+
if (cleanup && id) {
|
|
376
|
+
await this.cleanupSession(id);
|
|
377
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
378
|
+
success: true,
|
|
379
|
+
message: `Session ${id} cleaned up`,
|
|
380
|
+
mode: this.mode
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!mode) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
"Mode parameter is required when cleanup is not specified"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
334
389
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
335
390
|
this.clearModeTimeout();
|
|
336
391
|
await this.switchMode(mode, id);
|
|
@@ -346,7 +401,8 @@ var ProxyServer = class {
|
|
|
346
401
|
success: true,
|
|
347
402
|
mode: this.mode,
|
|
348
403
|
id: this.recordingId || this.replayId,
|
|
349
|
-
timeout
|
|
404
|
+
timeout,
|
|
405
|
+
recordingsDir: this.recordingsDir
|
|
350
406
|
});
|
|
351
407
|
} catch (error) {
|
|
352
408
|
console.error("Control request error:", error);
|
|
@@ -411,16 +467,21 @@ var ProxyServer = class {
|
|
|
411
467
|
this.replayId = id;
|
|
412
468
|
this.recordingId = null;
|
|
413
469
|
this.currentSession = null;
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
470
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
471
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
472
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
473
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
474
|
+
try {
|
|
475
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
476
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
479
|
+
sessionState.loadedSession = null;
|
|
420
480
|
}
|
|
421
481
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
422
482
|
}
|
|
423
483
|
setupModeTimeout(timeout) {
|
|
484
|
+
clearTimeout(this.modeTimeout || 0);
|
|
424
485
|
this.modeTimeout = setTimeout(async () => {
|
|
425
486
|
console.log("Timeout reached, switching back to transparent mode");
|
|
426
487
|
await this.saveCurrentSession();
|
|
@@ -429,21 +490,32 @@ var ProxyServer = class {
|
|
|
429
490
|
}, timeout);
|
|
430
491
|
}
|
|
431
492
|
async flushPendingRecordings() {
|
|
493
|
+
if (this.flushPromise) {
|
|
494
|
+
await this.flushPromise;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
432
497
|
if (this.recordingPromises.length === 0) {
|
|
433
498
|
return;
|
|
434
499
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
500
|
+
this.flushPromise = (async () => {
|
|
501
|
+
try {
|
|
502
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
503
|
+
if (this.currentSession) {
|
|
504
|
+
for (const result of results) {
|
|
505
|
+
if (result.status === "fulfilled" && result.value) {
|
|
506
|
+
this.currentSession.recordings.push(result.value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(
|
|
510
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
511
|
+
);
|
|
440
512
|
}
|
|
513
|
+
this.recordingPromises = [];
|
|
514
|
+
} finally {
|
|
515
|
+
this.flushPromise = null;
|
|
441
516
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
this.recordingPromises = [];
|
|
517
|
+
})();
|
|
518
|
+
await this.flushPromise;
|
|
447
519
|
}
|
|
448
520
|
async saveCurrentSession() {
|
|
449
521
|
if (!this.currentSession) {
|
|
@@ -456,7 +528,29 @@ var ProxyServer = class {
|
|
|
456
528
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
457
529
|
}
|
|
458
530
|
getRecordingIdOrError(req, res) {
|
|
459
|
-
const
|
|
531
|
+
const recordingIdFromRequest = this.getRecordingIdFromRequest(req);
|
|
532
|
+
if (recordingIdFromRequest) {
|
|
533
|
+
return recordingIdFromRequest;
|
|
534
|
+
}
|
|
535
|
+
if (this.replaySessions.size > 1) {
|
|
536
|
+
console.warn(
|
|
537
|
+
`[CONCURRENT REPLAY WARNING] Request to ${req.method} ${req.url} is missing ${RECORDING_ID_HEADER} header/cookie. Active sessions: ${[...this.replaySessions.keys()].join(", ")}. this.replayId fallback would be: ${this.replayId} (NOT USING - could be wrong session)`
|
|
538
|
+
);
|
|
539
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
540
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
541
|
+
"Content-Type": "application/json",
|
|
542
|
+
...corsHeaders
|
|
543
|
+
});
|
|
544
|
+
res.end(
|
|
545
|
+
JSON.stringify({
|
|
546
|
+
error: "Missing recording ID in concurrent replay mode. Ensure x-test-rcrd-id header is set.",
|
|
547
|
+
activeSessions: [...this.replaySessions.keys()],
|
|
548
|
+
hint: "This usually means page.setExtraHTTPHeaders() did not apply to this request type"
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const recordingId = this.replayId;
|
|
460
554
|
if (!recordingId) {
|
|
461
555
|
const corsHeaders = this.getCorsHeaders(req);
|
|
462
556
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -466,22 +560,30 @@ var ProxyServer = class {
|
|
|
466
560
|
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
467
561
|
return null;
|
|
468
562
|
}
|
|
563
|
+
console.log(
|
|
564
|
+
`[FALLBACK] Using replayId fallback for ${req.method} ${req.url} -> session: ${recordingId} (single session mode)`
|
|
565
|
+
);
|
|
469
566
|
return recordingId;
|
|
470
567
|
}
|
|
471
|
-
async ensureSessionLoaded(recordingId, filePath) {
|
|
472
|
-
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
473
|
-
if (!sessionState.loadedSession) {
|
|
474
|
-
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
475
|
-
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
476
|
-
}
|
|
477
|
-
return sessionState;
|
|
478
|
-
}
|
|
479
568
|
getServedTracker(sessionState, key) {
|
|
480
569
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
481
570
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
482
571
|
}
|
|
483
572
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
484
573
|
}
|
|
574
|
+
getSortedRecordings(sessionState, key) {
|
|
575
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
576
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
577
|
+
}
|
|
578
|
+
const session = sessionState.loadedSession;
|
|
579
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
580
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
581
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
582
|
+
return aSeq - bSeq;
|
|
583
|
+
});
|
|
584
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
585
|
+
return sortedRecords;
|
|
586
|
+
}
|
|
485
587
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
486
588
|
for (const rec of recordsWithKey) {
|
|
487
589
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -502,18 +604,17 @@ var ProxyServer = class {
|
|
|
502
604
|
const key = getReqID(req);
|
|
503
605
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
504
606
|
try {
|
|
505
|
-
const sessionState =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
607
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
608
|
+
if (!sessionState.loadedSession) {
|
|
609
|
+
const error = new Error(
|
|
610
|
+
`Recording session file not found: ${filePath}`
|
|
611
|
+
);
|
|
612
|
+
error.code = "ENOENT";
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
510
615
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
511
616
|
const host = req.headers.host || "unknown";
|
|
512
|
-
const recordsWithKey =
|
|
513
|
-
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
514
|
-
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
515
|
-
return aSeq - bSeq;
|
|
516
|
-
});
|
|
617
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
517
618
|
if (recordsWithKey.length === 0) {
|
|
518
619
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
519
620
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -724,7 +825,6 @@ var ProxyServer = class {
|
|
|
724
825
|
})();
|
|
725
826
|
});
|
|
726
827
|
this.recordingPromises.push(recordingPromise);
|
|
727
|
-
await recordingPromise;
|
|
728
828
|
}
|
|
729
829
|
handleUpgrade(req, socket, head) {
|
|
730
830
|
if (this.mode === Modes.replay) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|