Skip to content

Commit 973af65

Browse files
committed
test(MUSD-747): increase MoneyHomeView coverage to >80% on all metrics
Added tests for the peek-and-hide animation handlers introduced by the stepper visibility feature: handleScroll, handleScrollViewLayout, handleStepperLayout, handleFooterLayout, and updateStepperVisibility. Also added tests for three pre-existing navigation handlers (handleGetNowPress, handleEarnCryptoPress, handleLearnMorePress) that were below the 80% threshold. Final coverage: Statements 93.07%, Branches 96.87%, Functions 85.71%, Lines 95%.
1 parent b891b8b commit 973af65

1 file changed

Lines changed: 307 additions & 3 deletions

File tree

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx

Lines changed: 307 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
2-
import { fireEvent } from '@testing-library/react-native';
2+
import { act, fireEvent } from '@testing-library/react-native';
3+
import type { ReactTestInstance } from 'react-test-renderer';
34
import renderWithProvider from '../../../../../util/test/renderWithProvider';
45
import MoneyHomeView from './MoneyHomeView';
56
import { MoneyHomeViewTestIds } from './MoneyHomeView.testIds';
@@ -335,6 +336,47 @@ describe('MoneyHomeView', () => {
335336
});
336337
});
337338

339+
it('navigates to Card root when Get now row is pressed', () => {
340+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
341+
342+
fireEvent.press(getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW));
343+
344+
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ROOT);
345+
});
346+
347+
it('navigates to potential earnings screen when View potential earnings is pressed', () => {
348+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
349+
350+
fireEvent.press(
351+
getByTestId(
352+
MoneyPotentialEarningsTestIds.VIEW_POTENTIAL_EARNINGS_BUTTON,
353+
),
354+
);
355+
356+
expect(mockNavigate).toHaveBeenCalledWith(Routes.MONEY.POTENTIAL_EARNINGS);
357+
});
358+
359+
it('opens the MUSD learn more URL when learn more is pressed in empty state', () => {
360+
const mockOpenURL = jest
361+
.spyOn(require('react-native').Linking, 'openURL')
362+
.mockResolvedValue(undefined);
363+
364+
mockUseMoneyAccountTransactions.mockReturnValue({
365+
allTransactions: [],
366+
deposits: [],
367+
transfers: [],
368+
submittedTransactions: [],
369+
moneyAddress: '0x0000000000000000000000000000000000000001',
370+
});
371+
372+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
373+
374+
fireEvent.press(getByTestId(MoneyWhatYouGetTestIds.LEARN_MORE_BUTTON));
375+
376+
expect(mockOpenURL).toHaveBeenCalledTimes(1);
377+
mockOpenURL.mockRestore();
378+
});
379+
338380
describe('projected earnings', () => {
339381
it('passes the formatted projected earnings to MoneyEarnings', () => {
340382
mockMoneyFormatFiat.mockReturnValue('$0.12');
@@ -597,8 +639,270 @@ describe('MoneyHomeView', () => {
597639
it('mounts the footer in its hidden initial position', () => {
598640
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
599641
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
600-
// Animation behavior is verified by computeStepperVisibility's unit
601-
// tests and the Detox/E2E pass; we don't simulate scroll/layout here.
642+
});
643+
644+
it('handleScrollViewLayout updates scroll view height and calls updateStepperVisibility', () => {
645+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
646+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
647+
648+
act(() => {
649+
fireEvent(scrollView, 'layout', {
650+
nativeEvent: { layout: { height: 700, width: 390, x: 0, y: 0 } },
651+
});
652+
});
653+
654+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
655+
});
656+
657+
it('handleScrollViewLayout is a no-op when height is unchanged', () => {
658+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
659+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
660+
661+
act(() => {
662+
fireEvent(scrollView, 'layout', {
663+
nativeEvent: { layout: { height: 600, width: 390, x: 0, y: 0 } },
664+
});
665+
});
666+
667+
act(() => {
668+
fireEvent(scrollView, 'layout', {
669+
nativeEvent: { layout: { height: 600, width: 390, x: 0, y: 0 } },
670+
});
671+
});
672+
673+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
674+
});
675+
676+
it('handleScroll records the current scroll offset and calls updateStepperVisibility', () => {
677+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
678+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
679+
680+
act(() => {
681+
fireEvent.scroll(scrollView, {
682+
nativeEvent: {
683+
contentOffset: { y: 300, x: 0 },
684+
contentSize: { height: 1200, width: 390 },
685+
layoutMeasurement: { height: 700, width: 390 },
686+
},
687+
});
688+
});
689+
690+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
691+
});
692+
693+
it('handleStepperLayout stores new layout and triggers visibility update', () => {
694+
const { UNSAFE_getAllByType, getByTestId } = renderWithProvider(
695+
<MoneyHomeView />,
696+
);
697+
698+
const Box = jest.requireActual(
699+
'@metamask/design-system-react-native',
700+
).Box;
701+
const stepperBox = UNSAFE_getAllByType(Box).find(
702+
(b: { props: { onLayout?: unknown } }) => b.props.onLayout,
703+
);
704+
705+
act(() => {
706+
stepperBox?.props.onLayout({
707+
nativeEvent: { layout: { y: 200, height: 120, x: 0, width: 390 } },
708+
});
709+
});
710+
711+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
712+
});
713+
714+
it('handleStepperLayout is a no-op when layout dimensions are unchanged', () => {
715+
const { UNSAFE_getAllByType, getByTestId } = renderWithProvider(
716+
<MoneyHomeView />,
717+
);
718+
719+
const Box = jest.requireActual(
720+
'@metamask/design-system-react-native',
721+
).Box;
722+
const stepperBox = UNSAFE_getAllByType(Box).find(
723+
(b: { props: { onLayout?: unknown } }) => b.props.onLayout,
724+
);
725+
726+
act(() => {
727+
stepperBox?.props.onLayout({
728+
nativeEvent: { layout: { y: 200, height: 120, x: 0, width: 390 } },
729+
});
730+
});
731+
732+
act(() => {
733+
stepperBox?.props.onLayout({
734+
nativeEvent: { layout: { y: 200, height: 120, x: 0, width: 390 } },
735+
});
736+
});
737+
738+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
739+
});
740+
741+
it('footer peek-in: scrolling past stepper bottom triggers animateFooter(true)', () => {
742+
const { UNSAFE_getAllByType, getByTestId } = renderWithProvider(
743+
<MoneyHomeView />,
744+
);
745+
746+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
747+
748+
act(() => {
749+
fireEvent(scrollView, 'layout', {
750+
nativeEvent: { layout: { height: 700, width: 390, x: 0, y: 0 } },
751+
});
752+
});
753+
754+
const Box = jest.requireActual(
755+
'@metamask/design-system-react-native',
756+
).Box;
757+
const stepperBox = UNSAFE_getAllByType(Box).find(
758+
(b: { props: { onLayout?: unknown } }) => b.props.onLayout,
759+
);
760+
761+
act(() => {
762+
stepperBox?.props.onLayout({
763+
nativeEvent: { layout: { y: 100, height: 200, x: 0, width: 390 } },
764+
});
765+
});
766+
767+
act(() => {
768+
fireEvent.scroll(scrollView, {
769+
nativeEvent: {
770+
contentOffset: { y: 500, x: 0 },
771+
contentSize: { height: 2000, width: 390 },
772+
layoutMeasurement: { height: 700, width: 390 },
773+
},
774+
});
775+
});
776+
777+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
778+
});
779+
780+
it('footer hide: scrolling back above stepper bottom triggers animateFooter(false)', () => {
781+
const { UNSAFE_getAllByType, getByTestId } = renderWithProvider(
782+
<MoneyHomeView />,
783+
);
784+
785+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
786+
787+
act(() => {
788+
fireEvent(scrollView, 'layout', {
789+
nativeEvent: { layout: { height: 700, width: 390, x: 0, y: 0 } },
790+
});
791+
});
792+
793+
const Box = jest.requireActual(
794+
'@metamask/design-system-react-native',
795+
).Box;
796+
const stepperBox = UNSAFE_getAllByType(Box).find(
797+
(b: { props: { onLayout?: unknown } }) => b.props.onLayout,
798+
);
799+
800+
act(() => {
801+
stepperBox?.props.onLayout({
802+
nativeEvent: { layout: { y: 100, height: 200, x: 0, width: 390 } },
803+
});
804+
});
805+
806+
act(() => {
807+
fireEvent.scroll(scrollView, {
808+
nativeEvent: {
809+
contentOffset: { y: 500, x: 0 },
810+
contentSize: { height: 2000, width: 390 },
811+
layoutMeasurement: { height: 700, width: 390 },
812+
},
813+
});
814+
});
815+
816+
act(() => {
817+
fireEvent.scroll(scrollView, {
818+
nativeEvent: {
819+
contentOffset: { y: 50, x: 0 },
820+
contentSize: { height: 2000, width: 390 },
821+
layoutMeasurement: { height: 700, width: 390 },
822+
},
823+
});
824+
});
825+
826+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
827+
});
828+
829+
it('updateStepperVisibility does not animate when visibility is unchanged', () => {
830+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
831+
const scrollView = getByTestId(MoneyHomeViewTestIds.SCROLL_VIEW);
832+
833+
act(() => {
834+
fireEvent.scroll(scrollView, {
835+
nativeEvent: {
836+
contentOffset: { y: 0, x: 0 },
837+
contentSize: { height: 2000, width: 390 },
838+
layoutMeasurement: { height: 700, width: 390 },
839+
},
840+
});
841+
});
842+
843+
act(() => {
844+
fireEvent.scroll(scrollView, {
845+
nativeEvent: {
846+
contentOffset: { y: 10, x: 0 },
847+
contentSize: { height: 2000, width: 390 },
848+
layoutMeasurement: { height: 700, width: 390 },
849+
},
850+
});
851+
});
852+
853+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
854+
});
855+
856+
it('handleFooterLayout updates footer height on first measurement', () => {
857+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
858+
859+
const footerEl = getByTestId(MoneyFooterTestIds.CONTAINER);
860+
let footerAnimatedView: ReactTestInstance | null = null;
861+
let cursor: ReactTestInstance | null = footerEl.parent ?? null;
862+
while (cursor) {
863+
if (typeof cursor.props?.onLayout === 'function') {
864+
footerAnimatedView = cursor;
865+
break;
866+
}
867+
cursor = cursor.parent ?? null;
868+
}
869+
870+
act(() => {
871+
footerAnimatedView?.props.onLayout?.({
872+
nativeEvent: { layout: { height: 80, width: 390, x: 0, y: 0 } },
873+
});
874+
});
875+
876+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
877+
});
878+
879+
it('handleFooterLayout is a no-op when footer height is unchanged', () => {
880+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
881+
882+
const footerEl = getByTestId(MoneyFooterTestIds.CONTAINER);
883+
let footerAnimatedView: ReactTestInstance | null = null;
884+
let cursor: ReactTestInstance | null = footerEl.parent ?? null;
885+
while (cursor) {
886+
if (typeof cursor.props?.onLayout === 'function') {
887+
footerAnimatedView = cursor;
888+
break;
889+
}
890+
cursor = cursor.parent ?? null;
891+
}
892+
893+
act(() => {
894+
footerAnimatedView?.props.onLayout?.({
895+
nativeEvent: { layout: { height: 80, width: 390, x: 0, y: 0 } },
896+
});
897+
});
898+
899+
act(() => {
900+
footerAnimatedView?.props.onLayout?.({
901+
nativeEvent: { layout: { height: 80, width: 390, x: 0, y: 0 } },
902+
});
903+
});
904+
905+
expect(getByTestId(MoneyFooterTestIds.CONTAINER)).toBeOnTheScreen();
602906
});
603907
});
604908
});

0 commit comments

Comments
 (0)