import { extractQueryStringObjectFromUrl , UpdatePolicy , HttpHeaders , HttpRequestMethod , MimeType , } from '../../../utils' ; import { Page , Request , Route } from 'playwright' ; export type ResponseData = Record < string , unknown > | Record < string , never > | string | undefined | unknown ; export type PostData = Record < string , unknown > | string | undefined | null | unknown ; export type QueryString = Record < string , string > ; export interface RequestInfos { request : Request ; queryString : QueryString ; postData : PostData ; sharedContext : unknown ; } export interface export interface FluentMock { type { string } memberof displayName : string ; memberof urlMatcher : ( url : string ) => boolean ; memberof methodMatcher : ( method : HttpRequestMethod ) => boolean ; memberof queryStringMatcher : ( queryString : QueryString ) => boolean ; memberof postDataMatcher : ( postData : PostData ) => boolean ; /**
     * Optional predicate acting on the shared context.
     * This predicate will be called only if all predicates  urlMatcher ,  queryStringMatcher ,  postDataMatcher , returns true.
     * If you do not set a contextMatcher, a default one that always returns true is provided.
     * A mock can update the shared context on any method passing a  RequestInfos object.
     * A context matcher should be used when the mock response depends on the requests history,
     * for example when the mock must respond only to the nth request given by the urlMatcher.
     *
     *  memberof contextMatcher : ( context : unknown ) => boolean ; memberof customMatcher : ( requestInfos : RequestInfos ) => boolean ; memberof enrichResponseHeaders : ( headers : HttpHeaders ) => HttpHeaders ; type { ( 'json' | 'string' | 'javascript' | 'empty' | 'continue' ) } memberof responseType : 'json' | 'string' | 'javascript' | 'empty' | 'continue' ; memberof status : number | ( ( requestInfos : RequestInfos ) => number ) ; memberof jsonResponse : ( requestInfos : RequestInfos ) => ResponseData ; memberof rawResponse : ( requestInfos : RequestInfos ) => string ; type { number } memberof delayInMilliseconds : number ; /**
     * Optional callback to update the data source of the mocked response.
     * When provided, this method will be called automatically when
     *  1°) the mock is found to be outdated by the helper  getOutdatedMocks )
     *  2°) and the call to  lastUpdated gives a date that is older than the  updatePolicy memberof updateData : ( requestInfos : RequestInfos , response : ResponseData ) => void ; /**
     * Optional callback to get the last update of the data source used to mock the response.
     * This method will be called automatically when the mock is found to be outdated by the helper  getOutdatedMocks )
     * Defaults to the current date.
     *
     *  type { Date } memberof lastUpdated : ( ) => Date ; type { UpdatePolicy } memberof updatePolicy : UpdatePolicy ; } export function noopVoidFunc ( ) { } export function getPostDataOf ( request ) { try { return request . postDataJSON ( ) ; } catch ( error ) { return request . postData ( ) ; } } export const passthroughMock = { displayName : 'passthroughMock' , urlMatcher : ( ) => true , methodMatcher : ( ) => true , queryStringMatcher : ( ) => true , postDataMatcher : ( ) => true , contextMatcher : ( ) => true , customMatcher : ( ) => true , enrichResponseHeaders : ( headers ) => headers , responseType : 'continue' , status : 200 , jsonResponse : ( ) => { return { } ; } , rawResponse : ( ) => '' , delayInMilliseconds : 0 , updateData : noopVoidFunc , lastUpdated : ( ) => new Date ( ) , updatePolicy : 'always' , } ; export interface WithMocksOptions { onMockFound : ( mock : Partial < FluentMock > , requestInfos : RequestInfos ) => void ; onMockNotFound : ( requestInfos : RequestInfos ) => void ; onInternalError : ( error : Error , mock : Partial < FluentMock > , requestInfos : RequestInfos ) => void ; } export const defaultMocksOptions = { onMockNotFound : ( ) => { } , onMockFound : ( ) => { } , onInternalError : ( ) => { } , } ; export function getMockStatus ( mock , requestInfos ) { if ( typeof mock . status === 'function' ) { return mock . status ( requestInfos ) ; } if ( typeof mock . status === 'number' ) { return mock . status ; } return 200 ; } export function inferMockResponseTypeIfNeeded ( mock ) { if ( mock . responseType ) { return mock ; } if ( typeof mock . jsonResponse === 'function' && typeof mock . rawResponse !== 'function' ) { return { ... mock , responseType : 'json' , } ; } if ( typeof mock . rawResponse === 'function' && typeof mock . jsonResponse !== 'function' ) { return { ... mock , responseType : 'string' , } ; } if ( typeof mock . rawResponse !== 'function' && typeof mock . jsonResponse !== 'function' ) { return { ... mock , responseType : 'empty' , } ; } return mock ; } export function spreadMissingProperties ( mock ) { return { ... passthroughMock , displayName : 'not set' , ... mock } ; } export interface RouteOptions { /**
     * Response body.
     */ body : string | Buffer ; /**
     * If set, equals to setting `Content-Type` response header.
     */ contentType : string ; /**
     * Response headers. Header values will be converted to a string.
     */ headers : { [ key ] : string ; } ; /**
     * File path to respond with. The content type will be inferred from file extension. If `path` is a relative path, then it
     * is resolved relative to the current working directory.
     */ path : string ; /**
     * Response status code, defaults to `200`.
     */ status : number ; } async function fullfillRouteWithMock ( mock , route , options ) { if ( mock . delayInMilliseconds > 0 ) { setTimeout ( ( ) => route . fulfill ( options ) , mock . delayInMilliseconds ) ; return ; } route . fulfill ( options ) ; } export async function withMocks ( mocks , context , options , page ) { if ( typeof mocks !== 'function' ) { throw new Error ( `mocks must be a function that returns an array of FluentMock objects.` ) ; } if ( ! Array . isArray ( mocks ( ) ) ) { throw new Error ( `mocks must be a function that returns an array of FluentMock objects.` ) ; } if ( ! page ) { throw new Error ( `Cannot intercept requests with mocks because no browser has been launched` ) ; } const mockOptions = { ... defaultMocksOptions , ... options , } ; await page . route ( ( uri ) => { if ( mocks ( ) . length === 0 ) { return false ; } const mockExists = mocks ( ) . map ( inferMockResponseTypeIfNeeded ) . map ( spreadMissingProperties ) . map ( ( mock ) => mock . urlMatcher ( uri . toString ( ) ) ) . some ( ( match ) => match === true ) ; return mockExists ; } , ( route , request ) => { const requestMethod = request . method ( ) as HttpRequestMethod ; const url = request . url ( ) ; const queryString = extractQueryStringObjectFromUrl ( url ) as QueryString ; const postData = getPostDataOf ( request ) ; const sharedContext = context ( ) ; const requestInfos = { request , queryString , postData , sharedContext } ; const mock = mocks ( ) . map ( inferMockResponseTypeIfNeeded ) . map ( spreadMissingProperties ) . filter ( ( mock ) => mock . urlMatcher ( url ) ) . filter ( ( mock ) => mock . methodMatcher ( requestMethod ) ) . filter ( ( mock ) => mock . queryStringMatcher ( queryString ) ) . filter ( ( mock ) => mock . postDataMatcher ( postData ) ) . filter ( ( mock ) => mock . contextMatcher ( sharedContext ) ) . filter ( ( mock ) => mock . customMatcher ( requestInfos ) ) . pop ( ) ; if ( ! mock ) { mockOptions . onMockNotFound ( requestInfos ) ; route . continue ( ) ; return ; } if ( mock . responseType === 'continue' && mock . delayInMilliseconds === 0 ) { route . continue ( ) ; return ; } if ( mock . responseType === 'continue' && mock . delayInMilliseconds > 0 ) { setTimeout ( ( ) => route . continue ( ) , mock . delayInMilliseconds ) ; return ; } if ( mock . responseType === 'json' ) { const responseObject = mock . jsonResponse ( requestInfos ) ; const headers = mock . enrichResponseHeaders ( { 'Access-Control-Allow-Credentials' : 'true' , 'Access-Control-Allow-Origin' : '*' , } ) ; const status = getMockStatus ( mock , requestInfos ) ; const body = JSON . stringify ( responseObject ) ; const contentType = 'application/json' ; mockOptions . onMockFound ( mock , requestInfos ) ; fullfillRouteWithMock ( mock , route , { status , headers , contentType , body } ) ; return ; } if ( mock . responseType === 'string' ) { const headers = mock . enrichResponseHeaders ( { 'Access-Control-Allow-Credentials' : 'true' , 'Access-Control-Allow-Origin' : '*' , } ) ; const status = getMockStatus ( mock , requestInfos ) ; const body = mock . rawResponse ( requestInfos ) ; const contentType = 'text/plain' ; mockOptions . onMockFound ( mock , requestInfos ) ; fullfillRouteWithMock ( mock , route , { status , headers , contentType , body } ) ; return ; } if ( mock . responseType === 'empty' ) { const headers = mock . enrichResponseHeaders ( { 'Access-Control-Allow-Credentials' : 'true' , 'Access-Control-Allow-Origin' : '*' , } ) ; const status = getMockStatus ( mock , requestInfos ) ; const body = '' ; const contentType = 'text/plain' ; mockOptions . onMockFound ( mock , requestInfos ) ; fullfillRouteWithMock ( mock , route , { status , headers , contentType , body } ) ; return ; } if ( mock . responseType === 'javascript' ) { const headers = mock . enrichResponseHeaders ( { 'access-control-allow-methods' : '*' , 'Access-Control-Allow-Credentials' : 'true' , 'Access-Control-Allow-Origin' : '*' , } ) ; const status = getMockStatus ( mock , requestInfos ) ; const body = mock . rawResponse ( requestInfos ) ; const contentType = 'application/javascript' ; mockOptions . onMockFound ( mock , requestInfos ) ; fullfillRouteWithMock ( mock , route , { status , headers , contentType , body } ) ; return ; } throw new Error ( `mock with response type '${ mock . responseType }' is not yet implemented` ) ; } ) ; }