Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/adapter/objectPreview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,84 @@ export function formatAsTable(param: Cdp.Runtime.ObjectPreview): string {
return table.map(row => stringUtils.trimEnd(row, maxTableWidth)).join('\n');
}

function formatPropertyPreviewAsJson(prop: Cdp.Runtime.PropertyPreview): string | undefined {
switch (prop.type) {
case 'string':
return JSON.stringify(prop.value ?? '');
case 'number': {
const num = Number(prop.value);
return isFinite(num) ? String(num) : 'null';
}
case 'boolean':
return prop.value === 'true' ? 'true' : 'false';
case 'undefined':
case 'function':
case 'symbol':
case 'accessor':
return undefined; // omit from JSON (matches JSON.stringify behavior)
case 'bigint':
return undefined; // not JSON serializable
case 'object':
if (prop.subtype === 'null') return 'null';
if (prop.valuePreview) return formatObjectPreviewAsJson(prop.valuePreview);
return '{}';
default:
return prop.value !== undefined ? JSON.stringify(prop.value) : 'null';
}
}

function formatObjectPreviewAsJson(preview: Cdp.Runtime.ObjectPreview): string {
if (preview.subtype === 'array' || preview.subtype === 'typedarray') {
const items: (string | undefined)[] = [];
for (const prop of preview.properties || []) {
if (!/^\d+$/.test(prop.name)) continue;
items[parseInt(prop.name, 10)] = formatPropertyPreviewAsJson(prop) ?? 'null';
}
const result: string[] = [];
for (let i = 0; i < items.length; i++) {
result.push(items[i] ?? 'null');
}
return '[' + result.join(',') + ']';
}

const parts: string[] = [];
for (const prop of preview.properties || []) {
const value = formatPropertyPreviewAsJson(prop);
if (value !== undefined) {
parts.push(JSON.stringify(prop.name) + ':' + value);
}
}
return '{' + parts.join(',') + '}';
}

function formatAsJson(param: ObjectPreview.AnyObject): string {
const raw = param as Cdp.Runtime.RemoteObject;

if (raw.type === 'string') {
return JSON.stringify(raw.value ?? raw.description ?? '');
}
if (raw.type === 'number') {
if (raw.unserializableValue) return 'null'; // NaN, Infinity, -Infinity → null in JSON
return String(raw.value ?? raw.description);
}
if (raw.type === 'boolean') {
return String(raw.value);
}
if (raw.type === 'undefined') {
return 'undefined';
}
if (raw.type === 'bigint' || raw.type === 'symbol') {
return 'undefined'; // not JSON serializable
}
if (raw.type === 'function') {
return 'undefined';
}
// raw.type === 'object'
if (raw.subtype === 'null') return 'null';
if (raw.preview) return formatObjectPreviewAsJson(raw.preview);
return '{}';
}

export const messageFormatters: messageFormat.Formatters<ObjectPreview.AnyObject> = new Map([
['', (param, context) => previewRemoteObjectInternal(param, context)],
['s', (param, context) => formatAsString(param as ObjectPreview.StringObj, context.budget)],
Expand All @@ -619,6 +697,7 @@ export const messageFormatters: messageFormat.Formatters<ObjectPreview.AnyObject
formatAsNumber(param as ObjectPreview.Numeric, false, context.budget, undefined),
],
['c', param => messageFormat.formatCssAsAnsi((param as { value: string }).value)],
['j', param => formatAsJson(param)],
['o', (param, context) => previewRemoteObjectInternal(param, context)],
['O', (param, context) => previewRemoteObjectInternal(param, context)],
]);
Expand Down
172 changes: 172 additions & 0 deletions src/adapter/objectPreview/objectPreview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { expect } from 'chai';
import { formatMessage } from '../messageFormat';
import { messageFormatters } from './index';

describe('objectPreview', () => {
describe('%j format specifier', () => {
const format = (fmt: string, ...args: object[]) =>
formatMessage(fmt, args as never, messageFormatters).result;

it('formats a simple object as JSON', () => {
expect(
format('%j', {
type: 'object',
subtype: undefined,
className: 'Object',
description: 'Object',
preview: {
type: 'object',
description: 'Object',
overflow: false,
properties: [{ name: 'host', type: 'string', value: 'localhost' }],
},
}),
).to.equal('{"host":"localhost"}');
});

it('formats a nested object as JSON', () => {
expect(
format('%j', {
type: 'object',
subtype: undefined,
className: 'Object',
description: 'Object',
preview: {
type: 'object',
description: 'Object',
overflow: false,
properties: [
{
name: 'a',
type: 'object',
subtype: undefined,
valuePreview: {
type: 'object',
description: 'Object',
overflow: false,
properties: [{ name: 'b', type: 'number', value: '1' }],
},
},
],
},
}),
).to.equal('{"a":{"b":1}}');
});

it('formats an array as JSON', () => {
expect(
format('%j', {
type: 'object',
subtype: 'array',
className: 'Array',
description: 'Array(3)',
preview: {
type: 'object',
subtype: 'array',
description: 'Array(3)',
overflow: false,
properties: [
{ name: '0', type: 'number', value: '1' },
{ name: '1', type: 'number', value: '2' },
{ name: '2', type: 'number', value: '3' },
],
},
}),
).to.equal('[1,2,3]');
});

it('formats null as JSON', () => {
expect(format('%j', { type: 'object', subtype: 'null' })).to.equal('null');
});

it('formats a string as JSON', () => {
expect(
format('%j', { type: 'string', value: 'hello', subtype: undefined }),
).to.equal('"hello"');
});

it('formats a number as JSON', () => {
expect(
format('%j', { type: 'number', value: 42, description: '42', subtype: undefined }),
).to.equal('42');
});

it('formats NaN as null in JSON', () => {
expect(
format('%j', {
type: 'number',
unserializableValue: 'NaN',
description: 'NaN',
subtype: undefined,
}),
).to.equal('null');
});

it('formats a boolean as JSON', () => {
expect(
format('%j', { type: 'boolean', value: true, description: 'true', subtype: undefined }),
).to.equal('true');
});

it('formats undefined as the string "undefined"', () => {
expect(format('%j', { type: 'undefined', subtype: undefined })).to.equal('undefined');
});

it('formats a function as the string "undefined"', () => {
expect(
format('%j', {
type: 'function',
subtype: undefined,
description: 'function() {}',
}),
).to.equal('undefined');
});

it('handles mixed specifiers correctly', () => {
expect(
format(
'%s: %s %j',
{ type: 'string', value: 'id', subtype: undefined },
{ type: 'string', value: 'Request headers', subtype: undefined },
{
type: 'object',
subtype: undefined,
className: 'Object',
description: 'Object',
preview: {
type: 'object',
description: 'Object',
overflow: false,
properties: [{ name: 'host', type: 'string', value: 'localhost' }],
},
},
),
).to.equal('id: Request headers {"host":"localhost"}');
});

it('omits undefined properties from JSON object', () => {
expect(
format('%j', {
type: 'object',
subtype: undefined,
className: 'Object',
description: 'Object',
preview: {
type: 'object',
description: 'Object',
overflow: false,
properties: [
{ name: 'a', type: 'string', value: 'hello' },
{ name: 'fn', type: 'function', value: 'function()' },
{ name: 'undef', type: 'undefined' },
],
},
}),
).to.equal('{"a":"hello"}');
});
});
});
18 changes: 18 additions & 0 deletions src/test/console/console-format-json-format.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Evaluating: 'console.log("%j", {host: "localhost"})'
stdout> {"host":"localhost"}
stdout> > {"host":"localhost"}
stdout> > arg1: {host: 'localhost'}

Evaluating: 'console.log("%s: %s %j", "id", "Request headers", {host: "localhost"})'
stdout> id: Request headers {"host":"localhost"}
stdout> > id: Request headers {"host":"localhost"}
stdout> > arg3: {host: 'localhost'}

Evaluating: 'console.log("%j", [1, 2, 3])'
stdout> [1,2,3]
stdout> > [1,2,3]
stdout> > arg1: (3) [1, 2, 3]

Evaluating: 'console.log("%j", null)'
stdout> null

11 changes: 11 additions & 0 deletions src/test/console/consoleFormatTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,17 @@ describe('console format', () => {
p.assertLog();
});

itIntegrates('json format', async ({ r }) => {
const p = await r.launchAndLoad('blank');
await p.logger.evaluateAndLog([
`console.log("%j", {host: "localhost"})`,
`console.log("%s: %s %j", "id", "Request headers", {host: "localhost"})`,
`console.log("%j", [1, 2, 3])`,
`console.log("%j", null)`,
]);
p.assertLog();
});

itIntegrates('colors', async ({ r }) => {
const p = await r.launchAndLoad(`blank`);

Expand Down
Loading