Skip to content

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

parseTestFile(filePath, content?)   // src/parser.ts
  • 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:

  1. getResolvableEachTable evaluates the table argument with the scope bindings. It must come out as an array; a table that is imported or computed does not.
  2. For each row, createRowBindings binds 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.
  3. formatTitle fills the title: %s %d %i %f %j %o %p from the row values in order, $prop and $obj.nested from object rows, %# and $# with the row index.
  4. One node per row is emitted, all at the same position, each with eachTemplate set to the original title. For describe.each, the body is walked once per row with the row's bindings, so nested it titles using the row values are expanded too (tracked with the internal __JTR_DESCRIBE_EACH_INNER_TEST flag).

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.