From fae12a55fe711588c28bc625d1e3244e9130107f Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 6 Aug 2025 17:46:06 +0200 Subject: [PATCH 1/7] perf: optimize useSidebarOrderedReports hook --- Mobile-Expensify | 2 +- src/hooks/useSidebarOrderedReports.tsx | 5 +- tests/unit/useSidebarOrderedReportsTest.tsx | 305 ++++++++++++++++++++ 3 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 tests/unit/useSidebarOrderedReportsTest.tsx diff --git a/Mobile-Expensify b/Mobile-Expensify index 9b52ac7b0ba20..6db359196b62d 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9b52ac7b0ba20731fa8bd56a9b2f6b9f97199429 +Subproject commit 6db359196b62d470925cfd8a70e41820a67e9179 diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index fdb173f69117f..3a758773795c3 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -12,6 +12,7 @@ import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; import useCurrentReportID from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; import useDiffPrevious from './useDiffPrevious'; +import useDeepCompareRef from './useDeepCompareRef'; import useLocalize from './useLocalize'; import useOnyx from './useOnyx'; import usePrevious from './usePrevious'; @@ -176,12 +177,14 @@ function SidebarOrderedReportsContextProvider({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDraftsUpdates]); + const deepComparedReports = useDeepCompareRef(reportsToDisplayInLHN); + useEffect(() => { setCurrentReportsToDisplay(reportsToDisplayInLHN); }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(reportsToDisplayInLHN, priorityMode, localeCompare, reportNameValuePairs, reportAttributes, drafts), + () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReports ?? {}, priorityMode, localeCompare, reportNameValuePairs, reportAttributes, drafts), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [reportsToDisplayInLHN, localeCompare], diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx new file mode 100644 index 0000000000000..b0167752f2e36 --- /dev/null +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -0,0 +1,305 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */ +import {renderHook} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import {SidebarOrderedReportsContextProvider, useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports'; +import SidebarUtils from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +// Mock dependencies +jest.mock('@libs/SidebarUtils', () => ({ + sortReportsToDisplayInLHN: jest.fn(), + getReportsToDisplayInLHN: jest.fn(), + updateReportsToDisplayInLHN: jest.fn(), +})); +jest.mock('@libs/Navigation/Navigation', () => ({ + getTopmostReportId: jest.fn(), +})); +jest.mock('@libs/ReportUtils', () => ({ + parseReportRouteParams: jest.fn(() => ({reportID: undefined})), + getReportIDFromLink: jest.fn(() => ''), +})); + +const mockSidebarUtils = SidebarUtils as any; + +describe('useSidebarOrderedReports', () => { + beforeAll(async () => { + Onyx.init({keys: ONYXKEYS}); + // Set up basic session data + await Onyx.set(ONYXKEYS.SESSION, { + accountID: 12345, + email: 'test@example.com', + authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, + }); + return waitForBatchedUpdates(); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + Onyx.clear(); + + // Set up basic session data for each test + await Onyx.set(ONYXKEYS.SESSION, { + accountID: 12345, + email: 'test@example.com', + authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, + }); + + // Set up required Onyx data that the hook depends on + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, + [ONYXKEYS.COLLECTION.REPORT]: {}, + [ONYXKEYS.COLLECTION.POLICY]: {}, + [ONYXKEYS.COLLECTION.TRANSACTION]: {}, + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: {}, + [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: {}, + [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: {}, + [ONYXKEYS.BETAS]: [], + [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: {reports: {}}, + } as any); + + // Default mock implementations + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); + mockSidebarUtils.updateReportsToDisplayInLHN.mockReturnValue({}); + mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue([]); + + return waitForBatchedUpdates(); + }); + + afterAll(async () => { + Onyx.clear(); + await waitForBatchedUpdates(); + }); + + const createMockReports = (reports: Record>) => { + const mockReports: Record = {}; + Object.entries(reports).forEach(([key, report]) => { + const reportId = key.replace('report', ''); + mockReports[reportId] = { + reportID: reportId, + reportName: `Report ${reportId}`, + lastVisibleActionCreated: '2024-01-01 10:00:00', + type: CONST.REPORT.TYPE.CHAT, + ...report, + } as Report; + }); + return mockReports; + }; + + function TestWrapper({children}: {children: React.ReactNode}) { + return ( + + + {children} + + + ); + } + + it('should prevent unnecessary re-renders when reports have same content but different references', () => { + // Given reports with same content but different object references + const reportsContent = { + report1: {reportName: 'Chat 1', lastVisibleActionCreated: '2024-01-01 10:00:00'}, + report2: {reportName: 'Chat 2', lastVisibleActionCreated: '2024-01-01 11:00:00'}, + }; + + // When the initial reports are set + const initialReports = createMockReports(reportsContent); + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); + + // When the hook is rendered + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + // Then the mock calls are cleared + mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + + // When the reports are updated + const newReportsWithSameContent = createMockReports(reportsContent); + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); + + rerender({}); + + // Then sortReportsToDisplayInLHN should not be called again since deep comparison shows no change + expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); + }); + + it('should trigger re-render when reports content actually changes', async () => { + // Given the initial reports are set + const initialReports = createMockReports({ + report1: {reportName: 'Chat 1'}, + report2: {reportName: 'Chat 2'}, + }); + + // When the reports are updated + const updatedReports = createMockReports({ + report1: {reportName: 'Chat 1 Updated'}, // Content changed + report2: {reportName: 'Chat 2'}, + report3: {reportName: 'Chat 3'}, // New report added + }); + + // Then the initial reports are set + await Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT}1`]: initialReports['1'], + [`${ONYXKEYS.COLLECTION.REPORT}2`]: initialReports['2'], + } as any); + + // When the mock is updated + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); + + // When the hook is rendered + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + await waitForBatchedUpdates(); + + // Then the mock calls are cleared + mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + + // When the mock is updated + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(updatedReports); + + // When the priority mode is changed + await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + + rerender({}); + + await waitForBatchedUpdates(); + + // Then sortReportsToDisplayInLHN should be called with the updated reports + expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledWith( + updatedReports, + expect.any(String), // priorityMode + expect.any(Function), // localeCompare + expect.any(Object), // reportNameValuePairs + expect.any(Object), // reportAttributes + ); + }); + + it('should optimize performance by avoiding unnecessary sorting when only report order changes', () => { + // Given the initial reports are set + const reports = createMockReports({ + report1: {reportName: 'Chat A'}, + report2: {reportName: 'Chat B'}, + report3: {reportName: 'Chat C'}, + }); + + // When the initial reports are set + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); + mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue(['1', '2', '3']); + + // When the hook is rendered + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + // Then the mock calls are cleared + expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); + + // When the mock is updated + mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + + // When the mock is updated + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); + + rerender({}); + + // Then sorting should not be called again since deep comparison shows no change + expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); + }); + + it('should handle empty reports correctly with deep comparison', async () => { + // Given the initial reports are set + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); + + // When the hook is rendered + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + await waitForBatchedUpdates(); + + // Then the mock calls are cleared + mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + + // When the mock is updated + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); + + // When the priority mode is changed + await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + + rerender({}); + + await waitForBatchedUpdates(); + + // Then sortReportsToDisplayInLHN should be called when priority mode changes, even with same content + expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledWith({}, expect.any(String), expect.any(Function), expect.any(Object), expect.any(Object)); + }); + + it('should maintain referential stability across multiple renders with same content', () => { + // Given the initial reports are set + const reportsContent = { + report1: {reportName: 'Stable Chat'}, + }; + + // When the initial reports are set + const initialReports = createMockReports(reportsContent); + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); + + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + // When the mock is updated + const newReportsWithSameContent = createMockReports(reportsContent); + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); + + rerender({}); + + // When the mock is updated + const thirdReportsWithSameContent = createMockReports(reportsContent); + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(thirdReportsWithSameContent); + + rerender({}); + + // Then sortReportsToDisplayInLHN should be called only once (initial render) + expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); + }); + + it('should handle priority mode changes correctly with deep comparison', async () => { + // Given the initial reports are set + const reports = createMockReports({ + report1: {reportName: 'Chat A'}, + report2: {reportName: 'Chat B'}, + }); + + mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); + + // When the hook is rendered + const {rerender} = renderHook(() => useSidebarOrderedReports(), { + wrapper: TestWrapper, + }); + + await waitForBatchedUpdates(); + + // Then the mock calls are cleared + mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + + // When the priority mode is changed + await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + + rerender({}); + + await waitForBatchedUpdates(); + + // Then sortReportsToDisplayInLHN should be called when priority mode changes + expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalled(); + }); +}); From 3ac6ef95a177102c8ba7f125f02eed2bca010b1d Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 13 Aug 2025 11:09:32 +0200 Subject: [PATCH 2/7] add missing dependency and change tests --- src/hooks/useSidebarOrderedReports.tsx | 6 +++--- tests/unit/useSidebarOrderedReportsTest.tsx | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 3a758773795c3..5cfa0e2bcb28c 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -177,17 +177,17 @@ function SidebarOrderedReportsContextProvider({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDraftsUpdates]); - const deepComparedReports = useDeepCompareRef(reportsToDisplayInLHN); + const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN); useEffect(() => { setCurrentReportsToDisplay(reportsToDisplayInLHN); }, [reportsToDisplayInLHN]); const getOrderedReportIDs = useCallback( - () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReports ?? {}, priorityMode, localeCompare, reportNameValuePairs, reportAttributes, drafts), + () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportNameValuePairs, reportAttributes, drafts), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [reportsToDisplayInLHN, localeCompare], + [deepComparedReportsToDisplayInLHN, localeCompare], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index b0167752f2e36..abb43aba43004 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */ import {renderHook} from '@testing-library/react-native'; import React from 'react'; +import type {OnyxMultiSetInput} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; @@ -25,7 +25,7 @@ jest.mock('@libs/ReportUtils', () => ({ getReportIDFromLink: jest.fn(() => ''), })); -const mockSidebarUtils = SidebarUtils as any; +const mockSidebarUtils = SidebarUtils as jest.Mocked; describe('useSidebarOrderedReports', () => { beforeAll(async () => { @@ -61,7 +61,7 @@ describe('useSidebarOrderedReports', () => { [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: {}, [ONYXKEYS.BETAS]: [], [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: {reports: {}}, - } as any); + } as unknown as OnyxMultiSetInput); // Default mock implementations mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); @@ -148,7 +148,7 @@ describe('useSidebarOrderedReports', () => { await Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT}1`]: initialReports['1'], [`${ONYXKEYS.COLLECTION.REPORT}2`]: initialReports['2'], - } as any); + } as unknown as OnyxMultiSetInput); // When the mock is updated mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); @@ -239,8 +239,8 @@ describe('useSidebarOrderedReports', () => { await waitForBatchedUpdates(); - // Then sortReportsToDisplayInLHN should be called when priority mode changes, even with same content - expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledWith({}, expect.any(String), expect.any(Function), expect.any(Object), expect.any(Object)); + // Then sortReportsToDisplayInLHN should not be called again since reports are empty + expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); }); it('should maintain referential stability across multiple renders with same content', () => { From b500cc60e550c5ac6cfc7494497bfb9ccd43c4cd Mon Sep 17 00:00:00 2001 From: Github Date: Thu, 14 Aug 2025 15:16:17 +0200 Subject: [PATCH 3/7] fix tests --- tests/unit/useSidebarOrderedReportsTest.tsx | 30 +++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index abb43aba43004..e035fcca27391 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -64,8 +64,8 @@ describe('useSidebarOrderedReports', () => { } as unknown as OnyxMultiSetInput); // Default mock implementations - mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); - mockSidebarUtils.updateReportsToDisplayInLHN.mockReturnValue({}); + mockSidebarUtils.getReportsToDisplayInLHN.mockImplementation(() => ({})); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => prev); mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue([]); return waitForBatchedUpdates(); @@ -91,17 +91,19 @@ describe('useSidebarOrderedReports', () => { return mockReports; }; + let currentReportIDForTestsValue: string | undefined; + function TestWrapper({children}: {children: React.ReactNode}) { return ( - {children} + {children} ); } - it('should prevent unnecessary re-renders when reports have same content but different references', () => { + it('should prevent unnecessary re-renders when reports have same content but different references', async () => { // Given reports with same content but different object references const reportsContent = { report1: {reportName: 'Chat 1', lastVisibleActionCreated: '2024-01-01 10:00:00'}, @@ -111,6 +113,8 @@ describe('useSidebarOrderedReports', () => { // When the initial reports are set const initialReports = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => ({...prev})); + currentReportIDForTestsValue = '1'; // When the hook is rendered const {rerender} = renderHook(() => useSidebarOrderedReports(), { @@ -120,6 +124,10 @@ describe('useSidebarOrderedReports', () => { // Then the mock calls are cleared mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + // Then the report name value pairs are updated + // @ts-expect-error - we want to test the case where getUpdatedReports() is non-empty + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}`, {dummy: true}); + // When the reports are updated const newReportsWithSameContent = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); @@ -183,7 +191,7 @@ describe('useSidebarOrderedReports', () => { ); }); - it('should optimize performance by avoiding unnecessary sorting when only report order changes', () => { + it('should optimize performance by avoiding unnecessary sorting when only report order changes', async () => { // Given the initial reports are set const reports = createMockReports({ report1: {reportName: 'Chat A'}, @@ -194,6 +202,8 @@ describe('useSidebarOrderedReports', () => { // When the initial reports are set mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue(['1', '2', '3']); + mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => ({...prev})); + currentReportIDForTestsValue = '1'; // When the hook is rendered const {rerender} = renderHook(() => useSidebarOrderedReports(), { @@ -206,6 +216,10 @@ describe('useSidebarOrderedReports', () => { // When the mock is updated mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + // Then the report name value pairs are updated + // @ts-expect-error - we want to test the case where getUpdatedReports() is non-empty + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}`, {dummy: true}); + // When the mock is updated mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); @@ -252,6 +266,8 @@ describe('useSidebarOrderedReports', () => { // When the initial reports are set const initialReports = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(initialReports); + mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue(['1']); + currentReportIDForTestsValue = '1'; const {rerender} = renderHook(() => useSidebarOrderedReports(), { wrapper: TestWrapper, @@ -262,12 +278,14 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); rerender({}); + currentReportIDForTestsValue = '2'; // When the mock is updated const thirdReportsWithSameContent = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(thirdReportsWithSameContent); rerender({}); + currentReportIDForTestsValue = '3'; // Then sortReportsToDisplayInLHN should be called only once (initial render) expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); @@ -281,6 +299,7 @@ describe('useSidebarOrderedReports', () => { }); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); + currentReportIDForTestsValue = '1'; // When the hook is rendered const {rerender} = renderHook(() => useSidebarOrderedReports(), { @@ -291,6 +310,7 @@ describe('useSidebarOrderedReports', () => { // Then the mock calls are cleared mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); + currentReportIDForTestsValue = '2'; // When the priority mode is changed await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); From 169e354dc76f27062e45fb9d5fdb8a18405c0fb4 Mon Sep 17 00:00:00 2001 From: Github Date: Mon, 18 Aug 2025 13:48:17 +0200 Subject: [PATCH 4/7] change dependency and adjust unit tests --- src/hooks/useSidebarOrderedReports.tsx | 2 +- tests/unit/useSidebarOrderedReportsTest.tsx | 47 +-------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 5cfa0e2bcb28c..394037c231984 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -187,7 +187,7 @@ function SidebarOrderedReportsContextProvider({ () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, reportNameValuePairs, reportAttributes, drafts), // Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [deepComparedReportsToDisplayInLHN, localeCompare], + [reportsToDisplayInLHN, localeCompare], ); const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index e035fcca27391..87ed8d7f380c4 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -103,7 +103,7 @@ describe('useSidebarOrderedReports', () => { ); } - it('should prevent unnecessary re-renders when reports have same content but different references', async () => { + it('should prevent unnecessary re-renders when reports have same content but different references', () => { // Given reports with same content but different object references const reportsContent = { report1: {reportName: 'Chat 1', lastVisibleActionCreated: '2024-01-01 10:00:00'}, @@ -124,10 +124,6 @@ describe('useSidebarOrderedReports', () => { // Then the mock calls are cleared mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); - // Then the report name value pairs are updated - // @ts-expect-error - we want to test the case where getUpdatedReports() is non-empty - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}`, {dummy: true}); - // When the reports are updated const newReportsWithSameContent = createMockReports(reportsContent); mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(newReportsWithSameContent); @@ -191,44 +187,6 @@ describe('useSidebarOrderedReports', () => { ); }); - it('should optimize performance by avoiding unnecessary sorting when only report order changes', async () => { - // Given the initial reports are set - const reports = createMockReports({ - report1: {reportName: 'Chat A'}, - report2: {reportName: 'Chat B'}, - report3: {reportName: 'Chat C'}, - }); - - // When the initial reports are set - mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); - mockSidebarUtils.sortReportsToDisplayInLHN.mockReturnValue(['1', '2', '3']); - mockSidebarUtils.updateReportsToDisplayInLHN.mockImplementation((prev) => ({...prev})); - currentReportIDForTestsValue = '1'; - - // When the hook is rendered - const {rerender} = renderHook(() => useSidebarOrderedReports(), { - wrapper: TestWrapper, - }); - - // Then the mock calls are cleared - expect(mockSidebarUtils.sortReportsToDisplayInLHN).toHaveBeenCalledTimes(1); - - // When the mock is updated - mockSidebarUtils.sortReportsToDisplayInLHN.mockClear(); - - // Then the report name value pairs are updated - // @ts-expect-error - we want to test the case where getUpdatedReports() is non-empty - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}`, {dummy: true}); - - // When the mock is updated - mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue(reports); - - rerender({}); - - // Then sorting should not be called again since deep comparison shows no change - expect(mockSidebarUtils.sortReportsToDisplayInLHN).not.toHaveBeenCalled(); - }); - it('should handle empty reports correctly with deep comparison', async () => { // Given the initial reports are set mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); @@ -246,9 +204,6 @@ describe('useSidebarOrderedReports', () => { // When the mock is updated mockSidebarUtils.getReportsToDisplayInLHN.mockReturnValue({}); - // When the priority mode is changed - await Onyx.set(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - rerender({}); await waitForBatchedUpdates(); From cb1fe71b9c5dc1b44b11eff6baf1bf16fa8fc91f Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 20 Aug 2025 12:56:13 +0200 Subject: [PATCH 5/7] fix prettier and tests --- src/hooks/useSidebarOrderedReports.tsx | 2 +- tests/unit/useSidebarOrderedReportsTest.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 394037c231984..c99dd892e1b0a 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -11,8 +11,8 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; import useCurrentReportID from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; -import useDiffPrevious from './useDiffPrevious'; import useDeepCompareRef from './useDeepCompareRef'; +import useDiffPrevious from './useDiffPrevious'; import useLocalize from './useLocalize'; import useOnyx from './useOnyx'; import usePrevious from './usePrevious'; diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 87ed8d7f380c4..412eba40b6800 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -184,6 +184,7 @@ describe('useSidebarOrderedReports', () => { expect.any(Function), // localeCompare expect.any(Object), // reportNameValuePairs expect.any(Object), // reportAttributes + expect.any(Object), // drafts ); }); From faa0a10cfb85e286674984d697a0dc69c1804906 Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 20 Aug 2025 13:02:17 +0200 Subject: [PATCH 6/7] lint fix --- tests/unit/useSidebarOrderedReportsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 412eba40b6800..9b0aff7848211 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -58,7 +58,7 @@ describe('useSidebarOrderedReports', () => { [ONYXKEYS.COLLECTION.TRANSACTION]: {}, [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: {}, [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: {}, - [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: {}, + [ONYXKEYS.NVP_DRAFT_REPORT_COMMENTS]: {}, [ONYXKEYS.BETAS]: [], [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: {reports: {}}, } as unknown as OnyxMultiSetInput); From 91d68b4498a1bd4c608e5bedd6ff0d2773bafdcb Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 20 Aug 2025 13:15:22 +0200 Subject: [PATCH 7/7] remove submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 6db359196b62d..9b52ac7b0ba20 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 6db359196b62d470925cfd8a70e41820a67e9179 +Subproject commit 9b52ac7b0ba20731fa8bd56a9b2f6b9f97199429