Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
76 changes: 64 additions & 12 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): [existing: boolean, pool: BrowserPool] => {
if (projectPools.has(project)) {
return projectPools.get(project)!
return [true, 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 [false, pool]
}

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

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

const pool = ensurePool(project)
const [existing, pool] = ensurePool(project)
// eager session creation and registration: don't block execution
vitest.state.clearFiles(project, files.map(f => f.filepath))
providers.add(project.browser!.provider)

const prepareSession: Promise<void> | undefined = project.browser!.provider.name === 'preview'
? !existing ? pool.prepareSession() : undefined // <== DON'T RECREATE THE SESSION => Vitest will hang
: 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 +124,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 +209,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 +239,35 @@ class BrowserPool {

this._queue.push(...files)

const testRun = this.readySessions.size > 0 && this._queue.length > 0

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

if (this.orchestrators.size >= this.options.maxWorkers) {
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') {
if (testRun) {
return this._promise
}
const sessionId = this.project.vitest._browserSessions.findSessionByBrowser(this.project)
if (sessionId) {
this.runNextTest(method, sessionId)
debug?.('all sessions are created')
return this._promise
}
this._promise.reject(new Error('Preview session not found when starting tests.'))
this.cancel()
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 +293,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