test-proxy-recorder 0.1.4 → 0.1.6

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.cjs CHANGED
@@ -35,7 +35,7 @@ var Modes = {
35
35
  };
36
36
  var JSON_INDENT_SPACES = 2;
37
37
  function getRecordingPath(recordingsDir, id) {
38
- return path__default.default.join(recordingsDir, `${id}.json`);
38
+ return path__default.default.join(recordingsDir, `${id}.mock.json`);
39
39
  }
40
40
  async function loadRecordingSession(filePath) {
41
41
  const fileContent = await fs__default.default.readFile(filePath, "utf8");
@@ -43,6 +43,8 @@ async function loadRecordingSession(filePath) {
43
43
  }
44
44
  async function saveRecordingSession(recordingsDir, session) {
45
45
  const filePath = getRecordingPath(recordingsDir, session.id);
46
+ const dirPath = path__default.default.dirname(filePath);
47
+ await fs__default.default.mkdir(dirPath, { recursive: true });
46
48
  await fs__default.default.writeFile(
47
49
  filePath,
48
50
  JSON.stringify(session, null, JSON_INDENT_SPACES)
@@ -127,6 +129,7 @@ var ProxyServer = class {
127
129
  this.handleUpgrade(req, socket, head);
128
130
  });
129
131
  server.listen(port, () => {
132
+ process.env.TEST_PROXY_RECORDER_PORT = String(port);
130
133
  this.logServerStartup(port);
131
134
  });
132
135
  return server;
@@ -135,14 +138,17 @@ var ProxyServer = class {
135
138
  this.proxy.on("error", this.handleProxyError.bind(this));
136
139
  this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
137
140
  }
138
- handleProxyError(err, _req, res) {
141
+ handleProxyError(err, req, res) {
139
142
  console.error("Proxy error:", err);
140
143
  if (!(res instanceof http__default.default.ServerResponse)) {
141
144
  return;
142
145
  }
143
146
  if (!res.headersSent) {
147
+ const origin = req.headers.origin;
144
148
  res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
145
- "Content-Type": "application/json"
149
+ "Content-Type": "application/json",
150
+ "Access-Control-Allow-Origin": origin || "*",
151
+ "Access-Control-Allow-Credentials": "true"
146
152
  });
147
153
  }
148
154
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
@@ -198,7 +204,7 @@ var ProxyServer = class {
198
204
  async switchMode(mode, id) {
199
205
  if (this.currentSession) {
200
206
  console.log("Switching mode, saving current session first");
201
- await this.saveCurrentSession();
207
+ await this.saveCurrentSession(true);
202
208
  console.log("Session saved, continuing with mode switch");
203
209
  }
204
210
  switch (mode) {
@@ -253,13 +259,13 @@ var ProxyServer = class {
253
259
  if (timeout && timeout > 0) {
254
260
  this.modeTimeout = setTimeout(async () => {
255
261
  console.log("Timeout reached, switching back to transparent mode");
256
- await this.saveCurrentSession();
262
+ await this.saveCurrentSession(true);
257
263
  this.switchToTransparentMode();
258
264
  this.modeTimeout = null;
259
265
  }, timeout);
260
266
  }
261
267
  }
262
- async saveCurrentSession() {
268
+ async saveCurrentSession(filterIncomplete = false) {
263
269
  if (!this.currentSession) {
264
270
  console.log("No current session to save");
265
271
  return;
@@ -268,13 +274,27 @@ var ProxyServer = class {
268
274
  console.log("Session has no recordings, skipping save");
269
275
  return;
270
276
  }
277
+ if (filterIncomplete) {
278
+ const incompleteCount = this.currentSession.recordings.filter(
279
+ (r) => !r.response
280
+ ).length;
281
+ if (incompleteCount > 0) {
282
+ console.log(
283
+ `Removing ${incompleteCount} incomplete recording(s) without responses`
284
+ );
285
+ this.currentSession.recordings = this.currentSession.recordings.filter(
286
+ (r) => r.response
287
+ );
288
+ }
289
+ }
271
290
  console.log(
272
291
  `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
273
292
  );
274
293
  await saveRecordingSession(this.recordingsDir, this.currentSession);
275
294
  }
276
- async saveRequestRecord(req, body) {
295
+ saveRequestRecordSync(req, body) {
277
296
  if (!this.currentSession) {
297
+ console.log("saveRequestRecordSync: No current session");
278
298
  return;
279
299
  }
280
300
  const key = getReqID(req);
@@ -292,6 +312,30 @@ var ProxyServer = class {
292
312
  sequence: currentSequence
293
313
  };
294
314
  this.currentSession.recordings.push(record);
315
+ console.log(
316
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
317
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
318
+ );
319
+ }
320
+ updateRequestBodySync(req, body) {
321
+ if (!this.currentSession) {
322
+ console.log("updateRequestBodySync: No current session");
323
+ return;
324
+ }
325
+ const key = getReqID(req);
326
+ const record = this.currentSession.recordings.findLast(
327
+ (r) => r.key === key && !r.response
328
+ );
329
+ if (!record) {
330
+ console.error(
331
+ `updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
332
+ );
333
+ return;
334
+ }
335
+ record.request.body = body || null;
336
+ console.log(
337
+ `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
338
+ );
295
339
  }
296
340
  async recordResponse(req, proxyRes) {
297
341
  if (!this.currentSession) {
@@ -320,49 +364,105 @@ var ProxyServer = class {
320
364
  console.log(`Recorded: ${req.method} ${req.url}`);
321
365
  });
322
366
  }
367
+ async recordResponseData(req, proxyRes, body) {
368
+ if (!this.currentSession) {
369
+ console.log("recordResponseData: No current session");
370
+ return false;
371
+ }
372
+ const key = getReqID(req);
373
+ const record = this.currentSession.recordings.findLast(
374
+ (r) => r.key === key && !r.response
375
+ );
376
+ if (!record) {
377
+ const host = req.headers.host || "unknown";
378
+ const recordsWithKey = this.currentSession.recordings.filter(
379
+ (r) => r.key === key
380
+ );
381
+ console.error(
382
+ `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
383
+ );
384
+ console.error(
385
+ ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
386
+ );
387
+ console.error(
388
+ ` Records with key:`,
389
+ recordsWithKey.map((r) => ({
390
+ seq: r.sequence,
391
+ hasResponse: !!r.response
392
+ }))
393
+ );
394
+ return false;
395
+ }
396
+ record.response = {
397
+ statusCode: proxyRes.statusCode,
398
+ headers: proxyRes.headers,
399
+ body: body || null
400
+ };
401
+ await this.saveCurrentSession();
402
+ console.log(
403
+ `recordResponseData: Recorded response for ${req.method} ${req.url}`
404
+ );
405
+ return true;
406
+ }
323
407
  async handleReplayRequest(req, res) {
324
408
  const key = getReqID(req);
325
409
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
326
410
  try {
327
411
  const session = await loadRecordingSession(filePath);
328
- const currentSequence = this.replaySequenceMap.get(key) || 0;
329
- const record = session.recordings.find(
330
- (r) => r.key === key && r.sequence === currentSequence
412
+ const host = req.headers.host || "unknown";
413
+ const recordsWithKey = session.recordings.filter(
414
+ (r) => r.key === key && r.response
331
415
  );
332
- if (!record) {
416
+ if (recordsWithKey.length === 0) {
333
417
  throw new Error(
334
- `No recording found for ${key} with sequence ${currentSequence}`
418
+ `No recording found for ${key} at ${req.method} ${host}${req.url}`
335
419
  );
336
420
  }
421
+ const usageCount = this.replaySequenceMap.get(key) || 0;
422
+ const recordIndex = usageCount % recordsWithKey.length;
423
+ const record = recordsWithKey[recordIndex];
424
+ console.log(
425
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
426
+ );
427
+ this.replaySequenceMap.set(key, usageCount + 1);
337
428
  if (!record.response) {
338
- throw new Error("No response recorded for this request");
429
+ throw new Error(
430
+ `No response recorded for this request: ${req.method} ${host}${req.url}`
431
+ );
339
432
  }
340
- this.replaySequenceMap.set(key, currentSequence + 1);
341
433
  const { statusCode, headers, body } = record.response;
342
434
  const origin = req.headers.origin;
343
435
  const responseHeaders = {
344
436
  ...headers,
345
437
  "access-control-allow-origin": origin || "*",
346
- "access-control-allow-credentials": "true"
438
+ "access-control-allow-credentials": "true",
439
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
440
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
441
+ "access-control-expose-headers": "*"
347
442
  };
348
443
  res.writeHead(statusCode, responseHeaders);
349
444
  res.end(body);
350
- console.log(
351
- `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
352
- );
353
445
  } catch (error) {
354
- this.handleReplayError(res, error, key, filePath);
446
+ this.handleReplayError(req, res, error, key, filePath);
355
447
  }
356
448
  }
357
- handleReplayError(res, err, key, filePath) {
449
+ handleReplayError(req, res, err, key, filePath) {
358
450
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
359
451
  console.error("Replay error:", err);
360
- sendJsonResponse(res, HTTP_STATUS_NOT_FOUND, {
361
- error: isFileNotFound ? "Recording file not found" : "Recording not found",
362
- message: err instanceof Error ? err.message : "Unknown error",
363
- key,
364
- filePath
452
+ const origin = req.headers.origin;
453
+ res.writeHead(HTTP_STATUS_NOT_FOUND, {
454
+ "Content-Type": "application/json",
455
+ "Access-Control-Allow-Origin": origin || "*",
456
+ "Access-Control-Allow-Credentials": "true"
365
457
  });
458
+ res.end(
459
+ JSON.stringify({
460
+ error: isFileNotFound ? "Recording file not found" : "Recording not found",
461
+ message: err instanceof Error ? err.message : "Unknown error",
462
+ key,
463
+ filePath
464
+ })
465
+ );
366
466
  }
367
467
  async handleRequest(req, res) {
368
468
  if (req.method === "OPTIONS") {
@@ -392,6 +492,7 @@ var ProxyServer = class {
392
492
  const target = this.getTarget();
393
493
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
394
494
  if (this.mode === Modes.record) {
495
+ this.saveRequestRecordSync(req, null);
395
496
  await this.bufferAndProxyRequest(req, res, target);
396
497
  } else {
397
498
  this.proxy.web(req, res, { target });
@@ -402,11 +503,20 @@ var ProxyServer = class {
402
503
  req.on("data", (chunk) => {
403
504
  chunks.push(chunk);
404
505
  });
405
- await new Promise((resolve) => {
406
- req.on("end", () => resolve());
407
- });
506
+ try {
507
+ await new Promise((resolve, reject) => {
508
+ req.on("end", () => resolve());
509
+ req.on("error", (err) => reject(err));
510
+ setTimeout(
511
+ () => reject(new Error("Request buffering timeout")),
512
+ 3e4
513
+ );
514
+ });
515
+ } catch (error) {
516
+ console.error("Error buffering request:", error);
517
+ }
408
518
  const body = Buffer.concat(chunks).toString("utf8");
409
- await this.saveRequestRecord(req, body);
519
+ this.updateRequestBodySync(req, body);
410
520
  const targetUrl = new URL(target);
411
521
  const isHttps = targetUrl.protocol === "https:";
412
522
  const requestModule = isHttps ? https__default.default : http__default.default;
@@ -421,9 +531,38 @@ var ProxyServer = class {
421
531
  },
422
532
  (proxyRes) => {
423
533
  this.addCorsHeaders(proxyRes, req);
424
- this.recordResponse(req, proxyRes);
425
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
426
- proxyRes.pipe(res);
534
+ const responseChunks = [];
535
+ proxyRes.on("data", (chunk) => {
536
+ responseChunks.push(chunk);
537
+ });
538
+ proxyRes.on("end", async () => {
539
+ const responseBody = Buffer.concat(responseChunks);
540
+ const recorded = await this.recordResponseData(
541
+ req,
542
+ proxyRes,
543
+ responseBody.toString("utf8")
544
+ );
545
+ const origin = req.headers.origin;
546
+ const responseHeaders = {
547
+ ...proxyRes.headers,
548
+ "access-control-allow-origin": origin || "*",
549
+ "access-control-allow-credentials": "true",
550
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
551
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
552
+ "access-control-expose-headers": "*"
553
+ };
554
+ res.writeHead(proxyRes.statusCode || 200, responseHeaders);
555
+ res.end(responseBody);
556
+ if (recorded) {
557
+ console.log(`Recorded: ${req.method} ${req.url}`);
558
+ }
559
+ });
560
+ proxyRes.on("error", (err) => {
561
+ console.error("Proxy response error:", err);
562
+ if (!res.headersSent) {
563
+ this.handleProxyError(err, req, res);
564
+ }
565
+ });
427
566
  }
428
567
  );
429
568
  proxyReq.on("error", (err) => {
@@ -605,15 +744,25 @@ var ProxyServer = class {
605
744
  };
606
745
 
607
746
  // src/playwright/index.ts
608
- var INTERNAL_API_URL = process.env.INTERNAL_API_URL || "http://localhost:8100";
747
+ function getProxyPort() {
748
+ const envPort = process.env.TEST_PROXY_RECORDER_PORT;
749
+ if (envPort) {
750
+ const parsed = Number.parseInt(envPort, 10);
751
+ if (!Number.isNaN(parsed)) {
752
+ return parsed;
753
+ }
754
+ }
755
+ return 8100;
756
+ }
609
757
  async function setProxyMode(mode, sessionId, timeout) {
758
+ const proxyPort = getProxyPort();
610
759
  try {
611
760
  const body = {
612
761
  mode,
613
762
  id: sessionId,
614
763
  ...timeout && { timeout }
615
764
  };
616
- const response = await fetch(`${INTERNAL_API_URL}/__control`, {
765
+ const response = await fetch(`http://127.0.0.1:${proxyPort}/__control`, {
617
766
  method: "POST",
618
767
  headers: { "Content-Type": "application/json" },
619
768
  body: JSON.stringify(body)
@@ -630,8 +779,34 @@ async function setProxyMode(mode, sessionId, timeout) {
630
779
  throw error;
631
780
  }
632
781
  }
782
+ function parseSpecFilePath(specPath) {
783
+ const folderMatch = specPath.match(/^(.+?)\/([^/]+)\.(spec|test)\.ts$/);
784
+ if (folderMatch) {
785
+ return { folder: folderMatch[1], fileName: folderMatch[2] };
786
+ }
787
+ const fileMatch = specPath.match(/^([^/]+)\.(spec|test)\.ts$/);
788
+ if (fileMatch) {
789
+ return { folder: null, fileName: fileMatch[1] };
790
+ }
791
+ return { folder: null, fileName: null };
792
+ }
793
+ function buildSessionPath(folder, fileName, testName) {
794
+ if (folder && fileName) {
795
+ return `${folder}/${fileName}__${testName}`;
796
+ }
797
+ if (fileName) {
798
+ return `${fileName}__${testName}`;
799
+ }
800
+ return testName;
801
+ }
633
802
  function generateSessionId(testInfo) {
634
- return testInfo.title.toLowerCase().replaceAll(/\s+/g, "-");
803
+ const { titlePath } = testInfo;
804
+ if (!titlePath || titlePath.length === 0) {
805
+ return testInfo.title.toLowerCase().replaceAll(/\s+/g, "-");
806
+ }
807
+ const { folder, fileName } = parseSpecFilePath(titlePath[0]);
808
+ const testName = titlePath.at(-1).toLowerCase().replaceAll(/\s+/g, "-");
809
+ return buildSessionPath(folder, fileName, testName);
635
810
  }
636
811
  async function startRecording(testInfo) {
637
812
  const sessionId = generateSessionId(testInfo);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-De4mgziH.cjs';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CBjvm5rb.cjs';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -30,8 +30,10 @@ declare class ProxyServer {
30
30
  private switchToReplayMode;
31
31
  private setupModeTimeout;
32
32
  private saveCurrentSession;
33
- private saveRequestRecord;
33
+ private saveRequestRecordSync;
34
+ private updateRequestBodySync;
34
35
  private recordResponse;
36
+ private recordResponseData;
35
37
  private handleReplayRequest;
36
38
  private handleReplayError;
37
39
  private handleRequest;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-De4mgziH.js';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CBjvm5rb.js';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -30,8 +30,10 @@ declare class ProxyServer {
30
30
  private switchToReplayMode;
31
31
  private setupModeTimeout;
32
32
  private saveCurrentSession;
33
- private saveRequestRecord;
33
+ private saveRequestRecordSync;
34
+ private updateRequestBodySync;
34
35
  private recordResponse;
36
+ private recordResponseData;
35
37
  private handleReplayRequest;
36
38
  private handleReplayError;
37
39
  private handleRequest;