From baa559f0e4a245ea968160891b5c0fad8389cde8 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:10:37 +0000 Subject: [PATCH 1/2] test: vendor four Node v26.3.0 tests that already pass test-compile-cache-permission-allowed.js test-compile-cache-typescript-commonjs.js test-compile-cache-typescript-strip-sourcemaps.js test-eslint-prefer-abort-signal-abort.js Verbatim copies of the upstream v26.3.0 files. Each was run one process per file with bunfig.node-test.toml and exits 0 with no runtime changes. --- .../test-compile-cache-permission-allowed.js | 78 ++++++++ .../test-compile-cache-typescript-commonjs.js | 169 ++++++++++++++++++ ...mpile-cache-typescript-strip-sourcemaps.js | 62 +++++++ .../test-eslint-prefer-abort-signal-abort.js | 118 ++++++++++++ 4 files changed, 427 insertions(+) create mode 100644 test/js/node/test/parallel/test-compile-cache-permission-allowed.js create mode 100644 test/js/node/test/parallel/test-compile-cache-typescript-commonjs.js create mode 100644 test/js/node/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js create mode 100644 test/js/node/test/parallel/test-eslint-prefer-abort-signal-abort.js diff --git a/test/js/node/test/parallel/test-compile-cache-permission-allowed.js b/test/js/node/test/parallel/test-compile-cache-permission-allowed.js new file mode 100644 index 000000000000..43ce4c274780 --- /dev/null +++ b/test/js/node/test/parallel/test-compile-cache-permission-allowed.js @@ -0,0 +1,78 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works in existing directory. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); + +function testAllowed(readDir, writeDir, envDir) { + console.log(readDir, writeDir, envDir); // Logging for debugging. + + tmpdir.refresh(); + const dummyDir = tmpdir.resolve('dummy'); + fs.mkdirSync(dummyDir); + const script = tmpdir.resolve(dummyDir, 'empty.js'); + fs.copyFileSync(fixtures.path('empty.js'), script); + // If the directory doesn't exist, permission will just be disallowed. + fs.mkdirSync(tmpdir.resolve(envDir)); + + spawnSyncAndAssert( + process.execPath, + [ + '--permission', + `--allow-fs-read=${dummyDir}`, + `--allow-fs-read=${readDir}`, + `--allow-fs-write=${writeDir}`, + script, + ], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: `${envDir}` + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for .*empty\.js.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [ + '--permission', + `--allow-fs-read=${dummyDir}`, + `--allow-fs-read=${readDir}`, + `--allow-fs-write=${writeDir}`, + script, + ], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: `${envDir}` + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /cache for .*empty\.js was accepted/); + return true; + } + }); +} + +{ + testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), '.compile_cache'); + testAllowed(tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache'), tmpdir.resolve('.compile_cache')); + testAllowed('*', '*', '.compile_cache'); + testAllowed('*', tmpdir.resolve('.compile_cache'), '.compile_cache'); + testAllowed(tmpdir.resolve('.compile_cache'), '*', '.compile_cache'); +} diff --git a/test/js/node/test/parallel/test-compile-cache-typescript-commonjs.js b/test/js/node/test/parallel/test-compile-cache-typescript-commonjs.js new file mode 100644 index 000000000000..60e86c3c0f9d --- /dev/null +++ b/test/js/node/test/parallel/test-compile-cache-typescript-commonjs.js @@ -0,0 +1,169 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works for CommonJS with types. + +const common = require('../common'); +if (!process.config.variables.node_use_amaro) { + common.skip('Requires Amaro'); +} +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +// Check cache for .ts files that would be run as CommonJS. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'ts', 'test-commonjs-parsing.ts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-commonjs-parsing\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-commonjs-parsing\.ts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-commonjs-parsing\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-commonjs-parsing\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-commonjs-parsing\.ts because cache was the same/); + return true; + } + }); +} + +// Check cache for .cts files that require .cts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'cts', 'test-require-commonjs.cts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-require-commonjs\.cts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /writing cache for CommonJS .*test-require-commonjs\.cts.*success/); + assert.match(output, /writing cache for CommonJS .*test-cts-export-foo\.cts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-require-commonjs\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-require-commonjs\.cts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-cts-export-foo\.cts because cache was the same/); + + assert.match(output, /V8 code cache for CommonJS .*test-require-commonjs\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-require-commonjs\.cts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-cts-export-foo\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-cts-export-foo\.cts because cache was the same/); + return true; + } + }); +} + +// Check cache for .cts files that require .mts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'cts', 'test-require-mts-module.cts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-require-mts-module\.cts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /writing cache for CommonJS .*test-require-mts-module\.cts.*success/); + assert.match(output, /writing cache for ESM .*test-mts-export-foo\.mts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-require-mts-module\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-require-mts-module\.cts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-mts-export-foo\.mts because cache was the same/); + + assert.match(output, /V8 code cache for CommonJS .*test-require-mts-module\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-require-mts-module\.cts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-mts-export-foo\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-mts-export-foo\.mts because cache was the same/); + return true; + } + }); +} diff --git a/test/js/node/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js b/test/js/node/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js new file mode 100644 index 000000000000..a6c95e432b78 --- /dev/null +++ b/test/js/node/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js @@ -0,0 +1,62 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE can be used for type stripping and ignores +// --enable-source-maps as there's no difference in the code generated. + +const common = require('../common'); +if (!process.config.variables.node_use_amaro) { + common.skip('Requires Amaro'); +} +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +const dir = tmpdir.resolve('.compile_cache_dir'); +const script = fixtures.path('typescript', 'ts', 'test-typescript.ts'); + +spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-typescript\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-typescript\.ts.*success/); + return true; + } + }); + +// Reloading with source maps should hit the cache generated without source maps, because for +// type stripping, only sourceURL is added regardless of whether source map is enabled. +spawnSyncAndAssert( + process.execPath, + ['--enable-source-maps', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + // Both the transpile cache and the code cache should be missed. + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-typescript\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-typescript\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-typescript\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-typescript\.ts because cache was the same/); + return true; + } + }); diff --git a/test/js/node/test/parallel/test-eslint-prefer-abort-signal-abort.js b/test/js/node/test/parallel/test-eslint-prefer-abort-signal-abort.js new file mode 100644 index 000000000000..954d7df83dab --- /dev/null +++ b/test/js/node/test/parallel/test-eslint-prefer-abort-signal-abort.js @@ -0,0 +1,118 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-abort-signal-abort'); + +const message = 'Use AbortSignal.abort() instead of creating and aborting an AbortController.'; + +new RuleTester().run('prefer-abort-signal-abort', rule, { + valid: [ + 'const signal = AbortSignal.abort();', + ` + const controller = new AbortController(); + controller.abort(); + controller.abort(); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + controller.abort(); + console.log(controller); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + // This comment should not be removed. + controller.abort(); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + setImmediate(() => controller.abort()); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + controller.abort('reason', 'extra'); + fn(controller.signal); + `, + ], + invalid: [ + { + code: ` + const controller = new AbortController(); + controller.abort(); + fn(controller.signal); + `, + errors: [{ message }], + output: ` + fn(AbortSignal.abort()); + `, + }, + { + code: ` + const abortController = new AbortController(); + abortController.abort(new Error('aborted')); + fn({ signal: abortController.signal }); + `, + errors: [{ message }], + output: ` + fn({ signal: AbortSignal.abort(new Error('aborted')) }); + `, + }, + { + code: ` + { + const ac = new AbortController(); + ac.abort(); + await wait({ signal: ac.signal }); + } + `, + errors: [{ message }], + output: ` + { + await wait({ signal: AbortSignal.abort() }); + } + `, + }, + { + code: ` + { + const controller = new AbortController(); + controller.abort(); + fn(controller.signal, controller.signal); + } + `, + errors: [{ message }], + output: ` + { + const controller = AbortSignal.abort(); + fn(controller, controller); + } + `, + }, + { + code: ` + { + const controller = new AbortController(); + controller.abort("reason"); + fn(controller.signal, controller.signal); + } + `, + errors: [{ message }], + output: ` + { + const controller = AbortSignal.abort("reason"); + fn(controller, controller); + } + `, + }, + ] +}); From e7718d3cc81d500708595e40f46571e69dde504e Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:20:36 +0000 Subject: [PATCH 2/2] console: match Node's cli_table layout in the JS Console Three fixes to Console#table (the `node:console` Console class, not the native global console.table): - Cells are left-justified. Node changed cli_table.js from centering to left-justifying, so every table Bun printed had the wrong padding. - Map and Set iterators are previewed instead of consumed as opaque iterables, so `console.table(map.entries())` renders separate Key and Values columns like Node. internal/util/inspect already had a previewEntries helper; it is now exported and wired up. - `isBuffer` was destructured off the `node:buffer` module namespace, where it does not exist, so any table containing a nested object threw "isBuffer is not a function". Vendors the upstream v26.3.0 test-console-table.js, which now passes, and updates console-table-iterators.test.ts to the layout real Node prints. --- src/js/builtins/ConsoleObject.ts | 26 +- src/js/internal/util/inspect.js | 1 + .../console/console-table-iterators.test.ts | 16 +- .../node/test/parallel/test-console-table.js | 293 ++++++++++++++++++ 4 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 test/js/node/test/parallel/test-console-table.js diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index a32e6a684fe6..8dbeb3ef5b0b 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -143,8 +143,11 @@ export function write(this: Console, input) { // TODO: probably could extract `getStringWidth`; probably make that a native function. note how it is copied from `readline.js` export function createConsoleConstructor(console: typeof globalThis.console) { const { inspect, formatWithOptions } = require("node:util"); - const { isBuffer } = require("node:buffer"); + const { + Buffer: { isBuffer }, + } = require("node:buffer"); const { isMapIterator, isSetIterator } = require("node:util/types"); + const { previewEntries } = require("internal/util/inspect"); const { validateObject, validateInteger, validateArray, validateOneOf } = require("internal/validators"); const kMaxGroupIndentation = 1000; @@ -191,11 +194,8 @@ export function createConsoleConstructor(console: typeof globalThis.console) { for (let i = 0; i < row.length; i++) { const cell = row[i]; const len = getStringWidth(cell); - const needed = (columnWidths[i] - len) / 2; - // round(needed) + ceil(needed) will always add up to the amount - // of spaces we need while also left justifying the output. - out += - (StringPrototypeRepeat as any).$call(" ", needed) + cell + StringPrototypeRepeat.$call(" ", Math.ceil(needed)); + const needed = columnWidths[i] - len; + out += cell + StringPrototypeRepeat.$call(" ", Math.ceil(needed)); if (i !== row.length - 1) out += tableChars.middle; } out += tableChars.right; @@ -666,11 +666,11 @@ export function createConsoleConstructor(console: typeof globalThis.console) { const mapIter = isMapIterator(tabularData); let isKeyValue = false; let i = 0; - // if (mapIter) { - // const res = previewEntries(tabularData, true); - // tabularData = res[0]; - // isKeyValue = res[1]; - // } + if (mapIter) { + const res = previewEntries(tabularData, true); + tabularData = res[0]; + isKeyValue = res[1]; + } if (isKeyValue || $isMap(tabularData)) { const keys = []; @@ -693,7 +693,9 @@ export function createConsoleConstructor(console: typeof globalThis.console) { } const setIter = isSetIterator(tabularData); - // if (setIter) tabularData = previewEntries(tabularData); + // Node passes only the iterator; V8's previewEntries infers it. Bun's + // helper needs the flag, and returns [entries, isKeyValue]. + if (setIter) tabularData = previewEntries(tabularData, true)[0]; const setlike = setIter || mapIter || $isSet(tabularData); if (setlike) { diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index ab9a42d718cf..4eacd4c45e21 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -2745,6 +2745,7 @@ export default { formatWithOptions, stripVTControlCharacters, getStringWidth, + previewEntries, //! non-standard properties, should these be kept? (not currently exposed) //stylizeWithColor, //stylizeWithHTML(str, styleType) { diff --git a/test/js/node/console/console-table-iterators.test.ts b/test/js/node/console/console-table-iterators.test.ts index c9eb214a0e8c..a30e16d3b43b 100644 --- a/test/js/node/console/console-table-iterators.test.ts +++ b/test/js/node/console/console-table-iterators.test.ts @@ -24,17 +24,17 @@ test("console.Console#table renders Map and Set iterators", async () => { expect(stderr).toBe(""); } expect(stdout).toMatchInlineSnapshot(` - "┌───────────────────┬────────────┐ - │ (iteration index) │ Values │ - ├───────────────────┼────────────┤ - │ 0 │ [ 'a', 1 ] │ - │ 1 │ [ 'b', 2 ] │ - └───────────────────┴────────────┘ + "┌───────────────────┬─────┬────────┐ + │ (iteration index) │ Key │ Values │ + ├───────────────────┼─────┼────────┤ + │ 0 │ 'a' │ 1 │ + │ 1 │ 'b' │ 2 │ + └───────────────────┴─────┴────────┘ ┌───────────────────┬────────┐ │ (iteration index) │ Values │ ├───────────────────┼────────┤ - │ 0 │ 7 │ - │ 1 │ 8 │ + │ 0 │ 7 │ + │ 1 │ 8 │ └───────────────────┴────────┘ " `); diff --git a/test/js/node/test/parallel/test-console-table.js b/test/js/node/test/parallel/test-console-table.js new file mode 100644 index 000000000000..3dac53a5663b --- /dev/null +++ b/test/js/node/test/parallel/test-console-table.js @@ -0,0 +1,293 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const { Console } = require('console'); + +const queue = []; + +const console = new Console({ write: (x) => { + queue.push(x); +}, removeListener: () => {} }, process.stderr, false); + +function test(data, only, expected) { + if (arguments.length === 2) { + expected = only; + only = undefined; + } + console.table(data, only); + assert.deepStrictEqual( + queue.shift().split('\n'), + expected.trimLeft().split('\n') + ); +} + +assert.throws(() => console.table([], false), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +test(null, 'null\n'); +test(undefined, 'undefined\n'); +test(false, 'false\n'); +test('hi', 'hi\n'); +test(Symbol(), 'Symbol()\n'); +test(function() {}, '[Function (anonymous)]\n'); + +test([1, 2, 3], ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test([Symbol(), 5, [10]], ` +┌─────────┬────┬──────────┐ +│ (index) │ 0 │ Values │ +├─────────┼────┼──────────┤ +│ 0 │ │ Symbol() │ +│ 1 │ │ 5 │ +│ 2 │ 10 │ │ +└─────────┴────┴──────────┘ +`); + +test([null, 5], ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ null │ +│ 1 │ 5 │ +└─────────┴────────┘ +`); + +test([undefined, 5], ` +┌─────────┬───────────┐ +│ (index) │ Values │ +├─────────┼───────────┤ +│ 0 │ undefined │ +│ 1 │ 5 │ +└─────────┴───────────┘ +`); + +test({ a: 1, b: Symbol(), c: [10] }, ` +┌─────────┬────┬──────────┐ +│ (index) │ 0 │ Values │ +├─────────┼────┼──────────┤ +│ a │ │ 1 │ +│ b │ │ Symbol() │ +│ c │ 10 │ │ +└─────────┴────┴──────────┘ +`); + +test(new Map([ ['a', 1], [Symbol(), [2]] ]), ` +┌───────────────────┬──────────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼──────────┼────────┤ +│ 0 │ 'a' │ 1 │ +│ 1 │ Symbol() │ [ 2 ] │ +└───────────────────┴──────────┴────────┘ +`); + +test(new Set([1, 2, Symbol()]), ` +┌───────────────────┬──────────┐ +│ (iteration index) │ Values │ +├───────────────────┼──────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ Symbol() │ +└───────────────────┴──────────┘ +`); + +test({ a: 1, b: 2 }, ['a'], ` +┌─────────┬───┐ +│ (index) │ a │ +├─────────┼───┤ +│ a │ │ +│ b │ │ +└─────────┴───┘ +`); + +test([{ a: 1, b: 2 }, { a: 3, c: 4 }], ['a'], ` +┌─────────┬───┐ +│ (index) │ a │ +├─────────┼───┤ +│ 0 │ 1 │ +│ 1 │ 3 │ +└─────────┴───┘ +`); + +test(new Map([[1, 1], [2, 2], [3, 3]]).entries(), ` +┌───────────────────┬─────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼─────┼────────┤ +│ 0 │ 1 │ 1 │ +│ 1 │ 2 │ 2 │ +│ 2 │ 3 │ 3 │ +└───────────────────┴─────┴────────┘ +`); + +test(new Map([[1, 1], [2, 2], [3, 3]]).values(), ` +┌───────────────────┬────────┐ +│ (iteration index) │ Values │ +├───────────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───────────────────┴────────┘ +`); + +test(new Map([[1, 1], [2, 2], [3, 3]]).keys(), ` +┌───────────────────┬────────┐ +│ (iteration index) │ Values │ +├───────────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───────────────────┴────────┘ +`); + +test(new Set([1, 2, 3]).values(), ` +┌───────────────────┬────────┐ +│ (iteration index) │ Values │ +├───────────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└───────────────────┴────────┘ +`); + + +test({ a: { a: 1, b: 2, c: 3 } }, ` +┌─────────┬───┬───┬───┐ +│ (index) │ a │ b │ c │ +├─────────┼───┼───┼───┤ +│ a │ 1 │ 2 │ 3 │ +└─────────┴───┴───┴───┘ +`); + +test({ a: { a: { a: 1, b: 2, c: 3 } } }, ` +┌─────────┬──────────┐ +│ (index) │ a │ +├─────────┼──────────┤ +│ a │ [Object] │ +└─────────┴──────────┘ +`); + +test({ a: [1, 2] }, ` +┌─────────┬───┬───┐ +│ (index) │ 0 │ 1 │ +├─────────┼───┼───┤ +│ a │ 1 │ 2 │ +└─────────┴───┴───┘ +`); + +test({ a: [1, 2, 3, 4, 5], b: 5, c: { e: 5 } }, ` +┌─────────┬───┬───┬───┬───┬───┬───┬────────┐ +│ (index) │ 0 │ 1 │ 2 │ 3 │ 4 │ e │ Values │ +├─────────┼───┼───┼───┼───┼───┼───┼────────┤ +│ a │ 1 │ 2 │ 3 │ 4 │ 5 │ │ │ +│ b │ │ │ │ │ │ │ 5 │ +│ c │ │ │ │ │ │ 5 │ │ +└─────────┴───┴───┴───┴───┴───┴───┴────────┘ +`); + +test(new Uint8Array([1, 2, 3]), ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test(Buffer.from([1, 2, 3]), ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +└─────────┴────────┘ +`); + +test({ a: undefined }, ['x'], ` +┌─────────┬───┐ +│ (index) │ x │ +├─────────┼───┤ +│ a │ │ +└─────────┴───┘ +`); + +test([], ` +┌─────────┐ +│ (index) │ +├─────────┤ +└─────────┘ +`); + +test(new Map(), ` +┌───────────────────┬─────┬────────┐ +│ (iteration index) │ Key │ Values │ +├───────────────────┼─────┼────────┤ +└───────────────────┴─────┴────────┘ +`); + +test([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ` +┌─────────┬─────┬─────┐ +│ (index) │ a │ b │ +├─────────┼─────┼─────┤ +│ 0 │ 1 │ 'Y' │ +│ 1 │ 'Z' │ 2 │ +└─────────┴─────┴─────┘ +`); + +{ + const line = '─'.repeat(79); + const header = `name${' '.repeat(77)}`; + const name = 'very long long long long long long long long long long long ' + + 'long long long long'; + test([{ name }], ` +┌─────────┬──${line}──┐ +│ (index) │ ${header} │ +├─────────┼──${line}──┤ +│ 0 │ '${name}' │ +└─────────┴──${line}──┘ +`); +} + +test({ foo: '¥', bar: '¥' }, ` +┌─────────┬────────┐ +│ (index) │ Values │ +├─────────┼────────┤ +│ foo │ '¥' │ +│ bar │ '¥' │ +└─────────┴────────┘ +`); + +test({ foo: '你好', bar: 'hello' }, ` +┌─────────┬─────────┐ +│ (index) │ Values │ +├─────────┼─────────┤ +│ foo │ '你好' │ +│ bar │ 'hello' │ +└─────────┴─────────┘ +`); + +// Regression test for prototype pollution via console.table. Earlier versions +// of Node.js created an object with a non-null prototype within console.table +// and then wrote to object[column][index], which lead to an error as well as +// modifications to Object.prototype. +test([{ foo: 10 }, { foo: 20 }], ['__proto__'], ` +┌─────────┬───────────┐ +│ (index) │ __proto__ │ +├─────────┼───────────┤ +│ 0 │ │ +│ 1 │ │ +└─────────┴───────────┘ +`); +assert.strictEqual('0' in Object.prototype, false); +assert.strictEqual('1' in Object.prototype, false);