[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix: open browser in `--standalone` mode without running tests (#9911)

authored by

Vladimir and committed by
GitHub
(Mar 20, 2026, 3:40 PM +0100) e78adcf9 d27b9287

+91 -41
+1 -1
docs/guide/cli-generated.md
··· 853 853 854 854 - **CLI:** `--standalone` 855 855 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`) 857 857 858 858 ### listTags 859 859
+1 -1
docs/api/advanced/reporters.md
··· 55 55 This method is called when [Vitest](/api/advanced/vitest) was initiated or started, but before the tests were filtered. 56 56 57 57 ::: 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. 59 59 ::: 60 60 61 61 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.
+6 -4
docs/api/advanced/vitest.md
··· 233 233 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). 234 234 235 235 ::: 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. 237 237 ::: 238 238 239 239 This method is called automatically by [`startVitest`](/guide/advanced/tests) if `config.mergeReports` and `config.standalone` are not set. 240 240 241 - ## init 241 + ## standalone <Version type="experimental">4.1.1</Version> {#standalone} 242 242 243 243 ```ts 244 - function init(): Promise<void> 244 + function standalone(): Promise<void> 245 245 ``` 246 + 247 + - **Alias**: `init` <Deprecated /> 246 248 247 249 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. 248 250 ··· 545 547 function createCoverageProvider(): Promise<CoverageProvider | null> 546 548 ``` 547 549 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. 549 551 550 552 ::: warning 551 553 This method will also clean all previous reports if [`coverage.clean`](/config/coverage#coverage-clean) is not set to `false`.
+11 -1
packages/vitest/src/node/core.ts
··· 795 795 } 796 796 797 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 + 805 + /** 798 806 * Initialize reporters and the coverage provider. This method doesn't run any tests. 799 807 * If the `--watch` flag is provided, Vitest will still run changed tests even if this method was not called. 800 808 */ 801 - async init(): Promise<void> { 809 + async standalone(): Promise<void> { 802 810 await this._traces.$('vitest.init', async () => { 803 811 try { 804 812 await this.initCoverageProvider() ··· 810 818 811 819 // populate test files cache so watch mode can trigger a file rerun 812 820 await this.globTestSpecifications() 821 + 822 + await Promise.all(this.projects.map(project => project._standalone())) 813 823 814 824 if (this.config.watch) { 815 825 await this.report('onWatcherStart')
+1 -1
packages/vitest/src/node/logger.ts
··· 258 258 } 259 259 260 260 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`)) 262 262 } 263 263 else { 264 264 this.log()
+62
packages/vitest/src/node/project.ts
··· 15 15 TestProjectInlineConfiguration, 16 16 UserConfig, 17 17 } from './types/config' 18 + import crypto from 'node:crypto' 18 19 import { promises as fs, readFileSync } from 'node:fs' 19 20 import { rm } from 'node:fs/promises' 20 21 import { tmpdir } from 'node:os' ··· 70 71 /** @internal */ _fetcher!: VitestFetchFunction 71 72 /** @internal */ _serializedDefines?: string 72 73 /** @internal */ testFilesList: string[] | null = null 74 + /** @internal */ _browserReadySessions = new Set<string>() 73 75 74 76 private runner!: ModuleRunner 75 77 ··· 602 604 ...Object.values(this.browser?.vite.environments || {}), 603 605 ...Object.values(this.vite.environments || {}), 604 606 ] 607 + } 608 + 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) 605 667 } 606 668 607 669 private _serializeOverriddenConfig(): SerializedConfig {
+2 -2
packages/vitest/src/node/cli/cli-api.ts
··· 93 93 94 94 ctx.onAfterSetServer(() => { 95 95 if (ctx.config.standalone) { 96 - ctx.init() 96 + ctx.standalone() 97 97 } 98 98 else { 99 99 ctx.start(cliFilters) ··· 111 111 await ctx.mergeReports() 112 112 } 113 113 else if (ctx.config.standalone) { 114 - await ctx.init() 114 + await ctx.standalone() 115 115 } 116 116 else { 117 117 await ctx.start(cliFilters)
+1 -1
packages/vitest/src/node/cli/cli-config.ts
··· 807 807 }, 808 808 standalone: { 809 809 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`)', 811 811 }, 812 812 mergeReports: { 813 813 description:
+6 -30
packages/vitest/src/node/pools/browser.ts
··· 41 41 42 42 debug?.('creating pool for project %s', project.name) 43 43 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 - 53 44 const pool: BrowserPool = new BrowserPool(project, { 54 45 maxWorkers: getThreadsCount(project), 55 - origin, 56 46 }) 57 47 projectPools.set(project, pool) 58 48 vitest.onCancel(() => { ··· 198 188 private _promise: DeferPromise<void> | undefined 199 189 private _providedContext: string | undefined 200 190 201 - private readySessions = new Set<string>() 191 + private readySessions: Set<string> 202 192 203 193 private _traces: Traces 204 194 private _otel: { ··· 210 200 private project: TestProject, 211 201 private options: { 212 202 maxWorkers: number 213 - origin: string 214 203 }, 215 204 ) { 216 205 this._traces = project.vitest._traces ··· 219 208 'vitest.project': project.name, 220 209 'vitest.browser.provider': this.project.browser!.provider.name, 221 210 }) 211 + this.readySessions = project._browserReadySessions 222 212 } 223 213 224 214 public cancel(): void { ··· 296 286 } 297 287 298 288 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 + }) 317 293 } 318 294 319 295 private getOrchestrator(sessionId: string) {