1
+ import type { MockedModule } from '@vitest/mocker'
1
2
import type {
2
3
Browser ,
3
4
BrowserContext ,
@@ -7,11 +8,15 @@ import type {
7
8
LaunchOptions ,
8
9
Page ,
9
10
} from 'playwright'
11
+ import type { SourceMap } from 'rollup'
12
+ import type { ResolvedConfig } from 'vite'
10
13
import type {
14
+ BrowserModuleMocker ,
11
15
BrowserProvider ,
12
16
BrowserProviderInitializationOptions ,
13
17
TestProject ,
14
18
} from 'vitest/node'
19
+ import { createManualModuleSource } from '@vitest/mocker/node'
15
20
16
21
export const playwrightBrowsers = [ 'firefox' , 'webkit' , 'chromium' ] as const
17
22
export type PlaywrightBrowser = ( typeof playwrightBrowsers ) [ number ]
@@ -40,6 +45,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
40
45
41
46
private browserPromise : Promise < Browser > | null = null
42
47
48
+ public mocker : BrowserModuleMocker | undefined
49
+
43
50
getSupportedBrowsers ( ) : readonly string [ ] {
44
51
return playwrightBrowsers
45
52
}
@@ -51,6 +58,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
51
58
this . project = project
52
59
this . browserName = browser
53
60
this . options = options as any
61
+ this . mocker = this . createMocker ( )
54
62
}
55
63
56
64
private async openBrowser ( ) {
@@ -103,6 +111,136 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
103
111
return this . browserPromise
104
112
}
105
113
114
+ private createMocker ( ) : BrowserModuleMocker {
115
+ const idPreficates = new Map < string , ( url : URL ) => boolean > ( )
116
+ const sessionIds = new Map < string , string [ ] > ( )
117
+
118
+ function createPredicate ( sessionId : string , url : string ) {
119
+ const moduleUrl = new URL ( url , 'http://localhost' )
120
+ const predicate = ( url : URL ) => {
121
+ if ( url . searchParams . has ( '_vitest_original' ) ) {
122
+ return false
123
+ }
124
+
125
+ // different modules, ignore request
126
+ if ( url . pathname !== moduleUrl . pathname ) {
127
+ return false
128
+ }
129
+
130
+ url . searchParams . delete ( 't' )
131
+ url . searchParams . delete ( 'v' )
132
+ url . searchParams . delete ( 'import' )
133
+
134
+ // different search params, ignore request
135
+ if ( url . searchParams . size !== moduleUrl . searchParams . size ) {
136
+ return false
137
+ }
138
+
139
+ // check that all search params are the same
140
+ for ( const [ param , value ] of url . searchParams . entries ( ) ) {
141
+ if ( moduleUrl . searchParams . get ( param ) !== value ) {
142
+ return false
143
+ }
144
+ }
145
+
146
+ return true
147
+ }
148
+ const ids = sessionIds . get ( sessionId ) || [ ]
149
+ ids . push ( moduleUrl . href )
150
+ sessionIds . set ( sessionId , ids )
151
+ idPreficates . set ( moduleUrl . href , predicate )
152
+ return predicate
153
+ }
154
+
155
+ return {
156
+ register : async ( sessionId : string , module : MockedModule ) : Promise < void > => {
157
+ const page = this . getPage ( sessionId )
158
+ await page . route ( createPredicate ( sessionId , module . url ) , async ( route ) => {
159
+ if ( module . type === 'manual' ) {
160
+ const exports = Object . keys ( await module . resolve ( ) )
161
+ const body = createManualModuleSource ( module . url , exports )
162
+ return route . fulfill ( {
163
+ body,
164
+ headers : getHeaders ( this . project . browser ! . vite . config ) ,
165
+ } )
166
+ }
167
+
168
+ // webkit doesn't support redirect responses
169
+ // https://github.com/microsoft/playwright/issues/18318
170
+ const isWebkit = this . browserName === 'webkit'
171
+ if ( isWebkit ) {
172
+ const url = module . type === 'redirect'
173
+ ? ( ( ) => {
174
+ // url has http:// which vite.trasnformRequest doesn't understand
175
+ const url = new URL ( module . redirect )
176
+ return url . href . slice ( url . origin . length )
177
+ } ) ( )
178
+ : ( ( ) => {
179
+ const url = new URL ( route . request ( ) . url ( ) )
180
+ url . searchParams . set ( 'mock' , module . type )
181
+ return url . href . slice ( url . origin . length )
182
+ } ) ( )
183
+ const result = await this . project . browser ! . vite . transformRequest ( url ) . catch ( ( ) => null )
184
+ if ( ! result ) {
185
+ return route . continue ( )
186
+ }
187
+ let content = result . code
188
+ if ( result . map && 'version' in result . map && result . map . mappings ) {
189
+ const type = isDirectCSSRequest ( url ) ? 'css' : 'js'
190
+ content = getCodeWithSourcemap ( type , content . toString ( ) , result . map )
191
+ }
192
+ return route . fulfill ( {
193
+ body : content ,
194
+ headers : getHeaders ( this . project . browser ! . vite . config ) ,
195
+ } )
196
+ }
197
+
198
+ if ( module . type === 'redirect' ) {
199
+ return route . fulfill ( {
200
+ status : 302 ,
201
+ headers : {
202
+ Location : module . redirect ,
203
+ } ,
204
+ } )
205
+ }
206
+ else if ( module . type === 'automock' || module . type === 'autospy' ) {
207
+ const url = new URL ( route . request ( ) . url ( ) )
208
+ url . searchParams . set ( 'mock' , module . type )
209
+ return route . fulfill ( {
210
+ status : 302 ,
211
+ headers : {
212
+ Location : url . href ,
213
+ } ,
214
+ } )
215
+ }
216
+ else {
217
+ // all types are exhausted
218
+ const _module : never = module
219
+ }
220
+ } )
221
+ } ,
222
+ delete : async ( sessionId : string , id : string ) : Promise < void > => {
223
+ const page = this . getPage ( sessionId )
224
+ const predicate = idPreficates . get ( id )
225
+ if ( predicate ) {
226
+ await page . unroute ( predicate ) . finally ( ( ) => idPreficates . delete ( id ) )
227
+ }
228
+ } ,
229
+ clear : async ( sessionId : string ) : Promise < void > => {
230
+ const page = this . getPage ( sessionId )
231
+ const ids = sessionIds . get ( sessionId ) || [ ]
232
+ const promises = ids . map ( ( id ) => {
233
+ const predicate = idPreficates . get ( id )
234
+ if ( predicate ) {
235
+ return page . unroute ( predicate ) . finally ( ( ) => idPreficates . delete ( id ) )
236
+ }
237
+ return null
238
+ } )
239
+ await Promise . all ( promises ) . finally ( ( ) => sessionIds . delete ( sessionId ) )
240
+ } ,
241
+ }
242
+ }
243
+
106
244
private async createContext ( sessionId : string ) {
107
245
if ( this . contexts . has ( sessionId ) ) {
108
246
return this . contexts . get ( sessionId ) !
@@ -113,7 +251,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
113
251
const options = {
114
252
...contextOptions ,
115
253
ignoreHTTPSErrors : true ,
116
- serviceWorkers : 'allow' ,
117
254
} satisfies BrowserContextOptions
118
255
if ( this . project . config . browser . ui ) {
119
256
options . viewport = null
@@ -154,7 +291,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
154
291
const timeout = setTimeout ( ( ) => {
155
292
const err = new Error ( `Cannot find "vitest-iframe" on the page. This is a bug in Vitest, please report it.` )
156
293
reject ( err )
157
- } , 1000 )
294
+ } , 1000 ) . unref ( )
158
295
page . on ( 'frameattached' , ( frame ) => {
159
296
clearTimeout ( timeout )
160
297
resolve ( frame )
@@ -241,3 +378,44 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
241
378
await browser ?. close ( )
242
379
}
243
380
}
381
+
382
+ function getHeaders ( config : ResolvedConfig ) {
383
+ const headers : Record < string , string > = {
384
+ 'Content-Type' : 'application/javascript' ,
385
+ }
386
+
387
+ for ( const name in config . server . headers ) {
388
+ headers [ name ] = String ( config . server . headers [ name ] ! )
389
+ }
390
+ return headers
391
+ }
392
+
393
+ function getCodeWithSourcemap (
394
+ type : 'js' | 'css' ,
395
+ code : string ,
396
+ map : SourceMap ,
397
+ ) : string {
398
+ if ( type === 'js' ) {
399
+ code += `\n//# sourceMappingURL=${ genSourceMapUrl ( map ) } `
400
+ }
401
+ else if ( type === 'css' ) {
402
+ code += `\n/*# sourceMappingURL=${ genSourceMapUrl ( map ) } */`
403
+ }
404
+
405
+ return code
406
+ }
407
+
408
+ function genSourceMapUrl ( map : SourceMap | string ) : string {
409
+ if ( typeof map !== 'string' ) {
410
+ map = JSON . stringify ( map )
411
+ }
412
+ return `data:application/json;base64,${ Buffer . from ( map ) . toString ( 'base64' ) } `
413
+ }
414
+
415
+ const CSS_LANGS_RE
416
+ = / \. (?: c s s | l e s s | s a s s | s c s s | s t y l | s t y l u s | p c s s | p o s t c s s | s s s ) (?: $ | \? ) /
417
+ const directRequestRE = / [ ? & ] d i r e c t \b /
418
+
419
+ function isDirectCSSRequest ( request : string ) : boolean {
420
+ return CSS_LANGS_RE . test ( request ) && directRequestRE . test ( request )
421
+ }
0 commit comments