Skip to content

Commit f93f34d

Browse files
authored
Merge branch 'main' into feat/ramps-ub2-immediate-order-details-after-webview-callback
2 parents e8f218a + fbf8dcd commit f93f34d

6 files changed

Lines changed: 416 additions & 267 deletions

File tree

app/components/UI/Rewards/Views/MusdCalculatorView.test.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,6 @@ describe('MusdCalculatorView', () => {
6666
expect(getByTestId('musd-calculator-tab')).toBeOnTheScreen();
6767
});
6868

69-
it('wraps the calculator in a keyboard avoiding view', () => {
70-
const { getByTestId } = render(<MusdCalculatorView />);
71-
expect(
72-
getByTestId('musd-calculator-keyboard-avoiding-view'),
73-
).toBeOnTheScreen();
74-
});
75-
7669
it('tracks REWARDS_PAGE_VIEWED on mount with page_type musd_calculator', () => {
7770
render(<MusdCalculatorView />);
7871

app/components/UI/Rewards/Views/MusdCalculatorView.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import { HeaderStandard } from '@metamask/design-system-react-native';
33
import { useNavigation } from '@react-navigation/native';
4-
import { KeyboardAvoidingView, Platform } from 'react-native';
54
import { SafeAreaView } from 'react-native-safe-area-context';
65
import { useTailwind } from '@metamask/design-system-twrnc-preset';
76
import ErrorBoundary from '../../../Views/ErrorBoundary';
@@ -19,21 +18,14 @@ const MusdCalculatorView: React.FC = () => {
1918
<ErrorBoundary navigation={navigation} view="MusdCalculatorView">
2019
<SafeAreaView
2120
edges={{ top: 'additive' }}
22-
style={tw.style('flex-1 bg-default')}
21+
style={tw.style('flex-1 bg-default pb-4')}
2322
>
2423
<HeaderStandard
2524
title={strings('rewards.musd.page_title')}
2625
onBack={() => navigation.goBack()}
2726
backButtonProps={{ testID: 'header-back-button' }}
2827
/>
29-
<KeyboardAvoidingView
30-
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
31-
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
32-
style={tw.style('flex-1')}
33-
testID="musd-calculator-keyboard-avoiding-view"
34-
>
35-
<MusdCalculatorTab />
36-
</KeyboardAvoidingView>
28+
<MusdCalculatorTab />
3729
</SafeAreaView>
3830
</ErrorBoundary>
3931
);

app/components/UI/Rewards/components/Tabs/MusdCalculatorTab/MusdCalculatorTab.test.tsx

Lines changed: 125 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,27 @@ import { amountToPercent } from '../../../utils/musdCalculatorSlider';
99
const mockPanGestureHandlers: {
1010
onBegin?: (event: { x: number }) => void;
1111
onUpdate?: (event: { x: number }) => void;
12+
onEnd?: (event: { x: number }) => void;
1213
onFinalize?: (event: { x: number }) => void;
1314
} = {};
15+
const mockTapGestureHandlers: {
16+
onEnd?: (event: { x: number }) => void;
17+
} = {};
1418

1519
jest.mock('react-native-gesture-handler', () => ({
1620
GestureHandlerRootView: jest.requireActual('react-native').View,
1721
GestureDetector: ({ children }: { children: React.ReactNode }) => children,
1822
Gesture: {
23+
Simultaneous: jest.fn((...gestures: unknown[]) => gestures),
24+
Tap: jest.fn(() => ({
25+
onEnd: jest.fn(function (
26+
this: unknown,
27+
handler: (event: { x: number }) => void,
28+
) {
29+
mockTapGestureHandlers.onEnd = handler;
30+
return this;
31+
}),
32+
})),
1933
Pan: jest.fn(() => ({
2034
minDistance: jest.fn().mockReturnThis(),
2135
onBegin: jest.fn(function (
@@ -32,6 +46,13 @@ jest.mock('react-native-gesture-handler', () => ({
3246
mockPanGestureHandlers.onUpdate = handler;
3347
return this;
3448
}),
49+
onEnd: jest.fn(function (
50+
this: unknown,
51+
handler: (event: { x: number }) => void,
52+
) {
53+
mockPanGestureHandlers.onEnd = handler;
54+
return this;
55+
}),
3556
onFinalize: jest.fn(function (
3657
this: unknown,
3758
handler: (event: { x: number }) => void,
@@ -60,12 +81,18 @@ jest.mock('../../../../../../../locales/i18n', () => ({
6081
strings: jest.fn((key: string) => key),
6182
}));
6283

63-
jest.mock('../../../../SimulationDetails/FiatDisplay/useFiatFormatter', () =>
64-
jest.fn(
65-
() => (value: { toNumber: () => number }) =>
66-
`$${value.toNumber().toLocaleString('en-US')}`,
67-
),
68-
);
84+
jest.mock('react-native-keyboard-aware-scroll-view', () => {
85+
const ReactActual = jest.requireActual('react');
86+
const { ScrollView } = jest.requireActual('react-native');
87+
return {
88+
KeyboardAwareScrollView: ({
89+
children,
90+
...props
91+
}: {
92+
children?: React.ReactNode;
93+
}) => ReactActual.createElement(ScrollView, props, children),
94+
};
95+
});
6996

7097
jest.mock('../../../../../../core/DeeplinkManager', () => ({
7198
handleDeeplink: jest.fn(),
@@ -90,7 +117,9 @@ describe('MusdCalculatorTab', () => {
90117
jest.clearAllMocks();
91118
mockPanGestureHandlers.onBegin = undefined;
92119
mockPanGestureHandlers.onUpdate = undefined;
120+
mockPanGestureHandlers.onEnd = undefined;
93121
mockPanGestureHandlers.onFinalize = undefined;
122+
mockTapGestureHandlers.onEnd = undefined;
94123
jest.mocked(useAnalytics).mockReturnValue(
95124
createMockUseAnalyticsHook({
96125
trackEvent: mockTrackEvent,
@@ -114,6 +143,20 @@ describe('MusdCalculatorTab', () => {
114143
expect(getByTestId('musd-slider-track')).toBeOnTheScreen();
115144
});
116145

146+
it('uses a keyboard-aware scroll container for the amount input', () => {
147+
const { getByTestId } = render(<MusdCalculatorTab />);
148+
const scrollView = getByTestId(
149+
'musd-calculator-keyboard-aware-scroll-view',
150+
);
151+
152+
expect(scrollView).toHaveProp('keyboardShouldPersistTaps', 'handled');
153+
expect(scrollView).toHaveProp('keyboardDismissMode', 'none');
154+
expect(scrollView).toHaveProp('enableOnAndroid', true);
155+
expect(scrollView).toHaveProp('enableAutomaticScroll', true);
156+
expect(scrollView).toHaveProp('enableResetScrollToCoords', false);
157+
expect(scrollView).toHaveProp('extraScrollHeight', 20);
158+
});
159+
117160
it('calls handleDeeplink and tracks buy_musd event when Buy button is pressed', () => {
118161
const { handleDeeplink } = jest.requireMock(
119162
'../../../../../../core/DeeplinkManager',
@@ -159,14 +202,14 @@ describe('MusdCalculatorTab', () => {
159202
});
160203

161204
const locationX = (amountToPercent(5000) / 100) * 300;
162-
fireEvent(track, 'pressIn', {
163-
nativeEvent: { locationX },
205+
await act(async () => {
206+
mockTapGestureHandlers.onEnd?.({ x: locationX });
164207
});
165208

166209
await waitFor(() => {
167210
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
168211
'value',
169-
'$5,000',
212+
'5000',
170213
);
171214
expect(
172215
getByText(/rewards\.musd\.earnings_per_day_suffix/),
@@ -183,52 +226,42 @@ describe('MusdCalculatorTab', () => {
183226
const { getByTestId, getByText } = render(<MusdCalculatorTab />);
184227
const amountInput = getByTestId('musd-slider-amount-display');
185228

186-
fireEvent(amountInput, 'focus');
187229
fireEvent.changeText(amountInput, '5000');
188230

189231
await waitFor(() => {
190232
expect(amountInput).toHaveProp('value', '5000');
191233
expect(getByText(/\$150/)).toBeOnTheScreen();
192234
});
193-
194-
fireEvent(amountInput, 'endEditing');
195-
196-
await waitFor(() => {
197-
expect(amountInput).toHaveProp('value', '$5,000');
198-
});
199235
});
200236

201-
it('accepts amounts over the slider maximum', async () => {
237+
it('caps typed amounts at the slider maximum', async () => {
202238
const { getByTestId, getByText } = render(<MusdCalculatorTab />);
203239
const amountInput = getByTestId('musd-slider-amount-display');
204240

205-
fireEvent(amountInput, 'focus');
206241
fireEvent.changeText(amountInput, '12000');
207242

208243
await waitFor(() => {
209-
expect(amountInput).toHaveProp('value', '12000');
210-
expect(getByText(/\$360/)).toBeOnTheScreen();
244+
expect(amountInput).toHaveProp('value', '10000');
245+
expect(getByText(/\$300/)).toBeOnTheScreen();
211246
});
212247
});
213248

214-
it('normalizes decorated decimal input while editing', async () => {
249+
it('normalizes decorated decimal input to two decimal places while editing', async () => {
215250
const { getByTestId, getByText } = render(<MusdCalculatorTab />);
216251
const amountInput = getByTestId('musd-slider-amount-display');
217252

218-
fireEvent(amountInput, 'focus');
219253
fireEvent.changeText(amountInput, '$1,234.56.78');
220254

221255
await waitFor(() => {
222-
expect(amountInput).toHaveProp('value', '1234.5678');
223-
expect(getByText(/\$37\.037/)).toBeOnTheScreen();
256+
expect(amountInput).toHaveProp('value', '1234.56');
257+
expect(getByText(/\$37\.04/)).toBeOnTheScreen();
224258
});
225259
});
226260

227261
it('treats invalid numeric input as zero', async () => {
228262
const { getByTestId, getAllByText } = render(<MusdCalculatorTab />);
229263
const amountInput = getByTestId('musd-slider-amount-display');
230264

231-
fireEvent(amountInput, 'focus');
232265
fireEvent.changeText(amountInput, '.');
233266

234267
await waitFor(() => {
@@ -240,13 +273,13 @@ describe('MusdCalculatorTab', () => {
240273
it('ignores slider presses before the track is measured', () => {
241274
const { getByTestId } = render(<MusdCalculatorTab />);
242275

243-
fireEvent(getByTestId('musd-slider-track'), 'pressIn', {
244-
nativeEvent: { locationX: 200 },
276+
act(() => {
277+
mockTapGestureHandlers.onEnd?.({ x: 200 });
245278
});
246279

247280
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
248281
'value',
249-
'$1,000',
282+
'1000',
250283
);
251284
});
252285

@@ -261,17 +294,79 @@ describe('MusdCalculatorTab', () => {
261294
act(() => {
262295
mockPanGestureHandlers.onBegin?.({ x: 150 });
263296
mockPanGestureHandlers.onUpdate?.({ x: 300 });
297+
mockPanGestureHandlers.onEnd?.({ x: 300 });
264298
mockPanGestureHandlers.onFinalize?.({ x: 300 });
265299
});
266300

267301
await waitFor(() => {
268302
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
269303
'value',
270-
'$10,000',
304+
'10000',
271305
);
272306
});
273307
});
274308

309+
it('does not reset the slider when a press finalizes without a pan', async () => {
310+
const { getByTestId } = render(<MusdCalculatorTab />);
311+
const track = getByTestId('musd-slider-track');
312+
313+
fireEvent(track, 'layout', {
314+
nativeEvent: { layout: { width: 300, height: 32, x: 0, y: 0 } },
315+
});
316+
317+
const locationX = (amountToPercent(5000) / 100) * 300;
318+
await act(async () => {
319+
mockTapGestureHandlers.onEnd?.({ x: locationX });
320+
});
321+
322+
await waitFor(() => {
323+
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
324+
'value',
325+
'5000',
326+
);
327+
});
328+
329+
act(() => {
330+
mockPanGestureHandlers.onFinalize?.({ x: 0 });
331+
});
332+
333+
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
334+
'value',
335+
'5000',
336+
);
337+
});
338+
339+
it('does not reset when tapping the same slider position twice', async () => {
340+
const { getByTestId } = render(<MusdCalculatorTab />);
341+
const track = getByTestId('musd-slider-track');
342+
343+
fireEvent(track, 'layout', {
344+
nativeEvent: { layout: { width: 300, height: 32, x: 0, y: 0 } },
345+
});
346+
347+
const locationX = (amountToPercent(5000) / 100) * 300;
348+
349+
await act(async () => {
350+
mockTapGestureHandlers.onEnd?.({ x: locationX });
351+
});
352+
353+
await waitFor(() => {
354+
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
355+
'value',
356+
'5000',
357+
);
358+
});
359+
360+
await act(async () => {
361+
mockTapGestureHandlers.onEnd?.({ x: locationX });
362+
});
363+
364+
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
365+
'value',
366+
'5000',
367+
);
368+
});
369+
275370
it('ignores pan gesture handlers before the track is measured', () => {
276371
const { getByTestId } = render(<MusdCalculatorTab />);
277372

@@ -282,7 +377,7 @@ describe('MusdCalculatorTab', () => {
282377

283378
expect(getByTestId('musd-slider-amount-display')).toHaveProp(
284379
'value',
285-
'$1,000',
380+
'1000',
286381
);
287382
});
288383
});

0 commit comments

Comments
 (0)