test-proxy-recorder 0.2.0 → 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 +130 -29
- package/dist/{index-nmNRt1WE.d.cts → index-CVuiglPk.d.cts} +6 -10
- package/dist/{index-nmNRt1WE.d.ts → index-CVuiglPk.d.ts} +6 -10
- package/dist/index.cjs +78 -16
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.mjs +75 -17
- 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 +20 -11
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.mjs +20 -11
- package/dist/proxy.js +25 -3
- 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)
|
|
@@ -128,14 +129,10 @@ Create `e2e/example.spec.ts`:
|
|
|
128
129
|
import { test, expect } from '@playwright/test';
|
|
129
130
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
130
131
|
|
|
131
|
-
// Setup afterEach hook to reset proxy after each test
|
|
132
|
-
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
133
|
-
await playwrightProxy.after(testInfo);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
132
|
test('example test with proxy', async ({ page }, testInfo) => {
|
|
137
133
|
// Set proxy mode: 'record' to capture, 'replay' to use recordings
|
|
138
|
-
|
|
134
|
+
// This automatically sets up page.on('close') for cleanup
|
|
135
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
139
136
|
|
|
140
137
|
await page.goto('/');
|
|
141
138
|
await expect(page.getByText('Welcome')).toBeVisible();
|
|
@@ -147,13 +144,13 @@ test('example test with proxy', async ({ page }, testInfo) => {
|
|
|
147
144
|
**First run (record mode)**:
|
|
148
145
|
|
|
149
146
|
```typescript
|
|
150
|
-
await playwrightProxy.before(testInfo, 'record');
|
|
147
|
+
await playwrightProxy.before(page, testInfo, 'record');
|
|
151
148
|
```
|
|
152
149
|
|
|
153
150
|
**Subsequent runs (replay mode)**:
|
|
154
151
|
|
|
155
152
|
```typescript
|
|
156
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
153
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
157
154
|
```
|
|
158
155
|
|
|
159
156
|
## CLI Usage
|
|
@@ -186,24 +183,26 @@ test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
|
|
|
186
183
|
|
|
187
184
|
## Playwright Integration
|
|
188
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
|
+
|
|
189
192
|
### Basic Test Structure
|
|
190
193
|
|
|
191
|
-
Every test
|
|
194
|
+
Every test using the proxy should follow this pattern:
|
|
192
195
|
|
|
193
196
|
```typescript
|
|
194
197
|
import { test } from '@playwright/test';
|
|
195
198
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
196
199
|
|
|
197
|
-
// Setup afterEach hook once per test file
|
|
198
|
-
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
199
|
-
await playwrightProxy.after(testInfo);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
200
|
test('test name', async ({ page }, testInfo) => {
|
|
203
|
-
//
|
|
204
|
-
|
|
201
|
+
// Set mode BEFORE test actions
|
|
202
|
+
// This automatically sets the recording ID header and cleanup handler
|
|
203
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
205
204
|
|
|
206
|
-
//
|
|
205
|
+
// Test code
|
|
207
206
|
await page.goto('/page');
|
|
208
207
|
// Test assertions...
|
|
209
208
|
});
|
|
@@ -215,14 +214,9 @@ test('test name', async ({ page }, testInfo) => {
|
|
|
215
214
|
import { test } from '@playwright/test';
|
|
216
215
|
import { playwrightProxy } from 'test-proxy-recorder';
|
|
217
216
|
|
|
218
|
-
// Setup afterEach hook to automatically cleanup after each test
|
|
219
|
-
test.afterEach(async ({ page: _page }, testInfo) => {
|
|
220
|
-
await playwrightProxy.after(testInfo);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
217
|
// Recording mode - captures API responses
|
|
224
218
|
test('create user', async ({ page }, testInfo) => {
|
|
225
|
-
await playwrightProxy.before(testInfo, 'record');
|
|
219
|
+
await playwrightProxy.before(page, testInfo, 'record');
|
|
226
220
|
|
|
227
221
|
await page.goto('/users/new');
|
|
228
222
|
await page.fill('[name="username"]', 'testuser');
|
|
@@ -231,7 +225,7 @@ test('create user', async ({ page }, testInfo) => {
|
|
|
231
225
|
|
|
232
226
|
// Replay mode - uses recorded responses
|
|
233
227
|
test('create user', async ({ page }, testInfo) => {
|
|
234
|
-
await playwrightProxy.before(testInfo, 'replay');
|
|
228
|
+
await playwrightProxy.before(page, testInfo, 'replay');
|
|
235
229
|
|
|
236
230
|
await page.goto('/users/new');
|
|
237
231
|
await page.fill('[name="username"]', 'testuser');
|
|
@@ -274,6 +268,78 @@ export default defineConfig({
|
|
|
274
268
|
});
|
|
275
269
|
```
|
|
276
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
|
+
|
|
277
343
|
## Control Endpoint
|
|
278
344
|
|
|
279
345
|
The proxy exposes a control endpoint at `/__control` for programmatic mode switching.
|
|
@@ -410,20 +476,20 @@ class ProxyServer {
|
|
|
410
476
|
### Playwright Integration
|
|
411
477
|
|
|
412
478
|
```typescript
|
|
413
|
-
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';
|
|
414
481
|
|
|
415
482
|
// Main helper for Playwright tests
|
|
416
483
|
const playwrightProxy = {
|
|
417
|
-
// 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
|
|
418
486
|
async before(
|
|
487
|
+
page: Page,
|
|
419
488
|
testInfo: TestInfo,
|
|
420
489
|
mode: 'record' | 'replay' | 'transparent',
|
|
421
490
|
timeout?: number
|
|
422
491
|
): Promise<void>;
|
|
423
492
|
|
|
424
|
-
// Reset replay session and return to transparent mode after test
|
|
425
|
-
// Resets sequence counters to ensure next replay starts fresh
|
|
426
|
-
async after(testInfo: TestInfo): Promise<void>;
|
|
427
493
|
|
|
428
494
|
// Global teardown - switches proxy to transparent mode
|
|
429
495
|
// Use in Playwright's globalTeardown configuration
|
|
@@ -436,6 +502,41 @@ async function setProxyMode(
|
|
|
436
502
|
id?: string,
|
|
437
503
|
timeout?: number
|
|
438
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>;
|
|
439
540
|
```
|
|
440
541
|
|
|
441
542
|
### Control Endpoint
|
|
@@ -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,22 +82,18 @@ 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
|
|
92
94
|
* @param timeout - Optional timeout in milliseconds
|
|
93
95
|
*/
|
|
94
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
95
|
-
/**
|
|
96
|
-
* Cleanup after test - resets replay session by re-entering replay mode
|
|
97
|
-
* switchToReplayMode automatically clears sequence counters
|
|
98
|
-
* @param testInfo - Playwright test info object
|
|
99
|
-
*/
|
|
100
|
-
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
96
|
+
before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
101
97
|
/**
|
|
102
98
|
* Global teardown - switches proxy to transparent mode
|
|
103
99
|
* Use this in Playwright's globalTeardown to ensure clean state
|
|
@@ -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,22 +82,18 @@ 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
|
|
92
94
|
* @param timeout - Optional timeout in milliseconds
|
|
93
95
|
*/
|
|
94
|
-
before(testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
95
|
-
/**
|
|
96
|
-
* Cleanup after test - resets replay session by re-entering replay mode
|
|
97
|
-
* switchToReplayMode automatically clears sequence counters
|
|
98
|
-
* @param testInfo - Playwright test info object
|
|
99
|
-
*/
|
|
100
|
-
after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
96
|
+
before(page: Page, testInfo: PlaywrightTestInfo, mode: Mode, timeout?: number): Promise<void>;
|
|
101
97
|
/**
|
|
102
98
|
* Global teardown - switches proxy to transparent mode
|
|
103
99
|
* Use this in Playwright's globalTeardown to ensure clean state
|
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, {
|
|
@@ -954,23 +974,29 @@ async function stopProxy(testInfo) {
|
|
|
954
974
|
}
|
|
955
975
|
var playwrightProxy = {
|
|
956
976
|
/**
|
|
957
|
-
* 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
|
|
958
980
|
* @param testInfo - Playwright test info object
|
|
959
981
|
* @param mode - The proxy mode to use for this test
|
|
960
982
|
* @param timeout - Optional timeout in milliseconds
|
|
961
983
|
*/
|
|
962
|
-
async before(testInfo, mode, timeout) {
|
|
984
|
+
async before(page, testInfo, mode, timeout) {
|
|
963
985
|
const sessionId = generateSessionId(testInfo);
|
|
986
|
+
await page.setExtraHTTPHeaders({
|
|
987
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
988
|
+
});
|
|
964
989
|
await setProxyMode(mode, sessionId, timeout);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
+
});
|
|
974
1000
|
},
|
|
975
1001
|
/**
|
|
976
1002
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -981,9 +1007,45 @@ var playwrightProxy = {
|
|
|
981
1007
|
}
|
|
982
1008
|
};
|
|
983
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
|
+
|
|
984
1042
|
exports.ProxyServer = ProxyServer;
|
|
1043
|
+
exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
|
|
1044
|
+
exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
|
|
985
1045
|
exports.generateSessionId = generateSessionId;
|
|
1046
|
+
exports.getRecordingId = getRecordingId;
|
|
986
1047
|
exports.playwrightProxy = playwrightProxy;
|
|
1048
|
+
exports.setNextProxyHeaders = setNextProxyHeaders;
|
|
987
1049
|
exports.setProxyMode = setProxyMode;
|
|
988
1050
|
exports.startRecording = startRecording;
|
|
989
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, {
|
|
@@ -942,23 +962,29 @@ async function stopProxy(testInfo) {
|
|
|
942
962
|
}
|
|
943
963
|
var playwrightProxy = {
|
|
944
964
|
/**
|
|
945
|
-
* 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
|
|
946
968
|
* @param testInfo - Playwright test info object
|
|
947
969
|
* @param mode - The proxy mode to use for this test
|
|
948
970
|
* @param timeout - Optional timeout in milliseconds
|
|
949
971
|
*/
|
|
950
|
-
async before(testInfo, mode, timeout) {
|
|
972
|
+
async before(page, testInfo, mode, timeout) {
|
|
951
973
|
const sessionId = generateSessionId(testInfo);
|
|
974
|
+
await page.setExtraHTTPHeaders({
|
|
975
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
976
|
+
});
|
|
952
977
|
await setProxyMode(mode, sessionId, timeout);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
+
});
|
|
962
988
|
},
|
|
963
989
|
/**
|
|
964
990
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -969,6 +995,38 @@ var playwrightProxy = {
|
|
|
969
995
|
}
|
|
970
996
|
};
|
|
971
997
|
|
|
972
|
-
|
|
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 };
|
|
973
1031
|
//# sourceMappingURL=index.mjs.map
|
|
974
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,23 +89,29 @@ 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
|
|
92
97
|
* @param timeout - Optional timeout in milliseconds
|
|
93
98
|
*/
|
|
94
|
-
async before(testInfo, mode, timeout) {
|
|
99
|
+
async before(page, testInfo, mode, timeout) {
|
|
95
100
|
const sessionId = generateSessionId(testInfo);
|
|
101
|
+
await page.setExtraHTTPHeaders({
|
|
102
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
103
|
+
});
|
|
96
104
|
await setProxyMode(mode, sessionId, timeout);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
});
|
|
106
115
|
},
|
|
107
116
|
/**
|
|
108
117
|
* Global teardown - switches proxy to transparent mode
|
|
@@ -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,23 +87,29 @@ 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
|
|
90
95
|
* @param timeout - Optional timeout in milliseconds
|
|
91
96
|
*/
|
|
92
|
-
async before(testInfo, mode, timeout) {
|
|
97
|
+
async before(page, testInfo, mode, timeout) {
|
|
93
98
|
const sessionId = generateSessionId(testInfo);
|
|
99
|
+
await page.setExtraHTTPHeaders({
|
|
100
|
+
[RECORDING_ID_HEADER]: sessionId
|
|
101
|
+
});
|
|
94
102
|
await setProxyMode(mode, sessionId, timeout);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
});
|
|
104
113
|
},
|
|
105
114
|
/**
|
|
106
115
|
* Global teardown - switches proxy to transparent mode
|
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, {
|
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": {
|