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
2 changes: 2 additions & 0 deletions docs/config/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ outline: deep

Listen to port and serve API for [the UI](/guide/ui) or [browser server](/guide/browser/). When set to `true`, the default port is `51204` or `63315` if running in Browser Mode.

In Browser Mode the server binds its port on the first browser launch, unless `api` or `ui` is enabled.

## api.allowWrite <Version>4.1.0</Version> {#api-allowwrite}

- **Type:** `boolean`
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/browser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default defineConfig({
```

::: info
Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`api`](/config/api) option.
Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`api`](/config/api) option. The port is bound when the first browser is launched, or on startup if `api` or `ui` is enabled.
:::

If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure.
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ export class Logger {

this.log(PAD + c.dim(c.green(`UI started at ${url}`)))
}
else if (this.ctx.config.api?.port) {
// browser mode always has `api.port`, but only listens on startup for `api`/`ui`
else if (this.ctx.config.api?.port && this.ctx.config._apiRequested) {
const resolvedUrls = this.ctx.vite.resolvedUrls
// workaround for https://github.com/vitejs/vite/issues/15438, it was fixed in vite 5.1
const fallbackUrl = `http://${this.ctx.config.api.host || 'localhost'}:${this.ctx.config.api.port}`
Expand Down
28 changes: 27 additions & 1 deletion packages/vitest/src/node/plugins/browserLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ export function BrowserLoaderPlugin(
]
}

const deferredListen = new WeakMap<ViteDevServer, { port: number | undefined; listening?: Promise<void> }>()

export function listenClusterServer(server: ViteDevServer): Promise<void> {
const deferred = deferredListen.get(server)
if (!deferred) {
return Promise.resolve()
}
deferred.listening ??= server.listen(deferred.port).then(
() => {
deferredListen.delete(server)
},
(error) => {
deferred.listening = undefined
throw error
},
)
return deferred.listening
}

export async function createClusterServer(
vitest: Vitest,
viteConfig: ResolvedViteConfig,
Expand Down Expand Up @@ -173,7 +192,14 @@ export async function createClusterServer(
}

const server = await createViteServer(viteConfig)
await server.listen(config.api.port)
// listen on startup only for `api`/`ui`, otherwise on the first browser launch
// (collect-only processes never listen, so Vite never starts a dependency scan)
if (config._apiRequested) {
await server.listen(config.api.port)
}
else {
deferredListen.set(server, { port: config.api.port })
}
contribution.setupRpc(parent)
return { server, parent }
}
2 changes: 2 additions & 0 deletions packages/vitest/src/node/plugins/testConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,14 @@ export function TestConfigPlugin(
resolvedTestConfig._scriptDefines = scriptDefines
}

const apiRequested = !!(testConfig.ui || testConfig.api)
const api = resolveApiServerConfig(
testConfig,
isBrowserEnabled ? harness._browserLastPort++ : defaultPort,
harness.logger,
) as ResolvedApiConfig
testConfig.api = api
resolvedTestConfig._apiRequested = apiRequested
if (globalConfig) {
api.token = globalConfig.api.token
api.tokenCreated = globalConfig.api.tokenCreated
Expand Down
4 changes: 3 additions & 1 deletion packages/vitest/src/node/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { serializeConfig } from './config/serializeConfig'
import { createFetchModuleFunction } from './environments/fetchModule'
import { ServerModuleRunner } from './environments/serverRunner'
import { loadGlobalSetupFiles } from './globalSetup'
import { listenClusterServer } from './plugins/browserLoader'
import { getFilePoolName } from './pool'
import { globProjectFiles, globProjectTestFiles, isInSourceTestCode } from './projects/globProjectFiles'
import { VitestResolver } from './resolver'
Expand Down Expand Up @@ -552,8 +553,9 @@ export class TestProject {
return
}
// The browser server is created eagerly with the project, so `this.browser`
// is already set here; we only need to initialize the provider.
// is already set here; bind the port if startup did not, then init the provider.
if (this.browser) {
await listenClusterServer(this.vite)
await this.vitest.report('onBrowserInit', this)
}
await this.browser?.initBrowserProvider(this)
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/types/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ export interface BrowserServerContribution {
* when the `Vitest` instance exists.
*/
createParent: (ctx: { config: ResolvedConfig; vitest: Vitest }) => ParentProjectBrowser
/** Called by core after `server.listen()` to wire up the browser RPC. */
/** Called by core once the server exists to wire up the browser RPC (the port may be bound later). */
setupRpc: (parent: ParentProjectBrowser) => void
/**
* Mutable. Filled by core at server creation; the pushed `BrowserPlugin`
Expand Down
5 changes: 5 additions & 0 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,11 @@ export interface ResolvedConfig
* @internal
*/
_browserContribution?: BrowserServerContribution
/**
* `api` or `ui` was set by the user. Browser mode always assigns `api.port`.
* @internal
*/
_apiRequested?: boolean
}

/**
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/fixtures/browser-server-listen/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { expect, test } from 'vitest'

test('basic', () => {
expect(1 + 1).toBe(2)
})
13 changes: 13 additions & 0 deletions test/e2e/fixtures/browser-server-listen/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
})
34 changes: 34 additions & 0 deletions test/e2e/test/browser-server-listen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { TestUserConfig } from 'vitest/node'
import { expect, onTestFinished, test } from 'vitest'
import { createVitest } from 'vitest/node'

// browser config is in the fixture, CLI `instances` do not define projects
function createBrowserVitest(config: TestUserConfig = {}) {
return createVitest('test', {
root: 'fixtures/browser-server-listen',
watch: false,
reporters: [{}],
...config,
})
}

test('the browser server binds its port on the first browser launch', { tags: ['browser'] }, async () => {
const ctx = await createBrowserVitest()
onTestFinished(() => ctx.close())
expect(ctx.vite.httpServer?.listening).toBe(false)

// static collection does not launch a browser
const specifications = await ctx.globTestSpecifications()
await ctx.parseSpecifications(specifications)
expect(ctx.vite.httpServer?.listening).toBe(false)

const { testModules } = await ctx.runTestSpecifications(specifications, false)
expect(ctx.vite.httpServer?.listening).toBe(true)
expect(testModules.map(testModule => testModule.state())).toEqual(['passed'])
})

test('the browser server listens on startup when the API is requested', { tags: ['browser'] }, async () => {
const ctx = await createBrowserVitest({ api: true })
onTestFinished(() => ctx.close())
expect(ctx.vite.httpServer?.listening).toBe(true)
})
Loading