Reporters & Result Matching¶
A Test Explorer run has to end with a state for every test item: passed, failed (with a message and a location), skipped or errored. This page explains how the runner's output becomes those states: the reporters Jest Runner injects, the framing protocol they use, the fallback parsers, the common result shape and the matching of results to items.
The common shape: JestResults¶
Every path ends in the same structure (src/testResultTypes.ts), modelled on Jest's --json output:
interface JestResults {
success: boolean;
numTotalTests, numPassedTests, numFailedTests, numPendingTests: number;
numTotalTestSuites, numPassedTestSuites, numFailedTestSuites, numPendingTestSuites: number;
testResults: Array<{
name: string; // the test file (absolute, or relative for some JUnit reports)
status: string; // 'passed' | 'failed'
message: string; // file-level error, e.g. an import error
assertionResults: Array<{
ancestorTitles: string[]; // describe names, outermost first
title: string; // the test's own name
fullName?: string; // ancestors + title joined with spaces
status: 'passed' | 'failed' | 'skipped' | 'pending' | 'todo';
duration?: number;
failureMessages?: string[];
location?: { line: number; column: number } | null;
}>;
}>;
}
Because all parsers produce it, testResultProcessor and TestMatcher are framework-agnostic.
Injected reporters¶
src/reporters/ holds three reporter templates as plain JavaScript. They are bundled into the extension as
strings (Vite ?raw import) and written once per session to <tmp>/vscode-jest-runner-reporters/ by
getReporterPaths() (reporterPaths.ts):
| File | Framework API | Passed as |
|---|---|---|
jest-reporter.cjs |
Jest reporter class, onRunComplete(contexts, results) |
--reporters default --reporters <path> |
vitest-reporter.cjs |
Vitest reporter object, onTestRunEnd(testModules) (Vitest 4+) or onFinished(files) (older) |
--reporter=default --reporter=<path> |
node-reporter.mjs |
node:test async generator reporter over test:pass / test:fail / test:skip / test:todo events |
--test-reporter <path> --test-reporter-destination stdout |
Each maps the framework's own result objects to JestResults (the Jest reporter uses testFilePath, marks a
suite with a testExecError as failed; the Vitest reporter walks the task tree and collects file-level
errors; the Node reporter extracts Expected / Received from assertion errors) and then emits one
framed message to stdout.
On Windows the Node reporter path is passed as a file:// URL, because --test-reporter takes a module
specifier.
The framing protocol (reporting/structuredOutput.ts)¶
sessionIdis theJSTR_SESSION_IDenvironment variable the executor sets per run (a UUID), so output of another run, a stale reporter or a nested runner is ignored.typeisresults.lengthis the JSON's length in UTF-16 code units, so the parser can slice the payload without scanning for the end marker through arbitrary JSON content.
extractStructuredMessages(buffer, sessionId) is the incremental parser used on every stdout chunk. It
finds @@JTR_START::, validates the header with a sticky regex, checks the length and the matching end
marker, parses the JSON and returns the messages plus the unconsumed tail: the start of an incomplete
message, or just enough characters to complete a start marker that was split across chunks. Anything that
looks like a marker but does not validate is skipped as noise. parseStructuredResults(output, sessionId) is
the one-shot variant for a complete output.
Fallback parsers (src/parsers/)¶
When no structured block arrived (framework without reporter API, reporter failed to load, output
suppressed), processTestResults(output, tests, run, framework, sessionId) chooses by framework:
| Framework | Parser | Source |
|---|---|---|
| node-test | tapParser.parseTapOutput |
TAP output: # Subtest: nesting, ok/not ok lines, # SKIP/# TODO directives, YAML diagnostics with error, expected, actual, location |
| vitest | OutputParser.parseVitestOutput |
the --reporter=json output, already in the Jest shape (Vitest mimics it) |
| bun, deno, rstest | junitParser.parseJUnitXML |
JUnit XML (Bun and Deno write a file that the executor appended to the output): <testcase name file/classname>, <failure>, <skipped>; names containing > are split into ancestors and title |
| jest and anything else | OutputParser.parseJestOutput |
the --json output, found by brace matching from {"numFailedTestSuites", {"testResults" or {"numTotalTestSuites" even when logs surround it |
If none yields a result, processTestResultsFallback scans the text for pass and fail indicators (PASS,
โ, FAIL, โ, AssertionError, ...) and marks tests accordingly, or errors them with a hint to run from
the terminal.
Applying results to test items (testResultProcessor.ts)¶
processTestResultsFromParsed(results, tests, run):
- All assertion results are flattened and indexed, each with its file name.
- File scoping: candidates for a test are the results of the test's own file. Paths are compared
normalized, case-insensitively on Windows and, when they still differ, with symlinks resolved
(
realpathSync.native), so/var/...and/private/var/...on macOS are the same file. Relative result paths (some JUnit reports) match by suffix. A file with no results of its own may use unattributed results (no or unknown file name, as with TAP), but never another requested file's results. - For every test item,
findPotentialMatchesIn(candidates, test)frommatchers/TestMatcher.tsreturns the results that may belong to it (see below). - No match: if the file as a whole failed without running tests (import error), the test is marked failed with that message; otherwise skipped.
- Several matches for a template label (
%s,$name): all are aggregated withreportTemplateTestResult: failed if any failed, the durations summed, the failure messages concatenated with their titles. - Otherwise
findBestMatchprefers an unused result on the same line, then any unused result, and marks the index used so two tests with the same name consume different results. reportTestResultcallsrun.passed(test, duration),run.failed(test, message, duration)with the message located at the reported line, orrun.skipped(test).
Matching rules (matchers/TestMatcher.ts)¶
A result r matches a test item t (matchesTest) when any of these holds:
r.title === t.label, ort.labelwith template variables (%s,$x,${x}) turned into(.*?)matchesr.title,r.titleor the full pathancestors + titleequalst.labelplus a(n)retry suffix,r.fullName === t.label,- the full path matches the templated label.
Then preferSameAncestors keeps the matches whose ancestorTitles equal the item's ancestor labels (the
parent items up to the file), or at least end with them, so a test named works in describe('a') does not
take the result of works in describe('b').
When nothing matches exactly, matchesShortName tries the last word of the label against the title, a loose
fallback for labels that were cleaned (Foo.prototype.bar.name โ bar), never for labels that are only a
template variable.
A label that is only a template variable (it('%s', ...)) matches by ancestors alone.
Fast mode¶
A single test without coverage skips all of this: executeTestCommandFast reports passed on exit code 0,
failed with the stripped stderr or stdout otherwise, and skipped on cancel. The command has no reporter.