test-proxy-recorder 0.1.2 → 0.1.4
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/dist/index-De4mgziH.d.cts +96 -0
- package/dist/index-De4mgziH.d.ts +96 -0
- package/dist/index.cjs +121 -41
- package/dist/index.d.cts +7 -48
- package/dist/index.d.ts +7 -48
- package/dist/index.mjs +119 -41
- package/dist/playwright/index.cjs +11 -4
- package/dist/playwright/index.d.cts +3 -50
- package/dist/playwright/index.d.ts +3 -50
- package/dist/playwright/index.mjs +11 -4
- package/dist/proxy.js +115 -37
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TestInfo } from '@playwright/test';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
|
|
4
|
+
declare const Modes: {
|
|
5
|
+
readonly transparent: "transparent";
|
|
6
|
+
readonly record: "record";
|
|
7
|
+
readonly replay: "replay";
|
|
8
|
+
};
|
|
9
|
+
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
10
|
+
interface ControlRequest {
|
|
11
|
+
mode: Mode;
|
|
12
|
+
id?: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
15
|
+
interface RecordedRequest {
|
|
16
|
+
method: string;
|
|
17
|
+
url: string;
|
|
18
|
+
headers: http.IncomingHttpHeaders;
|
|
19
|
+
body: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface RecordedResponse {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
headers: http.IncomingHttpHeaders;
|
|
24
|
+
body: string | null;
|
|
25
|
+
}
|
|
26
|
+
interface Recording {
|
|
27
|
+
request: RecordedRequest;
|
|
28
|
+
response?: RecordedResponse;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
key: string;
|
|
31
|
+
sequence: number;
|
|
32
|
+
}
|
|
33
|
+
interface WebSocketMessage {
|
|
34
|
+
direction: 'client-to-server' | 'server-to-client';
|
|
35
|
+
data: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
interface WebSocketRecording {
|
|
39
|
+
url: string;
|
|
40
|
+
messages: WebSocketMessage[];
|
|
41
|
+
timestamp: string;
|
|
42
|
+
key: string;
|
|
43
|
+
}
|
|
44
|
+
interface RecordingSession {
|
|
45
|
+
id: string;
|
|
46
|
+
recordings: Recording[];
|
|
47
|
+
websocketRecordings: WebSocketRecording[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
51
|
+
/**
|
|
52
|
+
* Set the proxy mode for a given session
|
|
53
|
+
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
54
|
+
* @param sessionId - Unique identifier for the session
|
|
55
|
+
* @param timeout - Optional timeout in milliseconds
|
|
56
|
+
*/
|
|
57
|
+
declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Generate a session ID from test info
|
|
60
|
+
* @param testInfo - Playwright test info object
|
|
61
|
+
*/
|
|
62
|
+
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
63
|
+
/**
|
|
64
|
+
* Start recording for a test
|
|
65
|
+
* @param testInfo - Playwright test info object
|
|
66
|
+
*/
|
|
67
|
+
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Start replay for a test
|
|
70
|
+
* @param testInfo - Playwright test info object
|
|
71
|
+
*/
|
|
72
|
+
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Stop recording/replay and return to transparent mode
|
|
75
|
+
* @param testInfo - Playwright test info object
|
|
76
|
+
*/
|
|
77
|
+
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Playwright test fixture helper for managing proxy mode
|
|
80
|
+
* Use this in beforeEach/afterEach hooks
|
|
81
|
+
*/
|
|
82
|
+
declare const playwrightProxy: {
|
|
83
|
+
/**
|
|
84
|
+
* Setup before test - sets the proxy mode
|
|
85
|
+
* @param testInfo - Playwright test info object
|
|
86
|
+
* @param mode - The proxy mode to use for this test
|
|
87
|
+
*/
|
|
88
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Cleanup after test - returns to transparent mode
|
|
91
|
+
* @param testInfo - Playwright test info object
|
|
92
|
+
*/
|
|
93
|
+
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
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 };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TestInfo } from '@playwright/test';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
|
|
4
|
+
declare const Modes: {
|
|
5
|
+
readonly transparent: "transparent";
|
|
6
|
+
readonly record: "record";
|
|
7
|
+
readonly replay: "replay";
|
|
8
|
+
};
|
|
9
|
+
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
10
|
+
interface ControlRequest {
|
|
11
|
+
mode: Mode;
|
|
12
|
+
id?: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
15
|
+
interface RecordedRequest {
|
|
16
|
+
method: string;
|
|
17
|
+
url: string;
|
|
18
|
+
headers: http.IncomingHttpHeaders;
|
|
19
|
+
body: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface RecordedResponse {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
headers: http.IncomingHttpHeaders;
|
|
24
|
+
body: string | null;
|
|
25
|
+
}
|
|
26
|
+
interface Recording {
|
|
27
|
+
request: RecordedRequest;
|
|
28
|
+
response?: RecordedResponse;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
key: string;
|
|
31
|
+
sequence: number;
|
|
32
|
+
}
|
|
33
|
+
interface WebSocketMessage {
|
|
34
|
+
direction: 'client-to-server' | 'server-to-client';
|
|
35
|
+
data: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
interface WebSocketRecording {
|
|
39
|
+
url: string;
|
|
40
|
+
messages: WebSocketMessage[];
|
|
41
|
+
timestamp: string;
|
|
42
|
+
key: string;
|
|
43
|
+
}
|
|
44
|
+
interface RecordingSession {
|
|
45
|
+
id: string;
|
|
46
|
+
recordings: Recording[];
|
|
47
|
+
websocketRecordings: WebSocketRecording[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
|
|
51
|
+
/**
|
|
52
|
+
* Set the proxy mode for a given session
|
|
53
|
+
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
54
|
+
* @param sessionId - Unique identifier for the session
|
|
55
|
+
* @param timeout - Optional timeout in milliseconds
|
|
56
|
+
*/
|
|
57
|
+
declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Generate a session ID from test info
|
|
60
|
+
* @param testInfo - Playwright test info object
|
|
61
|
+
*/
|
|
62
|
+
declare function generateSessionId(testInfo: PlaywrightTestInfo): string;
|
|
63
|
+
/**
|
|
64
|
+
* Start recording for a test
|
|
65
|
+
* @param testInfo - Playwright test info object
|
|
66
|
+
*/
|
|
67
|
+
declare function startRecording(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Start replay for a test
|
|
70
|
+
* @param testInfo - Playwright test info object
|
|
71
|
+
*/
|
|
72
|
+
declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Stop recording/replay and return to transparent mode
|
|
75
|
+
* @param testInfo - Playwright test info object
|
|
76
|
+
*/
|
|
77
|
+
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Playwright test fixture helper for managing proxy mode
|
|
80
|
+
* Use this in beforeEach/afterEach hooks
|
|
81
|
+
*/
|
|
82
|
+
declare const playwrightProxy: {
|
|
83
|
+
/**
|
|
84
|
+
* Setup before test - sets the proxy mode
|
|
85
|
+
* @param testInfo - Playwright test info object
|
|
86
|
+
* @param mode - The proxy mode to use for this test
|
|
87
|
+
*/
|
|
88
|
+
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Cleanup after test - returns to transparent mode
|
|
91
|
+
* @param testInfo - Playwright test info object
|
|
92
|
+
*/
|
|
93
|
+
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
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
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
var fs = require('fs/promises');
|
|
4
4
|
var http = require('http');
|
|
5
|
+
var https = require('https');
|
|
5
6
|
var httpProxy = require('http-proxy');
|
|
6
7
|
var ws = require('ws');
|
|
7
8
|
var path = require('path');
|
|
9
|
+
var filenamify = require('filenamify');
|
|
8
10
|
|
|
9
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
12
|
|
|
11
13
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
12
14
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
15
|
+
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
13
16
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
14
17
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
18
|
+
var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
|
|
15
19
|
|
|
16
20
|
// src/ProxyServer.ts
|
|
17
21
|
|
|
@@ -47,6 +51,24 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
47
51
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
48
52
|
);
|
|
49
53
|
}
|
|
54
|
+
var QUERY_HASH_LENGTH = 8;
|
|
55
|
+
function getReqID(req) {
|
|
56
|
+
const urlParts = req.url.split("?");
|
|
57
|
+
const pathname = urlParts[0];
|
|
58
|
+
const query = urlParts[1] || "";
|
|
59
|
+
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
60
|
+
const normalizedPath = filenamify__default.default(pathPart, { replacement: "_" });
|
|
61
|
+
const queryHash = generateQueryHash(query);
|
|
62
|
+
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
63
|
+
return filenamify__default.default(filename, { replacement: "_" });
|
|
64
|
+
}
|
|
65
|
+
function generateQueryHash(query) {
|
|
66
|
+
if (!query) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
70
|
+
return `_${hash}`;
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
// src/utils/httpHelpers.ts
|
|
52
74
|
var CONTENT_TYPE_JSON = "application/json";
|
|
@@ -62,28 +84,6 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
62
84
|
res.end(JSON.stringify(data));
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
// src/utils/requestKeyGenerator.ts
|
|
66
|
-
var QUERY_HASH_LENGTH = 8;
|
|
67
|
-
function generateRequestKey(req) {
|
|
68
|
-
const urlParts = req.url.split("?");
|
|
69
|
-
const pathname = urlParts[0];
|
|
70
|
-
const query = urlParts[1] || "";
|
|
71
|
-
const normalizedPath = normalizePathname(pathname);
|
|
72
|
-
const queryHash = generateQueryHash(query);
|
|
73
|
-
return `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
74
|
-
}
|
|
75
|
-
function normalizePathname(pathname) {
|
|
76
|
-
const normalized = pathname.replaceAll("/", "_").replace(/^_/, "");
|
|
77
|
-
return normalized || "root";
|
|
78
|
-
}
|
|
79
|
-
function generateQueryHash(query) {
|
|
80
|
-
if (!query) {
|
|
81
|
-
return "";
|
|
82
|
-
}
|
|
83
|
-
const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
84
|
-
return `_${hash}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
87
|
// src/ProxyServer.ts
|
|
88
88
|
var ProxyServer = class {
|
|
89
89
|
targets;
|
|
@@ -95,6 +95,10 @@ var ProxyServer = class {
|
|
|
95
95
|
proxy;
|
|
96
96
|
currentSession;
|
|
97
97
|
recordingsDir;
|
|
98
|
+
requestSequenceMap;
|
|
99
|
+
// Track sequence per request key
|
|
100
|
+
replaySequenceMap;
|
|
101
|
+
// Track replay position per request key
|
|
98
102
|
constructor(targets, recordingsDir) {
|
|
99
103
|
this.targets = targets;
|
|
100
104
|
this.currentTargetIndex = 0;
|
|
@@ -104,6 +108,8 @@ var ProxyServer = class {
|
|
|
104
108
|
this.modeTimeout = null;
|
|
105
109
|
this.currentSession = null;
|
|
106
110
|
this.recordingsDir = recordingsDir;
|
|
111
|
+
this.requestSequenceMap = /* @__PURE__ */ new Map();
|
|
112
|
+
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
107
113
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
108
114
|
secure: false,
|
|
109
115
|
changeOrigin: true
|
|
@@ -142,10 +148,19 @@ var ProxyServer = class {
|
|
|
142
148
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
143
149
|
}
|
|
144
150
|
handleProxyResponse(proxyRes, req) {
|
|
151
|
+
this.addCorsHeaders(proxyRes, req);
|
|
145
152
|
if (this.mode === Modes.record && this.recordingId) {
|
|
146
153
|
this.recordResponse(req, proxyRes);
|
|
147
154
|
}
|
|
148
155
|
}
|
|
156
|
+
addCorsHeaders(proxyRes, req) {
|
|
157
|
+
const origin = req.headers.origin;
|
|
158
|
+
proxyRes.headers["access-control-allow-origin"] = origin || "*";
|
|
159
|
+
proxyRes.headers["access-control-allow-credentials"] = "true";
|
|
160
|
+
proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
161
|
+
proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
|
|
162
|
+
proxyRes.headers["access-control-expose-headers"] = "*";
|
|
163
|
+
}
|
|
149
164
|
getTarget() {
|
|
150
165
|
const target = this.targets[this.currentTargetIndex];
|
|
151
166
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
@@ -209,6 +224,7 @@ var ProxyServer = class {
|
|
|
209
224
|
this.recordingId = null;
|
|
210
225
|
this.replayId = null;
|
|
211
226
|
this.currentSession = null;
|
|
227
|
+
clearTimeout(this.modeTimeout || 0);
|
|
212
228
|
console.log("Switched to transparent mode");
|
|
213
229
|
}
|
|
214
230
|
switchToRecordMode(id) {
|
|
@@ -219,6 +235,7 @@ var ProxyServer = class {
|
|
|
219
235
|
this.recordingId = id;
|
|
220
236
|
this.replayId = null;
|
|
221
237
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
238
|
+
this.requestSequenceMap.clear();
|
|
222
239
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
223
240
|
}
|
|
224
241
|
switchToReplayMode(id) {
|
|
@@ -229,6 +246,7 @@ var ProxyServer = class {
|
|
|
229
246
|
this.replayId = id;
|
|
230
247
|
this.recordingId = null;
|
|
231
248
|
this.currentSession = null;
|
|
249
|
+
this.replaySequenceMap.clear();
|
|
232
250
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
233
251
|
}
|
|
234
252
|
setupModeTimeout(timeout) {
|
|
@@ -259,7 +277,9 @@ var ProxyServer = class {
|
|
|
259
277
|
if (!this.currentSession) {
|
|
260
278
|
return;
|
|
261
279
|
}
|
|
262
|
-
const key =
|
|
280
|
+
const key = getReqID(req);
|
|
281
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
282
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
263
283
|
const record = {
|
|
264
284
|
request: {
|
|
265
285
|
method: req.method,
|
|
@@ -268,7 +288,8 @@ var ProxyServer = class {
|
|
|
268
288
|
body: body || null
|
|
269
289
|
},
|
|
270
290
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
271
|
-
key
|
|
291
|
+
key,
|
|
292
|
+
sequence: currentSequence
|
|
272
293
|
};
|
|
273
294
|
this.currentSession.recordings.push(record);
|
|
274
295
|
}
|
|
@@ -276,8 +297,10 @@ var ProxyServer = class {
|
|
|
276
297
|
if (!this.currentSession) {
|
|
277
298
|
return;
|
|
278
299
|
}
|
|
279
|
-
const key =
|
|
280
|
-
const record = this.currentSession.recordings.
|
|
300
|
+
const key = getReqID(req);
|
|
301
|
+
const record = this.currentSession.recordings.findLast(
|
|
302
|
+
(r) => r.key === key && !r.response
|
|
303
|
+
);
|
|
281
304
|
if (!record) {
|
|
282
305
|
console.error("Request record not found for response:", key);
|
|
283
306
|
return;
|
|
@@ -298,21 +321,35 @@ var ProxyServer = class {
|
|
|
298
321
|
});
|
|
299
322
|
}
|
|
300
323
|
async handleReplayRequest(req, res) {
|
|
301
|
-
const key =
|
|
324
|
+
const key = getReqID(req);
|
|
302
325
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
303
326
|
try {
|
|
304
327
|
const session = await loadRecordingSession(filePath);
|
|
305
|
-
const
|
|
328
|
+
const currentSequence = this.replaySequenceMap.get(key) || 0;
|
|
329
|
+
const record = session.recordings.find(
|
|
330
|
+
(r) => r.key === key && r.sequence === currentSequence
|
|
331
|
+
);
|
|
306
332
|
if (!record) {
|
|
307
|
-
throw new Error(
|
|
333
|
+
throw new Error(
|
|
334
|
+
`No recording found for ${key} with sequence ${currentSequence}`
|
|
335
|
+
);
|
|
308
336
|
}
|
|
309
337
|
if (!record.response) {
|
|
310
338
|
throw new Error("No response recorded for this request");
|
|
311
339
|
}
|
|
340
|
+
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
312
341
|
const { statusCode, headers, body } = record.response;
|
|
313
|
-
|
|
342
|
+
const origin = req.headers.origin;
|
|
343
|
+
const responseHeaders = {
|
|
344
|
+
...headers,
|
|
345
|
+
"access-control-allow-origin": origin || "*",
|
|
346
|
+
"access-control-allow-credentials": "true"
|
|
347
|
+
};
|
|
348
|
+
res.writeHead(statusCode, responseHeaders);
|
|
314
349
|
res.end(body);
|
|
315
|
-
console.log(
|
|
350
|
+
console.log(
|
|
351
|
+
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
352
|
+
);
|
|
316
353
|
} catch (error) {
|
|
317
354
|
this.handleReplayError(res, error, key, filePath);
|
|
318
355
|
}
|
|
@@ -328,6 +365,9 @@ var ProxyServer = class {
|
|
|
328
365
|
});
|
|
329
366
|
}
|
|
330
367
|
async handleRequest(req, res) {
|
|
368
|
+
if (req.method === "OPTIONS") {
|
|
369
|
+
return this.handleCorsPreflightRequest(req, res);
|
|
370
|
+
}
|
|
331
371
|
if (req.url === CONTROL_ENDPOINT) {
|
|
332
372
|
return this.handleControlRequest(req, res);
|
|
333
373
|
}
|
|
@@ -336,23 +376,63 @@ var ProxyServer = class {
|
|
|
336
376
|
}
|
|
337
377
|
await this.handleProxyRequest(req, res);
|
|
338
378
|
}
|
|
379
|
+
handleCorsPreflightRequest(req, res) {
|
|
380
|
+
const origin = req.headers.origin;
|
|
381
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
382
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
383
|
+
"Access-Control-Allow-Credentials": "true",
|
|
384
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
385
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
386
|
+
"Access-Control-Max-Age": "86400"
|
|
387
|
+
// 24 hours
|
|
388
|
+
});
|
|
389
|
+
res.end();
|
|
390
|
+
}
|
|
339
391
|
async handleProxyRequest(req, res) {
|
|
340
392
|
const target = this.getTarget();
|
|
341
393
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
342
394
|
if (this.mode === Modes.record) {
|
|
343
|
-
await this.
|
|
395
|
+
await this.bufferAndProxyRequest(req, res, target);
|
|
396
|
+
} else {
|
|
397
|
+
this.proxy.web(req, res, { target });
|
|
344
398
|
}
|
|
345
|
-
this.proxy.web(req, res, { target });
|
|
346
399
|
}
|
|
347
|
-
async
|
|
400
|
+
async bufferAndProxyRequest(req, res, target) {
|
|
348
401
|
const chunks = [];
|
|
349
402
|
req.on("data", (chunk) => {
|
|
350
403
|
chunks.push(chunk);
|
|
351
404
|
});
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await this.saveRequestRecord(req, body);
|
|
405
|
+
await new Promise((resolve) => {
|
|
406
|
+
req.on("end", () => resolve());
|
|
355
407
|
});
|
|
408
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
409
|
+
await this.saveRequestRecord(req, body);
|
|
410
|
+
const targetUrl = new URL(target);
|
|
411
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
412
|
+
const requestModule = isHttps ? https__default.default : http__default.default;
|
|
413
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
414
|
+
const proxyReq = requestModule.request(
|
|
415
|
+
{
|
|
416
|
+
hostname: targetUrl.hostname,
|
|
417
|
+
port: targetUrl.port || defaultPort,
|
|
418
|
+
path: req.url,
|
|
419
|
+
method: req.method,
|
|
420
|
+
headers: req.headers
|
|
421
|
+
},
|
|
422
|
+
(proxyRes) => {
|
|
423
|
+
this.addCorsHeaders(proxyRes, req);
|
|
424
|
+
this.recordResponse(req, proxyRes);
|
|
425
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
426
|
+
proxyRes.pipe(res);
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
proxyReq.on("error", (err) => {
|
|
430
|
+
this.handleProxyError(err, req, res);
|
|
431
|
+
});
|
|
432
|
+
if (chunks.length > 0) {
|
|
433
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
434
|
+
}
|
|
435
|
+
proxyReq.end();
|
|
356
436
|
}
|
|
357
437
|
handleUpgrade(req, socket, head) {
|
|
358
438
|
if (this.mode === Modes.replay) {
|
|
@@ -555,15 +635,15 @@ function generateSessionId(testInfo) {
|
|
|
555
635
|
}
|
|
556
636
|
async function startRecording(testInfo) {
|
|
557
637
|
const sessionId = generateSessionId(testInfo);
|
|
558
|
-
await setProxyMode(
|
|
638
|
+
await setProxyMode(Modes.record, sessionId);
|
|
559
639
|
}
|
|
560
640
|
async function startReplay(testInfo) {
|
|
561
641
|
const sessionId = generateSessionId(testInfo);
|
|
562
|
-
await setProxyMode(
|
|
642
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
563
643
|
}
|
|
564
644
|
async function stopProxy(testInfo) {
|
|
565
645
|
const sessionId = generateSessionId(testInfo);
|
|
566
|
-
await setProxyMode(
|
|
646
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
567
647
|
}
|
|
568
648
|
var playwrightProxy = {
|
|
569
649
|
/**
|
|
@@ -582,7 +662,7 @@ var playwrightProxy = {
|
|
|
582
662
|
*/
|
|
583
663
|
async after(testInfo) {
|
|
584
664
|
const sessionId = generateSessionId(testInfo);
|
|
585
|
-
await setProxyMode(
|
|
665
|
+
await setProxyMode(Modes.transparent, sessionId);
|
|
586
666
|
}
|
|
587
667
|
};
|
|
588
668
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { PlaywrightTestInfo,
|
|
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-De4mgziH.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,12 +12,15 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
+
private requestSequenceMap;
|
|
16
|
+
private replaySequenceMap;
|
|
15
17
|
constructor(targets: string[], recordingsDir: string);
|
|
16
18
|
init(): Promise<void>;
|
|
17
19
|
listen(port: number): http.Server;
|
|
18
20
|
private setupProxyEventHandlers;
|
|
19
21
|
private handleProxyError;
|
|
20
22
|
private handleProxyResponse;
|
|
23
|
+
private addCorsHeaders;
|
|
21
24
|
private getTarget;
|
|
22
25
|
private handleControlRequest;
|
|
23
26
|
private clearModeTimeout;
|
|
@@ -32,57 +35,13 @@ declare class ProxyServer {
|
|
|
32
35
|
private handleReplayRequest;
|
|
33
36
|
private handleReplayError;
|
|
34
37
|
private handleRequest;
|
|
38
|
+
private handleCorsPreflightRequest;
|
|
35
39
|
private handleProxyRequest;
|
|
36
|
-
private
|
|
40
|
+
private bufferAndProxyRequest;
|
|
37
41
|
private handleUpgrade;
|
|
38
42
|
private handleRecordWebSocket;
|
|
39
43
|
private handleReplayWebSocket;
|
|
40
44
|
private logServerStartup;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
readonly transparent: "transparent";
|
|
45
|
-
readonly record: "record";
|
|
46
|
-
readonly replay: "replay";
|
|
47
|
-
};
|
|
48
|
-
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
49
|
-
interface ControlRequest {
|
|
50
|
-
mode: Mode;
|
|
51
|
-
id?: string;
|
|
52
|
-
timeout?: number;
|
|
53
|
-
}
|
|
54
|
-
interface RecordedRequest {
|
|
55
|
-
method: string;
|
|
56
|
-
url: string;
|
|
57
|
-
headers: http.IncomingHttpHeaders;
|
|
58
|
-
body: string | null;
|
|
59
|
-
}
|
|
60
|
-
interface RecordedResponse {
|
|
61
|
-
statusCode: number;
|
|
62
|
-
headers: http.IncomingHttpHeaders;
|
|
63
|
-
body: string | null;
|
|
64
|
-
}
|
|
65
|
-
interface Recording {
|
|
66
|
-
request: RecordedRequest;
|
|
67
|
-
response?: RecordedResponse;
|
|
68
|
-
timestamp: string;
|
|
69
|
-
key: string;
|
|
70
|
-
}
|
|
71
|
-
interface WebSocketMessage {
|
|
72
|
-
direction: 'client-to-server' | 'server-to-client';
|
|
73
|
-
data: string;
|
|
74
|
-
timestamp: string;
|
|
75
|
-
}
|
|
76
|
-
interface WebSocketRecording {
|
|
77
|
-
url: string;
|
|
78
|
-
messages: WebSocketMessage[];
|
|
79
|
-
timestamp: string;
|
|
80
|
-
key: string;
|
|
81
|
-
}
|
|
82
|
-
interface RecordingSession {
|
|
83
|
-
id: string;
|
|
84
|
-
recordings: Recording[];
|
|
85
|
-
websocketRecordings: WebSocketRecording[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { type ControlRequest, type Mode, ProxyServer, type Recording, type RecordingSession, type WebSocketRecording };
|
|
47
|
+
export { ProxyServer };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { PlaywrightTestInfo,
|
|
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-De4mgziH.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,12 +12,15 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
+
private requestSequenceMap;
|
|
16
|
+
private replaySequenceMap;
|
|
15
17
|
constructor(targets: string[], recordingsDir: string);
|
|
16
18
|
init(): Promise<void>;
|
|
17
19
|
listen(port: number): http.Server;
|
|
18
20
|
private setupProxyEventHandlers;
|
|
19
21
|
private handleProxyError;
|
|
20
22
|
private handleProxyResponse;
|
|
23
|
+
private addCorsHeaders;
|
|
21
24
|
private getTarget;
|
|
22
25
|
private handleControlRequest;
|
|
23
26
|
private clearModeTimeout;
|
|
@@ -32,57 +35,13 @@ declare class ProxyServer {
|
|
|
32
35
|
private handleReplayRequest;
|
|
33
36
|
private handleReplayError;
|
|
34
37
|
private handleRequest;
|
|
38
|
+
private handleCorsPreflightRequest;
|
|
35
39
|
private handleProxyRequest;
|
|
36
|
-
private
|
|
40
|
+
private bufferAndProxyRequest;
|
|
37
41
|
private handleUpgrade;
|
|
38
42
|
private handleRecordWebSocket;
|
|
39
43
|
private handleReplayWebSocket;
|
|
40
44
|
private logServerStartup;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
readonly transparent: "transparent";
|
|
45
|
-
readonly record: "record";
|
|
46
|
-
readonly replay: "replay";
|
|
47
|
-
};
|
|
48
|
-
type Mode = (typeof Modes)[keyof typeof Modes];
|
|
49
|
-
interface ControlRequest {
|
|
50
|
-
mode: Mode;
|
|
51
|
-
id?: string;
|
|
52
|
-
timeout?: number;
|
|
53
|
-
}
|
|
54
|
-
interface RecordedRequest {
|
|
55
|
-
method: string;
|
|
56
|
-
url: string;
|
|
57
|
-
headers: http.IncomingHttpHeaders;
|
|
58
|
-
body: string | null;
|
|
59
|
-
}
|
|
60
|
-
interface RecordedResponse {
|
|
61
|
-
statusCode: number;
|
|
62
|
-
headers: http.IncomingHttpHeaders;
|
|
63
|
-
body: string | null;
|
|
64
|
-
}
|
|
65
|
-
interface Recording {
|
|
66
|
-
request: RecordedRequest;
|
|
67
|
-
response?: RecordedResponse;
|
|
68
|
-
timestamp: string;
|
|
69
|
-
key: string;
|
|
70
|
-
}
|
|
71
|
-
interface WebSocketMessage {
|
|
72
|
-
direction: 'client-to-server' | 'server-to-client';
|
|
73
|
-
data: string;
|
|
74
|
-
timestamp: string;
|
|
75
|
-
}
|
|
76
|
-
interface WebSocketRecording {
|
|
77
|
-
url: string;
|
|
78
|
-
messages: WebSocketMessage[];
|
|
79
|
-
timestamp: string;
|
|
80
|
-
key: string;
|
|
81
|
-
}
|
|
82
|
-
interface RecordingSession {
|
|
83
|
-
id: string;
|
|
84
|
-
recordings: Recording[];
|
|
85
|
-
websocketRecordings: WebSocketRecording[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export { type ControlRequest, type Mode, ProxyServer, type Recording, type RecordingSession, type WebSocketRecording };
|
|
47
|
+
export { ProxyServer };
|