Command Building & Execution¶
Once a file and its framework are known, Jest Runner has to produce a command line and run it. Three callers need that: the terminal path (one test or file), the Test Explorer (many files, batched) and the debugger (a launch configuration, see Debugging). This page follows the first two.
The pieces¶
flowchart LR
subgraph Config["TestRunnerConfig (testRunnerConfig.ts)"]
GC["getTestCommand(file)<br/>executable"]
CWD["getTestRunCwd(file)<br/>working directory"]
CFG["getJestConfigPath / getVitestConfigPath / getRstestConfigPath"]
BTA["buildTestArgs(file, name, withQuotes, options)"]
end
FA["frameworkAdapters.ts<br/>buildJestArgs, buildVitestArgs, ..."]
TAB["execution/TestArgumentBuilder.ts<br/>JestStrategy, VitestStrategy, ..."]
TR["testRunner.ts<br/>TestRunner"]
TM["TerminalManager.ts"]
TRE["testController/TestRunExecutor.ts"]
TPR["execution/TestProcessRunner.ts"]
TR --> GC & CWD & BTA
BTA --> FA
TR --> TM
TRE --> GC & CWD
TRE --> TAB --> FA
TAB --> CFG
TRE --> TPR
TestRunnerConfig: the facade¶
One instance per entry point (extension.ts creates one for the terminal path, TestController one for the
Test Explorer). It wraps config/Settings.ts and the detection modules and answers:
getTestCommand(file): the executable. For Jest, Vitest and Rstest it is the*Commandsetting (with${workspaceFolder}and friends resolved), else the package's binary found withresolveBinaryPathfrom the working directory (node <path/to/jest.js>), elsenpx --no-install <name>. Playwright isnpx playwright, Node.js isnode, Bun and Deno arebunanddeno.getCwd(file)/getTestRunCwd(file): see Test Detection.get*ConfigPath(file): the config to pass explicitly, usually empty.buildTestArgs(file, testName, withQuotes, options): dispatches to the framework's adapter with the config path and the framework's*RunOptions.getEnvironmentForRun(file):NODE_OPTIONS=--experimental-vm-moduleswhenjestrunner.enableESMis on.
frameworkAdapters.ts: one file, one test¶
Pure functions (file, testName, withQuotes, options, configPath, runOptions) → string[], one per
framework. They are the single source of truth for "how do I run file X, test Y with framework Z" and are used
directly by the terminal path and, for single-file runs, by the Test Explorer and the debugger. In summary:
| Framework | Arguments |
|---|---|
| jest | <file as escaped regex> [-c <config>] [-t ^name$] options runOptions |
| vitest | [run] <file> [--config <config>] [-t ^name$] options runOptions; run only without --watch; the name pattern allows > between levels (Vitest 5) |
| node-test | --test [--test-reporter <node-reporter.mjs> --test-reporter-destination stdout] [--test-name-pattern ^name$] options [coverage reporters] <file> |
| bun | test [-t ^name$] [--coverage --coverage-reporter=lcov] options runOptions <file> |
| deno | test --allow-all [--filter ^name$] --junit-path=.deno-report.xml options runOptions [--coverage=coverage] <file> |
| playwright | test [-g name] options runOptions <file> |
| rstest | [--config <config>] <file> [-t ^name$] options runOptions |
withQuotes decides whether the file path and the name are wrapped by quote() (single quotes on POSIX,
doubled double quotes on Windows). The terminal path needs quotes (the shell splits the line); the spawn and
debug paths pass false, or unquote again, because arguments go to the process as an array.
appendUniqueArgs / prependUniqueArgs (utils/ArgUtils.ts) merge option lists without duplicating a flag
and keep a flag together with its value (-t x, --config y, --reporter z), so a --coverage from the
CodeLens and a --coverage from runOptions appear once.
The terminal path¶
TestRunner (testRunner.ts) handles every command registered in extension.ts:
- The document is saved.
- The test name comes from the CodeLens argument, from the editor selection (unquoted), or from the parsed
tree at the cursor line (
findCurrentTestName): ancestors joined with spaces, regex-escaped, adescribeline extended with(\s[\s\S]*)?. buildCommandjoinsgetTestCommand(file)withbuildTestArgs(file, name, true, options)afterquoteArgsForTerminal(args, vscode.env.shell)has adapted the quoting to the terminal's shell (see Windows).TerminalManager.runCommand(command, { framework, cwd, env, preserveEditorFocus })sends the text.
TerminalManager keeps one terminal named jest or vitest. It is reused while the name, the cwd and
the env stay the same; otherwise it is disposed and recreated (a terminal cannot change its directory or
environment after creation). Closing the terminal by hand resets the state. runPreviousTest replays the last
command string (or debug configuration) with the same cwd and env.
--collectCoverageFrom for the current-test-coverage CodeLens is computed in getCoverageOptions:
foo.test.ts → **/foo.ts if that file exists next to the test, else **/<test dir name>/**.
The Test Explorer path¶
TestRunExecutor (testController/TestRunExecutor.ts) implements the Run, Coverage and
Update Snapshots profiles.
1. Collect¶
collectTestsByFile(request, controller) (execution/TestCollector.ts) walks the requested items (or all
items when the request has no include), skips excluded ones, and returns Map<file, leaf TestItem[]>.
Every leaf is marked started.
2. Group into run contexts¶
createRunContexts groups the files by a key of framework, workspace folder, working directory, config
path and, for Jest, Vitest, Rstest and Deno, partial or full selection. Each group becomes one process.
Partial selection (isPartiallySelected: fewer leaves requested than the file has) matters because the
name filter (-t) applies to the whole process: a fully selected file in the same process would only run the
tests whose names happen to match the filter. So fully and partially selected files never share a process.
3. Fast mode or standard mode¶
canUseFastMode: exactly one file, exactly one test, no coverage, no extra args. Then the run uses the
framework adapter (buildTestArgsFast) without any reporter and reports pass or fail from the exit code
(executeTestCommandFast). This is the common "click the run icon next to one test" case and avoids the
reporter round trip.
Everything else is standard mode.
4. Build the batched arguments¶
buildTestArgs(files, testsByFile, framework, additionalArgs, coverage, config, controller) in
execution/TestArgumentBuilder.ts picks a strategy class:
| Strategy | Single file, partially selected | Several files or fully selected |
|---|---|---|
JestStrategy |
adapter with the joined name pattern (a\|b\|c) plus --json --reporters default --reporters <jest-reporter.cjs> |
all files as escaped regexes, the reporters, -c when configured, -t only when some file is partial |
VitestStrategy |
adapter plus --reporter=json --reporter=default --reporter=<vitest-reporter.cjs> |
run <files> --reporter=... [--config] [-t] |
RstestStrategy |
adapter plus --reporter=junit |
<files> --reporter=junit [--config] [-t] |
PlaywrightStrategy |
adapter with -g |
adapter for the first file, file popped and all files appended |
NodeTestStrategy |
--test --test-reporter <node-reporter.mjs> --test-reporter-destination stdout [lcov reporters] [--test-name-pattern] <files> |
same |
BunTestStrategy |
test [-t] --reporter=junit --reporter-outfile=.bun-report.xml [--coverage ...] <files> |
same |
DenoTestStrategy |
test --allow-all [--filter] --junit-path=.deno-report.xml [--coverage=coverage] <files> |
same |
Coverage adds --coverage --coverageReporters=json (Jest), --coverage --coverage.reporter json
--coverage.reportOnFailure (Vitest), --coverage (Rstest), --experimental-test-coverage with an lcov
reporter (Node.js), --coverage --coverage-reporter=lcov (Bun) or --coverage=coverage (Deno).
5. Spawn and stream¶
executeTestCommand(command, args, token, tests, run, cwd, env, sessionId) in
execution/TestProcessRunner.ts:
resolveSpawnCommandsplits a command such asTZ=UTC node --import tsxinto env, executable and leading args (parseCommandAndEnv), unquotes the adapter's quoted args (normalizeArgsForNonShellSpawn), setsFORCE_COLOR=trueand addsJSTR_SESSION_ID=<uuid>for the reporters.- The process is spawned without a shell, except on Windows when the executable is a
.cmd/.batshim (see below). - stdout is decoded as a UTF-8 stream and scanned incrementally for structured reporter blocks
(
extractStructuredMessages); each completeresultsblock is applied immediately withprocessTestResultsFromParsed. - stderr is collected; both are capped at
jestrunner.maxBufferSizeMB, beyond which the process is killed and all tests fail with a message. - Cancellation kills the process (tree) and marks the tests skipped.
- On close: if a structured block was seen, the run is done; otherwise the combined output is returned for
the fallback parsers, or, with no output at all, all tests fail with
No output from test runner.
settle() guards against error, close, cancellation and overflow firing for the same process: the first
one wins.
6. Fallbacks and coverage¶
Back in TestRunExecutor.runStandardMode: without structured results, Bun's and Deno's JUnit files are read
(and deleted) and appended to the output, then processTestResults picks a parser by framework (see
Reporters & Result Matching). With coverage, processCoverageData reads the report and calls
run.addCoverage for every file (see Coverage). For Bun a stale coverage/lcov.info is removed
before the run; for Deno deno coverage coverage --lcov converts the profile afterwards.
Windows¶
Windows needs extra care in both paths, all of it in utils/ShellUtils.ts, utils/PathUtils.ts and
TestProcessRunner.ts:
Quoting for the terminal. quote() produces "..." with doubled inner quotes on Windows.
quoteArgsForTerminal then re-quotes those arguments for the shell that vscode.env.shell names: single
quotes for PowerShell (with typographic quotes doubled and, for Windows PowerShell 5.1, backslash-escaped
inner quotes) and for Git Bash, and quoteForCmd for cmd.exe, which doubles backslashes before quotes and
writes every % outside the quotes as ^% so %VAR% is not expanded.
.cmd shims when spawning. Node refuses to spawn npx.cmd without a shell. needsWindowsShell checks
whether the executable (searched on PATH) exists only as .cmd/.bat and no .exe/.com; then the whole
command line is quoted with quoteForCmd and run through cmd.exe (shell: true).
Command line length. cmd.exe allows 8,191 characters, a direct spawn about 32,000.
exceedsWindowsCommandLength checks against 7,500 and 30,000. Jest and Vitest then drop the explicit file
arguments and run the project with the name filter (adjustArgsForWindowsLengthLimit); the other
frameworks are split in halves recursively (runInHalves).
Killing. child.kill() only ends cmd.exe or the runner's main process; killProcessTree uses
taskkill /pid <pid> /T /F so worker processes die too.
Paths. toRunnerPath resolves symlinks (and keeps mapped network drives), normalizePath turns
backslashes into slashes and upper-cases the drive letter, and result matching compares paths
case-insensitively.
Logging¶
logTestExecution writes the framework, the full command, the number of tests and files and whether ESM is
on to the Jest Runner channel for every standard run; fast runs log their command too. With
jestrunner.enableDebugLogs the raw output head and the structured parsing steps are logged as well.