-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[No QA] Add tests for SearchMoneyRequestReportPage dismiss-on-removal logic #89247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mountiny
merged 3 commits into
Expensify:main
from
callstack-internal:test/search-money-request-report-tests
May 4, 2026
+220
−28
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import {useIsFocused} from '@react-navigation/native'; | ||
| import {useEffect, useRef} from 'react'; | ||
| import {isMoneyRequestReport} from '@libs/ReportUtils'; | ||
| import Navigation from '@navigation/Navigation'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import useOnyx from './useOnyx'; | ||
| import usePrevious from './usePrevious'; | ||
|
|
||
| /** | ||
| * Dismisses the modal when a money request report is removed (e.g. deleted or merged). | ||
| * Skips dismissal during route changes — the new report's data may not be loaded yet, | ||
| * so the absent `report` should not be interpreted as removal. | ||
| */ | ||
| function useDismissOnMoneyRequestReportRemoval(reportIDFromRoute: string | undefined) { | ||
| const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`); | ||
| const prevReport = usePrevious(report); | ||
| const prevReportIDFromRoute = usePrevious(reportIDFromRoute); | ||
| const isFocused = useIsFocused(); | ||
| const firstRenderRef = useRef(true); | ||
|
|
||
| useEffect(() => { | ||
| if (firstRenderRef.current) { | ||
| firstRenderRef.current = false; | ||
| return; | ||
| } | ||
|
|
||
| if (prevReportIDFromRoute !== reportIDFromRoute) { | ||
| return; | ||
| } | ||
|
|
||
| const isRemovalExpectedForReportType = !report && isMoneyRequestReport(prevReport); | ||
|
|
||
| if (isRemovalExpectedForReportType) { | ||
| if (!isFocused) { | ||
| return; | ||
| } | ||
| Navigation.dismissModal(); | ||
| } | ||
| }, [report, isFocused, prevReport, prevReportIDFromRoute, reportIDFromRoute]); | ||
| } | ||
|
|
||
| export default useDismissOnMoneyRequestReportRemoval; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
tests/unit/hooks/useDismissOnMoneyRequestReportRemoval.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import type * as ReactNavigation from '@react-navigation/native'; | ||
| import {renderHook} from '@testing-library/react-native'; | ||
| import Onyx from 'react-native-onyx'; | ||
| import useDismissOnMoneyRequestReportRemoval from '@hooks/useDismissOnMoneyRequestReportRemoval'; | ||
| import Navigation from '@navigation/Navigation'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {Report} from '@src/types/onyx'; | ||
| import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; | ||
|
|
||
| const mockUseIsFocused = jest.fn().mockReturnValue(true); | ||
|
|
||
| jest.mock('@react-navigation/native', () => { | ||
| const actualNav = jest.requireActual<typeof ReactNavigation>('@react-navigation/native'); | ||
| return { | ||
| ...actualNav, | ||
| useIsFocused: () => mockUseIsFocused() as boolean, | ||
| }; | ||
| }); | ||
|
|
||
| jest.mock('@navigation/Navigation', () => ({ | ||
| dismissModal: jest.fn(), | ||
| })); | ||
|
|
||
| const REPORT_A_ID = '1'; | ||
| const REPORT_B_ID = '2'; | ||
|
|
||
| function buildMoneyRequestReport(id: string, overrides?: Partial<Report>): Report { | ||
| return { | ||
| reportID: id, | ||
| type: CONST.REPORT.TYPE.EXPENSE, | ||
| chatType: undefined, | ||
| ...overrides, | ||
| } as Report; | ||
| } | ||
|
|
||
| function buildChatReport(id: string): Report { | ||
| return { | ||
| reportID: id, | ||
| type: CONST.REPORT.TYPE.CHAT, | ||
| } as Report; | ||
| } | ||
|
|
||
| describe('useDismissOnMoneyRequestReportRemoval', () => { | ||
| beforeAll(() => { | ||
| Onyx.init({keys: ONYXKEYS}); | ||
| }); | ||
|
|
||
| beforeEach(async () => { | ||
| jest.clearAllMocks(); | ||
| mockUseIsFocused.mockReturnValue(true); | ||
| await Onyx.clear(); | ||
| await waitForBatchedUpdates(); | ||
| }); | ||
|
|
||
| it('dismisses the modal when a focused money request report is removed', async () => { | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildMoneyRequestReport(REPORT_A_ID)); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, null); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_A_ID}); | ||
|
|
||
| expect(Navigation.dismissModal).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('does not dismiss the modal on first render even if the report is missing', async () => { | ||
| renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not dismiss the modal when the screen is unfocused', async () => { | ||
| mockUseIsFocused.mockReturnValue(false); | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildMoneyRequestReport(REPORT_A_ID)); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, null); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_A_ID}); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not dismiss the modal when the previous report was not a money request report', async () => { | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildChatReport(REPORT_A_ID)); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, null); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_A_ID}); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Regression for https://github.com/Expensify/App/pull/89150 — navigating between reports with the | ||
| // prev/next arrows in the search/saved-search RHP should not dismiss the modal when the next report's | ||
| // data has not yet loaded into Onyx. | ||
| it('does not dismiss the modal when the route changes to a report whose data has not loaded yet', async () => { | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildMoneyRequestReport(REPORT_A_ID)); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| rerender({reportID: REPORT_B_ID}); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Reviewer scenario: while navigating with the arrows in a saved/canned search, the previous report's | ||
| // status changes (e.g. open → processing) on the backend. The arrows must keep working — the modal must | ||
| // not be dismissed in any of the intermediate render passes. | ||
| it('keeps the arrows working when a report status changes mid-navigation in a saved search', async () => { | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildMoneyRequestReport(REPORT_A_ID, {stateNum: CONST.REPORT.STATE_NUM.OPEN})); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| // Status change on the previous report (still defined, just mutated). | ||
| await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, {stateNum: CONST.REPORT.STATE_NUM.SUBMITTED}); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_A_ID}); | ||
|
|
||
| // Arrow forward — report B is not in Onyx yet. | ||
| rerender({reportID: REPORT_B_ID}); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| // Report B data arrives. | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_B_ID}`, buildMoneyRequestReport(REPORT_B_ID, {stateNum: CONST.REPORT.STATE_NUM.OPEN})); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_B_ID}); | ||
|
|
||
| // Arrow back — report A is still around. | ||
| rerender({reportID: REPORT_A_ID}); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not dismiss the modal when the report is updated but still present', async () => { | ||
| await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, buildMoneyRequestReport(REPORT_A_ID, {stateNum: CONST.REPORT.STATE_NUM.OPEN})); | ||
| await waitForBatchedUpdates(); | ||
|
|
||
| const {rerender} = renderHook(({reportID}: {reportID: string}) => useDismissOnMoneyRequestReportRemoval(reportID), { | ||
| initialProps: {reportID: REPORT_A_ID}, | ||
| }); | ||
|
|
||
| await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_A_ID}`, {stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED}); | ||
| await waitForBatchedUpdates(); | ||
| rerender({reportID: REPORT_A_ID}); | ||
|
|
||
| expect(Navigation.dismissModal).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.