diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index fc189a3aef36..fc38b102b8e8 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -122,7 +122,10 @@ function ReportActionItemSingle(props) { id: secondaryAccountId, }; } else if (!isWorkspaceActor) { - secondaryAvatar = ReportUtils.getIcons(props.report, {})[props.report.isOwnPolicyExpenseChat ? 0 : 1]; + const avatarIconIndex = props.report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(props.report) ? 0 : 1; + const reportIcons = ReportUtils.getIcons(props.report, {}); + + secondaryAvatar = reportIcons[avatarIconIndex]; } const icon = {source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName, id: isWorkspaceActor ? '' : actorAccountID}; diff --git a/tests/unit/ReportActionItemSingleTest.js b/tests/unit/ReportActionItemSingleTest.js new file mode 100644 index 000000000000..d6b46eb55414 --- /dev/null +++ b/tests/unit/ReportActionItemSingleTest.js @@ -0,0 +1,86 @@ +import Onyx from 'react-native-onyx'; +import {cleanup, screen} from '@testing-library/react-native'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; + +const ONYXKEYS = { + PERSONAL_DETAILS_LIST: 'personalDetailsList', + IS_LOADING_REPORT_DATA: 'isLoadingReportData', + COLLECTION: { + REPORT_ACTIONS: 'reportActions_', + POLICY: 'policy_', + }, + NETWORK: 'network', +}; + +describe('ReportActionItemSingle', () => { + beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + registerStorageEventListener: () => {}, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }), + ); + + beforeEach(() => { + // Wrap Onyx each onyx action with waitForBatchedUpdates + wrapOnyxWithWaitForBatchedUpdates(Onyx); + // Initialize the network key for OfflineWithFeedback + return Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + }); + + // Clear out Onyx after each test so that each test starts with a clean slate + afterEach(() => { + cleanup(); + Onyx.clear(); + }); + + describe('when the Report is a policy expense chat', () => { + describe('and the property "shouldShowSubscriptAvatar" is true', () => { + const shouldShowSubscriptAvatar = true; + const fakeReport = LHNTestUtils.getFakeReportWithPolicy([1, 2]); + const fakeReportAction = LHNTestUtils.getFakeAdvancedReportAction(); + const fakePolicy = LHNTestUtils.getFakePolicy(fakeReport.policyID); + const fakePersonalDetails = { + [fakeReportAction.actorAccountID]: { + accountID: fakeReportAction.actorAccountID, + login: 'email1@test.com', + displayName: 'Email One', + avatar: 'https://example.com/avatar.png', + firstName: 'One', + }, + }; + + beforeEach(() => { + LHNTestUtils.getDefaultRenderedReportActionItemSingle(shouldShowSubscriptAvatar, fakeReport, fakeReportAction); + }); + + function setup() { + return waitForBatchedUpdates().then(() => + Onyx.multiSet({ + [ONYXKEYS.PERSONAL_DETAILS_LIST]: fakePersonalDetails, + [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [`${ONYXKEYS.COLLECTION.POLICY}${fakeReport.policyID}`]: fakePolicy, + }), + ); + } + + it('renders secondary Avatar properly', () => { + const expectedSecondaryIconTestId = 'SvgDefaultAvatar_w Icon'; + + return setup().then(() => { + expect(screen.getByTestId(expectedSecondaryIconTestId)).toBeDefined(); + }); + }); + + it('renders Person information', () => { + const [expectedPerson] = fakeReportAction.person; + + return setup().then(() => { + expect(screen.getByText(expectedPerson.text)).toBeDefined(); + }); + }); + }); + }); +}); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index ce4edc75b444..6e7d4390b4e9 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -5,10 +5,13 @@ import ComposeProviders from '../../src/components/ComposeProviders'; import OnyxProvider from '../../src/components/OnyxProvider'; import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import SidebarLinksData from '../../src/pages/home/sidebar/SidebarLinksData'; +import ReportActionItemSingle from '../../src/pages/home/report/ReportActionItemSingle'; import {EnvironmentProvider} from '../../src/components/withEnvironment'; import {CurrentReportIDContextProvider} from '../../src/components/withCurrentReportID'; import CONST from '../../src/CONST'; import DateUtils from '../../src/libs/DateUtils'; +import reportPropTypes from '../../src/pages/reportPropTypes'; +import reportActionPropTypes from '../../src/pages/home/report/reportActionPropTypes'; // we have to mock `useIsFocused` because it's used in the SidebarLinks component const mockedNavigate = jest.fn(); @@ -167,6 +170,63 @@ function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorksp }; } +/** + * @param {Number[]} [participantAccountIDs] + * @param {Number} [millisecondsInThePast] the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) + * @param {boolean} [isUnread] + * @returns {Object} + */ +function getFakeReportWithPolicy(participantAccountIDs = [1, 2], millisecondsInThePast = 0, isUnread = false) { + return { + ...getFakeReport(participantAccountIDs, millisecondsInThePast, isUnread), + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID: '08CE60F05A5D86E1', + oldPolicyName: '', + isOwnPolicyExpenseChat: false, + ownerAccountID: participantAccountIDs[0], + }; +} + +/** + * @param {Number} [id] + * @param {String} [name] + * @returns {Object} + */ +function getFakePolicy(id = 1, name = 'Workspace-Test-001') { + return { + id, + name, + isFromFullPolicy: false, + role: 'admin', + type: 'free', + owner: 'myuser@gmail.com', + outputCurrency: 'BRL', + avatar: '', + employeeList: [], + isPolicyExpenseChatEnabled: true, + areChatRoomsEnabled: true, + lastModified: 1697323926777105, + autoReporting: true, + autoReportingFrequency: 'immediate', + defaultBillable: false, + disabledFields: {defaultBillable: true, reimbursable: false}, + }; +} + +/** + * @param {String} actionName + * @param {String} actor + * @param {Number} millisecondsInThePast the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) + * @returns {Object} + */ +function getFakeAdvancedReportAction(actionName = 'IOU', actor = 'email1@test.com', millisecondsInThePast = 0) { + return { + ...getFakeReportAction(actor, millisecondsInThePast), + actionName, + }; +} + /** * @param {String} [currentReportID] */ @@ -218,4 +278,97 @@ MockedSidebarLinks.defaultProps = { currentReportID: '', }; -export {fakePersonalDetails, getDefaultRenderedSidebarLinks, getAdvancedFakeReport, getFakeReport, getFakeReportAction, MockedSidebarLinks}; +/** + * @param {React.ReactElement} component + */ +function internalRender(component) { + // A try-catch block needs to be added to the rendering so that any errors that happen while the component + // renders are caught and logged to the console. Without the try-catch block, Jest might only report the error + // as "The above error occurred in your component", without providing specific details. By using a try-catch block, + // any errors are caught and logged, allowing you to identify the exact error that might be causing a rendering issue + // when developing tests. + + try { + render(component); + } catch (error) { + console.error(error); + } +} + +/** + * @param {Boolean} [shouldShowSubscriptAvatar] + * @param {Object} [report] + * @param {Object} [reportAction] + */ +function getDefaultRenderedReportActionItemSingle(shouldShowSubscriptAvatar = true, report = null, reportAction = null) { + const currentReport = report || getFakeReport(); + const currentReportAction = reportAction || getFakeAdvancedReportAction(); + + internalRender( + , + ); +} + +/** + * @param {Boolean} shouldShowSubscriptAvatar + * @param {Object} report + * @param {Object} reportAction + * @returns {JSX.Element} + */ +function MockedReportActionItemSingle({shouldShowSubscriptAvatar, report, reportAction}) { + const personalDetailsList = { + [reportAction.actorAccountID]: { + accountID: reportAction.actorAccountID, + login: 'email1@test.com', + displayName: 'Email One', + avatar: 'https://example.com/avatar.png', + firstName: 'One', + }, + }; + + return ( + + + + ); +} + +MockedReportActionItemSingle.propTypes = { + shouldShowSubscriptAvatar: PropTypes.bool, + report: reportPropTypes, + reportAction: PropTypes.shape(reportActionPropTypes), +}; + +MockedReportActionItemSingle.defaultProps = { + shouldShowSubscriptAvatar: true, + report: null, + reportAction: null, +}; + +export { + fakePersonalDetails, + getDefaultRenderedSidebarLinks, + getAdvancedFakeReport, + getFakeReport, + getFakeReportAction, + MockedSidebarLinks, + getDefaultRenderedReportActionItemSingle, + MockedReportActionItemSingle, + getFakeReportWithPolicy, + getFakePolicy, + getFakeAdvancedReportAction, +};