From ea6d7322ede49a5210129c7fd164065c4fdcacca Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 10 Sep 2025 03:56:57 +0900 Subject: [PATCH] fix: avoid recursively applying `$` and `%` formatting to `test.for/each` title (#8557) --- packages/runner/src/suite.ts | 52 ++++++++++++++++--- packages/utils/src/display.ts | 2 +- .../reporters/fixtures/test-for-title.test.ts | 19 +++++++ test/reporters/tests/default.test.ts | 10 +++- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 917ec7fe6..c7353e0ec 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -17,7 +17,7 @@ import type { TestFunction, TestOptions, } from './types/tasks' -import { format, objDisplay } from '@vitest/utils/display' +import { format, formatRegExp, objDisplay } from '@vitest/utils/display' import { isNegativeNaN, isObject, @@ -865,11 +865,9 @@ function formatTitle(template: string, items: any[], idx: number) { }) } - let formatted = format(template, ...items.slice(0, count)) const isObjectItem = isObject(items[0]) - formatted = formatted.replace( - /\$([$\w.]+)/g, - (_, key: string) => { + function formatAttribute(s: string) { + return s.replace(/\$([$\w.]+)/g, (_, key: string) => { const isArrayKey = /^\d+$/.test(key) if (!isObjectItem && !isArrayKey) { return `$${key}` @@ -878,10 +876,50 @@ function formatTitle(template: string, items: any[], idx: number) { const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement return objDisplay(value, { truncate: runner?.config?.chaiConfig?.truncateThreshold, - }) as unknown as string + }) + }) + } + + let output = '' + let i = 0 + handleRegexMatch( + template, + formatRegExp, + // format "%" + (match) => { + if (i < count) { + output += format(match[0], items[i++]) + } + else { + output += match[0] + } + }, + // format "$" + (nonMatch) => { + output += formatAttribute(nonMatch) }, ) - return formatted + return output +} + +// based on https://github.com/unocss/unocss/blob/2e74b31625bbe3b9c8351570749aa2d3f799d919/packages/autocomplete/src/parse.ts#L11 +function handleRegexMatch( + input: string, + regex: RegExp, + onMatch: (match: RegExpMatchArray) => void, + onNonMatch: (nonMatch: string) => void, +) { + let lastIndex = 0 + for (const m of input.matchAll(regex)) { + if (lastIndex < m.index) { + onNonMatch(input.slice(lastIndex, m.index)) + } + onMatch(m) + lastIndex = m.index + m[0].length + } + if (lastIndex < input.length) { + onNonMatch(input.slice(lastIndex)) + } } function formatTemplateString(cases: any[], args: any[]): any[] { diff --git a/packages/utils/src/display.ts b/packages/utils/src/display.ts index 56189b39c..99b93f49c 100644 --- a/packages/utils/src/display.ts +++ b/packages/utils/src/display.ts @@ -78,7 +78,7 @@ export function stringify( : result } -const formatRegExp = /%[sdjifoOc%]/g +export const formatRegExp: RegExp = /%[sdjifoOc%]/g export function format(...args: unknown[]): string { if (typeof args[0] !== 'string') { diff --git a/test/reporters/fixtures/test-for-title.test.ts b/test/reporters/fixtures/test-for-title.test.ts index 353889928..d44f4faac 100644 --- a/test/reporters/fixtures/test-for-title.test.ts +++ b/test/reporters/fixtures/test-for-title.test.ts @@ -48,3 +48,22 @@ test.for([ { k: "v1" }, { k: "v2" }, ])('not array: 0 = $0, 1 = $1, k = $k', () => {}) + + +test.each([[343434, "$343,434.00"]])( + "handles whole numbers: %s as %s", + (input, expected) => { + expect( + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(input), + ).toBe(expected); + }, +); + +test.each([{ a: "$b", b: "yay" }])("%o", () => {}); +test.each([{ a: "%o" }])("$a", () => {}); +test.each([{ a: "%o" }])("%o", () => {}); +test.each([{ a: "%o" }])("$a %o", () => {}); +test.each([{ a: "%o" }])("%o $a", () => {}); diff --git a/test/reporters/tests/default.test.ts b/test/reporters/tests/default.test.ts index e8512616c..b75c85a27 100644 --- a/test/reporters/tests/default.test.ts +++ b/test/reporters/tests/default.test.ts @@ -237,7 +237,7 @@ describe('default reporter', async () => { }) expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(` - "✓ fixtures/test-for-title.test.ts (18 tests) [...]ms + "✓ fixtures/test-for-title.test.ts (24 tests) [...]ms ✓ test.for object : 0 = 'a', 2 = { te: 'st' } [...]ms ✓ test.for object : 0 = 'b', 2 = [ 'test' ] [...]ms ✓ test.each object : 0 = 'a', 2 = { te: 'st' } [...]ms @@ -255,7 +255,13 @@ describe('default reporter', async () => { ✓ first array element is object: 0 = { k1: 'v1' }, 1 = { k2: 'v2' }, k1 = 'v1', k2 = undefined [...]ms ✓ first array element is not object: 0 = 'foo', 1 = 'bar', k = $k [...]ms ✓ not array: 0 = { k: 'v1' }, 1 = undefined, k = 'v1' [...]ms - ✓ not array: 0 = { k: 'v2' }, 1 = undefined, k = 'v2' [...]ms" + ✓ not array: 0 = { k: 'v2' }, 1 = undefined, k = 'v2' [...]ms + ✓ handles whole numbers: 343434 as $343,434.00 [...]ms + ✓ { a: '$b', b: 'yay' } [...]ms + ✓ '%o' [...]ms + ✓ { a: '%o' } [...]ms + ✓ '%o' { a: '%o' } [...]ms + ✓ { a: '%o' } '%o' [...]ms" `) }) -- 2.51.2