From a1bb5447bb0bff8846f3cef0aaf8f2ea119035af Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Wed, 15 Jul 2026 00:03:44 -0700 Subject: [PATCH 1/3] refactor(perps): migrate Close All Positions sheet to MMDS Co-authored-by: Cursor --- .../PerpsCloseAllPositionsView.styles.ts | 22 -- .../PerpsCloseAllPositionsView.test.tsx | 125 +++++---- .../PerpsCloseAllPositionsView.tsx | 149 +++++------ .../Views/PerpsHomeView/PerpsHomeView.tsx | 5 +- .../PerpsCloseSummary.styles.ts | 18 +- .../PerpsCloseSummary.test.tsx | 4 - .../PerpsCloseSummary/PerpsCloseSummary.tsx | 244 +++++++----------- 7 files changed, 243 insertions(+), 324 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.styles.ts b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.styles.ts index 65651903c3b..4d803ed6e60 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.styles.ts +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.styles.ts @@ -3,16 +3,9 @@ import type { Theme } from '../../../../../util/theme/models'; export const createStyles = (_theme: Theme) => StyleSheet.create({ - contentContainer: { - paddingHorizontal: 16, - paddingVertical: 8, - }, description: { marginBottom: 16, }, - breakdownContainer: { - gap: 12, - }, loadingContainer: { paddingVertical: 32, alignItems: 'center', @@ -23,22 +16,7 @@ export const createStyles = (_theme: Theme) => }, emptyContainer: { paddingVertical: 32, - paddingHorizontal: 16, alignItems: 'center', justifyContent: 'center', }, - footerContainer: { - paddingHorizontal: 16, - zIndex: 0, // Ensure footer stays behind modal overlays - elevation: 0, // Android equivalent - }, - footerButtonSecondary: { - flex: 1, - borderWidth: 0, - }, - labelWithTooltip: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, }); diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx index 89807a5f46c..8111d97c909 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx @@ -45,56 +45,87 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ usePerpsEventTracking: jest.fn(), })); -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheet', - () => { - const mockReact = jest.requireActual('react'); - return mockReact.forwardRef( - (props: { children: React.ReactNode }, _ref) => <>{props.children}, - ); - }, -); - -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheetHeader', - () => 'BottomSheetHeader', -); - -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheetFooter', - () => { - const { View, TouchableOpacity, Text } = jest.requireActual('react-native'); - - return { - __esModule: true, - default: ({ - buttonPropsArray, +jest.mock('@metamask/design-system-react-native', () => { + const MockReact = jest.requireActual('react'); + const { View, Text, TouchableOpacity } = jest.requireActual('react-native'); + const actual = jest.requireActual('@metamask/design-system-react-native'); + + const BottomSheet = MockReact.forwardRef( + ( + { + children, }: { - buttonPropsArray?: { - label: string; - onPress: () => void; - disabled?: boolean; - }[]; - }) => ( - - {buttonPropsArray?.map((buttonProps, index) => ( - - {buttonProps.label} - - ))} - - ), - ButtonsAlignment: { - Horizontal: 'Horizontal', - Vertical: 'Vertical', + children: React.ReactNode; }, + ref: React.Ref<{ + onOpenBottomSheet: () => void; + onCloseBottomSheet: (callback?: () => void) => void; + }>, + ) => { + MockReact.useImperativeHandle(ref, () => ({ + onOpenBottomSheet: jest.fn(), + onCloseBottomSheet: (callback?: () => void) => { + callback?.(); + }, + })); + + return {children}; + }, + ); + BottomSheet.displayName = 'BottomSheet'; + + const BottomSheetHeader = ({ children }: { children: React.ReactNode }) => ( + + {typeof children === 'string' ? {children} : children} + + ); + + const BottomSheetFooter = ({ + primaryButtonProps, + secondaryButtonProps, + }: { + primaryButtonProps?: { + children?: React.ReactNode; + onPress?: () => void; + isDisabled?: boolean; + }; + secondaryButtonProps?: { + children?: React.ReactNode; + onPress?: () => void; + isDisabled?: boolean; }; - }, -); + }) => ( + + {secondaryButtonProps && ( + + {secondaryButtonProps.children} + + )} + {primaryButtonProps && ( + + {primaryButtonProps.children} + + )} + + ); + + return { + ...actual, + BottomSheet, + BottomSheetHeader, + BottomSheetFooter, + ButtonsAlignment: { + Horizontal: 'Horizontal', + Vertical: 'Vertical', + }, + }; +}); jest.mock('../../components/PerpsCloseSummary', () => 'PerpsCloseSummary'); diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx index 9a12d50dc68..c7496f9d4e1 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx @@ -1,23 +1,20 @@ import { useNavigation } from '@react-navigation/native'; import React, { useCallback, useMemo, useRef } from 'react'; import { View, ActivityIndicator } from 'react-native'; -import { NotificationMoment } from '../../../../../util/haptics'; -import { strings } from '../../../../../../locales/i18n'; -import BottomSheet, { +import { + BottomSheet, + BottomSheetFooter, + BottomSheetHeader, BottomSheetRef, -} from '../../../../../component-library/components/BottomSheets/BottomSheet'; -import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; -import BottomSheetFooter, { + Box, + ButtonSize, ButtonsAlignment, -} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; -import Text, { + Text, TextColor, TextVariant, -} from '../../../../../component-library/components/Texts/Text'; -import { - ButtonSize, - ButtonVariants, -} from '../../../../../component-library/components/Buttons/Button'; +} from '@metamask/design-system-react-native'; +import { NotificationMoment } from '../../../../../util/haptics'; +import { strings } from '../../../../../../locales/i18n'; import { IconName } from '../../../../../component-library/components/Icons/Icon'; import { ToastVariants } from '../../../../../component-library/components/Toast/Toast.types'; import { @@ -222,54 +219,47 @@ const PerpsCloseAllPositionsView: React.FC = ({ } }, [externalSheetRef, handleClose, handleKeepPositions]); - const footerButtons = useMemo( - () => [ - { - label: strings('perps.close_all_modal.keep_positions'), - onPress: handleKeepButtonPress, - variant: ButtonVariants.Secondary, - size: ButtonSize.Lg, - style: styles.footerButtonSecondary, - disabled: isClosing, - }, - { - label: isClosing - ? strings('perps.close_all_modal.closing') - : strings('perps.close_all_modal.close_all'), - onPress: handleCloseAll, - variant: ButtonVariants.Primary, - size: ButtonSize.Lg, - disabled: isClosing, - danger: true, - }, - ], - [ - handleKeepButtonPress, - handleCloseAll, - isClosing, - styles.footerButtonSecondary, - ], + const secondaryButtonProps = useMemo( + () => ({ + children: strings('perps.close_all_modal.keep_positions'), + onPress: handleKeepButtonPress, + size: ButtonSize.Lg, + isDisabled: isClosing, + }), + [handleKeepButtonPress, isClosing], ); + const primaryButtonProps = useMemo( + () => ({ + children: isClosing + ? strings('perps.close_all_modal.closing') + : strings('perps.close_all_modal.close_all'), + onPress: handleCloseAll, + size: ButtonSize.Lg, + isDisabled: isClosing, + isDanger: true, + }), + [handleCloseAll, isClosing], + ); + + const goBack = externalSheetRef ? undefined : navigation.goBack; + const onClose = externalSheetRef ? onExternalClose : undefined; + // Show loading state while fetching positions if (isInitialLoading) { return ( - + - - {strings('perps.close_all_modal.title')} - + {strings('perps.close_all_modal.title')} - - - + + + + + ); } @@ -277,41 +267,34 @@ const PerpsCloseAllPositionsView: React.FC = ({ // Show empty state if no positions if (!positions || positions.length === 0) { return ( - + - - {strings('perps.close_all_modal.title')} - + {strings('perps.close_all_modal.title')} - - - {strings('perps.position.no_positions')} - - + + + + {strings('perps.position.no_positions')} + + + ); } return ( - + - - {strings('perps.close_all_modal.title')} - + {strings('perps.close_all_modal.title')} - + {strings('perps.close_all_modal.description')} @@ -324,8 +307,8 @@ const PerpsCloseAllPositionsView: React.FC = ({ color={theme.colors.primary.default} /> {strings('perps.close_all_modal.closing')} @@ -352,12 +335,12 @@ const PerpsCloseAllPositionsView: React.FC = ({ enableTooltips={false} /> )} - + ); diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx index ceda24a063e..0ab2b8bd554 100644 --- a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx +++ b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx @@ -15,6 +15,7 @@ import { type RouteProp, } from '@react-navigation/native'; import { + BottomSheetRef, Button, ButtonVariant, ButtonSize, @@ -112,7 +113,7 @@ import { } from '../../Perps.testIds'; import PerpsCloseAllPositionsView from '../PerpsCloseAllPositionsView/PerpsCloseAllPositionsView'; import PerpsCancelAllOrdersView from '../PerpsCancelAllOrdersView/PerpsCancelAllOrdersView'; -import { BottomSheetRef } from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import { BottomSheetRef as ComponentLibraryBottomSheetRef } from '../../../../../component-library/components/BottomSheets/BottomSheet'; import PerpsNavigationCard, { NavigationItem, } from '../../components/PerpsNavigationCard/PerpsNavigationCard'; @@ -209,7 +210,7 @@ const PerpsHomeView = ({ const [showCloseAllSheet, setShowCloseAllSheet] = useState(false); const [showCancelAllSheet, setShowCancelAllSheet] = useState(false); const closeAllSheetRef = useRef(null); - const cancelAllSheetRef = useRef(null); + const cancelAllSheetRef = useRef(null); // Use hook for eligibility checks and action handlers // Pass button location for tracking deposit entry point diff --git a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.styles.ts b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.styles.ts index e339585e69d..b6f91607088 100644 --- a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.styles.ts +++ b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.styles.ts @@ -1,10 +1,8 @@ import { StyleSheet } from 'react-native'; import type { Theme } from '../../../../../util/theme/models'; -const styleSheet = (_params: { theme: Theme }) => { - const { colors } = _params.theme; - - return StyleSheet.create({ +const styleSheet = (_params: { theme: Theme }) => + StyleSheet.create({ summaryContainer: { paddingTop: 16, paddingBottom: 16, @@ -26,17 +24,6 @@ const styleSheet = (_params: { theme: Theme }) => { flexShrink: 0, alignItems: 'flex-end', }, - summaryTotalRow: { - marginTop: 4, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: colors.border.muted, - }, - labelWithTooltip: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, inclusiveFeeRow: { flexDirection: 'row', gap: 4, @@ -47,6 +34,5 @@ const styleSheet = (_params: { theme: Theme }) => { gap: 8, }, }); -}; export default styleSheet; diff --git a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx index 0bab0c54e42..1134ed0e0c0 100644 --- a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx +++ b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx @@ -26,10 +26,6 @@ jest.mock('../../../../hooks/useStyles', () => ({ summaryLabel: {}, summaryValue: {}, inclusiveFeeRow: {}, - labelWithTooltip: {}, - summaryTotalRow: {}, - rewardsRow: {}, - rewardsContent: {}, loadingContainer: {}, }, theme: mockTheme, diff --git a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.tsx b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.tsx index ddd47adb49e..aa8874f29cd 100644 --- a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.tsx +++ b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.tsx @@ -1,10 +1,5 @@ import React, { useCallback } from 'react'; -import { - View, - TouchableOpacity, - ActivityIndicator, - type ViewStyle, -} from 'react-native'; +import { View, ActivityIndicator, type ViewStyle } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; import { @@ -22,13 +17,14 @@ import createStyles from './PerpsCloseSummary.styles'; import Routes from '../../../../../constants/navigation/Routes'; import { InternalAccount } from '@metamask/keyring-internal-api'; import { + FontWeight, + IconName, + KeyValueRow, + KeyValueRowVariant, + SectionDivider, Text, TextColor, TextVariant, - Icon, - IconName, - IconSize, - IconColor, } from '@metamask/design-system-react-native'; export interface PerpsCloseSummaryProps { @@ -154,6 +150,33 @@ const PerpsCloseSummary: React.FC = ({ const rewardAnimationState = getRewardAnimationState(); + const feesValue = isLoadingFees ? ( + + + + ) : totalFees !== undefined ? ( + + ) : ( + '--' + ); + + const pointsValue = accountOptedIn ? ( + + ) : ( + + ); + return ( = ({ style, ]} > - {/* Margin with P&L breakdown */} + {/* Margin with P&L breakdown — custom row to support nested includes P&L */} - + {strings('perps.close_position.margin')} - + {formatPerpsFiat(totalMargin, { ranges: PRICE_RANGES_MINIMAL_VIEW, })} @@ -194,148 +226,60 @@ const PerpsCloseSummary: React.FC = ({ - {/* Fees with discount */} - - - {enableTooltips ? ( - - handleTooltipPress('closing_fees', { - metamaskFeeRate, - protocolFeeRate, - originalMetamaskFeeRate, - feeDiscountPercentage, - }) - } - style={styles.labelWithTooltip} - testID={testIDs?.feesTooltip} - > - - {strings('perps.close_position.fees')} - - - - ) : ( - - {strings('perps.close_position.fees')} - - )} - - - {isLoadingFees ? ( - - - - ) : totalFees !== undefined ? ( - - ) : ( - -- - )} - - + + handleTooltipPress('closing_fees', { + metamaskFeeRate, + protocolFeeRate, + originalMetamaskFeeRate, + feeDiscountPercentage, + }), + testID: testIDs?.feesTooltip, + }, + })} + value={feesValue} + /> - {/* You'll receive */} - - - {enableTooltips ? ( - handleTooltipPress('close_position_you_receive')} - style={styles.labelWithTooltip} - testID={testIDs?.receiveTooltip} - > - - {strings('perps.close_position.you_receive')} - - - - ) : ( - - {strings('perps.close_position.you_receive')} - - )} - - - - {formatPerpsFiat(receiveAmount, { - ranges: PRICE_RANGES_MINIMAL_VIEW, - })} - - - + + + handleTooltipPress('close_position_you_receive'), + testID: testIDs?.receiveTooltip, + }, + })} + value={formatPerpsFiat(receiveAmount, { + ranges: PRICE_RANGES_MINIMAL_VIEW, + })} + valueTextProps={{ + testID: testIDs?.receiveValue, + }} + /> - {/* Estimated Points */} {shouldShowRewards && (accountOptedIn || (accountOptedIn === false && rewardsAccount !== undefined)) && ( - - - {enableTooltips ? ( - handleTooltipPress('points')} - style={styles.labelWithTooltip} - testID={testIDs?.pointsTooltip} - > - - {strings('perps.estimated_points')} - - - - ) : ( - - {strings('perps.estimated_points')} - - )} - - - {accountOptedIn ? ( - - ) : ( - - )} - - + handleTooltipPress('points'), + testID: testIDs?.pointsTooltip, + }, + })} + value={pointsValue} + /> )} ); From 7a015429f65a1ace071385d70ef2706fbfd72331 Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Wed, 15 Jul 2026 10:59:58 -0700 Subject: [PATCH 2/3] test(perps): harden Close All Positions unit assertions Pin mock timestamps and replace weak/vacuous matchers with toBeOnTheScreen so presence checks stay deterministic and meaningful. Co-authored-by: Cursor --- .../PerpsCloseAllPositionsView.test.tsx | 22 +++++------ .../PerpsCloseSummary.test.tsx | 38 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx index 8111d97c909..9034ba4dbeb 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx @@ -169,7 +169,7 @@ describe('PerpsCloseAllPositionsView', () => { takeProfitCount: 0, stopLossCount: 0, marketPrice: '50200', - timestamp: Date.now(), + timestamp: 1700000000000, // fixed epoch ms – 2023-11-14T22:13:20.000Z }, ]; @@ -238,7 +238,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.title')).toBeTruthy(); + expect(getByText('perps.close_all_modal.title')).toBeOnTheScreen(); }); it('renders empty state when no positions', () => { @@ -252,7 +252,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.position.no_positions')).toBeTruthy(); + expect(getByText('perps.position.no_positions')).toBeOnTheScreen(); }); it('renders close all positions view with positions', () => { @@ -260,8 +260,8 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.title')).toBeTruthy(); - expect(getByText('perps.close_all_modal.description')).toBeTruthy(); + expect(getByText('perps.close_all_modal.title')).toBeOnTheScreen(); + expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); }); it('renders loading state when closing', () => { @@ -284,8 +284,8 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.keep_positions')).toBeTruthy(); - expect(getByText('perps.close_all_modal.close_all')).toBeTruthy(); + expect(getByText('perps.close_all_modal.keep_positions')).toBeOnTheScreen(); + expect(getByText('perps.close_all_modal.close_all')).toBeOnTheScreen(); }); it('shows closing label on close button when in progress', () => { @@ -314,7 +314,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.position.no_positions')).toBeTruthy(); + expect(getByText('perps.position.no_positions')).toBeOnTheScreen(); }); it('renders PerpsCloseSummary when not closing', () => { @@ -322,7 +322,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeTruthy(); + expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); }); it('calls usePerpsRewardAccountOptedIn with totalEstimatedPoints', () => { @@ -371,7 +371,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeTruthy(); + expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); expect(mockUsePerpsRewardAccountOptedIn).toHaveBeenCalled(); }); @@ -386,7 +386,7 @@ describe('PerpsCloseAllPositionsView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeTruthy(); + expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); expect(mockUsePerpsRewardAccountOptedIn).toHaveBeenCalled(); }); }); diff --git a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx index 1134ed0e0c0..14aabc092f9 100644 --- a/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx +++ b/app/components/UI/Perps/components/PerpsCloseSummary/PerpsCloseSummary.test.tsx @@ -100,7 +100,7 @@ describe('PerpsCloseSummary', () => { // Assert expect(strings).toHaveBeenCalledWith('perps.close_position.margin'); expect(strings).toHaveBeenCalledWith('perps.close_position.includes_pnl'); - expect(getByText('perps.close_position.margin')).toBeTruthy(); + expect(getByText('perps.close_position.margin')).toBeOnTheScreen(); }); it('renders margin with negative P&L', () => { @@ -111,7 +111,7 @@ describe('PerpsCloseSummary', () => { const { getByText } = render(); // Assert - expect(getByText('perps.close_position.margin')).toBeTruthy(); + expect(getByText('perps.close_position.margin')).toBeOnTheScreen(); expect(strings).toHaveBeenCalledWith('perps.close_position.includes_pnl'); }); @@ -124,8 +124,8 @@ describe('PerpsCloseSummary', () => { const { getByTestId, getByText } = render(); // Assert - expect(getByText('perps.close_position.fees')).toBeTruthy(); - expect(getByTestId('fees-tooltip-button')).toBeTruthy(); + expect(getByText('perps.close_position.fees')).toBeOnTheScreen(); + expect(getByTestId('fees-tooltip-button')).toBeOnTheScreen(); }); it('renders receive amount with tooltip', () => { @@ -137,8 +137,8 @@ describe('PerpsCloseSummary', () => { const { getByTestId, getByText } = render(); // Assert - expect(getByText('perps.close_position.you_receive')).toBeTruthy(); - expect(getByTestId('receive-tooltip-button')).toBeTruthy(); + expect(getByText('perps.close_position.you_receive')).toBeOnTheScreen(); + expect(getByTestId('receive-tooltip-button')).toBeOnTheScreen(); }); it('renders rewards section when enabled and account opted in', () => { @@ -155,7 +155,7 @@ describe('PerpsCloseSummary', () => { const { getByText } = render(); // Assert - expect(getByText('perps.estimated_points')).toBeTruthy(); + expect(getByText('perps.estimated_points')).toBeOnTheScreen(); }); it('renders rewards with loading state when account opted in', () => { @@ -172,7 +172,7 @@ describe('PerpsCloseSummary', () => { const { getByText } = render(); // Assert - expect(getByText('perps.estimated_points')).toBeTruthy(); + expect(getByText('perps.estimated_points')).toBeOnTheScreen(); }); it('applies custom style prop', () => { @@ -181,10 +181,10 @@ describe('PerpsCloseSummary', () => { const props = { ...defaultProps, style: customStyle }; // Act - const { getByTestId } = render(); + const { getByText } = render(); // Assert - component renders without crashing with custom style - expect(getByTestId).toBeDefined(); + expect(getByText('perps.close_position.margin')).toBeOnTheScreen(); }); it('applies padding when input focused', () => { @@ -192,10 +192,10 @@ describe('PerpsCloseSummary', () => { const props = { ...defaultProps, isInputFocused: true }; // Act - const { getByTestId } = render(); + const { getByText } = render(); // Assert - component renders without crashing with focused state - expect(getByTestId).toBeDefined(); + expect(getByText('perps.close_position.margin')).toBeOnTheScreen(); }); it('disables tooltip interactions when enableTooltips is false', () => { @@ -234,7 +234,7 @@ describe('PerpsCloseSummary', () => { const { getByText } = render(); // Assert - rewards section still renders with error state - expect(getByText('perps.estimated_points')).toBeTruthy(); + expect(getByText('perps.estimated_points')).toBeOnTheScreen(); }); it('handles tooltip press to open fees tooltip', () => { @@ -248,7 +248,7 @@ describe('PerpsCloseSummary', () => { fireEvent.press(getByTestId('fees-tooltip')); - expect(getByTestId('fees-tooltip')).toBeTruthy(); + expect(getByTestId('fees-tooltip')).toBeOnTheScreen(); }); it('handles tooltip press to open receive amount tooltip', () => { @@ -262,7 +262,7 @@ describe('PerpsCloseSummary', () => { fireEvent.press(getByTestId('receive-tooltip')); - expect(getByTestId('receive-tooltip')).toBeTruthy(); + expect(getByTestId('receive-tooltip')).toBeOnTheScreen(); }); it('handles tooltip press to open points tooltip when rewards enabled and account opted in', () => { @@ -279,7 +279,7 @@ describe('PerpsCloseSummary', () => { fireEvent.press(getByTestId('points-tooltip')); - expect(getByTestId('points-tooltip')).toBeTruthy(); + expect(getByTestId('points-tooltip')).toBeOnTheScreen(); }); it('does not trigger tooltip callback when tooltips are disabled', () => { @@ -308,8 +308,8 @@ describe('PerpsCloseSummary', () => { const { getByTestId, getByText } = render(); // Assert - expect(getByText('perps.estimated_points')).toBeTruthy(); - expect(getByTestId('add-rewards-account')).toBeTruthy(); + expect(getByText('perps.estimated_points')).toBeOnTheScreen(); + expect(getByTestId('add-rewards-account')).toBeOnTheScreen(); }); it('renders RewardsAnimations when account opted in', () => { @@ -328,7 +328,7 @@ describe('PerpsCloseSummary', () => { ); // Assert - expect(getByText('perps.estimated_points')).toBeTruthy(); + expect(getByText('perps.estimated_points')).toBeOnTheScreen(); expect(queryByTestId('add-rewards-account')).toBeNull(); }); From 134b7f42e58cf555dc8908300c5e491a1cb725fc Mon Sep 17 00:00:00 2001 From: Brian Nguyen Date: Wed, 15 Jul 2026 20:34:50 -0700 Subject: [PATCH 3/3] test(perps): use testIDs for Close All Positions unit tests Remove the MMDS BottomSheet mock and assert via shared selectors instead. Co-authored-by: Cursor --- app/components/UI/Perps/Perps.testIds.ts | 14 ++ .../PerpsCloseAllPositionsView.test.tsx | 156 ++++++------------ .../PerpsCloseAllPositionsView.tsx | 46 +++++- 3 files changed, 103 insertions(+), 113 deletions(-) diff --git a/app/components/UI/Perps/Perps.testIds.ts b/app/components/UI/Perps/Perps.testIds.ts index 35f8439b724..7d3f7124a4c 100644 --- a/app/components/UI/Perps/Perps.testIds.ts +++ b/app/components/UI/Perps/Perps.testIds.ts @@ -306,6 +306,20 @@ export const PerpsPositionsViewSelectorsIDs = { POSITIONS_SECTION_TITLE: 'perps-positions-section-title', }; +// ======================================== +// PERPS CLOSE ALL POSITIONS VIEW SELECTORS +// ======================================== + +export const PerpsCloseAllPositionsViewSelectorsIDs = { + SHEET: 'perps-close-all-positions-sheet', + TITLE: 'perps-close-all-positions-title', + DESCRIPTION: 'perps-close-all-positions-description', + EMPTY_STATE: 'perps-close-all-positions-empty-state', + CLOSING_STATE: 'perps-close-all-positions-closing-state', + KEEP_BUTTON: 'perps-close-all-positions-keep-button', + CLOSE_ALL_BUTTON: 'perps-close-all-positions-close-all-button', +} as const; + export const PerpsPositionDetailsViewSelectorsIDs = { CANDLESTICK_CHART: 'candlestick-chart', TRADINGVIEW_CHART: 'tradingview-chart', diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx index 9034ba4dbeb..25fa6562239 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.test.tsx @@ -8,6 +8,7 @@ import { usePerpsRewardAccountOptedIn, } from '../../hooks'; import { InternalAccount } from '@metamask/keyring-internal-api'; +import { PerpsCloseAllPositionsViewSelectorsIDs } from '../../Perps.testIds'; // Mock all dependencies jest.mock('@react-navigation/native', () => ({ @@ -45,88 +46,6 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ usePerpsEventTracking: jest.fn(), })); -jest.mock('@metamask/design-system-react-native', () => { - const MockReact = jest.requireActual('react'); - const { View, Text, TouchableOpacity } = jest.requireActual('react-native'); - const actual = jest.requireActual('@metamask/design-system-react-native'); - - const BottomSheet = MockReact.forwardRef( - ( - { - children, - }: { - children: React.ReactNode; - }, - ref: React.Ref<{ - onOpenBottomSheet: () => void; - onCloseBottomSheet: (callback?: () => void) => void; - }>, - ) => { - MockReact.useImperativeHandle(ref, () => ({ - onOpenBottomSheet: jest.fn(), - onCloseBottomSheet: (callback?: () => void) => { - callback?.(); - }, - })); - - return {children}; - }, - ); - BottomSheet.displayName = 'BottomSheet'; - - const BottomSheetHeader = ({ children }: { children: React.ReactNode }) => ( - - {typeof children === 'string' ? {children} : children} - - ); - - const BottomSheetFooter = ({ - primaryButtonProps, - secondaryButtonProps, - }: { - primaryButtonProps?: { - children?: React.ReactNode; - onPress?: () => void; - isDisabled?: boolean; - }; - secondaryButtonProps?: { - children?: React.ReactNode; - onPress?: () => void; - isDisabled?: boolean; - }; - }) => ( - - {secondaryButtonProps && ( - - {secondaryButtonProps.children} - - )} - {primaryButtonProps && ( - - {primaryButtonProps.children} - - )} - - ); - - return { - ...actual, - BottomSheet, - BottomSheetHeader, - BottomSheetFooter, - ButtonsAlignment: { - Horizontal: 'Horizontal', - Vertical: 'Vertical', - }, - }; -}); - jest.mock('../../components/PerpsCloseSummary', () => 'PerpsCloseSummary'); const mockUsePerpsLivePositions = usePerpsLivePositions as jest.MockedFunction< @@ -235,10 +154,12 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.title')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.TITLE), + ).toBeOnTheScreen(); }); it('renders empty state when no positions', () => { @@ -249,19 +170,25 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.position.no_positions')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.EMPTY_STATE), + ).toBeOnTheScreen(); }); it('renders close all positions view with positions', () => { // Arrange & Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.title')).toBeOnTheScreen(); - expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.TITLE), + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.DESCRIPTION), + ).toBeOnTheScreen(); }); it('renders loading state when closing', () => { @@ -272,20 +199,25 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getAllByText } = render(); + const { getByTestId } = render(); // Assert - const closingElements = getAllByText('perps.close_all_modal.closing'); - expect(closingElements.length).toBeGreaterThan(0); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.CLOSING_STATE), + ).toBeOnTheScreen(); }); it('displays footer buttons with correct labels', () => { // Arrange & Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.keep_positions')).toBeOnTheScreen(); - expect(getByText('perps.close_all_modal.close_all')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.KEEP_BUTTON), + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.CLOSE_ALL_BUTTON), + ).toBeOnTheScreen(); }); it('shows closing label on close button when in progress', () => { @@ -296,11 +228,15 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getAllByText } = render(); + const { getByTestId } = render(); // Assert - const closingElements = getAllByText('perps.close_all_modal.closing'); - expect(closingElements.length).toBeGreaterThan(0); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.CLOSE_ALL_BUTTON), + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.CLOSING_STATE), + ).toBeOnTheScreen(); }); it('renders with empty positions gracefully', () => { @@ -311,18 +247,22 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.position.no_positions')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.EMPTY_STATE), + ).toBeOnTheScreen(); }); it('renders PerpsCloseSummary when not closing', () => { // Arrange & Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.DESCRIPTION), + ).toBeOnTheScreen(); }); it('calls usePerpsRewardAccountOptedIn with totalEstimatedPoints', () => { @@ -368,10 +308,12 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.DESCRIPTION), + ).toBeOnTheScreen(); expect(mockUsePerpsRewardAccountOptedIn).toHaveBeenCalled(); }); @@ -383,10 +325,12 @@ describe('PerpsCloseAllPositionsView', () => { }); // Act - const { getByText } = render(); + const { getByTestId } = render(); // Assert - expect(getByText('perps.close_all_modal.description')).toBeOnTheScreen(); + expect( + getByTestId(PerpsCloseAllPositionsViewSelectorsIDs.DESCRIPTION), + ).toBeOnTheScreen(); expect(mockUsePerpsRewardAccountOptedIn).toHaveBeenCalled(); }); }); diff --git a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx index c7496f9d4e1..8754d09fec0 100644 --- a/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx +++ b/app/components/UI/Perps/Views/PerpsCloseAllPositionsView/PerpsCloseAllPositionsView.tsx @@ -37,6 +37,7 @@ import { PERPS_EVENT_VALUE, type ClosePositionsResult, } from '@metamask/perps-controller'; +import { PerpsCloseAllPositionsViewSelectorsIDs } from '../../Perps.testIds'; interface PerpsCloseAllPositionsViewProps { sheetRef?: React.RefObject; @@ -225,6 +226,7 @@ const PerpsCloseAllPositionsView: React.FC = ({ onPress: handleKeepButtonPress, size: ButtonSize.Lg, isDisabled: isClosing, + testID: PerpsCloseAllPositionsViewSelectorsIDs.KEEP_BUTTON, }), [handleKeepButtonPress, isClosing], ); @@ -238,6 +240,7 @@ const PerpsCloseAllPositionsView: React.FC = ({ size: ButtonSize.Lg, isDisabled: isClosing, isDanger: true, + testID: PerpsCloseAllPositionsViewSelectorsIDs.CLOSE_ALL_BUTTON, }), [handleCloseAll, isClosing], ); @@ -248,8 +251,16 @@ const PerpsCloseAllPositionsView: React.FC = ({ // Show loading state while fetching positions if (isInitialLoading) { return ( - - + + {strings('perps.close_all_modal.title')} @@ -267,8 +278,16 @@ const PerpsCloseAllPositionsView: React.FC = ({ // Show empty state if no positions if (!positions || positions.length === 0) { return ( - - + + {strings('perps.close_all_modal.title')} @@ -276,6 +295,7 @@ const PerpsCloseAllPositionsView: React.FC = ({ {strings('perps.position.no_positions')} @@ -286,8 +306,16 @@ const PerpsCloseAllPositionsView: React.FC = ({ } return ( - - + + {strings('perps.close_all_modal.title')} @@ -296,12 +324,16 @@ const PerpsCloseAllPositionsView: React.FC = ({ variant={TextVariant.BodyMd} color={TextColor.TextAlternative} style={styles.description} + testID={PerpsCloseAllPositionsViewSelectorsIDs.DESCRIPTION} > {strings('perps.close_all_modal.description')} {isClosing ? ( - +