Skip to content

Commit

Permalink
Improve formatting of table in console.table
Browse files Browse the repository at this point in the history
Summary:
Changelog: [General][Changed] Improved formatting of values logged via `console.table` (including Markdown format).

This provides several improvements over the format of tables logged via `console.table`:
* Markdown format for easy integration in existing documents.
* Increased alignment with the spec and Chrome/Firefox implementations:
  * Added index columns.
  * Logged all available columns.
  * Format for all types of values (including objects, functions, etc.).

Differential Revision: D67794858
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jan 10, 2025
1 parent ad2bd5a commit b2315a7
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 36 deletions.
144 changes: 117 additions & 27 deletions packages/polyfills/__tests__/console-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const LOG_LEVELS = {

describe('console', () => {
describe('.table(data, rows)', () => {
it('should print the passed array as a table', () => {
it('should print the passed array as a Markdown table', () => {
const originalNativeLoggingHook = global.nativeLoggingHook;
const logFn = (global.nativeLoggingHook = jest.fn());

Expand All @@ -34,20 +34,20 @@ describe('console', () => {
expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
name | value
-------|------
First | 500 \u0020
Second | 600 \u0020
Third | 700 \u0020
Fourth | 800 `,
| (index) | name | value | extraValue |
| ------- | -------- | ----- | ---------- |
| 0 | 'First' | 500 | |
| 1 | 'Second' | 600 | |
| 2 | 'Third' | 700 | |
| 3 | 'Fourth' | 800 | true |`,
LOG_LEVELS.info,
]);
} finally {
global.nativeLoggingHook = originalNativeLoggingHook;
}
});

it('should print the passed dictionary as a table', () => {
it('should print the passed dictionary as a Markdown table', () => {
const originalNativeLoggingHook = global.nativeLoggingHook;
const logFn = (global.nativeLoggingHook = jest.fn());

Expand All @@ -63,12 +63,102 @@ Fourth | 800 `,
expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
(index) | name | value
--------|--------|------
first | First | 500 \u0020
second | Second | 600 \u0020
third | Third | 700 \u0020
fourth | Fourth | 800 `,
| (index) | name | value | extraValue |
| ------- | -------- | ----- | ---------- |
| first | 'First' | 500 | |
| second | 'Second' | 600 | |
| third | 'Third' | 700 | |
| fourth | 'Fourth' | 800 | true |`,
LOG_LEVELS.info,
]);
} finally {
global.nativeLoggingHook = originalNativeLoggingHook;
}
});

it('should work with different types of values', () => {
const originalNativeLoggingHook = global.nativeLoggingHook;
const logFn = (global.nativeLoggingHook = jest.fn());

// TODO: replace with `beforeEach` when supported.
try {
console.table([
{
string: '',
number: 0,
boolean: true,
function: () => {},
object: {a: 1, b: 2},
null: null,
undefined: undefined,
},
{
string: 'a',
number: 1,
boolean: true,
function: () => {},
object: {a: 1, b: 2},
null: null,
undefined: undefined,
},
{
string: 'aa',
number: 2,
boolean: false,
function: () => {},
object: {a: 1, b: 2},
null: null,
undefined: undefined,
},
{
string: 'aaa',
number: 3,
boolean: false,
function: () => {},
object: {a: 1, b: 2},
null: null,
undefined: undefined,
},
]);

expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
| (index) | string | number | boolean | function | object | null | undefined |
| ------- | ------ | ------ | ------- | -------- | ------ | ---- | --------- |
| 0 | '' | 0 | true | ƒ | {…} | null | undefined |
| 1 | 'a' | 1 | true | ƒ | {…} | null | undefined |
| 2 | 'aa' | 2 | false | ƒ | {…} | null | undefined |
| 3 | 'aaa' | 3 | false | ƒ | {…} | null | undefined |`,
LOG_LEVELS.info,
]);
} finally {
global.nativeLoggingHook = originalNativeLoggingHook;
}
});

it('should print the keys in all the objects', () => {
const originalNativeLoggingHook = global.nativeLoggingHook;
const logFn = (global.nativeLoggingHook = jest.fn());

// TODO: replace with `beforeEach` when supported.
try {
console.table([
{name: 'foo'},
{name: 'bar', value: 1},
{value: 2, surname: 'baz'},
{address: 'other'},
]);

expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
| (index) | name | value | surname | address |
| ------- | ----- | ----- | ------- | ------- |
| 0 | 'foo' | | | |
| 1 | 'bar' | 1 | | |
| 2 | | 2 | 'baz' | |
| 3 | | | | 'other' |`,
LOG_LEVELS.info,
]);
} finally {
Expand Down Expand Up @@ -107,7 +197,7 @@ fourth | Fourth | 800 `,
});

// This test is currently failing
it.skip('should print an indices table for an array of empty objects', () => {
it('should print an indices table for an array of empty objects', () => {
const originalNativeLoggingHook = global.nativeLoggingHook;
const logFn = (global.nativeLoggingHook = jest.fn());

Expand All @@ -118,12 +208,12 @@ fourth | Fourth | 800 `,
expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
(index)
-------
0 \u0020
1 \u0020
2 \u0020
3 `,
| (index) |
| ------- |
| 0 |
| 1 |
| 2 |
| 3 |`,
LOG_LEVELS.info,
]);
} finally {
Expand All @@ -147,12 +237,12 @@ fourth | Fourth | 800 `,
expect(logFn).toHaveBeenCalledTimes(1);
expect(logFn.mock.lastCall).toEqual([
`
(index)
-------
first \u0020
second\u0020
third \u0020
fourth `,
| (index) |
| ------- |
| first |
| second |
| third |
| fourth |`,
LOG_LEVELS.info,
]);
} finally {
Expand Down
53 changes: 44 additions & 9 deletions packages/polyfills/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ const inspect = (function () {
return inspect;
})();

const OBJECT_COLUMN_NAME = '(index)';
const INDEX_COLUMN_NAME = '(index)';
const LOG_LEVELS = {
trace: 0,
info: 1,
Expand Down Expand Up @@ -433,16 +433,46 @@ function repeat(element, n) {
});
}

function formatCellValue(cell, key) {
if (key === INDEX_COLUMN_NAME) {
return cell[key];
}

if (cell.hasOwnProperty(key)) {
var cellValue = cell[key];

switch (typeof cellValue) {
case 'function':
return 'ƒ';
case 'string':
return "'" + cellValue + "'";
case 'object':
return cellValue == null ? 'null' : '{…}';
}

return String(cellValue);
}
return '';
}

function consoleTablePolyfill(rows) {
// convert object -> array
if (!Array.isArray(rows)) {
if (Array.isArray(rows)) {
rows = rows.map((row, index) => {
var processedRow = {};
processedRow[INDEX_COLUMN_NAME] = String(index);
Object.assign(processedRow, row);
return processedRow;
});
} else {
var data = rows;
rows = [];
for (var key in data) {
if (data.hasOwnProperty(key)) {
var row = Object.assign({}, data[key]);
row[OBJECT_COLUMN_NAME] = key;
rows.push(row);
var processedRow = {};
processedRow[INDEX_COLUMN_NAME] = key;
Object.assign(processedRow, data[key]);
rows.push(processedRow);
}
}
}
Expand All @@ -451,7 +481,12 @@ function consoleTablePolyfill(rows) {
return;
}

var columns = Object.keys(rows[0]).sort();
var columns = Array.from(
rows.reduce((columnSet, row) => {
Object.keys(row).forEach(key => columnSet.add(key));
return columnSet;
}, new Set()),
);
var stringRows = [];
var columnWidths = [];

Expand All @@ -460,7 +495,7 @@ function consoleTablePolyfill(rows) {
columns.forEach(function (k, i) {
columnWidths[i] = k.length;
for (var j = 0; j < rows.length; j++) {
var cellStr = (rows[j][k] || '?').toString();
var cellStr = formatCellValue(rows[j], k);
stringRows[j] = stringRows[j] || [];
stringRows[j][i] = cellStr;
columnWidths[i] = Math.max(columnWidths[i], cellStr.length);
Expand All @@ -475,13 +510,13 @@ function consoleTablePolyfill(rows) {
return cell + extraSpaces;
});
space = space || ' ';
return cells.join(space + '|' + space);
return '| ' + cells.join(space + '|' + space) + ' |';
}

var separators = columnWidths.map(function (columnWidth) {
return repeat('-', columnWidth).join('');
});
var separatorRow = joinRow(separators, '-');
var separatorRow = joinRow(separators);
var header = joinRow(columns);
var table = [header, separatorRow];

Expand Down

0 comments on commit b2315a7

Please sign in to comment.