Skip to content

Commit 573cb16

Browse files
authored
ci: fix flaky browser tests (#7887)
1 parent 03660f9 commit 573cb16

26 files changed

+235
-78
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { UserEventUploadOptions } from '@vitest/browser/context'
12
import type { UserEventCommand } from './utils'
23
import { dirname, resolve } from 'pathe'
34
import { PlaywrightBrowserProvider } from '../providers/playwright'
@@ -7,10 +8,11 @@ export const upload: UserEventCommand<(element: string, files: Array<string | {
78
name: string
89
mimeType: string
910
base64: string
10-
}>) => void> = async (
11+
}>, options: UserEventUploadOptions) => void> = async (
1112
context,
1213
selector,
1314
files,
15+
options,
1416
) => {
1517
const testPath = context.testPath
1618
if (!testPath) {
@@ -30,7 +32,7 @@ export const upload: UserEventCommand<(element: string, files: Array<string | {
3032
buffer: Buffer.from(file.base64, 'base64'),
3133
}
3234
})
33-
await iframe.locator(selector).setInputFiles(playwrightFiles as string[])
35+
await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options)
3436
}
3537
else if (context.provider instanceof WebdriverBrowserProvider) {
3638
for (const file of files) {

packages/browser/src/node/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export async function createBrowserServer(
4545
const vite = await createViteServer({
4646
...project.options, // spread project config inlined in root workspace config
4747
base: '/',
48+
root: project.config.root,
4849
logLevel,
4950
customLogger: {
5051
...logger,

packages/browser/src/node/pool.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
8282
return
8383
}
8484

85+
debug?.('provider is ready for %s project', project.name)
86+
8587
const pool = ensurePool(project)
8688
vitest.state.clearFiles(project, files)
8789
providers.add(project.browser!.provider)
@@ -112,12 +114,14 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
112114
name: 'browser',
113115
async close() {
114116
await Promise.all([...providers].map(provider => provider.close()))
117+
vitest._browserSessions.sessionIds.clear()
115118
providers.clear()
116119
vitest.projects.forEach((project) => {
117120
project.browser?.state.orchestrators.forEach((orchestrator) => {
118121
orchestrator.$close()
119122
})
120123
})
124+
debug?.('browser pool closed all providers')
121125
},
122126
runTests: files => runWorkspaceTests('run', files),
123127
collectTests: files => runWorkspaceTests('collect', files),
@@ -161,6 +165,7 @@ class BrowserPool {
161165
this._promise ??= createDefer<void>()
162166

163167
if (!files.length) {
168+
debug?.('no tests found, finishing test run immediately')
164169
this._promise.resolve()
165170
return this._promise
166171
}
@@ -177,6 +182,7 @@ class BrowserPool {
177182
})
178183

179184
if (this.orchestrators.size >= this.options.maxWorkers) {
185+
debug?.('all orchestrators are ready, not creating more')
180186
return this._promise
181187
}
182188

@@ -190,13 +196,17 @@ class BrowserPool {
190196
const promises: Promise<void>[] = []
191197
for (let i = 0; i < workerCount; i++) {
192198
const sessionId = crypto.randomUUID()
199+
this.project.vitest._browserSessions.sessionIds.add(sessionId)
200+
const project = this.project.name
201+
debug?.('[%s] creating session for %s', sessionId, project)
193202
const page = this.openPage(sessionId).then(() => {
194203
// start running tests on the page when it's ready
195204
this.runNextTest(method, sessionId)
196205
})
197206
promises.push(page)
198207
}
199208
await Promise.all(promises)
209+
debug?.('all sessions are created')
200210
return this._promise
201211
}
202212

@@ -230,7 +240,14 @@ class BrowserPool {
230240
if (this.readySessions.size === this.orchestrators.size) {
231241
this._promise?.resolve()
232242
this._promise = undefined
233-
debug?.('all tests finished running')
243+
debug?.('[%s] all tests finished running', sessionId)
244+
}
245+
else {
246+
debug?.(
247+
`did not finish sessions for ${sessionId}: |ready - %s| |overall - %s|`,
248+
[...this.readySessions].join(', '),
249+
[...this.orchestrators.keys()].join(', '),
250+
)
234251
}
235252
}
236253

@@ -288,6 +305,7 @@ class BrowserPool {
288305
this.cancel()
289306
this._promise?.resolve()
290307
this._promise = undefined
308+
debug?.('[%s] browser connection was closed', sessionId)
291309
return
292310
}
293311
debug?.('[%s] error during %s test run: %s', sessionId, file, error)

packages/browser/src/node/projectParent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class ParentBrowserProject {
7979
if (mod) {
8080
return id
8181
}
82-
const resolvedPath = resolve(project.config.root, id.slice(1))
82+
const resolvedPath = resolve(this.vite.config.root, id.slice(1))
8383
const modUrl = this.vite.moduleGraph.getModuleById(resolvedPath)
8484
if (modUrl) {
8585
return resolvedPath

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import type {
1818
TestProject,
1919
} from 'vitest/node'
2020
import { createManualModuleSource } from '@vitest/mocker/node'
21+
import { createDebugger } from 'vitest/node'
22+
23+
const debug = createDebugger('vitest:browser:playwright')
2124

2225
export const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const
2326
export type PlaywrightBrowser = (typeof playwrightBrowsers)[number]
@@ -48,6 +51,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
4851

4952
public mocker: BrowserModuleMocker | undefined
5053

54+
private closing = false
55+
5156
getSupportedBrowsers(): readonly string[] {
5257
return playwrightBrowsers
5358
}
@@ -56,18 +61,23 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
5661
project: TestProject,
5762
{ browser, options }: PlaywrightProviderOptions,
5863
): void {
64+
this.closing = false
5965
this.project = project
6066
this.browserName = browser
6167
this.options = options as any
6268
this.mocker = this.createMocker()
6369
}
6470

6571
private async openBrowser() {
72+
await this._throwIfClosing()
73+
6674
if (this.browserPromise) {
75+
debug?.('[%s] the browser is resolving, reusing the promise', this.browserName)
6776
return this.browserPromise
6877
}
6978

7079
if (this.browser) {
80+
debug?.('[%s] the browser is resolved, reusing it', this.browserName)
7181
return this.browser
7282
}
7383

@@ -103,8 +113,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
103113
}
104114
}
105115

106-
const browser = await playwright[this.browserName].launch(launchOptions)
107-
this.browser = browser
116+
debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions)
117+
this.browser = await playwright[this.browserName].launch(launchOptions)
108118
this.browserPromise = null
109119
return this.browser
110120
})()
@@ -243,11 +253,15 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
243253
}
244254

245255
private async createContext(sessionId: string) {
256+
await this._throwIfClosing()
257+
246258
if (this.contexts.has(sessionId)) {
259+
debug?.('[%s][%s] the context already exists, reusing it', sessionId, this.browserName)
247260
return this.contexts.get(sessionId)!
248261
}
249262

250263
const browser = await this.openBrowser()
264+
await this._throwIfClosing(browser)
251265
const { actionTimeout, ...contextOptions } = this.options?.context ?? {}
252266
const options = {
253267
...contextOptions,
@@ -257,9 +271,11 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
257271
options.viewport = null
258272
}
259273
const context = await browser.newContext(options)
274+
await this._throwIfClosing(context)
260275
if (actionTimeout) {
261276
context.setDefaultTimeout(actionTimeout)
262277
}
278+
debug?.('[%s][%s] the context is ready', sessionId, this.browserName)
263279
this.contexts.set(sessionId, context)
264280
return context
265281
}
@@ -306,14 +322,19 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
306322
}
307323

308324
private async openBrowserPage(sessionId: string) {
325+
await this._throwIfClosing()
326+
309327
if (this.pages.has(sessionId)) {
328+
debug?.('[%s][%s] the page already exists, closing the old one', sessionId, this.browserName)
310329
const page = this.pages.get(sessionId)!
311330
await page.close()
312331
this.pages.delete(sessionId)
313332
}
314333

315334
const context = await this.createContext(sessionId)
316335
const page = await context.newPage()
336+
debug?.('[%s][%s] the page is ready', sessionId, this.browserName)
337+
await this._throwIfClosing(page)
317338
this.pages.set(sessionId, page)
318339

319340
if (process.env.VITEST_PW_DEBUG) {
@@ -333,9 +354,24 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
333354
}
334355

335356
async openPage(sessionId: string, url: string, beforeNavigate?: () => Promise<void>): Promise<void> {
357+
debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
336358
const browserPage = await this.openBrowserPage(sessionId)
337359
await beforeNavigate?.()
360+
debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
338361
await browserPage.goto(url, { timeout: 0 })
362+
await this._throwIfClosing(browserPage)
363+
}
364+
365+
private async _throwIfClosing(disposable?: { close: () => Promise<void> }) {
366+
if (this.closing) {
367+
debug?.('[%s] provider was closed, cannot perform the action on %s', this.browserName, String(disposable))
368+
await disposable?.close()
369+
this.pages.clear()
370+
this.contexts.clear()
371+
this.browser = null
372+
this.browserPromise = null
373+
throw new Error(`[vitest] The provider was closed.`)
374+
}
339375
}
340376

341377
async getCDPSession(sessionid: string): Promise<CDPSession> {
@@ -359,13 +395,20 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
359395
}
360396

361397
async close(): Promise<void> {
398+
debug?.('[%s] closing provider', this.browserName)
399+
this.closing = true
362400
const browser = this.browser
363401
this.browser = null
402+
if (this.browserPromise) {
403+
await this.browserPromise
404+
this.browserPromise = null
405+
}
364406
await Promise.all([...this.pages.values()].map(p => p.close()))
365407
this.pages.clear()
366408
await Promise.all([...this.contexts.values()].map(c => c.close()))
367409
this.contexts.clear()
368410
await browser?.close()
411+
debug?.('[%s] provider is closed', this.browserName)
369412
}
370413
}
371414

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type {
44
BrowserProviderInitializationOptions,
55
TestProject,
66
} from 'vitest/node'
7+
import { createDebugger } from 'vitest/node'
8+
9+
const debug = createDebugger('vitest:browser:wdio')
710

811
const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const
912
type WebdriverBrowser = (typeof webdriverBrowsers)[number]
@@ -24,6 +27,8 @@ export class WebdriverBrowserProvider implements BrowserProvider {
2427

2528
private options?: Capabilities.WebdriverIOConfig
2629

30+
private closing = false
31+
2732
getSupportedBrowsers(): readonly string[] {
2833
return webdriverBrowsers
2934
}
@@ -32,6 +37,7 @@ export class WebdriverBrowserProvider implements BrowserProvider {
3237
ctx: TestProject,
3338
{ browser, options }: WebdriverProviderOptions,
3439
): Promise<void> {
40+
this.closing = false
3541
this.project = ctx
3642
this.browserName = browser
3743
this.options = options as Capabilities.WebdriverIOConfig
@@ -71,7 +77,10 @@ export class WebdriverBrowserProvider implements BrowserProvider {
7177
}
7278

7379
async openBrowser(): Promise<WebdriverIO.Browser> {
80+
await this._throwIfClosing('opening the browser')
81+
7482
if (this.browser) {
83+
debug?.('[%s] the browser is already opened, reusing it', this.browserName)
7584
return this.browser
7685
}
7786

@@ -87,12 +96,16 @@ export class WebdriverBrowserProvider implements BrowserProvider {
8796

8897
const { remote } = await import('webdriverio')
8998

90-
// TODO: close everything, if browser is closed from the outside
91-
this.browser = await remote({
99+
const remoteOptions: Capabilities.WebdriverIOConfig = {
92100
...this.options,
93101
logLevel: 'error',
94102
capabilities: this.buildCapabilities(),
95-
})
103+
}
104+
105+
debug?.('[%s] opening the browser with options: %O', this.browserName, remoteOptions)
106+
// TODO: close everything, if browser is closed from the outside
107+
this.browser = await remote(remoteOptions)
108+
await this._throwIfClosing()
96109

97110
return this.browser
98111
}
@@ -134,12 +147,26 @@ export class WebdriverBrowserProvider implements BrowserProvider {
134147
return capabilities
135148
}
136149

137-
async openPage(_sessionId: string, url: string): Promise<void> {
150+
async openPage(sessionId: string, url: string): Promise<void> {
151+
await this._throwIfClosing('creating the browser')
152+
debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
138153
const browserInstance = await this.openBrowser()
154+
debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
139155
await browserInstance.url(url)
156+
await this._throwIfClosing('opening the url')
157+
}
158+
159+
private async _throwIfClosing(action?: string) {
160+
if (this.closing) {
161+
debug?.(`[%s] provider was closed, cannot perform the action${action ? ` ${action}` : ''}`, this.browserName)
162+
await (this.browser?.sessionId ? this.browser?.deleteSession?.() : null)
163+
throw new Error(`[vitest] The provider was closed.`)
164+
}
140165
}
141166

142167
async close(): Promise<void> {
168+
debug?.('[%s] closing provider', this.browserName)
169+
this.closing = true
143170
await Promise.all([
144171
this.browser?.sessionId ? this.browser?.deleteSession?.() : null,
145172
])

packages/browser/src/node/rpc.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
5858
)
5959
}
6060

61+
if (!vitest._browserSessions.sessionIds.has(sessionId)) {
62+
const ids = [...vitest._browserSessions.sessionIds].join(', ')
63+
return error(
64+
new Error(`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`),
65+
)
66+
}
67+
6168
if (type === 'orchestrator') {
6269
const session = vitest._browserSessions.getSession(sessionId)
6370
// it's possible the session was already resolved by the preview provider

packages/browser/src/node/serverOrchestrator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export async function resolveOrchestrator(
2626
return
2727
}
2828

29+
// ignore uknown pages
30+
if (sessionId && sessionId !== 'none' && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
31+
return
32+
}
33+
2934
const injectorJs = typeof globalServer.injectorJs === 'string'
3035
? globalServer.injectorJs
3136
: await globalServer.injectorJs

packages/vitest/src/node/browser/sessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { createDefer } from '@vitest/utils'
55
export class BrowserSessions {
66
private sessions = new Map<string, BrowserServerStateSession>()
77

8+
public sessionIds: Set<string> = new Set()
9+
810
getSession(sessionId: string): BrowserServerStateSession | undefined {
911
return this.sessions.get(sessionId)
1012
}

0 commit comments

Comments
 (0)