@@ -24,6 +24,7 @@ import type {
2424 BrowserCommand ,
2525 BrowserModuleMocker ,
2626 BrowserProvider ,
27+ BrowserProviderCloseOptions ,
2728 BrowserProviderOption ,
2829 CDPSession ,
2930 TestProject ,
@@ -102,6 +103,7 @@ export function playwright(
102103}
103104
104105interface WarmBrowser {
106+ config : LaunchContext [ 'config' ]
105107 promise : Promise < Browser >
106108 launchOptionsJson : string
107109 pending : Set < WarmBrowser >
@@ -140,23 +142,16 @@ function prewarmBrowser(project: LaunchContext, options: PlaywrightProviderOptio
140142 if ( warmBrowsers . has ( project . config ) ) {
141143 return
142144 }
143- let pending = pendingWarmBrowsers . get ( project . vitest )
144- if ( ! pending ) {
145- const pendingBrowsers = new Set < WarmBrowser > ( )
146- pending = pendingBrowsers
147- pendingWarmBrowsers . set ( project . vitest , pendingBrowsers )
148- // Browsers whose projects never initialize a provider (they have no test
149- // files to run) are cleaned up when Vitest closes.
150- project . vitest . onClose ( ( ) => closeWarmBrowsers ( pendingBrowsers ) )
151- }
145+ const pending = getPendingWarmBrowsers ( project . vitest )
152146 const launchOptions = resolveLaunchOptions (
153147 project . config . browser ,
154148 project . vitest . config . inspector ,
155149 options ,
156150 browserName ,
157151 )
158152 const entry : WarmBrowser = {
159- launchOptionsJson : JSON . stringify ( launchOptions ) ,
153+ config : project . config ,
154+ launchOptionsJson : getWarmBrowserKey ( browserName , launchOptions ) ,
160155 pending,
161156 promise : ( async ( ) => {
162157 debug ?.( '[%s] prewarming the browser' , browserName )
@@ -176,6 +171,41 @@ function prewarmBrowser(project: LaunchContext, options: PlaywrightProviderOptio
176171 warmBrowsers . set ( project . config , entry )
177172}
178173
174+ function getWarmBrowserKey ( browserName : string , launchOptions : LaunchOptions ) : string {
175+ return JSON . stringify ( [ browserName , launchOptions ] )
176+ }
177+
178+ function getPendingWarmBrowsers ( vitest : LaunchContext [ 'vitest' ] ) : Set < WarmBrowser > {
179+ let pending = pendingWarmBrowsers . get ( vitest )
180+ if ( ! pending ) {
181+ const pendingBrowsers = new Set < WarmBrowser > ( )
182+ pending = pendingBrowsers
183+ pendingWarmBrowsers . set ( vitest , pendingBrowsers )
184+ // Browsers whose projects never initialize a provider (they have no test
185+ // files to run) are cleaned up when Vitest closes.
186+ vitest . onClose ( ( ) => closeWarmBrowsers ( pendingBrowsers ) )
187+ }
188+ return pending
189+ }
190+
191+ // the pool closes an idle instance to start another project; the browser
192+ // process stays warm so the next project adopts it instead of launching one
193+ function keepWarmBrowser (
194+ project : LaunchContext ,
195+ browser : Promise < Browser > ,
196+ launchOptionsJson : string ,
197+ ) {
198+ const pending = getPendingWarmBrowsers ( project . vitest )
199+ const entry : WarmBrowser = {
200+ config : project . config ,
201+ launchOptionsJson,
202+ pending,
203+ promise : browser ,
204+ }
205+ pending . add ( entry )
206+ warmBrowsers . set ( project . config , entry )
207+ }
208+
179209function takeWarmBrowser ( config : LaunchContext [ 'config' ] ) : WarmBrowser | undefined {
180210 const warm = warmBrowsers . get ( config )
181211 if ( warm ) {
@@ -185,6 +215,26 @@ function takeWarmBrowser(config: LaunchContext['config']): WarmBrowser | undefin
185215 return warm
186216}
187217
218+ // fewer browsers are prewarmed than there are projects and the pool starts
219+ // projects in test order, so a browser prepared for another project is
220+ // adopted when it is the same browser launched with the same options
221+ function takeAnyWarmBrowser (
222+ vitest : LaunchContext [ 'vitest' ] ,
223+ launchOptionsJson : string ,
224+ ) : WarmBrowser | undefined {
225+ const pending = pendingWarmBrowsers . get ( vitest )
226+ if ( ! pending ) {
227+ return
228+ }
229+ for ( const warm of pending ) {
230+ if ( warm . launchOptionsJson === launchOptionsJson ) {
231+ pending . delete ( warm )
232+ warmBrowsers . delete ( warm . config )
233+ return warm
234+ }
235+ }
236+ }
237+
188238async function closeWarmBrowsers ( pending : Set < WarmBrowser > ) : Promise < void > {
189239 const closing = Array . from ( pending , ( warm ) =>
190240 warm . promise . then ( ( browser ) => browser . close ( ) ) . catch ( ( ) => { } ) ,
@@ -245,6 +295,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
245295 public browserName : PlaywrightBrowser
246296
247297 private browserPromise : Promise < Browser > | null = null
298+ // set for a locally launched browser, which can be kept warm for another project
299+ private launchOptionsJson : string | null = null
248300 private closing = false
249301 private armedContexts = new WeakMap < BrowserContext , Promise < void > > ( )
250302
@@ -369,20 +421,26 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
369421 )
370422 this . browser = this . persistentContext . browser ( ) !
371423 } else {
372- const warm = takeWarmBrowser ( this . project . config )
373- if ( warm && warm . launchOptionsJson === JSON . stringify ( launchOptions ) ) {
424+ const launchOptionsJson = getWarmBrowserKey ( this . browserName , launchOptions )
425+ let warm = takeWarmBrowser ( this . project . config )
426+ if ( warm && warm . launchOptionsJson !== launchOptionsJson ) {
427+ debug ?.( '[%s] discarding the prewarmed browser, launch options changed' , this . browserName )
428+ void warm . promise . then ( ( browser ) => browser . close ( ) ) . catch ( ( ) => { } )
429+ warm = undefined
430+ }
431+ warm ??= takeAnyWarmBrowser ( this . project . vitest , launchOptionsJson )
432+ if ( warm ) {
374433 const browser = await warm . promise . catch ( ( ) => null )
375434 if ( browser ?. isConnected ( ) ) {
376435 debug ?.( '[%s] adopting the prewarmed browser' , this . browserName )
377436 this . browser = browser
378437 this . browserPromise = null
438+ this . launchOptionsJson = launchOptionsJson
379439 return this . browser
380440 }
381- } else if ( warm ) {
382- debug ?.( '[%s] discarding the prewarmed browser, launch options changed' , this . browserName )
383- void warm . promise . then ( ( browser ) => browser . close ( ) ) . catch ( ( ) => { } )
384441 }
385442 this . browser = await playwright [ this . browserName ] . launch ( launchOptions )
443+ this . launchOptionsJson = launchOptionsJson
386444 }
387445 this . browserPromise = null
388446 return this . browser
@@ -731,10 +789,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
731789 String ( disposable ) ,
732790 )
733791 await disposable ?. close ( )
734- this . pages . clear ( )
735- this . contexts . clear ( )
736- this . browser = null
737- this . browserPromise = null
738792 throw new Error ( `[vitest] The provider was closed.` )
739793 }
740794 }
@@ -752,7 +806,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
752806 } as any // overloaded CDPSession type is too tricky in monorepo
753807 }
754808
755- async close ( ) : Promise < void > {
809+ async close ( options ?: BrowserProviderCloseOptions ) : Promise < void > {
756810 process . off ( 'SIGTERM' , this . onSIGTERM )
757811
758812 debug ?.( '[%s] closing provider' , this . browserName )
@@ -768,6 +822,28 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
768822 }
769823 const browser = this . browser
770824 this . browser = null
825+ const closed = this . closeContexts ( )
826+ if ( options ?. keepWarm && browser && this . launchOptionsJson && browser . isConnected ( ) ) {
827+ // registered before the contexts are closed, so the project that takes
828+ // over the pool slot finds the browser right away and waits for it
829+ const kept = closed . then (
830+ ( ) => browser ,
831+ async ( error ) => {
832+ await browser . close ( ) . catch ( ( ) => { } )
833+ throw error
834+ } ,
835+ )
836+ keepWarmBrowser ( this . project , kept , this . launchOptionsJson )
837+ await kept
838+ debug ?.( '[%s] provider is closed, the browser is kept warm' , this . browserName )
839+ return
840+ }
841+ await closed
842+ await browser ?. close ( )
843+ debug ?.( '[%s] provider is closed' , this . browserName )
844+ }
845+
846+ private async closeContexts ( ) : Promise < void > {
771847 await Promise . all ( Array . from ( this . pages . values ( ) , ( p ) => p . close ( ) ) )
772848 this . pages . clear ( )
773849 if ( this . persistentContext ) {
@@ -776,8 +852,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
776852 await Promise . all ( Array . from ( this . contexts . values ( ) , ( c ) => c . close ( ) ) )
777853 }
778854 this . contexts . clear ( )
779- await browser ?. close ( )
780- debug ?.( '[%s] provider is closed' , this . browserName )
781855 }
782856}
783857
0 commit comments