Skip to content

Commit 9f2ba6d

Browse files
authored
Merge pull request Expensify#72700 from callstack-internal/test/report-transaction-rows
[NO QA] Add TransactionGroupListItem tests
2 parents 182576d + f35c57e commit 9f2ba6d

10 files changed

Lines changed: 272 additions & 3 deletions

File tree

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5386,6 +5386,7 @@ const CONST = {
53865386
LOTTIE_VIEW_TEST_ID: 'LottieView',
53875387

53885388
DOT_INDICATOR_TEST_ID: 'DotIndicator',
5389+
ANIMATED_COLLAPSIBLE_CONTENT_TEST_ID: 'animated-collapsible-content',
53895390

53905391
CHAT_HEADER_LOADER_HEIGHT: 36,
53915392

src/components/AnimatedCollapsible/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function AnimatedCollapsible({isExpanded, children, header, duration = 300, styl
110110
<Animated.View style={[contentAnimatedStyle, contentStyle]}>
111111
{isExpanded || isRendered ? (
112112
<Animated.View
113+
testID={CONST.ANIMATED_COLLAPSIBLE_CONTENT_TEST_ID}
113114
style={styles.stickToTop}
114115
onLayout={(e) => {
115116
const height = e.nativeEvent.layout.height;

src/components/ReportSearchHeader/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function ReportSearchHeader({report, style, transactions, avatarBorderColor}: Re
2525
<View
2626
dataSet={{dragArea: false}}
2727
style={[style, styles.reportSearchHeaderBar]}
28+
testID={ReportSearchHeader.displayName}
2829
>
2930
<View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.justifyContentBetween, styles.overflowHidden]}>{middleContent}</View>
3031
</View>

src/components/SelectionListWithSections/Search/ActionCell.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function ActionCell({
110110

111111
return isLargeScreenWidth ? (
112112
<Button
113+
testID={ActionCell.displayName}
113114
text={text}
114115
onPress={goToItem}
115116
small

src/components/SelectionListWithSections/Search/TotalCell.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function TotalCell({total, currency}: TotalCellProps) {
1313

1414
return (
1515
<TextWithTooltip
16+
testID={TotalCell.displayName}
1617
shouldShowTooltip
1718
text={convertToDisplayString(total, currency)}
1819
style={[styles.optionDisplayName, styles.pre, styles.justifyContentCenter, styles.textBold, styles.textAlignRight]}

src/components/TextWithTooltip/index.native.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import useThemeStyles from '@hooks/useThemeStyles';
44
import {getProcessedText, splitTextWithEmojis} from '@libs/EmojiUtils';
55
import type TextWithTooltipProps from './types';
66

7-
function TextWithTooltip({text, style, numberOfLines = 1, forwardedFSClass}: TextWithTooltipProps) {
7+
function TextWithTooltip({testID, text, style, numberOfLines = 1, forwardedFSClass}: TextWithTooltipProps) {
88
const styles = useThemeStyles();
99
const processedTextArray = splitTextWithEmojis(text);
1010

1111
return (
1212
<Text
13+
testID={testID}
1314
style={style}
1415
numberOfLines={numberOfLines}
1516
fsClass={forwardedFSClass}

src/components/TextWithTooltip/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type LayoutChangeEvent = {
77
target: HTMLElement;
88
};
99

10-
function TextWithTooltip({text, shouldShowTooltip, style, numberOfLines = 1, forwardedFSClass}: TextWithTooltipProps) {
10+
function TextWithTooltip({testID, text, shouldShowTooltip, style, numberOfLines = 1, forwardedFSClass}: TextWithTooltipProps) {
1111
const [showTooltip, setShowTooltip] = useState(false);
1212

1313
return (
@@ -16,6 +16,7 @@ function TextWithTooltip({text, shouldShowTooltip, style, numberOfLines = 1, for
1616
text={text}
1717
>
1818
<Text
19+
testID={testID}
1920
style={style}
2021
numberOfLines={numberOfLines}
2122
onLayout={(e) => {

src/components/TextWithTooltip/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ type TextWithTooltipProps = ForwardedFSClassProps & {
1313

1414
/** Custom number of lines for text wrapping */
1515
numberOfLines?: number;
16+
17+
/** TestID of the Text component */
18+
testID?: string;
1619
};
1720

1821
export default TextWithTooltipProps;

src/components/TransactionItemRow/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,10 @@ function TransactionItemRow({
395395

396396
if (shouldUseNarrowLayout) {
397397
return (
398-
<View style={[styles.expenseWidgetRadius, styles.justifyContentEvenly, bgActiveStyles, style, styles.overflowHidden]}>
398+
<View
399+
style={[styles.expenseWidgetRadius, styles.justifyContentEvenly, bgActiveStyles, style, styles.overflowHidden]}
400+
testID="transaction-item-row"
401+
>
399402
<View style={[styles.flexRow]}>
400403
{shouldShowCheckbox && (
401404
<Checkbox
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import * as NativeNavigation from '@react-navigation/native';
2+
import {fireEvent, render, screen} from '@testing-library/react-native';
3+
import React, {act} from 'react';
4+
import Onyx from 'react-native-onyx';
5+
import ComposeProviders from '@components/ComposeProviders';
6+
import {LocaleContextProvider} from '@components/LocaleContextProvider';
7+
import OnyxListItemProvider from '@components/OnyxListItemProvider';
8+
import ScreenWrapper from '@components/ScreenWrapper';
9+
import {SearchContextProvider} from '@components/Search/SearchContext';
10+
import TransactionGroupListItem from '@src/components/SelectionListWithSections/Search/TransactionGroupListItem';
11+
import type {TransactionGroupListItemProps, TransactionListItemType, TransactionReportGroupListItemType} from '@src/components/SelectionListWithSections/types';
12+
import CONST from '@src/CONST';
13+
import ONYXKEYS from '@src/ONYXKEYS';
14+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
15+
16+
jest.mock('@libs/actions/Search', () => ({
17+
search: jest.fn(),
18+
}));
19+
20+
jest.mock('@libs/SearchUIUtils', () => ({
21+
getSections: jest.fn(() => []),
22+
isCorrectSearchUserName: jest.fn(() => true),
23+
}));
24+
25+
const mockTransaction: TransactionListItemType = {
26+
accountID: 1,
27+
amount: 0,
28+
canDelete: true,
29+
canHold: true,
30+
canUnhold: false,
31+
category: '',
32+
convertedAmount: 1284,
33+
convertedCurrency: 'USD',
34+
created: '2025-09-19',
35+
currency: 'USD',
36+
managerID: 1,
37+
merchant: '(none)',
38+
modifiedAmount: -1284,
39+
modifiedCreated: '2025-09-07',
40+
modifiedCurrency: 'USD',
41+
modifiedMerchant: 'The Home Depot',
42+
policyID: '06F34677820A4D07',
43+
reportID: '515146912679679',
44+
reportType: 'expense',
45+
tag: '',
46+
transactionID: '1',
47+
transactionThreadReportID: '2925191332104975',
48+
transactionType: 'cash',
49+
action: 'approve',
50+
allActions: ['approve'],
51+
formattedFrom: 'Main Applause QA',
52+
formattedTo: 'Main Applause QA',
53+
formattedTotal: -1284,
54+
formattedMerchant: 'The Home Depot',
55+
date: '2025-09-07',
56+
shouldShowMerchant: true,
57+
shouldShowYear: true,
58+
keyForList: '1',
59+
isAmountColumnWide: false,
60+
isTaxAmountColumnWide: false,
61+
shouldAnimateInHighlight: false,
62+
report: {
63+
reportID: '515146912679679',
64+
},
65+
from: {
66+
accountID: 1,
67+
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_15.png',
68+
displayName: 'Main Applause QA',
69+
},
70+
to: {
71+
accountID: 1,
72+
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_15.png',
73+
displayName: 'Main Applause QA',
74+
},
75+
};
76+
77+
const mockReport: TransactionReportGroupListItemType = {
78+
accountID: 1,
79+
chatReportID: '4735435600700077',
80+
chatType: undefined,
81+
created: '2025-09-19 20:00:47',
82+
currency: 'USD',
83+
isOneTransactionReport: true,
84+
isOwnPolicyExpenseChat: false,
85+
isPolicyExpenseChat: false,
86+
isWaitingOnBankAccount: false,
87+
managerID: 1,
88+
nonReimbursableTotal: 0,
89+
oldPolicyName: '',
90+
ownerAccountID: 1,
91+
parentReportActionID: '2454187434077044186',
92+
parentReportID: '4735435600700077',
93+
policyID: '06F34677820A4D07',
94+
private_isArchived: '',
95+
reportID: '515146912679679',
96+
reportName: 'Expense Report #515146912679679',
97+
stateNum: 1,
98+
statusNum: 1,
99+
total: -1284,
100+
type: 'expense',
101+
unheldTotal: -1284,
102+
from: {
103+
accountID: 1,
104+
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_15.png',
105+
displayName: 'Main Applause QA',
106+
},
107+
to: {
108+
accountID: 1,
109+
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_15.png',
110+
displayName: 'Main Applause QA',
111+
},
112+
transactions: [],
113+
groupedBy: 'reports',
114+
keyForList: '515146912679679',
115+
};
116+
117+
const createFakeTransactions = (numberOfTransactions: number): TransactionListItemType[] => {
118+
return Array.from({length: numberOfTransactions}, (_, index) => ({
119+
...mockTransaction,
120+
transactionID: index.toString(),
121+
}));
122+
};
123+
124+
const createFakeReport = (numberOfTransactions: number): TransactionReportGroupListItemType => {
125+
return {
126+
...mockReport,
127+
transactions: createFakeTransactions(numberOfTransactions),
128+
};
129+
};
130+
131+
describe('TransactionGroupListItem', () => {
132+
beforeAll(() => {
133+
Onyx.init({
134+
keys: ONYXKEYS,
135+
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
136+
});
137+
jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''});
138+
});
139+
140+
beforeEach(() => {
141+
jest.clearAllMocks();
142+
return act(async () => {
143+
await Onyx.clear();
144+
await waitForBatchedUpdatesWithAct();
145+
});
146+
});
147+
148+
const mockOnSelectRow = jest.fn();
149+
const numberOfTransactions = 21;
150+
const report = createFakeReport(numberOfTransactions);
151+
152+
const defaultProps: TransactionGroupListItemProps<TransactionReportGroupListItemType> = {
153+
item: report,
154+
showTooltip: false,
155+
onSelectRow: mockOnSelectRow,
156+
groupBy: CONST.SEARCH.GROUP_BY.REPORTS,
157+
canSelectMultiple: true,
158+
};
159+
160+
function TestWrapper({children}: {children: React.ReactNode}) {
161+
return (
162+
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
163+
<ScreenWrapper testID="test">
164+
<SearchContextProvider>{children}</SearchContextProvider>
165+
</ScreenWrapper>
166+
</ComposeProviders>
167+
);
168+
}
169+
170+
const renderTransactionGroupListItem = () => {
171+
return render(
172+
<TransactionGroupListItem
173+
// eslint-disable-next-line react/jsx-props-no-spreading
174+
{...defaultProps}
175+
/>,
176+
{wrapper: TestWrapper},
177+
);
178+
};
179+
180+
const expand = async () => {
181+
const expandButton = screen.getByLabelText('Expand');
182+
fireEvent.press(expandButton);
183+
await waitForBatchedUpdatesWithAct();
184+
};
185+
186+
const collapse = async () => {
187+
const collapseButton = screen.getByLabelText('Collapse');
188+
fireEvent.press(collapseButton);
189+
await waitForBatchedUpdatesWithAct();
190+
};
191+
192+
const showMore = async () => {
193+
const showMoreButton = screen.getByText('Show more');
194+
fireEvent.press(showMoreButton);
195+
await waitForBatchedUpdatesWithAct();
196+
};
197+
198+
const getVisibleTransactionRowsCount = () => screen.getAllByTestId('transaction-item-row').length;
199+
200+
it('should render TransactionGroupListItem with groupBy reports', async () => {
201+
renderTransactionGroupListItem();
202+
await waitForBatchedUpdatesWithAct();
203+
204+
expect(screen.getByRole(CONST.ROLE.CHECKBOX)).toBeTruthy();
205+
expect(screen.getByRole(CONST.ROLE.CHECKBOX)).not.toBeChecked();
206+
expect(screen.getByTestId('ReportSearchHeader')).toBeTruthy();
207+
expect(screen.getByTestId('TotalCell')).toBeTruthy();
208+
expect(screen.getByTestId('ActionCell')).toBeTruthy();
209+
expect(screen.getByLabelText('Expand')).toBeTruthy();
210+
expect(screen.queryByTestId(CONST.ANIMATED_COLLAPSIBLE_CONTENT_TEST_ID)).toBeNull();
211+
});
212+
213+
it(`should toggle expansion state with ${CONST.TRANSACTION.RESULTS_PAGE_SIZE} items when Expand is triggered`, async () => {
214+
renderTransactionGroupListItem();
215+
await waitForBatchedUpdatesWithAct();
216+
await expand();
217+
218+
expect(screen.getByLabelText('Collapse')).toBeTruthy();
219+
expect(screen.getByTestId(CONST.ANIMATED_COLLAPSIBLE_CONTENT_TEST_ID)).toBeTruthy();
220+
221+
expect(getVisibleTransactionRowsCount()).toBe(CONST.TRANSACTION.RESULTS_PAGE_SIZE);
222+
});
223+
224+
it('should show more transactions and hide button when show more button is triggered and limit of transactions is reached', async () => {
225+
renderTransactionGroupListItem();
226+
await waitForBatchedUpdatesWithAct();
227+
await expand();
228+
await showMore();
229+
230+
expect(getVisibleTransactionRowsCount()).toBe(numberOfTransactions);
231+
232+
const showMoreButtonSecond = screen.queryByText('Show more');
233+
expect(showMoreButtonSecond).toBeNull();
234+
});
235+
236+
it('should collapse the list when Collapse is triggered', async () => {
237+
renderTransactionGroupListItem();
238+
await waitForBatchedUpdatesWithAct();
239+
await expand();
240+
await collapse();
241+
242+
expect(screen.getByLabelText('Expand')).toBeTruthy();
243+
});
244+
245+
it(`should show only ${CONST.TRANSACTION.RESULTS_PAGE_SIZE} transactions when collapsed and expanded again`, async () => {
246+
renderTransactionGroupListItem();
247+
await waitForBatchedUpdatesWithAct();
248+
await expand();
249+
await showMore();
250+
await collapse();
251+
await expand();
252+
253+
expect(getVisibleTransactionRowsCount()).toBe(CONST.TRANSACTION.RESULTS_PAGE_SIZE);
254+
expect(screen.getByText('Show more')).toBeTruthy();
255+
});
256+
});

0 commit comments

Comments
 (0)