From b00e9171084eb6cbce289fa3f07a09176bca4cba Mon Sep 17 00:00:00 2001 From: Krisztiaan Date: Sun, 10 Aug 2025 14:17:46 +0200 Subject: [PATCH] Add tests for hook differences in getUpdateInfo function --- src/getUpdateInfo.js | 41 ++++++- tests/getUpdateInfo.test.js | 228 ++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 4 deletions(-) diff --git a/src/getUpdateInfo.js b/src/getUpdateInfo.js index e7bfb3a5..89635298 100644 --- a/src/getUpdateInfo.js +++ b/src/getUpdateInfo.js @@ -19,10 +19,43 @@ function getOwnerDifferences(prevOwner, nextOwner) { prevOwnerData.hooksInfo.slice(prevOwnerData.hooksInfo.length / 2) : prevOwnerData.hooksInfo; - const hookDifferences = prevOwnerDataHooks.map(({hookName, result}, i) => ({ - hookName, - differences: findObjectsDifferences(result, nextOwnerData.hooksInfo[i].result, {shallow: false}), - })); + const prevHooks = Array.isArray(prevOwnerDataHooks) ? prevOwnerDataHooks : []; + const nextHooks = Array.isArray(nextOwnerData?.hooksInfo) ? nextOwnerData.hooksInfo : []; + + // Handle different array lengths safely + const hookDifferences = []; + const maxLen = Math.max(prevHooks.length, nextHooks.length); + + for (let i = 0; i < maxLen; i++) { + const prev = prevHooks[i]; + const next = nextHooks[i]; + + if (!prev && next) { + // Hook was added + hookDifferences.push({ + hookName: next.hookName, + differences: {change: 'added'}, + }); + } else if (prev && !next) { + // Hook was removed + hookDifferences.push({ + hookName: prev.hookName, + differences: {change: 'removed'}, + }); + } else if (prev && next) { + // Both exist, compare them + let differences; + try { + differences = findObjectsDifferences(prev.result, next.result, {shallow: false}); + } catch { + differences = {error: 'diff_failed'}; + } + hookDifferences.push({ + hookName: prev.hookName, + differences, + }); + } + } return { propsDifferences: findObjectsDifferences(prevOwnerData.props, nextOwnerData.props), diff --git a/tests/getUpdateInfo.test.js b/tests/getUpdateInfo.test.js index f3f1c632..04a58532 100644 --- a/tests/getUpdateInfo.test.js +++ b/tests/getUpdateInfo.test.js @@ -521,3 +521,231 @@ describe('getUpdateInfo', () => { }); }); }); + +describe('getUpdateInfo - hook differences', () => { + let wdyrStore; + + beforeEach(() => { + whyDidYouRender(React); + wdyrStore = require('~/wdyrStore').default; + }); + + afterEach(() => { + React.__REVERT_WHY_DID_YOU_RENDER__(); + jest.restoreAllMocks(); + }); + + test('Empty next hooks - prev length > 0, next []', () => { + const prevOwner = {}; + const nextOwner = {}; + + wdyrStore.ownerDataMap.set(prevOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: {value: 1}}, + {hookName: 'useEffect', result: undefined}, + {hookName: 'useMemo', result: 42}, + ], + }); + + wdyrStore.ownerDataMap.set(nextOwner, { + props: {}, + state: null, + hooksInfo: [], + }); + + const updateInfo = getUpdateInfo({ + Component: TestComponent, + displayName: getDisplayName(TestComponent), + prevOwner, + nextOwner, + prevProps: {}, + prevState: null, + nextProps: {}, + nextState: null, + }); + + expect(updateInfo.reason.ownerDifferences).toBeTruthy(); + expect(updateInfo.reason.ownerDifferences.hookDifferences).toEqual([ + { + hookName: 'useState', + differences: {change: 'removed'}, + }, + { + hookName: 'useEffect', + differences: {change: 'removed'}, + }, + { + hookName: 'useMemo', + differences: {change: 'removed'}, + }, + ]); + }); + + test('Shorter next hooks - prev length n, next length n-1', () => { + const prevOwner = {}; + const nextOwner = {}; + + const stateResult = {value: 1}; + + wdyrStore.ownerDataMap.set(prevOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: stateResult}, + {hookName: 'useEffect', result: undefined}, + {hookName: 'useMemo', result: 42}, + ], + }); + + wdyrStore.ownerDataMap.set(nextOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: stateResult}, + {hookName: 'useEffect', result: undefined}, + ], + }); + + const updateInfo = getUpdateInfo({ + Component: TestComponent, + displayName: getDisplayName(TestComponent), + prevOwner, + nextOwner, + prevProps: {}, + prevState: null, + nextProps: {}, + nextState: null, + }); + + expect(updateInfo.reason.ownerDifferences).toBeTruthy(); + expect(updateInfo.reason.ownerDifferences.hookDifferences).toEqual([ + { + hookName: 'useState', + differences: false, + }, + { + hookName: 'useEffect', + differences: false, + }, + { + hookName: 'useMemo', + differences: {change: 'removed'}, + }, + ]); + }); + + test('Longer next hooks - prev n-1, next n', () => { + const prevOwner = {}; + const nextOwner = {}; + + const stateResult = {value: 1}; + + wdyrStore.ownerDataMap.set(prevOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: stateResult}, + {hookName: 'useEffect', result: undefined}, + ], + }); + + wdyrStore.ownerDataMap.set(nextOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: stateResult}, + {hookName: 'useEffect', result: undefined}, + {hookName: 'useMemo', result: 42}, + ], + }); + + const updateInfo = getUpdateInfo({ + Component: TestComponent, + displayName: getDisplayName(TestComponent), + prevOwner, + nextOwner, + prevProps: {}, + prevState: null, + nextProps: {}, + nextState: null, + }); + + expect(updateInfo.reason.ownerDifferences).toBeTruthy(); + expect(updateInfo.reason.ownerDifferences.hookDifferences).toEqual([ + { + hookName: 'useState', + differences: false, + }, + { + hookName: 'useEffect', + differences: false, + }, + { + hookName: 'useMemo', + differences: {change: 'added'}, + }, + ]); + }); + + test('Diff failure safety - findObjectsDifferences throws error', () => { + const prevOwner = {}; + const nextOwner = {}; + + wdyrStore.ownerDataMap.set(prevOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: {value: 1}}, + {hookName: 'useEffect', result: {deps: [1, 2, 3]}}, + ], + }); + + wdyrStore.ownerDataMap.set(nextOwner, { + props: {}, + state: null, + hooksInfo: [ + {hookName: 'useState', result: {value: 2}}, + {hookName: 'useEffect', result: {deps: [4, 5, 6]}}, + ], + }); + + const findObjectsDifferencesModule = require('~/findObjectsDifferences'); + const originalFindObjectsDifferences = findObjectsDifferencesModule.default; + + let hookDiffCallCount = 0; + jest.spyOn(findObjectsDifferencesModule, 'default').mockImplementation((prev, next, options) => { + if (options && options.shallow === false && prev && next) { + hookDiffCallCount++; + if (hookDiffCallCount <= 2) { + throw new Error('Simulated diff error'); + } + } + return originalFindObjectsDifferences(prev, next, options); + }); + + const updateInfo = getUpdateInfo({ + Component: TestComponent, + displayName: getDisplayName(TestComponent), + prevOwner, + nextOwner, + prevProps: {}, + prevState: null, + nextProps: {}, + nextState: null, + }); + + expect(updateInfo.reason.ownerDifferences).toBeTruthy(); + expect(updateInfo.reason.ownerDifferences.hookDifferences).toEqual([ + { + hookName: 'useState', + differences: {error: 'diff_failed'}, + }, + { + hookName: 'useEffect', + differences: {error: 'diff_failed'}, + }, + ]); + }); +});