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
package/dist/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ var https = require('https');
|
|
|
6
6
|
var httpProxy = require('http-proxy');
|
|
7
7
|
var ws = require('ws');
|
|
8
8
|
var crypto = require('crypto');
|
|
9
|
-
var
|
|
9
|
+
var path2 = require('path');
|
|
10
10
|
var filenamify2 = require('filenamify');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -16,7 +16,7 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
16
16
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
17
17
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
18
18
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
19
|
-
var
|
|
19
|
+
var path2__default = /*#__PURE__*/_interopDefault(path2);
|
|
20
20
|
var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
|
|
21
21
|
|
|
22
22
|
// src/constants.ts
|
|
@@ -53,7 +53,7 @@ function getRecordingPath(recordingsDir, id) {
|
|
|
53
53
|
maxLength: 255
|
|
54
54
|
// Set explicit max to prevent filenamify's default truncation
|
|
55
55
|
});
|
|
56
|
-
return
|
|
56
|
+
return path2__default.default.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
57
57
|
}
|
|
58
58
|
async function loadRecordingSession(filePath) {
|
|
59
59
|
const fileContent = await fs__default.default.readFile(filePath, "utf8");
|
|
@@ -94,16 +94,19 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
94
94
|
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
|
-
function
|
|
98
|
-
const urlParts = req.url.split("?");
|
|
99
|
-
const pathname = urlParts[0];
|
|
100
|
-
const query = urlParts[1] || "";
|
|
97
|
+
function generateRecordingKey(pathname, query, method) {
|
|
101
98
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
102
99
|
const normalizedPath = filenamify2__default.default(pathPart, { replacement: "_" });
|
|
103
100
|
const queryHash = generateQueryHash(query);
|
|
104
|
-
const filename = `${
|
|
101
|
+
const filename = `${method}_${normalizedPath}${queryHash}.json`;
|
|
105
102
|
return filenamify2__default.default(filename, { replacement: "_" });
|
|
106
103
|
}
|
|
104
|
+
function getReqID(req) {
|
|
105
|
+
const urlParts = req.url.split("?");
|
|
106
|
+
const pathname = urlParts[0];
|
|
107
|
+
const query = urlParts[1] || "";
|
|
108
|
+
return generateRecordingKey(pathname, query, req.method);
|
|
109
|
+
}
|
|
107
110
|
function generateQueryHash(query) {
|
|
108
111
|
if (!query) {
|
|
109
112
|
return "";
|
|
@@ -145,6 +148,8 @@ var ProxyServer = class {
|
|
|
145
148
|
// Track multiple concurrent replay sessions by recording ID
|
|
146
149
|
recordingPromises;
|
|
147
150
|
// Stack of promises that resolve to completed recordings
|
|
151
|
+
flushPromise;
|
|
152
|
+
// Promise for in-progress flush operation
|
|
148
153
|
constructor(targets, recordingsDir) {
|
|
149
154
|
this.targets = targets;
|
|
150
155
|
this.currentTargetIndex = 0;
|
|
@@ -158,6 +163,7 @@ var ProxyServer = class {
|
|
|
158
163
|
this.recordingsDir = recordingsDir;
|
|
159
164
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
160
165
|
this.recordingPromises = [];
|
|
166
|
+
this.flushPromise = null;
|
|
161
167
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
162
168
|
secure: false,
|
|
163
169
|
changeOrigin: true,
|
|
@@ -256,7 +262,15 @@ var ProxyServer = class {
|
|
|
256
262
|
* @returns The recording ID, or null if not found
|
|
257
263
|
*/
|
|
258
264
|
getRecordingIdFromRequest(req) {
|
|
259
|
-
|
|
265
|
+
const fromHeader = this.getRecordingIdFromHeader(req);
|
|
266
|
+
const fromCookie = this.getRecordingIdFromCookie(req);
|
|
267
|
+
if (fromHeader) {
|
|
268
|
+
return fromHeader;
|
|
269
|
+
}
|
|
270
|
+
if (fromCookie) {
|
|
271
|
+
return fromCookie;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
260
274
|
}
|
|
261
275
|
/**
|
|
262
276
|
* Get or create a replay session state for a given recording ID
|
|
@@ -272,7 +286,8 @@ var ProxyServer = class {
|
|
|
272
286
|
recordingId,
|
|
273
287
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
274
288
|
loadedSession: null,
|
|
275
|
-
lastAccessTime: Date.now()
|
|
289
|
+
lastAccessTime: Date.now(),
|
|
290
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
276
291
|
};
|
|
277
292
|
this.replaySessions.set(recordingId, session);
|
|
278
293
|
console.log(
|
|
@@ -281,6 +296,24 @@ var ProxyServer = class {
|
|
|
281
296
|
}
|
|
282
297
|
return session;
|
|
283
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Clean up a session - removes it from memory and resets counters
|
|
301
|
+
* @param sessionId The session ID to clean up
|
|
302
|
+
*/
|
|
303
|
+
async cleanupSession(sessionId) {
|
|
304
|
+
if (this.replaySessions.has(sessionId)) {
|
|
305
|
+
this.replaySessions.delete(sessionId);
|
|
306
|
+
}
|
|
307
|
+
if (this.recordingId === sessionId) {
|
|
308
|
+
await this.saveCurrentSession();
|
|
309
|
+
this.currentSession = null;
|
|
310
|
+
this.recordingId = null;
|
|
311
|
+
}
|
|
312
|
+
if (this.replayId === sessionId) {
|
|
313
|
+
this.replayId = null;
|
|
314
|
+
}
|
|
315
|
+
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
316
|
+
}
|
|
284
317
|
parseGetParams(req) {
|
|
285
318
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
286
319
|
const mode = url.searchParams.get("mode");
|
|
@@ -304,9 +337,31 @@ var ProxyServer = class {
|
|
|
304
337
|
throw new Error("Unsupported control method");
|
|
305
338
|
}
|
|
306
339
|
async handleControlRequest(req, res) {
|
|
340
|
+
if (req.method === "GET") {
|
|
341
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
342
|
+
recordingsDir: this.recordingsDir,
|
|
343
|
+
mode: this.mode,
|
|
344
|
+
id: this.recordingId || this.replayId
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
307
348
|
try {
|
|
308
349
|
const data = await this.parseControlRequest(req);
|
|
309
|
-
const { mode, id, timeout: requestTimeout } = data;
|
|
350
|
+
const { mode, id, timeout: requestTimeout, cleanup } = data;
|
|
351
|
+
if (cleanup && id) {
|
|
352
|
+
await this.cleanupSession(id);
|
|
353
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
354
|
+
success: true,
|
|
355
|
+
message: `Session ${id} cleaned up`,
|
|
356
|
+
mode: this.mode
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!mode) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"Mode parameter is required when cleanup is not specified"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
310
365
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
311
366
|
this.clearModeTimeout();
|
|
312
367
|
await this.switchMode(mode, id);
|
|
@@ -322,7 +377,8 @@ var ProxyServer = class {
|
|
|
322
377
|
success: true,
|
|
323
378
|
mode: this.mode,
|
|
324
379
|
id: this.recordingId || this.replayId,
|
|
325
|
-
timeout
|
|
380
|
+
timeout,
|
|
381
|
+
recordingsDir: this.recordingsDir
|
|
326
382
|
});
|
|
327
383
|
} catch (error) {
|
|
328
384
|
console.error("Control request error:", error);
|
|
@@ -387,16 +443,21 @@ var ProxyServer = class {
|
|
|
387
443
|
this.replayId = id;
|
|
388
444
|
this.recordingId = null;
|
|
389
445
|
this.currentSession = null;
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
446
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
447
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
448
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
449
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
450
|
+
try {
|
|
451
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
452
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
455
|
+
sessionState.loadedSession = null;
|
|
396
456
|
}
|
|
397
457
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
398
458
|
}
|
|
399
459
|
setupModeTimeout(timeout) {
|
|
460
|
+
clearTimeout(this.modeTimeout || 0);
|
|
400
461
|
this.modeTimeout = setTimeout(async () => {
|
|
401
462
|
console.log("Timeout reached, switching back to transparent mode");
|
|
402
463
|
await this.saveCurrentSession();
|
|
@@ -405,21 +466,32 @@ var ProxyServer = class {
|
|
|
405
466
|
}, timeout);
|
|
406
467
|
}
|
|
407
468
|
async flushPendingRecordings() {
|
|
469
|
+
if (this.flushPromise) {
|
|
470
|
+
await this.flushPromise;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
408
473
|
if (this.recordingPromises.length === 0) {
|
|
409
474
|
return;
|
|
410
475
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
476
|
+
this.flushPromise = (async () => {
|
|
477
|
+
try {
|
|
478
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
479
|
+
if (this.currentSession) {
|
|
480
|
+
for (const result of results) {
|
|
481
|
+
if (result.status === "fulfilled" && result.value) {
|
|
482
|
+
this.currentSession.recordings.push(result.value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log(
|
|
486
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
487
|
+
);
|
|
416
488
|
}
|
|
489
|
+
this.recordingPromises = [];
|
|
490
|
+
} finally {
|
|
491
|
+
this.flushPromise = null;
|
|
417
492
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
this.recordingPromises = [];
|
|
493
|
+
})();
|
|
494
|
+
await this.flushPromise;
|
|
423
495
|
}
|
|
424
496
|
async saveCurrentSession() {
|
|
425
497
|
if (!this.currentSession) {
|
|
@@ -432,7 +504,29 @@ var ProxyServer = class {
|
|
|
432
504
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
433
505
|
}
|
|
434
506
|
getRecordingIdOrError(req, res) {
|
|
435
|
-
const
|
|
507
|
+
const recordingIdFromRequest = this.getRecordingIdFromRequest(req);
|
|
508
|
+
if (recordingIdFromRequest) {
|
|
509
|
+
return recordingIdFromRequest;
|
|
510
|
+
}
|
|
511
|
+
if (this.replaySessions.size > 1) {
|
|
512
|
+
console.warn(
|
|
513
|
+
`[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)`
|
|
514
|
+
);
|
|
515
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
516
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
517
|
+
"Content-Type": "application/json",
|
|
518
|
+
...corsHeaders
|
|
519
|
+
});
|
|
520
|
+
res.end(
|
|
521
|
+
JSON.stringify({
|
|
522
|
+
error: "Missing recording ID in concurrent replay mode. Ensure x-test-rcrd-id header is set.",
|
|
523
|
+
activeSessions: [...this.replaySessions.keys()],
|
|
524
|
+
hint: "This usually means page.setExtraHTTPHeaders() did not apply to this request type"
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const recordingId = this.replayId;
|
|
436
530
|
if (!recordingId) {
|
|
437
531
|
const corsHeaders = this.getCorsHeaders(req);
|
|
438
532
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -442,22 +536,30 @@ var ProxyServer = class {
|
|
|
442
536
|
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
443
537
|
return null;
|
|
444
538
|
}
|
|
539
|
+
console.log(
|
|
540
|
+
`[FALLBACK] Using replayId fallback for ${req.method} ${req.url} -> session: ${recordingId} (single session mode)`
|
|
541
|
+
);
|
|
445
542
|
return recordingId;
|
|
446
543
|
}
|
|
447
|
-
async ensureSessionLoaded(recordingId, filePath) {
|
|
448
|
-
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
449
|
-
if (!sessionState.loadedSession) {
|
|
450
|
-
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
451
|
-
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
452
|
-
}
|
|
453
|
-
return sessionState;
|
|
454
|
-
}
|
|
455
544
|
getServedTracker(sessionState, key) {
|
|
456
545
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
457
546
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
458
547
|
}
|
|
459
548
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
460
549
|
}
|
|
550
|
+
getSortedRecordings(sessionState, key) {
|
|
551
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
552
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
553
|
+
}
|
|
554
|
+
const session = sessionState.loadedSession;
|
|
555
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
556
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
557
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
558
|
+
return aSeq - bSeq;
|
|
559
|
+
});
|
|
560
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
561
|
+
return sortedRecords;
|
|
562
|
+
}
|
|
461
563
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
462
564
|
for (const rec of recordsWithKey) {
|
|
463
565
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -478,18 +580,17 @@ var ProxyServer = class {
|
|
|
478
580
|
const key = getReqID(req);
|
|
479
581
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
480
582
|
try {
|
|
481
|
-
const sessionState =
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
583
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
584
|
+
if (!sessionState.loadedSession) {
|
|
585
|
+
const error = new Error(
|
|
586
|
+
`Recording session file not found: ${filePath}`
|
|
587
|
+
);
|
|
588
|
+
error.code = "ENOENT";
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
486
591
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
487
592
|
const host = req.headers.host || "unknown";
|
|
488
|
-
const recordsWithKey =
|
|
489
|
-
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
490
|
-
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
491
|
-
return aSeq - bSeq;
|
|
492
|
-
});
|
|
593
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
493
594
|
if (recordsWithKey.length === 0) {
|
|
494
595
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
495
596
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -700,7 +801,6 @@ var ProxyServer = class {
|
|
|
700
801
|
})();
|
|
701
802
|
});
|
|
702
803
|
this.recordingPromises.push(recordingPromise);
|
|
703
|
-
await recordingPromise;
|
|
704
804
|
}
|
|
705
805
|
handleUpgrade(req, socket, head) {
|
|
706
806
|
if (this.mode === Modes.replay) {
|
|
@@ -865,8 +965,6 @@ var ProxyServer = class {
|
|
|
865
965
|
);
|
|
866
966
|
}
|
|
867
967
|
};
|
|
868
|
-
|
|
869
|
-
// src/playwright/index.ts
|
|
870
968
|
function getProxyPort() {
|
|
871
969
|
const envPort = process.env.TEST_PROXY_RECORDER_PORT;
|
|
872
970
|
if (envPort) {
|
|
@@ -896,12 +994,34 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
896
994
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
897
995
|
}
|
|
898
996
|
await response.json();
|
|
899
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
900
997
|
} catch (error) {
|
|
901
998
|
console.error(`Error setting proxy mode:`, error);
|
|
902
999
|
throw error;
|
|
903
1000
|
}
|
|
904
1001
|
}
|
|
1002
|
+
async function cleanupSession(sessionId) {
|
|
1003
|
+
const proxyPort = getProxyPort();
|
|
1004
|
+
try {
|
|
1005
|
+
const body = {
|
|
1006
|
+
cleanup: true,
|
|
1007
|
+
id: sessionId
|
|
1008
|
+
};
|
|
1009
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`, {
|
|
1010
|
+
method: "POST",
|
|
1011
|
+
headers: { "Content-Type": "application/json" },
|
|
1012
|
+
body: JSON.stringify(body)
|
|
1013
|
+
});
|
|
1014
|
+
if (!response.ok) {
|
|
1015
|
+
const text = await response.text();
|
|
1016
|
+
console.error(`Failed to cleanup session ${sessionId}:`, text);
|
|
1017
|
+
throw new Error(`Failed to cleanup session: ${text}`);
|
|
1018
|
+
}
|
|
1019
|
+
await response.json();
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
905
1025
|
function parseSpecFilePath(specPath) {
|
|
906
1026
|
const folderMatch = specPath.match(/^(.+?)\/([^/]+)\.(spec|test)\.ts$/);
|
|
907
1027
|
if (folderMatch) {
|
|
@@ -943,6 +1063,50 @@ async function stopProxy(testInfo) {
|
|
|
943
1063
|
const sessionId = generateSessionId(testInfo);
|
|
944
1064
|
await setProxyMode(Modes.transparent, sessionId);
|
|
945
1065
|
}
|
|
1066
|
+
var cachedRecordingsDir = null;
|
|
1067
|
+
async function getRecordingsDir() {
|
|
1068
|
+
if (cachedRecordingsDir) {
|
|
1069
|
+
return cachedRecordingsDir;
|
|
1070
|
+
}
|
|
1071
|
+
const proxyPort = getProxyPort();
|
|
1072
|
+
try {
|
|
1073
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`);
|
|
1074
|
+
if (response.ok) {
|
|
1075
|
+
const data = await response.json();
|
|
1076
|
+
if (data.recordingsDir) {
|
|
1077
|
+
cachedRecordingsDir = data.recordingsDir;
|
|
1078
|
+
return cachedRecordingsDir;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
console.warn(
|
|
1083
|
+
"Failed to get recordings directory from proxy, using default:",
|
|
1084
|
+
error
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
cachedRecordingsDir = path2__default.default.join(process.cwd(), "e2e", "recordings");
|
|
1088
|
+
return cachedRecordingsDir;
|
|
1089
|
+
}
|
|
1090
|
+
async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
1091
|
+
const harFileName = sessionId.replaceAll("/", "__");
|
|
1092
|
+
const recordingsDir = await getRecordingsDir();
|
|
1093
|
+
const harPath = path2__default.default.join(recordingsDir, `${harFileName}.har`);
|
|
1094
|
+
try {
|
|
1095
|
+
await page.routeFromHAR(harPath, {
|
|
1096
|
+
url,
|
|
1097
|
+
update: mode === Modes.record,
|
|
1098
|
+
updateContent: "embed"
|
|
1099
|
+
});
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (mode === Modes.replay) {
|
|
1102
|
+
console.error(
|
|
1103
|
+
`[Client-Side Replay] Failed to load HAR file. Run tests in record mode first.`,
|
|
1104
|
+
error
|
|
1105
|
+
);
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
946
1110
|
var playwrightProxy = {
|
|
947
1111
|
/**
|
|
948
1112
|
* Setup before test - sets the proxy mode and configures page with custom header
|
|
@@ -950,24 +1114,66 @@ var playwrightProxy = {
|
|
|
950
1114
|
* @param page - Playwright page object
|
|
951
1115
|
* @param testInfo - Playwright test info object
|
|
952
1116
|
* @param mode - The proxy mode to use for this test
|
|
953
|
-
* @param
|
|
1117
|
+
* @param options - Optional configuration including timeout and client-side recording patterns
|
|
954
1118
|
*/
|
|
955
|
-
async before(page, testInfo, mode,
|
|
1119
|
+
async before(page, testInfo, mode, options) {
|
|
1120
|
+
const timeout = typeof options === "number" ? options : options?.timeout;
|
|
1121
|
+
const clientSideOptions = typeof options === "object" && options !== null ? options : void 0;
|
|
956
1122
|
const sessionId = generateSessionId(testInfo);
|
|
957
1123
|
await page.setExtraHTTPHeaders({
|
|
958
1124
|
[RECORDING_ID_HEADER]: sessionId
|
|
959
1125
|
});
|
|
960
1126
|
await setProxyMode(mode, sessionId, timeout);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
1127
|
+
if (clientSideOptions?.url) {
|
|
1128
|
+
await setupClientSideRecording(
|
|
1129
|
+
page,
|
|
1130
|
+
sessionId,
|
|
1131
|
+
mode,
|
|
1132
|
+
clientSideOptions.url
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1136
|
+
const proxyUrl = `localhost:${proxyPort}`;
|
|
1137
|
+
await page.route(
|
|
1138
|
+
(url) => {
|
|
1139
|
+
const urlStr = url.toString();
|
|
1140
|
+
const matches = urlStr.includes(proxyUrl);
|
|
1141
|
+
return matches;
|
|
1142
|
+
},
|
|
1143
|
+
async (route) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const headers = route.request().headers();
|
|
1146
|
+
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1147
|
+
await route.continue({ headers });
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
console.error(
|
|
1150
|
+
`[Route Handler Error] Failed to add ${RECORDING_ID_HEADER} header:`,
|
|
1151
|
+
error
|
|
1152
|
+
);
|
|
1153
|
+
await route.fallback();
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
{ times: Infinity }
|
|
1157
|
+
// Ensure the handler applies to all matching requests
|
|
1158
|
+
);
|
|
1159
|
+
const context = page.context();
|
|
1160
|
+
const contextId = context._guid || "default";
|
|
1161
|
+
const handlerKey = `cleanup_${contextId}`;
|
|
1162
|
+
if (!globalThis[handlerKey]) {
|
|
1163
|
+
globalThis[handlerKey] = true;
|
|
1164
|
+
context.on("close", async () => {
|
|
1165
|
+
try {
|
|
1166
|
+
await cleanupSession(sessionId);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
console.warn(
|
|
1169
|
+
`[Cleanup] Failed to cleanup session ${sessionId}:`,
|
|
1170
|
+
error
|
|
1171
|
+
);
|
|
1172
|
+
} finally {
|
|
1173
|
+
delete globalThis[handlerKey];
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
971
1177
|
},
|
|
972
1178
|
/**
|
|
973
1179
|
* Global teardown - switches proxy to transparent mode
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.cjs';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
-
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-
|
|
3
|
+
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-BlBWqSE4.cjs';
|
|
4
4
|
import '@playwright/test';
|
|
5
5
|
|
|
6
6
|
declare class ProxyServer {
|
|
@@ -17,6 +17,7 @@ declare class ProxyServer {
|
|
|
17
17
|
private sequenceCounterByKey;
|
|
18
18
|
private replaySessions;
|
|
19
19
|
private recordingPromises;
|
|
20
|
+
private flushPromise;
|
|
20
21
|
constructor(targets: string[], recordingsDir: string);
|
|
21
22
|
init(): Promise<void>;
|
|
22
23
|
listen(port: number): http.Server;
|
|
@@ -56,6 +57,11 @@ declare class ProxyServer {
|
|
|
56
57
|
* @returns The replay session state
|
|
57
58
|
*/
|
|
58
59
|
private getOrCreateReplaySession;
|
|
60
|
+
/**
|
|
61
|
+
* Clean up a session - removes it from memory and resets counters
|
|
62
|
+
* @param sessionId The session ID to clean up
|
|
63
|
+
*/
|
|
64
|
+
private cleanupSession;
|
|
59
65
|
private parseGetParams;
|
|
60
66
|
private parseControlRequest;
|
|
61
67
|
private handleControlRequest;
|
|
@@ -68,8 +74,8 @@ declare class ProxyServer {
|
|
|
68
74
|
private flushPendingRecordings;
|
|
69
75
|
private saveCurrentSession;
|
|
70
76
|
private getRecordingIdOrError;
|
|
71
|
-
private ensureSessionLoaded;
|
|
72
77
|
private getServedTracker;
|
|
78
|
+
private getSortedRecordings;
|
|
73
79
|
private selectReplayRecord;
|
|
74
80
|
private handleReplayRequest;
|
|
75
81
|
private handleReplayError;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.js';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
-
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-
|
|
3
|
+
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-BlBWqSE4.js';
|
|
4
4
|
import '@playwright/test';
|
|
5
5
|
|
|
6
6
|
declare class ProxyServer {
|
|
@@ -17,6 +17,7 @@ declare class ProxyServer {
|
|
|
17
17
|
private sequenceCounterByKey;
|
|
18
18
|
private replaySessions;
|
|
19
19
|
private recordingPromises;
|
|
20
|
+
private flushPromise;
|
|
20
21
|
constructor(targets: string[], recordingsDir: string);
|
|
21
22
|
init(): Promise<void>;
|
|
22
23
|
listen(port: number): http.Server;
|
|
@@ -56,6 +57,11 @@ declare class ProxyServer {
|
|
|
56
57
|
* @returns The replay session state
|
|
57
58
|
*/
|
|
58
59
|
private getOrCreateReplaySession;
|
|
60
|
+
/**
|
|
61
|
+
* Clean up a session - removes it from memory and resets counters
|
|
62
|
+
* @param sessionId The session ID to clean up
|
|
63
|
+
*/
|
|
64
|
+
private cleanupSession;
|
|
59
65
|
private parseGetParams;
|
|
60
66
|
private parseControlRequest;
|
|
61
67
|
private handleControlRequest;
|
|
@@ -68,8 +74,8 @@ declare class ProxyServer {
|
|
|
68
74
|
private flushPendingRecordings;
|
|
69
75
|
private saveCurrentSession;
|
|
70
76
|
private getRecordingIdOrError;
|
|
71
|
-
private ensureSessionLoaded;
|
|
72
77
|
private getServedTracker;
|
|
78
|
+
private getSortedRecordings;
|
|
73
79
|
private selectReplayRecord;
|
|
74
80
|
private handleReplayRequest;
|
|
75
81
|
private handleReplayError;
|