1+ import type { BrowserContext , Request , Route } from '@playwright/test' ;
2+ import { readFile } from 'node:fs/promises' ;
3+ import path from 'node:path' ;
4+ import { normalizeRecording } from '../../src/shared/schema' ;
5+
6+ type RecordedRequest = {
7+ key : string ;
8+ method : string ;
9+ url : string ;
10+ path : string ;
11+ status : number ;
12+ headers : Record < string , string > ;
13+ body : string ;
14+ } ;
15+
16+ export interface RecordingMockOptions {
17+ fallbackMatching ?: boolean ;
18+ strictUnmatched ?: boolean ;
19+ urlBase ?: string ;
20+ debug ?: ( message : string ) => void ;
21+ }
22+
23+ export async function applyRecordingMocks (
24+ context : BrowserContext ,
25+ recordingPath : string ,
26+ options : RecordingMockOptions = { }
27+ ) : Promise < { dispose : ( ) => Promise < void > } > {
28+ const raw = await readFile ( path . resolve ( recordingPath ) , 'utf-8' ) ;
29+ const parsed = JSON . parse ( raw ) ;
30+ const recording = normalizeRecording ( parsed ) ;
31+
32+ if ( ! recording ) {
33+ throw new Error ( `Unable to parse recording JSON at ${ recordingPath } ` ) ;
34+ }
35+
36+ const enabledRequests = Object . entries ( recording . requests )
37+ . filter ( ( [ , request ] ) => request . enabled !== false )
38+ . map ( ( [ key , request ] ) : RecordedRequest => {
39+ const requestUrl = normalizeUrl ( request . url , options . urlBase ) ;
40+ const requestPath = getPathnameAndSearch ( requestUrl ) ;
41+ return {
42+ key,
43+ method : request . method . toUpperCase ( ) ,
44+ url : requestUrl ,
45+ path : requestPath ,
46+ status : request . status ?? 200 ,
47+ headers : {
48+ 'content-type' : 'application/json' ,
49+ ...( request . responseHeaders ?? { } )
50+ } ,
51+ body : request . responseBody ?? ''
52+ } ;
53+ } ) ;
54+
55+ const exactIndex = new Map < string , RecordedRequest > ( ) ;
56+ const fallbackIndex = new Map < string , RecordedRequest > ( ) ;
57+
58+ for ( const request of enabledRequests ) {
59+ exactIndex . set ( toExactKey ( request . method , request . url ) , request ) ;
60+ if ( ! fallbackIndex . has ( toFallbackKey ( request . method , request . path ) ) ) {
61+ fallbackIndex . set ( toFallbackKey ( request . method , request . path ) , request ) ;
62+ }
63+ }
64+
65+ const routeHandler = async ( route : Route , request : Request ) => {
66+ const method = request . method ( ) . toUpperCase ( ) ;
67+ const url = request . url ( ) ;
68+ const exactMatch = exactIndex . get ( toExactKey ( method , url ) ) ;
69+
70+ const fallbackMatch =
71+ ! exactMatch && options . fallbackMatching
72+ ? fallbackIndex . get ( toFallbackKey ( method , getPathnameAndSearch ( url ) ) )
73+ : undefined ;
74+
75+ const matchedRequest = exactMatch ?? fallbackMatch ;
76+
77+ if ( ! matchedRequest ) {
78+ const message = `No recording match for ${ method } ${ url } ` ;
79+ options . debug ?.( message ) ;
80+ if ( options . strictUnmatched ) {
81+ throw new Error ( message ) ;
82+ }
83+ await route . continue ( ) ;
84+ return ;
85+ }
86+
87+ await route . fulfill ( {
88+ status : matchedRequest . status ,
89+ headers : matchedRequest . headers ,
90+ body : matchedRequest . body
91+ } ) ;
92+ } ;
93+
94+ await context . route ( '**/*' , routeHandler ) ;
95+
96+ return {
97+ dispose : async ( ) => {
98+ await context . unroute ( '**/*' , routeHandler ) ;
99+ }
100+ } ;
101+ }
102+
103+ function toExactKey ( method : string , url : string ) : string {
104+ return `${ method } ${ url } ` ;
105+ }
106+
107+ function toFallbackKey ( method : string , pathValue : string ) : string {
108+ return `${ method } ${ pathValue } ` ;
109+ }
110+
111+ function getPathnameAndSearch ( urlValue : string ) : string {
112+ const url = new URL ( urlValue ) ;
113+ return `${ url . pathname } ${ url . search } ` ;
114+ }
115+
116+ function normalizeUrl ( urlValue : string , urlBase ?: string ) : string {
117+ if ( ! urlBase ) {
118+ return urlValue ;
119+ }
120+
121+ try {
122+ const parsed = new URL ( urlValue ) ;
123+ const base = new URL ( urlBase ) ;
124+ parsed . protocol = base . protocol ;
125+ parsed . host = base . host ;
126+ return parsed . toString ( ) ;
127+ } catch {
128+ return new URL ( urlValue , urlBase ) . toString ( ) ;
129+ }
130+ }
0 commit comments