From de7dc7301bd5fcc1bceafd8fef03badd3610ce2f Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:31:35 +0800 Subject: [PATCH 1/2] chore: fix --- .../ContractDetails/contract-details.tsx | 27 ++++++++++++------- .../trader/src/AppV2/Hooks/useOrderDetails.ts | 10 ++++--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/trader/src/AppV2/Containers/ContractDetails/contract-details.tsx b/packages/trader/src/AppV2/Containers/ContractDetails/contract-details.tsx index 5b6edb8a9378..8a4185c546dc 100644 --- a/packages/trader/src/AppV2/Containers/ContractDetails/contract-details.tsx +++ b/packages/trader/src/AppV2/Containers/ContractDetails/contract-details.tsx @@ -19,6 +19,8 @@ import { isValidToCancel, WS, TContractStore, + isValidToSell, + hasContractEntered, } from '@deriv/shared'; import classNames from 'classnames'; import ContractDetailsFooter from 'AppV2/Components/ContractDetailsFooter'; @@ -31,7 +33,7 @@ const ContractDetails = observer(() => { const { common } = useStore(); const { server_time } = common; const { is_take_profit_visible, is_stop_loss_visible } = getContractDetailsConfig(contract_type ?? ''); - + const is_valid_to_sell = isValidToSell(contract_info); type TContractUpdateHistory = TContractStore['contract_update_history']; type TResponse = { contract_update_history: TContractUpdateHistory; @@ -55,14 +57,17 @@ const ContractDetails = observer(() => { if (is_loading) return <>; const is_multiplier = isMultiplierContract(contract_info.contract_type); + const is_valid_to_cancel = isValidToCancel(contract_info); const should_show_sell = - (hasContractStarted(contract_info) || + (hasContractEntered(contract_info) || isForwardStarting(contract_info?.shortcode ?? '', contract_info.purchase_time)) && isOpen(contract_info); - const { is_tp_history_visible } = getContractDetailsConfig(contract_info.contract_type ?? ''); + const { is_tp_history_visible, is_deal_cancellation_visible } = getContractDetailsConfig( + contract_info.contract_type ?? '' + ); const show_cancel_button = is_multiplier && is_valid_to_cancel; - + console.log(is_valid_to_sell); return (
{
- {isOpen(contract_info) && (is_take_profit_visible || is_stop_loss_visible) && ( - - - - - )} + {isOpen(contract_info) && + (is_take_profit_visible || is_stop_loss_visible) && + (is_valid_to_sell || is_deal_cancellation_visible) && ( + + + + + )} diff --git a/packages/trader/src/AppV2/Hooks/useOrderDetails.ts b/packages/trader/src/AppV2/Hooks/useOrderDetails.ts index e5486eacdc5f..3ea9b2a694b4 100644 --- a/packages/trader/src/AppV2/Hooks/useOrderDetails.ts +++ b/packages/trader/src/AppV2/Hooks/useOrderDetails.ts @@ -9,7 +9,7 @@ import { isAccumulatorContract, isResetContract, } from '@deriv/shared'; -import { Localize, localize } from '@deriv/translations'; +import { localize } from '@deriv/translations'; import { getBarrierValue } from 'App/Components/Elements/PositionsDrawer/helpers'; // Contains all key values that are used more than once in different transform objects @@ -95,8 +95,12 @@ const transformAccumulatorData = (data: TContractInfo) => { const commonFields = getCommonFields(data); return { [localize('Reference ID')]: commonFields[localize('Reference ID')], + ...{ + ...((data.is_expired || data.is_sold) && { + [localize('Duration')]: commonFields[localize('Duration')], + }), + }, [localize('Growth rate')]: data.growth_rate ? `${getGrowthRatePercentage(data.growth_rate)}%` : '', - [localize('Duration')]: `${getDurationTime(data) ?? ''} ${getDurationUnitText(getDurationPeriod(data)) ?? ''}`, [localize('Stake')]: commonFields[localize('Stake')], ...{ ...(data.limit_order?.take_profit && { @@ -136,6 +140,7 @@ const transformFunctionMap: Record Record Record { const contractInfo = contract_info; - if (!contractInfo.contract_type) return; const transformFunction = transformFunctionMap[contractInfo.contract_type]; const details = transformFunction ? transformFunction(contractInfo) : {}; From a22d71d0b0b5898b46818e55289fff6b0f52fba0 Mon Sep 17 00:00:00 2001 From: ahmadtaimoor-deriv <129935294+ahmadtaimoor-deriv@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:13:50 +0800 Subject: [PATCH 2/2] chore: test cases --- .../contract-details-footer.spec.tsx | 179 ++++++++++++++++++ .../EntryExitDetails/entry-exit-details.tsx | 101 ++++------ .../__tests__/payout-info-model.spec.tsx | 48 +++++ .../risk-management-info-modal.spec.tsx | 84 ++++++++ .../risk-management-item.tsx | 31 +-- .../__tests__/chart-placeholder.spec.tsx | 12 ++ .../__tests__/useContractDetails.spec.tsx | 90 +++++++++ .../Hooks/__tests__/useOrderDetails.spec.tsx | 158 ++++++++++++++++ .../src/AppV2/Utils/__tests__/helper.spec.ts | 80 ++++++++ .../trader/src/AppV2/__tests__/app.spec.tsx | 29 +-- 10 files changed, 713 insertions(+), 99 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/ContractDetailsFooter/__tests__/contract-details-footer.spec.tsx create mode 100644 packages/trader/src/AppV2/Components/PayoutInfoModal/__tests__/payout-info-model.spec.tsx create mode 100644 packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx create mode 100644 packages/trader/src/AppV2/Containers/Chart/__tests__/chart-placeholder.spec.tsx create mode 100644 packages/trader/src/AppV2/Hooks/__tests__/useContractDetails.spec.tsx create mode 100644 packages/trader/src/AppV2/Hooks/__tests__/useOrderDetails.spec.tsx create mode 100644 packages/trader/src/AppV2/Utils/__tests__/helper.spec.ts diff --git a/packages/trader/src/AppV2/Components/ContractDetailsFooter/__tests__/contract-details-footer.spec.tsx b/packages/trader/src/AppV2/Components/ContractDetailsFooter/__tests__/contract-details-footer.spec.tsx new file mode 100644 index 000000000000..ded94b69f15b --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractDetailsFooter/__tests__/contract-details-footer.spec.tsx @@ -0,0 +1,179 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +userEvent; +import '@testing-library/jest-dom'; +import { useStore } from '@deriv/stores'; +import { getCardLabels, isValidToSell, isValidToCancel, isMultiplierContract } from '@deriv/shared'; +import ContractDetailsFooter from '../contract-details-footer'; +import { getRemainingTime } from 'AppV2/Utils/helper'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@deriv/stores', () => ({ + useStore: jest.fn(), +})); +jest.mock('@deriv/shared', () => ({ + getCardLabels: jest.fn(), + isValidToSell: jest.fn(), + isValidToCancel: jest.fn(), + isMultiplierContract: jest.fn(), +})); +jest.mock('AppV2/Utils/helper', () => ({ + getRemainingTime: jest.fn(), +})); + +const mockContractInfo = { + bid_price: 100, + currency: 'USD', + contract_id: 1, + profit: 10, + contract_type: 'non-multiplier', +}; + +describe('ContractDetailsFooter', () => { + let mockStore: any; + + beforeEach(() => { + mockStore = { + contract_replay: { + onClickCancel: jest.fn(), + onClickSell: jest.fn(), + is_sell_requested: false, + }, + common: { + server_time: new Date(), + }, + }; + + (useStore as jest.Mock).mockImplementation(() => mockStore); + (getCardLabels as jest.Mock).mockImplementation(() => ({ + CLOSE: 'Close', + CANCEL: 'Cancel', + RESALE_NOT_OFFERED: 'Resale not offered', + })); + jest.clearAllMocks(); + }); + + it('should render close button with bid price and currency for non-multiplier contracts', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => false); + (isValidToSell as jest.Mock).mockImplementation(() => true); + + render(); + + const closeButton = screen.getByRole('button', { name: /close @ 100 usd/i }); + expect(closeButton).toBeInTheDocument(); + expect(closeButton).toBeEnabled(); + }); + + it('should render resale not offered message for non-valid sell contracts', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => false); + (isValidToSell as jest.Mock).mockImplementation(() => false); + + render(); + + const resaleMessage = screen.getByText(/resale not offered/i); + expect(resaleMessage).toBeInTheDocument(); + }); + + it('should render close and cancel buttons for multiplier contracts', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => true); + (isValidToSell as jest.Mock).mockImplementation(() => true); + (isValidToCancel as jest.Mock).mockImplementation(() => true); + (getRemainingTime as jest.Mock).mockImplementation(() => '10:00'); + + render( + + ); + + const closeButton = screen.getByRole('button', { name: /close/i }); + const cancelButton = screen.getByRole('button', { name: /cancel 10:00/i }); + + expect(closeButton).toBeInTheDocument(); + expect(cancelButton).toBeInTheDocument(); + }); + + it('should call onClickSell when close button is clicked', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => false); + (isValidToSell as jest.Mock).mockImplementation(() => true); + + render(); + + const closeButton = screen.getByRole('button', { name: /close @ 100 usd/i }); + + userEvent.click(closeButton); + + expect(mockStore.contract_replay.onClickSell).toHaveBeenCalledWith(1); + }); + + it('should disable close button for multiplier contract when profit is negative and valid to cancel', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => true); + (isValidToSell as jest.Mock).mockImplementation(() => true); + (isValidToCancel as jest.Mock).mockImplementation(() => true); + + render( + + ); + + const closeButton = screen.getByRole('button', { name: /close/i }); + expect(closeButton).toBeDisabled(); + }); + + it('should disable cancel button when profit is non-negative', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => true); + (isValidToCancel as jest.Mock).mockImplementation(() => true); + (getRemainingTime as jest.Mock).mockImplementation(() => '10:00'); + + render( + + ); + + const cancelButton = screen.getByRole('button', { name: /cancel 10:00/i }); + expect(cancelButton).toBeDisabled(); + }); + + it('should not render cancel button if not valid to cancel', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => true); + (isValidToCancel as jest.Mock).mockImplementation(() => false); + + render(); + + const cancelButton = screen.queryByRole('button', { name: /cancel/i }); + expect(cancelButton).not.toBeInTheDocument(); + }); + + it('should not call onClickSell if not valid to sell', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => false); + (isValidToSell as jest.Mock).mockImplementation(() => false); + + render(); + + const closeButton = screen.queryByRole('button', { name: /close @ 100 usd/i }); + if (closeButton) { + userEvent.click(closeButton); + } + + expect(mockStore.contract_replay.onClickSell).not.toHaveBeenCalled(); + }); + + it('should render correct button label for non-multiplier contract when not valid to sell', () => { + (isMultiplierContract as jest.Mock).mockImplementation(() => false); + (isValidToSell as jest.Mock).mockImplementation(() => false); + + render(); + + const resaleMessage = screen.getByRole('button', { name: /resale not offered/i }); + expect(resaleMessage).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/EntryExitDetails/entry-exit-details.tsx b/packages/trader/src/AppV2/Components/EntryExitDetails/entry-exit-details.tsx index 44e1784d2823..b125b68a8331 100644 --- a/packages/trader/src/AppV2/Components/EntryExitDetails/entry-exit-details.tsx +++ b/packages/trader/src/AppV2/Components/EntryExitDetails/entry-exit-details.tsx @@ -1,6 +1,6 @@ import { Text, CaptionText } from '@deriv-com/quill-ui'; import CardWrapper from '../CardWrapper'; -import React from 'react'; +import React, { useMemo } from 'react'; import { TContractInfo, addComma, getEndTime } from '@deriv/shared'; const getDateTimeFromEpoch = (epoch: number) => { @@ -13,78 +13,49 @@ const getDateTimeFromEpoch = (epoch: number) => { }; }; +const DateTimeRow = ({ label, value, date, time }: { label: string; value?: string; date: string; time: string }) => ( +
+
+ + {label} + +
+
+ {value} + + {date} + + {time} +
+
+); + const EntryExitDetails = ({ contract_info }: { contract_info: TContractInfo }) => { const { entry_tick_time, entry_spot_display_value, exit_tick_time, date_start, exit_tick_display_value } = contract_info; - const entryDisplayValue = entry_spot_display_value ? addComma(entry_spot_display_value) : null; - const entrySpotDateTime = entry_tick_time ? getDateTimeFromEpoch(entry_tick_time) : null; - const exitSpotDateTime = exit_tick_time ? getDateTimeFromEpoch(exit_tick_time) : null; - const startDateTime = date_start ? getDateTimeFromEpoch(date_start) : null; - const exitSpot = exit_tick_display_value ? addComma(exit_tick_display_value) : null; - const endTime = getEndTime(contract_info); - const exitDateTime = endTime ? getDateTimeFromEpoch(endTime) : null; + + const dateTimes = useMemo( + () => ({ + entry: entry_tick_time && getDateTimeFromEpoch(entry_tick_time), + exit: exit_tick_time && getDateTimeFromEpoch(exit_tick_time), + start: date_start && getDateTimeFromEpoch(date_start), + end: getEndTime(contract_info) && getDateTimeFromEpoch(getEndTime(contract_info)), + }), + [contract_info] + ); + + const entryValue = entry_spot_display_value ? addComma(entry_spot_display_value) : null; + const exitValue = exit_tick_display_value ? addComma(exit_tick_display_value) : null; return (
- {startDateTime && ( -
-
- - Start time - -
-
- {startDateTime.date} - {startDateTime.time} -
-
- )} - {entrySpotDateTime && entryDisplayValue && ( -
-
- - Entry spot - -
-
- {entryDisplayValue} - - {entrySpotDateTime.date} - - {entrySpotDateTime.time} -
-
- )} - {exitDateTime && ( -
-
- - Exit time - -
-
- {exitDateTime.date} - {exitDateTime.time} -
-
- )} - {exitSpotDateTime && exitSpot && ( -
-
- - Exit spot - -
-
- {exitSpot} - - {exitSpotDateTime.date} - - {exitSpotDateTime.time} -
-
+ {dateTimes.start && } + {dateTimes.entry && entryValue && ( + )} + {dateTimes.end && } + {dateTimes.exit && exitValue && }
); diff --git a/packages/trader/src/AppV2/Components/PayoutInfoModal/__tests__/payout-info-model.spec.tsx b/packages/trader/src/AppV2/Components/PayoutInfoModal/__tests__/payout-info-model.spec.tsx new file mode 100644 index 000000000000..c8ebf506e139 --- /dev/null +++ b/packages/trader/src/AppV2/Components/PayoutInfoModal/__tests__/payout-info-model.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PayoutInfoModal from '../payout-info-modal'; + +describe('PayoutInfoModal', () => { + const bodyContent = 'This is the body content of the modal.'; + + it('should render the button and modal content correctly', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(screen.getByText('How do I earn a payout?')).toBeInTheDocument(); + expect(screen.queryByText(bodyContent)).not.toBeInTheDocument(); + }); + + it('should toggle the modal visibility when the button is clicked', async () => { + render(); + + const button = screen.getByRole('button'); + await userEvent.click(button); + expect(screen.getByText(bodyContent)).toBeInTheDocument(); + + await userEvent.click(button); + + await waitFor(() => { + expect(screen.queryByText(bodyContent)).not.toBeInTheDocument(); + }); + }); + + it('should close the modal when the primary button is clicked', async () => { + render(); + + const button = screen.getByRole('button'); + await userEvent.click(button); + + expect(screen.getByText(bodyContent)).toBeInTheDocument(); + + const primaryButton = screen.getByText('Got it'); + await userEvent.click(primaryButton); + + await waitFor(() => { + expect(screen.queryByText(bodyContent)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx b/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx new file mode 100644 index 000000000000..7d1d3df3c7d3 --- /dev/null +++ b/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx @@ -0,0 +1,84 @@ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import RiskManagementInfoModal from '../risk-management-info-modal'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@deriv/quill-icons', () => ({ + LabelPairedCircleInfoSmRegularIcon: () => , +})); + +describe('RiskManagementInfoModal', () => { + const headerContent = 'Risk Management Info'; + const bodyContent = 'This is the body content of the modal.'; + const infoMessage = 'Additional info message'; + + it('should render the button and modal content correctly', () => { + render( + + ); + + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + + expect(screen.queryByText(headerContent)).not.toBeInTheDocument(); + }); + + it('should toggle the modal visibility when the button is clicked', () => { + render( + + ); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.getByText(headerContent)).toBeInTheDocument(); + expect(screen.getByText(bodyContent)).toBeInTheDocument(); + expect(screen.getByText(infoMessage)).toBeInTheDocument(); + + fireEvent.click(button); + + expect(screen.queryByText(headerContent)).not.toBeInTheDocument(); + }); + + it('should not render the info message if it is not provided', () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(screen.getByText(headerContent)).toBeInTheDocument(); + expect(screen.getByText(bodyContent)).toBeInTheDocument(); + expect(screen.queryByText(infoMessage)).not.toBeInTheDocument(); + }); + + it('should close the modal when the primary button is clicked', () => { + render( + + ); + + const button = screen.getByRole('button'); + userEvent.click(button); + + expect(screen.getByText(headerContent)).toBeInTheDocument(); + + const primaryButton = screen.getByText('Got it'); + userEvent.click(primaryButton); + + waitFor(() => expect(screen.queryByText(headerContent)).not.toBeInTheDocument()); + }); +}); diff --git a/packages/trader/src/AppV2/Components/RiskManagementItem/risk-management-item.tsx b/packages/trader/src/AppV2/Components/RiskManagementItem/risk-management-item.tsx index 167db94facb4..71bc63201c38 100644 --- a/packages/trader/src/AppV2/Components/RiskManagementItem/risk-management-item.tsx +++ b/packages/trader/src/AppV2/Components/RiskManagementItem/risk-management-item.tsx @@ -10,21 +10,13 @@ import { CONTRACT_TYPES, isAccumulatorContract, isValidToCancel } from '@deriv/s type RiskManagementItemProps = { label: React.ReactNode; modal_body_content: React.ReactNode; - validation_message?: React.ReactNode; is_deal_cancellation?: boolean; value?: number | null; type?: string; }; const RiskManagementItem = observer( - ({ - label, - modal_body_content, - validation_message, - is_deal_cancellation = false, - value, - type, - }: RiskManagementItemProps) => { + ({ label, modal_body_content, is_deal_cancellation = false, value, type }: RiskManagementItemProps) => { const [toggle, setToggle] = React.useState(Boolean(value)); const [isSheetOpen, setIsSheetOpen] = React.useState(false); const [isEnabled, setIsEnabled] = React.useState(false); @@ -49,23 +41,18 @@ const RiskManagementItem = observer( const errorKey = `contract_update_${type}` as 'contract_update_stop_loss' | 'contract_update_take_profit'; const errorMessage = validation_errors[errorKey]?.length > 0 ? validation_errors[errorKey][0] : ''; + const getMessageForMultiplier = (is_valid_to_cancel: boolean, is_deal_cancellation: boolean) => + is_valid_to_cancel && !is_deal_cancellation ? ( + + ) : null; + const info_message = { [CONTRACT_TYPES.ACCUMULATOR]: ( ), - [CONTRACT_TYPES.MULTIPLIER.UP]: - is_valid_to_cancel && !is_deal_cancellation ? ( - - ) : ( - '' - ), - [CONTRACT_TYPES.MULTIPLIER.DOWN]: - is_valid_to_cancel && !is_deal_cancellation ? ( - - ) : ( - '' - ), - } as const; + [CONTRACT_TYPES.MULTIPLIER.UP]: getMessageForMultiplier(is_valid_to_cancel, is_deal_cancellation), + [CONTRACT_TYPES.MULTIPLIER.DOWN]: getMessageForMultiplier(is_valid_to_cancel, is_deal_cancellation), + }; const onChange = ( e: React.ChangeEvent | { target: { name: string; value: number | string | boolean } } diff --git a/packages/trader/src/AppV2/Containers/Chart/__tests__/chart-placeholder.spec.tsx b/packages/trader/src/AppV2/Containers/Chart/__tests__/chart-placeholder.spec.tsx new file mode 100644 index 000000000000..5969a4c6e3e0 --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Chart/__tests__/chart-placeholder.spec.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ChartPlaceholder from '../chart-placeholder'; + +describe('ChartPlaceholder', () => { + it('should render the chart placeholder with correct text', () => { + render(); + const placeholderText = screen.getByText('Placeholder Chart'); + expect(placeholderText).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useContractDetails.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useContractDetails.spec.tsx new file mode 100644 index 000000000000..631cb7491d4c --- /dev/null +++ b/packages/trader/src/AppV2/Hooks/__tests__/useContractDetails.spec.tsx @@ -0,0 +1,90 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLocation } from 'react-router'; +import { useStore } from '@deriv/stores'; +import useContractDetails from '../useContractDetails'; + +jest.mock('react-router', () => ({ + useLocation: jest.fn(), +})); + +jest.mock('@deriv/stores', () => ({ + useStore: jest.fn(), +})); + +describe('useContractDetails', () => { + const mockOnMount = jest.fn(); + const mockGetContractById = jest.fn(); + const mockContractInfo = { contract_id: null }; + + const setupMocks = (contractInfoOverride = {}) => { + (useLocation as jest.Mock).mockReturnValue({ + pathname: '/contract/12345', + }); + + (useStore as jest.Mock).mockReturnValue({ + contract_replay: { + contract_store: { + contract_info: { ...mockContractInfo, ...contractInfoOverride }, + }, + onMount: mockOnMount, + }, + contract_trade: { + getContractById: mockGetContractById, + }, + }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call onMount with contract_id from URL when contract_id is not available', () => { + setupMocks(); + + renderHook(() => useContractDetails()); + + expect(mockOnMount).toHaveBeenCalledWith(12345); + }); + + it('should not call onMount if contract_id is available', () => { + setupMocks({ contract_id: 67890 }); + + renderHook(() => useContractDetails()); + + expect(mockOnMount).not.toHaveBeenCalled(); + }); + + it('should return the correct contract_info and contract', () => { + const mockContract = { id: 67890 }; + mockGetContractById.mockReturnValue(mockContract); + + setupMocks({ contract_id: 67890 }); + + const { result } = renderHook(() => useContractDetails()); + + expect(result.current.contract_info.contract_id).toBe(67890); + expect(result.current.contract).toBe(mockContract); + expect(result.current.is_loading).toBe(false); + }); + + it('should return is_loading as true if contract_id is not available', () => { + setupMocks(); + + const { result } = renderHook(() => useContractDetails()); + + expect(result.current.is_loading).toBe(true); + }); + + it('should update on location change', () => { + const { rerender } = renderHook(() => useContractDetails(), { + initialProps: { pathname: '/contract/12345' }, + }); + + setupMocks(); + (useLocation as jest.Mock).mockReturnValue({ pathname: '/contract/67890' }); + + rerender(); + + expect(mockOnMount).toHaveBeenCalledWith(67890); + }); +}); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useOrderDetails.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useOrderDetails.spec.tsx new file mode 100644 index 000000000000..17c7add75442 --- /dev/null +++ b/packages/trader/src/AppV2/Hooks/__tests__/useOrderDetails.spec.tsx @@ -0,0 +1,158 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { CONTRACT_TYPES, TContractInfo } from '@deriv/shared'; +import useOrderDetails from '../useOrderDetails'; + +jest.mock('@deriv/translations', () => ({ + localize: jest.fn(text => text), +})); + +jest.mock('@deriv/shared', () => ({ + CONTRACT_TYPES: { + TURBOS: { LONG: 'turbos_long', SHORT: 'turbos_short' }, + MULTIPLIER: { DOWN: 'multiplier_down', UP: 'multiplier_up' }, + MATCH_DIFF: { MATCH: 'match_diff_match', DIFF: 'match_diff_diff' }, + EVEN_ODD: { EVEN: 'even_odd_even', ODD: 'even_odd_odd' }, + OVER_UNDER: { OVER: 'over_under_over', UNDER: 'over_under_under' }, + RESET: { CALL: 'reset_call' }, + PUT: 'put', + CALLE: 'calle', + CALL: 'call', + TOUCH: { ONE_TOUCH: 'touch_one_touch', NO_TOUCH: 'touch_no_touch' }, + ACCUMULATOR: 'accumulator', + VANILLA: { CALL: 'vanilla_call', PUT: 'vanilla_put' }, + }, + getDurationPeriod: jest.fn(), + getDurationTime: jest.fn(), + getDurationUnitText: jest.fn(), + getGrowthRatePercentage: jest.fn(() => '10'), + isAccumulatorContract: jest.fn(), + isResetContract: jest.fn(), + addComma: jest.fn(), +})); + +jest.mock('App/Components/Elements/PositionsDrawer/helpers', () => ({ + getBarrierValue: jest.fn(), +})); + +const mockData: TContractInfo = { + transaction_ids: { buy: 12345, sell: 67890 }, + buy_price: 100, + currency: 'USD', + tick_count: 5, + tick_passed: 3, + contract_type: '', + display_number_of_contracts: '1', + commission: 5, + limit_order: { + take_profit: { order_amount: 200 }, + stop_loss: { order_amount: 50 }, + stop_out: { order_amount: 30 }, + }, + barrier: '1000', + growth_rate: 10, + entry_spot_display_value: '1000', + is_expired: 1, + is_sold: 1, +}; + +describe('useOrderDetails', () => { + it('should return correct details for Multiplier contract', () => { + mockData.contract_type = CONTRACT_TYPES.MULTIPLIER.UP; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Multiplier: '', + Stake: '100.00 USD', + Commission: '5 USD', + 'Take Profit': '200.00 USD', + 'Stop loss': '50.00 USD', + 'Stop out level': '30.00 USD', + }); + }); + + it('should return correct details for Rise contract', () => { + mockData.contract_type = CONTRACT_TYPES.CALL; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Duration: '5 ticks', + Barrier: '1000', + Stake: '100.00 USD', + }); + }); + + it('should return correct details for Turbos contract', () => { + mockData.contract_type = CONTRACT_TYPES.TURBOS.LONG; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Duration: '5 ticks', + Barrier: '1000', + 'Payout per point': '1', + Stake: '100.00 USD', + 'Take Profit': '200.00 USD', + }); + }); + + it('should return correct details for Matcher contract', () => { + mockData.contract_type = CONTRACT_TYPES.MATCH_DIFF.MATCH; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Duration: ' ', + Target: undefined, + Stake: '100.00 USD', + }); + }); + + it('should return correct details for Accumulator contract', () => { + mockData.contract_type = CONTRACT_TYPES.ACCUMULATOR; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Duration: '5 ticks', + 'Growth rate': '10%', + Stake: '100.00 USD', + 'Take Profit': '200 USD', + }); + }); + + it('should return correct details for Vanilla contract', () => { + mockData.contract_type = CONTRACT_TYPES.VANILLA.CALL; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + 'Strike Price': ' - ', + Duration: ' ', + 'Payout per point': '1', + Stake: '100.00 USD', + }); + }); + + it('should return default details for unknown contract type', () => { + mockData.contract_type = 'unknown'; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current?.details).toEqual({}); + }); + + it('should handle missing contract type', () => { + mockData.contract_type = ''; + const { result } = renderHook(() => useOrderDetails(mockData)); + expect(result.current).toBeUndefined(); + }); + + it('should handle contracts without limit_order', () => { + const modifiedData = { ...mockData, limit_order: undefined }; + modifiedData.contract_type = CONTRACT_TYPES.MULTIPLIER.UP; + const { result } = renderHook(() => useOrderDetails(modifiedData)); + expect(result.current?.details).toEqual({ + 'Reference ID': ['12345 (Buy)', '67890 (Sell)'], + Multiplier: '', + Stake: '100.00 USD', + Commission: '5 USD', + 'Take Profit': 'Not set', + 'Stop loss': 'Not set', + 'Stop out level': '', + }); + }); +}); diff --git a/packages/trader/src/AppV2/Utils/__tests__/helper.spec.ts b/packages/trader/src/AppV2/Utils/__tests__/helper.spec.ts new file mode 100644 index 000000000000..17e6e0de2788 --- /dev/null +++ b/packages/trader/src/AppV2/Utils/__tests__/helper.spec.ts @@ -0,0 +1,80 @@ +import moment from 'moment'; +import { TGetCardLables } from '@deriv/components'; +import { formatDuration, getDiffDuration } from '@deriv/shared'; +import { getRemainingTime } from '../helper'; + +jest.mock('@deriv/shared', () => ({ + formatDuration: jest.fn(), + getDiffDuration: jest.fn(), +})); + +describe('getRemainingTime', () => { + const mockGetCardLabels: TGetCardLables = jest.fn(() => ({ + DAYS: 'days', + DAY: 'day', + })); + + it('should return undefined if end_time is not provided', () => { + const result = getRemainingTime({ + end_time: undefined, + start_time: moment(), + format: 'mm:ss', + getCardLabels: mockGetCardLabels, + }); + + expect(result).toBeUndefined(); + }); + + it('should return undefined if start_time is greater than end_time', () => { + const result = getRemainingTime({ + end_time: moment().subtract(1, 'day').unix(), + start_time: moment(), + format: 'mm:ss', + getCardLabels: mockGetCardLabels, + }); + + expect(result).toBeUndefined(); + }); + + it('should return remaining time in correct format when days are 0', () => { + (getDiffDuration as jest.Mock).mockReturnValue(3600); + (formatDuration as jest.Mock).mockReturnValue({ days: 0, timestamp: '01:00:00' }); + + const result = getRemainingTime({ + end_time: moment().add(1, 'hour').unix(), + start_time: moment(), + format: 'HH:mm:ss', + getCardLabels: mockGetCardLabels, + }); + + expect(result).toBe('01:00:00'); + }); + + it('should return remaining time with days in correct format', () => { + (getDiffDuration as jest.Mock).mockReturnValue(90000); + (formatDuration as jest.Mock).mockReturnValue({ days: 1, timestamp: '01:00:00' }); + + const result = getRemainingTime({ + end_time: moment().add(1, 'day').add(1, 'hour').unix(), + start_time: moment(), + format: 'HH:mm:ss', + getCardLabels: mockGetCardLabels, + }); + + expect(result).toBe('1 day 01:00:00'); + }); + + it('should return remaining time with plural days in correct format', () => { + (getDiffDuration as jest.Mock).mockReturnValue(180000); + (formatDuration as jest.Mock).mockReturnValue({ days: 2, timestamp: '02:00:00' }); + + const result = getRemainingTime({ + end_time: moment().add(2, 'days').add(2, 'hours').unix(), + start_time: moment(), + format: 'HH:mm:ss', + getCardLabels: mockGetCardLabels, + }); + + expect(result).toBe('2 days 02:00:00'); + }); +}); diff --git a/packages/trader/src/AppV2/__tests__/app.spec.tsx b/packages/trader/src/AppV2/__tests__/app.spec.tsx index 52f42a2fc3a1..ac8a44846dbc 100644 --- a/packages/trader/src/AppV2/__tests__/app.spec.tsx +++ b/packages/trader/src/AppV2/__tests__/app.spec.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react'; import App from '../app'; import { mockStore } from '@deriv/stores'; import moment from 'moment'; +import { BrowserRouter } from 'react-router-dom'; const rootStore = mockStore({ common: { @@ -57,12 +58,14 @@ jest.mock('AppV2/Components/BottomNav', () => { describe('App', () => { it('should render the app component', () => { const { container } = render( - + + + ); expect(container).toBeInTheDocument(); }); @@ -71,12 +74,14 @@ describe('App', () => { const setPromptHandler = jest.fn(); rootStore.ui.setPromptHandler = setPromptHandler; const { unmount } = render( - + + + ); unmount(); expect(setPromptHandler).toHaveBeenCalledWith(false);