Skip to content
Draft
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
3 changes: 3 additions & 0 deletions packages/browser-preview/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export class PreviewBrowserProvider implements BrowserProvider {
}

async openPage(_sessionId: string, url: string): Promise<void> {
if (this.open) {
return
}
this.open = true
if (!this.project.browser) {
throw new Error('Browser is not initialized')
Expand Down
21 changes: 16 additions & 5 deletions packages/browser/src/node/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,38 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke

const sessions = vitest._browserSessions

let connect = true

if (!sessions.sessionIds.has(sessionId)) {
const ids = [...sessions.sessionIds].join(', ')
return error(
console.warn([
`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`,
'Close old browser instances/tabs',
].join('\n'))
/* return error(
new Error(`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`),
)
) */
connect = false
}

if (type === 'orchestrator') {
if (connect && type === 'orchestrator') {
const session = sessions.getSession(sessionId)
// it's possible the session was already resolved by the preview provider
session?.connected()
}

const project = vitest.getProjectByName(projectName)
const project = connect ? vitest.getProjectByName(projectName) : undefined

if (!project) {
if (connect && !project) {
return error(
new Error(`[vitest] Project "${projectName}" not found.`),
)
}

if (!project) {
return
}

wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)

Expand Down
26 changes: 25 additions & 1 deletion packages/vitest/src/node/browser/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,36 @@ export class BrowserSessions {
return this.sessions.get(sessionId)
}

findSessionByBrowser(project: TestProject): string | undefined {
const name = project.config.browser.name
for (const [sessionId, session] of this.sessions.entries()) {
if (session.project.config.browser.name === name) {
return sessionId
}
}
}

getPreviewProviderSessions(project: TestProject): string | undefined {
if (project.config.browser.provider?.name !== 'preview') {
return undefined
}

const name = project.config.browser.name
for (const [sessionId, session] of this.sessions.entries()) {
if (session.project.config.browser.name === name) {
return sessionId
}
}

return undefined
}

destroySession(sessionId: string): void {
this.sessions.delete(sessionId)
}

createSession(sessionId: string, project: TestProject, pool: { reject: (error: Error) => void }): Promise<void> {
// this promise only waits for the WS connection with the orhcestrator to be established
// this promise only waits for the WS connection with the orchestrator to be established
const defer = createDefer<void>()

const timeout = setTimeout(() => {
Expand Down
25 changes: 22 additions & 3 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,16 +727,35 @@ export function resolveConfig(
if (isPreview && resolved.browser.screenshotFailures === true) {
console.warn(c.yellow(
[
`Browser provider "preview" doesn't support screenshots, `,
`so "browser.screenshotFailures" option is forcefully disabled. `,
`Set "browser.screenshotFailures" to false or remove it from the config to suppress this warning.`,
'Browser provider "preview" doesn\'t support screenshots, ',
'so "browser.screenshotFailures" option is forcefully disabled. ',
'Set "browser.screenshotFailures" to false or remove it from the config to suppress this warning.',
].join(''),
))
resolved.browser.screenshotFailures = false
}
else {
resolved.browser.screenshotFailures ??= !isPreview && !resolved.browser.ui
}
if (isPreview && resolved.browser.enabled && resolved.browser.instances && resolved.browser.instances.length > 1) {
if (stdProvider === 'stackblitz') {
console.warn(c.yellow(
[
'Browser provider "preview" doesn\'t support multiple instances when running on stackblitz, ',
'so "browser.instances" option is forcefully to use the first instance. ',
'You can use "import { provider } from \'std-env\'" and check if it is "stackblitz" to configure ',
'the browser.instances correctly to suppress this warning.',
].join(''),
))
resolved.browser.instances = [resolved.browser.instances[0]]
}
else {
console.warn(c.yellow([
'Vitest is running multiple browser instances with the "preview" provider. ',
'Tests may not start until you focus each browser window.',
].join('')))
}
}
if (resolved.browser.provider && resolved.browser.provider.options == null) {
resolved.browser.provider.options = {}
}
Expand Down
22 changes: 16 additions & 6 deletions packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,22 @@ export class Logger {
? ''
: formatProjectName(project)
const provider = project.browser.provider?.name
const providerString = provider === 'preview' ? '' : ` by ${c.reset(c.bold(provider))}`
this.log(
c.dim(
`${output}Browser runner started${providerString} ${c.dim('at')} ${c.blue(new URL('/__vitest_test__/', origin))}\n`,
),
)
if (provider === 'preview') {
const sessionId = project.vitest._browserSessions.findSessionByBrowser(project)
const sessionQuery = sessionId ? `?sessionId=${sessionId}` : ''
this.log(
c.dim(
`${output}Browser runner started ${c.dim('at')} ${c.blue(new URL(`/__vitest_test__/${sessionQuery}`, origin))}\n`,
),
)
}
else {
this.log(
c.dim(
`${output}Browser runner started by ${c.reset(c.bold(provider))} ${c.dim('at')} ${c.blue(new URL('/__vitest_test__/', origin))}\n`,
),
)
}
}

printUnhandledErrors(errors: ReadonlyArray<unknown>): void {
Expand Down
92 changes: 76 additions & 16 deletions packages/vitest/src/node/pools/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {

const projectPools = new WeakMap<TestProject, BrowserPool>()

const ensurePool = (project: TestProject) => {
const ensurePool = (project: TestProject): [isNew: boolean, pool: BrowserPool] => {
if (projectPools.has(project)) {
return projectPools.get(project)!
return [false, projectPools.get(project)!]
}

debug?.('creating pool for project %s', project.name)
Expand All @@ -55,7 +55,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
pool.cancel()
})

return pool
return [true, pool]
}

const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => {
Expand Down Expand Up @@ -87,14 +87,24 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {

debug?.('provider is ready for %s project', project.name)

const pool = ensurePool(project)
const [isNew, pool] = ensurePool(project)
vitest.state.clearFiles(project, files.map(f => f.filepath))
providers.add(project.browser!.provider)

// eager session creation and registration when using preview provider and pool created:
// - DON'T RECREATE THE SESSION => Vitest will hang
const prepareSession: Promise<void> | undefined = project.browser!.provider.name === 'preview' && isNew
? pool.prepareSession()
: undefined

return {
pool,
prepareSession,
provider: project.browser!.provider,
runTests: () => pool.runTests(method, files),
runTests: prepareSession
// run tests once the browser is ready: the client orchestrator connects via RPC
? () => prepareSession.then(() => pool.runTests(method, files))
: () => pool.runTests(method, files),
}
}))

Expand All @@ -115,6 +125,8 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
parallelPools.push(pool.runTests)
}
else {
// launch browsers instances if required
await pool.pool.launchPreviewProvider()
nonParallelPools.push(pool.runTests)
}
}
Expand Down Expand Up @@ -198,6 +210,23 @@ class BrowserPool {
return this.project.browser!.state.orchestrators
}

prepareSession(): Promise<void> {
const sessionId = crypto.randomUUID()
this.project.vitest._browserSessions.sessionIds.add(sessionId)
return this.project.vitest._browserSessions.createSession(
sessionId,
this.project,
this,
)
}

launchPreviewProvider(): Promise<void> | undefined {
const sessionId = this.project.vitest._browserSessions.getPreviewProviderSessions(this.project)
if (sessionId) {
return this.openPage(sessionId)
}
}

async runTests(method: 'run' | 'collect', files: FileSpecification[]): Promise<void> {
this._promise ??= createDefer<void>()

Expand All @@ -211,18 +240,42 @@ class BrowserPool {

this._queue.push(...files)

if (this.project.browser!.provider.name !== 'preview' && this.orchestrators.size >= this.options.maxWorkers) {
debug?.('all orchestrators are ready, not creating more')
return this._promise
}

if (this.project.browser!.provider.name === 'preview') {
const sessionId = this.project.vitest._browserSessions.findSessionByBrowser(this.project)
if (sessionId) {
const sessionsCreated = this.readySessions.size === 0
// readySessions we have only one sessionId
if (this.readySessions.size > 0 && this._queue.length > 0) {
this.readySessions.delete(sessionId)
}
if (this._queue.length > 0) {
this.runNextTest(method, sessionId)
if (sessionsCreated) {
debug?.('all sessions are created')
}
}
else {
this._promise.resolve()
}
return this._promise
}
this._promise.reject(new Error('Preview session not found when starting tests.'))
this.cancel()
return this._promise
}

this.readySessions.forEach((sessionId) => {
if (this._queue.length) {
this.readySessions.delete(sessionId)
this.runNextTest(method, sessionId)
}
})

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

// open the minimum amount of tabs
// if there is only 1 file running, we don't need 8 tabs running
const workerCount = Math.min(
Expand All @@ -248,19 +301,26 @@ class BrowserPool {
}

private async openPage(sessionId: string) {
const sessionPromise = this.project.vitest._browserSessions.createSession(
sessionId,
this.project,
this,
)
const browser = this.project.browser!
const url = new URL('/__vitest_test__/', this.options.origin)
url.searchParams.set('sessionId', sessionId)
const pagePromise = browser.provider.openPage(
sessionId,
url.toString(),
)
await Promise.all([sessionPromise, pagePromise])
if (browser.provider.name === 'preview') {
await pagePromise
}
else {
await Promise.all([
this.project.vitest._browserSessions.createSession(
sessionId,
this.project,
this,
),
pagePromise,
])
}
}

private getOrchestrator(sessionId: string) {
Expand Down
2 changes: 2 additions & 0 deletions test/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"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:preview:chrome": "BROWSER=chrome PROVIDER=preview vitest",
"test:browser:preview:firefox": "BROWSER=firefox PROVIDER=preview vitest",
"test:browser:playwright": "PROVIDER=playwright vitest",
"test:browser:webdriverio": "PROVIDER=webdriverio vitest",
"test:browser:playwright:html": "PROVIDER=playwright vitest --reporter=html"
Expand Down
Loading