Skip to content

Commit 50dcda4

Browse files
authored
Merge pull request Expensify#51330 from allgandalf/issue50351
[No QA]: Add unit test for automatically approved expense & add UI tests
2 parents 2e3615a + 1a6b1f9 commit 50dcda4

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {PortalProvider} from '@gorhom/portal';
2+
import * as NativeNavigation from '@react-navigation/native';
3+
import {render, screen} from '@testing-library/react-native';
4+
import React from 'react';
5+
import Onyx from 'react-native-onyx';
6+
import ComposeProviders from '@components/ComposeProviders';
7+
import HTMLEngineProvider from '@components/HTMLEngineProvider';
8+
import {LocaleContextProvider} from '@components/LocaleContextProvider';
9+
import OnyxProvider from '@components/OnyxProvider';
10+
import OptionsListContextProvider from '@components/OptionListContextProvider';
11+
import ScreenWrapper from '@components/ScreenWrapper';
12+
import {translateLocal} from '@libs/Localize';
13+
import {getIOUActionForReportID} from '@libs/ReportActionsUtils';
14+
import PureReportActionItem from '@pages/home/report/PureReportActionItem';
15+
import CONST from '@src/CONST';
16+
import * as ReportActionUtils from '@src/libs/ReportActionsUtils';
17+
import ONYXKEYS from '@src/ONYXKEYS';
18+
import type {ReportAction} from '@src/types/onyx';
19+
import type {OriginalMessage} from '@src/types/onyx/ReportAction';
20+
import type ReportActionName from '@src/types/onyx/ReportActionName';
21+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
22+
import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
23+
24+
jest.mock('@react-navigation/native');
25+
26+
const ACTOR_ACCOUNT_ID = 123456789;
27+
const actorEmail = 'test@test.com';
28+
29+
const createReportAction = (actionName: ReportActionName, originalMessageExtras: Partial<OriginalMessage<ReportActionName>>) =>
30+
({
31+
reportActionID: '12345',
32+
actorAccountID: ACTOR_ACCOUNT_ID,
33+
created: '2025-07-12 09:03:17.653',
34+
actionName,
35+
automatic: true,
36+
shouldShow: true,
37+
avatar: '',
38+
person: [{type: 'TEXT', style: 'strong', text: 'Concierge'}],
39+
message: [{type: 'COMMENT', html: 'some message', text: 'some message'}],
40+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
41+
originalMessage: {
42+
...originalMessageExtras,
43+
},
44+
}) as ReportAction;
45+
46+
describe('PureReportActionItem', () => {
47+
beforeAll(() => {
48+
Onyx.init({
49+
keys: ONYXKEYS,
50+
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
51+
});
52+
jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''});
53+
jest.spyOn(ReportActionUtils, 'getIOUActionForReportID').mockImplementation(getIOUActionForReportID);
54+
});
55+
56+
beforeEach(() => {
57+
wrapOnyxWithWaitForBatchedUpdates(Onyx);
58+
return Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}).then(() =>
59+
Onyx.merge(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, {
60+
[ACTOR_ACCOUNT_ID]: {
61+
accountID: ACTOR_ACCOUNT_ID,
62+
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png',
63+
displayName: actorEmail,
64+
login: actorEmail,
65+
},
66+
}),
67+
);
68+
});
69+
70+
afterEach(() => {
71+
Onyx.clear();
72+
});
73+
74+
function renderItemWithAction(action: ReportAction) {
75+
return render(
76+
<ComposeProviders components={[OnyxProvider, LocaleContextProvider, HTMLEngineProvider]}>
77+
<OptionsListContextProvider>
78+
<ScreenWrapper testID="test">
79+
<PortalProvider>
80+
<PureReportActionItem
81+
allReports={undefined}
82+
policies={undefined}
83+
report={undefined}
84+
reportActions={[]}
85+
parentReportAction={undefined}
86+
action={action}
87+
displayAsGroup={false}
88+
isMostRecentIOUReportAction={false}
89+
shouldDisplayNewMarker={false}
90+
index={0}
91+
isFirstVisibleReportAction={false}
92+
taskReport={undefined}
93+
linkedReport={undefined}
94+
iouReportOfLinkedReport={undefined}
95+
/>
96+
</PortalProvider>
97+
</ScreenWrapper>
98+
</OptionsListContextProvider>
99+
</ComposeProviders>,
100+
);
101+
}
102+
103+
describe('Automatic actions', () => {
104+
it('APPROVED action via workspace rules', async () => {
105+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.APPROVED, {automaticAction: true});
106+
renderItemWithAction(action);
107+
await waitForBatchedUpdatesWithAct();
108+
109+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
110+
expect(screen.getByText(translateLocal('iou.automaticallyApproved'))).toBeOnTheScreen();
111+
});
112+
113+
it('FORWARDED action via workspace rules', async () => {
114+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.FORWARDED, {automaticAction: true});
115+
renderItemWithAction(action);
116+
await waitForBatchedUpdatesWithAct();
117+
118+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
119+
expect(screen.getByText(translateLocal('iou.automaticallyForwarded'))).toBeOnTheScreen();
120+
});
121+
122+
it('SUBMITTED action via harvesting', async () => {
123+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: true});
124+
renderItemWithAction(action);
125+
await waitForBatchedUpdatesWithAct();
126+
127+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
128+
expect(screen.getByText(translateLocal('iou.automaticallySubmitted'))).toBeOnTheScreen();
129+
});
130+
131+
it('SUBMITTED_AND_CLOSED action via harvesting', async () => {
132+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, {harvesting: true});
133+
renderItemWithAction(action);
134+
await waitForBatchedUpdatesWithAct();
135+
136+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
137+
expect(screen.getByText(translateLocal('iou.automaticallySubmitted'))).toBeOnTheScreen();
138+
});
139+
});
140+
141+
describe('Manual actions', () => {
142+
it('APPROVED action', async () => {
143+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.APPROVED, {automaticAction: false});
144+
renderItemWithAction(action);
145+
await waitForBatchedUpdatesWithAct();
146+
147+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
148+
expect(screen.getByText(translateLocal('iou.approvedMessage'))).toBeOnTheScreen();
149+
});
150+
151+
it('FORWARDED action', async () => {
152+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.FORWARDED, {automaticAction: false});
153+
renderItemWithAction(action);
154+
await waitForBatchedUpdatesWithAct();
155+
156+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
157+
expect(screen.getByText(translateLocal('iou.forwarded'))).toBeOnTheScreen();
158+
});
159+
160+
it('SUBMITTED action', async () => {
161+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED, {harvesting: false});
162+
renderItemWithAction(action);
163+
await waitForBatchedUpdatesWithAct();
164+
165+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
166+
expect(screen.getByText(translateLocal('iou.submitted'))).toBeOnTheScreen();
167+
});
168+
169+
it('SUBMITTED_AND_CLOSED action', async () => {
170+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED, {harvesting: false});
171+
renderItemWithAction(action);
172+
await waitForBatchedUpdatesWithAct();
173+
174+
expect(screen.getByText(actorEmail)).toBeOnTheScreen();
175+
expect(screen.getByText(translateLocal('iou.submitted'))).toBeOnTheScreen();
176+
});
177+
});
178+
});

tests/unit/ReportUtilsTest.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,102 @@ describe('ReportUtils', () => {
787787
});
788788
});
789789

790+
describe('Automatically approved report message via automatic (not by a human) action is', () => {
791+
test('shown when the report is forwarded (Control feature)', () => {
792+
const expenseReport = {
793+
...LHNTestUtils.getFakeReport(),
794+
type: CONST.REPORT.TYPE.EXPENSE,
795+
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
796+
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
797+
parentReportID: '101',
798+
policyID: policy.id,
799+
};
800+
const submittedParentReportAction = {
801+
actionName: CONST.REPORT.ACTIONS.TYPE.FORWARDED,
802+
originalMessage: {
803+
amount: 169,
804+
currency: 'USD',
805+
automaticAction: true,
806+
},
807+
} as ReportAction;
808+
809+
expect(getReportName(expenseReport, policy, submittedParentReportAction)).toBe(
810+
'approved via <a href="https://help.expensify.com/articles/new-expensify/workspaces/Set-up-rules#configure-expense-report-rules">workspace rules</a>',
811+
);
812+
});
813+
814+
test('shown when the report is approved', () => {
815+
const expenseReport = {
816+
...LHNTestUtils.getFakeReport(),
817+
type: CONST.REPORT.TYPE.EXPENSE,
818+
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
819+
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
820+
parentReportID: '101',
821+
policyID: policy.id,
822+
};
823+
const submittedParentReportAction = {
824+
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
825+
originalMessage: {
826+
amount: 169,
827+
currency: 'USD',
828+
automaticAction: true,
829+
},
830+
} as ReportAction;
831+
832+
expect(getReportName(expenseReport, policy, submittedParentReportAction)).toBe(
833+
'approved via <a href="https://help.expensify.com/articles/new-expensify/workspaces/Set-up-rules#configure-expense-report-rules">workspace rules</a>',
834+
);
835+
});
836+
});
837+
838+
describe('Automatically submitted via harvesting (delayed submit) report message is', () => {
839+
test('shown when report is submitted and status is submitted', () => {
840+
const expenseReport = {
841+
...LHNTestUtils.getFakeReport(),
842+
type: CONST.REPORT.TYPE.EXPENSE,
843+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
844+
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
845+
parentReportID: '101',
846+
policyID: policy.id,
847+
};
848+
const submittedParentReportAction = {
849+
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
850+
originalMessage: {
851+
amount: 169,
852+
currency: 'USD',
853+
harvesting: true,
854+
},
855+
} as ReportAction;
856+
857+
expect(getReportName(expenseReport, policy, submittedParentReportAction)).toBe(
858+
'submitted via <a href="https://help.expensify.com/articles/new-expensify/workspaces/Set-up-workflows#select-workflows">delay submissions</a>',
859+
);
860+
});
861+
862+
test('shown when report is submitted and status is closed', () => {
863+
const expenseReport = {
864+
...LHNTestUtils.getFakeReport(),
865+
type: CONST.REPORT.TYPE.EXPENSE,
866+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
867+
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
868+
parentReportID: '101',
869+
policyID: policy.id,
870+
};
871+
const submittedParentReportAction = {
872+
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED,
873+
originalMessage: {
874+
amount: 169,
875+
currency: 'USD',
876+
harvesting: true,
877+
},
878+
} as ReportAction;
879+
880+
expect(getReportName(expenseReport, policy, submittedParentReportAction)).toBe(
881+
'submitted via <a href="https://help.expensify.com/articles/new-expensify/workspaces/Set-up-workflows#select-workflows">delay submissions</a>',
882+
);
883+
});
884+
});
885+
790886
describe('requiresAttentionFromCurrentUser', () => {
791887
afterEach(async () => {
792888
await Onyx.clear();

0 commit comments

Comments
 (0)