diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts new file mode 100644 index 000000000000..44a82253b7c0 --- /dev/null +++ b/src/hooks/usePaginatedReportActions.ts @@ -0,0 +1,33 @@ +import {useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +/** + * Get the longest continuous chunk of reportActions including the linked reportAction. If not linking to a specific action, returns the continuous chunk of newest reportActions. + */ +function usePaginatedReportActions(reportID?: string, reportActionID?: string) { + // Use `||` instead of `??` to handle empty string. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportIDWithDefault = reportID || '-1'; + const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { + canEvict: false, + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + }); + + const reportActions = useMemo(() => { + if (!sortedAllReportActions.length) { + return []; + } + return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionID); + }, [reportActionID, sortedAllReportActions]); + + const linkedAction = useMemo(() => sortedAllReportActions.find((obj) => String(obj.reportActionID) === String(reportActionID)), [reportActionID, sortedAllReportActions]); + + return { + reportActions, + linkedAction, + }; +} + +export default usePaginatedReportActions; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index b04e56f288e9..dbc082142a2f 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -22,6 +22,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; @@ -79,21 +80,10 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); - // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || '-1'}`); - const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID ?? '-1'}`, { - canEvict: false, - selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), - }); - - const reportActions = useMemo(() => { - if (!sortedAllReportActions.length) { - return []; - } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions); - }, [sortedAllReportActions]); + const {reportActions} = usePaginatedReportActions(report.reportID); const transactionThreadReportID = useMemo( () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 3c2ae7bbc6e6..3076aeb87bd1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -26,11 +26,11 @@ import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP'; import useLastAccessedReportID from '@hooks/useLastAccessedReportID'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {getCurrentUserAccountID} from '@libs/actions/Report'; import Timing from '@libs/actions/Timing'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -68,9 +68,6 @@ type ReportScreenOnyxProps = { /** The policies which the user has access to */ policies: OnyxCollection; - /** An array containing all report actions related to this report, sorted based on a date criterion */ - sortedAllReportActions: OnyxTypes.ReportAction[]; - /** Additional report details */ reportNameValuePairs: OnyxEntry; @@ -119,7 +116,6 @@ function ReportScreen({ betas = [], route, reportNameValuePairs, - sortedAllReportActions, reportMetadata = { isLoadingInitialReportActions: true, isLoadingOlderReportActions: false, @@ -283,12 +279,14 @@ function ReportScreen({ const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute); - const reportActions = useMemo(() => { - if (!sortedAllReportActions.length) { - return []; - } - return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute); - }, [reportActionIDFromRoute, sortedAllReportActions]); + + const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID}); + const {reportActions, linkedAction} = usePaginatedReportActions(report.reportID, reportActionIDFromRoute); + const isLinkedActionDeleted = useMemo(() => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID), [linkedAction]); + const isLinkedActionInaccessibleWhisper = useMemo( + () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), + [currentUserAccountID, linkedAction], + ); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. // If we have cached reportActions, they will be shown immediately. @@ -313,10 +311,6 @@ function ReportScreen({ const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]); const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; - const isLinkedMessageAvailable = useMemo( - (): boolean => sortedAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)) > -1, - [sortedAllReportActions, reportActionIDFromRoute], - ); // If there's a non-404 error for the report we should show it instead of blocking the screen const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound'); @@ -408,12 +402,12 @@ function ReportScreen({ const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isReportOpenInRHP) || PersonalDetailsUtils.isPersonalDetailsEmpty()); const shouldShowSkeleton = - !isLinkedMessageAvailable && + !linkedAction && (isLinkingToMessage || !isCurrentReportLoadedFromOnyx || (reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions) || isLoading || - (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions)); + (!!reportActionIDFromRoute && !!reportMetadata?.isLoadingInitialReportActions)); const shouldShowReportActionList = isCurrentReportLoadedFromOnyx && !isLoading; const currentReportIDFormRoute = route.params?.reportID; @@ -678,28 +672,16 @@ function ReportScreen({ fetchReport(); }, [fetchReport]); - const {isLinkedReportActionDeleted, isInaccessibleWhisper} = useMemo(() => { - const currentUserAccountID = getCurrentUserAccountID(); - if (!reportActionIDFromRoute || !sortedAllReportActions) { - return {isLinkedReportActionDeleted: false, isInaccessibleWhisper: false}; - } - const action = sortedAllReportActions.find((item) => item.reportActionID === reportActionIDFromRoute); - return { - isLinkedReportActionDeleted: action && !ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID), - isInaccessibleWhisper: action && ReportActionsUtils.isWhisperAction(action) && !(action?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), - }; - }, [reportActionIDFromRoute, sortedAllReportActions]); - // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to, // then we set reportActionID as empty string, so we display them the report and not the "Not found page". useEffect(() => { - if (!isInaccessibleWhisper) { + if (!isLinkedActionInaccessibleWhisper) { return; } Navigation.isNavigationReady().then(() => { Navigation.setParams({reportActionID: ''}); }); - }, [isInaccessibleWhisper]); + }, [isLinkedActionInaccessibleWhisper]); useEffect(() => { if (!!report.lastReadTime || !ReportUtils.isTaskReport(report)) { @@ -709,7 +691,7 @@ function ReportScreen({ Report.readNewestAction(report.reportID); }, [report]); - if ((!isInaccessibleWhisper && isLinkedReportActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { + if ((!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), - }, reportNameValuePairs: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getReportID(route)}`, allowStaleData: true, @@ -856,7 +833,6 @@ export default withCurrentReportID( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && lodashIsEqual(prevProps.betas, nextProps.betas) && lodashIsEqual(prevProps.policies, nextProps.policies) && diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index b5990ee5d002..f84c75823753 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -200,81 +200,104 @@ let reportAction9CreatedDate: string; /** * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. */ -function signInAndGetAppWithUnreadChat(): Promise { +async function signInAndGetAppWithUnreadChat() { // Render the App and sign in as a test user. render(); - return waitForBatchedUpdatesWithAct() - .then(async () => { - await waitForBatchedUpdatesWithAct(); - const hintText = Localize.translateLocal('loginForm.loginForm'); - const loginForm = screen.queryAllByLabelText(hintText); - expect(loginForm).toHaveLength(1); - - await act(async () => { - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - }); - return waitForBatchedUpdatesWithAct(); - }) - .then(() => { - User.subscribeToUserEvents(); - return waitForBatchedUpdates(); - }) - .then(async () => { - const TEN_MINUTES_AGO = subMinutes(new Date(), 10); - reportAction3CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 30), CONST.DATE.FNS_DB_FORMAT_STRING); - reportAction9CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 90), CONST.DATE.FNS_DB_FORMAT_STRING); - - // Simulate setting an unread report and personal details - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastReadTime: reportAction3CreatedDate, - lastVisibleActionCreated: reportAction9CreatedDate, - lastMessageText: 'Test', - participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, - lastActorAccountID: USER_B_ACCOUNT_ID, - type: CONST.REPORT.TYPE.CHAT, - }); - const createdReportActionID = NumberUtils.rand64().toString(); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - [createdReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), - reportActionID: createdReportActionID, - message: [ - { - style: 'strong', - text: '__FAKE__', - type: 'TEXT', - }, - { - style: 'normal', - text: 'created this report', - type: 'TEXT', - }, - ], + await waitForBatchedUpdatesWithAct(); + await waitForBatchedUpdatesWithAct(); + + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + await waitForBatchedUpdatesWithAct(); + + User.subscribeToUserEvents(); + await waitForBatchedUpdates(); + + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + reportAction3CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 30), CONST.DATE.FNS_DB_FORMAT_STRING); + reportAction9CreatedDate = format(addSeconds(TEN_MINUTES_AGO, 90), CONST.DATE.FNS_DB_FORMAT_STRING); + + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastReadTime: reportAction3CreatedDate, + lastVisibleActionCreated: reportAction9CreatedDate, + lastMessageText: 'Test', + participants: {[USER_B_ACCOUNT_ID]: {hidden: false}}, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + }); + const createdReportActionID = NumberUtils.rand64().toString(); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [createdReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + created: format(TEN_MINUTES_AGO, CONST.DATE.FNS_DB_FORMAT_STRING), + reportActionID: createdReportActionID, + message: [ + { + style: 'strong', + text: '__FAKE__', + type: 'TEXT', }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), - }); - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(); - return waitForBatchedUpdatesWithAct(); - }); + { + style: 'normal', + text: 'created this report', + type: 'TEXT', + }, + ], + }, + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), + }); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + + await waitForBatchedUpdatesWithAct(); +} + +let lastComment = 'Current User Comment 1'; +async function addComment() { + const num = Number.parseInt(lastComment.slice(-1), 10); + lastComment = `${lastComment.slice(0, -1)}${num + 1}`; + const comment = lastComment; + const reportActionsBefore = (await TestHelper.onyxGet(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`)) as Record; + Report.addComment(REPORT_ID, comment); + const reportActionsAfter = (await TestHelper.onyxGet(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`)) as Record; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newReportActionID = Object.keys(reportActionsAfter).find((reportActionID) => !reportActionsBefore[reportActionID])!; + await act(() => + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [newReportActionID]: { + previousReportActionID: '9', + }, + }), + ); + await waitForBatchedUpdatesWithAct(); + + // Verify the comment is visible (it will appear twice, once in the LHN and once on the report screen) + expect(screen.getAllByText(comment)[0]).toBeOnTheScreen(); } +const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); + describe('Unread Indicators', () => { afterEach(() => { jest.clearAllMocks(); @@ -319,7 +342,6 @@ describe('Unread Indicators', () => { expect(reportComments).toHaveLength(9); // Since the last read timestamp is the timestamp of action 3 we should have an unread indicator above the next "unread" action which will // have actionID of 4 - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; @@ -335,7 +357,6 @@ describe('Unread Indicators', () => { .then(async () => { await act(() => transitionEndCB?.()); // Verify the unread indicator is present - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); }) @@ -358,7 +379,6 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the unread indicator is not present - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); // Tap on the chat again @@ -366,7 +386,6 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the unread indicator is not present - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); expect(areYouOnChatListScreen()).toBe(false); @@ -476,7 +495,6 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify the indicator appears above the last action - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; @@ -511,7 +529,6 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(() => { - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); @@ -520,30 +537,23 @@ describe('Unread Indicators', () => { return waitFor(() => expect(isNewMessagesBadgeVisible()).toBe(false)); })); - it('Keep showing the new line indicator when a new message is created by the current user', () => - signInAndGetAppWithUnreadChat() - .then(() => { - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(areYouOnChatListScreen()).toBe(true); + it('Keep showing the new line indicator when a new message is created by the current user', async () => { + await signInAndGetAppWithUnreadChat(); - // Navigate to the report and verify the indicator is present - return navigateToSidebarOption(0); - }) - .then(async () => { - await act(() => transitionEndCB?.()); - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); - const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); - expect(unreadIndicator).toHaveLength(1); + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(areYouOnChatListScreen()).toBe(true); - // Leave a comment as the current user and verify the indicator is removed - Report.addComment(REPORT_ID, 'Current User Comment 1'); - return waitForBatchedUpdates(); - }) - .then(() => { - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); - const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); - expect(unreadIndicator).toHaveLength(1); - })); + // Navigate to the report and verify the indicator is present + await navigateToSidebarOption(0); + await act(() => transitionEndCB?.()); + let unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); + expect(unreadIndicator).toHaveLength(1); + + // Leave a comment as the current user and verify the indicator is not removed + await addComment(); + unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); + expect(unreadIndicator).toHaveLength(1); + }); xit('Keeps the new line indicator when the user moves the App to the background', () => signInAndGetAppWithUnreadChat() @@ -555,7 +565,6 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(() => { - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); @@ -564,7 +573,6 @@ describe('Unread Indicators', () => { }) .then(() => navigateToSidebarOption(0)) .then(() => { - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(0); @@ -573,7 +581,6 @@ describe('Unread Indicators', () => { return waitForBatchedUpdates(); }) .then(() => { - const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); let unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); @@ -597,11 +604,10 @@ describe('Unread Indicators', () => { signInAndGetAppWithUnreadChat() // Navigate to the chat and simulate leaving a comment from the current user .then(() => navigateToSidebarOption(0)) - .then(() => { + .then(() => // Leave a comment as the current user - Report.addComment(REPORT_ID, 'Current User Comment 1'); - return waitForBatchedUpdates(); - }) + addComment(), + ) .then(() => { // Simulate the response from the server so that the comment can be deleted in this test lastReportAction = reportActions ? CollectionUtils.lastItem(reportActions) : undefined; @@ -619,7 +625,7 @@ describe('Unread Indicators', () => { expect(alternateText).toHaveLength(1); // This message is visible on the sidebar and the report screen, so there are two occurrences. - expect(screen.getAllByText('Current User Comment 1')[0]).toBeOnTheScreen(); + expect(screen.getAllByText(lastComment)[0]).toBeOnTheScreen(); if (lastReportAction) { Report.deleteReportComment(REPORT_ID, lastReportAction); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 9ca0969abc6a..dffb2b4e312a 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,5 +1,6 @@ import {Str} from 'expensify-common'; import Onyx from 'react-native-onyx'; +import type {ConnectOptions, OnyxKey} from 'react-native-onyx'; import CONST from '@src/CONST'; import * as Session from '@src/libs/actions/Session'; import HttpUtils from '@src/libs/HttpUtils'; @@ -247,5 +248,33 @@ const createAddListenerMock = () => { return {triggerTransitionEnd, addListener}; }; +/** + * Get an Onyx value. Only for use in tests for now. + */ +async function onyxGet(key: OnyxKey): Promise>['callback']>[0]> { + return new Promise((resolve) => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + // @ts-expect-error This does not need more strict type checking as it's only for tests + const connectionID = Onyx.connect({ + key, + callback: (value) => { + Onyx.disconnect(connectionID); + resolve(value); + }, + waitForCollectionCallback: true, + }); + }); +} + export type {MockFetch, FormData}; -export {assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, createAddListenerMock, getGlobalFetchMock, setPersonalDetails, signInWithTestUser, signOutTestUser}; +export { + assertFormDataMatchesObject, + buildPersonalDetails, + buildTestReportComment, + createAddListenerMock, + getGlobalFetchMock, + setPersonalDetails, + signInWithTestUser, + signOutTestUser, + onyxGet, +};