diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index bbdf068e4211..cec5144b296a 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -1,4 +1,4 @@ -import { format, isObject, objDisplay, objectAttr } from '@vitest/utils' +import { format, isNegativeNaN, isObject, objDisplay, objectAttr } from '@vitest/utils' import { parseSingleStack } from '@vitest/utils/source-map' import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types' import type { VitestRunner } from './types/runner' @@ -433,6 +433,21 @@ function formatTitle(template: string, items: any[], idx: number) { .replace(/__vitest_escaped_%__/g, '%%') } const count = template.split('%').length - 1 + + if (template.includes('%f')) { + const placeholders = template.match(/%f/g) || [] + placeholders.forEach((_, i) => { + if (isNegativeNaN(items[i]) || Object.is(items[i], -0)) { + // Replace the i-th occurrence of '%f' with '-%f' + let occurrence = 0 + template = template.replace(/%f/g, (match) => { + occurrence++ + return occurrence === i + 1 ? '-%f' : match + }) + } + }) + } + let formatted = format(template, ...items.slice(0, count)) if (isObject(items[0])) { formatted = formatted.replace( diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index aece4d7abd8b..671fd18bdd6f 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -216,3 +216,14 @@ export function getCallLastIndex(code: string) { } return null } + +export function isNegativeNaN(val: number) { + if (!Number.isNaN(val)) + return false + const f64 = new Float64Array(1) + f64[0] = val + const u32 = new Uint32Array(f64.buffer) + const isNegative = u32[1] >>> 31 === 1 + + return isNegative +} diff --git a/test/core/test/utils-display.spec.ts b/test/core/test/utils-display.spec.ts index 92b1e6eeabe2..094b6423ecf4 100644 --- a/test/core/test/utils-display.spec.ts +++ b/test/core/test/utils-display.spec.ts @@ -60,7 +60,7 @@ describe('format', () => { expect(format(formatString, ...args), `failed ${formatString}`).toBe(util.format(formatString, ...args)) }) - test('cannont serialize some values', () => { + test('cannot serialize some values', () => { expect(() => format('%j', 100n)).toThrowErrorMatchingInlineSnapshot(`[TypeError: Do not know how to serialize a BigInt]`) }) diff --git a/test/core/test/utils.spec.ts b/test/core/test/utils.spec.ts index 3e9344f52b17..931a06e62bea 100644 --- a/test/core/test/utils.spec.ts +++ b/test/core/test/utils.spec.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, test } from 'vitest' -import { assertTypes, createColors, deepClone, objDisplay, objectAttr, toArray } from '@vitest/utils' +import { assertTypes, createColors, deepClone, isNegativeNaN, objDisplay, objectAttr, toArray } from '@vitest/utils' import { deepMerge, resetModules } from '../../../packages/vitest/src/utils' import { deepMergeSnapshot } from '../../../packages/snapshot/src/port/utils' import type { EncodedSourceMap } from '../../../packages/vite-node/src/types' @@ -295,3 +295,19 @@ describe(createColors, () => { expect(c.blue(c.blue('x').repeat(10000))).toBeTruthy() }) }) + +describe('isNegativeNaN', () => { + test.each` + value | expected + ${Number.NaN} | ${false} + ${-Number.NaN} | ${true} + ${0} | ${false} + ${-0} | ${false} + ${1} | ${false} + ${-1} | ${false} + ${Number.POSITIVE_INFINITY} | ${false} + ${Number.NEGATIVE_INFINITY} | ${false} + `('isNegativeNaN($value) -> $expected', ({ value, expected }) => { + expect(isNegativeNaN(value)).toBe(expected) + }) +})