jam-cloud/jam-ui/test/utils/api-interceptor.ts

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);
}
}