[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): string formatting bug when including placeholders in console.log (fix #9030) (#9131)

Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>

authored by

Michael Debs
Hiroshi Ogawa
and committed by
GitHub
(Dec 15, 2025, 10:22 AM +0900) 84a30621 6a024c50

+42 -19
+28 -6
packages/utils/src/display.ts
··· 80 80 81 81 export const formatRegExp: RegExp = /%[sdjifoOc%]/g 82 82 83 - export function format(...args: unknown[]): string { 83 + interface FormatOptions { 84 + prettifyObject?: boolean 85 + } 86 + 87 + function baseFormat(args: unknown[], options: FormatOptions = {}): string { 88 + const formatArg = (item: unknown, inspecOptions?: LoupeOptions) => { 89 + if (options.prettifyObject) { 90 + return stringify(item, undefined, { 91 + printBasicPrototype: false, 92 + escapeString: false, 93 + }) 94 + } 95 + return inspect(item, inspecOptions) 96 + } 97 + 84 98 if (typeof args[0] !== 'string') { 85 99 const objects = [] 86 100 for (let i = 0; i < args.length; i++) { 87 - objects.push(inspect(args[i], { depth: 0, colors: false })) 101 + objects.push(formatArg(args[i], { depth: 0, colors: false })) 88 102 } 89 103 return objects.join(' ') 90 104 } ··· 112 126 if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { 113 127 return value.toString() 114 128 } 115 - return inspect(value, { depth: 0, colors: false }) 129 + return formatArg(value, { depth: 0, colors: false }) 116 130 } 117 131 return String(value) 118 132 } ··· 133 147 case '%f': 134 148 return Number.parseFloat(String(args[i++])).toString() 135 149 case '%o': 136 - return inspect(args[i++], { showHidden: true, showProxy: true }) 150 + return formatArg(args[i++], { showHidden: true, showProxy: true }) 137 151 case '%O': 138 - return inspect(args[i++]) 152 + return formatArg(args[i++]) 139 153 case '%c': { 140 154 i++ 141 155 return '' ··· 168 182 str += ` ${x}` 169 183 } 170 184 else { 171 - str += ` ${inspect(x)}` 185 + str += ` ${formatArg(x)}` 172 186 } 173 187 } 174 188 return str 189 + } 190 + 191 + export function format(...args: unknown[]): string { 192 + return baseFormat(args) 193 + } 194 + 195 + export function browserFormat(...args: unknown[]): string { 196 + return baseFormat(args, { prettifyObject: true }) 175 197 } 176 198 177 199 export function inspect(obj: unknown, options: LoupeOptions = {}): string {
+4
test/browser/specs/runner.test.ts
··· 128 128 expect(stdout).toMatch(/time: [\d.]+ ms/) 129 129 expect(stdout).toMatch(/\[console-time-fake\]: [\d.]+ ms/) 130 130 expect(stdout).not.toContain('[console-time-fake]: 0 ms') 131 + expect(stdout).toContain('hello from one') 132 + expect(stdout).toContain(`hello from two { 133 + "hello": "object", 134 + }`) 131 135 }) 132 136 133 137 test('logs are redirected to stderr', () => {
+1
test/core/test/exports.test.ts
··· 231 231 "DecodedMap": "function", 232 232 "SpyModule": "object", 233 233 "__INTERNAL": "object", 234 + "browserFormat": "function", 234 235 "collectTests": "function", 235 236 "format": "function", 236 237 "getOriginalPosition": "function",
+1
packages/vitest/src/public/browser.ts
··· 12 12 export * as SpyModule from '@vitest/spy' 13 13 export type { LoupeOptions, ParsedStack, StringifyOptions } from '@vitest/utils' 14 14 export { 15 + browserFormat, 15 16 format, 16 17 inspect, 17 18 stringify,
+3 -13
packages/browser/src/client/tester/logger.ts
··· 1 - import { format, stringify } from 'vitest/internal/browser' 1 + import { browserFormat } from 'vitest/internal/browser' 2 2 import { getConfig } from '../utils' 3 3 import { rpc } from './rpc' 4 4 import { getBrowserRunner } from './runner' ··· 30 30 31 31 console.dir = (item, options) => { 32 32 dir(item, options) 33 - sendLog('stdout', formatInput(item)) 33 + sendLog('stdout', browserFormat(item)) 34 34 } 35 35 36 36 console.dirxml = (...args) => { ··· 113 113 } 114 114 } 115 115 116 - function formatInput(input: unknown) { 117 - if (typeof input === 'object') { 118 - return stringify(input, undefined, { 119 - printBasicPrototype: false, 120 - escapeString: false, 121 - }) 122 - } 123 - return format(input) 124 - } 125 - 126 116 function processLog(args: unknown[]) { 127 - return args.map(formatInput).join(' ') 117 + return browserFormat(...args) 128 118 } 129 119 130 120 function sendLog(
+5
test/browser/fixtures/print-logs/test/logs.test.ts
··· 68 68 await new Promise(r => setTimeout(r, 500)) 69 69 console.timeEnd('[console-time-fake]') 70 70 }) 71 + 72 + test('log with placeholders', () => { 73 + console.log('hello from %s', "one") 74 + console.log('hello from %s', "two", { hello: 'object' }) 75 + })