| | import { |
| | Request as PlaywrightRequest, |
| | Response as PlaywrightResponse, |
| | } from 'playwright' |
| | import { inspect } from 'node:util' |
| | import { Playwright } from '../browsers/playwright' |
| |
|
| | export type RequestMatcherObject = { |
| | pathname: string |
| | search?: string |
| | method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'HEAD' | 'OPTIONS' |
| | } |
| |
|
| | export type RequestMatcher = |
| | | RequestMatcherObject |
| | | ((request: PlaywrightRequest) => Promise<boolean>) |
| |
|
| | export type RequestTracker = ReturnType<typeof createRequestTracker> |
| |
|
| | |
| | export function createRequestTracker(browser: Playwright) { |
| | |
| | async function captureResponse<T>( |
| | action: () => Promise<T>, |
| | { |
| | request: requestMatcher, |
| | timeoutMs = 5000, |
| | }: { |
| | request: RequestMatcher |
| | timeoutMs?: number |
| | } |
| | ): Promise<[result: T, captured: PlaywrightResponse]> { |
| | const isMatchingRequest = async (request: PlaywrightRequest) => { |
| | if (typeof requestMatcher === 'function') { |
| | |
| | return await requestMatcher(request) |
| | } else { |
| | const url = new URL(request.url()) |
| | if (requestMatcher.pathname !== url.pathname) { |
| | return false |
| | } |
| | if (requestMatcher.search !== undefined) { |
| | if (url.search !== requestMatcher.search) { |
| | return false |
| | } |
| | } |
| | if (requestMatcher.method !== undefined) { |
| | if (request.method() !== requestMatcher.method) { |
| | return false |
| | } |
| | } |
| | return true |
| | } |
| | } |
| |
|
| | const responseCtrl = promiseWithResolvers<PlaywrightResponse>() |
| | let isSettled = false |
| |
|
| | let capturedRequest: PlaywrightRequest | undefined |
| |
|
| | const cleanups: (() => void)[] = [] |
| |
|
| | |
| | responseCtrl.promise.finally(() => { |
| | isSettled = true |
| | cleanups.forEach((cb) => cb()) |
| | }) |
| |
|
| | |
| | const onRequest = async (request: PlaywrightRequest) => { |
| | if (!(await isMatchingRequest(request))) { |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| | if (capturedRequest) { |
| | const criteriaDescription = |
| | typeof requestMatcher === 'function' |
| | ? 'the specified criteria' |
| | : inspect(requestMatcher) |
| | return responseCtrl.reject( |
| | new Error( |
| | [ |
| | `Captured multiple requests that match ${criteriaDescription} during a \`captureResponse\` call:`, |
| | ...[capturedRequest, request].map( |
| | (req) => ` - ${req.method} ${req.url}` |
| | ), |
| | 'This is currently not supported.', |
| | ].join('\n') |
| | ) |
| | ) |
| | } |
| |
|
| | |
| | capturedRequest = request |
| | console.log( |
| | `[request-tracker] request: ${request.method()} ${request.url()}` + |
| | (['POST', 'PUT', 'PATCH'].includes(request.method()) |
| | ? ` (content-type: ${request.headers()['content-type']})` |
| | : '') |
| | ) |
| | const onResponse = (response: PlaywrightResponse) => { |
| | if (isSettled) { |
| | return |
| | } |
| | if (response.request() === request) { |
| | |
| | console.log(`[request-tracker] response: ${response.status()}`) |
| | return responseCtrl.resolve(response) |
| | } |
| | } |
| | browser.on('response', onResponse) |
| | cleanups.push(() => browser.off('response', onResponse)) |
| | } |
| |
|
| | |
| | browser.on('request', onRequest) |
| | cleanups.push(() => browser.off('request', onRequest)) |
| |
|
| | |
| | |
| | const actionPromise = Promise.resolve().then(action) |
| |
|
| | const resultPromise = Promise.all([ |
| | actionPromise, |
| | responseCtrl.promise, |
| | ] as const) |
| |
|
| | actionPromise.then( |
| | () => { |
| | |
| | |
| | if (isSettled) { |
| | return |
| | } |
| |
|
| | |
| | |
| | const abortTimeoutId = setTimeout(() => { |
| | if (isSettled) { |
| | return |
| | } |
| | return responseCtrl.reject( |
| | new Error( |
| | capturedRequest === undefined |
| | ? `Did not intercept a request within ${timeoutMs}ms of the action callback finishing` |
| | : `Did not intercept a response within ${timeoutMs}ms of the action callback finishing` |
| | ) |
| | ) |
| | }, timeoutMs) |
| | cleanups.push(() => clearTimeout(abortTimeoutId)) |
| | }, |
| | () => { |
| | |
| | |
| | |
| | |
| | return responseCtrl.resolve(null!) |
| | } |
| | ) |
| |
|
| | return resultPromise |
| | } |
| |
|
| | return { captureResponse } |
| | } |
| |
|
| | function promiseWithResolvers<T>() { |
| | let resolve: (value: T) => void = undefined! |
| | let reject: (error: unknown) => void = undefined! |
| | const promise = new Promise<T>((_resolve, _reject) => { |
| | resolve = _resolve |
| | reject = _reject |
| | }) |
| | return { promise, resolve, reject } |
| | } |
| |
|