Skip to content

Commit 830c589

Browse files
committed
feat: add extendWithApiSpy and wrapWithApiSpy for custom test integration
1 parent dc89b73 commit 830c589

7 files changed

Lines changed: 656 additions & 5 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Custom Test Integration
2+
3+
This guide explains how to integrate `playwright-api-spy` with custom test objects, such as those extended by other libraries like `playwright-relay`, `playwright-bdd`, or your own custom fixtures.
4+
5+
## The Problem
6+
7+
By default, `playwright-api-spy` intercepts the built-in `request` fixture from Playwright. However, when you extend `test` through other libraries, the `request` fixture might not be properly intercepted:
8+
9+
```typescript
10+
// playwright-relay exports its own test
11+
import { test as relayTest } from 'playwright-relay';
12+
13+
// Extending with your fixtures
14+
export const test = relayTest.extend<MyFixtures>({
15+
rawApi: async ({ request }, use) => {
16+
await use(request); // This request is NOT wrapped by api-spy!
17+
},
18+
api: async ({ rawApi }, use) => {
19+
await use(new MyApi(rawApi));
20+
},
21+
});
22+
```
23+
24+
**Result:** Requests made through `api` are not logged by `playwright-api-spy`.
25+
26+
## Solutions
27+
28+
### Solution 1: `extendWithApiSpy(baseTest)`
29+
30+
The easiest approach - extend any test object with API Spy capabilities:
31+
32+
```typescript
33+
import { test as relayTest } from 'playwright-relay';
34+
import { extendWithApiSpy } from 'playwright-api-spy';
35+
36+
// Step 1: Extend relay with api-spy
37+
const testWithSpy = extendWithApiSpy(relayTest);
38+
39+
// Step 2: Extend with your fixtures
40+
export const test = testWithSpy.extend<MyFixtures>({
41+
settings: async ({}, use) => {
42+
await use(getSettings());
43+
},
44+
45+
// rawApi now uses the wrapped request automatically!
46+
rawApi: async ({ request }, use) => {
47+
await use(request);
48+
},
49+
50+
api: async ({ rawApi }, use) => {
51+
await use(new MyApi(rawApi));
52+
},
53+
});
54+
55+
export { expect } from '@playwright/test';
56+
```
57+
58+
This automatically:
59+
60+
- Creates an `apiSpy` fixture
61+
- Wraps the `request` fixture with API Spy interception
62+
63+
### Solution 2: `extendWithApiSpyFixture(baseTest)` + `wrapWithApiSpy()`
64+
65+
For more control over which contexts are wrapped:
66+
67+
```typescript
68+
import { test as relayTest } from 'playwright-relay';
69+
import { extendWithApiSpyFixture, wrapWithApiSpy } from 'playwright-api-spy';
70+
71+
// Add only the apiSpy fixture (without auto-wrapping request)
72+
const testWithSpy = extendWithApiSpyFixture(relayTest);
73+
74+
export const test = testWithSpy.extend<MyFixtures>({
75+
// Manually wrap specific fixtures
76+
rawApi: async ({ request, apiSpy }, use) => {
77+
const wrapped = wrapWithApiSpy(request, apiSpy);
78+
await use(wrapped);
79+
},
80+
81+
// This one is NOT wrapped
82+
internalApi: async ({ request }, use) => {
83+
await use(request);
84+
},
85+
86+
api: async ({ rawApi }, use) => {
87+
await use(new MyApi(rawApi));
88+
},
89+
});
90+
```
91+
92+
### Solution 3: Manual Setup with `ApiSpyInstance`
93+
94+
For complete control, create your own spy instance:
95+
96+
```typescript
97+
import { test as relayTest } from 'playwright-relay';
98+
import {
99+
ApiSpyInstance,
100+
getApiSpyConfig,
101+
wrapWithApiSpy,
102+
globalApiSpyStore
103+
} from 'playwright-api-spy';
104+
105+
export const test = relayTest.extend<MyFixtures>({
106+
apiSpy: async ({}, use, testInfo) => {
107+
// Create spy with config from withApiSpy()
108+
const config = getApiSpyConfig();
109+
const spy = new ApiSpyInstance(config);
110+
111+
spy.setTestInfo({
112+
title: testInfo.title,
113+
file: testInfo.file,
114+
line: testInfo.line,
115+
});
116+
117+
await use(spy);
118+
119+
// Add entries to global store for report generation
120+
const entries = spy.getEntriesForReport();
121+
globalApiSpyStore.addEntries(entries);
122+
123+
// Optionally attach to Playwright report
124+
if (config.attachToPlaywrightReport && entries.length > 0) {
125+
await testInfo.attach('api-spy-requests', {
126+
body: Buffer.from(JSON.stringify(entries, null, 2)),
127+
contentType: 'application/json',
128+
});
129+
}
130+
},
131+
132+
rawApi: async ({ request, apiSpy }, use) => {
133+
const wrapped = wrapWithApiSpy(request, apiSpy);
134+
await use(wrapped);
135+
},
136+
});
137+
```
138+
139+
## Complete Example with playwright-relay
140+
141+
Here's a full example integrating with `playwright-relay`:
142+
143+
### `src/fixtures/engine.fixtures.ts`
144+
145+
```typescript
146+
import { test as relayTest } from 'playwright-relay';
147+
import { extendWithApiSpy } from 'playwright-api-spy';
148+
import { EngineApi } from './engine-api';
149+
import { getSettings } from './settings';
150+
151+
// Types
152+
export interface EngineFixtures {
153+
settings: Settings;
154+
rawApi: APIRequestContext;
155+
api: EngineApi;
156+
}
157+
158+
// Step 1: Extend relay with api-spy
159+
const baseTest = extendWithApiSpy(relayTest);
160+
161+
// Step 2: Extend with your fixtures
162+
export const test = baseTest.extend<EngineFixtures>({
163+
settings: async ({}, use) => {
164+
await use(getSettings());
165+
},
166+
167+
rawApi: async ({ request }, use) => {
168+
await use(request); // Automatically wrapped!
169+
},
170+
171+
api: async ({ rawApi }, use) => {
172+
await use(new EngineApi(rawApi));
173+
},
174+
});
175+
176+
export { expect } from '@playwright/test';
177+
```
178+
179+
### `playwright.config.ts`
180+
181+
```typescript
182+
import { defineConfig } from '@playwright/test';
183+
import { withRelay } from 'playwright-relay';
184+
import { withApiSpy } from 'playwright-api-spy';
185+
186+
export default defineConfig(withApiSpy(withRelay({
187+
testDir: 'tests',
188+
use: {
189+
baseURL: 'https://api.example.com',
190+
},
191+
}, {
192+
persistCache: true,
193+
}), {
194+
console: true,
195+
verbosity: 'normal',
196+
htmlReport: { enabled: true },
197+
jsonReport: { enabled: true },
198+
}));
199+
```
200+
201+
### `tests/engine.spec.ts`
202+
203+
```typescript
204+
import { test, expect } from '../src/fixtures/engine.fixtures';
205+
206+
test('should create user', async ({ api, apiSpy }) => {
207+
// All requests through api are now captured
208+
const user = await api.createUser({ name: 'John' });
209+
210+
expect(user.id).toBeDefined();
211+
212+
// You can also access captured requests
213+
expect(apiSpy.lastRequest?.method).toBe('POST');
214+
expect(apiSpy.lastResponse?.status).toBe(201);
215+
});
216+
```
217+
218+
## API Reference
219+
220+
### `extendWithApiSpy(baseTest)`
221+
222+
Extends a test object with both `apiSpy` fixture and wrapped `request` fixture.
223+
224+
```typescript
225+
function extendWithApiSpy<TestArgs, WorkerArgs>(
226+
baseTest: TestType<TestArgs, WorkerArgs>
227+
): TestType<TestArgs & ApiSpyFixtures, WorkerArgs>
228+
```
229+
230+
### `extendWithApiSpyFixture(baseTest)`
231+
232+
Extends a test object with only the `apiSpy` fixture (no auto-wrapping of request).
233+
234+
```typescript
235+
function extendWithApiSpyFixture<TestArgs, WorkerArgs>(
236+
baseTest: TestType<TestArgs, WorkerArgs>
237+
): TestType<TestArgs & ApiSpyFixtures, WorkerArgs>
238+
```
239+
240+
### `wrapWithApiSpy(context, spy)`
241+
242+
Wraps an `APIRequestContext` with API Spy interception.
243+
244+
```typescript
245+
function wrapWithApiSpy(
246+
apiContext: APIRequestContext,
247+
spy: ApiSpy | ApiSpyInstance
248+
): APIRequestContext
249+
```
250+
251+
### `getApiSpyConfig()`
252+
253+
Gets the current API Spy configuration from the global store (set via `withApiSpy()`).
254+
255+
```typescript
256+
function getApiSpyConfig(): Required<ApiSpyConfig>
257+
```
258+
259+
### `ApiSpyInstance`
260+
261+
The main class for capturing requests. Can be instantiated manually for custom setups.
262+
263+
```typescript
264+
class ApiSpyInstance implements ApiSpy {
265+
constructor(config?: ApiSpyConfig);
266+
setTestInfo(info: { title: string; file: string; line?: number }): void;
267+
captureRequest(method: string, url: string, options?: {...}): Promise<CapturedRequest | null>;
268+
captureResponse(request: CapturedRequest, response: {...}, duration: number): Promise<void>;
269+
captureError(request: CapturedRequest, error: Error): Promise<void>;
270+
getEntriesForReport(): CapturedEntry[];
271+
// ... and more
272+
}
273+
```
274+
275+
### `globalApiSpyStore`
276+
277+
Global store for configuration and captured entries. Used internally by the reporter.
278+
279+
```typescript
280+
const globalApiSpyStore: {
281+
config: Required<ApiSpyConfig>;
282+
setConfig(config: Required<ApiSpyConfig>): void;
283+
addEntries(entries: CapturedEntry[]): void;
284+
getAllEntries(): CapturedEntry[];
285+
reset(): void;
286+
clear(): void;
287+
}
288+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ nav:
2828
- Getting Started:
2929
- Installation: getting-started/installation.md
3030
- Quick Start: getting-started/quick-start.md
31+
- Custom Test Integration: getting-started/custom-integration.md
3132
- Configuration:
3233
- Options: configuration/options.md
3334
- Filtering: configuration/filtering.md

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "playwright-api-spy",
3-
"version": "1.0.0-beta.1",
3+
"version": "1.0.0-beta.2",
44
"description": "A Playwright plugin for capturing and logging API requests/responses with beautiful HTML and JSON reports",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,28 @@ export function withApiSpy<T extends PlaywrightTestConfig>(
126126
export function defineApiSpyConfig(config: ApiSpyConfig): Required<ApiSpyConfig> {
127127
return mergeApiSpyConfig(config);
128128
}
129+
130+
/**
131+
* Gets the current API Spy configuration from the global store.
132+
*
133+
* This is useful when creating custom fixtures that need access to the config
134+
* set via `withApiSpy()`.
135+
*
136+
* @returns The current API Spy configuration
137+
*
138+
* @example
139+
* ```typescript
140+
* import { getApiSpyConfig, ApiSpyInstance } from 'playwright-api-spy';
141+
*
142+
* export const test = baseTest.extend({
143+
* customApiSpy: async ({}, use, testInfo) => {
144+
* const config = getApiSpyConfig();
145+
* const spy = new ApiSpyInstance(config);
146+
* // ...
147+
* },
148+
* });
149+
* ```
150+
*/
151+
export function getApiSpyConfig(): Required<ApiSpyConfig> {
152+
return globalApiSpyStore.config;
153+
}

0 commit comments

Comments
 (0)