diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx index 003f1d8cb0d2..16a96f22b3b1 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx @@ -580,6 +580,8 @@ const PerpsOrderViewContentBase: React.FC = () => { limitPrice: orderForm.limitPrice, initialTakeProfitPrice: orderForm.takeProfitPrice, initialStopLossPrice: orderForm.stopLossPrice, + amount: orderForm.amount, + szDecimals: marketData?.szDecimals, onConfirm: async (takeProfitPrice?: string, stopLossPrice?: string) => { // Use the same clearing approach as the "Off" button // If values are undefined or empty, ensure they're cleared properly @@ -599,11 +601,13 @@ const PerpsOrderViewContentBase: React.FC = () => { orderForm.leverage, orderForm.takeProfitPrice, orderForm.stopLossPrice, + orderForm.amount, assetData.price, showToast, navigation, setTakeProfitPrice, setStopLossPrice, + marketData?.szDecimals, ]); const handleAmountPress = () => { diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts index d49e0371f3d1..9a7cd6c2d37c 100644 --- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts @@ -14,14 +14,22 @@ export const createStyles = (colors: Theme['colors']) => header: { flexDirection: 'row', alignItems: 'center', - gap: 16, + justifyContent: 'center', paddingHorizontal: 16, paddingVertical: 16, - borderBottomWidth: 1, - borderBottomColor: colors.border.muted, + position: 'relative', + }, + headerBackButton: { + position: 'absolute', + left: 16, + zIndex: 1, + }, + headerTitleContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', }, footer: { - paddingHorizontal: 16, paddingBottom: 16, }, priceInfoContainer: { @@ -53,10 +61,8 @@ export const createStyles = (colors: Theme['colors']) => marginBottom: 8, }, inputContainer: { - backgroundColor: colors.background.default, + backgroundColor: colors.background.muted, borderRadius: 8, - borderWidth: 1, - borderColor: colors.border.muted, paddingHorizontal: 16, paddingVertical: 12, flexDirection: 'row', @@ -70,9 +76,11 @@ export const createStyles = (colors: Theme['colors']) => marginLeft: 4, }, inputContainerActive: { + borderWidth: 1, borderColor: colors.primary.default, }, inputContainerError: { + borderWidth: 1, borderColor: colors.error.default, }, input: { @@ -81,12 +89,11 @@ export const createStyles = (colors: Theme['colors']) => color: colors.text.default, paddingVertical: 0, textAlign: 'left', - marginRight: 8, + marginLeft: 8, }, percentageRow: { flexDirection: 'row', - justifyContent: 'space-between', marginBottom: 12, gap: 8, }, @@ -94,25 +101,13 @@ export const createStyles = (colors: Theme['colors']) => flex: 1, paddingVertical: 10, paddingHorizontal: 8, - backgroundColor: colors.background.pressed, + backgroundColor: colors.background.muted, borderRadius: 8, alignItems: 'center', - borderWidth: 1, - borderColor: colors.border.muted, minWidth: 50, }, percentageButtonOff: { - backgroundColor: colors.background.pressed, - borderWidth: 1, - borderColor: colors.border.muted, - }, - percentageButtonActiveTP: { - borderWidth: 1, - borderColor: colors.primary.default, - }, - percentageButtonActiveSL: { - borderWidth: 1, - borderColor: colors.primary.default, + backgroundColor: colors.background.muted, }, helperText: { marginTop: 4, @@ -172,7 +167,6 @@ export const createStyles = (colors: Theme['colors']) => }, percentageButtonsContainer: { flexDirection: 'row', - justifyContent: 'space-between', marginBottom: 12, gap: 8, }, @@ -203,4 +197,22 @@ export const createStyles = (colors: Theme['colors']) => width: '100%', marginBottom: 8, }, + expectedPnLText: { + marginTop: 8, + textAlign: 'right', + }, + sectionTitleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + footerButtonsRow: { + flexDirection: 'row', + gap: 12, + width: '100%', + }, + footerButton: { + flex: 1, + }, }); diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx index cb4bb4dd29f5..a16b704c7eaf 100644 --- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react-native'; import PerpsTPSLView from './PerpsTPSLView'; import type { Position } from '../../controllers/types'; -// Mock dependencies - only what's absolutely necessary +// Mock dependencies jest.mock('react-native-reanimated', () => jest.requireActual('react-native-reanimated/mock'), ); @@ -25,7 +25,6 @@ jest.mock('react-native-gesture-handler', () => ({ jest.mock('react-native-linear-gradient', () => 'LinearGradient'); -// Mock safe area context (required for BottomSheet and SafeAreaView) jest.mock('react-native-safe-area-context', () => { const { View } = jest.requireActual('react-native'); const inset = { top: 0, right: 0, bottom: 0, left: 0 }; @@ -45,15 +44,15 @@ jest.mock('react-native-safe-area-context', () => { }; }); -// Mock theme const mockUseTheme = jest.fn(); jest.mock('../../../../../util/theme', () => ({ useTheme: mockUseTheme, })); -// Mock hooks +jest.mock('../../hooks/stream', () => ({ + usePerpsLivePrices: jest.fn(() => ({})), +})); -// Mock liquidation price hook to avoid async side effects jest.mock('../../hooks/usePerpsLiquidationPrice', () => ({ usePerpsLiquidationPrice: jest.fn(() => ({ liquidationPrice: '2500.00', @@ -62,398 +61,38 @@ jest.mock('../../hooks/usePerpsLiquidationPrice', () => ({ })), })); -// Mock TPSL form hook - provide direct implementation -jest.mock('../../hooks/usePerpsTPSLForm', () => ({ - __esModule: true, - usePerpsTPSLForm: jest.fn(() => ({ - formState: { - takeProfitPrice: '', - stopLossPrice: '', - takeProfitPercentage: '', - stopLossPercentage: '', - selectedTpPercentage: null, - selectedSlPercentage: null, - tpPriceInputFocused: false, - tpPercentInputFocused: false, - slPriceInputFocused: false, - slPercentInputFocused: false, - tpUsingPercentage: false, - slUsingPercentage: false, - }, - handlers: { - handleTakeProfitPriceChange: jest.fn(), - handleTakeProfitPercentageChange: jest.fn(), - handleStopLossPriceChange: jest.fn(), - handleStopLossPercentageChange: jest.fn(), - handleTakeProfitPriceFocus: jest.fn(), - handleTakeProfitPriceBlur: jest.fn(), - handleTakeProfitPercentageFocus: jest.fn(), - handleTakeProfitPercentageBlur: jest.fn(), - handleStopLossPriceFocus: jest.fn(), - handleStopLossPriceBlur: jest.fn(), - handleStopLossPercentageFocus: jest.fn(), - handleStopLossPercentageBlur: jest.fn(), - }, - buttons: { - handleTakeProfitPercentageButton: jest.fn(), - handleStopLossPercentageButton: jest.fn(), - handleTakeProfitOff: jest.fn(), - handleStopLossOff: jest.fn(), - }, - validation: { - isValid: true, - hasChanges: false, - takeProfitError: '', - stopLossError: '', - }, - display: { - formattedTakeProfitPercentage: '', - formattedStopLossPercentage: '', - }, - })), -})); - -// Get a reference to the mock function so we can modify it in tests -const mockUsePerpsTPSLForm = jest.requireMock( - '../../hooks/usePerpsTPSLForm', -).usePerpsTPSLForm; - -// Define the default mock return value -const defaultMockReturn = { - formState: { - takeProfitPrice: '', - stopLossPrice: '', - takeProfitPercentage: '', - stopLossPercentage: '', - selectedTpPercentage: null, - selectedSlPercentage: null, - tpPriceInputFocused: false, - tpPercentInputFocused: false, - slPriceInputFocused: false, - slPercentInputFocused: false, - tpUsingPercentage: false, - slUsingPercentage: false, - }, - handlers: { - handleTakeProfitPriceChange: jest.fn(), - handleTakeProfitPercentageChange: jest.fn(), - handleStopLossPriceChange: jest.fn(), - handleStopLossPercentageChange: jest.fn(), - handleTakeProfitPriceFocus: jest.fn(), - handleTakeProfitPriceBlur: jest.fn(), - handleTakeProfitPercentageFocus: jest.fn(), - handleTakeProfitPercentageBlur: jest.fn(), - handleStopLossPriceFocus: jest.fn(), - handleStopLossPriceBlur: jest.fn(), - handleStopLossPercentageFocus: jest.fn(), - handleStopLossPercentageBlur: jest.fn(), - }, - buttons: { - handleTakeProfitPercentageButton: jest.fn(), - handleStopLossPercentageButton: jest.fn(), - handleTakeProfitOff: jest.fn(), - handleStopLossOff: jest.fn(), - }, - validation: { - isValid: true, - hasChanges: false, - takeProfitError: '', - stopLossError: '', - stopLossLiquidationError: '', - }, - display: { - formattedTakeProfitPercentage: '', - formattedStopLossPercentage: '', - }, -}; - -// Mock stream hooks -jest.mock('../../hooks/stream', () => ({ - usePerpsLivePrices: jest.fn(() => ({})), // Return empty object for prices -})); - -// Mock format utilities jest.mock('../../utils/formatUtils', () => ({ formatPrice: jest.fn((value) => { const num = typeof value === 'string' ? parseFloat(value) : value; - return isNaN(num) ? '$0.00' : `$${num.toFixed(2)}`; + return isNaN(num) + ? '$0.00' + : `$${num.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; }), formatPerpsFiat: jest.fn((value) => { const num = typeof value === 'string' ? parseFloat(value) : value; - return isNaN(num) ? '$0.00' : `$${num.toFixed(2)}`; + return isNaN(num) + ? '$0.00' + : `$${num.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; }), - PRICE_RANGES_UNIVERSAL: [ - { - condition: (v: number) => v >= 1, - minimumDecimals: 2, - maximumDecimals: 2, - threshold: 1, - }, - { - condition: (v: number) => v < 1, - minimumDecimals: 2, - maximumDecimals: 7, - significantDigits: 4, - threshold: 0.0000001, - }, - ], + PRICE_RANGES_UNIVERSAL: {}, + PRICE_RANGES_MINIMAL_VIEW: {}, })); -// Mock strings -jest.mock('../../../../../../locales/i18n', () => ({ - strings: jest.fn((key) => key), +jest.mock('../../hooks/usePerpsTPSLForm', () => ({ + __esModule: true, + usePerpsTPSLForm: jest.fn(), })); -// Mock BottomSheet components from component library -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheet', - () => { - const { View } = jest.requireActual('react-native'); - const { forwardRef, useImperativeHandle } = jest.requireActual('react'); - const MockBottomSheet = forwardRef( - (props: { children: React.ReactNode }, ref: React.Ref) => { - useImperativeHandle(ref, () => ({ - onOpenBottomSheet: jest.fn(), - onCloseBottomSheet: jest.fn(), - })); - - return {props.children}; - }, - ); - - return { - __esModule: true, - default: MockBottomSheet, - }; - }, -); - -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheetHeader', - () => { - const { View } = jest.requireActual('react-native'); - return { - __esModule: true, - default: (props: { children: React.ReactNode; onClose?: () => void }) => ( - {props.children} - ), - }; - }, -); - -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; - isDisabled?: boolean; - loading?: boolean; - variant?: string; - size?: string; - }[]; - }) => ( - - {buttonPropsArray?.map((buttonProps, index) => ( - - - {buttonProps.loading ? 'Loading...' : buttonProps.label} - - - ))} - - ), - }; - }, -); - -// Mock component library Text component -jest.mock('../../../../../component-library/components/Texts/Text', () => { - const { Text } = jest.requireActual('react-native'); - return { - __esModule: true, - default: (props: { children: React.ReactNode }) => ( - {props.children} - ), - TextVariant: { - HeadingMD: 'HeadingMD', - BodyMD: 'BodyMD', - BodyLGMedium: 'BodyLGMedium', - BodySM: 'BodySM', - }, - TextColor: { - Default: 'Default', - Alternative: 'Alternative', - Error: 'Error', - Inverse: 'Inverse', - }, - }; -}); - -// Mock ButtonIcon component -jest.mock( - '../../../../../component-library/components/Buttons/ButtonIcon', - () => { - const { TouchableOpacity } = jest.requireActual('react-native'); - - const MockButtonIcon = ({ - onPress, - testID, - }: { - onPress?: () => void; - testID?: string; - }) => ; - - return { - __esModule: true, - default: MockButtonIcon, - ButtonIconSizes: { - Sm: 'Sm', - Md: 'Md', - Lg: 'Lg', - }, - }; - }, -); - -// Mock Button component and enums -jest.mock('../../../../../component-library/components/Buttons/Button', () => { - const { TouchableOpacity, Text } = jest.requireActual('react-native'); - - const MockButton = ({ - label, - onPress, - isDisabled, - loading, - style, - }: { - label: string; - onPress: () => void; - isDisabled?: boolean; - loading?: boolean; - style?: unknown; - }) => ( - - {loading ? 'Loading...' : label} - - ); - - return { - __esModule: true, - default: MockButton, - ButtonSize: { - Lg: 'Lg', - }, - ButtonVariants: { - Primary: 'Primary', - }, - ButtonWidthTypes: { - Full: 'Full', - }, - }; -}); - -// Mock Keypad component -jest.mock('../../../../../components/Base/Keypad', () => { - const { View, Text, TouchableOpacity } = jest.requireActual('react-native'); - - // Mock compound component structure - const MockKeypadRow = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - const MockKeypadButton = ({ - children, - onPress, - }: { - children: React.ReactNode; - onPress?: () => void; - }) => ( - - {children} - - ); - - const MockKeypadDeleteButton = ({ - onPress, - onLongPress, - testID, - }: { - onPress?: () => void; - onLongPress?: () => void; - testID?: string; - }) => ( - - Del - - ); - - const MockKeypad = ({ - value, - onChange, - currency, - decimals, - children, - }: { - value: string; - onChange: ({ value }: { value: string; valueAsNumber: number }) => void; - currency: string; - decimals: number; - children?: React.ReactNode; - }) => ( - - {value} - {currency} - {decimals} - onChange({ value: '123.45', valueAsNumber: 123.45 })} - > - Test Keypad Input - - {children} - - ); - - // Attach sub-components to main component - MockKeypad.Row = MockKeypadRow; - MockKeypad.Button = MockKeypadButton; - MockKeypad.DeleteButton = MockKeypadDeleteButton; - - return { - __esModule: true, - default: MockKeypad, - }; -}); - -// Mock Platform - moved to top level to avoid conflicts -const mockPlatform = { OS: 'ios' }; -jest.doMock('react-native', () => ({ - ...jest.requireActual('react-native'), - Platform: mockPlatform, -})); +const mockUsePerpsTPSLForm = jest.requireMock( + '../../hooks/usePerpsTPSLForm', +).usePerpsTPSLForm; -// Mock React Navigation const mockNavigate = jest.fn(); const mockGoBack = jest.fn(); const mockNavigation = { @@ -463,12 +102,6 @@ const mockNavigation = { addListener: jest.fn(), removeListener: jest.fn(), canGoBack: jest.fn(() => true), - dispatch: jest.fn(), - getParent: jest.fn(), - getState: jest.fn(), - isFocused: jest.fn(() => true), - reset: jest.fn(), - setParams: jest.fn(), }; let mockRouteParams: Record = {}; @@ -484,1226 +117,426 @@ jest.mock('@react-navigation/native', () => ({ useIsFocused: () => true, })); -// Mock styles jest.mock('./PerpsTPSLView.styles', () => ({ createStyles: () => ({ - container: { padding: 16 }, - priceDisplay: { backgroundColor: '#f0f0f0' }, - section: { marginBottom: 24 }, - sectionTitle: { marginBottom: 8 }, - inputRow: { flexDirection: 'row' }, - inputContainer: { flex: 1, borderWidth: 1 }, - inputContainerLeft: { marginRight: 4 }, - inputContainerRight: { marginLeft: 4 }, - inputContainerActive: { borderColor: 'blue' }, - inputContainerError: { borderColor: 'red' }, - input: { flex: 1 }, - percentageRow: { flexDirection: 'row' }, - percentageButton: { flex: 1 }, - percentageButtonActive: { backgroundColor: 'blue' }, - helperText: { marginTop: 4 }, - keypadContainer: { paddingHorizontal: 16, paddingVertical: 8 }, - scrollContent: { flex: 1 }, - doneButton: { - width: '100%', - marginBottom: 8, - }, + container: {}, + section: {}, + inputRow: {}, + keypadContainer: {}, }), })); +jest.mock('../../../../../../locales/i18n', () => ({ + strings: jest.fn((key) => key), +})); + describe('PerpsTPSLView', () => { const mockTheme = { colors: { background: { alternative: '#f0f0f0' }, - text: { default: '#000000', muted: '#666666' }, + text: { default: '#000', muted: '#666', alternative: '#888' }, border: { muted: '#e1e1e1' }, - primary: { default: '#0066cc' }, - error: { default: '#ff0000' }, + primary: { default: '#0376c9' }, + error: { default: '#d73847' }, }, }; - const defaultRouteParams = { - asset: 'ETH', - currentPrice: 3000, - direction: 'long' as const, - onConfirm: jest.fn(), + const defaultMockReturn = { + formState: { + takeProfitPrice: '', + stopLossPrice: '', + takeProfitPercentage: '', + stopLossPercentage: '', + selectedTpPercentage: null, + selectedSlPercentage: null, + tpPriceInputFocused: false, + tpPercentInputFocused: false, + slPriceInputFocused: false, + slPercentInputFocused: false, + tpUsingPercentage: false, + slUsingPercentage: false, + }, + handlers: { + handleTakeProfitPriceChange: jest.fn(), + handleTakeProfitPercentageChange: jest.fn(), + handleStopLossPriceChange: jest.fn(), + handleStopLossPercentageChange: jest.fn(), + handleTakeProfitPriceFocus: jest.fn(), + handleTakeProfitPriceBlur: jest.fn(), + handleTakeProfitPercentageFocus: jest.fn(), + handleTakeProfitPercentageBlur: jest.fn(), + handleStopLossPriceFocus: jest.fn(), + handleStopLossPriceBlur: jest.fn(), + handleStopLossPercentageFocus: jest.fn(), + handleStopLossPercentageBlur: jest.fn(), + }, + buttons: { + handleTakeProfitPercentageButton: jest.fn(), + handleStopLossPercentageButton: jest.fn(), + handleTakeProfitOff: jest.fn(), + handleStopLossOff: jest.fn(), + }, + validation: { + isValid: true, + hasChanges: false, + takeProfitError: '', + stopLossError: '', + stopLossLiquidationError: '', + }, + display: { + formattedTakeProfitPercentage: '', + formattedStopLossPercentage: '', + expectedTakeProfitPnL: '', + expectedStopLossPnL: '', + }, }; - const mockPosition: Position = { + const defaultRouteParams = { + currentPrice: '3000.00', coin: 'ETH', - size: '2.5', - entryPrice: '2800.00', - positionValue: '7000.00', - unrealizedPnl: '500.00', - marginUsed: '700.00', - leverage: { - type: 'isolated', - value: 10, - }, - liquidationPrice: '2500.00', - maxLeverage: 20, - returnOnEquity: '14.3', - cumulativeFunding: { - allTime: '15.00', - sinceOpen: '8.00', - sinceChange: '3.00', - }, - takeProfitCount: 0, - stopLossCount: 0, + direction: 'long', + onConfirm: jest.fn(), }; beforeEach(() => { + jest.clearAllMocks(); mockUseTheme.mockReturnValue(mockTheme); - // Reset the mock to default values mockUsePerpsTPSLForm.mockReturnValue(defaultMockReturn); - - // Reset route params to defaults mockRouteParams = { ...defaultRouteParams }; - - // Clear all mock calls - jest.clearAllMocks(); - mockNavigate.mockClear(); - mockGoBack.mockClear(); }); - describe('Component Rendering', () => { - it('renders correctly', () => { - // Act - render(); + // ==================== Test Helpers ==================== - // Assert - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.take_profit_long')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.stop_loss_long')).toBeOnTheScreen(); + const renderView = (overrides = {}) => { + mockUsePerpsTPSLForm.mockReturnValue({ + ...defaultMockReturn, + ...overrides, }); + return render(); + }; - it('displays current price for new orders', () => { - // Act - render(); + const getTakeProfitPriceInput = () => + screen.getAllByPlaceholderText('perps.tpsl.trigger_price_placeholder')[0]; + + const getTakeProfitPercentageInput = () => + screen.getByPlaceholderText('perps.tpsl.profit_roe_placeholder'); + + const getStopLossPriceInput = () => + screen.getAllByPlaceholderText('perps.tpsl.trigger_price_placeholder')[1]; + + const getStopLossPercentageInput = () => + screen.getByPlaceholderText('perps.tpsl.loss_roe_placeholder'); + + // ==================== User Interactions ==================== + + describe('User Interactions', () => { + it.each([ + [ + 'take profit price', + () => getTakeProfitPriceInput(), + 'handleTakeProfitPriceChange', + ], + [ + 'stop loss price', + () => getStopLossPriceInput(), + 'handleStopLossPriceChange', + ], + ])( + 'calls %s handler when user types', + (_description, getInput, handlerName) => { + const mockHandler = jest.fn(); + renderView({ + handlers: { + ...defaultMockReturn.handlers, + [handlerName]: mockHandler, + }, + }); + + fireEvent.changeText(getInput(), '123.45'); + + expect(mockHandler).toHaveBeenCalledWith('123.45'); + }, + ); + + it('calls take profit clear handler when Clear button pressed', () => { + const mockHandler = jest.fn(); + renderView({ + formState: { + ...defaultMockReturn.formState, + takeProfitPrice: '3150', + }, + buttons: { + ...defaultMockReturn.buttons, + handleTakeProfitOff: mockHandler, + }, + }); + + const clearButtons = screen.getAllByText('perps.tpsl.clear'); + fireEvent.press(clearButtons[0]); - // Assert - expect(screen.getByText('perps.tpsl.current_price')).toBeOnTheScreen(); - expect(screen.getByText('$3000.00')).toBeOnTheScreen(); + expect(mockHandler).toHaveBeenCalled(); }); - it('displays entry price when editing existing position', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - position: mockPosition, - currentPrice: undefined, - }; + it('does not show Clear button when no value is set', () => { + renderView({ + formState: { + ...defaultMockReturn.formState, + takeProfitPrice: '', + stopLossPrice: '', + }, + }); + + expect(screen.queryByText('perps.tpsl.clear')).toBeNull(); + }); + + it.each([ + [ + 'take profit price', + () => getTakeProfitPriceInput(), + 'handleTakeProfitPriceChange', + ], + [ + 'take profit percentage', + () => getTakeProfitPercentageInput(), + 'handleTakeProfitPercentageChange', + ], + [ + 'stop loss price', + () => getStopLossPriceInput(), + 'handleStopLossPriceChange', + ], + [ + 'stop loss percentage', + () => getStopLossPercentageInput(), + 'handleStopLossPercentageChange', + ], + ])( + 'prevents %s input exceeding 9 digits', + (_description, getInput, handlerName) => { + const mockHandler = jest.fn(); + renderView({ + handlers: { + ...defaultMockReturn.handlers, + [handlerName]: mockHandler, + }, + }); + + fireEvent.changeText(getInput(), '1234567890'); + + expect(mockHandler).not.toHaveBeenCalled(); + }, + ); + + it.each([ + [ + 'take profit price', + () => getTakeProfitPriceInput(), + 'handleTakeProfitPriceFocus', + 'handleTakeProfitPriceBlur', + ], + [ + 'take profit percentage', + () => getTakeProfitPercentageInput(), + 'handleTakeProfitPercentageFocus', + 'handleTakeProfitPercentageBlur', + ], + [ + 'stop loss price', + () => getStopLossPriceInput(), + 'handleStopLossPriceFocus', + 'handleStopLossPriceBlur', + ], + [ + 'stop loss percentage', + () => getStopLossPercentageInput(), + 'handleStopLossPercentageFocus', + 'handleStopLossPercentageBlur', + ], + ])( + 'handles focus and blur events for %s input', + (_description, getInput, focusHandler, blurHandler) => { + const mockFocusHandler = jest.fn(); + const mockBlurHandler = jest.fn(); + renderView({ + handlers: { + ...defaultMockReturn.handlers, + [focusHandler]: mockFocusHandler, + [blurHandler]: mockBlurHandler, + }, + }); + + fireEvent(getInput(), 'focus'); + fireEvent(getInput(), 'blur'); + + expect(mockFocusHandler).toHaveBeenCalled(); + expect(mockBlurHandler).toHaveBeenCalled(); + }, + ); + }); + + // ==================== Display Hook Data ==================== + + describe('Display Hook Data', () => { + it('displays validation errors from hook', () => { + renderView({ + validation: { + ...defaultMockReturn.validation, + isValid: false, + takeProfitError: 'perps.order.validation.take_profit_below_entry', + }, + }); + + expect( + screen.getByText('perps.order.validation.take_profit_below_entry'), + ).toBeOnTheScreen(); + }); - // Act - render(); + it('displays stop loss liquidation error for long positions', () => { + renderView({ + validation: { + ...defaultMockReturn.validation, + isValid: false, + stopLossLiquidationError: + 'perps.order.validation.stop_loss_liquidation_long', + }, + }); - // Assert - expect(screen.getByText('perps.tpsl.entry_price')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.current_price')).toBeOnTheScreen(); - expect(screen.getAllByText('$2800.00')).toHaveLength(2); + expect( + screen.getByText('perps.order.validation.stop_loss_liquidation_long'), + ).toBeOnTheScreen(); }); - it('renders percentage buttons with correct RoE values', () => { - // Act - render(); - - // Assert - Take Profit buttons (RoE percentages) - expect(screen.getByText('+10%')).toBeOnTheScreen(); - expect(screen.getByText('+25%')).toBeOnTheScreen(); - expect(screen.getByText('+50%')).toBeOnTheScreen(); - expect(screen.getByText('+100%')).toBeOnTheScreen(); - - // Assert - Stop Loss buttons (RoE percentages) - expect(screen.getByText('-5%')).toBeOnTheScreen(); - expect(screen.getByText('-10%')).toBeOnTheScreen(); - expect(screen.getByText('-25%')).toBeOnTheScreen(); - expect(screen.getByText('-50%')).toBeOnTheScreen(); + it('displays formatted prices from hook', () => { + renderView({ + formState: { + ...defaultMockReturn.formState, + takeProfitPrice: '$3,150.00', + stopLossPrice: '$2,850.00', + }, + }); + + expect(getTakeProfitPriceInput().props.value).toBe('$3,150.00'); + expect(getStopLossPriceInput().props.value).toBe('$2,850.00'); }); + }); - it('renders without crashing when position is provided', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - position: mockPosition, - }; + // ==================== Navigation and Actions ==================== - // Act - render(); + describe('Navigation and Actions', () => { + it('navigates back when back button pressed', () => { + renderView(); - // Assert - Component should render successfully with position - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); + const backButton = screen.getByTestId('back-button'); + fireEvent.press(backButton); + + expect(mockNavigation.goBack).toHaveBeenCalled(); }); - it('renders without crashing when leverage prop is provided', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - leverage: 5, - }; + it('calls onConfirm with hook values when Set button pressed', () => { + const mockOnConfirm = jest.fn(); + mockRouteParams = { ...defaultRouteParams, onConfirm: mockOnConfirm }; + renderView({ + formState: { + ...defaultMockReturn.formState, + takeProfitPrice: '$3,150.00', + stopLossPrice: '$2,850.00', + }, + }); - // Act - render(); + const setButton = screen.getByText('perps.tpsl.set'); + fireEvent.press(setButton); - // Assert - Component should render successfully with leverage prop - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); + expect(mockOnConfirm).toHaveBeenCalledWith('3150.00', '2850.00'); }); - it('renders without crashing when margin is provided', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - marginRequired: '500.00', - }; + it('calls onConfirm with undefined when values are empty', () => { + const mockOnConfirm = jest.fn(); + mockRouteParams = { ...defaultRouteParams, onConfirm: mockOnConfirm }; + renderView(); - // Act - render(); + const setButton = screen.getByText('perps.tpsl.set'); + fireEvent.press(setButton); - // Assert - Component should render successfully - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); + expect(mockOnConfirm).toHaveBeenCalledWith(undefined, undefined); }); - }); - describe('Initial Values', () => { - it('initializes with provided take profit and stop loss prices', () => { - // Arrange - Mock the form hook to return initial values - mockUsePerpsTPSLForm.mockImplementation(() => ({ - ...defaultMockReturn, + it('dismisses keypad before confirming when input is focused', async () => { + const mockOnConfirm = jest.fn().mockResolvedValue(undefined); + mockRouteParams = { ...defaultRouteParams, onConfirm: mockOnConfirm }; + renderView({ formState: { ...defaultMockReturn.formState, - takeProfitPrice: '$3300.00', - stopLossPrice: '$2700.00', + takeProfitPrice: '3150', }, validation: { ...defaultMockReturn.validation, hasChanges: true, }, - display: { - ...defaultMockReturn.display, - formattedTakeProfitPercentage: '10', - formattedStopLossPercentage: '10', - }, - })); + }); - mockRouteParams = { - ...defaultRouteParams, - initialTakeProfitPrice: '3300', - initialStopLossPrice: '2700', - }; + fireEvent(getTakeProfitPriceInput(), 'focus'); - // Act - render(); + const doneButton = screen.getByText('perps.tpsl.done'); + fireEvent.press(doneButton); - // Assert - const takeProfitInputs = screen.getAllByDisplayValue('$3300.00'); - const stopLossInputs = screen.getAllByDisplayValue('$2700.00'); - expect(takeProfitInputs.length).toBeGreaterThan(0); - expect(stopLossInputs.length).toBeGreaterThan(0); - }); + const setButton = screen.getByText('perps.tpsl.set'); + fireEvent.press(setButton); - it('passes initial prices to the form hook correctly', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - initialTakeProfitPrice: '3300', - initialStopLossPrice: '2700', - leverage: 10, - }; + expect(mockOnConfirm).toHaveBeenCalled(); + }); + }); - // Act - render(); + // ==================== Keypad Integration ==================== - // Assert - The hook should have been called with the component - // The initial prices are passed via route params, so the component should render - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); + describe('Keypad Integration', () => { + it('shows action buttons when keypad is not active', () => { + renderView(); - // The hook receives these initial values and the component displays them - // This tests that the component properly passes route params to the hook - expect(mockUsePerpsTPSLForm).toHaveBeenCalled(); + expect(screen.getByText('perps.tpsl.cancel')).toBeOnTheScreen(); + expect(screen.getByText('perps.tpsl.set')).toBeOnTheScreen(); }); }); - describe('Input Handling', () => { - it('calls price change handler when take profit price input changes', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceChange: mockHandler, - }, - }); + // ==================== Edge Cases ==================== - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); + describe('Edge Cases', () => { + it('displays entry price when editing existing position', () => { + const mockPosition: Position = { + coin: 'ETH', + entryPrice: '2800.00', + size: '0.5', + positionValue: '1400.00', + unrealizedPnl: '100.00', + marginUsed: '140.00', + leverage: { type: 'isolated', value: 10 }, + liquidationPrice: '2500.00', + maxLeverage: 50, + returnOnEquity: '0.71', + cumulativeFunding: { + allTime: '0.00', + sinceOpen: '0.00', + sinceChange: '0.00', + }, + takeProfitCount: 0, + stopLossCount: 0, + }; + mockRouteParams = { ...defaultRouteParams, position: mockPosition }; + renderView(); - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; // First input is TP price + expect(screen.getByText('$2,800.00')).toBeOnTheScreen(); + }); - // Act - fireEvent.changeText(takeProfitPriceInput, '3150'); + it('displays current price for new orders', () => { + renderView(); - // Assert - Handler should be called with the new value - expect(mockHandler).toHaveBeenCalledWith('3150'); + expect(screen.getByText('$3,000.00')).toBeOnTheScreen(); }); - it('calls handler when take profit price input changes', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceChange: mockHandler, - }, - }); - - render(); - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent.changeText(takeProfitPriceInput, '123.45'); - - // Assert - Handler should be called - expect(mockHandler).toHaveBeenCalledWith('123.45'); - }); - - it('prevents multiple decimal points in take profit price input', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceChange: mockHandler, - }, - }); - - render(); - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent.changeText(takeProfitPriceInput, '123.45.67'); - - // Assert - Handler should not be called for invalid input with multiple decimal points - // The component has logic to prevent more than 9 digits, but decimal validation is handled by the hook - expect(mockHandler).toHaveBeenCalledWith('123.45.67'); - }); - - it('calls percentage change handler when take profit RoE percentage input changes', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPercentageChange: mockHandler, - }, - }); - - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); - - const takeProfitPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.profit_roe_placeholder', - )[0]; // TP RoE percentage input - - // Act - fireEvent.changeText(takeProfitPercentInput, '25'); - - // Assert - Handler should be called with the new percentage value - expect(mockHandler).toHaveBeenCalledWith('25'); - }); - - it('calls stop loss change handler when stop loss price input changes', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleStopLossPriceChange: mockHandler, - }, - }); - - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); - - const stopLossPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[1]; // Second trigger price input is SL price - - // Act - fireEvent.changeText(stopLossPriceInput, '2700'); - - // Assert - Handler should be called with the new value - expect(mockHandler).toHaveBeenCalledWith('2700'); - }); - - it('calls stop loss percentage change handler when stop loss RoE percentage input changes', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleStopLossPercentageChange: mockHandler, - }, - }); - - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); - - const stopLossPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.loss_roe_placeholder', - )[0]; // SL RoE percentage input - - // Act - fireEvent.changeText(stopLossPercentInput, '25'); - - // Assert - Handler should be called with the new percentage value - expect(mockHandler).toHaveBeenCalledWith('25'); - }); - }); - - describe('RoE Percentage Button Functionality', () => { - it('calls button handler when RoE percentage button is pressed', () => { - // Arrange - const mockButtonHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - buttons: { - ...defaultMockReturn.buttons, - handleTakeProfitPercentageButton: mockButtonHandler, - }, - }); - - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); - const tenPercentButton = screen.getByText('+10%'); - - // Act - fireEvent.press(tenPercentButton); - - // Assert - Button handler should be called with percentage - expect(mockButtonHandler).toHaveBeenCalledWith(10); - }); - - it('calls stop loss button handler when RoE percentage button is pressed', () => { - // Arrange - const mockButtonHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - buttons: { - ...defaultMockReturn.buttons, - handleStopLossPercentageButton: mockButtonHandler, - }, - }); - - mockRouteParams = { ...defaultRouteParams, leverage: 10 }; - render(); - const fivePercentButton = screen.getByText('-5%'); - - // Act - fireEvent.press(fivePercentButton); - - // Assert - Button handler should be called with percentage - expect(mockButtonHandler).toHaveBeenCalledWith(-5); - }); - }); - - describe('Off Button Functionality', () => { - it('calls take profit off button handler when off button is pressed', () => { - // Arrange - const mockOffHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - buttons: { - ...defaultMockReturn.buttons, - handleTakeProfitOff: mockOffHandler, - }, - }); - - render(); - - // Get the take profit off button - there are two "Off" buttons, get all and find the first one - const offButtons = screen.getAllByText('perps.tpsl.off'); - const takeProfitOffButton = offButtons[0]; // First "Off" button is for take profit - - // Act - fireEvent.press(takeProfitOffButton); - - // Assert - Handler should be called - expect(mockOffHandler).toHaveBeenCalled(); - }); - - it('calls stop loss off button handler when off button is pressed', () => { - // Arrange - const mockOffHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - buttons: { - ...defaultMockReturn.buttons, - handleStopLossOff: mockOffHandler, - }, - }); - - render(); - - // Get the stop loss off button - there are two "Off" buttons, get all and find the second one - const offButtons = screen.getAllByText('perps.tpsl.off'); - const stopLossOffButton = offButtons[1]; // Second "Off" button is for stop loss - - // Act - fireEvent.press(stopLossOffButton); - - // Assert - Handler should be called - expect(mockOffHandler).toHaveBeenCalled(); - }); - - it('displays values from form state correctly', () => { - // Arrange - Mock state with values - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPrice: '3200', - takeProfitPercentage: '66.67', - }, - display: { - ...defaultMockReturn.display, - formattedTakeProfitPercentage: '66.67', - }, - }); - - render(); - - // Assert - Component should display the form state values - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - const takeProfitPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.profit_roe_placeholder', - )[0]; - - expect(takeProfitPriceInput.props.value).toBe('3200'); - expect(takeProfitPercentInput.props.value).toBe('66.67'); - }); - - it('displays stop loss values from form state correctly', () => { - // Arrange - Mock state with stop loss values - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - stopLossPrice: '2800', - stopLossPercentage: '66.67', - }, - display: { - ...defaultMockReturn.display, - formattedStopLossPercentage: '66.67', - }, - }); - - render(); - - // Assert - Component should display the form state values - const stopLossPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[1]; - const stopLossPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.loss_roe_placeholder', - )[0]; - - expect(stopLossPriceInput.props.value).toBe('2800'); - expect(stopLossPercentInput.props.value).toBe('66.67'); - }); - }); - - describe('Validation and Error States', () => { - it('can display validation errors when form validation has errors', () => { - // Arrange - Mock form state with validation errors - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - takeProfitError: 'perps.order.validation.invalid_take_profit', - }, - }); - - render(); - - // Assert - Component can handle validation errors - // (Note: The component might not display error messages directly, - // but it should render without crashing when there are validation errors) - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); - }); - - it('renders correctly when validation has errors', () => { - // Arrange - Mock form state with validation errors - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossError: 'perps.order.validation.invalid_stop_loss', - }, - }); - - render(); - - // Assert - Component renders correctly even with validation errors - const confirmButton = screen.getByText('perps.tpsl.done'); - expect(confirmButton).toBeOnTheScreen(); - - // Note: The actual button disable behavior depends on the component implementation - // This test ensures the component handles validation error state without crashing - }); - - it('displays stop loss liquidation error for long orders', () => { - // Arrange - Mock form state with stop loss liquidation error for long position - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossLiquidationError: - 'perps.order.validation.stop_loss_liquidation_long', - }, - }); - - mockRouteParams = { ...defaultRouteParams, direction: 'long' }; - render(); - - // Assert - Stop loss liquidation error should be displayed - expect( - screen.getByText('perps.order.validation.stop_loss_liquidation_long'), - ).toBeOnTheScreen(); - }); - - it('displays stop loss liquidation error for short orders', () => { - // Arrange - Mock form state with stop loss liquidation error for short position - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossLiquidationError: - 'perps.order.validation.stop_loss_liquidation_short', - }, - }); - - mockRouteParams = { ...defaultRouteParams, direction: 'short' }; - render(); - - // Assert - Stop loss liquidation error should be displayed - expect( - screen.getByText('perps.order.validation.stop_loss_liquidation_short'), - ).toBeOnTheScreen(); - }); - - it('displays stop loss error when both stopLossError and stopLossLiquidationError are present', () => { - // Arrange - Mock form state with both stop loss errors (stopLossError takes precedence in current implementation) - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossError: 'perps.order.validation.invalid_stop_loss', - stopLossLiquidationError: - 'perps.order.validation.stop_loss_liquidation_long', - }, - }); - - mockRouteParams = { ...defaultRouteParams, direction: 'long' }; - render(); - - // Assert - Stop loss error should be displayed (takes precedence in current implementation) - expect( - screen.getByText('perps.order.validation.invalid_stop_loss'), - ).toBeOnTheScreen(); - // Liquidation error should not be displayed when regular stop loss error is present - expect( - screen.queryByText('perps.order.validation.stop_loss_liquidation_long'), - ).toBeNull(); - }); - - it('displays regular stop loss error when only stopLossError is present', () => { - // Arrange - Mock form state with only regular stop loss error - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossError: 'perps.order.validation.invalid_stop_loss', - stopLossLiquidationError: '', // No liquidation error - }, - }); - - render(); - - // Assert - Regular stop loss error should be displayed - expect( - screen.getByText('perps.order.validation.invalid_stop_loss'), - ).toBeOnTheScreen(); - }); - - it('displays stop loss liquidation error when only stopLossLiquidationError is present', () => { - // Arrange - Mock form state with only liquidation error - mockUsePerpsTPSLForm.mockReturnValueOnce({ - ...defaultMockReturn, - validation: { - ...defaultMockReturn.validation, - isValid: false, - stopLossError: '', // No regular stop loss error - stopLossLiquidationError: - 'perps.order.validation.stop_loss_liquidation_long', - }, - }); - - mockRouteParams = { ...defaultRouteParams, direction: 'long' }; - render(); - - // Assert - Stop loss liquidation error should be displayed - expect( - screen.getByText('perps.order.validation.stop_loss_liquidation_long'), - ).toBeOnTheScreen(); - }); - }); - - describe('Focus and Blur Behavior', () => { - it('calls blur handler when input loses focus', () => { - // Arrange - const mockBlurHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceBlur: mockBlurHandler, - }, - }); - - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'blur'); - - // Assert - Blur handler should be called - expect(mockBlurHandler).toHaveBeenCalled(); - }); - }); - - describe('Confirm and Close Actions', () => { - it('calls onConfirm with parsed prices when confirmed', () => { - // Arrange - const mockOnConfirm = jest.fn(); - - // Mock form state to have values - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPrice: '$3,150.00', - stopLossPrice: '$2,850.00', - }, - }); - - mockRouteParams = { ...defaultRouteParams, onConfirm: mockOnConfirm }; - render(); - - const confirmButton = screen.getByText('perps.tpsl.done'); - - // Act - fireEvent.press(confirmButton); - - // Assert - Component should parse and clean the prices - expect(mockOnConfirm).toHaveBeenCalledWith('3150.00', '2850.00'); - }); - - it('calls onConfirm with undefined for empty values', () => { - // Arrange - const mockOnConfirm = jest.fn(); - mockRouteParams = { ...defaultRouteParams, onConfirm: mockOnConfirm }; - render(); - - const confirmButton = screen.getByText('perps.tpsl.done'); - - // Act - fireEvent.press(confirmButton); - - // Assert - expect(mockOnConfirm).toHaveBeenCalledWith(undefined, undefined); - }); - }); - - describe('Direction-based Logic', () => { - it('renders correctly for SHORT position direction', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - direction: 'short' as const, - }; - - // Act - render(); - - // Assert - Should display short-specific labels - expect( - screen.getByText('perps.tpsl.take_profit_short'), - ).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.stop_loss_short')).toBeOnTheScreen(); - }); - - it('renders RoE percentage buttons for both directions', () => { - // Assert - RoE buttons should always be present regardless of direction - mockRouteParams = { ...defaultRouteParams, direction: 'long' }; - render(); - expect(screen.getByText('+10%')).toBeOnTheScreen(); - expect(screen.getByText('-5%')).toBeOnTheScreen(); - }); - - it('renders RoE percentage buttons for short direction', () => { - // Assert - RoE buttons should always be present regardless of direction + it('renders for SHORT direction', () => { mockRouteParams = { ...defaultRouteParams, direction: 'short' }; - render(); - expect(screen.getByText('+10%')).toBeOnTheScreen(); - expect(screen.getByText('-5%')).toBeOnTheScreen(); - }); - }); - - describe('Edge Cases', () => { - it('handles missing currentPrice gracefully', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - currentPrice: undefined, - }; - - // Act & Assert - Should not crash - expect(() => render()).not.toThrow(); - }); - - it('handles missing direction gracefully', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - direction: undefined, - }; - - // Act & Assert - Should not crash - expect(() => render()).not.toThrow(); - }); - - it('displays currentPrice when provided with position', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - position: mockPosition, - currentPrice: 3200, // Live price should be displayed - }; - - // Act - render(); - - // Assert - Should display current price (live price) - expect(screen.getByText('$3200.00')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.current_price')).toBeOnTheScreen(); - }); - - it('displays entry price when currentPrice not provided', () => { - // Arrange - mockRouteParams = { - ...defaultRouteParams, - position: mockPosition, - currentPrice: undefined, // No current price provided - should fall back to entry price - }; - - // Act - render(); - - // Assert - Should display position entry price as fallback - expect(screen.getAllByText('$2800.00')).toHaveLength(2); - expect(screen.getByText('perps.tpsl.entry_price')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.current_price')).toBeOnTheScreen(); - }); - }); - - describe('Keypad Functionality', () => { - beforeEach(() => { - // Platform is already mocked at the top level - mockPlatform.OS = 'ios'; - }); - - it('shows keypad when take profit price input is focused', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPrice: '3200', - }, - }); - - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - expect(screen.getByTestId('keypad-value')).toHaveTextContent('3200'); - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - - it('shows keypad when take profit percentage input is focused', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPercentage: '25.50', - }, - display: { - ...defaultMockReturn.display, - formattedTakeProfitPercentage: '25.50', - }, - }); - - render(); - - const takeProfitPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.profit_roe_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPercentInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - expect(screen.getByTestId('keypad-value')).toHaveTextContent('25.50'); - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - - it('shows keypad when stop loss price input is focused', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - stopLossPrice: '2800', - }, - }); - - render(); - - const stopLossPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[1]; - - // Act - fireEvent(stopLossPriceInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - expect(screen.getByTestId('keypad-value')).toHaveTextContent('2800'); - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - - it('shows keypad when stop loss percentage input is focused', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - stopLossPercentage: '15.75', - }, - display: { - ...defaultMockReturn.display, - formattedStopLossPercentage: '15.75', - }, - }); - - render(); - - const stopLossPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.loss_roe_placeholder', - )[0]; - - // Act - fireEvent(stopLossPercentInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - expect(screen.getByTestId('keypad-value')).toHaveTextContent('15.75'); - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - - it('hides keypad when input loses focus', () => { - // Arrange - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - - fireEvent(takeProfitPriceInput, 'blur'); - - // Assert - expect(screen.queryByTestId('keypad')).toBeNull(); - }); - - it('calls appropriate handler when keypad value changes for take profit price', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceChange: mockHandler, - }, - }); - - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - const keypadButton = screen.getByTestId('keypad-test-button'); - fireEvent.press(keypadButton); - - // Assert - expect(mockHandler).toHaveBeenCalledWith('123.45'); - }); - - it('calls appropriate handler when keypad value changes for take profit percentage', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPercentageChange: mockHandler, - }, - }); - - render(); - - const takeProfitPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.profit_roe_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPercentInput, 'focus'); - const keypadButton = screen.getByTestId('keypad-test-button'); - fireEvent.press(keypadButton); - - // Assert - expect(mockHandler).toHaveBeenCalledWith('123.45'); - }); - - it('calls appropriate handler when keypad value changes for stop loss price', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleStopLossPriceChange: mockHandler, - }, - }); - - render(); - - const stopLossPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[1]; - - // Act - fireEvent(stopLossPriceInput, 'focus'); - const keypadButton = screen.getByTestId('keypad-test-button'); - fireEvent.press(keypadButton); - - // Assert - expect(mockHandler).toHaveBeenCalledWith('123.45'); - }); - - it('calls appropriate handler when keypad value changes for stop loss percentage', () => { - // Arrange - const mockHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleStopLossPercentageChange: mockHandler, - }, - }); - - render(); - - const stopLossPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.loss_roe_placeholder', - )[0]; - - // Act - fireEvent(stopLossPercentInput, 'focus'); - const keypadButton = screen.getByTestId('keypad-test-button'); - fireEvent.press(keypadButton); - - // Assert - expect(mockHandler).toHaveBeenCalledWith('123.45'); - }); - - it('verifies keypad is shown when input is focused', () => { - // Arrange - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - - // Assert - Keypad should be visible when input is focused - expect(screen.getByTestId('keypad')).toBeOnTheScreen(); - expect(screen.getByText('perps.tpsl.done')).toBeOnTheScreen(); - }); - - it('calls both original blur handler and custom blur handler when input loses focus', () => { - // Arrange - const mockOriginalBlurHandler = jest.fn(); - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - handlers: { - ...defaultMockReturn.handlers, - handleTakeProfitPriceBlur: mockOriginalBlurHandler, - }, - }); - - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - fireEvent(takeProfitPriceInput, 'blur'); - - // Assert - expect(mockOriginalBlurHandler).toHaveBeenCalled(); - expect(screen.queryByTestId('keypad')).toBeNull(); - }); - - it('configures keypad with correct currency and decimals for price inputs', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPrice: '3200.12345', - }, - }); - - render(); - - const takeProfitPriceInput = screen.getAllByPlaceholderText( - 'perps.tpsl.trigger_price_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPriceInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - - it('configures keypad with correct currency and decimals for all inputs', () => { - // Arrange - mockUsePerpsTPSLForm.mockReturnValue({ - ...defaultMockReturn, - formState: { - ...defaultMockReturn.formState, - takeProfitPercentage: '25.50', - }, - display: { - ...defaultMockReturn.display, - formattedTakeProfitPercentage: '25.50', - }, - }); - - render(); - - const takeProfitPercentInput = screen.getAllByPlaceholderText( - 'perps.tpsl.profit_roe_placeholder', - )[0]; - - // Act - fireEvent(takeProfitPercentInput, 'focus'); - - // Assert - expect(screen.getByTestId('keypad-currency')).toHaveTextContent( - 'USD_PERPS', - ); - expect(screen.getByTestId('keypad-decimals')).toHaveTextContent('5'); - }); - }); - - describe('Platform-specific Styling', () => { - it('renders correctly on iOS', () => { - // Arrange - mockPlatform.OS = 'ios'; - - render(); - - // Assert - Component should render without issues on iOS - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); - }); - - it('renders correctly on Android', () => { - // Arrange - mockPlatform.OS = 'android'; - - render(); + renderView(); - // Assert - Component should render without issues on Android - expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); + expect(screen.getByTestId('back-button')).toBeOnTheScreen(); }); }); }); diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx index b880a5e1a570..9a00099fb737 100644 --- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx @@ -46,33 +46,12 @@ import { createStyles } from './PerpsTPSLView.styles'; import { formatPerpsFiat, PRICE_RANGES_UNIVERSAL, + PRICE_RANGES_MINIMAL_VIEW, } from '../../utils/formatUtils'; -import { TP_SL_VIEW_CONFIG } from '../../constants/perpsConfig'; -import type { Position } from '../../controllers/types'; - -// Helper function to calculate effective entry price -const calculateEffectiveEntryPrice = ( - position: Position | undefined, - orderType: 'market' | 'limit' | undefined, - limitPrice: string | undefined, - spotPrice: number, - livePrice: number | undefined, - initialCurrentPrice: number | undefined, -): number => { - const hasPositionEntry = position?.entryPrice - ? parseFloat(position.entryPrice) - : 0; - const limitPriceValue = - orderType === 'limit' && limitPrice && parseFloat(limitPrice) > 0 - ? parseFloat(limitPrice) - : 0; - const fallbackPrice = - spotPrice > 0 ? spotPrice : livePrice || initialCurrentPrice || 0; - // Use proper precedence checking instead of || operator to avoid treating 0 as falsy - if (hasPositionEntry > 0) return hasPositionEntry; - if (limitPriceValue > 0) return limitPriceValue; - return fallbackPrice; -}; +import { + TP_SL_VIEW_CONFIG, + PERPS_CONSTANTS, +} from '../../constants/perpsConfig'; const PerpsTPSLView: React.FC = () => { const navigation = useNavigation(); @@ -89,6 +68,8 @@ const PerpsTPSLView: React.FC = () => { leverage: propLeverage, orderType, limitPrice, + amount, + szDecimals, onConfirm, } = route.params; @@ -133,22 +114,28 @@ const PerpsTPSLView: React.FC = () => { // For limit orders, use the limit price as entry price if available // For market orders or when limit price is not set, use spot price // Ensure we always have a valid price > 0 for calculations - const effectiveEntryPrice = calculateEffectiveEntryPrice( - position, - orderType, - limitPrice, - spotPrice, - livePrice, - initialCurrentPrice, - ); + let effectiveEntryPrice: number; + if (position?.entryPrice) { + effectiveEntryPrice = parseFloat(position.entryPrice); + } else if ( + orderType === 'limit' && + limitPrice && + parseFloat(limitPrice) > 0 + ) { + effectiveEntryPrice = parseFloat(limitPrice); + } else if (spotPrice > 0) { + effectiveEntryPrice = spotPrice; + } else { + effectiveEntryPrice = livePrice || initialCurrentPrice || 0; + } // Determine direction for tracking events - const actualDirection = (() => { - if (position) { - return parseFloat(position.size) > 0 ? 'long' : 'short'; - } - return direction; - })(); + let actualDirection: 'long' | 'short'; + if (position) { + actualDirection = parseFloat(position.size) > 0 ? 'long' : 'short'; + } else { + actualDirection = direction || 'long'; + } // Calculate liquidation price for new orders (when there's no existing position) const shouldCalculateLiquidation = @@ -179,15 +166,12 @@ const PerpsTPSLView: React.FC = () => { isVisible: true, liquidationPrice: displayLiquidationPrice, orderType, + amount, + szDecimals, }); // Extract form state and handlers for easier access - const { - takeProfitPrice, - stopLossPrice, - selectedTpPercentage, - selectedSlPercentage, - } = tpslForm.formState; + const { takeProfitPrice, stopLossPrice } = tpslForm.formState; const { handleTakeProfitPriceChange, @@ -218,8 +202,12 @@ const PerpsTPSLView: React.FC = () => { stopLossError, stopLossLiquidationError, } = tpslForm.validation; - const { formattedTakeProfitPercentage, formattedStopLossPercentage } = - tpslForm.display; + const { + formattedTakeProfitPercentage, + formattedStopLossPercentage, + expectedTakeProfitPnL, + expectedStopLossPnL, + } = tpslForm.display; usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, @@ -269,6 +257,28 @@ const PerpsTPSLView: React.FC = () => { (inputType: string) => { setFocusedInput(inputType); + // Auto-scroll to keep input visible when keypad is active + if (scrollViewRef.current) { + let yOffset = 0; + + // Calculate scroll position based on which input is focused + switch (inputType) { + case 'takeProfitPrice': + case 'takeProfitPercentage': + yOffset = 150; // Take Profit section + break; + case 'stopLossPrice': + case 'stopLossPercentage': + yOffset = 350; // Stop Loss section + break; + } + + scrollViewRef.current.scrollTo({ + y: yOffset, + animated: true, + }); + } + // Call the appropriate original focus handler switch (inputType) { case 'takeProfitPrice': @@ -365,16 +375,20 @@ const PerpsTPSLView: React.FC = () => { {/* Simple header with back button and title */} - - - {strings('perps.tpsl.title')} - + + + + + + {strings('perps.tpsl.title')} + + { showsVerticalScrollIndicator={false} > - {/* Description text */} - {!focusedInput && ( - - {strings('perps.tpsl.description')} - - )} - {/* Current price and liquidation price info */} { ? formatPerpsFiat(position.entryPrice, { ranges: PRICE_RANGES_UNIVERSAL, }) - : '--'} + : PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY} )} @@ -436,7 +439,7 @@ const PerpsTPSLView: React.FC = () => { ? formatPerpsFiat(currentPrice, { ranges: PRICE_RANGES_UNIVERSAL, }) - : '--'} + : PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY} @@ -451,45 +454,38 @@ const PerpsTPSLView: React.FC = () => { ? formatPerpsFiat(displayLiquidationPrice, { ranges: PRICE_RANGES_UNIVERSAL, }) - : '--'} + : PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY} {/* Take Profit Section */} - - {actualDirection === 'short' - ? strings('perps.tpsl.take_profit_short') - : strings('perps.tpsl.take_profit_long')} - + {/* Section title row with Clear button */} + + + {actualDirection === 'short' + ? strings('perps.tpsl.take_profit_short') + : strings('perps.tpsl.take_profit_long')} + + {Boolean(takeProfitPrice) && ( + + + {strings('perps.tpsl.clear')} + + + )} + {/* Percentage buttons */} - - - {strings('perps.tpsl.off')} - - {TP_SL_VIEW_CONFIG.TAKE_PROFIT_ROE_PRESETS.map((percentage) => ( handleTakeProfitPercentageButton(percentage)} testID={getPerpsTPSLViewSelector.takeProfitPercentageButton( percentage, @@ -517,6 +513,12 @@ const PerpsTPSLView: React.FC = () => { !isValid && takeProfitError && styles.inputError, ]} > + + {strings('perps.tpsl.usd_label')} + { selectionColor={colors.primary.default} cursorColor={colors.primary.default} /> - - {strings('perps.tpsl.usd_label')} - {/* RoE Percentage Input */} @@ -587,6 +583,44 @@ const PerpsTPSLView: React.FC = () => { + {/* Expected Profit/Loss for Take Profit */} + {Boolean(takeProfitPrice) && + expectedTakeProfitPnL !== undefined && ( + + {expectedTakeProfitPnL >= 0 + ? strings('perps.tpsl.expected_profit', { + amount: formatPerpsFiat( + Math.abs(expectedTakeProfitPnL), + { + ranges: PRICE_RANGES_MINIMAL_VIEW, + }, + ), + }) + : strings('perps.tpsl.expected_loss', { + amount: formatPerpsFiat( + Math.abs(expectedTakeProfitPnL), + { + ranges: PRICE_RANGES_MINIMAL_VIEW, + }, + ), + })} + + )} + {Boolean(takeProfitPrice) && + expectedTakeProfitPnL === undefined && ( + + {PERPS_CONSTANTS.FALLBACK_DATA_DISPLAY} + + )} + {/* Error message */} {!isValid && Boolean(takeProfitError) && ( @@ -597,38 +631,31 @@ const PerpsTPSLView: React.FC = () => { {/* Stop Loss Section */} - - {actualDirection === 'short' - ? strings('perps.tpsl.stop_loss_short') - : strings('perps.tpsl.stop_loss_long')} - + {/* Section title row with Clear button */} + + + {actualDirection === 'short' + ? strings('perps.tpsl.stop_loss_short') + : strings('perps.tpsl.stop_loss_long')} + + {Boolean(stopLossPrice) && ( + + + {strings('perps.tpsl.clear')} + + + )} + {/* Percentage buttons */} - - - {strings('perps.tpsl.off')} - - {TP_SL_VIEW_CONFIG.STOP_LOSS_ROE_PRESETS.map((percentage) => ( handleStopLossPercentageButton(percentage)} testID={getPerpsTPSLViewSelector.stopLossPercentageButton( percentage, @@ -656,6 +683,12 @@ const PerpsTPSLView: React.FC = () => { !isValid && stopLossError && styles.inputError, ]} > + + {strings('perps.tpsl.usd_label')} + { selectionColor={colors.primary.default} cursorColor={colors.primary.default} /> - - {strings('perps.tpsl.usd_label')} - {/* Percentage Input */} @@ -726,6 +753,36 @@ const PerpsTPSLView: React.FC = () => { + {/* Expected Profit/Loss for Stop Loss */} + {Boolean(stopLossPrice) && expectedStopLossPnL !== undefined && ( + + {expectedStopLossPnL >= 0 + ? strings('perps.tpsl.expected_profit', { + amount: formatPerpsFiat(Math.abs(expectedStopLossPnL), { + ranges: PRICE_RANGES_MINIMAL_VIEW, + }), + }) + : strings('perps.tpsl.expected_loss', { + amount: formatPerpsFiat(Math.abs(expectedStopLossPnL), { + ranges: PRICE_RANGES_MINIMAL_VIEW, + }), + })} + + )} + {Boolean(stopLossPrice) && expectedStopLossPnL === undefined && ( + + {PERPS_CONSTANTS.FALLBACK_DATA_DISPLAY} + + )} + {/* Error message */} {!isValid && Boolean(stopLossError || stopLossLiquidationError) && ( @@ -744,9 +801,8 @@ const PerpsTPSLView: React.FC = () => { label={strings('perps.tpsl.done')} variant={ButtonVariants.Primary} size={ButtonSize.Lg} - onPress={handleConfirm} - isDisabled={confirmDisabled} - loading={isUpdating} + width={ButtonWidthTypes.Full} + onPress={dismissKeypad} /> { ) : ( -