@@ -124,7 +124,7 @@ export class IframeOrchestrator {
124124 if ( ! iframe ) {
125125 return
126126 }
127- await sendEventToIframe ( {
127+ await this . sendEventToIframe ( {
128128 event : 'cleanup' ,
129129 iframeId : ID_ALL ,
130130 } )
@@ -158,7 +158,7 @@ export class IframeOrchestrator {
158158
159159 await setIframeViewport ( iframe , width , height )
160160 debug ( 'run non-isolated tests' , options . files . join ( ', ' ) )
161- await sendEventToIframe ( {
161+ await this . sendEventToIframe ( {
162162 event : 'execute' ,
163163 iframeId : ID_ALL ,
164164 files : options . files ,
@@ -195,15 +195,15 @@ export class IframeOrchestrator {
195195 )
196196 await setIframeViewport ( iframe , width , height )
197197 // running tests after the "prepare" event
198- await sendEventToIframe ( {
198+ await this . sendEventToIframe ( {
199199 event : 'execute' ,
200200 files : [ spec ] ,
201201 method : options . method ,
202202 iframeId : file ,
203203 context : options . providedContext ,
204204 } )
205205 // perform "cleanup" to cleanup resources and calculate the coverage
206- await sendEventToIframe ( {
206+ await this . sendEventToIframe ( {
207207 event : 'cleanup' ,
208208 iframeId : file ,
209209 } )
@@ -233,12 +233,21 @@ export class IframeOrchestrator {
233233 `Cannot connect to the iframe. `
234234 + `Did you change the location or submitted a form? `
235235 + 'If so, don\'t forget to call `event.preventDefault()` to avoid reloading the page.\n\n'
236- + `Received URL: ${ href || 'unknown' } \nExpected: ${ iframe . src } ` ,
236+ + `Received URL: ${ href || 'unknown due to CORS ' } \nExpected: ${ iframe . src } ` ,
237237 ) ) )
238238 }
239+ else if ( this . iframes . has ( iframeId ) ) {
240+ const events = this . iframeEvents . get ( iframe )
241+ if ( events ?. size ) {
242+ this . dispatchIframeError ( new Error ( this . createWarningMessage ( iframeId , 'during a test' ) ) )
243+ }
244+ else {
245+ this . warnReload ( iframe , iframeId )
246+ }
247+ }
239248 else {
240249 this . iframes . set ( iframeId , iframe )
241- sendEventToIframe ( {
250+ this . sendEventToIframe ( {
242251 event : 'prepare' ,
243252 iframeId,
244253 startTime,
@@ -261,6 +270,32 @@ export class IframeOrchestrator {
261270 return iframe
262271 }
263272
273+ private loggedIframe = new WeakSet < HTMLIFrameElement > ( )
274+
275+ private createWarningMessage ( iframeId : string , location : string ) {
276+ return `The iframe${ iframeId === ID_ALL ? '' : ` for "${ iframeId } "` } was reloaded ${ location } . `
277+ + `This can lead to unexpected behavior during tests, duplicated test results or tests hanging.\n\n`
278+ + `Make sure that your test code does not change window's location, submit forms without preventing default behavior, or imports unoptimized dependencies.\n`
279+ + `If you are using a framework that manipulates browser history (like React Router), consider using memory-based routing for tests. `
280+ + `If you think this is a false positive, open an issue with a reproduction: https://github.com/vitest-dev/vitest/issues/new`
281+ }
282+
283+ private warnReload ( iframe : HTMLIFrameElement , iframeId : string ) {
284+ if ( this . loggedIframe . has ( iframe ) ) {
285+ return
286+ }
287+ this . loggedIframe . add ( iframe )
288+ const message = `\x1B[41m WARNING \x1B[49m ${ this . createWarningMessage ( iframeId , 'multiple times' ) } `
289+
290+ client . rpc . sendLog ( 'run' , {
291+ type : 'stderr' ,
292+ time : Date . now ( ) ,
293+ content : message ,
294+ size : message . length ,
295+ taskId : iframeId === ID_ALL ? undefined : generateFileId ( iframeId ) ,
296+ } ) . catch ( ( ) => { /* ignore */ } )
297+ }
298+
264299 private getIframeHref ( iframe : HTMLIFrameElement ) {
265300 try {
266301 // same origin iframe has contentWindow
@@ -345,6 +380,46 @@ export class IframeOrchestrator {
345380 }
346381 }
347382 }
383+
384+ private iframeEvents = new WeakMap < HTMLIFrameElement , Set < string > > ( )
385+
386+ private async sendEventToIframe ( event : IframeChannelOutgoingEvent ) : Promise < void > {
387+ const iframe = this . iframes . get ( event . iframeId )
388+ if ( ! iframe ) {
389+ throw new Error ( `Cannot find iframe with id ${ event . iframeId } ` )
390+ }
391+ let events = this . iframeEvents . get ( iframe )
392+ if ( ! events ) {
393+ events = new Set ( )
394+ this . iframeEvents . set ( iframe , events )
395+ }
396+ events . add ( event . event )
397+
398+ channel . postMessage ( event )
399+ return new Promise < void > ( ( resolve , reject ) => {
400+ const cleanupEvents = ( ) => {
401+ channel . removeEventListener ( 'message' , onReceived )
402+ this . eventTarget . removeEventListener ( 'iframeerror' , onError )
403+ }
404+
405+ function onReceived ( e : MessageEvent ) {
406+ if ( e . data . iframeId === event . iframeId && e . data . event === `response:${ event . event } ` ) {
407+ resolve ( )
408+ cleanupEvents ( )
409+ events ! . delete ( event . event )
410+ }
411+ }
412+
413+ function onError ( e : Event ) {
414+ reject ( ( e as CustomEvent ) . detail )
415+ cleanupEvents ( )
416+ events ! . delete ( event . event )
417+ }
418+
419+ this . eventTarget . addEventListener ( 'iframeerror' , onError )
420+ channel . addEventListener ( 'message' , onReceived )
421+ } )
422+ }
348423}
349424
350425const orchestrator = new IframeOrchestrator ( )
@@ -365,31 +440,6 @@ async function getContainer(config: SerializedConfig): Promise<HTMLDivElement> {
365440 return document . querySelector ( '#vitest-tester' ) as HTMLDivElement
366441}
367442
368- async function sendEventToIframe ( event : IframeChannelOutgoingEvent ) {
369- channel . postMessage ( event )
370- return new Promise < void > ( ( resolve , reject ) => {
371- function cleanupEvents ( ) {
372- channel . removeEventListener ( 'message' , onReceived )
373- orchestrator . eventTarget . removeEventListener ( 'iframeerror' , onError )
374- }
375-
376- function onReceived ( e : MessageEvent ) {
377- if ( e . data . iframeId === event . iframeId && e . data . event === `response:${ event . event } ` ) {
378- resolve ( )
379- cleanupEvents ( )
380- }
381- }
382-
383- function onError ( e : Event ) {
384- reject ( ( e as CustomEvent ) . detail )
385- cleanupEvents ( )
386- }
387-
388- orchestrator . eventTarget . addEventListener ( 'iframeerror' , onError )
389- channel . addEventListener ( 'message' , onReceived )
390- } )
391- }
392-
393443function generateFileId ( file : string ) {
394444 const config = getConfig ( )
395445 const path = relative ( config . root , file )
0 commit comments