test-proxy-recorder 0.3.1 → 0.3.2
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 +234 -26
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.mjs +233 -25
- package/dist/playwright/index.cjs +151 -13
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.mjs +147 -14
- package/dist/proxy.js +88 -9
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import https from 'https';
|
|
|
4
4
|
import httpProxy from 'http-proxy';
|
|
5
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
-
import
|
|
7
|
+
import path2 from 'path';
|
|
8
8
|
import filenamify2 from 'filenamify';
|
|
9
9
|
|
|
10
10
|
// src/constants.ts
|
|
@@ -41,7 +41,7 @@ function getRecordingPath(recordingsDir, id) {
|
|
|
41
41
|
maxLength: 255
|
|
42
42
|
// Set explicit max to prevent filenamify's default truncation
|
|
43
43
|
});
|
|
44
|
-
return
|
|
44
|
+
return path2.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
45
45
|
}
|
|
46
46
|
async function loadRecordingSession(filePath) {
|
|
47
47
|
const fileContent = await fs.readFile(filePath, "utf8");
|
|
@@ -82,16 +82,19 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
82
82
|
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
|
-
function
|
|
86
|
-
const urlParts = req.url.split("?");
|
|
87
|
-
const pathname = urlParts[0];
|
|
88
|
-
const query = urlParts[1] || "";
|
|
85
|
+
function generateRecordingKey(pathname, query, method) {
|
|
89
86
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
90
87
|
const normalizedPath = filenamify2(pathPart, { replacement: "_" });
|
|
91
88
|
const queryHash = generateQueryHash(query);
|
|
92
|
-
const filename = `${
|
|
89
|
+
const filename = `${method}_${normalizedPath}${queryHash}.json`;
|
|
93
90
|
return filenamify2(filename, { replacement: "_" });
|
|
94
91
|
}
|
|
92
|
+
function getReqID(req) {
|
|
93
|
+
const urlParts = req.url.split("?");
|
|
94
|
+
const pathname = urlParts[0];
|
|
95
|
+
const query = urlParts[1] || "";
|
|
96
|
+
return generateRecordingKey(pathname, query, req.method);
|
|
97
|
+
}
|
|
95
98
|
function generateQueryHash(query) {
|
|
96
99
|
if (!query) {
|
|
97
100
|
return "";
|
|
@@ -244,7 +247,15 @@ var ProxyServer = class {
|
|
|
244
247
|
* @returns The recording ID, or null if not found
|
|
245
248
|
*/
|
|
246
249
|
getRecordingIdFromRequest(req) {
|
|
247
|
-
|
|
250
|
+
const fromHeader = this.getRecordingIdFromHeader(req);
|
|
251
|
+
const fromCookie = this.getRecordingIdFromCookie(req);
|
|
252
|
+
if (fromHeader) {
|
|
253
|
+
return fromHeader;
|
|
254
|
+
}
|
|
255
|
+
if (fromCookie) {
|
|
256
|
+
return fromCookie;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
248
259
|
}
|
|
249
260
|
/**
|
|
250
261
|
* Get or create a replay session state for a given recording ID
|
|
@@ -269,6 +280,27 @@ var ProxyServer = class {
|
|
|
269
280
|
}
|
|
270
281
|
return session;
|
|
271
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Clean up a session - removes it from memory and resets counters
|
|
285
|
+
* @param sessionId The session ID to clean up
|
|
286
|
+
*/
|
|
287
|
+
async cleanupSession(sessionId) {
|
|
288
|
+
if (this.replaySessions.has(sessionId)) {
|
|
289
|
+
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
290
|
+
this.replaySessions.delete(sessionId);
|
|
291
|
+
}
|
|
292
|
+
if (this.recordingId === sessionId) {
|
|
293
|
+
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
294
|
+
await this.saveCurrentSession();
|
|
295
|
+
this.currentSession = null;
|
|
296
|
+
this.recordingId = null;
|
|
297
|
+
}
|
|
298
|
+
if (this.replayId === sessionId) {
|
|
299
|
+
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
300
|
+
this.replayId = null;
|
|
301
|
+
}
|
|
302
|
+
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
303
|
+
}
|
|
272
304
|
parseGetParams(req) {
|
|
273
305
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
274
306
|
const mode = url.searchParams.get("mode");
|
|
@@ -292,9 +324,29 @@ var ProxyServer = class {
|
|
|
292
324
|
throw new Error("Unsupported control method");
|
|
293
325
|
}
|
|
294
326
|
async handleControlRequest(req, res) {
|
|
327
|
+
if (req.method === "GET") {
|
|
328
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
329
|
+
recordingsDir: this.recordingsDir,
|
|
330
|
+
mode: this.mode,
|
|
331
|
+
id: this.recordingId || this.replayId
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
295
335
|
try {
|
|
296
336
|
const data = await this.parseControlRequest(req);
|
|
297
|
-
const { mode, id, timeout: requestTimeout } = data;
|
|
337
|
+
const { mode, id, timeout: requestTimeout, cleanup } = data;
|
|
338
|
+
if (cleanup && id) {
|
|
339
|
+
await this.cleanupSession(id);
|
|
340
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
341
|
+
success: true,
|
|
342
|
+
message: `Session ${id} cleaned up`,
|
|
343
|
+
mode: this.mode
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!mode) {
|
|
348
|
+
throw new Error("Mode parameter is required when cleanup is not specified");
|
|
349
|
+
}
|
|
298
350
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
299
351
|
this.clearModeTimeout();
|
|
300
352
|
await this.switchMode(mode, id);
|
|
@@ -310,7 +362,8 @@ var ProxyServer = class {
|
|
|
310
362
|
success: true,
|
|
311
363
|
mode: this.mode,
|
|
312
364
|
id: this.recordingId || this.replayId,
|
|
313
|
-
timeout
|
|
365
|
+
timeout,
|
|
366
|
+
recordingsDir: this.recordingsDir
|
|
314
367
|
});
|
|
315
368
|
} catch (error) {
|
|
316
369
|
console.error("Control request error:", error);
|
|
@@ -385,6 +438,7 @@ var ProxyServer = class {
|
|
|
385
438
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
386
439
|
}
|
|
387
440
|
setupModeTimeout(timeout) {
|
|
441
|
+
clearTimeout(this.modeTimeout || 0);
|
|
388
442
|
this.modeTimeout = setTimeout(async () => {
|
|
389
443
|
console.log("Timeout reached, switching back to transparent mode");
|
|
390
444
|
await this.saveCurrentSession();
|
|
@@ -420,7 +474,29 @@ var ProxyServer = class {
|
|
|
420
474
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
421
475
|
}
|
|
422
476
|
getRecordingIdOrError(req, res) {
|
|
423
|
-
const
|
|
477
|
+
const recordingIdFromRequest = this.getRecordingIdFromRequest(req);
|
|
478
|
+
if (recordingIdFromRequest) {
|
|
479
|
+
return recordingIdFromRequest;
|
|
480
|
+
}
|
|
481
|
+
if (this.replaySessions.size > 1) {
|
|
482
|
+
console.warn(
|
|
483
|
+
`[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)`
|
|
484
|
+
);
|
|
485
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
486
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
487
|
+
"Content-Type": "application/json",
|
|
488
|
+
...corsHeaders
|
|
489
|
+
});
|
|
490
|
+
res.end(
|
|
491
|
+
JSON.stringify({
|
|
492
|
+
error: "Missing recording ID in concurrent replay mode. Ensure x-test-rcrd-id header is set.",
|
|
493
|
+
activeSessions: [...this.replaySessions.keys()],
|
|
494
|
+
hint: "This usually means page.setExtraHTTPHeaders() did not apply to this request type"
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const recordingId = this.replayId;
|
|
424
500
|
if (!recordingId) {
|
|
425
501
|
const corsHeaders = this.getCorsHeaders(req);
|
|
426
502
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -430,6 +506,9 @@ var ProxyServer = class {
|
|
|
430
506
|
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
431
507
|
return null;
|
|
432
508
|
}
|
|
509
|
+
console.log(
|
|
510
|
+
`[FALLBACK] Using replayId fallback for ${req.method} ${req.url} -> session: ${recordingId} (single session mode)`
|
|
511
|
+
);
|
|
433
512
|
return recordingId;
|
|
434
513
|
}
|
|
435
514
|
async ensureSessionLoaded(recordingId, filePath) {
|
|
@@ -853,8 +932,6 @@ var ProxyServer = class {
|
|
|
853
932
|
);
|
|
854
933
|
}
|
|
855
934
|
};
|
|
856
|
-
|
|
857
|
-
// src/playwright/index.ts
|
|
858
935
|
function getProxyPort() {
|
|
859
936
|
const envPort = process.env.TEST_PROXY_RECORDER_PORT;
|
|
860
937
|
if (envPort) {
|
|
@@ -890,6 +967,30 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
890
967
|
throw error;
|
|
891
968
|
}
|
|
892
969
|
}
|
|
970
|
+
async function cleanupSession(sessionId) {
|
|
971
|
+
const proxyPort = getProxyPort();
|
|
972
|
+
try {
|
|
973
|
+
const body = {
|
|
974
|
+
cleanup: true,
|
|
975
|
+
id: sessionId
|
|
976
|
+
};
|
|
977
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`, {
|
|
978
|
+
method: "POST",
|
|
979
|
+
headers: { "Content-Type": "application/json" },
|
|
980
|
+
body: JSON.stringify(body)
|
|
981
|
+
});
|
|
982
|
+
if (!response.ok) {
|
|
983
|
+
const text = await response.text();
|
|
984
|
+
console.error(`Failed to cleanup session ${sessionId}:`, text);
|
|
985
|
+
throw new Error(`Failed to cleanup session: ${text}`);
|
|
986
|
+
}
|
|
987
|
+
await response.json();
|
|
988
|
+
console.log(`Session cleaned up: ${sessionId}`);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
console.error(`Error cleaning up session:`, error);
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
893
994
|
function parseSpecFilePath(specPath) {
|
|
894
995
|
const folderMatch = specPath.match(/^(.+?)\/([^/]+)\.(spec|test)\.ts$/);
|
|
895
996
|
if (folderMatch) {
|
|
@@ -931,6 +1032,53 @@ async function stopProxy(testInfo) {
|
|
|
931
1032
|
const sessionId = generateSessionId(testInfo);
|
|
932
1033
|
await setProxyMode(Modes.transparent, sessionId);
|
|
933
1034
|
}
|
|
1035
|
+
var cachedRecordingsDir = null;
|
|
1036
|
+
async function getRecordingsDir() {
|
|
1037
|
+
if (cachedRecordingsDir) {
|
|
1038
|
+
return cachedRecordingsDir;
|
|
1039
|
+
}
|
|
1040
|
+
const proxyPort = getProxyPort();
|
|
1041
|
+
try {
|
|
1042
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`);
|
|
1043
|
+
if (response.ok) {
|
|
1044
|
+
const data = await response.json();
|
|
1045
|
+
if (data.recordingsDir) {
|
|
1046
|
+
cachedRecordingsDir = data.recordingsDir;
|
|
1047
|
+
return cachedRecordingsDir;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.warn(
|
|
1052
|
+
"Failed to get recordings directory from proxy, using default:",
|
|
1053
|
+
error
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
cachedRecordingsDir = path2.join(process.cwd(), "e2e", "recordings");
|
|
1057
|
+
return cachedRecordingsDir;
|
|
1058
|
+
}
|
|
1059
|
+
async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
1060
|
+
const harFileName = sessionId.replaceAll("/", "__");
|
|
1061
|
+
const recordingsDir = await getRecordingsDir();
|
|
1062
|
+
const harPath = path2.join(recordingsDir, `${harFileName}.har`);
|
|
1063
|
+
console.log(
|
|
1064
|
+
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
1065
|
+
);
|
|
1066
|
+
try {
|
|
1067
|
+
await page.routeFromHAR(harPath, {
|
|
1068
|
+
url,
|
|
1069
|
+
update: mode === Modes.record,
|
|
1070
|
+
updateContent: "embed"
|
|
1071
|
+
});
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
if (mode === Modes.replay) {
|
|
1074
|
+
console.error(
|
|
1075
|
+
`[Client-Side Replay] Failed to load HAR file. Run tests in record mode first.`,
|
|
1076
|
+
error
|
|
1077
|
+
);
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
934
1082
|
var playwrightProxy = {
|
|
935
1083
|
/**
|
|
936
1084
|
* Setup before test - sets the proxy mode and configures page with custom header
|
|
@@ -938,24 +1086,84 @@ var playwrightProxy = {
|
|
|
938
1086
|
* @param page - Playwright page object
|
|
939
1087
|
* @param testInfo - Playwright test info object
|
|
940
1088
|
* @param mode - The proxy mode to use for this test
|
|
941
|
-
* @param
|
|
1089
|
+
* @param options - Optional configuration including timeout and client-side recording patterns
|
|
942
1090
|
*/
|
|
943
|
-
async before(page, testInfo, mode,
|
|
1091
|
+
async before(page, testInfo, mode, options) {
|
|
1092
|
+
const timeout = typeof options === "number" ? options : options?.timeout;
|
|
1093
|
+
const clientSideOptions = typeof options === "object" && options !== null ? options : void 0;
|
|
944
1094
|
const sessionId = generateSessionId(testInfo);
|
|
945
1095
|
await page.setExtraHTTPHeaders({
|
|
946
1096
|
[RECORDING_ID_HEADER]: sessionId
|
|
947
1097
|
});
|
|
1098
|
+
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
948
1099
|
await setProxyMode(mode, sessionId, timeout);
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1100
|
+
console.log(`[Setup] Proxy mode set successfully`);
|
|
1101
|
+
if (clientSideOptions?.url) {
|
|
1102
|
+
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
1103
|
+
await setupClientSideRecording(
|
|
1104
|
+
page,
|
|
1105
|
+
sessionId,
|
|
1106
|
+
mode,
|
|
1107
|
+
clientSideOptions.url
|
|
1108
|
+
);
|
|
1109
|
+
console.log(`[Setup] Client-side recording setup complete`);
|
|
1110
|
+
}
|
|
1111
|
+
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1112
|
+
const proxyUrl = `localhost:${proxyPort}`;
|
|
1113
|
+
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
1114
|
+
await page.route(
|
|
1115
|
+
(url) => {
|
|
1116
|
+
const urlStr = url.toString();
|
|
1117
|
+
const matches = urlStr.includes(proxyUrl);
|
|
1118
|
+
if (matches) {
|
|
1119
|
+
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
1120
|
+
}
|
|
1121
|
+
return matches;
|
|
1122
|
+
},
|
|
1123
|
+
async (route) => {
|
|
1124
|
+
try {
|
|
1125
|
+
const url = route.request().url();
|
|
1126
|
+
const method = route.request().method();
|
|
1127
|
+
const headers = route.request().headers();
|
|
1128
|
+
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
1129
|
+
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1130
|
+
console.log(
|
|
1131
|
+
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
1132
|
+
);
|
|
1133
|
+
await route.continue({ headers });
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
console.error(
|
|
1136
|
+
`[Route Handler Error] Failed to add ${RECORDING_ID_HEADER} header:`,
|
|
1137
|
+
error
|
|
1138
|
+
);
|
|
1139
|
+
await route.fallback();
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
{ times: Infinity }
|
|
1143
|
+
// Ensure the handler applies to all matching requests
|
|
1144
|
+
);
|
|
1145
|
+
console.log(`[Setup] Proxy route handler registered`);
|
|
1146
|
+
const context = page.context();
|
|
1147
|
+
const contextId = context._guid || "default";
|
|
1148
|
+
const handlerKey = `cleanup_${contextId}`;
|
|
1149
|
+
if (!globalThis[handlerKey]) {
|
|
1150
|
+
globalThis[handlerKey] = true;
|
|
1151
|
+
context.on("close", async () => {
|
|
1152
|
+
try {
|
|
1153
|
+
console.log(
|
|
1154
|
+
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
1155
|
+
);
|
|
1156
|
+
await cleanupSession(sessionId);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.warn(
|
|
1159
|
+
`[Cleanup] Failed to cleanup session ${sessionId}:`,
|
|
1160
|
+
error
|
|
1161
|
+
);
|
|
1162
|
+
} finally {
|
|
1163
|
+
delete globalThis[handlerKey];
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
959
1167
|
},
|
|
960
1168
|
/**
|
|
961
1169
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var path = require('path');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
8
|
+
|
|
9
|
+
// src/playwright/index.ts
|
|
4
10
|
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
5
11
|
|
|
6
12
|
// src/types.ts
|
|
@@ -46,6 +52,30 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
46
52
|
throw error;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
55
|
+
async function cleanupSession(sessionId) {
|
|
56
|
+
const proxyPort = getProxyPort();
|
|
57
|
+
try {
|
|
58
|
+
const body = {
|
|
59
|
+
cleanup: true,
|
|
60
|
+
id: sessionId
|
|
61
|
+
};
|
|
62
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify(body)
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
console.error(`Failed to cleanup session ${sessionId}:`, text);
|
|
70
|
+
throw new Error(`Failed to cleanup session: ${text}`);
|
|
71
|
+
}
|
|
72
|
+
await response.json();
|
|
73
|
+
console.log(`Session cleaned up: ${sessionId}`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`Error cleaning up session:`, error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
49
79
|
function parseSpecFilePath(specPath) {
|
|
50
80
|
const folderMatch = specPath.match(/^(.+?)\/([^/]+)\.(spec|test)\.ts$/);
|
|
51
81
|
if (folderMatch) {
|
|
@@ -87,6 +117,53 @@ async function stopProxy(testInfo) {
|
|
|
87
117
|
const sessionId = generateSessionId(testInfo);
|
|
88
118
|
await setProxyMode(Modes.transparent, sessionId);
|
|
89
119
|
}
|
|
120
|
+
var cachedRecordingsDir = null;
|
|
121
|
+
async function getRecordingsDir() {
|
|
122
|
+
if (cachedRecordingsDir) {
|
|
123
|
+
return cachedRecordingsDir;
|
|
124
|
+
}
|
|
125
|
+
const proxyPort = getProxyPort();
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`);
|
|
128
|
+
if (response.ok) {
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
if (data.recordingsDir) {
|
|
131
|
+
cachedRecordingsDir = data.recordingsDir;
|
|
132
|
+
return cachedRecordingsDir;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(
|
|
137
|
+
"Failed to get recordings directory from proxy, using default:",
|
|
138
|
+
error
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
cachedRecordingsDir = path__default.default.join(process.cwd(), "e2e", "recordings");
|
|
142
|
+
return cachedRecordingsDir;
|
|
143
|
+
}
|
|
144
|
+
async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
145
|
+
const harFileName = sessionId.replaceAll("/", "__");
|
|
146
|
+
const recordingsDir = await getRecordingsDir();
|
|
147
|
+
const harPath = path__default.default.join(recordingsDir, `${harFileName}.har`);
|
|
148
|
+
console.log(
|
|
149
|
+
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
150
|
+
);
|
|
151
|
+
try {
|
|
152
|
+
await page.routeFromHAR(harPath, {
|
|
153
|
+
url,
|
|
154
|
+
update: mode === Modes.record,
|
|
155
|
+
updateContent: "embed"
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (mode === Modes.replay) {
|
|
159
|
+
console.error(
|
|
160
|
+
`[Client-Side Replay] Failed to load HAR file. Run tests in record mode first.`,
|
|
161
|
+
error
|
|
162
|
+
);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
90
167
|
var playwrightProxy = {
|
|
91
168
|
/**
|
|
92
169
|
* Setup before test - sets the proxy mode and configures page with custom header
|
|
@@ -94,24 +171,84 @@ var playwrightProxy = {
|
|
|
94
171
|
* @param page - Playwright page object
|
|
95
172
|
* @param testInfo - Playwright test info object
|
|
96
173
|
* @param mode - The proxy mode to use for this test
|
|
97
|
-
* @param
|
|
174
|
+
* @param options - Optional configuration including timeout and client-side recording patterns
|
|
98
175
|
*/
|
|
99
|
-
async before(page, testInfo, mode,
|
|
176
|
+
async before(page, testInfo, mode, options) {
|
|
177
|
+
const timeout = typeof options === "number" ? options : options?.timeout;
|
|
178
|
+
const clientSideOptions = typeof options === "object" && options !== null ? options : void 0;
|
|
100
179
|
const sessionId = generateSessionId(testInfo);
|
|
101
180
|
await page.setExtraHTTPHeaders({
|
|
102
181
|
[RECORDING_ID_HEADER]: sessionId
|
|
103
182
|
});
|
|
183
|
+
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
104
184
|
await setProxyMode(mode, sessionId, timeout);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
185
|
+
console.log(`[Setup] Proxy mode set successfully`);
|
|
186
|
+
if (clientSideOptions?.url) {
|
|
187
|
+
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
188
|
+
await setupClientSideRecording(
|
|
189
|
+
page,
|
|
190
|
+
sessionId,
|
|
191
|
+
mode,
|
|
192
|
+
clientSideOptions.url
|
|
193
|
+
);
|
|
194
|
+
console.log(`[Setup] Client-side recording setup complete`);
|
|
195
|
+
}
|
|
196
|
+
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
197
|
+
const proxyUrl = `localhost:${proxyPort}`;
|
|
198
|
+
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
199
|
+
await page.route(
|
|
200
|
+
(url) => {
|
|
201
|
+
const urlStr = url.toString();
|
|
202
|
+
const matches = urlStr.includes(proxyUrl);
|
|
203
|
+
if (matches) {
|
|
204
|
+
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
205
|
+
}
|
|
206
|
+
return matches;
|
|
207
|
+
},
|
|
208
|
+
async (route) => {
|
|
209
|
+
try {
|
|
210
|
+
const url = route.request().url();
|
|
211
|
+
const method = route.request().method();
|
|
212
|
+
const headers = route.request().headers();
|
|
213
|
+
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
214
|
+
headers[RECORDING_ID_HEADER] = sessionId;
|
|
215
|
+
console.log(
|
|
216
|
+
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
217
|
+
);
|
|
218
|
+
await route.continue({ headers });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(
|
|
221
|
+
`[Route Handler Error] Failed to add ${RECORDING_ID_HEADER} header:`,
|
|
222
|
+
error
|
|
223
|
+
);
|
|
224
|
+
await route.fallback();
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{ times: Infinity }
|
|
228
|
+
// Ensure the handler applies to all matching requests
|
|
229
|
+
);
|
|
230
|
+
console.log(`[Setup] Proxy route handler registered`);
|
|
231
|
+
const context = page.context();
|
|
232
|
+
const contextId = context._guid || "default";
|
|
233
|
+
const handlerKey = `cleanup_${contextId}`;
|
|
234
|
+
if (!globalThis[handlerKey]) {
|
|
235
|
+
globalThis[handlerKey] = true;
|
|
236
|
+
context.on("close", async () => {
|
|
237
|
+
try {
|
|
238
|
+
console.log(
|
|
239
|
+
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
240
|
+
);
|
|
241
|
+
await cleanupSession(sessionId);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`[Cleanup] Failed to cleanup session ${sessionId}:`,
|
|
245
|
+
error
|
|
246
|
+
);
|
|
247
|
+
} finally {
|
|
248
|
+
delete globalThis[handlerKey];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
115
252
|
},
|
|
116
253
|
/**
|
|
117
254
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -122,6 +259,7 @@ var playwrightProxy = {
|
|
|
122
259
|
}
|
|
123
260
|
};
|
|
124
261
|
|
|
262
|
+
exports.cleanupSession = cleanupSession;
|
|
125
263
|
exports.generateSessionId = generateSessionId;
|
|
126
264
|
exports.playwrightProxy = playwrightProxy;
|
|
127
265
|
exports.setProxyMode = setProxyMode;
|
|
@@ -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.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 { 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';
|