test-proxy-recorder 0.1.11 → 0.3.0
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 +143 -26
- package/dist/{index-Cx_Kflfl.d.cts → index-CVuiglPk.d.cts} +10 -7
- package/dist/{index-Cx_Kflfl.d.ts → index-CVuiglPk.d.ts} +10 -7
- package/dist/index.cjs +91 -17
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.mjs +88 -18
- package/dist/nextjs/index.cjs +43 -0
- package/dist/nextjs/index.d.cts +89 -0
- package/dist/nextjs/index.d.ts +89 -0
- package/dist/nextjs/index.mjs +38 -0
- package/dist/playwright/index.cjs +26 -9
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.mjs +26 -9
- package/dist/proxy.js +32 -6
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ HTTP proxy server for recording and replaying network requests in testing. Works
|
|
|
21
21
|
- [Complete Setup Guide](#complete-setup-guide)
|
|
22
22
|
- [CLI Usage](#cli-usage)
|
|
23
23
|
- [Playwright Integration](#playwright-integration)
|
|
24
|
+
- [Next.js Integration](#nextjs-integration)
|
|
24
25
|
- [Control Endpoint](#control-endpoint)
|
|
25
26
|
- [Typical Workflow](#typical-workflow)
|
|
26
27
|
- [Recording Format](#recording-format)
|
|
@@ -99,10 +100,10 @@ This marks recording files as binary, which causes long mock files to be collaps
|
|
|
99
100
|
Create `e2e/global-teardown.ts`:
|
|
100
101
|
|
|
101
102
|
```typescript
|
|
102
|
-
import {
|
|
103
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
103
104
|
|
|
104
105
|
async function globalTeardown() {
|
|
105
|
-
await
|
|
106
|
+
await playwrightProxy.teardown();
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
export default globalTeardown;
|
|
@@ -130,13 +131,11 @@ import { playwrightProxy } from 'test-proxy-recorder';
|
|
|
130
131
|
|
|
131
132
|
test('example test with proxy', async ({ page }, testInfo) => {
|
|
132
133
|
// Set proxy mode: 'record' to capture, 'replay' to use recordings
|
|
133
|
-
|
|
134
|
+
// This automatically sets up page.on('close') for cleanup
|
|
135
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
134
136
|
|
|
135
137
|
await page.goto('/');
|
|
136
138
|
await expect(page.getByText('Welcome')).toBeVisible();
|
|
137
|
-
|
|
138
|
-
// Always cleanup after test
|
|
139
|
-
await playwrightProxy.after(testInfo);
|
|
140
139
|
});
|
|
141
140
|
```
|
|
142
141
|
|
|
@@ -145,13 +144,13 @@ test('example test with proxy', async ({ page }, testInfo) => {
|
|
|
145
144
|
**First run (record mode)**:
|
|
146
145
|
|
|
147
146
|
```typescript
|
|
148
|
-
await playwrightProxy.before(testInfo, 'record');
|
|
147
|
+
await playwrightProxy.before(page, testInfo, 'record');
|
|
149
148
|
```
|
|
150
149
|
|
|
151
150
|
**Subsequent runs (replay mode)**:
|
|
152
151
|
|
|
153
152
|
```typescript
|
|
154
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
153
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
155
154
|
```
|
|
156
155
|
|
|
157
156
|
## CLI Usage
|
|
@@ -184,6 +183,12 @@ test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
|
|
|
184
183
|
|
|
185
184
|
## Playwright Integration
|
|
186
185
|
|
|
186
|
+
### Session Identification
|
|
187
|
+
|
|
188
|
+
The proxy uses a **custom HTTP header** (`x-test-rcrd-id`) to identify recording sessions. This header is automatically set by the `playwrightProxy.before()` method and works seamlessly with Next.js and other server-side rendering frameworks.
|
|
189
|
+
|
|
190
|
+
**Cookie fallback**: For backward compatibility, the proxy also supports cookie-based session identification, but the custom header is preferred.
|
|
191
|
+
|
|
187
192
|
### Basic Test Structure
|
|
188
193
|
|
|
189
194
|
Every test using the proxy should follow this pattern:
|
|
@@ -193,40 +198,38 @@ import { test } from '@playwright/test';
|
|
|
193
198
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
194
199
|
|
|
195
200
|
test('test name', async ({ page }, testInfo) => {
|
|
196
|
-
//
|
|
197
|
-
|
|
201
|
+
// Set mode BEFORE test actions
|
|
202
|
+
// This automatically sets the recording ID header and cleanup handler
|
|
203
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
198
204
|
|
|
199
|
-
//
|
|
205
|
+
// Test code
|
|
200
206
|
await page.goto('/page');
|
|
201
|
-
|
|
202
|
-
// 3. Reset mode AFTER test completes
|
|
203
|
-
await playwrightProxy.after(testInfo);
|
|
207
|
+
// Test assertions...
|
|
204
208
|
});
|
|
205
209
|
```
|
|
206
210
|
|
|
207
211
|
### Recording vs Replay
|
|
208
212
|
|
|
209
213
|
```typescript
|
|
214
|
+
import { test } from '@playwright/test';
|
|
215
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
216
|
+
|
|
210
217
|
// Recording mode - captures API responses
|
|
211
218
|
test('create user', async ({ page }, testInfo) => {
|
|
212
|
-
await playwrightProxy.before(testInfo, 'record');
|
|
219
|
+
await playwrightProxy.before(page, testInfo, 'record');
|
|
213
220
|
|
|
214
221
|
await page.goto('/users/new');
|
|
215
222
|
await page.fill('[name="username"]', 'testuser');
|
|
216
223
|
await page.click('button[type="submit"]');
|
|
217
|
-
|
|
218
|
-
await playwrightProxy.after(testInfo);
|
|
219
224
|
});
|
|
220
225
|
|
|
221
226
|
// Replay mode - uses recorded responses
|
|
222
227
|
test('create user', async ({ page }, testInfo) => {
|
|
223
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
228
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
224
229
|
|
|
225
230
|
await page.goto('/users/new');
|
|
226
231
|
await page.fill('[name="username"]', 'testuser');
|
|
227
232
|
await page.click('button[type="submit"]');
|
|
228
|
-
|
|
229
|
-
await playwrightProxy.after(testInfo);
|
|
230
233
|
});
|
|
231
234
|
```
|
|
232
235
|
|
|
@@ -244,10 +247,10 @@ Recording files are auto-generated from test names:
|
|
|
244
247
|
Create `e2e/global-teardown.ts`:
|
|
245
248
|
|
|
246
249
|
```typescript
|
|
247
|
-
import {
|
|
250
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
248
251
|
|
|
249
252
|
async function globalTeardown() {
|
|
250
|
-
await
|
|
253
|
+
await playwrightProxy.teardown();
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
export default globalTeardown;
|
|
@@ -265,6 +268,78 @@ export default defineConfig({
|
|
|
265
268
|
});
|
|
266
269
|
```
|
|
267
270
|
|
|
271
|
+
## Next.js Integration
|
|
272
|
+
|
|
273
|
+
When testing Next.js applications with server-side rendering (SSR) or API routes, you need to ensure the recording ID header is forwarded to the proxy. The package provides helpers for this.
|
|
274
|
+
|
|
275
|
+
### Option 1: Using Next.js Middleware (Recommended)
|
|
276
|
+
|
|
277
|
+
Create or update `middleware.ts` in your Next.js project root:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { NextResponse } from 'next/server';
|
|
281
|
+
import type { NextRequest } from 'next/server';
|
|
282
|
+
import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
|
|
283
|
+
|
|
284
|
+
export function middleware(request: NextRequest) {
|
|
285
|
+
const response = NextResponse.next();
|
|
286
|
+
|
|
287
|
+
// Forward the recording ID header during tests
|
|
288
|
+
// Only runs in non-production or when TEST_PROXY_RECORDER_ENABLED=true
|
|
289
|
+
setNextProxyHeaders(request, response);
|
|
290
|
+
|
|
291
|
+
return response;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Environment Variables:**
|
|
296
|
+
- Automatically skipped when `NODE_ENV=production`
|
|
297
|
+
- Can be explicitly enabled in production with `TEST_PROXY_RECORDER_ENABLED=true`
|
|
298
|
+
|
|
299
|
+
### Option 2: Manual Header Forwarding in API Routes
|
|
300
|
+
|
|
301
|
+
For API routes or server components, manually include the header in fetch requests:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// app/api/data/route.ts
|
|
305
|
+
import { headers } from 'next/headers';
|
|
306
|
+
import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
|
|
307
|
+
|
|
308
|
+
export async function GET() {
|
|
309
|
+
const requestHeaders = await headers();
|
|
310
|
+
|
|
311
|
+
const response = await fetch('http://localhost:8100/api/data', {
|
|
312
|
+
headers: createHeadersWithRecordingId(requestHeaders, {
|
|
313
|
+
'Content-Type': 'application/json',
|
|
314
|
+
})
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return Response.json(await response.json());
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Option 3: Using getRecordingId Helper
|
|
322
|
+
|
|
323
|
+
For more control, extract the recording ID and use it manually:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { headers } from 'next/headers';
|
|
327
|
+
import { getRecordingId, RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
|
|
328
|
+
|
|
329
|
+
export async function GET() {
|
|
330
|
+
const recordingId = getRecordingId(await headers());
|
|
331
|
+
|
|
332
|
+
const response = await fetch('http://localhost:8100/api/data', {
|
|
333
|
+
headers: {
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
...(recordingId && { [RECORDING_ID_HEADER]: recordingId })
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return Response.json(await response.json());
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
268
343
|
## Control Endpoint
|
|
269
344
|
|
|
270
345
|
The proxy exposes a control endpoint at `/__control` for programmatic mode switching.
|
|
@@ -401,19 +476,24 @@ class ProxyServer {
|
|
|
401
476
|
### Playwright Integration
|
|
402
477
|
|
|
403
478
|
```typescript
|
|
404
|
-
import { playwrightProxy, setProxyMode } from 'test-proxy-recorder';
|
|
479
|
+
import { playwrightProxy, setProxyMode, RECORDING_ID_HEADER } from 'test-proxy-recorder';
|
|
480
|
+
import type { Page } from '@playwright/test';
|
|
405
481
|
|
|
406
482
|
// Main helper for Playwright tests
|
|
407
483
|
const playwrightProxy = {
|
|
408
|
-
// Set proxy mode before test
|
|
484
|
+
// Set proxy mode before test and configure page with recording ID header
|
|
485
|
+
// Automatically sets up page.on('close') handler for cleanup
|
|
409
486
|
async before(
|
|
487
|
+
page: Page,
|
|
410
488
|
testInfo: TestInfo,
|
|
411
489
|
mode: 'record' | 'replay' | 'transparent',
|
|
412
490
|
timeout?: number
|
|
413
491
|
): Promise<void>;
|
|
414
492
|
|
|
415
|
-
|
|
416
|
-
|
|
493
|
+
|
|
494
|
+
// Global teardown - switches proxy to transparent mode
|
|
495
|
+
// Use in Playwright's globalTeardown configuration
|
|
496
|
+
async teardown(): Promise<void>;
|
|
417
497
|
};
|
|
418
498
|
|
|
419
499
|
// Direct mode control
|
|
@@ -422,6 +502,41 @@ async function setProxyMode(
|
|
|
422
502
|
id?: string,
|
|
423
503
|
timeout?: number
|
|
424
504
|
): Promise<void>;
|
|
505
|
+
|
|
506
|
+
// Recording ID header constant
|
|
507
|
+
const RECORDING_ID_HEADER: string; // 'x-test-rcrd-id'
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Next.js Integration
|
|
511
|
+
|
|
512
|
+
**IMPORTANT**: Use the `/nextjs` import path to avoid webpack bundling issues in Next.js:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import {
|
|
516
|
+
setNextProxyHeaders,
|
|
517
|
+
getRecordingId,
|
|
518
|
+
createHeadersWithRecordingId,
|
|
519
|
+
RECORDING_ID_HEADER
|
|
520
|
+
} from 'test-proxy-recorder/nextjs';
|
|
521
|
+
import type { NextRequest, NextResponse } from 'next/server';
|
|
522
|
+
|
|
523
|
+
// Forward recording ID header in Next.js middleware
|
|
524
|
+
// Automatically skipped in production unless TEST_PROXY_RECORDER_ENABLED=true
|
|
525
|
+
function setNextProxyHeaders(
|
|
526
|
+
request: NextRequest,
|
|
527
|
+
response: NextResponse
|
|
528
|
+
): void;
|
|
529
|
+
|
|
530
|
+
// Get recording ID from request headers
|
|
531
|
+
function getRecordingId(
|
|
532
|
+
requestHeaders: NextRequest | Headers
|
|
533
|
+
): string | null;
|
|
534
|
+
|
|
535
|
+
// Create headers object with recording ID for fetch requests
|
|
536
|
+
function createHeadersWithRecordingId(
|
|
537
|
+
requestHeaders: NextRequest | Headers,
|
|
538
|
+
additionalHeaders?: Record<string, string>
|
|
539
|
+
): Record<string, string>;
|
|
425
540
|
```
|
|
426
541
|
|
|
427
542
|
### Control Endpoint
|
|
@@ -438,6 +553,8 @@ async function setProxyMode(
|
|
|
438
553
|
}
|
|
439
554
|
```
|
|
440
555
|
|
|
556
|
+
**Note**: Switching to replay mode automatically resets session counters (clears served recordings tracker), allowing replay from the beginning.
|
|
557
|
+
|
|
441
558
|
**Response**:
|
|
442
559
|
|
|
443
560
|
```typescript
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TestInfo } from '@playwright/test';
|
|
1
|
+
import { TestInfo, Page } from '@playwright/test';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
|
|
4
4
|
declare const Modes: {
|
|
@@ -82,20 +82,23 @@ declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
|
82
82
|
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
83
83
|
/**
|
|
84
84
|
* Playwright test fixture helper for managing proxy mode
|
|
85
|
-
* Use this in
|
|
85
|
+
* Use this in test functions with page.on('close') for automatic cleanup
|
|
86
86
|
*/
|
|
87
87
|
declare const playwrightProxy: {
|
|
88
88
|
/**
|
|
89
|
-
* Setup before test - sets the proxy mode
|
|
89
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
90
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
91
|
+
* @param page - Playwright page object
|
|
90
92
|
* @param testInfo - Playwright test info object
|
|
91
93
|
* @param mode - The proxy mode to use for this test
|
|
94
|
+
* @param timeout - Optional timeout in milliseconds
|
|
92
95
|
*/
|
|
93
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
96
|
+
before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
94
97
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
98
|
+
* Global teardown - switches proxy to transparent mode
|
|
99
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
97
100
|
*/
|
|
98
|
-
|
|
101
|
+
teardown(): Promise<void>;
|
|
99
102
|
};
|
|
100
103
|
|
|
101
104
|
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TestInfo } from '@playwright/test';
|
|
1
|
+
import { TestInfo, Page } from '@playwright/test';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
|
|
4
4
|
declare const Modes: {
|
|
@@ -82,20 +82,23 @@ declare function startReplay(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
|
82
82
|
declare function stopProxy(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
83
83
|
/**
|
|
84
84
|
* Playwright test fixture helper for managing proxy mode
|
|
85
|
-
* Use this in
|
|
85
|
+
* Use this in test functions with page.on('close') for automatic cleanup
|
|
86
86
|
*/
|
|
87
87
|
declare const playwrightProxy: {
|
|
88
88
|
/**
|
|
89
|
-
* Setup before test - sets the proxy mode
|
|
89
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
90
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
91
|
+
* @param page - Playwright page object
|
|
90
92
|
* @param testInfo - Playwright test info object
|
|
91
93
|
* @param mode - The proxy mode to use for this test
|
|
94
|
+
* @param timeout - Optional timeout in milliseconds
|
|
92
95
|
*/
|
|
93
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode): Promise<void>;
|
|
96
|
+
before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
94
97
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
98
|
+
* Global teardown - switches proxy to transparent mode
|
|
99
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
97
100
|
*/
|
|
98
|
-
|
|
101
|
+
teardown(): Promise<void>;
|
|
99
102
|
};
|
|
100
103
|
|
|
101
104
|
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
|
@@ -19,8 +19,6 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
|
19
19
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
20
|
var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
|
|
21
21
|
|
|
22
|
-
// src/ProxyServer.ts
|
|
23
|
-
|
|
24
22
|
// src/constants.ts
|
|
25
23
|
var DEFAULT_TIMEOUT_MS = 120 * 1e3;
|
|
26
24
|
var HTTP_STATUS_BAD_GATEWAY = 502;
|
|
@@ -28,6 +26,7 @@ var HTTP_STATUS_OK = 200;
|
|
|
28
26
|
var HTTP_STATUS_BAD_REQUEST = 400;
|
|
29
27
|
var HTTP_STATUS_NOT_FOUND = 404;
|
|
30
28
|
var CONTROL_ENDPOINT = "/__control";
|
|
29
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
31
30
|
|
|
32
31
|
// src/types.ts
|
|
33
32
|
var Modes = {
|
|
@@ -200,7 +199,7 @@ var ProxyServer = class {
|
|
|
200
199
|
return {
|
|
201
200
|
"access-control-allow-origin": origin || "*",
|
|
202
201
|
"access-control-allow-credentials": "true",
|
|
203
|
-
"access-control-allow-headers": req.headers["access-control-request-headers"] ||
|
|
202
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
|
|
204
203
|
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
205
204
|
"access-control-expose-headers": "*"
|
|
206
205
|
};
|
|
@@ -214,9 +213,22 @@ var ProxyServer = class {
|
|
|
214
213
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
215
214
|
return target;
|
|
216
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Extract recording ID from custom HTTP header
|
|
218
|
+
* Used for concurrent replay session routing, especially with Next.js
|
|
219
|
+
* @param req The incoming HTTP request
|
|
220
|
+
* @returns The recording ID from header, or null if not found
|
|
221
|
+
*/
|
|
222
|
+
getRecordingIdFromHeader(req) {
|
|
223
|
+
const headerValue = req.headers[RECORDING_ID_HEADER];
|
|
224
|
+
if (!headerValue) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
228
|
+
}
|
|
217
229
|
/**
|
|
218
230
|
* Extract recording ID from request cookie
|
|
219
|
-
* Used for concurrent replay session routing
|
|
231
|
+
* Used for concurrent replay session routing (fallback method)
|
|
220
232
|
* @param req The incoming HTTP request
|
|
221
233
|
* @returns The recording ID from cookie, or null if not found
|
|
222
234
|
*/
|
|
@@ -228,6 +240,14 @@ var ProxyServer = class {
|
|
|
228
240
|
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
229
241
|
return match ? decodeURIComponent(match[1]) : null;
|
|
230
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Extract recording ID from request using custom header (preferred) or cookie (fallback)
|
|
245
|
+
* @param req The incoming HTTP request
|
|
246
|
+
* @returns The recording ID, or null if not found
|
|
247
|
+
*/
|
|
248
|
+
getRecordingIdFromRequest(req) {
|
|
249
|
+
return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
|
|
250
|
+
}
|
|
231
251
|
/**
|
|
232
252
|
* Get or create a replay session state for a given recording ID
|
|
233
253
|
* @param recordingId The recording ID to get/create session for
|
|
@@ -505,7 +525,7 @@ var ProxyServer = class {
|
|
|
505
525
|
return true;
|
|
506
526
|
}
|
|
507
527
|
async handleReplayRequest(req, res) {
|
|
508
|
-
const recordingId = this.
|
|
528
|
+
const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
|
|
509
529
|
if (!recordingId) {
|
|
510
530
|
const corsHeaders = this.getCorsHeaders(req);
|
|
511
531
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -529,7 +549,11 @@ var ProxyServer = class {
|
|
|
529
549
|
}
|
|
530
550
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
531
551
|
const host = req.headers.host || "unknown";
|
|
532
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
552
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
553
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
554
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
555
|
+
return aSeq - bSeq;
|
|
556
|
+
});
|
|
533
557
|
if (recordsWithKey.length === 0) {
|
|
534
558
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
535
559
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -552,7 +576,7 @@ var ProxyServer = class {
|
|
|
552
576
|
}
|
|
553
577
|
const requestCount = servedForThisKey.size + 1;
|
|
554
578
|
console.log(
|
|
555
|
-
`[
|
|
579
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
556
580
|
);
|
|
557
581
|
let record;
|
|
558
582
|
for (const rec of recordsWithKey) {
|
|
@@ -569,7 +593,7 @@ var ProxyServer = class {
|
|
|
569
593
|
}
|
|
570
594
|
servedForThisKey.add(record.recordingId);
|
|
571
595
|
console.log(
|
|
572
|
-
`[
|
|
596
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
573
597
|
);
|
|
574
598
|
if (!record.response) {
|
|
575
599
|
throw new Error(
|
|
@@ -950,28 +974,78 @@ async function stopProxy(testInfo) {
|
|
|
950
974
|
}
|
|
951
975
|
var playwrightProxy = {
|
|
952
976
|
/**
|
|
953
|
-
* Setup before test - sets the proxy mode
|
|
977
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
978
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
979
|
+
* @param page - Playwright page object
|
|
954
980
|
* @param testInfo - Playwright test info object
|
|
955
981
|
* @param mode - The proxy mode to use for this test
|
|
982
|
+
* @param timeout - Optional timeout in milliseconds
|
|
956
983
|
*/
|
|
957
|
-
async before(testInfo, mode) {
|
|
984
|
+
async before(page, testInfo, mode, timeout) {
|
|
958
985
|
const sessionId = generateSessionId(testInfo);
|
|
959
|
-
|
|
960
|
-
|
|
986
|
+
await page.setExtraHTTPHeaders({
|
|
987
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
988
|
+
});
|
|
989
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
990
|
+
page.on("close", async () => {
|
|
991
|
+
try {
|
|
992
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
993
|
+
console.log(
|
|
994
|
+
`[Cleanup] Switched to transparent mode for session: ${sessionId}`
|
|
995
|
+
);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
961
1000
|
},
|
|
962
1001
|
/**
|
|
963
|
-
*
|
|
964
|
-
*
|
|
1002
|
+
* Global teardown - switches proxy to transparent mode
|
|
1003
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
965
1004
|
*/
|
|
966
|
-
async
|
|
967
|
-
|
|
968
|
-
await setProxyMode(Modes.transparent, sessionId);
|
|
1005
|
+
async teardown() {
|
|
1006
|
+
await setProxyMode(Modes.transparent);
|
|
969
1007
|
}
|
|
970
1008
|
};
|
|
971
1009
|
|
|
1010
|
+
// src/nextjs/middleware.ts
|
|
1011
|
+
function isRecorderEnabled() {
|
|
1012
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1013
|
+
const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
|
|
1014
|
+
return !isProduction || isExplicitlyEnabled;
|
|
1015
|
+
}
|
|
1016
|
+
function setNextProxyHeaders(request, response) {
|
|
1017
|
+
if (!isRecorderEnabled()) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const recordingId = request.headers.get(RECORDING_ID_HEADER);
|
|
1021
|
+
if (recordingId) {
|
|
1022
|
+
response.headers.set(RECORDING_ID_HEADER, recordingId);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function getRecordingId(requestHeaders) {
|
|
1026
|
+
if (requestHeaders instanceof Headers) {
|
|
1027
|
+
return requestHeaders.get(RECORDING_ID_HEADER);
|
|
1028
|
+
}
|
|
1029
|
+
return requestHeaders.headers.get(RECORDING_ID_HEADER);
|
|
1030
|
+
}
|
|
1031
|
+
function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
|
|
1032
|
+
if (!isRecorderEnabled()) {
|
|
1033
|
+
return additionalHeaders;
|
|
1034
|
+
}
|
|
1035
|
+
const recordingId = getRecordingId(requestHeaders);
|
|
1036
|
+
return {
|
|
1037
|
+
...additionalHeaders,
|
|
1038
|
+
...recordingId && { [RECORDING_ID_HEADER]: recordingId }
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
972
1042
|
exports.ProxyServer = ProxyServer;
|
|
1043
|
+
exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
|
|
1044
|
+
exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
|
|
973
1045
|
exports.generateSessionId = generateSessionId;
|
|
1046
|
+
exports.getRecordingId = getRecordingId;
|
|
974
1047
|
exports.playwrightProxy = playwrightProxy;
|
|
1048
|
+
exports.setNextProxyHeaders = setNextProxyHeaders;
|
|
975
1049
|
exports.setProxyMode = setProxyMode;
|
|
976
1050
|
exports.startRecording = startRecording;
|
|
977
1051
|
exports.startReplay = startReplay;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.cjs';
|
|
1
2
|
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-
|
|
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-CVuiglPk.cjs';
|
|
3
4
|
import '@playwright/test';
|
|
4
5
|
|
|
5
6
|
declare class ProxyServer {
|
|
@@ -28,13 +29,26 @@ declare class ProxyServer {
|
|
|
28
29
|
private getCorsHeaders;
|
|
29
30
|
private addCorsHeaders;
|
|
30
31
|
private getTarget;
|
|
32
|
+
/**
|
|
33
|
+
* Extract recording ID from custom HTTP header
|
|
34
|
+
* Used for concurrent replay session routing, especially with Next.js
|
|
35
|
+
* @param req The incoming HTTP request
|
|
36
|
+
* @returns The recording ID from header, or null if not found
|
|
37
|
+
*/
|
|
38
|
+
private getRecordingIdFromHeader;
|
|
31
39
|
/**
|
|
32
40
|
* Extract recording ID from request cookie
|
|
33
|
-
* Used for concurrent replay session routing
|
|
41
|
+
* Used for concurrent replay session routing (fallback method)
|
|
34
42
|
* @param req The incoming HTTP request
|
|
35
43
|
* @returns The recording ID from cookie, or null if not found
|
|
36
44
|
*/
|
|
37
45
|
private getRecordingIdFromCookie;
|
|
46
|
+
/**
|
|
47
|
+
* Extract recording ID from request using custom header (preferred) or cookie (fallback)
|
|
48
|
+
* @param req The incoming HTTP request
|
|
49
|
+
* @returns The recording ID, or null if not found
|
|
50
|
+
*/
|
|
51
|
+
private getRecordingIdFromRequest;
|
|
38
52
|
/**
|
|
39
53
|
* Get or create a replay session state for a given recording ID
|
|
40
54
|
* @param recordingId The recording ID to get/create session for
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.js';
|
|
1
2
|
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-
|
|
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-CVuiglPk.js';
|
|
3
4
|
import '@playwright/test';
|
|
4
5
|
|
|
5
6
|
declare class ProxyServer {
|
|
@@ -28,13 +29,26 @@ declare class ProxyServer {
|
|
|
28
29
|
private getCorsHeaders;
|
|
29
30
|
private addCorsHeaders;
|
|
30
31
|
private getTarget;
|
|
32
|
+
/**
|
|
33
|
+
* Extract recording ID from custom HTTP header
|
|
34
|
+
* Used for concurrent replay session routing, especially with Next.js
|
|
35
|
+
* @param req The incoming HTTP request
|
|
36
|
+
* @returns The recording ID from header, or null if not found
|
|
37
|
+
*/
|
|
38
|
+
private getRecordingIdFromHeader;
|
|
31
39
|
/**
|
|
32
40
|
* Extract recording ID from request cookie
|
|
33
|
-
* Used for concurrent replay session routing
|
|
41
|
+
* Used for concurrent replay session routing (fallback method)
|
|
34
42
|
* @param req The incoming HTTP request
|
|
35
43
|
* @returns The recording ID from cookie, or null if not found
|
|
36
44
|
*/
|
|
37
45
|
private getRecordingIdFromCookie;
|
|
46
|
+
/**
|
|
47
|
+
* Extract recording ID from request using custom header (preferred) or cookie (fallback)
|
|
48
|
+
* @param req The incoming HTTP request
|
|
49
|
+
* @returns The recording ID, or null if not found
|
|
50
|
+
*/
|
|
51
|
+
private getRecordingIdFromRequest;
|
|
38
52
|
/**
|
|
39
53
|
* Get or create a replay session state for a given recording ID
|
|
40
54
|
* @param recordingId The recording ID to get/create session for
|
package/dist/index.mjs
CHANGED
|
@@ -7,8 +7,6 @@ import crypto from 'crypto';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import filenamify2 from 'filenamify';
|
|
9
9
|
|
|
10
|
-
// src/ProxyServer.ts
|
|
11
|
-
|
|
12
10
|
// src/constants.ts
|
|
13
11
|
var DEFAULT_TIMEOUT_MS = 120 * 1e3;
|
|
14
12
|
var HTTP_STATUS_BAD_GATEWAY = 502;
|
|
@@ -16,6 +14,7 @@ var HTTP_STATUS_OK = 200;
|
|
|
16
14
|
var HTTP_STATUS_BAD_REQUEST = 400;
|
|
17
15
|
var HTTP_STATUS_NOT_FOUND = 404;
|
|
18
16
|
var CONTROL_ENDPOINT = "/__control";
|
|
17
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
19
18
|
|
|
20
19
|
// src/types.ts
|
|
21
20
|
var Modes = {
|
|
@@ -188,7 +187,7 @@ var ProxyServer = class {
|
|
|
188
187
|
return {
|
|
189
188
|
"access-control-allow-origin": origin || "*",
|
|
190
189
|
"access-control-allow-credentials": "true",
|
|
191
|
-
"access-control-allow-headers": req.headers["access-control-request-headers"] ||
|
|
190
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
|
|
192
191
|
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
193
192
|
"access-control-expose-headers": "*"
|
|
194
193
|
};
|
|
@@ -202,9 +201,22 @@ var ProxyServer = class {
|
|
|
202
201
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
203
202
|
return target;
|
|
204
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract recording ID from custom HTTP header
|
|
206
|
+
* Used for concurrent replay session routing, especially with Next.js
|
|
207
|
+
* @param req The incoming HTTP request
|
|
208
|
+
* @returns The recording ID from header, or null if not found
|
|
209
|
+
*/
|
|
210
|
+
getRecordingIdFromHeader(req) {
|
|
211
|
+
const headerValue = req.headers[RECORDING_ID_HEADER];
|
|
212
|
+
if (!headerValue) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
216
|
+
}
|
|
205
217
|
/**
|
|
206
218
|
* Extract recording ID from request cookie
|
|
207
|
-
* Used for concurrent replay session routing
|
|
219
|
+
* Used for concurrent replay session routing (fallback method)
|
|
208
220
|
* @param req The incoming HTTP request
|
|
209
221
|
* @returns The recording ID from cookie, or null if not found
|
|
210
222
|
*/
|
|
@@ -216,6 +228,14 @@ var ProxyServer = class {
|
|
|
216
228
|
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
217
229
|
return match ? decodeURIComponent(match[1]) : null;
|
|
218
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Extract recording ID from request using custom header (preferred) or cookie (fallback)
|
|
233
|
+
* @param req The incoming HTTP request
|
|
234
|
+
* @returns The recording ID, or null if not found
|
|
235
|
+
*/
|
|
236
|
+
getRecordingIdFromRequest(req) {
|
|
237
|
+
return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
|
|
238
|
+
}
|
|
219
239
|
/**
|
|
220
240
|
* Get or create a replay session state for a given recording ID
|
|
221
241
|
* @param recordingId The recording ID to get/create session for
|
|
@@ -493,7 +513,7 @@ var ProxyServer = class {
|
|
|
493
513
|
return true;
|
|
494
514
|
}
|
|
495
515
|
async handleReplayRequest(req, res) {
|
|
496
|
-
const recordingId = this.
|
|
516
|
+
const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
|
|
497
517
|
if (!recordingId) {
|
|
498
518
|
const corsHeaders = this.getCorsHeaders(req);
|
|
499
519
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -517,7 +537,11 @@ var ProxyServer = class {
|
|
|
517
537
|
}
|
|
518
538
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
519
539
|
const host = req.headers.host || "unknown";
|
|
520
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
540
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
541
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
542
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
543
|
+
return aSeq - bSeq;
|
|
544
|
+
});
|
|
521
545
|
if (recordsWithKey.length === 0) {
|
|
522
546
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
523
547
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -540,7 +564,7 @@ var ProxyServer = class {
|
|
|
540
564
|
}
|
|
541
565
|
const requestCount = servedForThisKey.size + 1;
|
|
542
566
|
console.log(
|
|
543
|
-
`[
|
|
567
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
544
568
|
);
|
|
545
569
|
let record;
|
|
546
570
|
for (const rec of recordsWithKey) {
|
|
@@ -557,7 +581,7 @@ var ProxyServer = class {
|
|
|
557
581
|
}
|
|
558
582
|
servedForThisKey.add(record.recordingId);
|
|
559
583
|
console.log(
|
|
560
|
-
`[
|
|
584
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
561
585
|
);
|
|
562
586
|
if (!record.response) {
|
|
563
587
|
throw new Error(
|
|
@@ -938,25 +962,71 @@ async function stopProxy(testInfo) {
|
|
|
938
962
|
}
|
|
939
963
|
var playwrightProxy = {
|
|
940
964
|
/**
|
|
941
|
-
* Setup before test - sets the proxy mode
|
|
965
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
966
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
967
|
+
* @param page - Playwright page object
|
|
942
968
|
* @param testInfo - Playwright test info object
|
|
943
969
|
* @param mode - The proxy mode to use for this test
|
|
970
|
+
* @param timeout - Optional timeout in milliseconds
|
|
944
971
|
*/
|
|
945
|
-
async before(testInfo, mode) {
|
|
972
|
+
async before(page, testInfo, mode, timeout) {
|
|
946
973
|
const sessionId = generateSessionId(testInfo);
|
|
947
|
-
|
|
948
|
-
|
|
974
|
+
await page.setExtraHTTPHeaders({
|
|
975
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
976
|
+
});
|
|
977
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
978
|
+
page.on("close", async () => {
|
|
979
|
+
try {
|
|
980
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
981
|
+
console.log(
|
|
982
|
+
`[Cleanup] Switched to transparent mode for session: ${sessionId}`
|
|
983
|
+
);
|
|
984
|
+
} catch (error) {
|
|
985
|
+
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
986
|
+
}
|
|
987
|
+
});
|
|
949
988
|
},
|
|
950
989
|
/**
|
|
951
|
-
*
|
|
952
|
-
*
|
|
990
|
+
* Global teardown - switches proxy to transparent mode
|
|
991
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
953
992
|
*/
|
|
954
|
-
async
|
|
955
|
-
|
|
956
|
-
await setProxyMode(Modes.transparent, sessionId);
|
|
993
|
+
async teardown() {
|
|
994
|
+
await setProxyMode(Modes.transparent);
|
|
957
995
|
}
|
|
958
996
|
};
|
|
959
997
|
|
|
960
|
-
|
|
998
|
+
// src/nextjs/middleware.ts
|
|
999
|
+
function isRecorderEnabled() {
|
|
1000
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1001
|
+
const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
|
|
1002
|
+
return !isProduction || isExplicitlyEnabled;
|
|
1003
|
+
}
|
|
1004
|
+
function setNextProxyHeaders(request, response) {
|
|
1005
|
+
if (!isRecorderEnabled()) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const recordingId = request.headers.get(RECORDING_ID_HEADER);
|
|
1009
|
+
if (recordingId) {
|
|
1010
|
+
response.headers.set(RECORDING_ID_HEADER, recordingId);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function getRecordingId(requestHeaders) {
|
|
1014
|
+
if (requestHeaders instanceof Headers) {
|
|
1015
|
+
return requestHeaders.get(RECORDING_ID_HEADER);
|
|
1016
|
+
}
|
|
1017
|
+
return requestHeaders.headers.get(RECORDING_ID_HEADER);
|
|
1018
|
+
}
|
|
1019
|
+
function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
|
|
1020
|
+
if (!isRecorderEnabled()) {
|
|
1021
|
+
return additionalHeaders;
|
|
1022
|
+
}
|
|
1023
|
+
const recordingId = getRecordingId(requestHeaders);
|
|
1024
|
+
return {
|
|
1025
|
+
...additionalHeaders,
|
|
1026
|
+
...recordingId && { [RECORDING_ID_HEADER]: recordingId }
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export { ProxyServer, RECORDING_ID_HEADER, createHeadersWithRecordingId, generateSessionId, getRecordingId, playwrightProxy, setNextProxyHeaders, setProxyMode, startRecording, startReplay, stopProxy };
|
|
961
1031
|
//# sourceMappingURL=index.mjs.map
|
|
962
1032
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
5
|
+
|
|
6
|
+
// src/nextjs/middleware.ts
|
|
7
|
+
function isRecorderEnabled() {
|
|
8
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
9
|
+
const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
|
|
10
|
+
return !isProduction || isExplicitlyEnabled;
|
|
11
|
+
}
|
|
12
|
+
function setNextProxyHeaders(request, response) {
|
|
13
|
+
if (!isRecorderEnabled()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const recordingId = request.headers.get(RECORDING_ID_HEADER);
|
|
17
|
+
if (recordingId) {
|
|
18
|
+
response.headers.set(RECORDING_ID_HEADER, recordingId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getRecordingId(requestHeaders) {
|
|
22
|
+
if (requestHeaders instanceof Headers) {
|
|
23
|
+
return requestHeaders.get(RECORDING_ID_HEADER);
|
|
24
|
+
}
|
|
25
|
+
return requestHeaders.headers.get(RECORDING_ID_HEADER);
|
|
26
|
+
}
|
|
27
|
+
function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
|
|
28
|
+
if (!isRecorderEnabled()) {
|
|
29
|
+
return additionalHeaders;
|
|
30
|
+
}
|
|
31
|
+
const recordingId = getRecordingId(requestHeaders);
|
|
32
|
+
return {
|
|
33
|
+
...additionalHeaders,
|
|
34
|
+
...recordingId && { [RECORDING_ID_HEADER]: recordingId }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
|
|
39
|
+
exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
|
|
40
|
+
exports.getRecordingId = getRecordingId;
|
|
41
|
+
exports.setNextProxyHeaders = setNextProxyHeaders;
|
|
42
|
+
//# sourceMappingURL=index.cjs.map
|
|
43
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
declare const RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal type for Next.js Request - compatible with next/server's NextRequest
|
|
5
|
+
* We define this locally to avoid requiring Next.js as a dependency
|
|
6
|
+
*/
|
|
7
|
+
interface NextJSRequest {
|
|
8
|
+
headers: Headers;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal type for Next.js Response - compatible with next/server's NextResponse
|
|
12
|
+
* We define this locally to avoid requiring Next.js as a dependency
|
|
13
|
+
*/
|
|
14
|
+
interface NextJSResponse {
|
|
15
|
+
headers: Headers;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Next.js middleware helper for forwarding test proxy recording headers
|
|
19
|
+
* Automatically forwards the recording ID header from incoming requests to the proxy
|
|
20
|
+
* Only runs in non-production environments or when TEST_PROXY_RECORDER_ENABLED is set
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // middleware.ts
|
|
24
|
+
* import { NextResponse } from 'next/server';
|
|
25
|
+
* import type { NextRequest } from 'next/server';
|
|
26
|
+
* import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
|
|
27
|
+
*
|
|
28
|
+
* export function middleware(request: NextRequest) {
|
|
29
|
+
* const response = NextResponse.next();
|
|
30
|
+
* // Only forwards headers in test/dev environments
|
|
31
|
+
* setNextProxyHeaders(request, response);
|
|
32
|
+
* return response;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @param request - Next.js request object (NextRequest from next/server)
|
|
36
|
+
* @param response - Next.js response object (NextResponse from next/server)
|
|
37
|
+
*/
|
|
38
|
+
declare function setNextProxyHeaders(request: NextJSRequest, response: NextJSResponse): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get the recording ID from the request if present
|
|
41
|
+
* Useful for manually adding the header to fetch requests in Next.js
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // In your API route or server component
|
|
45
|
+
* import { getRecordingId } from 'test-proxy-recorder/nextjs';
|
|
46
|
+
* import { headers } from 'next/headers';
|
|
47
|
+
*
|
|
48
|
+
* export async function GET() {
|
|
49
|
+
* const recordingId = getRecordingId(headers());
|
|
50
|
+
*
|
|
51
|
+
* const response = await fetch('http://localhost:8100/api/data', {
|
|
52
|
+
* headers: {
|
|
53
|
+
* ...(recordingId && { 'x-test-rcrd-id': recordingId })
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* return Response.json(await response.json());
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @param requestHeaders - Next.js headers object or NextRequest from next/server
|
|
61
|
+
* @returns The recording ID if present, null otherwise
|
|
62
|
+
*/
|
|
63
|
+
declare function getRecordingId(requestHeaders: NextJSRequest | Headers): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Create headers object with recording ID for fetch requests
|
|
66
|
+
* Use this helper when making fetch requests in Next.js to forward the recording ID
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // In your API route or server component
|
|
70
|
+
* import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
|
|
71
|
+
* import { headers } from 'next/headers';
|
|
72
|
+
*
|
|
73
|
+
* export async function GET() {
|
|
74
|
+
* const response = await fetch('http://localhost:8100/api/data', {
|
|
75
|
+
* headers: createHeadersWithRecordingId(headers(), {
|
|
76
|
+
* 'Content-Type': 'application/json',
|
|
77
|
+
* })
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* return Response.json(await response.json());
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* @param requestHeaders - Next.js headers object or NextRequest from next/server
|
|
84
|
+
* @param additionalHeaders - Optional additional headers to include
|
|
85
|
+
* @returns Headers object with recording ID if present
|
|
86
|
+
*/
|
|
87
|
+
declare function createHeadersWithRecordingId(requestHeaders: NextJSRequest | Headers, additionalHeaders?: Record<string, string>): Record<string, string>;
|
|
88
|
+
|
|
89
|
+
export { type NextJSRequest, type NextJSResponse, RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
declare const RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal type for Next.js Request - compatible with next/server's NextRequest
|
|
5
|
+
* We define this locally to avoid requiring Next.js as a dependency
|
|
6
|
+
*/
|
|
7
|
+
interface NextJSRequest {
|
|
8
|
+
headers: Headers;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal type for Next.js Response - compatible with next/server's NextResponse
|
|
12
|
+
* We define this locally to avoid requiring Next.js as a dependency
|
|
13
|
+
*/
|
|
14
|
+
interface NextJSResponse {
|
|
15
|
+
headers: Headers;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Next.js middleware helper for forwarding test proxy recording headers
|
|
19
|
+
* Automatically forwards the recording ID header from incoming requests to the proxy
|
|
20
|
+
* Only runs in non-production environments or when TEST_PROXY_RECORDER_ENABLED is set
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // middleware.ts
|
|
24
|
+
* import { NextResponse } from 'next/server';
|
|
25
|
+
* import type { NextRequest } from 'next/server';
|
|
26
|
+
* import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
|
|
27
|
+
*
|
|
28
|
+
* export function middleware(request: NextRequest) {
|
|
29
|
+
* const response = NextResponse.next();
|
|
30
|
+
* // Only forwards headers in test/dev environments
|
|
31
|
+
* setNextProxyHeaders(request, response);
|
|
32
|
+
* return response;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @param request - Next.js request object (NextRequest from next/server)
|
|
36
|
+
* @param response - Next.js response object (NextResponse from next/server)
|
|
37
|
+
*/
|
|
38
|
+
declare function setNextProxyHeaders(request: NextJSRequest, response: NextJSResponse): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get the recording ID from the request if present
|
|
41
|
+
* Useful for manually adding the header to fetch requests in Next.js
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // In your API route or server component
|
|
45
|
+
* import { getRecordingId } from 'test-proxy-recorder/nextjs';
|
|
46
|
+
* import { headers } from 'next/headers';
|
|
47
|
+
*
|
|
48
|
+
* export async function GET() {
|
|
49
|
+
* const recordingId = getRecordingId(headers());
|
|
50
|
+
*
|
|
51
|
+
* const response = await fetch('http://localhost:8100/api/data', {
|
|
52
|
+
* headers: {
|
|
53
|
+
* ...(recordingId && { 'x-test-rcrd-id': recordingId })
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* return Response.json(await response.json());
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @param requestHeaders - Next.js headers object or NextRequest from next/server
|
|
61
|
+
* @returns The recording ID if present, null otherwise
|
|
62
|
+
*/
|
|
63
|
+
declare function getRecordingId(requestHeaders: NextJSRequest | Headers): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Create headers object with recording ID for fetch requests
|
|
66
|
+
* Use this helper when making fetch requests in Next.js to forward the recording ID
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // In your API route or server component
|
|
70
|
+
* import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
|
|
71
|
+
* import { headers } from 'next/headers';
|
|
72
|
+
*
|
|
73
|
+
* export async function GET() {
|
|
74
|
+
* const response = await fetch('http://localhost:8100/api/data', {
|
|
75
|
+
* headers: createHeadersWithRecordingId(headers(), {
|
|
76
|
+
* 'Content-Type': 'application/json',
|
|
77
|
+
* })
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* return Response.json(await response.json());
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* @param requestHeaders - Next.js headers object or NextRequest from next/server
|
|
84
|
+
* @param additionalHeaders - Optional additional headers to include
|
|
85
|
+
* @returns Headers object with recording ID if present
|
|
86
|
+
*/
|
|
87
|
+
declare function createHeadersWithRecordingId(requestHeaders: NextJSRequest | Headers, additionalHeaders?: Record<string, string>): Record<string, string>;
|
|
88
|
+
|
|
89
|
+
export { type NextJSRequest, type NextJSResponse, RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
3
|
+
|
|
4
|
+
// src/nextjs/middleware.ts
|
|
5
|
+
function isRecorderEnabled() {
|
|
6
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
7
|
+
const isExplicitlyEnabled = process.env.TEST_PROXY_RECORDER_ENABLED === "true" || Number.parseInt(process.env.TEST_PROXY_RECORDER_ENABLED || "") === 1;
|
|
8
|
+
return !isProduction || isExplicitlyEnabled;
|
|
9
|
+
}
|
|
10
|
+
function setNextProxyHeaders(request, response) {
|
|
11
|
+
if (!isRecorderEnabled()) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const recordingId = request.headers.get(RECORDING_ID_HEADER);
|
|
15
|
+
if (recordingId) {
|
|
16
|
+
response.headers.set(RECORDING_ID_HEADER, recordingId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function getRecordingId(requestHeaders) {
|
|
20
|
+
if (requestHeaders instanceof Headers) {
|
|
21
|
+
return requestHeaders.get(RECORDING_ID_HEADER);
|
|
22
|
+
}
|
|
23
|
+
return requestHeaders.headers.get(RECORDING_ID_HEADER);
|
|
24
|
+
}
|
|
25
|
+
function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
|
|
26
|
+
if (!isRecorderEnabled()) {
|
|
27
|
+
return additionalHeaders;
|
|
28
|
+
}
|
|
29
|
+
const recordingId = getRecordingId(requestHeaders);
|
|
30
|
+
return {
|
|
31
|
+
...additionalHeaders,
|
|
32
|
+
...recordingId && { [RECORDING_ID_HEADER]: recordingId }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders };
|
|
37
|
+
//# sourceMappingURL=index.mjs.map
|
|
38
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
5
|
+
|
|
3
6
|
// src/types.ts
|
|
4
7
|
var Modes = {
|
|
5
8
|
transparent: "transparent",
|
|
@@ -86,22 +89,36 @@ async function stopProxy(testInfo) {
|
|
|
86
89
|
}
|
|
87
90
|
var playwrightProxy = {
|
|
88
91
|
/**
|
|
89
|
-
* Setup before test - sets the proxy mode
|
|
92
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
93
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
94
|
+
* @param page - Playwright page object
|
|
90
95
|
* @param testInfo - Playwright test info object
|
|
91
96
|
* @param mode - The proxy mode to use for this test
|
|
97
|
+
* @param timeout - Optional timeout in milliseconds
|
|
92
98
|
*/
|
|
93
|
-
async before(testInfo, mode) {
|
|
99
|
+
async before(page, testInfo, mode, timeout) {
|
|
94
100
|
const sessionId = generateSessionId(testInfo);
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
await page.setExtraHTTPHeaders({
|
|
102
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
103
|
+
});
|
|
104
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
105
|
+
page.on("close", async () => {
|
|
106
|
+
try {
|
|
107
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
108
|
+
console.log(
|
|
109
|
+
`[Cleanup] Switched to transparent mode for session: ${sessionId}`
|
|
110
|
+
);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
97
115
|
},
|
|
98
116
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
117
|
+
* Global teardown - switches proxy to transparent mode
|
|
118
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
101
119
|
*/
|
|
102
|
-
async
|
|
103
|
-
|
|
104
|
-
await setProxyMode(Modes.transparent, sessionId);
|
|
120
|
+
async teardown() {
|
|
121
|
+
await setProxyMode(Modes.transparent);
|
|
105
122
|
}
|
|
106
123
|
};
|
|
107
124
|
|
|
@@ -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 { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CVuiglPk.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 { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CVuiglPk.js';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
3
|
+
|
|
1
4
|
// src/types.ts
|
|
2
5
|
var Modes = {
|
|
3
6
|
transparent: "transparent",
|
|
@@ -84,22 +87,36 @@ async function stopProxy(testInfo) {
|
|
|
84
87
|
}
|
|
85
88
|
var playwrightProxy = {
|
|
86
89
|
/**
|
|
87
|
-
* Setup before test - sets the proxy mode
|
|
90
|
+
* Setup before test - sets the proxy mode and configures page with custom header
|
|
91
|
+
* Automatically sets up page.on('close') handler for cleanup
|
|
92
|
+
* @param page - Playwright page object
|
|
88
93
|
* @param testInfo - Playwright test info object
|
|
89
94
|
* @param mode - The proxy mode to use for this test
|
|
95
|
+
* @param timeout - Optional timeout in milliseconds
|
|
90
96
|
*/
|
|
91
|
-
async before(testInfo, mode) {
|
|
97
|
+
async before(page, testInfo, mode, timeout) {
|
|
92
98
|
const sessionId = generateSessionId(testInfo);
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
await page.setExtraHTTPHeaders({
|
|
100
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
101
|
+
});
|
|
102
|
+
await setProxyMode(mode, sessionId, timeout);
|
|
103
|
+
page.on("close", async () => {
|
|
104
|
+
try {
|
|
105
|
+
await setProxyMode(Modes.replay, sessionId);
|
|
106
|
+
console.log(
|
|
107
|
+
`[Cleanup] Switched to transparent mode for session: ${sessionId}`
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
95
113
|
},
|
|
96
114
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
115
|
+
* Global teardown - switches proxy to transparent mode
|
|
116
|
+
* Use this in Playwright's globalTeardown to ensure clean state
|
|
99
117
|
*/
|
|
100
|
-
async
|
|
101
|
-
|
|
102
|
-
await setProxyMode(Modes.transparent, sessionId);
|
|
118
|
+
async teardown() {
|
|
119
|
+
await setProxyMode(Modes.transparent);
|
|
103
120
|
}
|
|
104
121
|
};
|
|
105
122
|
|
package/dist/proxy.js
CHANGED
|
@@ -50,6 +50,7 @@ var HTTP_STATUS_OK = 200;
|
|
|
50
50
|
var HTTP_STATUS_BAD_REQUEST = 400;
|
|
51
51
|
var HTTP_STATUS_NOT_FOUND = 404;
|
|
52
52
|
var CONTROL_ENDPOINT = "/__control";
|
|
53
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
53
54
|
|
|
54
55
|
// src/types.ts
|
|
55
56
|
var Modes = {
|
|
@@ -222,7 +223,7 @@ var ProxyServer = class {
|
|
|
222
223
|
return {
|
|
223
224
|
"access-control-allow-origin": origin || "*",
|
|
224
225
|
"access-control-allow-credentials": "true",
|
|
225
|
-
"access-control-allow-headers": req.headers["access-control-request-headers"] ||
|
|
226
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
|
|
226
227
|
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
227
228
|
"access-control-expose-headers": "*"
|
|
228
229
|
};
|
|
@@ -236,9 +237,22 @@ var ProxyServer = class {
|
|
|
236
237
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
237
238
|
return target;
|
|
238
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Extract recording ID from custom HTTP header
|
|
242
|
+
* Used for concurrent replay session routing, especially with Next.js
|
|
243
|
+
* @param req The incoming HTTP request
|
|
244
|
+
* @returns The recording ID from header, or null if not found
|
|
245
|
+
*/
|
|
246
|
+
getRecordingIdFromHeader(req) {
|
|
247
|
+
const headerValue = req.headers[RECORDING_ID_HEADER];
|
|
248
|
+
if (!headerValue) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
252
|
+
}
|
|
239
253
|
/**
|
|
240
254
|
* Extract recording ID from request cookie
|
|
241
|
-
* Used for concurrent replay session routing
|
|
255
|
+
* Used for concurrent replay session routing (fallback method)
|
|
242
256
|
* @param req The incoming HTTP request
|
|
243
257
|
* @returns The recording ID from cookie, or null if not found
|
|
244
258
|
*/
|
|
@@ -250,6 +264,14 @@ var ProxyServer = class {
|
|
|
250
264
|
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
251
265
|
return match ? decodeURIComponent(match[1]) : null;
|
|
252
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Extract recording ID from request using custom header (preferred) or cookie (fallback)
|
|
269
|
+
* @param req The incoming HTTP request
|
|
270
|
+
* @returns The recording ID, or null if not found
|
|
271
|
+
*/
|
|
272
|
+
getRecordingIdFromRequest(req) {
|
|
273
|
+
return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
|
|
274
|
+
}
|
|
253
275
|
/**
|
|
254
276
|
* Get or create a replay session state for a given recording ID
|
|
255
277
|
* @param recordingId The recording ID to get/create session for
|
|
@@ -527,7 +549,7 @@ var ProxyServer = class {
|
|
|
527
549
|
return true;
|
|
528
550
|
}
|
|
529
551
|
async handleReplayRequest(req, res) {
|
|
530
|
-
const recordingId = this.
|
|
552
|
+
const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
|
|
531
553
|
if (!recordingId) {
|
|
532
554
|
const corsHeaders = this.getCorsHeaders(req);
|
|
533
555
|
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -551,7 +573,11 @@ var ProxyServer = class {
|
|
|
551
573
|
}
|
|
552
574
|
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
553
575
|
const host = req.headers.host || "unknown";
|
|
554
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) =>
|
|
576
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
577
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
578
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
579
|
+
return aSeq - bSeq;
|
|
580
|
+
});
|
|
555
581
|
if (recordsWithKey.length === 0) {
|
|
556
582
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
557
583
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -574,7 +600,7 @@ var ProxyServer = class {
|
|
|
574
600
|
}
|
|
575
601
|
const requestCount = servedForThisKey.size + 1;
|
|
576
602
|
console.log(
|
|
577
|
-
`[
|
|
603
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
578
604
|
);
|
|
579
605
|
let record;
|
|
580
606
|
for (const rec of recordsWithKey) {
|
|
@@ -591,7 +617,7 @@ var ProxyServer = class {
|
|
|
591
617
|
}
|
|
592
618
|
servedForThisKey.add(record.recordingId);
|
|
593
619
|
console.log(
|
|
594
|
-
`[
|
|
620
|
+
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
595
621
|
);
|
|
596
622
|
if (!record.response) {
|
|
597
623
|
throw new Error(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"require": "./dist/playwright/index.cjs",
|
|
16
16
|
"types": "./dist/playwright/index.d.ts",
|
|
17
17
|
"default": "./dist/playwright/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./nextjs": {
|
|
20
|
+
"require": "./dist/nextjs/index.cjs",
|
|
21
|
+
"types": "./dist/nextjs/index.d.ts",
|
|
22
|
+
"default": "./dist/nextjs/index.mjs"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"bin": {
|