diff --git a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.styles.ts b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.styles.ts deleted file mode 100644 index 1bd70533361..00000000000 --- a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.styles.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { StyleSheet } from 'react-native'; -import type { Theme } from '../../../../../util/theme/models'; - -export const createStyles = (_theme: Theme) => - StyleSheet.create({ - contentContainer: { - paddingHorizontal: 16, - paddingVertical: 16, - }, - loadingContainer: { - paddingVertical: 32, - alignItems: 'center', - justifyContent: 'center', - }, - loadingText: { - marginTop: 12, - }, - emptyContainer: { - paddingVertical: 32, - paddingHorizontal: 16, - alignItems: 'center', - justifyContent: 'center', - }, - footerContainer: { - paddingHorizontal: 16, - }, - }); diff --git a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx index 9da2c1efae7..020eb352194 100644 --- a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.test.tsx @@ -1,11 +1,14 @@ -import React from 'react'; -import { render } from '@testing-library/react-native'; +import React, { createRef } from 'react'; +import { fireEvent, render } from '@testing-library/react-native'; +import type { BottomSheetRef } from '@metamask/design-system-react-native'; import PerpsCancelAllOrdersView from './PerpsCancelAllOrdersView'; import { usePerpsCancelAllOrders, usePerpsLiveOrders } from '../../hooks'; +const mockGoBack = jest.fn(); + // Mock all dependencies jest.mock('@react-navigation/native', () => ({ - useNavigation: jest.fn(() => ({ navigate: jest.fn(), goBack: jest.fn() })), + useNavigation: jest.fn(() => ({ navigate: jest.fn(), goBack: mockGoBack })), })); jest.mock('../../../../../../locales/i18n', () => ({ @@ -33,56 +36,11 @@ 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, - }: { - buttonPropsArray?: { - label: string; - onPress: () => void; - disabled?: boolean; - }[]; - }) => ( - - {buttonPropsArray?.map((buttonProps, index) => ( - - {buttonProps.label} - - ))} - - ), - ButtonsAlignment: { - Horizontal: 'Horizontal', - Vertical: 'Vertical', - }, - }; - }, -); +jest.mock('@metamask/design-system-twrnc-preset', () => { + const tw = (..._args: unknown[]) => ({}); + tw.style = jest.fn(() => ({})); + return { useTailwind: () => tw }; +}); const mockUsePerpsLiveOrders = usePerpsLiveOrders as jest.MockedFunction< typeof usePerpsLiveOrders @@ -105,7 +63,7 @@ describe('PerpsCancelAllOrdersView', () => { filledSize: '0', remainingSize: '0.1', status: 'open' as const, - timestamp: Date.now(), + timestamp: 1722470400000, }, { orderId: 'order-2', @@ -118,7 +76,7 @@ describe('PerpsCancelAllOrdersView', () => { filledSize: '0', remainingSize: '1.0', status: 'open' as const, - timestamp: Date.now(), + timestamp: 1722470400000, }, ]; @@ -144,8 +102,8 @@ describe('PerpsCancelAllOrdersView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.cancel_all_modal.title')).toBeTruthy(); - expect(getByText('perps.cancel_all_modal.description')).toBeTruthy(); + expect(getByText('perps.cancel_all_modal.title')).toBeOnTheScreen(); + expect(getByText('perps.cancel_all_modal.description')).toBeOnTheScreen(); }); it('renders empty state when no orders', () => { @@ -163,10 +121,10 @@ describe('PerpsCancelAllOrdersView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.order.no_orders')).toBeTruthy(); + expect(getByText('perps.order.no_orders')).toBeOnTheScreen(); }); - it('renders loading state when canceling', () => { + it('keeps description and confirm label visible while canceling', () => { // Arrange mockUsePerpsCancelAllOrders.mockReturnValue({ ...mockCancelAllHook, @@ -174,11 +132,12 @@ describe('PerpsCancelAllOrdersView', () => { }); // Act - const { getAllByText } = render(); + const { getByText, queryByText } = render(); // Assert - const cancelingElements = getAllByText('perps.cancel_all_modal.canceling'); - expect(cancelingElements.length).toBeGreaterThan(0); + expect(getByText('perps.cancel_all_modal.description')).toBeOnTheScreen(); + expect(getByText('perps.cancel_all_modal.confirm')).toBeOnTheScreen(); + expect(queryByText('perps.cancel_all_modal.canceling')).toBeNull(); }); it('displays footer buttons with correct labels', () => { @@ -186,27 +145,39 @@ describe('PerpsCancelAllOrdersView', () => { const { getByText } = render(); // Assert - expect(getByText('perps.cancel_all_modal.keep_orders')).toBeTruthy(); - expect(getByText('perps.cancel_all_modal.confirm')).toBeTruthy(); + expect(getByText('perps.cancel_all_modal.keep_orders')).toBeOnTheScreen(); + expect(getByText('perps.cancel_all_modal.confirm')).toBeOnTheScreen(); }); - it('shows canceling label on confirm button when in progress', () => { + it('renders with empty orders gracefully', () => { // Arrange + mockUsePerpsLiveOrders.mockReturnValue({ + orders: [], + isInitialLoading: false, + }); mockUsePerpsCancelAllOrders.mockReturnValue({ ...mockCancelAllHook, - isCanceling: true, + orderCount: 0, }); // Act - const { getAllByText } = render(); + const { getByText } = render(); // Assert - const cancelingElements = getAllByText('perps.cancel_all_modal.canceling'); - expect(cancelingElements.length).toBeGreaterThan(0); + expect(getByText('perps.order.no_orders')).toBeOnTheScreen(); }); - it('renders with empty orders gracefully', () => { - // Arrange + it('navigates back when header close is pressed', () => { + // Arrange & Act — with orders + const { getByTestId, unmount } = render(); + + // Assert + fireEvent.press(getByTestId('header-close')); + expect(mockGoBack).toHaveBeenCalled(); + + // Arrange & Act — empty state uses the same close wiring + unmount(); + mockGoBack.mockClear(); mockUsePerpsLiveOrders.mockReturnValue({ orders: [], isInitialLoading: false, @@ -215,11 +186,44 @@ describe('PerpsCancelAllOrdersView', () => { ...mockCancelAllHook, orderCount: 0, }); + const { getByTestId: getEmptyTestId } = render( + , + ); - // Act - const { getByText } = render(); + fireEvent.press(getEmptyTestId('header-close')); + expect(mockGoBack).toHaveBeenCalled(); + }); - // Assert - expect(getByText('perps.order.no_orders')).toBeTruthy(); + it('does not navigate back when closing with an external sheetRef', () => { + // Arrange + const mockOnClose = jest.fn(); + const sheetRef = createRef(); + + // Act — with orders + const { getByTestId, unmount } = render( + , + ); + + // Assert — overlay mode closes via sheetRef, not navigation.goBack + fireEvent.press(getByTestId('header-close')); + expect(mockGoBack).not.toHaveBeenCalled(); + + // Act — empty state overlay path + unmount(); + mockGoBack.mockClear(); + mockUsePerpsLiveOrders.mockReturnValue({ + orders: [], + isInitialLoading: false, + }); + mockUsePerpsCancelAllOrders.mockReturnValue({ + ...mockCancelAllHook, + orderCount: 0, + }); + const { getByTestId: getEmptyTestId } = render( + , + ); + + fireEvent.press(getEmptyTestId('header-close')); + expect(mockGoBack).not.toHaveBeenCalled(); }); }); diff --git a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx index acb8b02de87..2379a7a4c79 100644 --- a/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx +++ b/app/components/UI/Perps/Views/PerpsCancelAllOrdersView/PerpsCancelAllOrdersView.tsx @@ -1,30 +1,25 @@ 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, { - BottomSheetRef, -} from '../../../../../component-library/components/BottomSheets/BottomSheet'; -import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; -import BottomSheetFooter, { +import { + BottomSheet, + BottomSheetFooter, + BottomSheetHeader, + type BottomSheetRef, + 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 { IconName } from '../../../../../component-library/components/Icons/Icon'; import { ToastVariants } from '../../../../../component-library/components/Toast/Toast.types'; import { usePerpsLiveOrders, usePerpsCancelAllOrders } from '../../hooks'; import usePerpsToasts, { type PerpsToastOptions, } from '../../hooks/usePerpsToasts'; -import { createStyles } from './PerpsCancelAllOrdersView.styles'; import { useTheme } from '../../../../../util/theme'; import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; @@ -44,7 +39,6 @@ const PerpsCancelAllOrdersView: React.FC = ({ onClose: onExternalClose, }) => { const theme = useTheme(); - const styles = createStyles(theme); const navigation = useNavigation(); const internalSheetRef = useRef(null); const sheetRef = externalSheetRef || internalSheetRef; @@ -190,27 +184,26 @@ const PerpsCancelAllOrdersView: React.FC = ({ } }, [externalSheetRef, handleClose, handleKeepOrders]); - const footerButtons = useMemo( - () => [ - { - label: strings('perps.cancel_all_modal.keep_orders'), - onPress: handleKeepButtonPress, - variant: ButtonVariants.Secondary, - size: ButtonSize.Lg, - disabled: isCanceling, - }, - { - label: isCanceling - ? strings('perps.cancel_all_modal.canceling') - : strings('perps.cancel_all_modal.confirm'), - onPress: handleCancelAll, - variant: ButtonVariants.Primary, - size: ButtonSize.Lg, - disabled: isCanceling, - danger: true, - }, - ], - [handleKeepButtonPress, handleCancelAll, isCanceling], + const secondaryButtonProps = useMemo( + () => ({ + children: strings('perps.cancel_all_modal.keep_orders'), + onPress: handleKeepButtonPress, + size: ButtonSize.Lg, + isDisabled: isCanceling, + }), + [handleKeepButtonPress, isCanceling], + ); + + const primaryButtonProps = useMemo( + () => ({ + children: strings('perps.cancel_all_modal.confirm'), + onPress: handleCancelAll, + size: ButtonSize.Lg, + isDanger: true, + isLoading: isCanceling, + isDisabled: isCanceling, + }), + [handleCancelAll, isCanceling], ); // Show empty state if no orders (WebSocket data loads instantly, no loading state needed) @@ -218,19 +211,20 @@ const PerpsCancelAllOrdersView: React.FC = ({ return ( navigation.goBack() : undefined} onClose={externalSheetRef ? onExternalClose : undefined} > - - - {strings('perps.cancel_all_modal.title')} - + + {strings('perps.cancel_all_modal.title')} - - + + {strings('perps.order.no_orders')} - + ); } @@ -238,41 +232,27 @@ const PerpsCancelAllOrdersView: React.FC = ({ return ( navigation.goBack() : undefined} onClose={externalSheetRef ? onExternalClose : undefined} > - - - {strings('perps.cancel_all_modal.title')} - + + {strings('perps.cancel_all_modal.title')} - - {isCanceling ? ( - - - - {strings('perps.cancel_all_modal.canceling')} - - - ) : ( - - {strings('perps.cancel_all_modal.description')} - - )} - + + + {strings('perps.cancel_all_modal.description')} + + ); diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx index ceda24a063e..cb04ace5111 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 as MmdsBottomSheetRef, Button, ButtonVariant, ButtonSize, @@ -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