diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 424775c82dfc..ef2bf603403e 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -96,8 +96,8 @@ Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experim | `--shard ` | Execute tests in a specified shard | | `--sequence` | Define in what order to run tests. Use [cac's dot notation] to specify options (for example, use `--sequence.shuffle` to run tests in random order or `--sequence.shuffle --sequence.seed SEED_ID` to run a specific order) | | `--no-color` | Removes colors from the console output | -| `--inspect` | Enables Node.js inspector | -| `--inspect-brk` | Enables Node.js inspector with break | +| `--inspect [[host:]port]` | Enable Node.js inspector (default: 127.0.0.1:9229) | +| `--inspect-brk [[host:]port]` | Enables Node.js inspector and break before the test starts | | `--bail ` | Stop test execution when given number of tests have failed | | `--retry ` | Retry the test specific number of times if it fails | | `--exclude ` | Additional file globs to be excluded from test | diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index b82a3ec7034d..6098c05428a0 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -1,6 +1,7 @@ // if changed, update also jsdocs and docs export const defaultPort = 51204 export const defaultBrowserPort = 63315 +export const defaultInspectPort = 9229 export const EXIT_CODE_RESTART = 43 diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index b5e16b9f526c..13e2c6aa8db9 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -308,7 +308,9 @@ export const cliOptionsConfig: VitestCLIOptions = { if (typeof browser === 'boolean') return { enabled: browser } if (browser === 'true' || browser === 'false') - return { enabled: browser !== 'false' } + return { enabled: browser === 'true' } + if (browser === 'yes' || browser === 'no') + return { enabled: browser === 'yes' } if (typeof browser === 'string') return { enabled: true, name: browser } return browser @@ -465,11 +467,28 @@ export const cliOptionsConfig: VitestCLIOptions = { }, }, inspect: { - description: 'Enable Node.js inspector', + description: 'Enable Node.js inspector (default: 127.0.0.1:9229)', + argument: '[[host:]port]', + transform(portOrEnabled) { + if (portOrEnabled === 0 || portOrEnabled === 'true' || portOrEnabled === 'yes') + return true + if (portOrEnabled === 'false' || portOrEnabled === 'no') + return false + return portOrEnabled + }, }, inspectBrk: { - description: 'Enable Node.js inspector with break', + description: 'Enable Node.js inspector and break before the test starts', + argument: '[[host:]port]', + transform(portOrEnabled) { + if (portOrEnabled === 0 || portOrEnabled === 'true' || portOrEnabled === 'yes') + return true + if (portOrEnabled === 'false' || portOrEnabled === 'no') + return false + return portOrEnabled + }, }, + inspector: null, testTimeout: { description: 'Default timeout of a test in milliseconds (default: 5000)', argument: '', diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 26d82b7fa1c1..3d03d9824818 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -3,7 +3,7 @@ import { normalize, relative, resolve } from 'pathe' import c from 'picocolors' import type { ResolvedConfig as ResolvedViteConfig } from 'vite' import type { ApiConfig, ResolvedConfig, UserConfig, VitestRunMode } from '../types' -import { defaultBrowserPort, defaultPort, extraInlineDeps } from '../constants' +import { defaultBrowserPort, defaultInspectPort, defaultPort, extraInlineDeps } from '../constants' import { benchmarkConfigDefaults, configDefaults } from '../defaults' import { isCI, stdProvider, toArray } from '../utils' import type { BuiltinPool } from '../types/pool-options' @@ -20,6 +20,21 @@ function resolvePath(path: string, root: string) { ) } +function parseInspector(inspect: string | undefined | boolean | number) { + if (typeof inspect === 'boolean' || inspect === undefined) + return {} + if (typeof inspect === 'number') + return { port: inspect } + + if (inspect.match(/https?:\//)) + throw new Error(`Inspector host cannot be a URL. Use "host:port" instead of "${inspect}"`) + + const [host, port] = inspect.split(':') + if (!port) + return { host } + return { host, port: Number(port) || defaultInspectPort } +} + export function resolveApiServerConfig( options: Options, ): ApiConfig | undefined { @@ -88,8 +103,14 @@ export function resolveConfig( mode, } as any as ResolvedConfig - resolved.inspect = Boolean(resolved.inspect) - resolved.inspectBrk = Boolean(resolved.inspectBrk) + const inspector = resolved.inspect || resolved.inspectBrk + + resolved.inspector = { + ...resolved.inspector, + ...parseInspector(inspector), + enabled: !!inspector, + waitForDebugger: options.inspector?.waitForDebugger ?? !!resolved.inspectBrk, + } if (viteConfig.base !== '/') resolved.base = viteConfig.base diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index c9df549c8324..456a3b4e2744 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -410,6 +410,7 @@ export class WorkspaceProject { }, inspect: this.ctx.config.inspect, inspectBrk: this.ctx.config.inspectBrk, + inspector: this.ctx.config.inspector, alias: [], includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation, env: { diff --git a/packages/vitest/src/runtime/inspector.ts b/packages/vitest/src/runtime/inspector.ts index fdc8f31ab25d..0631a7990bc5 100644 --- a/packages/vitest/src/runtime/inspector.ts +++ b/packages/vitest/src/runtime/inspector.ts @@ -12,7 +12,7 @@ let session: InstanceType */ export function setupInspect(ctx: ContextRPC) { const config = ctx.config - const isEnabled = config.inspect || config.inspectBrk + const isEnabled = config.inspector.enabled if (isEnabled) { inspector = __require('node:inspector') @@ -20,10 +20,13 @@ export function setupInspect(ctx: ContextRPC) { const isOpen = inspector.url() !== undefined if (!isOpen) { - inspector.open() + inspector.open( + config.inspector.port, + config.inspector.host, + config.inspector.waitForDebugger, + ) if (config.inspectBrk) { - inspector.waitForDebugger() const firstTestFile = ctx.files[0] // Stop at first test file diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index f75c660558c1..269936195a59 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -675,7 +675,7 @@ export interface InlineConfig { * * Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`. */ - inspect?: boolean + inspect?: boolean | string /** * Debug tests by opening `node:inspector` in worker / child process and wait for debugger to connect. @@ -683,7 +683,29 @@ export interface InlineConfig { * * Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`. */ - inspectBrk?: boolean + inspectBrk?: boolean | string + + /** + * Inspector options. If `--inspect` or `--inspect-brk` is enabled, these options will be passed to the inspector. + */ + inspector?: { + /** + * Enable inspector + */ + enabled?: boolean + /** + * Port to run inspector on + */ + port?: number + /** + * Host to run inspector on + */ + host?: string + /** + * Wait for debugger to connect before running tests + */ + waitForDebugger?: boolean + } /** * Modify default Chai config. Vitest uses Chai for `expect` and `assert` matches. diff --git a/test/config/test/resolution.test.ts b/test/config/test/resolution.test.ts index f36b522e5831..f047d7d44760 100644 --- a/test/config/test/resolution.test.ts +++ b/test/config/test/resolution.test.ts @@ -1,7 +1,7 @@ import type { UserConfig } from 'vitest' import type { UserConfig as ViteUserConfig } from 'vite' -import { describe, expect, it } from 'vitest' -import { createVitest } from 'vitest/node' +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { createVitest, parseCLI } from 'vitest/node' import { extraInlineDeps } from 'vitest/config' async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}) { @@ -263,3 +263,50 @@ describe('correctly defines api flag', () => { }) }) }) + +describe.each([ + '--inspect', + '--inspect-brk', +])('correctly parses %s flags', (inspectFlagName) => { + it.each([ + ['', { enabled: true }], + ['true', { enabled: true }], + ['yes', { enabled: true }], + ['false', { enabled: false }], + ['no', { enabled: false }], + + ['1002', { enabled: true, port: 1002 }], + ['www.remote.com:1002', { enabled: true, port: 1002, host: 'www.remote.com' }], + ['www.remote.com', { enabled: true, host: 'www.remote.com' }], + ])(`parses "vitest ${inspectFlagName} %s" value`, async (cliValue, inspect) => { + const rawConfig = parseCLI( + `vitest --no-file-parallelism ${inspectFlagName} ${cliValue}`, + ) + const c = await config(rawConfig.options) + expect(c.inspector).toEqual({ + ...inspect, + waitForDebugger: inspectFlagName === '--inspect-brk' && inspect.enabled, + }) + }) + it('cannot use a URL', async () => { + const url = 'https://www.remote.com:1002' + const rawConfig = parseCLI([ + 'vitest', + '--no-file-parallelism', + inspectFlagName, + url, + ]) + const error = vi.fn() + const originalError = console.error + console.error = error + onTestFinished(() => { + console.error = originalError + }) + await expect(async () => { + await config(rawConfig.options) + }).rejects.toThrowError() + expect(error.mock.lastCall[0]).toEqual( + expect.stringContaining(`Inspector host cannot be a URL. Use "host:port" instead of "${url}"`), + ) + }) +})