diff --git a/src/pages/home/ForYouSection/index.tsx b/src/pages/home/ForYouSection/index.tsx index be98abd0e9a07..cf86ac528b90d 100644 --- a/src/pages/home/ForYouSection/index.tsx +++ b/src/pages/home/ForYouSection/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import BaseWidgetItem from '@components/BaseWidgetItem'; @@ -15,7 +15,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {accountIDSelector} from '@src/selectors/Session'; -import todosReportCountsSelector from '@src/selectors/Todos'; +import todosReportCountsSelector, {EMPTY_TODOS_SINGLE_REPORT_IDS, todosSingleReportIDsSelector} from '@src/selectors/Todos'; import EmptyState from './EmptyState'; function ForYouSection() { @@ -26,6 +26,7 @@ function ForYouSection() { const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP); const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector}); + const [singleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosSingleReportIDsSelector}); const icons = useMemoizedLazyExpensifyIcons(['MoneyBag', 'Send', 'ThumbsUp', 'Export']); @@ -36,48 +37,72 @@ function ForYouSection() { const hasAnyTodos = submitCount > 0 || approveCount > 0 || payCount > 0 || exportCount > 0; - const createNavigationHandler = (action: string, queryParams: Record) => () => { - Navigation.navigate( - ROUTES.SEARCH_ROOT.getRoute({ - query: buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, - action, - ...queryParams, - }), - }), - ); - }; + const createNavigationHandler = useCallback( + (action: string, queryParams: Record, reportID?: string) => () => { + if (reportID) { + if (shouldUseNarrowLayout) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + } else { + Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})); + } + return; + } - const todoItems = [ - { - key: 'submit', - count: submitCount, - icon: icons.Send, - translationKey: 'homePage.forYouSection.submit' as const, - handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.SUBMIT, {from: [`${accountID}`]}), - }, - { - key: 'approve', - count: approveCount, - icon: icons.ThumbsUp, - translationKey: 'homePage.forYouSection.approve' as const, - handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.APPROVE, {to: [`${accountID}`]}), - }, - { - key: 'pay', - count: payCount, - icon: icons.MoneyBag, - translationKey: 'homePage.forYouSection.pay' as const, - handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.PAY, {reimbursable: CONST.SEARCH.BOOLEAN.YES, payer: accountID?.toString()}), - }, - { - key: 'export', - count: exportCount, - icon: icons.Export, - translationKey: 'homePage.forYouSection.export' as const, - handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.EXPORT, {exporter: [`${accountID}`], exportedOn: CONST.SEARCH.DATE_PRESETS.NEVER}), + Navigation.navigate( + ROUTES.SEARCH_ROOT.getRoute({ + query: buildQueryStringFromFilterFormValues({ + type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, + action, + ...queryParams, + }), + }), + ); }, - ].filter((item) => item.count > 0); + [shouldUseNarrowLayout], + ); + + const todoItems = useMemo( + () => + [ + { + key: 'submit', + count: submitCount, + icon: icons.Send, + translationKey: 'homePage.forYouSection.submit' as const, + handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.SUBMIT, {from: [`${accountID}`]}, singleReportIDs[CONST.SEARCH.SEARCH_KEYS.SUBMIT]), + }, + { + key: 'approve', + count: approveCount, + icon: icons.ThumbsUp, + translationKey: 'homePage.forYouSection.approve' as const, + handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.APPROVE, {to: [`${accountID}`]}, singleReportIDs[CONST.SEARCH.SEARCH_KEYS.APPROVE]), + }, + { + key: 'pay', + count: payCount, + icon: icons.MoneyBag, + translationKey: 'homePage.forYouSection.pay' as const, + handler: createNavigationHandler( + CONST.SEARCH.ACTION_FILTERS.PAY, + {reimbursable: CONST.SEARCH.BOOLEAN.YES, payer: accountID?.toString()}, + singleReportIDs[CONST.SEARCH.SEARCH_KEYS.PAY], + ), + }, + { + key: 'export', + count: exportCount, + icon: icons.Export, + translationKey: 'homePage.forYouSection.export' as const, + handler: createNavigationHandler( + CONST.SEARCH.ACTION_FILTERS.EXPORT, + {exporter: [`${accountID}`], exportedOn: CONST.SEARCH.DATE_PRESETS.NEVER}, + singleReportIDs[CONST.SEARCH.SEARCH_KEYS.EXPORT], + ), + }, + ].filter((item) => item.count > 0), + [accountID, approveCount, createNavigationHandler, exportCount, icons.Export, icons.MoneyBag, icons.Send, icons.ThumbsUp, payCount, singleReportIDs, submitCount], + ); const renderTodoItems = () => ( diff --git a/src/selectors/Todos.ts b/src/selectors/Todos.ts index 78b9c0f93a473..9c15ae49c35f2 100644 --- a/src/selectors/Todos.ts +++ b/src/selectors/Todos.ts @@ -1,7 +1,15 @@ +import {shallowEqual} from 'fast-equals'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {TodosDerivedValue} from '@src/types/onyx'; +const EMPTY_TODOS_SINGLE_REPORT_IDS = Object.freeze({ + submit: undefined, + approve: undefined, + pay: undefined, + export: undefined, +}); + const todosReportCountsSelector = (todos: OnyxEntry) => { if (!todos) { return CONST.EMPTY_TODOS_REPORT_COUNTS; @@ -15,4 +23,44 @@ const todosReportCountsSelector = (todos: OnyxEntry) => { }; }; +type SingleReportIDs = { + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: string | undefined; + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: string | undefined; + [CONST.SEARCH.SEARCH_KEYS.PAY]: string | undefined; + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: string | undefined; +}; + +let previousSingleReportIDs: SingleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS as SingleReportIDs; + +const todosSingleReportIDsSelector = (todos: OnyxEntry) => { + if (!todos) { + return EMPTY_TODOS_SINGLE_REPORT_IDS; + } + + const submitReportID = todos.reportsToSubmit.length === 1 ? todos.reportsToSubmit.at(0)?.reportID : undefined; + const approveReportID = todos.reportsToApprove.length === 1 ? todos.reportsToApprove.at(0)?.reportID : undefined; + const payReportID = todos.reportsToPay.length === 1 ? todos.reportsToPay.at(0)?.reportID : undefined; + const exportReportID = todos.reportsToExport.length === 1 ? todos.reportsToExport.at(0)?.reportID : undefined; + + if (!submitReportID && !approveReportID && !payReportID && !exportReportID) { + previousSingleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS; + return EMPTY_TODOS_SINGLE_REPORT_IDS; + } + + const newValue = { + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: submitReportID, + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: approveReportID, + [CONST.SEARCH.SEARCH_KEYS.PAY]: payReportID, + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: exportReportID, + }; + + if (shallowEqual(previousSingleReportIDs, newValue)) { + return previousSingleReportIDs; + } + + previousSingleReportIDs = newValue; + return newValue; +}; + export default todosReportCountsSelector; +export {todosSingleReportIDsSelector, EMPTY_TODOS_SINGLE_REPORT_IDS}; diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx new file mode 100644 index 0000000000000..f2f358bac0e79 --- /dev/null +++ b/tests/ui/ForYouSectionTest.tsx @@ -0,0 +1,324 @@ +import {act, fireEvent, render, screen} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import Navigation from '@libs/Navigation/Navigation'; +import ForYouSection from '@pages/home/ForYouSection'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {TodosDerivedValue} from '@src/types/onyx'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), +})); + +jest.mock('@hooks/useResponsiveLayout', () => jest.fn()); + +jest.mock('@hooks/useLocalize', () => + jest.fn(() => ({ + translate: jest.fn((key: string, params?: Record) => { + if (key === 'homePage.forYouSection.begin') { + return 'Begin'; + } + return params ? `${key}:${JSON.stringify(params)}` : key; + }), + numberFormat: jest.fn((num: number) => num.toString()), + localeCompare: jest.fn((a: string, b: string) => a.localeCompare(b)), + })), +); + +jest.mock('@hooks/useThemeStyles', () => + jest.fn( + () => + new Proxy( + {}, + { + get: () => jest.fn(() => ({})), + }, + ), + ), +); + +jest.mock('@hooks/useTheme', () => jest.fn(() => ({}))); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: jest.fn(() => ({ + MoneyBag: null, + Send: null, + ThumbsUp: null, + Export: null, + })), + useMemoizedLazyIllustrations: jest.fn(() => ({ + ThumbsUpStars: null, + Fireworks: null, + })), +})); + +jest.mock('react-native-reanimated', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return require('react-native-reanimated/mock'); +}); + +const mockNavigate = jest.mocked(Navigation.navigate); +const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; + +const ACCOUNT_ID = 12345; + +const BASE_TODOS: TodosDerivedValue = { + reportsToSubmit: [], + reportsToApprove: [], + reportsToPay: [], + reportsToExport: [], + transactionsByReportID: {}, +}; + +function renderForYouSection() { + return render(); +} + +function pressFirstBeginButton() { + const [firstButton] = screen.getAllByText('Begin'); + fireEvent.press(firstButton); +} + +describe('ForYouSection', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + mockUseResponsiveLayout.mockReturnValue({ + shouldUseNarrowLayout: false, + isSmallScreenWidth: false, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: true, + isExtraLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: false, + onboardingIsMediumOrLargerScreenWidth: true, + }); + + await act(async () => { + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {accountID: ACCOUNT_ID, email: 'test@example.com'}, + [ONYXKEYS.IS_LOADING_APP]: false, + }); + }); + await waitForBatchedUpdatesWithAct(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await act(async () => { + await Onyx.clear(); + }); + await waitForBatchedUpdatesWithAct(); + }); + + describe('EmptyState', () => { + it('renders EmptyState when there are no todos', async () => { + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + expect(screen.queryByText('Begin')).not.toBeOnTheScreen(); + }); + }); + + describe('navigation with multiple reports (search route)', () => { + it('navigates to SEARCH_ROOT when submit has multiple reports', async () => { + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToSubmit: [{reportID: '1'} as TodosDerivedValue['reportsToSubmit'][number], {reportID: '2'} as TodosDerivedValue['reportsToSubmit'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + const calledRoute = mockNavigate.mock.calls.at(0)?.at(0) as string; + expect(calledRoute).toContain(ROUTES.SEARCH_ROOT.route); + }); + + it('navigates to SEARCH_ROOT when approve has multiple reports', async () => { + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToApprove: [{reportID: '3'} as TodosDerivedValue['reportsToApprove'][number], {reportID: '4'} as TodosDerivedValue['reportsToApprove'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + const calledRoute = mockNavigate.mock.calls.at(0)?.at(0) as string; + expect(calledRoute).toContain(ROUTES.SEARCH_ROOT.route); + }); + }); + + describe('navigation with a single report (direct report route)', () => { + describe('wide layout', () => { + beforeEach(() => { + mockUseResponsiveLayout.mockReturnValue({ + shouldUseNarrowLayout: false, + isSmallScreenWidth: false, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: true, + isExtraLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: false, + onboardingIsMediumOrLargerScreenWidth: true, + }); + }); + + it('navigates to EXPENSE_REPORT_RHP when submit has exactly one report on wide layout', async () => { + const reportID = '42'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToSubmit: [{reportID} as TodosDerivedValue['reportsToSubmit'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})); + }); + + it('navigates to EXPENSE_REPORT_RHP when approve has exactly one report on wide layout', async () => { + const reportID = '55'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToApprove: [{reportID} as TodosDerivedValue['reportsToApprove'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})); + }); + + it('navigates to EXPENSE_REPORT_RHP when pay has exactly one report on wide layout', async () => { + const reportID = '66'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToPay: [{reportID} as TodosDerivedValue['reportsToPay'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})); + }); + + it('navigates to EXPENSE_REPORT_RHP when export has exactly one report on wide layout', async () => { + const reportID = '77'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToExport: [{reportID} as TodosDerivedValue['reportsToExport'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})); + }); + }); + + describe('narrow layout', () => { + beforeEach(() => { + mockUseResponsiveLayout.mockReturnValue({ + shouldUseNarrowLayout: true, + isSmallScreenWidth: true, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: false, + isExtraLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: true, + onboardingIsMediumOrLargerScreenWidth: false, + }); + }); + + it('navigates to REPORT_WITH_ID when submit has exactly one report on narrow layout', async () => { + const reportID = '99'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToSubmit: [{reportID} as TodosDerivedValue['reportsToSubmit'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + }); + + it('navigates to REPORT_WITH_ID when approve has exactly one report on narrow layout', async () => { + const reportID = '100'; + await act(async () => { + await Onyx.set(ONYXKEYS.DERIVED.TODOS, { + ...BASE_TODOS, + reportsToApprove: [{reportID} as TodosDerivedValue['reportsToApprove'][number]], + }); + }); + await waitForBatchedUpdatesWithAct(); + + renderForYouSection(); + await waitForBatchedUpdatesWithAct(); + + pressFirstBeginButton(); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + }); + }); + }); +}); diff --git a/tests/unit/selectors/TodosTest.ts b/tests/unit/selectors/TodosTest.ts index 2b14df4031334..7bb2bbeca7e29 100644 --- a/tests/unit/selectors/TodosTest.ts +++ b/tests/unit/selectors/TodosTest.ts @@ -1,4 +1,4 @@ -import todosReportCountsSelector from '@selectors/Todos'; +import todosReportCountsSelector, {todosSingleReportIDsSelector} from '@selectors/Todos'; import CONST from '@src/CONST'; import type {TodosDerivedValue} from '@src/types/onyx'; @@ -93,3 +93,122 @@ describe('todosReportCountsSelector', () => { }); }); }); + +describe('todosSingleReportIDsSelector', () => { + it('returns EMPTY_TODOS_SINGLE_REPORT_IDS when todos is undefined', () => { + const result = todosSingleReportIDsSelector(undefined); + + expect(result).toEqual({ + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: undefined, + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: undefined, + [CONST.SEARCH.SEARCH_KEYS.PAY]: undefined, + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: undefined, + }); + }); + + it('returns EMPTY_TODOS_SINGLE_REPORT_IDS when all arrays are empty', () => { + const todos: TodosDerivedValue = { + reportsToSubmit: [], + reportsToApprove: [], + reportsToPay: [], + reportsToExport: [], + transactionsByReportID: {}, + }; + + const result = todosSingleReportIDsSelector(todos); + + expect(result).toEqual({ + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: undefined, + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: undefined, + [CONST.SEARCH.SEARCH_KEYS.PAY]: undefined, + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: undefined, + }); + }); + + it('returns report ID when exactly one report exists in each array', () => { + const todos: TodosDerivedValue = { + reportsToSubmit: [{reportID: '1'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [{reportID: '2'}] as TodosDerivedValue['reportsToApprove'], + reportsToPay: [{reportID: '3'}] as TodosDerivedValue['reportsToPay'], + reportsToExport: [{reportID: '4'}] as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const result = todosSingleReportIDsSelector(todos); + + expect(result).toEqual({ + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: '1', + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: '2', + [CONST.SEARCH.SEARCH_KEYS.PAY]: '3', + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: '4', + }); + }); + + it('returns undefined for arrays with more than one report', () => { + const todos: TodosDerivedValue = { + reportsToSubmit: [{reportID: '1'}, {reportID: '2'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [{reportID: '3'}, {reportID: '4'}, {reportID: '5'}] as TodosDerivedValue['reportsToApprove'], + reportsToPay: [{reportID: '6'}] as TodosDerivedValue['reportsToPay'], + reportsToExport: [{reportID: '7'}, {reportID: '8'}] as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const result = todosSingleReportIDsSelector(todos); + + expect(result).toEqual({ + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: undefined, + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: undefined, + [CONST.SEARCH.SEARCH_KEYS.PAY]: '6', + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: undefined, + }); + }); + + it('returns the same object reference when called twice with shallowly equal values (memoization)', () => { + const todos: TodosDerivedValue = { + reportsToSubmit: [{reportID: '10'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [] as unknown as TodosDerivedValue['reportsToApprove'], + reportsToPay: [] as unknown as TodosDerivedValue['reportsToPay'], + reportsToExport: [] as unknown as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const first = todosSingleReportIDsSelector(todos); + + const todosClone: TodosDerivedValue = { + reportsToSubmit: [{reportID: '10'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [] as unknown as TodosDerivedValue['reportsToApprove'], + reportsToPay: [] as unknown as TodosDerivedValue['reportsToPay'], + reportsToExport: [] as unknown as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const second = todosSingleReportIDsSelector(todosClone); + + expect(second).toBe(first); + }); + + it('returns a new object reference when values change (memoization invalidation)', () => { + const todos: TodosDerivedValue = { + reportsToSubmit: [{reportID: '20'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [] as unknown as TodosDerivedValue['reportsToApprove'], + reportsToPay: [] as unknown as TodosDerivedValue['reportsToPay'], + reportsToExport: [] as unknown as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const first = todosSingleReportIDsSelector(todos); + + const todosChanged: TodosDerivedValue = { + reportsToSubmit: [{reportID: '21'}] as TodosDerivedValue['reportsToSubmit'], + reportsToApprove: [] as unknown as TodosDerivedValue['reportsToApprove'], + reportsToPay: [] as unknown as TodosDerivedValue['reportsToPay'], + reportsToExport: [] as unknown as TodosDerivedValue['reportsToExport'], + transactionsByReportID: {}, + }; + + const second = todosSingleReportIDsSelector(todosChanged); + + expect(second).not.toBe(first); + expect(second[CONST.SEARCH.SEARCH_KEYS.SUBMIT]).toBe('21'); + }); +});