From 84a306210cefa1caa6b9849a41fd26ec01775074 Mon Sep 17 00:00:00 2001 From: Michael Debs Date: Mon, 15 Dec 2025 03:22:27 +0200 Subject: [PATCH] fix(browser): string formatting bug when including placeholders in console.log (fix #9030) (#9131) Co-authored-by: Hiroshi Ogawa --- packages/browser/src/client/tester/logger.ts | 16 ++------- packages/utils/src/display.ts | 34 +++++++++++++++---- packages/vitest/src/public/browser.ts | 1 + .../fixtures/print-logs/test/logs.test.ts | 5 +++ test/browser/specs/runner.test.ts | 4 +++ test/core/test/exports.test.ts | 1 + 6 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/browser/src/client/tester/logger.ts b/packages/browser/src/client/tester/logger.ts index 88ebb4cef..6d497cfc0 100644 --- a/packages/browser/src/client/tester/logger.ts +++ b/packages/browser/src/client/tester/logger.ts @@ -1,4 +1,4 @@ -import { format, stringify } from 'vitest/internal/browser' +import { browserFormat } from 'vitest/internal/browser' import { getConfig } from '../utils' import { rpc } from './rpc' import { getBrowserRunner } from './runner' @@ -30,7 +30,7 @@ export function setupConsoleLogSpy(): void { console.dir = (item, options) => { dir(item, options) - sendLog('stdout', formatInput(item)) + sendLog('stdout', browserFormat(item)) } console.dirxml = (...args) => { @@ -113,18 +113,8 @@ function stderr(base: (...args: unknown[]) => void) { } } -function formatInput(input: unknown) { - if (typeof input === 'object') { - return stringify(input, undefined, { - printBasicPrototype: false, - escapeString: false, - }) - } - return format(input) -} - function processLog(args: unknown[]) { - return args.map(formatInput).join(' ') + return browserFormat(...args) } function sendLog( diff --git a/packages/utils/src/display.ts b/packages/utils/src/display.ts index 99b93f49c..0a47fb88f 100644 --- a/packages/utils/src/display.ts +++ b/packages/utils/src/display.ts @@ -80,11 +80,25 @@ export function stringify( export const formatRegExp: RegExp = /%[sdjifoOc%]/g -export function format(...args: unknown[]): string { +interface FormatOptions { + prettifyObject?: boolean +} + +function baseFormat(args: unknown[], options: FormatOptions = {}): string { + const formatArg = (item: unknown, inspecOptions?: LoupeOptions) => { + if (options.prettifyObject) { + return stringify(item, undefined, { + printBasicPrototype: false, + escapeString: false, + }) + } + return inspect(item, inspecOptions) + } + if (typeof args[0] !== 'string') { const objects = [] for (let i = 0; i < args.length; i++) { - objects.push(inspect(args[i], { depth: 0, colors: false })) + objects.push(formatArg(args[i], { depth: 0, colors: false })) } return objects.join(' ') } @@ -112,7 +126,7 @@ export function format(...args: unknown[]): string { if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { return value.toString() } - return inspect(value, { depth: 0, colors: false }) + return formatArg(value, { depth: 0, colors: false }) } return String(value) } @@ -133,9 +147,9 @@ export function format(...args: unknown[]): string { case '%f': return Number.parseFloat(String(args[i++])).toString() case '%o': - return inspect(args[i++], { showHidden: true, showProxy: true }) + return formatArg(args[i++], { showHidden: true, showProxy: true }) case '%O': - return inspect(args[i++]) + return formatArg(args[i++]) case '%c': { i++ return '' @@ -168,12 +182,20 @@ export function format(...args: unknown[]): string { str += ` ${x}` } else { - str += ` ${inspect(x)}` + str += ` ${formatArg(x)}` } } return str } +export function format(...args: unknown[]): string { + return baseFormat(args) +} + +export function browserFormat(...args: unknown[]): string { + return baseFormat(args, { prettifyObject: true }) +} + export function inspect(obj: unknown, options: LoupeOptions = {}): string { if (options.truncate === 0) { options.truncate = Number.POSITIVE_INFINITY diff --git a/packages/vitest/src/public/browser.ts b/packages/vitest/src/public/browser.ts index f3488a2d6..930e99f52 100644 --- a/packages/vitest/src/public/browser.ts +++ b/packages/vitest/src/public/browser.ts @@ -12,6 +12,7 @@ export { collectTests, startTests } from '@vitest/runner' export * as SpyModule from '@vitest/spy' export type { LoupeOptions, ParsedStack, StringifyOptions } from '@vitest/utils' export { + browserFormat, format, inspect, stringify, diff --git a/test/browser/fixtures/print-logs/test/logs.test.ts b/test/browser/fixtures/print-logs/test/logs.test.ts index 036373aff..ef17630ae 100644 --- a/test/browser/fixtures/print-logs/test/logs.test.ts +++ b/test/browser/fixtures/print-logs/test/logs.test.ts @@ -68,3 +68,8 @@ test('console.time', async () => { await new Promise(r => setTimeout(r, 500)) console.timeEnd('[console-time-fake]') }) + +test('log with placeholders', () => { + console.log('hello from %s', "one") + console.log('hello from %s', "two", { hello: 'object' }) +}) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 00c0d56f9..68c95a826 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -128,6 +128,10 @@ describe('console logging tests', async () => { expect(stdout).toMatch(/time: [\d.]+ ms/) expect(stdout).toMatch(/\[console-time-fake\]: [\d.]+ ms/) expect(stdout).not.toContain('[console-time-fake]: 0 ms') + expect(stdout).toContain('hello from one') + expect(stdout).toContain(`hello from two { + "hello": "object", +}`) }) test('logs are redirected to stderr', () => { diff --git a/test/core/test/exports.test.ts b/test/core/test/exports.test.ts index 849371ed2..387fc6bcd 100644 --- a/test/core/test/exports.test.ts +++ b/test/core/test/exports.test.ts @@ -231,6 +231,7 @@ it('exports snapshot', async ({ skip, task }) => { "DecodedMap": "function", "SpyModule": "object", "__INTERNAL": "object", + "browserFormat": "function", "collectTests": "function", "format": "function", "getOriginalPosition": "function", -- 2.51.2