Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 65 additions & 20 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export class IframeOrchestrator {
private recreateNonIsolatedIframe = false
private iframes = new Map<string, HTMLIFrameElement>()

public eventTarget: EventTarget = new EventTarget()

constructor() {
debug('init orchestrator', getBrowserState().sessionId)

Expand Down Expand Up @@ -122,6 +124,7 @@ export class IframeOrchestrator {
method: options.method,
context: options.providedContext,
})
debug('finished running tests', options.files.join(', '))
// we don't cleanup here because in non-isolated mode
// it is done after all tests finished running
}
Expand Down Expand Up @@ -156,34 +159,65 @@ export class IframeOrchestrator {
})
}

private dispatchIframeError(error: Error) {
const event = new CustomEvent('iframeerror', { detail: error })
this.eventTarget.dispatchEvent(event)
return error
}

private async prepareIframe(container: HTMLDivElement, iframeId: string, startTime: number) {
const iframe = this.createTestIframe(iframeId)
container.appendChild(iframe)

await new Promise<void>((resolve, reject) => {
iframe.onload = () => {
this.iframes.set(iframeId, iframe)
sendEventToIframe({
event: 'prepare',
iframeId,
startTime,
}).then(resolve, reject)
const href = this.getIframeHref(iframe)
debug('iframe loaded with href', href)
if (href !== iframe.src) {
reject(this.dispatchIframeError(new Error(
`Cannot connect to the iframe. `
+ `Did you change the location or submitted a form? `
+ 'If so, don\'t forget to call `event.preventDefault()` to avoid reloading the page.\n\n'
+ `Received URL: ${href || 'unknown'}\nExpected: ${iframe.src}`,
)))
}
else {
this.iframes.set(iframeId, iframe)
sendEventToIframe({
event: 'prepare',
iframeId,
startTime,
}).then(resolve, error => reject(this.dispatchIframeError(error)))
}
}
iframe.onerror = (e) => {
if (typeof e === 'string') {
reject(new Error(e))
reject(this.dispatchIframeError(new Error(e)))
}
else if (e instanceof ErrorEvent) {
reject(e.error)
reject(this.dispatchIframeError(e.error))
}
else {
reject(new Error(`Cannot load the iframe ${iframeId}.`))
reject(this.dispatchIframeError(new Error(`Cannot load the iframe ${iframeId}.`)))
}
}
})
return iframe
}

private getIframeHref(iframe: HTMLIFrameElement) {
try {
// same origin iframe has contentWindow
// same origin trusted iframe (where tests can run)
// also allows accessing "location"
return iframe.contentWindow?.location.href
}
catch {
// looks like this iframe is not a tester.html
return undefined
}
}

private createTestIframe(iframeId: string) {
const iframe = document.createElement('iframe')
const src = `/?sessionId=${getBrowserState().sessionId}&iframeId=${iframeId}`
Expand Down Expand Up @@ -257,7 +291,8 @@ export class IframeOrchestrator {
}
}

getBrowserState().orchestrator = new IframeOrchestrator()
const orchestrator = new IframeOrchestrator()
getBrowserState().orchestrator = orchestrator

async function getContainer(config: SerializedConfig): Promise<HTMLDivElement> {
if (config.browser.ui) {
Expand All @@ -276,16 +311,26 @@ async function getContainer(config: SerializedConfig): Promise<HTMLDivElement> {

async function sendEventToIframe(event: IframeChannelOutgoingEvent) {
channel.postMessage(event)
return new Promise<void>((resolve) => {
channel.addEventListener(
'message',
function handler(e) {
if (e.data.iframeId === event.iframeId && e.data.event === `response:${event.event}`) {
resolve()
channel.removeEventListener('message', handler)
}
},
)
return new Promise<void>((resolve, reject) => {
function cleanupEvents() {
channel.removeEventListener('message', onReceived)
orchestrator.eventTarget.removeEventListener('iframeerror', onError)
}

function onReceived(e: MessageEvent) {
if (e.data.iframeId === event.iframeId && e.data.event === `response:${event.event}`) {
resolve()
cleanupEvents()
}
}

function onError(e: Event) {
reject((e as CustomEvent).detail)
cleanupEvents()
}

orchestrator.eventTarget.addEventListener('iframeerror', onError)
channel.addEventListener('message', onReceived)
})
}

Expand Down
12 changes: 12 additions & 0 deletions test/browser/fixtures/broken-iframe/submit-form.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { userEvent } from '@vitest/browser/context';
import { test } from 'vitest';

test('submitting a form reloads the iframe with "?" query', async () => {
const form = document.createElement('form')
document.body.append(form)
form.id = 'form'
const button = document.createElement('button')
button.id = 'button'
form.append(button)
await userEvent.click(button)
})
15 changes: 15 additions & 0 deletions test/browser/fixtures/broken-iframe/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { provider, instances } from '../../settings'

export default defineConfig({
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
test: {
browser: {
enabled: true,
provider,
instances,
headless: true,
},
},
})
1 change: 1 addition & 0 deletions test/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"test-different-configs": "vitest --root ./fixtures/multiple-different-configs",
"test-setup-file": "vitest --root ./fixtures/setup-file",
"test-snapshots": "vitest --root ./fixtures/update-snapshot",
"test-broken-iframe": "vitest --root ./fixtures/broken-iframe",
"coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes",
"test:browser:preview": "PROVIDER=preview vitest",
"test:browser:playwright": "PROVIDER=playwright vitest",
Expand Down
23 changes: 23 additions & 0 deletions test/browser/specs/bail-out.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect, test } from 'vitest'
import { runBrowserTests } from './utils'

test('fails gracefully when browser crashes', async () => {
const { stderr } = await runBrowserTests({
root: './fixtures/browser-crash',
reporters: [['verbose', { isTTY: false }]],
})

expect(stderr).toContain('Browser connection was closed while running tests. Was the page closed unexpectedly?')
})

test('vitest bails out when the iframe is no longer accessible', async () => {
const { stderr } = await runBrowserTests({
root: './fixtures/broken-iframe',
reporters: [['verbose', { isTTY: false }]],
}, [], {}, { fails: true })
expect(stderr).toContain(
'Cannot connect to the iframe. Did you change the location or submitted a form? If so, don\'t forget to call `event.preventDefault()` to avoid reloading the page.',
)
expect(stderr).toContain('Received URL: http://')
expect(stderr).toContain('Expected: http://')
})
11 changes: 0 additions & 11 deletions test/browser/specs/browser-crash.test.ts

This file was deleted.

Loading