From d36ab01bef7e9c648b28765550da783c5551fd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 10 Jan 2025 02:22:33 -0800 Subject: [PATCH 1/4] Add tests for console.table (#48589) Summary: Changelog: [internal] Added basic tests for the current implementation of the `console.table` polyfill (not the CDP implementation). Differential Revision: D67791579 --- packages/polyfills/__tests__/console-itest.js | 163 ++++++++++++++++++ .../react-native-fantom/config/jest.config.js | 1 + 2 files changed, 164 insertions(+) create mode 100644 packages/polyfills/__tests__/console-itest.js diff --git a/packages/polyfills/__tests__/console-itest.js b/packages/polyfills/__tests__/console-itest.js new file mode 100644 index 00000000000000..d11b80fe617753 --- /dev/null +++ b/packages/polyfills/__tests__/console-itest.js @@ -0,0 +1,163 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +const LOG_LEVELS = { + trace: 0, + info: 1, + warn: 2, + error: 3, +}; + +describe('console', () => { + describe('.table(data, rows)', () => { + it('should print the passed array as a table', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table([ + {name: 'First', value: 500}, + {name: 'Second', value: 600}, + {name: 'Third', value: 700}, + {name: 'Fourth', value: 800, extraValue: true}, + ]); + + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([ + ` +name | value +-------|------ +First | 500 \u0020 +Second | 600 \u0020 +Third | 700 \u0020 +Fourth | 800 `, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + it('should print the passed dictionary as a table', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table({ + first: {name: 'First', value: 500}, + second: {name: 'Second', value: 600}, + third: {name: 'Third', value: 700}, + fourth: {name: 'Fourth', value: 800, extraValue: true}, + }); + + 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 `, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + it('should print an empty string for empty arrays', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table([]); + + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([``, LOG_LEVELS.info]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + it('should print an empty string for empty dictionaries', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table({}); + + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([``, LOG_LEVELS.info]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + // This test is currently failing + it.skip('should print an indices table for an array of empty objects', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table([{}, {}, {}, {}]); + + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([ + ` +(index) +------- +0 \u0020 +1 \u0020 +2 \u0020 +3 `, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + it('should print an indices table for a dictionary of empty objects', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + // TODO: replace with `beforeEach` when supported. + try { + console.table({ + first: {}, + second: {}, + third: {}, + fourth: {}, + }); + + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([ + ` +(index) +------- +first \u0020 +second\u0020 +third \u0020 +fourth `, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + }); +}); diff --git a/packages/react-native-fantom/config/jest.config.js b/packages/react-native-fantom/config/jest.config.js index 8bd2220d349238..1d647a91793f05 100644 --- a/packages/react-native-fantom/config/jest.config.js +++ b/packages/react-native-fantom/config/jest.config.js @@ -19,6 +19,7 @@ module.exports = { roots: [ '/packages/react-native', '/packages/react-native-fantom', + '/packages/polyfills', ], moduleFileExtensions: [...baseConfig.moduleFileExtensions, 'cpp', 'h'], // This allows running Meta-internal tests with the `-test.fb.js` suffix. From 498aceacdf64943233baa6c6b8ee51a46e5a1e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 10 Jan 2025 02:22:33 -0800 Subject: [PATCH 2/4] Prevent console.table from modifying passed values Summary: Changelog: [General][Fixed] Modified `console.table` to avoid mutating the received argument. Differential Revision: D67791795 --- packages/polyfills/__tests__/console-itest.js | 35 +++++++++++++++++++ packages/polyfills/console.js | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/polyfills/__tests__/console-itest.js b/packages/polyfills/__tests__/console-itest.js index d11b80fe617753..069b9fcbd830b7 100644 --- a/packages/polyfills/__tests__/console-itest.js +++ b/packages/polyfills/__tests__/console-itest.js @@ -159,5 +159,40 @@ fourth `, global.nativeLoggingHook = originalNativeLoggingHook; } }); + + it('should not modify the logged value', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + global.nativeLoggingHook = jest.fn(); + + // TODO: replace with `beforeEach` when supported. + try { + const array = [ + {name: 'First', value: 500}, + {name: 'Second', value: 600}, + {name: 'Third', value: 700}, + {name: 'Fourth', value: 800, extraValue: true}, + ]; + const originalArrayValue = JSON.parse(JSON.stringify(array)); + + console.table(array); + + expect(array).toEqual(originalArrayValue); + + const object = { + first: {name: 'First', value: 500}, + second: {name: 'Second', value: 600}, + third: {name: 'Third', value: 700}, + fourth: {name: 'Fourth', value: 800, extraValue: true}, + }; + + const originalObjectValue = JSON.parse(JSON.stringify(object)); + + console.table(object); + + expect(object).toEqual(originalObjectValue); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); }); }); diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index f4aae2d1c22e5a..d459eab17ae8bc 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -440,7 +440,7 @@ function consoleTablePolyfill(rows) { rows = []; for (var key in data) { if (data.hasOwnProperty(key)) { - var row = data[key]; + var row = Object.assign({}, data[key]); row[OBJECT_COLUMN_NAME] = key; rows.push(row); } From 00aa7551931b26108b044be1ac8890cff361a245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 10 Jan 2025 02:22:33 -0800 Subject: [PATCH 3/4] Improve formatting of table in console.table 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 --- packages/polyfills/__tests__/console-itest.js | 144 ++++++++++++++---- packages/polyfills/console.js | 53 +++++-- 2 files changed, 161 insertions(+), 36 deletions(-) diff --git a/packages/polyfills/__tests__/console-itest.js b/packages/polyfills/__tests__/console-itest.js index 069b9fcbd830b7..a02e8fe520e568 100644 --- a/packages/polyfills/__tests__/console-itest.js +++ b/packages/polyfills/__tests__/console-itest.js @@ -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()); @@ -34,12 +34,12 @@ 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 { @@ -47,7 +47,7 @@ Fourth | 800 `, } }); - 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()); @@ -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 { @@ -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()); @@ -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 { @@ -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 { diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index d459eab17ae8bc..f397228ee92bd2 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -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, @@ -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); } } } @@ -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 = []; @@ -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); @@ -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]; From e680d52e6b699195eb26bc9b974125d0ac0e2c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 10 Jan 2025 02:22:33 -0800 Subject: [PATCH 4/4] Add support for the columns option in console.table Summary: Changelog: [General][Added] Add support for the second parameter of `console.table` to specify a list of columns to print in the table. Differential Revision: D67803665 --- packages/polyfills/__tests__/console-itest.js | 62 +++++++++++++++++++ packages/polyfills/console.js | 25 +++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/packages/polyfills/__tests__/console-itest.js b/packages/polyfills/__tests__/console-itest.js index a02e8fe520e568..a58937f6a156d0 100644 --- a/packages/polyfills/__tests__/console-itest.js +++ b/packages/polyfills/__tests__/console-itest.js @@ -284,5 +284,67 @@ describe('console', () => { global.nativeLoggingHook = originalNativeLoggingHook; } }); + + it('should only print the selected columns, if specified (arrays)', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + try { + console.table( + [ + {first: 1, second: 2, third: 3}, + {first: 4, second: 5}, + {third: 7, fourth: 8}, + {fifth: 9}, + ], + // $FlowExpectedError[extra-arg] + ['first', 'fifth'], + ); + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([ + ` +| (index) | first | fifth | +| ------- | ----- | ----- | +| 0 | 1 | | +| 1 | 4 | | +| 2 | | | +| 3 | | 9 |`, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); + + it('should only print the selected columns, if specified (dictionaries)', () => { + const originalNativeLoggingHook = global.nativeLoggingHook; + const logFn = (global.nativeLoggingHook = jest.fn()); + + try { + console.table( + { + a: {first: 1, second: 2, third: 3}, + b: {first: 4, second: 5}, + c: {third: 7, fourth: 8}, + d: {fifth: 9}, + }, + // $FlowExpectedError[extra-arg] + ['first', 'fifth'], + ); + expect(logFn).toHaveBeenCalledTimes(1); + expect(logFn.mock.lastCall).toEqual([ + ` +| (index) | first | fifth | +| ------- | ----- | ----- | +| a | 1 | | +| b | 4 | | +| c | | | +| d | | 9 |`, + LOG_LEVELS.info, + ]); + } finally { + global.nativeLoggingHook = originalNativeLoggingHook; + } + }); }); }); diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index f397228ee92bd2..4f0b9cad52e24b 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -455,17 +455,18 @@ function formatCellValue(cell, key) { return ''; } -function consoleTablePolyfill(rows) { +function consoleTablePolyfill(data, columns) { + var rows; + // convert object -> array - if (Array.isArray(rows)) { - rows = rows.map((row, index) => { + if (Array.isArray(data)) { + rows = data.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)) { @@ -481,12 +482,16 @@ function consoleTablePolyfill(rows) { return; } - var columns = Array.from( - rows.reduce((columnSet, row) => { - Object.keys(row).forEach(key => columnSet.add(key)); - return columnSet; - }, new Set()), - ); + if (Array.isArray(columns)) { + columns = [INDEX_COLUMN_NAME].concat(columns); + } else { + columns = Array.from( + rows.reduce((columnSet, row) => { + Object.keys(row).forEach(key => columnSet.add(key)); + return columnSet; + }, new Set()), + ); + } var stringRows = []; var columnWidths = [];