4b. Title formatting
How Vitest renders a .each / .for title, and how far this analyzer reproduces it. The table
shapes are on .each and .for tables.
Title interpolation directives (printf-style, applied left-to-right against the row’s
positional values, independent of $field mode):
| Directive | Meaning |
|---|---|
%s |
string |
%d |
number |
%i |
integer |
%f |
float |
%j |
JSON |
%o |
object |
%# |
0-based index of the row within the table |
%$ |
1-based index of the row |
%% |
documented as a literal %; measured otherwise, see below |
What the numeric directives actually coerce
Section titled “What the numeric directives actually coerce”Measured against Vitest 5.0.3, one value per row, reading each test’s own currentTestName from
inside the run rather than inferring it from the pattern. %d is Number(value); %i and %f are
parseInt/parseFloat over the value’s string form, which is a different function on a different
input and disagrees with %d on most non-numbers.
| value | %s |
%d |
%i |
%f |
|---|---|---|---|---|
null |
null |
0 |
NaN |
NaN |
true |
true |
1 |
NaN |
NaN |
false |
false |
0 |
NaN |
NaN |
undefined |
undefined |
NaN |
NaN |
NaN |
0 |
0 |
0 |
0 |
0 |
-0 |
-0 |
0 |
0 |
-0 |
1.5 |
1.5 |
1.5 |
1 |
1.5 |
-1.5 |
-1.5 |
-1.5 |
-1 |
-1.5 |
'a' |
a |
NaN |
NaN |
NaN |
'' |
(empty) | 0 |
NaN |
NaN |
'1.5' |
1.5 |
1.5 |
1 |
1.5 |
'12abc' |
12abc |
NaN |
12 |
12 |
'0x1F' |
0x1F |
31 |
31 |
0 |
[] |
(empty) | 0 |
NaN |
NaN |
[2] |
2 |
2 |
2 |
2 |
[2, 3] |
2,3 |
NaN |
2 |
2 |
[null] |
(empty) | 0 |
NaN |
NaN |
[true] |
true |
NaN |
NaN |
NaN |
{ k: 1 } |
{ k: 1 } |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
Infinity |
Infinity |
Infinity |
NaN |
Infinity |
10n |
10n |
10n |
10n |
10 |
Four rows are worth naming because they are the ones a reimplementation gets wrong:
nullandtrueare0and1under%donly. Under%i/%fthey areNaN, becauseparseInt('null')isNaN. A summary that says “nullrenders 0” is true of one directive in three.%i/%fparse a prefix;%ddoes not.'12abc'is12under%i/%fandNaNunder%d.- An array goes through its string form.
[2, 3]is'2,3', so%iand%fyield2while%dyieldsNaN.[]is'', whichNumberreads as0butparseIntreads asNaN. -0keeps its sign under%falone, Vitest’s ownformatTitlerewrites the directive to-%ffor a negative-zero item (read from the 5.0.3 bundle), while%d/%iprint0. A BigInt is neverNumber(value):%d/%ispecial-case it to print the literal’s own10n, while%fhas no such case and printsparseFloat('10')=10.
What this analyzer renders, and where it refuses. The coercions above that yield a real
number are rendered exactly (null→0 and true→1 under %d; '12abc'→12 and [2,3]→2
under %i/%f; Infinity under %d/%f; -0 under %f). Every coercion that yields NaN is
refused, the title stays unrendered and the declaration opts out of title comparison,
because NaN is a title that does not discriminate between rows, and rendering it would report
rows that differ as the same case. The refusal lives in numeric_directive
(src/frontend/extract.rs).
%s is String(value) except for plain objects, which get pretty-format ({ k: 1 }, not
[object Object]). %o is the inspect form throughout ('a', [ 2, 3 ]).
%% consumes an argument (upstream defect)
Section titled “%% consumes an argument (upstream defect)”Documented as a literal %. Measured on Vitest 5.0.3, it emits %, a space, and the next
positional argument, consuming it, so every directive after it shifts by one:
| pattern | row | rendered title |
|---|---|---|
%% x %s y %s z %s |
['A','B','C'] |
% A x B y C z undefined |
x %s y %% z %s |
['A','B','C'] |
x A y % B z C |
x %s y %s z %% |
['A','B','C'] |
x A y B z % C |
%%%% |
['A','B','C'] |
% A% B |
100%% done %s |
['A','B'] |
100% A done B |
only %s |
['A','B','C'] |
only A |
The last row rules out “leftover arguments are appended”: they are not. %% is behaving as a
directive, not as an escape.
This analyzer refuses to render a title containing %% rather than reproducing this, because
it cannot know which Vitest version will run the tests it is reading. It parses configuration and
source without executing either, so the installed version is not a fact available to it. Mirroring
the current behaviour would produce wrong titles for anyone on a version where this is fixed, and a
wrong title is a wrong answer about which rows collide. Refusing costs only that rows with %% in
the title stay uncompared, and the run says so.
Depth truncation and width elision are one 40-character budget
Section titled “Depth truncation and width elision are one 40-character budget”Measured on Vitest 5.0.3 by the same probe method (one .each row per value, reading each
test’s own currentTestName), cross-checked by calling the shipped inspect directly, the two
agree on every value below. The probe is permanent: scripts/check/verify-titles.mjs runs
scripts/vitest-harness/title-values.test.ts through the pinned Vitest and through this analyzer
and fails on any title that differs. Every row of the table below is in that fixture except the
two nested past MAX_CELL_DEPTH, which this analyzer refuses: a refusal has no runtime title to
compare. There is no fixed depth limit and no fixed item-count limit: both
truncations are driven by taskTitleValueFormatTruncate (default 40 characters), applied per
interpolated value. The algorithm, read from the 5.0.3 bundle’s inspect/stringify and confirmed
by the measurements:
- Render the value with pretty-format at
maxDepth: 10. - While the result is ≥ 40 units and
maxDepth > 1, halvemaxDepth(10 → 5 → 2 → 1) and re-render. A composite pastmaxDepthprints as[Array]/[Object]. - If it is still > 40: a string keeps its first 38 units inside the quotes, then
…; an array, object, set or map re-renders atmaxDepth: 1with a binary-searched item width so the result fits 40, marking the cut…(k)wherekis the number of omitted entries; anything else re-renders atmaxDepth: 0.
Every length is JavaScript string.length: UTF-16 code units, so 😀 counts twice, and a cut
that would split a surrogate pair backs off one unit. A bare $field string takes a different
path, truncateString(value, 40): unchanged at 40 units, otherwise the first 39 then …. %s
of a plain object is the same inspect call as %o (the object has no own toString); %s of
a string or an array is not truncated at all.
| value interpolated into the title | rendered (Vitest 5.0.3, truncate 40) |
|---|---|
[0..9], 10 one-digit items (32 chars) |
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], full |
[0..19], 20 items |
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, …(10) ] |
[90..100], 11 two-digit items |
[ 90, 91, 92, 93, 94, 95, 96, 97, …(3) ], the width is searched to fit the budget, not a fixed 10 |
{ <46-char key>: 1 } |
{ …(1) }, the search can find width 0 |
| array nested 9 deep (37 chars) | full, depth alone never truncates |
| array nested 10 deep (41 chars) | [ [ [ [ [ [Array] ] ] ] ] ], halving landed at 5 levels |
| object nested 5 deep (37 chars) | { a: { a: { a: { a: { a: 1 } } } } }, full |
| object nested 6 deep (42 chars) | { a: { a: [Object] } } |
the string 'a'×38 under %o |
'aaaa…aaa', full: 40 units with the quotes |
the string 'a'×39, ×40, ×41, ×45 under %o |
'aaaa…aaa…', 38 characters, then …, inside the quotes |
the string 'a'×38, ×39, ×40 under $field |
full, no quotes |
the string 'a'×41, ×45 under $field |
aaaa…aaa…, 39 characters, then … |
'😀'×19 / '😀'×20 under %o |
full / '😀…😀…', 19 emoji, then … |
'😀'×20 / '😀'×21 under $field |
full / 😀…😀…, 19 emoji, then … |
[ '😀'×10, '😀'×10, 1 ] under %o (33 characters, 53 units) |
[ '😀😀😀😀😀😀😀😀😀😀', …(2) ] |
{ aaaaaaaaaaaaaaa: 1, bbbbbbbbbbbbbbbbbb: 2, ccccccccccccccc: 3 } under %s |
{ aaaaaaaaaaaaaaa: 1, …(2) } |
What this analyzer does with that: the budget is mirrored for the mechanisms this section measures, and the depth refusal stays separate:
- Depth: still a refusal, not a rendering difference.
render_inspect(src/frontend/registry.rs) renders composites only down toMAX_CELL_DEPTH(8), a bound against unbounded recursion over repository content. A composite nested past that bound is refused outright: the cell is unreadable, its row keeps the unrendered pattern, and the declaration opts out of title comparison (not-compared). No partially-rendered title is produced, and the budget mirror does not change this: a value that refuses the full render refuses everything (Vitest’s own halving starts atmaxDepth: 10; ours would start from a render this analyzer cannot prove). - Width: mirrored, within the depth bound. Below the refusal bound,
inspect_within_budgetreproduces the algorithm above in UTF-16 units: the full rendering, thenmaxDepthhalved 5 → 2 → 1 with composites at the limit printing[Array]/[Object], then, still over, a quoted string cut to 38 units plus…(41 with the quotes, exactly as the measured rows land), a bare$fieldstring cut to 39, and an array or object re-rendered atmaxDepth: 1showing the largest item count that fits, marked…(k). The mirror reproduces every row of the measured table within the depth bound byte-for-byte; the two rows nested 9 and 10 deep are pastMAX_CELL_DEPTHand are refused, as the previous point says. A rendering of exactly 40 units is returned as themaxDepth: 1render without the search (the algorithm’s threshold is strictly over). The covered mechanisms are the measured ones:%o/inspect,$field,%sof a plain object, and the tagged-template row’s assembled object.%son a scalar or array,%j, and the numeric directives are not truncated by Vitest either, and stay full here.
Mirroring the budget makes a rendered title a claim about the runtime string: two distinct values
that Vitest titles identically (the same …(k) elision) now render alike here too. That is the
point, and it is precision-safe because title comparison never decides a finding on its own,
the body and context fingerprints still see the full, injective renderings.
[v5 CHANGE] “Test Titles and Inspected Values Use pretty-format”, in v5, interpolated string
values are no longer wrapped in quotes in the rendered title, and a taskTitleValueFormatTruncate
option (default 40 chars) truncates long interpolated values. This changes the exact title string
an extractor reproduces vs. pre-v5 Vitest, the title-rendering logic must be versioned (or the
extractor must not claim byte-exact title reproduction across Vitest majors, only per-major).
Back to the Vitest surface index.