bun:test: format test.each titles with Jest's placeholder semantics

Closed
RO
8d ago
+340-140

Fixes #37821

Problem

  • test.each and describe.each titles diverge from Jest in three ways. A placeholder only substitutes when the argument already has the matching type (%s needs a string, %d/%f a number, %i an Int52), otherwise the literal token stays and the argument is still consumed: test.each([[1, 2, 3]])("%s + %s = %s") gives %s + %s = %s. %# and %% stop working once the positional arguments run out, and an unknown token loses its % (%z prints z). In the $var form, a null or undefined value, $#, ${a} and $5 each skip the character after the token (a=$a|b=$b| gives a=$ab=5|).
  • The cause is format_label in src/runtime/test_runner/jest.rs. The % arm was guarded by args_idx < args.len(), each specifier had a type gate, the _ arm dropped the %, and the $var fallback fell through to the loop's idx += 1.
  • Titles feed -t filtering, JUnit names and snapshot keys, so a wrong title is not only cosmetic. A suite that had a wrong title recorded in a .snap file gets a new key after this change.

Fix

  • %s, %d, %i, %f apply the util.format coercions Jest uses: String(), Number(), parseInt(), parseFloat(). %i and %f go through JSC's own parsers (ParseInt.h, and the parseFloat logic of JSGlobalObjectFunctions.cpp), exposed as JSValue::parse_int and JSValue::parse_float. A BigInt prints as 10n, a Symbol gives NaN for the numeric specifiers. %j/%o stay JSON.stringify, %p and %O pretty-print.
  • %%, %# and %$ (1-based index, Jest 30) always apply. A % with no argument left, or with an unknown letter, stays as written. $# is the row index. A $ that does not start an identifier is literal and the next character is kept. An existing null/undefined property renders as null/undefined; a missing property stays literal.
  • Non-string values in a title print on one line ({ a: 1 }), so a title never spans lines. docs/test/writing-tests.mdx lists %O, %$ and $#.
  • Verified: test/cli/test/bun-test.test.ts (two new tests, two existing expectations that encoded the eaten character are corrected). Stock bun fails 4 tests. Also jest-each.test.ts, jest-each-gc-root.test.ts, isolation, test-shard.

Background

  • Jest formats a title in jest-each/src/table/array.ts: %% becomes %, %#/%$ are replaced first, then each row value consumes the first remaining %[sdifjoOp] through util.format. Object rows use interpolation.ts: $key[.path] renders the value, $# the index.
  • Self-reviewed: 5 concerns raised, 4 addressed (string quoting for %p/%O, JSC-backed parsers, docs rows, lineage below). Not taken: hoisting Jest's template-vs-printf mode decision out of the per-row loop. Bun has always applied both forms in one pass, and changing that is a separate behaviour change.

Supersedes #38831 (the %s gate, by @deepshekhardas) and #34977 (the $var null/undefined case), and covers item 3 of #32817.


no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/test/bun-test.test.ts