Skip to content

Commit 14a217d

Browse files
authored
fix(browser): make userEvent more stable when running in parallel (#5974)
1 parent 49e973c commit 14a217d

File tree

14 files changed

+50
-31
lines changed

14 files changed

+50
-31
lines changed

docs/guide/browser.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -953,15 +953,16 @@ Custom functions will override built-in ones if they have the same name.
953953
Vitest exposes several `playwright` specific properties on the command context.
954954

955955
- `page` references the full page that contains the test iframe. This is the orchestrator HTML and you most likely shouldn't touch it to not break things.
956-
- `frame` is the tester [iframe instance](https://playwright.dev/docs/api/class-frame). It has a simillar API to the page, but it doesn't support certain methods.
956+
- `frame` is an async method that will resolve tester [`Frame`](https://playwright.dev/docs/api/class-frame). It has a simillar API to the `page`, but it doesn't support certain methods. If you need to query an element, you should prefer using `context.iframe` instead because it is more stable and faster.
957+
- `iframe` is a [`FrameLocator`](https://playwright.dev/docs/api/class-framelocator) that should be used to query other elements on the page.
957958
- `context` refers to the unique [BrowserContext](https://playwright.dev/docs/api/class-browsercontext).
958959

959960
```ts
960961
import { defineCommand } from '@vitest/browser'
961962

962963
export const myCommand = defineCommand(async (ctx, arg1, arg2) => {
963964
if (ctx.provider.name === 'playwright') {
964-
const element = await ctx.frame.findByRole('alert')
965+
const element = await ctx.iframe.findByRole('alert')
965966
const screenshot = await element.screenshot()
966967
// do something with the screenshot
967968
return difference

packages/browser/providers/playwright.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
BrowserContext,
33
BrowserContextOptions,
44
Frame,
5+
FrameLocator,
56
LaunchOptions,
67
Page,
78
CDPSession
@@ -20,7 +21,8 @@ declare module 'vitest/node' {
2021

2122
export interface BrowserCommandContext {
2223
page: Page
23-
frame: Frame
24+
frame(): Promise<Frame>
25+
iframe: FrameLocator
2426
context: BrowserContext
2527
}
2628
}

packages/browser/src/node/cdp.ts

-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ export class BrowserServerCDPHandler {
1515
return this.session.send(method, params)
1616
}
1717

18-
detach() {
19-
return this.session.detach()
20-
}
21-
2218
on(event: string, id: string, once = false) {
2319
if (!this.listenerIds[event]) {
2420
this.listenerIds[event] = []

packages/browser/src/node/commands/clear.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export const clear: UserEventCommand<UserEvent['clear']> = async (
88
xpath,
99
) => {
1010
if (context.provider instanceof PlaywrightBrowserProvider) {
11-
const { frame } = context
12-
const element = frame.locator(`xpath=${xpath}`)
11+
const { iframe } = context
12+
const element = iframe.locator(`xpath=${xpath}`)
1313
await element.clear({
1414
timeout: 1000,
1515
})

packages/browser/src/node/commands/click.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const click: UserEventCommand<UserEvent['click']> = async (
1010
) => {
1111
const provider = context.provider
1212
if (provider instanceof PlaywrightBrowserProvider) {
13-
const tester = context.frame
13+
const tester = context.iframe
1414
await tester.locator(`xpath=${xpath}`).click({
1515
timeout: 1000,
1616
...options,
@@ -33,7 +33,7 @@ export const dblClick: UserEventCommand<UserEvent['dblClick']> = async (
3333
) => {
3434
const provider = context.provider
3535
if (provider instanceof PlaywrightBrowserProvider) {
36-
const tester = context.frame
36+
const tester = context.iframe
3737
await tester.locator(`xpath=${xpath}`).dblclick(options)
3838
}
3939
else if (provider instanceof WebdriverBrowserProvider) {

packages/browser/src/node/commands/dragAndDrop.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async (
1010
options,
1111
) => {
1212
if (context.provider instanceof PlaywrightBrowserProvider) {
13-
await context.frame.dragAndDrop(
13+
const frame = await context.frame()
14+
await frame.dragAndDrop(
1415
`xpath=${source}`,
1516
`xpath=${target}`,
1617
{

packages/browser/src/node/commands/fill.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const fill: UserEventCommand<UserEvent['fill']> = async (
1010
options = {},
1111
) => {
1212
if (context.provider instanceof PlaywrightBrowserProvider) {
13-
const { frame } = context
14-
const element = frame.locator(`xpath=${xpath}`)
13+
const { iframe } = context
14+
const element = iframe.locator(`xpath=${xpath}`)
1515
await element.fill(text, { timeout: 1000, ...options })
1616
}
1717
else if (context.provider instanceof WebdriverBrowserProvider) {

packages/browser/src/node/commands/hover.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const hover: UserEventCommand<UserEvent['hover']> = async (
99
options = {},
1010
) => {
1111
if (context.provider instanceof PlaywrightBrowserProvider) {
12-
await context.frame.locator(`xpath=${xpath}`).hover({
12+
await context.iframe.locator(`xpath=${xpath}`).hover({
1313
timeout: 1000,
1414
...options,
1515
})

packages/browser/src/node/commands/keyboard.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export const keyboard: UserEventCommand<UserEvent['keyboard']> = async (
2121
}
2222

2323
if (context.provider instanceof PlaywrightBrowserProvider) {
24-
await context.frame.evaluate(focusIframe)
24+
const frame = await context.frame()
25+
await frame.evaluate(focusIframe)
2526
}
2627
else if (context.provider instanceof WebdriverBrowserProvider) {
2728
await context.browser.execute(focusIframe)
@@ -39,7 +40,8 @@ export const keyboard: UserEventCommand<UserEvent['keyboard']> = async (
3940
}
4041
}
4142
if (context.provider instanceof PlaywrightBrowserProvider) {
42-
await context.frame.evaluate(selectAll)
43+
const frame = await context.frame()
44+
await frame.evaluate(selectAll)
4345
}
4446
else if (context.provider instanceof WebdriverBrowserProvider) {
4547
await context.browser.execute(selectAll)

packages/browser/src/node/commands/screenshot.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async (
2828
if (context.provider instanceof PlaywrightBrowserProvider) {
2929
if (options.element) {
3030
const { element: elementXpath, ...config } = options
31-
const iframe = context.frame
32-
const element = iframe.locator(`xpath=${elementXpath}`)
31+
const element = context.iframe.locator(`xpath=${elementXpath}`)
3332
await element.screenshot({ ...config, path: savePath })
3433
}
3534
else {
36-
await context.frame.locator('body').screenshot({ ...options, path: savePath })
35+
await context.iframe.locator('body').screenshot({
36+
...options,
37+
path: savePath,
38+
})
3739
}
3840
return path
3941
}

packages/browser/src/node/commands/select.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async
1212
) => {
1313
if (context.provider instanceof PlaywrightBrowserProvider) {
1414
const value = userValues as any as (string | { element: string })[]
15-
const { frame } = context
16-
const selectElement = frame.locator(`xpath=${xpath}`)
15+
const { iframe } = context
16+
const selectElement = iframe.locator(`xpath=${xpath}`)
1717

1818
const values = await Promise.all(value.map(async (v) => {
1919
if (typeof v === 'string') {
2020
return v
2121
}
22-
const elementHandler = await frame.locator(`xpath=${v.element}`).elementHandle()
22+
const elementHandler = await iframe.locator(`xpath=${v.element}`).elementHandle()
2323
if (!elementHandler) {
2424
throw new Error(`Element not found: ${v.element}`)
2525
}

packages/browser/src/node/commands/type.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const type: UserEventCommand<UserEvent['type']> = async (
1313
const { skipClick = false, skipAutoClose = false } = options
1414

1515
if (context.provider instanceof PlaywrightBrowserProvider) {
16-
const { frame } = context
17-
const element = frame.locator(`xpath=${xpath}`)
16+
const { iframe } = context
17+
const element = iframe.locator(`xpath=${xpath}`)
1818

1919
if (!skipClick) {
2020
await element.focus()

packages/browser/src/node/providers/playwright.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type {
22
Browser,
3+
34
BrowserContext,
45
BrowserContextOptions,
6+
Frame,
57
LaunchOptions,
68
Page,
79
} from 'playwright'
@@ -105,8 +107,25 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
105107
return {
106108
page,
107109
context: this.contexts.get(contextId)!,
108-
get frame() {
109-
return page.frame('vitest-iframe')!
110+
frame() {
111+
return new Promise<Frame>((resolve, reject) => {
112+
const frame = page.frame('vitest-iframe')
113+
if (frame) {
114+
return resolve(frame)
115+
}
116+
117+
const timeout = setTimeout(() => {
118+
const err = new Error(`Cannot find "vitest-iframe" on the page. This is a bug in Vitest, please report it.`)
119+
reject(err)
120+
}, 1000)
121+
page.on('frameattached', (frame) => {
122+
clearTimeout(timeout)
123+
resolve(frame)
124+
})
125+
})
126+
},
127+
get iframe() {
128+
return page.frameLocator('[data-vitest="true"]')!
110129
},
111130
}
112131
}
@@ -147,9 +166,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
147166
once(event: string, listener: (...args: any[]) => void) {
148167
cdp.once(event as 'Accessibility.loadComplete', listener)
149168
},
150-
detach() {
151-
return cdp.detach()
152-
},
153169
}
154170
}
155171

packages/vitest/src/types/browser.ts

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface CDPSession {
1414
on: (event: string, listener: (...args: unknown[]) => void) => void
1515
once: (event: string, listener: (...args: unknown[]) => void) => void
1616
off: (event: string, listener: (...args: unknown[]) => void) => void
17-
detach: () => Promise<void>
1817
}
1918

2019
export interface BrowserProvider {

0 commit comments

Comments
 (0)