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);