167 lines
3.8 KiB
TypeScript
167 lines
3.8 KiB
TypeScript
import { Page, Request, Response } from '@playwright/test';
|
|
|
|
export interface APICall {
|
|
method: string;
|
|
url: string;
|
|
pathname: string;
|
|
timestamp: number;
|
|
requestHeaders?: Record<string, string>;
|
|
requestBody?: any;
|
|
responseStatus?: number;
|
|
responseHeaders?: Record<string, string>;
|
|
responseBody?: any;
|
|
duration?: number;
|
|
}
|
|
|
|
export class APIInterceptor {
|
|
private calls: APICall[] = [];
|
|
private pendingRequests: Map<string, APICall> = new Map();
|
|
|
|
/**
|
|
* Start intercepting API calls on the given page
|
|
*/
|
|
intercept(page: Page) {
|
|
page.on('request', (request: Request) => {
|
|
this.recordRequest(request);
|
|
});
|
|
|
|
page.on('response', async (response: Response) => {
|
|
await this.recordResponse(response);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Record an API request
|
|
*/
|
|
private recordRequest(request: Request) {
|
|
const url = request.url();
|
|
|
|
// Only capture API calls
|
|
if (!url.includes('/api/')) {
|
|
return;
|
|
}
|
|
|
|
const parsedUrl = new URL(url);
|
|
const call: APICall = {
|
|
method: request.method(),
|
|
url: url,
|
|
pathname: parsedUrl.pathname,
|
|
timestamp: Date.now(),
|
|
requestHeaders: request.headers(),
|
|
};
|
|
|
|
// Try to capture request body
|
|
try {
|
|
const postData = request.postData();
|
|
if (postData) {
|
|
try {
|
|
call.requestBody = JSON.parse(postData);
|
|
} catch {
|
|
call.requestBody = postData;
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore if we can't get post data
|
|
}
|
|
|
|
// Store as pending until we get the response
|
|
this.pendingRequests.set(url, call);
|
|
}
|
|
|
|
/**
|
|
* Record an API response and merge with request data
|
|
*/
|
|
private async recordResponse(response: Response) {
|
|
const url = response.url();
|
|
|
|
// Only capture API calls
|
|
if (!url.includes('/api/')) {
|
|
return;
|
|
}
|
|
|
|
const pendingCall = this.pendingRequests.get(url);
|
|
|
|
if (pendingCall) {
|
|
// Calculate duration
|
|
pendingCall.duration = Date.now() - pendingCall.timestamp;
|
|
pendingCall.responseStatus = response.status();
|
|
pendingCall.responseHeaders = response.headers();
|
|
|
|
// Try to capture response body
|
|
try {
|
|
const contentType = response.headers()['content-type'] || '';
|
|
if (contentType.includes('application/json')) {
|
|
pendingCall.responseBody = await response.json();
|
|
}
|
|
} catch {
|
|
// Ignore if we can't parse response
|
|
}
|
|
|
|
this.calls.push(pendingCall);
|
|
this.pendingRequests.delete(url);
|
|
} else {
|
|
// Response without matching request - create minimal record
|
|
const parsedUrl = new URL(url);
|
|
this.calls.push({
|
|
method: 'UNKNOWN',
|
|
url: url,
|
|
pathname: parsedUrl.pathname,
|
|
timestamp: Date.now(),
|
|
responseStatus: response.status(),
|
|
responseHeaders: response.headers(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all captured API calls
|
|
*/
|
|
getCalls(): APICall[] {
|
|
return this.calls;
|
|
}
|
|
|
|
/**
|
|
* Get calls filtered by method
|
|
*/
|
|
getCallsByMethod(method: string): APICall[] {
|
|
return this.calls.filter(call => call.method === method);
|
|
}
|
|
|
|
/**
|
|
* Get calls filtered by pathname pattern
|
|
*/
|
|
getCallsByPath(pattern: string | RegExp): APICall[] {
|
|
if (typeof pattern === 'string') {
|
|
return this.calls.filter(call => call.pathname.includes(pattern));
|
|
} else {
|
|
return this.calls.filter(call => pattern.test(call.pathname));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get unique endpoints called
|
|
*/
|
|
getUniqueEndpoints(): string[] {
|
|
const endpoints = new Set<string>();
|
|
for (const call of this.calls) {
|
|
endpoints.add(`${call.method} ${call.pathname}`);
|
|
}
|
|
return Array.from(endpoints);
|
|
}
|
|
|
|
/**
|
|
* Reset the interceptor
|
|
*/
|
|
reset() {
|
|
this.calls = [];
|
|
this.pendingRequests.clear();
|
|
}
|
|
|
|
/**
|
|
* Export calls to JSON
|
|
*/
|
|
toJSON(): string {
|
|
return JSON.stringify(this.calls, null, 2);
|
|
}
|
|
}
|