Skip to content

Commit e78adcf

Browse files
authored
fix: open browser in --standalone mode without running tests (#9911)
1 parent d27b928 commit e78adcf

File tree

9 files changed

+91
-41
lines changed

9 files changed

+91
-41
lines changed

docs/api/advanced/reporters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function onInit(vitest: Vitest): Awaitable<void>
5555
This method is called when [Vitest](/api/advanced/vitest) was initiated or started, but before the tests were filtered.
5656

5757
::: info
58-
Internally this method is called inside [`vitest.start`](/api/advanced/vitest#start), [`vitest.init`](/api/advanced/vitest#init) or [`vitest.mergeReports`](/api/advanced/vitest#mergereports). If you are using programmatic API, make sure to call either one depending on your needs before calling [`vitest.runTestSpecifications`](/api/advanced/vitest#runtestspecifications), for example. Built-in CLI will always run methods in correct order.
58+
Internally this method is called inside [`vitest.start`](/api/advanced/vitest#start), [`vitest.standalone`](/api/advanced/vitest#standalone) or [`vitest.mergeReports`](/api/advanced/vitest#mergereports). If you are using programmatic API, make sure to call either one depending on your needs before calling [`vitest.runTestSpecifications`](/api/advanced/vitest#runtestspecifications), for example. Built-in CLI will always run methods in correct order.
5959
:::
6060

6161
Note that you can also get access to `vitest` instance from test cases, suites and test modules via a [`project`](/api/advanced/test-project) property, but it might also be useful to store a reference to `vitest` in this method.

docs/api/advanced/vitest.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,19 @@ function start(filters?: string[]): Promise<TestRunResult>
233233
Initialize reporters, the coverage provider, and run tests. This method accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli).
234234

235235
::: warning
236-
This method should not be called if [`vitest.init()`](#init) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was initialised.
236+
This method should not be called if [`vitest.standalone()`](#standalone) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was initialised.
237237
:::
238238

239239
This method is called automatically by [`startVitest`](/guide/advanced/tests) if `config.mergeReports` and `config.standalone` are not set.
240240

241-
## init
241+
## standalone <Version type="experimental">4.1.1</Version> {#standalone}
242242

243243
```ts
244-
function init(): Promise<void>
244+
function standalone(): Promise<void>
245245
```
246246

247+
- **Alias**: `init` <Deprecated />
248+
247249
Initialize reporters and the coverage provider. This method doesn't run any tests. If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called.
248250

249251
Internally, this method is called only if [`--standalone`](/guide/cli#standalone) flag is enabled.
@@ -545,7 +547,7 @@ If there is a test run happening, returns a promise that will resolve when the t
545547
function createCoverageProvider(): Promise<CoverageProvider | null>
546548
```
547549

548-
Creates a coverage provider if `coverage` is enabled in the config. This is done automatically if you are running tests with [`start`](#start) or [`init`](#init) methods.
550+
Creates a coverage provider if `coverage` is enabled in the config. This is done automatically if you are running tests with [`start`](#start) or [`standalone`](#standalone) methods.
549551

550552
::: warning
551553
This method will also clean all previous reports if [`coverage.clean`](/config/coverage#coverage-clean) is not set to `false`.

docs/guide/cli-generated.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ Use `bundle` to bundle the config with esbuild or `runner` (experimental) to pro
853853

854854
- **CLI:** `--standalone`
855855

856-
Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)
856+
Start Vitest without running tests. Tests will be running only on change. If browser mode is enabled, the UI will be opened automatically. This option is ignored when CLI file filters are passed. (default: `false`)
857857

858858
### listTags
859859

packages/vitest/src/node/cli/cli-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export async function startVitest(
9393

9494
ctx.onAfterSetServer(() => {
9595
if (ctx.config.standalone) {
96-
ctx.init()
96+
ctx.standalone()
9797
}
9898
else {
9999
ctx.start(cliFilters)
@@ -111,7 +111,7 @@ export async function startVitest(
111111
await ctx.mergeReports()
112112
}
113113
else if (ctx.config.standalone) {
114-
await ctx.init()
114+
await ctx.standalone()
115115
}
116116
else {
117117
await ctx.start(cliFilters)

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
807807
},
808808
standalone: {
809809
description:
810-
'Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)',
810+
'Start Vitest without running tests. Tests will be running only on change. If browser mode is enabled, the UI will be opened automatically. This option is ignored when CLI file filters are passed. (default: `false`)',
811811
},
812812
mergeReports: {
813813
description:

packages/vitest/src/node/core.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,11 +794,19 @@ export class Vitest {
794794
})
795795
}
796796

797+
/**
798+
* @deprecated use `standalone()` instead
799+
*/
800+
init(): Promise<void> {
801+
this.logger.deprecate('`vitest.init()` is deprecated. Use `vitest.standalone()` instead.')
802+
return this.standalone()
803+
}
804+
797805
/**
798806
* Initialize reporters and the coverage provider. This method doesn't run any tests.
799807
* If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called.
800808
*/
801-
async init(): Promise<void> {
809+
async standalone(): Promise<void> {
802810
await this._traces.$('vitest.init', async () => {
803811
try {
804812
await this.initCoverageProvider()
@@ -811,6 +819,8 @@ export class Vitest {
811819
// populate test files cache so watch mode can trigger a file rerun
812820
await this.globTestSpecifications()
813821

822+
await Promise.all(this.projects.map(project => project._standalone()))
823+
814824
if (this.config.watch) {
815825
await this.report('onWatcherStart')
816826
}

packages/vitest/src/node/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export class Logger {
258258
}
259259

260260
if (this.ctx.config.standalone) {
261-
this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`))
261+
this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.\n`))
262262
}
263263
else {
264264
this.log()

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

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,8 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
4141

4242
debug?.('creating pool for project %s', project.name)
4343

44-
const resolvedUrls = project.browser!.vite.resolvedUrls
45-
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
46-
47-
if (!origin) {
48-
throw new Error(
49-
`Can't find browser origin URL for project "${project.name}"`,
50-
)
51-
}
52-
5344
const pool: BrowserPool = new BrowserPool(project, {
5445
maxWorkers: getThreadsCount(project),
55-
origin,
5646
})
5747
projectPools.set(project, pool)
5848
vitest.onCancel(() => {
@@ -198,7 +188,7 @@ class BrowserPool {
198188
private _promise: DeferPromise<void> | undefined
199189
private _providedContext: string | undefined
200190

201-
private readySessions = new Set<string>()
191+
private readySessions: Set<string>
202192

203193
private _traces: Traces
204194
private _otel: {
@@ -210,7 +200,6 @@ class BrowserPool {
210200
private project: TestProject,
211201
private options: {
212202
maxWorkers: number
213-
origin: string
214203
},
215204
) {
216205
this._traces = project.vitest._traces
@@ -219,6 +208,7 @@ class BrowserPool {
219208
'vitest.project': project.name,
220209
'vitest.browser.provider': this.project.browser!.provider.name,
221210
})
211+
this.readySessions = project._browserReadySessions
222212
}
223213

224214
public cancel(): void {
@@ -296,24 +286,10 @@ class BrowserPool {
296286
}
297287

298288
private async openPage(sessionId: string, options: { parallel: boolean }): Promise<void> {
299-
const sessionPromise = this.project.vitest._browserSessions.createSession(
300-
sessionId,
301-
this.project,
302-
this,
303-
)
304-
const browser = this.project.browser!
305-
const url = new URL('/__vitest_test__/', this.options.origin)
306-
url.searchParams.set('sessionId', sessionId)
307-
const otelCarrier = this._traces.getContextCarrier()
308-
if (otelCarrier) {
309-
url.searchParams.set('otelCarrier', JSON.stringify(otelCarrier))
310-
}
311-
const pagePromise = browser.provider.openPage(
312-
sessionId,
313-
url.toString(),
314-
options,
315-
)
316-
await Promise.all([sessionPromise, pagePromise])
289+
await this.project._openBrowserPage(sessionId, {
290+
reject: error => this.reject(error),
291+
parallel: options.parallel,
292+
})
317293
}
318294

319295
private getOrchestrator(sessionId: string) {

packages/vitest/src/node/project.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
TestProjectInlineConfiguration,
1616
UserConfig,
1717
} from './types/config'
18+
import crypto from 'node:crypto'
1819
import { promises as fs, readFileSync } from 'node:fs'
1920
import { rm } from 'node:fs/promises'
2021
import { tmpdir } from 'node:os'
@@ -70,6 +71,7 @@ export class TestProject {
7071
/** @internal */ _fetcher!: VitestFetchFunction
7172
/** @internal */ _serializedDefines?: string
7273
/** @internal */ testFilesList: string[] | null = null
74+
/** @internal */ _browserReadySessions = new Set<string>()
7375

7476
private runner!: ModuleRunner
7577

@@ -604,6 +606,66 @@ export class TestProject {
604606
]
605607
}
606608

609+
/** @internal */
610+
public async _openBrowserPage(sessionId: string, pool: {
611+
reject: (error: Error) => void
612+
parallel?: boolean
613+
}): Promise<void> {
614+
if (!this.browser) {
615+
throw new Error(`browser is not initialized`)
616+
}
617+
618+
const resolvedUrls = this.browser.vite.resolvedUrls
619+
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
620+
if (!origin) {
621+
throw new Error(
622+
`Can't find browser origin URL for project "${this.name}"`,
623+
)
624+
}
625+
626+
const url = new URL('/__vitest_test__/', origin)
627+
url.searchParams.set('sessionId', sessionId)
628+
const otelCarrier = this.vitest._traces.getContextCarrier()
629+
if (otelCarrier) {
630+
url.searchParams.set('otelCarrier', JSON.stringify(otelCarrier))
631+
}
632+
this.vitest._browserSessions.sessionIds.add(sessionId)
633+
const sessionPromise = this.vitest._browserSessions.createSession(
634+
sessionId,
635+
this,
636+
pool,
637+
)
638+
const pagePromise = this.browser.provider.openPage(
639+
sessionId,
640+
url.toString(),
641+
{ parallel: pool.parallel ?? false },
642+
)
643+
await Promise.all([
644+
sessionPromise,
645+
pagePromise,
646+
])
647+
}
648+
649+
/** @internal */
650+
public async _standalone(): Promise<void> {
651+
if (!this.isBrowserEnabled()) {
652+
return
653+
}
654+
655+
await this._initBrowserProvider()
656+
if (!this.browser) {
657+
return
658+
}
659+
660+
const sessionId = crypto.randomUUID()
661+
await this._openBrowserPage(sessionId, {
662+
reject: (error) => {
663+
this.vitest.state.catchError(error, 'Browser Error')
664+
},
665+
})
666+
this._browserReadySessions.add(sessionId)
667+
}
668+
607669
private _serializeOverriddenConfig(): SerializedConfig {
608670
// TODO: serialize the config _once_ or when needed
609671
const config = serializeConfig(this)

0 commit comments

Comments
 (0)