diff --git a/packages/internal/src/utils.js b/packages/internal/src/utils.js index 6e476418ef4d..adde6849d6ec 100644 --- a/packages/internal/src/utils.js +++ b/packages/internal/src/utils.js @@ -87,6 +87,13 @@ const deepMapObjectInternal = (value, name, container, mapper) => { }; /** + * Traverses a record object structure deeply, calling a replacer for each + * enumerable string property values of an object. If none of the values are + * changed, the original object is used as-is, maintaining its identity. + * + * When an object is found as a property value, the replacer is first called on + * it. If not replaced, the object is then traversed. + * * @param {object} obj * @param {(value: any, name: string, record: object) => any} mapper * @returns {object} diff --git a/packages/internal/test/utils.test.js b/packages/internal/test/utils.test.js index 94e8a82c6e71..7d1b9ba5092b 100644 --- a/packages/internal/test/utils.test.js +++ b/packages/internal/test/utils.test.js @@ -31,15 +31,20 @@ test('deeplyFulfilledObject', async t => { }); }); +/** + * @typedef {object} DeepMapObjectTestParams + * @property {any} input + * @property {[any, any][]} replacements + * @property {string[][]} unchangedPaths + * @property {any} [expectedOutput] + */ + +/** @type {import('ava').Macro<[DeepMapObjectTestParams]>} */ const deepMapObjectTest = test.macro({ - /** - * @param {import('ava').ExecutionContext} t - * @param {any} input - * @param {[any, any][]} replacements - * @param {string[][]} unchangedPaths - * @param {any} [expectedOutput] - */ - exec(t, input, replacements, unchangedPaths, expectedOutput) { + title(providedTitle, { input }) { + return `deepMapObject - ${providedTitle || JSON.stringify(input)}`; + }, + exec(t, { input, replacements, unchangedPaths, expectedOutput }) { const replacementMap = new Map(replacements); const output = deepMapObject(input, val => replacementMap.has(val) ? replacementMap.get(val) : val, @@ -65,64 +70,58 @@ const deepMapObjectTest = test.macro({ t.deepEqual(output, expectedOutput); } }, - title(providedTitle, input) { - return `deepMapObject - ${providedTitle || JSON.stringify(input)}`; - }, }); -test('identity', deepMapObjectTest, { foo: 42 }, [], [[]]); -test( - 'non object', - deepMapObjectTest, - 'not an object', - [['not an object', 'not replaced']], - [[]], - 'not an object', -); -test( - 'one level deep', - deepMapObjectTest, - { replace: 'replace me', notChanged: {} }, - [['replace me', 'replaced']], - [['notChanged']], - { replace: 'replaced', notChanged: {} }, -); +test('identity', deepMapObjectTest, { + input: { foo: 42 }, + replacements: [], + unchangedPaths: [[]], +}); +test('non object', deepMapObjectTest, { + input: 'not an object', + replacements: [['not an object', 'not replaced']], + unchangedPaths: [[]], + expectedOutput: 'not an object', +}); +test('one level deep', deepMapObjectTest, { + input: { replace: 'replace me', notChanged: {} }, + replacements: [['replace me', 'replaced']], + unchangedPaths: [['notChanged']], + expectedOutput: { replace: 'replaced', notChanged: {} }, +}); const testRecord = { maybeReplace: 'replace me' }; -test( - 'replace first before deep map', - deepMapObjectTest, - { replace: testRecord, notChanged: {} }, - [ +test('replace first before deep map', deepMapObjectTest, { + input: { replace: testRecord, notChanged: {} }, + replacements: [ [testRecord, { different: 'something new' }], ['replace me', 'should not be replaced'], ], - [['notChanged']], - { replace: { different: 'something new' }, notChanged: {} }, -); + unchangedPaths: [['notChanged']], + expectedOutput: { replace: { different: 'something new' }, notChanged: {} }, +}); -test( - 'not mapping container', - deepMapObjectTest, - testRecord, - [ +test('not mapping top level container', deepMapObjectTest, { + input: testRecord, + replacements: [ [testRecord, { different: 'should not be different' }], ['replace me', 'replaced'], ], - [], - { maybeReplace: 'replaced' }, -); -test( - 'deep mapping', - deepMapObjectTest, - { + unchangedPaths: [], + expectedOutput: { maybeReplace: 'replaced' }, +}); +test('deep mapping', deepMapObjectTest, { + input: { one: { two: { three: 'replace me' }, notChanged: {} }, another: 'replace me', }, - [['replace me', 'replaced']], - [['one', 'notChanged']], - { one: { two: { three: 'replaced' }, notChanged: {} }, another: 'replaced' }, -); + replacements: [['replace me', 'replaced']], + unchangedPaths: [['one', 'notChanged']], + expectedOutput: { + one: { two: { three: 'replaced' }, notChanged: {} }, + another: 'replaced', + }, +}); test('makeMeasureSeconds', async t => { const times = [1000.25, 2000.75, NaN];