Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fbc626c

Browse filesBrowse files
fix(browser): wait for iframe tester readiness before preparing (#10497) [backport to v4] (#10556)
Co-authored-by: Séamus O'Connor <seamus.oconnor@seeq.com>
1 parent 7fb2965 commit fbc626c
Copy full SHA for fbc626c

4 files changed

+119-12Lines changed: 119 additions & 12 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/browser/src/client/channel.ts‎

Copy file name to clipboardExpand all lines: packages/browser/src/client/channel.ts
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export interface IframeViewportDoneEvent {
2020
iframeId: string
2121
}
2222

23+
export interface IframeReadyEvent {
24+
event: 'ready'
25+
iframeId: string
26+
}
27+
2328
export interface GlobalChannelTestRunCanceledEvent {
2429
type: 'cancel'
2530
reason: CancelReason
@@ -49,6 +54,7 @@ export type GlobalChannelIncomingEvent = GlobalChannelTestRunCanceledEvent
4954

5055
export type IframeChannelIncomingEvent
5156
= | IframeViewportEvent
57+
| IframeReadyEvent
5258

5359
export type IframeChannelOutgoingEvent
5460
= | IframeExecuteEvent
Collapse file

‎packages/browser/src/client/orchestrator.ts‎

Copy file name to clipboardExpand all lines: packages/browser/src/client/orchestrator.ts
+48-12Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Context as OTELContext } from '@opentelemetry/api'
2-
import type { GlobalChannelIncomingEvent, IframeChannelIncomingEvent, IframeChannelOutgoingEvent, IframeViewportDoneEvent, IframeViewportFailEvent } from '@vitest/browser/client'
2+
import type { GlobalChannelIncomingEvent, IframeChannelEvent, IframeChannelOutgoingEvent, IframeViewportDoneEvent, IframeViewportFailEvent } from '@vitest/browser/client'
33
import type { FileSpecification } from '@vitest/runner'
44
import type { BrowserTesterOptions, SerializedConfig } from 'vitest'
55
import { channel, client, globalChannel } from '@vitest/browser/client'
@@ -15,6 +15,8 @@ export class IframeOrchestrator {
1515
private cancelled = false
1616
private recreateNonIsolatedIframe = false
1717
private iframes = new Map<string, HTMLIFrameElement>()
18+
private readyIframes = new Set<string>()
19+
private readyWaiters = new Map<string, () => void>()
1820

1921
public eventTarget: EventTarget = new EventTarget()
2022

@@ -91,6 +93,8 @@ export class IframeOrchestrator {
9193

9294
this.iframes.forEach(iframe => iframe.remove())
9395
this.iframes.clear()
96+
this.readyIframes.clear()
97+
this.readyWaiters.clear()
9498

9599
for (let i = 0; i < options.files.length; i++) {
96100
if (this.cancelled) {
@@ -149,8 +153,7 @@ export class IframeOrchestrator {
149153
// because we called "cleanup" in the previous run
150154
// the iframe is not removed immediately to let the user see the last test
151155
this.recreateNonIsolatedIframe = false
152-
this.iframes.get(ID_ALL)!.remove()
153-
this.iframes.delete(ID_ALL)
156+
this.removeIframe(ID_ALL)
154157
debug('recreate non-isolated iframe')
155158
}
156159

@@ -190,8 +193,7 @@ export class IframeOrchestrator {
190193
const file = spec.filepath
191194

192195
if (this.iframes.has(file)) {
193-
this.iframes.get(file)!.remove()
194-
this.iframes.delete(file)
196+
this.removeIframe(file)
195197
}
196198

197199
const iframe = await this.prepareIframe(
@@ -254,12 +256,14 @@ export class IframeOrchestrator {
254256
}
255257
else {
256258
this.iframes.set(iframeId, iframe)
257-
this.sendEventToIframe({
258-
event: 'prepare',
259-
iframeId,
260-
startTime,
261-
otelCarrier: this.traces.getContextCarrier(otelContext),
262-
}).then(resolve, error => reject(this.dispatchIframeError(error)))
259+
this.waitForReady(iframeId)
260+
.then(() => this.sendEventToIframe({
261+
event: 'prepare',
262+
iframeId,
263+
startTime,
264+
otelCarrier: this.traces.getContextCarrier(otelContext),
265+
}))
266+
.then(resolve, error => reject(this.dispatchIframeError(error)))
263267
}
264268
}
265269
iframe.onerror = (e) => {
@@ -277,6 +281,34 @@ export class IframeOrchestrator {
277281
return iframe
278282
}
279283

284+
private markReady(iframeId: string) {
285+
this.readyIframes.add(iframeId)
286+
287+
const waiter = this.readyWaiters.get(iframeId)
288+
if (waiter) {
289+
this.readyWaiters.delete(iframeId)
290+
waiter()
291+
}
292+
}
293+
294+
private waitForReady(iframeId: string): Promise<void> {
295+
if (this.readyIframes.has(iframeId)) {
296+
return Promise.resolve()
297+
}
298+
299+
return new Promise((resolve) => {
300+
this.readyWaiters.set(iframeId, resolve)
301+
})
302+
}
303+
304+
private removeIframe(iframeId: string) {
305+
const iframe = this.iframes.get(iframeId)
306+
this.iframes.delete(iframeId)
307+
this.readyIframes.delete(iframeId)
308+
this.readyWaiters.delete(iframeId)
309+
iframe?.remove()
310+
}
311+
280312
private loggedIframe = new WeakSet<HTMLIFrameElement>()
281313

282314
private createWarningMessage(iframeId: string, location: string) {
@@ -342,9 +374,13 @@ export class IframeOrchestrator {
342374
}
343375
}
344376

345-
private async onIframeEvent(e: MessageEvent<IframeChannelIncomingEvent>) {
377+
private async onIframeEvent(e: MessageEvent<IframeChannelEvent>) {
346378
debug('iframe event', JSON.stringify(e.data))
347379
switch (e.data.event) {
380+
case 'ready': {
381+
this.markReady(e.data.iframeId)
382+
break
383+
}
348384
case 'viewport': {
349385
const { width, height, iframeId: id } = e.data
350386
const iframe = this.iframes.get(id)
Collapse file

‎packages/browser/src/client/tester/tester.ts‎

Copy file name to clipboardExpand all lines: packages/browser/src/client/tester/tester.ts
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ getBrowserState().commands = commands
111111
getBrowserState().activeTraceTaskIds = new Set()
112112
getBrowserState().iframeId = iframeId
113113

114+
channel.postMessage({
115+
event: 'ready',
116+
iframeId,
117+
})
118+
114119
let contextSwitched = false
115120

116121
async function prepareTestEnvironment(options: PrepareOptions) {
Collapse file
+60Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect, test } from 'vitest'
2+
import { instances, runInlineBrowserTests } from './utils'
3+
4+
test('prepare waits until the tester can receive browser channel events', { timeout: 5000 }, async () => {
5+
const { stderr, testTree } = await runInlineBrowserTests(
6+
{
7+
'basic.test.ts': `
8+
import { expect, test } from 'vitest'
9+
10+
test('runs in the browser', () => {
11+
expect(1).toBe(1)
12+
})
13+
`,
14+
'delayed-tester.html': `
15+
<!DOCTYPE html>
16+
<html lang="en">
17+
<head>
18+
<meta charset="UTF-8" />
19+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
20+
<title>Delayed Tester</title>
21+
<script>
22+
const addEventListener = BroadcastChannel.prototype.addEventListener
23+
const postMessage = BroadcastChannel.prototype.postMessage
24+
BroadcastChannel.prototype.addEventListener = function(type, listener, options) {
25+
if (type === 'message') {
26+
setTimeout(() => addEventListener.call(this, type, listener, options), 100)
27+
return
28+
}
29+
return addEventListener.call(this, type, listener, options)
30+
}
31+
BroadcastChannel.prototype.postMessage = function(message) {
32+
if (message && message.event === 'ready') {
33+
setTimeout(() => postMessage.call(this, message), 150)
34+
return
35+
}
36+
return postMessage.call(this, message)
37+
}
38+
</script>
39+
</head>
40+
<body></body>
41+
</html>
42+
`,
43+
},
44+
{
45+
browser: {
46+
instances: [instances[0]],
47+
testerHtmlPath: './delayed-tester.html',
48+
},
49+
},
50+
)
51+
52+
expect(stderr).toBe('')
53+
expect(testTree()).toMatchInlineSnapshot(`
54+
{
55+
"basic.test.ts": {
56+
"runs in the browser": "passed",
57+
},
58+
}
59+
`)
60+
})

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.