diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 3e3f4d1b8e5d..596afc0ba6c0 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -307,6 +307,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti {hasBrickError && ( @@ -321,6 +322,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti {shouldShowGreenDotIndicator && ( diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 6693c90adaa0..b7e14bb5bd82 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -1,11 +1,15 @@ import {screen} from '@testing-library/react-native'; import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; +import type {OnyxMultiSetInput} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; +import FontUtils from '@styles/utils/FontUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ViolationName} from '@src/types/onyx'; import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; @@ -48,6 +52,7 @@ jest.mock('@components/withCurrentUserPersonalDetails', () => { const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; const betas = [CONST.BETAS.DEFAULT_ROOMS]; +const TEST_POLICY_ID = '1'; const signUpWithTestUser = () => { TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); @@ -64,13 +69,26 @@ const getDisplayNames = () => { }; // Reusable function to setup a mock report. Feel free to add more parameters as needed. -const createReport = (isPinned = false, participants = [1, 2], messageCount = 1) => { +const createReport = ( + isPinned = false, + participants = [1, 2], + messageCount = 1, + chatType: ValueOf | undefined = undefined, + policyID: string = CONST.POLICY.ID_FAKE, + isUnread = false, +) => { return { - ...LHNTestUtils.getFakeReport(participants, messageCount), + ...LHNTestUtils.getFakeReport(participants, messageCount, isUnread), isPinned, + chatType, + policyID, }; }; +const createFakeTransactionViolation = (violationName: ViolationName = CONST.VIOLATIONS.HOLD, showInReview = true) => { + return LHNTestUtils.getFakeTransactionViolation(violationName, showInReview); +}; + describe('SidebarLinksData', () => { beforeAll(() => { Onyx.init({ @@ -80,14 +98,15 @@ describe('SidebarLinksData', () => { }); // Helper to initialize common state - const initializeState = async (reportData: ReportCollectionDataSet) => { + const initializeState = async (reportData?: ReportCollectionDataSet, otherData?: OnyxMultiSetInput) => { await waitForBatchedUpdates(); await Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.BETAS]: betas, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_APP]: false, - ...reportData, + ...(reportData ?? {}), + ...(otherData ?? {}), }); }; @@ -172,7 +191,141 @@ describe('SidebarLinksData', () => { // Then the report should appear in the sidebar because it’s pinned. expect(getOptionRows()).toHaveLength(1); - // TODO add the proper assertion for the pinned report. + // And the pin icon should be shown + expect(screen.getByTestId('Pin Icon')).toBeOnTheScreen(); + }); + + it('should display the report with violations', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + + // And the report is initialized in Onyx. + const report: Report = { + ...createReport(true, undefined, undefined, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, TEST_POLICY_ID), + ownerAccountID: TEST_USER_ACCOUNT_ID, + }; + + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // The report should appear in the sidebar because it’s pinned. + expect(getOptionRows()).toHaveLength(1); + await waitForBatchedUpdatesWithAct(); + + const expenseReport: Report = { + ...createReport(false, undefined, undefined, undefined, TEST_POLICY_ID), + ownerAccountID: TEST_USER_ACCOUNT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + }; + const transaction = LHNTestUtils.getFakeTransaction(expenseReport.reportID); + const transactionViolation = createFakeTransactionViolation(); + + // When the report has outstanding violations + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, [transactionViolation]); + + // The RBR icon should be shown + expect(screen.getByTestId('RBR Icon')).toBeOnTheScreen(); + }); + + it('should display the report awaiting user action', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report: Report = { + ...createReport(false), + hasOutstandingChildRequest: true, + }; + + // And the report is initialized in Onyx. + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // Then the report should appear in the sidebar because it requires attention from the user + expect(getOptionRows()).toHaveLength(1); + + // And a green dot icon should be shown + expect(screen.getByTestId('GBR Icon')).toBeOnTheScreen(); + }); + + it('should display the archived report in the default mode', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const archivedReport: Report = { + ...createReport(false), + // eslint-disable-next-line @typescript-eslint/naming-convention + private_isArchived: DateUtils.getDBTime(), + }; + const reportNameValuePairs = { + type: 'chat', + // eslint-disable-next-line @typescript-eslint/naming-convention + private_isArchived: true, + }; + + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${archivedReport.reportID}`]: archivedReport, + }); + + await waitForBatchedUpdatesWithAct(); + + // And the user is in the default mode + await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs); + + // The report should appear in the sidebar because it's archived + expect(getOptionRows()).toHaveLength(1); + }); + + it('should display the selfDM report by default', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report = createReport(true, undefined, undefined, undefined, CONST.REPORT.CHAT_TYPE.SELF_DM, undefined); + + // And the selfDM is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // The selfDM report should appear in the sidebar by default + expect(getOptionRows()).toHaveLength(1); + }); + + it('should display the unread report in the focus mode with the bold text', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report: Report = { + ...createReport(undefined, undefined, undefined, undefined, undefined, true), + lastMessageText: 'fake last message', + lastActorAccountID: TEST_USER_ACCOUNT_ID, + }; + + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + await waitForBatchedUpdatesWithAct(); + + // And the user is in focus mode + await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + + // The report should appear in the sidebar because it's unread + expect(getOptionRows()).toHaveLength(1); + + // And the text is bold + const displayNameText = getDisplayNames()?.at(0); + expect(displayNameText).toHaveStyle({fontWeight: FontUtils.fontWeight.bold}); + + await waitForBatchedUpdatesWithAct(); + + // When the report is marked as read + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, { + lastReadTime: report.lastVisibleActionCreated, + }); + + // The report should not disappear in the sidebar because we are in the focus mode + expect(getOptionRows()).toHaveLength(0); }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index dc752ae73b1c..bf36041c7223 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -5,6 +5,7 @@ import Onyx from 'react-native-onyx'; import DateUtils from '@libs/DateUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import * as TransactionUtils from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; @@ -1180,4 +1181,219 @@ describe('ReportUtils', () => { }); }); }); + + describe('shouldReportBeInOptionList tests', () => { + afterEach(() => Onyx.clear()); + + it('should return true when the report is current active report', () => { + const report = LHNTestUtils.getFakeReport(); + const currentReportId = report.reportID; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeTruthy(); + }); + + it('should return true when the report has outstanding violations', async () => { + const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseCreatedAction1 = ReportUtils.buildOptimisticIOUReportAction( + 'create', + 100, + 'USD', + '', + [], + expenseTransaction.transactionID, + undefined, + expenseReport.reportID, + undefined, + false, + false, + undefined, + undefined, + ); + const expenseCreatedAction2 = ReportUtils.buildOptimisticIOUReportAction( + 'create', + 100, + 'USD', + '', + [], + expenseTransaction.transactionID, + undefined, + expenseReport.reportID, + undefined, + false, + false, + undefined, + undefined, + ); + const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction1, expenseReport); + const currentReportId = '1'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [expenseCreatedAction1.reportActionID]: expenseCreatedAction1, + [expenseCreatedAction2.reportActionID]: expenseCreatedAction2, + }); + expect( + ReportUtils.shouldReportBeInOptionList({ + report: transactionThreadReport, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: true, + excludeEmptyChats: false, + }), + ).toBeTruthy(); + }); + + it('should return true when the report needing user action', () => { + const chatReport: Report = { + ...LHNTestUtils.getFakeReport(), + hasOutstandingChildRequest: true, + }; + const currentReportId = '3'; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report: chatReport, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeTruthy(); + }); + + it('should return true when the report has valid draft comment', async () => { + const report = LHNTestUtils.getFakeReport(); + const currentReportId = '3'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, 'fake draft'); + + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeTruthy(); + }); + + it('should return true when the report is pinned', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + isPinned: true, + }; + const currentReportId = '3'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeTruthy(); + }); + + it('should return true when the report is unread and we are in the focus mode', async () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + lastReadTime: '1', + lastVisibleActionCreated: '2', + type: CONST.REPORT.TYPE.CHAT, + participants: { + '1': { + notificationPreference: 'always', + }, + }, + lastMessageText: 'fake', + }; + const currentReportId = '3'; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + + await Onyx.merge(ONYXKEYS.SESSION, { + accountID: 1, + }); + + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeTruthy(); + }); + + it('should return true when the report is an archived report and we are in the default mode', async () => { + const archivedReport: Report = { + ...LHNTestUtils.getFakeReport(), + reportID: '1', + private_isArchived: DateUtils.getDBTime(), + }; + const reportNameValuePairs = { + type: 'chat', + private_isArchived: true, + }; + const currentReportId = '3'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs); + + expect( + ReportUtils.shouldReportBeInOptionList({ + report: archivedReport, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: false, + excludeEmptyChats: false, + }), + ).toBeTruthy(); + }); + + it('should return false when the report is an archived report and we are in the focus mode', async () => { + const archivedReport: Report = { + ...LHNTestUtils.getFakeReport(), + reportID: '1', + private_isArchived: DateUtils.getDBTime(), + }; + const reportNameValuePairs = { + type: 'chat', + private_isArchived: true, + }; + const currentReportId = '3'; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs); + + expect( + ReportUtils.shouldReportBeInOptionList({ + report: archivedReport, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: false, + excludeEmptyChats: false, + }), + ).toBeFalsy(); + }); + + it('should return true when the report is selfDM', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, + }; + const currentReportId = '3'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + const includeSelfDM = true; + expect( + ReportUtils.shouldReportBeInOptionList({ + report, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: false, + excludeEmptyChats: false, + includeSelfDM, + }), + ).toBeTruthy(); + }); + }); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 73d10365c272..1a3fc3d07f28 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -14,7 +14,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import CONST from '@src/CONST'; -import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportAction, TransactionViolation, ViolationName} from '@src/types/onyx'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import waitForBatchedUpdatesWithAct from './waitForBatchedUpdatesWithAct'; @@ -124,6 +124,7 @@ const fakePersonalDetails: PersonalDetailsList = { let lastFakeReportID = 0; let lastFakeReportActionID = 0; +let lastFakeTransactionID = 0; /** * @param millisecondsInThePast the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) @@ -191,6 +192,15 @@ function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = }; } +function getFakeTransaction(expenseReportID: string, amount = 1, currency: string = CONST.CURRENCY.USD) { + return { + transactionID: `${++lastFakeTransactionID}`, + amount, + currency, + reportID: expenseReportID, + }; +} + function getAdvancedFakeReport(isArchived: boolean, isUserCreatedPolicyRoom: boolean, hasAddWorkspaceError: boolean, isUnread: boolean, isPinned: boolean): Report { return { ...getFakeReport([1, 2], 0, isUnread), @@ -244,6 +254,14 @@ function getFakePolicy(id = '1', name = 'Workspace-Test-001'): Policy { }; } +function getFakeTransactionViolation(violationName: ViolationName, showInReview = true): TransactionViolation { + return { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: violationName, + showInReview, + }; +} + /** * @param millisecondsInThePast the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) */ @@ -354,4 +372,6 @@ export { getFakeReportWithPolicy, getFakePolicy, getFakeAdvancedReportAction, + getFakeTransactionViolation, + getFakeTransaction, };