Skip to content

Commit 5a744e3

Browse files
test: add unit tests for FilterBar and MultichainTransactionDetailsSheet
Adds coverage for two components that were missing test files, bringing new-code coverage above the SonarCloud 80% threshold. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 02a4a2a commit 5a744e3

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React from 'react';
2+
import { fireEvent } from '@testing-library/react-native';
3+
import { renderScreen } from '../../../util/test/renderWithProvider';
4+
import { backgroundState } from '../../../util/test/initial-root-state';
5+
import MultichainTransactionDetailsSheet from './MultichainTransactionDetailsSheet';
6+
import type { MultichainTransactionDisplayData } from '../../hooks/useMultichainTransactionDisplay';
7+
import type { Transaction } from '@metamask/keyring-api';
8+
9+
const mockNavigate = jest.fn();
10+
const mockOnCloseBottomSheet = jest.fn();
11+
const mockUseRoute = jest.fn();
12+
13+
jest.mock('@react-navigation/native', () => ({
14+
...jest.requireActual('@react-navigation/native'),
15+
useNavigation: () => ({ navigate: mockNavigate }),
16+
useRoute: () => mockUseRoute(),
17+
}));
18+
19+
jest.mock(
20+
'../../../component-library/components/BottomSheets/BottomSheet',
21+
() => {
22+
const { forwardRef, useImperativeHandle } = jest.requireActual('react');
23+
const { View } = jest.requireActual('react-native');
24+
return {
25+
__esModule: true,
26+
default: forwardRef(
27+
(
28+
{ children }: { children: React.ReactNode },
29+
ref: React.Ref<unknown>,
30+
) => {
31+
useImperativeHandle(ref, () => ({
32+
onCloseBottomSheet: mockOnCloseBottomSheet,
33+
}));
34+
return <View>{children}</View>;
35+
},
36+
),
37+
};
38+
},
39+
);
40+
41+
jest.mock(
42+
'../../../component-library/components/BottomSheets/BottomSheetHeader',
43+
() => {
44+
const { View, TouchableOpacity, Text } = jest.requireActual('react-native');
45+
return ({
46+
children,
47+
onClose,
48+
}: {
49+
children: React.ReactNode;
50+
onClose: () => void;
51+
}) => (
52+
<View>
53+
<TouchableOpacity testID="bottomsheet-close" onPress={onClose} />
54+
{children}
55+
</View>
56+
);
57+
},
58+
);
59+
60+
jest.mock('../../../core/Multichain/utils', () => ({
61+
getTransactionUrl: jest.fn(
62+
(id: string, chain: string) =>
63+
`https://explorer.example.com/tx/${id}?chain=${chain}`,
64+
),
65+
getAddressUrl: jest.fn(
66+
(address: string, chain: string) =>
67+
`https://explorer.example.com/address/${address}?chain=${chain}`,
68+
),
69+
}));
70+
71+
const mockDisplayData: MultichainTransactionDisplayData = {
72+
title: 'Send ETH',
73+
isRedeposit: false,
74+
from: {
75+
address: '0xabc123def456abc123def456abc123def456abc1',
76+
amount: '0.5',
77+
unit: 'ETH',
78+
},
79+
to: {
80+
address: '0xdef456abc123def456abc123def456abc123def4',
81+
amount: '0.5',
82+
unit: 'ETH',
83+
},
84+
baseFee: { amount: '0.001', unit: 'ETH' },
85+
priorityFee: { amount: '0.0001', unit: 'ETH' },
86+
};
87+
88+
const mockTransaction = {
89+
id: 'tx-hash-123',
90+
timestamp: 1700000000,
91+
chain: 'eip155:1',
92+
status: 'confirmed',
93+
account: '0xabc123def456abc123def456abc123def456abc1',
94+
type: 'send',
95+
} as unknown as Transaction;
96+
97+
const renderSheet = () =>
98+
renderScreen(
99+
MultichainTransactionDetailsSheet,
100+
{ name: 'MultichainTransactionDetailsSheet' },
101+
{ state: { engine: { backgroundState } } },
102+
);
103+
104+
const defaultRouteParams = {
105+
params: { displayData: mockDisplayData, transaction: mockTransaction },
106+
};
107+
108+
describe('MultichainTransactionDetailsSheet', () => {
109+
beforeEach(() => {
110+
jest.clearAllMocks();
111+
mockUseRoute.mockReturnValue(defaultRouteParams);
112+
});
113+
114+
it('renders the transaction title', () => {
115+
const { getByText } = renderSheet();
116+
expect(getByText('Send ETH')).toBeTruthy();
117+
});
118+
119+
it('renders the transaction status row', () => {
120+
const { getByTestId } = renderSheet();
121+
expect(
122+
getByTestId(`transaction-status-${mockTransaction.id}`),
123+
).toBeTruthy();
124+
});
125+
126+
it('renders the from address row', () => {
127+
const { getByText } = renderSheet();
128+
// formatAddress shortens it — just check the label exists
129+
expect(getByText('From')).toBeTruthy();
130+
});
131+
132+
it('renders the to address row', () => {
133+
const { getByText } = renderSheet();
134+
expect(getByText('To')).toBeTruthy();
135+
});
136+
137+
it('renders the amount row', () => {
138+
const { getByText } = renderSheet();
139+
expect(getByText('Amount')).toBeTruthy();
140+
expect(getByText('0.5 ETH')).toBeTruthy();
141+
});
142+
143+
it('renders the network fee row', () => {
144+
const { getByText } = renderSheet();
145+
expect(getByText('Network fee')).toBeTruthy();
146+
expect(getByText('0.001 ETH')).toBeTruthy();
147+
});
148+
149+
it('renders the priority fee row', () => {
150+
const { getByText } = renderSheet();
151+
expect(getByText('0.0001 ETH')).toBeTruthy();
152+
});
153+
154+
it('closes the sheet when close button is pressed', () => {
155+
const { getByTestId } = renderSheet();
156+
fireEvent.press(getByTestId('bottomsheet-close'));
157+
expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1);
158+
});
159+
160+
it('renders without from/to/fees when they are absent', () => {
161+
mockUseRoute.mockReturnValue({
162+
params: {
163+
displayData: { title: 'Receive' },
164+
transaction: mockTransaction,
165+
},
166+
});
167+
const { getByText, queryByText } = renderSheet();
168+
expect(getByText('Receive')).toBeTruthy();
169+
expect(queryByText('From')).toBeNull();
170+
expect(queryByText('To')).toBeNull();
171+
expect(queryByText('Network fee')).toBeNull();
172+
});
173+
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import React from 'react';
2+
import { fireEvent } from '@testing-library/react-native';
3+
import renderWithProvider from '../../../../../util/test/renderWithProvider';
4+
import FilterBar, { FilterButton } from './FilterBar';
5+
6+
describe('FilterButton', () => {
7+
const defaultProps = {
8+
testID: 'filter-btn',
9+
label: 'Test Label',
10+
onPress: jest.fn(),
11+
};
12+
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
});
16+
17+
it('renders label text', () => {
18+
const { getByText } = renderWithProvider(
19+
<FilterButton {...defaultProps} />,
20+
);
21+
expect(getByText('Test Label')).toBeTruthy();
22+
});
23+
24+
it('calls onPress when pressed', () => {
25+
const onPress = jest.fn();
26+
const { getByTestId } = renderWithProvider(
27+
<FilterButton {...defaultProps} onPress={onPress} />,
28+
);
29+
fireEvent.press(getByTestId('filter-btn'));
30+
expect(onPress).toHaveBeenCalledTimes(1);
31+
});
32+
33+
it('is disabled when disabled prop is true', () => {
34+
const onPress = jest.fn();
35+
const { getByTestId } = renderWithProvider(
36+
<FilterButton {...defaultProps} onPress={onPress} disabled />,
37+
);
38+
const btn = getByTestId('filter-btn');
39+
expect(btn.props.accessibilityState?.disabled).toBe(true);
40+
fireEvent.press(btn);
41+
expect(onPress).not.toHaveBeenCalled();
42+
});
43+
44+
it('is enabled by default', () => {
45+
const { getByTestId } = renderWithProvider(
46+
<FilterButton {...defaultProps} />,
47+
);
48+
expect(
49+
getByTestId('filter-btn').props.accessibilityState?.disabled,
50+
).toBeFalsy();
51+
});
52+
53+
it('passes numberOfLines and ellipsizeMode to Text', () => {
54+
const { getByText } = renderWithProvider(
55+
<FilterButton {...defaultProps} numberOfLines={1} ellipsizeMode="tail" />,
56+
);
57+
const text = getByText('Test Label');
58+
expect(text.props.numberOfLines).toBe(1);
59+
expect(text.props.ellipsizeMode).toBe('tail');
60+
});
61+
62+
it('uses wide padding by default', () => {
63+
const { getByTestId } = renderWithProvider(
64+
<FilterButton {...defaultProps} />,
65+
);
66+
// wide=true is the default — component renders without error
67+
expect(getByTestId('filter-btn')).toBeTruthy();
68+
});
69+
70+
it('renders with wide=false (compact padding)', () => {
71+
const { getByTestId } = renderWithProvider(
72+
<FilterButton {...defaultProps} wide={false} />,
73+
);
74+
expect(getByTestId('filter-btn')).toBeTruthy();
75+
});
76+
});
77+
78+
describe('FilterBar', () => {
79+
const defaultProps = {
80+
priceChangeButtonText: '24h %',
81+
onPriceChangePress: jest.fn(),
82+
networkName: 'All Networks',
83+
onNetworkPress: jest.fn(),
84+
};
85+
86+
beforeEach(() => {
87+
jest.clearAllMocks();
88+
});
89+
90+
it('renders price-change button with correct label', () => {
91+
const { getByText } = renderWithProvider(<FilterBar {...defaultProps} />);
92+
expect(getByText('24h %')).toBeTruthy();
93+
});
94+
95+
it('renders network button with correct label', () => {
96+
const { getByText } = renderWithProvider(<FilterBar {...defaultProps} />);
97+
expect(getByText('All Networks')).toBeTruthy();
98+
});
99+
100+
it('calls onPriceChangePress when price-change button is pressed', () => {
101+
const onPriceChangePress = jest.fn();
102+
const { getByTestId } = renderWithProvider(
103+
<FilterBar {...defaultProps} onPriceChangePress={onPriceChangePress} />,
104+
);
105+
fireEvent.press(getByTestId('price-change-button'));
106+
expect(onPriceChangePress).toHaveBeenCalledTimes(1);
107+
});
108+
109+
it('calls onNetworkPress when network button is pressed', () => {
110+
const onNetworkPress = jest.fn();
111+
const { getByTestId } = renderWithProvider(
112+
<FilterBar {...defaultProps} onNetworkPress={onNetworkPress} />,
113+
);
114+
fireEvent.press(getByTestId('all-networks-button'));
115+
expect(onNetworkPress).toHaveBeenCalledTimes(1);
116+
});
117+
118+
it('disables price-change button when isPriceChangeDisabled is true', () => {
119+
const onPriceChangePress = jest.fn();
120+
const { getByTestId } = renderWithProvider(
121+
<FilterBar
122+
{...defaultProps}
123+
onPriceChangePress={onPriceChangePress}
124+
isPriceChangeDisabled
125+
/>,
126+
);
127+
const btn = getByTestId('price-change-button');
128+
expect(btn.props.accessibilityState?.disabled).toBe(true);
129+
fireEvent.press(btn);
130+
expect(onPriceChangePress).not.toHaveBeenCalled();
131+
});
132+
133+
it('price-change button is enabled by default', () => {
134+
const { getByTestId } = renderWithProvider(<FilterBar {...defaultProps} />);
135+
expect(
136+
getByTestId('price-change-button').props.accessibilityState?.disabled,
137+
).toBeFalsy();
138+
});
139+
140+
it('renders extra filters when provided', () => {
141+
const { getByTestId } = renderWithProvider(
142+
<FilterBar
143+
{...defaultProps}
144+
extraFilters={
145+
<FilterButton
146+
testID="extra-filter"
147+
label="Extra"
148+
onPress={jest.fn()}
149+
/>
150+
}
151+
/>,
152+
);
153+
expect(getByTestId('extra-filter')).toBeTruthy();
154+
});
155+
156+
it('renders without extra filters', () => {
157+
const { queryByTestId } = renderWithProvider(
158+
<FilterBar {...defaultProps} />,
159+
);
160+
expect(queryByTestId('extra-filter')).toBeNull();
161+
});
162+
});

0 commit comments

Comments
 (0)