Skip to content

Commit 5cd4abb

Browse files
committed
feat: improve Playwright report attachments
1 parent 85924b0 commit 5cd4abb

4 files changed

Lines changed: 143 additions & 49 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "playwright-api-spy",
3-
"version": "1.0.0-beta.3",
3+
"version": "1.0.0",
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",
@@ -70,4 +70,4 @@
7070
"require": "./dist/reporter.js"
7171
}
7272
}
73-
}
73+
}

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default defineConfig(withApiSpy({
66
use: {
77
baseURL: 'https://jsonplaceholder.typicode.com',
88
},
9-
reporter: [['list'], ['./dist/reporter.js']],
9+
reporter: [['list'], ['html'], ['./dist/reporter.js']],
1010
}, {
1111
console: false,
1212
verbosity: 'normal',

src/extend.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,31 @@ export function extendWithApiSpy<
6868

6969
// Attach to Playwright report if enabled
7070
if (config.attachToPlaywrightReport && entries.length > 0) {
71-
const attachmentContent = JSON.stringify(entries, null, 2);
72-
await testInfo.attach('api-spy-requests', {
73-
body: Buffer.from(attachmentContent),
74-
contentType: 'application/json',
75-
});
71+
for (const entry of entries) {
72+
const { request, response, error } = entry;
73+
74+
// Attach request
75+
await testInfo.attach(`Request ${request.method} ${request.url}`, {
76+
body: Buffer.from(JSON.stringify(request, null, 2)),
77+
contentType: 'application/json',
78+
});
79+
80+
// Attach response if exists
81+
if (response) {
82+
await testInfo.attach(`Response ${response.status} ${request.method} ${request.url}`, {
83+
body: Buffer.from(JSON.stringify(response, null, 2)),
84+
contentType: 'application/json',
85+
});
86+
}
87+
88+
// Attach error if exists
89+
if (error) {
90+
await testInfo.attach(`Error ${request.method} ${request.url}`, {
91+
body: Buffer.from(JSON.stringify(error, null, 2)),
92+
contentType: 'application/json',
93+
});
94+
}
95+
}
7696
}
7797
},
7898

@@ -137,11 +157,31 @@ export function extendWithApiSpyFixture<
137157

138158
// Attach to Playwright report if enabled
139159
if (config.attachToPlaywrightReport && entries.length > 0) {
140-
const attachmentContent = JSON.stringify(entries, null, 2);
141-
await testInfo.attach('api-spy-requests', {
142-
body: Buffer.from(attachmentContent),
143-
contentType: 'application/json',
144-
});
160+
for (const entry of entries) {
161+
const { request, response, error } = entry;
162+
163+
// Attach request
164+
await testInfo.attach(`Request ${request.method} ${request.url}`, {
165+
body: Buffer.from(JSON.stringify(request, null, 2)),
166+
contentType: 'application/json',
167+
});
168+
169+
// Attach response if exists
170+
if (response) {
171+
await testInfo.attach(`Response ${response.status} ${request.method} ${request.url}`, {
172+
body: Buffer.from(JSON.stringify(response, null, 2)),
173+
contentType: 'application/json',
174+
});
175+
}
176+
177+
// Attach error if exists
178+
if (error) {
179+
await testInfo.attach(`Error ${request.method} ${request.url}`, {
180+
body: Buffer.from(JSON.stringify(error, null, 2)),
181+
contentType: 'application/json',
182+
});
183+
}
184+
}
145185
}
146186
},
147187
});

src/fixture.ts

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,31 @@ interface FetchOptions {
2121
[key: string]: unknown;
2222
}
2323

24+
/**
25+
* Resolves a potentially relative URL to an absolute URL
26+
*/
27+
function resolveUrl(inputUrl: string, responseUrl: string): string {
28+
// If response URL is absolute, use it
29+
if (responseUrl.startsWith('http://') || responseUrl.startsWith('https://')) {
30+
return responseUrl;
31+
}
32+
33+
// If input URL is absolute, use it
34+
if (inputUrl.startsWith('http://') || inputUrl.startsWith('https://')) {
35+
return inputUrl;
36+
}
37+
38+
// Both are relative - return response URL as-is (will be shown as relative path)
39+
return responseUrl;
40+
}
41+
2442
/**
2543
* Creates APIRequestContext wrapper that intercepts requests
2644
*/
2745
function createApiRequestContextProxy(
2846
context: APIRequestContext,
29-
spy: ApiSpyInstance
47+
spy: ApiSpyInstance,
48+
baseURL?: string
3049
): APIRequestContext {
3150
const methodsToIntercept = ['get', 'post', 'put', 'patch', 'delete', 'head', 'fetch'] as const;
3251

@@ -59,8 +78,16 @@ function createApiRequestContextProxy(
5978
// Execute original request first to get actual URL from response
6079
const response: APIResponse = await (originalValue as Function).call(target, url, options);
6180

62-
// Use response.url() for the actual full URL (handles redirects, base URL, etc.)
63-
const actualUrl = response.url();
81+
// Get URL - response.url() should be absolute, but may be relative in some cases
82+
let actualUrl = response.url();
83+
84+
// If response URL is relative and we have baseURL, construct full URL
85+
if (baseURL && !actualUrl.startsWith('http://') && !actualUrl.startsWith('https://')) {
86+
// Construct full URL from baseURL
87+
const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
88+
const path = actualUrl.startsWith('/') ? actualUrl : '/' + actualUrl;
89+
actualUrl = base + path;
90+
}
6491

6592
// Capture request with actual URL
6693
const capturedRequest = await spy.captureRequest(method, actualUrl, {
@@ -154,8 +181,11 @@ export const test = base.extend<ApiSpyFixtures>({
154181
line: testInfo.line,
155182
});
156183

184+
// Get baseURL from project config
185+
const baseURL = testInfo.project.use.baseURL;
186+
157187
// Replace original request context with proxy
158-
const proxiedRequest = createApiRequestContextProxy(request, spy);
188+
const proxiedRequest = createApiRequestContextProxy(request, spy, baseURL);
159189

160190
// Replace request in testInfo so tests use proxy
161191
// @ts-expect-error - modifying private field
@@ -170,21 +200,31 @@ export const test = base.extend<ApiSpyFixtures>({
170200

171201
// Attach to Playwright report if enabled
172202
if (config.attachToPlaywrightReport && entries.length > 0) {
173-
// Attach requests
174-
const requests = entries.map(e => e.request);
175-
await testInfo.attach('requests', {
176-
body: Buffer.from(JSON.stringify(requests, null, 2)),
177-
contentType: 'application/json',
178-
});
179-
180-
// Attach responses
181-
const responses = entries
182-
.filter(e => e.response)
183-
.map(e => ({ requestId: e.request.id, ...e.response }));
184-
await testInfo.attach('responses', {
185-
body: Buffer.from(JSON.stringify(responses, null, 2)),
186-
contentType: 'application/json',
187-
});
203+
for (const entry of entries) {
204+
const { request, response, error } = entry;
205+
206+
// Attach request
207+
await testInfo.attach(`Request ${request.method} ${request.url}`, {
208+
body: Buffer.from(JSON.stringify(request, null, 2)),
209+
contentType: 'application/json',
210+
});
211+
212+
// Attach response if exists
213+
if (response) {
214+
await testInfo.attach(`Response ${response.status} ${request.method} ${request.url}`, {
215+
body: Buffer.from(JSON.stringify(response, null, 2)),
216+
contentType: 'application/json',
217+
});
218+
}
219+
220+
// Attach error if exists
221+
if (error) {
222+
await testInfo.attach(`Error ${request.method} ${request.url}`, {
223+
body: Buffer.from(JSON.stringify(error, null, 2)),
224+
contentType: 'application/json',
225+
});
226+
}
227+
}
188228
}
189229
},
190230
});
@@ -197,6 +237,9 @@ export const testWithApiSpy = base.extend<ApiSpyFixtures & { request: APIRequest
197237
const config = globalApiSpyStore.config;
198238
const spy = new ApiSpyInstance(config);
199239

240+
// Store baseURL for request proxy
241+
(spy as ApiSpyInstance & { _baseURL?: string })._baseURL = testInfo.project.use.baseURL;
242+
200243
spy.setTestInfo({
201244
title: testInfo.title,
202245
file: testInfo.file,
@@ -210,27 +253,38 @@ export const testWithApiSpy = base.extend<ApiSpyFixtures & { request: APIRequest
210253

211254
// Attach to Playwright report if enabled
212255
if (config.attachToPlaywrightReport && entries.length > 0) {
213-
// Attach requests
214-
const requests = entries.map(e => e.request);
215-
await testInfo.attach('requests', {
216-
body: Buffer.from(JSON.stringify(requests, null, 2)),
217-
contentType: 'application/json',
218-
});
219-
220-
// Attach responses
221-
const responses = entries
222-
.filter(e => e.response)
223-
.map(e => ({ requestId: e.request.id, ...e.response }));
224-
await testInfo.attach('responses', {
225-
body: Buffer.from(JSON.stringify(responses, null, 2)),
226-
contentType: 'application/json',
227-
});
256+
for (const entry of entries) {
257+
const { request, response, error } = entry;
258+
259+
// Attach request
260+
await testInfo.attach(`Request ${request.method} ${request.url}`, {
261+
body: Buffer.from(JSON.stringify(request, null, 2)),
262+
contentType: 'application/json',
263+
});
264+
265+
// Attach response if exists
266+
if (response) {
267+
await testInfo.attach(`Response ${response.status} ${request.method} ${request.url}`, {
268+
body: Buffer.from(JSON.stringify(response, null, 2)),
269+
contentType: 'application/json',
270+
});
271+
}
272+
273+
// Attach error if exists
274+
if (error) {
275+
await testInfo.attach(`Error ${request.method} ${request.url}`, {
276+
body: Buffer.from(JSON.stringify(error, null, 2)),
277+
contentType: 'application/json',
278+
});
279+
}
280+
}
228281
}
229282
},
230283

231-
request: async ({ request, apiSpy }, use) => {
284+
request: async ({ request, apiSpy }, use, testInfo) => {
232285
// Create proxy for request context
233-
const proxiedRequest = createApiRequestContextProxy(request, apiSpy as ApiSpyInstance);
286+
const baseURL = testInfo.project.use.baseURL;
287+
const proxiedRequest = createApiRequestContextProxy(request, apiSpy as ApiSpyInstance, baseURL);
234288
await use(proxiedRequest);
235289
},
236290
});

0 commit comments

Comments
 (0)