Test Explorer¶
With jestrunner.enableTestExplorer, Jest Runner registers a vscode.TestController and fills the native
Testing view. The code is src/TestController.ts, src/testDiscovery.ts and the helpers in
src/testController/.
Lifecycle¶
extension.ts creates the JestTestController when the setting is on and a workspace folder exists, and
disposes it when the setting is switched off. The constructor wires up:
flowchart TB
TC["JestTestController<br/>vscode.tests.createTestController('jestVitestTestController')"]
TC --> RP["run profiles<br/>Run · Debug · Coverage · Update Snapshots"]
TC --> RH["resolveHandler → ensureTestsDiscovered"]
TC --> TRE["TestRunExecutor<br/>Run, Coverage"]
TC --> DH["DebugHandler<br/>Debug"]
TC --> FW["TestFileWatcher<br/>files created / changed / deleted / opened / saved"]
TC --> CW["TestConfigWatcher<br/>config files and settings → refreshAllTests"]
TC --> CP["CoverageProvider"]
| Profile | Kind | Handler | Default |
|---|---|---|---|
| Run | Run |
TestRunExecutor.runHandler |
yes |
| Debug | Debug |
DebugHandler.debugHandler |
yes |
| Coverage | Coverage |
TestRunExecutor.coverageHandler, with loadDetailedCoverage for the inline details |
yes |
| Update Snapshots | Run |
TestRunExecutor.runHandler with ['-u'] |
no |
Every handler first awaits ensureTestsDiscovered(), so a run requested before the first scan (or during a
refresh) sees the complete tree.
Discovery¶
discoverAllTests() clears all caches and the tree, then runs discoverTests for each workspace folder:
findTestFilesscans withvscode.workspace.findFiles('**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}', '**/node_modules/**')below the workspace folder (orjestrunner.projectPath), and keeps the files for whichtestFileCache.isTestFileis true. The check runs in batches of 50 with asetImmediateyield in between, so a large workspace does not freeze the extension host.- For each test file,
getOrCreateFileTestItemcreates the folder chain (getOrCreateFolderTestItem, one item per directory level, id = absolute path) and the file item (id = absolute path, label = base name). parseTestsInFileparses the file (see Test File Parsing) andprocessTestNodesadds one item perdescribe/it: id<file>:<type>:<line>:<full name>, label = the (cleaned) name,rangefrom the node position, tagsuiteortest. Children are nested under theirdescribe.
The tree is therefore: workspace folder → folders → file → describe → test. Folder and file items carry
canResolveChildren = false; the whole tree is built eagerly.
Refresh coordination¶
refreshAllTests() serializes discoveries: a refresh requested while one is running is queued, and a second
request while one is queued returns the queued promise. A burst of config changes (an npm install touching
many files, a branch switch) therefore costs at most two full discoveries, and two discoveries never clear
each other's items mid-way. ensureTestsDiscovered() awaits a running or queued refresh before a run.
Keeping the tree in sync (TestFileWatcher)¶
| Event | Action |
|---|---|
| file changed on disk | invalidate its detection cache; if it has an item, re-parse (reparse) |
| file created | if it is a test file, create its item and parse it |
| file deleted | invalidate caches, remove the item from the root and from its folder item |
| document opened | if it is a test file without children yet, create and parse |
| document saved | invalidate, create the item if missing, re-parse |
| extension start | parse every already open test document |
reparse compares the file's modification time with the one it last parsed: a save fires both the save event
and the file system watcher, and the second one finds the file unchanged and does nothing. Re-parsing replaces
the file item's children, so ids of moved tests change and VS Code updates the view.
Re-discovering on config changes (TestConfigWatcher)¶
Fires onDidChange (→ refreshAllTests) when:
- a
jestrunner.*,jest.*orvitest.*setting changes, - any file matching
**/<config file>of any framework (frameworkDefinitions.testFrameworks[].configFiles, that isjest.config.*,package.json,vitest.config.*,vite.config.*,bun.lock,deno.json,playwright.config.*,rstest.config.*) is created, changed or deleted, - a file named in
jestrunner.configPathorjestrunner.vitestConfigPath(string or mapping values) is created, changed or deleted; these watchers are rebuilt when the settings change.
Because package.json is among the config files, installing a dependency triggers a refresh, which is how a
freshly added framework is picked up without reloading the window.
Running¶
See Command Building & Execution for TestRunExecutor, and
Reporters & Result Matching for how states come back. In short: collect leaves per file, group
into contexts by framework / cwd / config / selection, one process per context, stream results, add coverage.
Debugging (DebugHandler)¶
The debug profile walks the requested items breadth-first, skips excluded ones, descends into containers and
debugs the first leaf it reaches (a debug session is one process; several tests would need several
sessions). The name pattern comes from the item id (toTestItemNamePattern), the launch configuration from
TestRunnerConfig.getDebugConfiguration(file, name) (see Debugging), and
vscode.debug.startDebugging(workspaceFolder, config) starts it. Debugging a file item passes no name, so the
whole file runs.
Coverage¶
The Coverage profile runs like Run with collectCoverage = true. After the process, processCoverageData
asks CoverageProvider for a CoverageMap, converts it to DetailedFileCoverage objects (a
vscode.FileCoverage subclass carrying the raw data) and adds them to the run. When the user opens a file in
the coverage view, VS Code calls the profile's loadDetailedCoverage, which turns the raw statement, branch
and function maps into vscode.StatementCoverage, BranchCoverage and DeclarationCoverage ranges. See
Coverage.
IDs and labels, and why they matter¶
- Folder and file ids are absolute paths.
TestCollector.findFileItemwalks up from a leaf to the item whose id is the file path to count the file's leaves for the partial-selection check. - Test ids embed the type, the line and the full name (ancestors included).
toTestItemNamePatternextracts that name to build the-tpattern, so the filter uses the real name even when the label was cleaned (Foo.prototype.bar.name→bar) or the item is aneachrow. - Labels are what
TestMatchercompares with result titles; parent labels are the ancestor titles.