Test File Parsing¶
To place CodeLens actions, build the Test Explorer tree and find "the test at the cursor", Jest Runner needs
the structure of a test file: which describe blocks contain which it blocks, with names and positions. Two
parsers in src/parsers/ provide it; src/parser.ts chooses between them.
Entry point¶
- a file that imports
node:test→parseNodeTestFile(nodeTestParser.ts), - everything else (Jest, Vitest, Playwright, Rstest, Bun, Deno) →
parse(parsers/jestParser/).
Both return { root: { children: TestNode[] } } where every node has type (describe, it/test),
name, start and end (line, column) and children. content is optional: the CodeLens provider passes
the editor's text so unsaved changes are reflected; discovery reads the file from disk.
The Babel parser (parsers/jestParser/)¶
A descendant of the parser in jest-editor-support, rewritten around @babel/parser with the typescript,
jsx and flow plugins chosen by file extension (helper.ts). It does not use @babel/traverse; instead
parse() in babelParser.ts walks statement bodies itself, so it can carry a scope (variable bindings)
down the tree.
What becomes a node¶
| Source | Node |
|---|---|
describe(...), describe.only/skip/each(...) |
describe |
test.describe(...), test.describe.parallel/serial/only/skip/fixme/fail(...) (Playwright) |
describe |
it(...), test(...), fit(...), with any modifier chain such as .only, .skip, .todo, .each |
it |
Deno.test(...) |
it |
expect(...) chains |
expect (kept for tooling, no CodeLens) |
test.step(...) (Playwright) |
ignored |
The parser descends into callback bodies of these calls, into function expressions assigned to variables
(const suite = () => { it(...) }), into assignments and into return arguments, so tests generated by
helper functions in the same file are found.
Names¶
applyNameInfo resolves the first argument with astToValue (see Config File Parsing)
against the current scope, so these all get a usable name:
const NAME = 'adds numbers';
it(NAME, ...); // "adds numbers"
it(`${NAME} twice`, ...); // "adds numbers twice"
it(MyClass.prototype.method.name, ...); // "method" (after updateTestNameIfUsingProperties)
it(`dynamic ${value}`, ...); // "dynamic ${value}" → later a (.*?) wildcard
A name that does not resolve is taken verbatim from the source text. Placeholders (${x}, %s, $name)
survive into the name and are turned into (.*?) wildcards when the name is used as a filter
(resolveTestNameStringInterpolation in utils/TestNameUtils.ts).
each expansion¶
For it.each(table)('title %s', fn) and describe.each(table)(...) the parser tries to expand the table at
parse time:
getResolvableEachTableevaluates the table argument with the scope bindings. It must come out as an array; a table that is imported or computed does not.- For each row,
createRowBindingsbinds the callback parameters: a single identifier gets the whole row, an object pattern (({ a, b })) gets the row's properties, several identifiers get the array elements. formatTitlefills the title:%s %d %i %f %j %o %pfrom the row values in order,$propand$obj.nestedfrom object rows,%#and$#with the row index.- One node per row is emitted, all at the same position, each with
eachTemplateset to the original title. Fordescribe.each, the body is walked once per row with the row's bindings, so nestedittitles using the row values are expanded too (tracked with the internal__JTR_DESCRIBE_EACH_INNER_TESTflag).
A table that cannot be resolved yields a single node with the template as its name, which later matches all rows by wildcard.
The CodeLens provider groups the expanded nodes of one line back into a menu (Run All plus one entry per
row); the Test Explorer shows one item per row and TestMatcher aggregates their results when several runner
results match one template item.
Positions¶
registerNode stores the call expression's start and end (Babel lines are 1-based, columns 0-based; the
column is incremented so both are 1-based in the node). Consumers subtract one again for VS Code ranges.
nameRange marks the string argument.
The node:test parser (parsers/nodeTestParser.ts)¶
node:test files use describe/it/test as well, but they may be nested with t.test(...) subtests
and are usually ESM. The parser uses @babel/traverse with errorRecovery on, records every
describe/test/it call with its .only/.skip/.todo modifiers and the position of the enclosing
describe, then builds the tree from the positions (buildTestTree). Names are string literals or
expression-free template literals; other names are skipped.
Consumers¶
| Consumer | Uses |
|---|---|
TestRunnerCodeLensProvider.provideCodeLenses |
all nodes with positions → one CodeLens per option, each menus |
TestRunner.findCurrentTestName |
the node containing the cursor line, joined with its ancestors; a describe line gets the (\s[\s\S]*)? suffix |
testDiscovery.parseTestsInFile / processTestNodes |
nodes → vscode.TestItem tree with range, id <file>:<type>:<line>:<full name> and tags suite / test |
TestFileWatcher.reparse |
re-runs discovery for one file after a save or change |
The id format matters later: toTestItemNamePattern extracts the full name from the id when building the name
filter for a Test Explorer run, so the filter is the raw name rather than the (possibly cleaned) label.