diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/advanced-duration.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/advanced-duration.spec.tsx
new file mode 100644
index 000000000000..e50ec9112c0c
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/advanced-duration.spec.tsx
@@ -0,0 +1,187 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import TraderProviders from '../../../../../../../trader-providers';
+import AdvancedDuration from '../advanced-duration';
+import { mockStore } from '@deriv/stores';
+
+const button_toggle = 'MockedButtonToggle';
+const dropdown = 'MockedDropDown';
+const input_field = 'MockedInputField';
+const range_slider = 'MockedRangeSlider';
+const date_picker = 'MockedDatePicker';
+const time_picker = 'MockedTimePicker';
+const expiry_text = 'MockedExpiryText';
+const duration_range_text = 'MockedDurationRangeText';
+
+jest.mock('@deriv/components', () => ({
+ ...jest.requireActual('@deriv/components'),
+ ButtonToggle: jest.fn(() =>
{button_toggle}
),
+ Dropdown: jest.fn(() => {dropdown}
),
+ InputField: jest.fn(() => {input_field}
),
+}));
+jest.mock('App/Components/Form/RangeSlider', () => jest.fn(() => {range_slider}
));
+jest.mock('../../../DatePicker', () => jest.fn(() => {date_picker}
));
+jest.mock('../../../TimePicker', () => jest.fn(() => {time_picker}
));
+jest.mock('../expiry-text', () => jest.fn(() => {expiry_text}
));
+jest.mock('../duration-range-text', () => jest.fn(() => {duration_range_text}
));
+
+describe('', () => {
+ let mock_store: ReturnType, default_props: React.ComponentProps;
+ beforeEach(() => {
+ mock_store = {
+ ...mockStore({
+ ui: {
+ current_focus: '',
+ setCurrentFocus: jest.fn(),
+ },
+ modules: {
+ trade: {
+ contract_expiry_type: 'intraday',
+ duration_min_max: {
+ daily: {
+ min: 1234,
+ max: 2345,
+ },
+ intraday: {
+ min: 12345,
+ max: 23456,
+ },
+ },
+ validation_errors: {},
+ },
+ },
+ }),
+ };
+ default_props = {
+ advanced_duration_unit: 't',
+ advanced_expiry_type: 'duration',
+ changeDurationUnit: jest.fn(),
+ duration_t: 10,
+ duration_units_list: [
+ {
+ text: 'Ticks',
+ value: 't',
+ },
+ {
+ text: 'Seconds',
+ value: 's',
+ },
+ {
+ text: 'Minutes',
+ value: 'm',
+ },
+ {
+ text: 'Hours',
+ value: 'h',
+ },
+ {
+ text: 'Days',
+ value: 'd',
+ },
+ ],
+ expiry_date: '',
+ expiry_epoch: 1703057788,
+ expiry_list: [
+ {
+ text: 'Duration',
+ value: 'duration',
+ },
+ {
+ text: 'End time',
+ value: 'endtime',
+ },
+ ],
+ expiry_type: 'duration',
+ getDurationFromUnit: jest.fn(),
+ number_input_props: {
+ type: 'number',
+ is_incrementable: false,
+ },
+ onChange: jest.fn(),
+ onChangeUiStore: jest.fn(),
+ server_time: 0,
+ shared_input_props: {
+ is_hj_whitelisted: true,
+ max_value: 86400,
+ min_value: 15,
+ onChange: jest.fn(),
+ },
+ start_date: 0,
+ };
+ });
+ const renderAdvancedDuration = (
+ mock_store: ReturnType,
+ default_props: React.ComponentProps
+ ) => {
+ return render(
+
+
+
+ );
+ };
+ it('Should render mocked button toggle if expiry_list is of length > 1', () => {
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(button_toggle)).toBeInTheDocument();
+ });
+ it('Should not render mocked button toggle if expiry_list is of length <= 1', () => {
+ default_props.expiry_list = [
+ {
+ text: 'Duration',
+ value: 'duration',
+ },
+ ];
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.queryByText(button_toggle)).not.toBeInTheDocument();
+ });
+ it('Should render mocked trading date and trading time picker if contract is 24 hours and expiry type is endtime', () => {
+ default_props.expiry_type = 'endtime';
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(date_picker)).toBeInTheDocument();
+ expect(screen.getByText(time_picker)).toBeInTheDocument();
+ });
+ it('Should render mocked expiry text and should not render trading time picker if contract is not 24 hours and expiry type is endtime', () => {
+ default_props.expiry_type = 'endtime';
+ default_props.duration_units_list = [
+ {
+ text: 'Ticks',
+ value: 't',
+ },
+ {
+ text: 'Seconds',
+ value: 's',
+ },
+ {
+ text: 'Days',
+ value: 'd',
+ },
+ ];
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(expiry_text)).toBeInTheDocument();
+ expect(screen.queryByText(time_picker)).not.toBeInTheDocument();
+ });
+ it('Should render mocked dropdown if duration_units_list length is > 1', () => {
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(dropdown)).toBeInTheDocument();
+ });
+ it('Should not render mocked dropdown if duration_units_list length is > 0', () => {
+ default_props.duration_units_list = [];
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.queryByText(dropdown)).not.toBeInTheDocument();
+ });
+ it('Should render mocked trading date picker and mocked expiry text if advanced_duration_unit === d & !==t', () => {
+ default_props.advanced_duration_unit = 'd';
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(duration_range_text)).toBeInTheDocument();
+ expect(screen.getByText(expiry_text)).toBeInTheDocument();
+ });
+ it('Should render mocked trading date picker and mocked expiry text if advanced_duration_unit === t && contract_expiry_type === tick', () => {
+ mock_store.modules.trade.contract_expiry_type = 'tick';
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(range_slider)).toBeInTheDocument();
+ });
+ it('Should render mocked mocked input field if advanced_duration_unit is intraday like h or m', () => {
+ default_props.advanced_duration_unit = 'm';
+ renderAdvancedDuration(mock_store, default_props);
+ expect(screen.getByText(input_field)).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-mobile.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-mobile.spec.tsx
new file mode 100644
index 000000000000..58dc082c6f13
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-mobile.spec.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import TraderProviders from '../../../../../../../trader-providers';
+import DurationMobile from '../duration-mobile';
+import { mockStore } from '@deriv/stores';
+
+jest.mock('@deriv/components', () => {
+ return {
+ ...jest.requireActual('@deriv/components'),
+ Tabs: jest.fn(({ onTabItemClick, children }) => (
+
+ {children}
+
+
+ )),
+ };
+});
+
+jest.mock('../duration-ticks-widget-mobile.tsx', () => jest.fn(() => 'MockedDurationTicksWidgetMobile'));
+jest.mock('../duration-numbers-widget-mobile.tsx', () => jest.fn(() => 'MockedDurationNumbersWidgetMobile'));
+
+describe('', () => {
+ let mock_store: ReturnType, default_props: React.ComponentProps;
+ beforeEach(() => {
+ mock_store = {
+ ...mockStore({
+ modules: {
+ trade: {
+ duration_units_list: [
+ {
+ text: 'Ticks',
+ value: 't',
+ },
+ {
+ text: 'Seconds',
+ value: 's',
+ },
+ {
+ text: 'Minutes',
+ value: 'm',
+ },
+ {
+ text: 'Hours',
+ value: 'h',
+ },
+ {
+ text: 'Days',
+ value: 'd',
+ },
+ {
+ text: 'Weeks',
+ value: 'w',
+ },
+ ],
+ duration_unit: 't',
+ basis: 'stake',
+ duration_min_max: {
+ daily: {
+ min: 1234,
+ max: 2345,
+ },
+ intraday: {
+ min: 12345,
+ max: 23456,
+ },
+ },
+ validation_errors: {},
+ },
+ },
+ }),
+ };
+ default_props = {
+ amount_tab_idx: 0,
+ d_duration: 1,
+ duration_tab_idx: 1,
+ expiry_epoch: 1703057788,
+ h_duration: 1,
+ has_amount_error: false,
+ m_duration: 1,
+ payout_value: 123,
+ s_duration: 1,
+ setDurationError: jest.fn(),
+ setDurationTabIdx: jest.fn(),
+ setSelectedDuration: jest.fn(),
+ stake_value: 12,
+ t_duration: 1,
+ toggleModal: jest.fn(),
+ };
+ });
+ const renderDurationMobile = (
+ mock_store: ReturnType,
+ default_props: React.ComponentProps
+ ) => {
+ return render(
+
+
+
+ );
+ };
+ it('Should render 1 Ticks Widget, 4 Numbers Widget and mocked date picker', () => {
+ renderDurationMobile(mock_store, default_props);
+ expect(screen.getByText(/mockeddurationtickswidgetmobile/i)).toBeInTheDocument();
+ expect(screen.getAllByText(/mockeddurationnumberswidgetmobile/i)).toHaveLength(4);
+ expect(screen.getByText(/pick an end date/i)).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-numbers-widget-mobile.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-numbers-widget-mobile.spec.tsx
new file mode 100644
index 000000000000..5e429a14eb5e
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-numbers-widget-mobile.spec.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import TraderProviders from '../../../../../../../trader-providers';
+import { mockStore } from '@deriv/stores';
+import DurationNumbersWidgetMobile from '../duration-numbers-widget-mobile';
+import * as durationUtils from '../duration-utils';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('../duration-range-text.tsx', () => jest.fn(() => MockedDurationRangeText
));
+jest.mock('../expiry-text.tsx', () => jest.fn(() => MockedExpiryText
));
+jest.spyOn(durationUtils, 'updateAmountChanges');
+describe('', () => {
+ let mock_store: ReturnType,
+ default_props: React.ComponentProps;
+ beforeEach(() => {
+ mock_store = {
+ ...mockStore({
+ ui: {
+ addToast: jest.fn(),
+ },
+ modules: {
+ trade: {
+ amount: 123,
+ basis: 'stake',
+ duration: 5,
+ duration_unit: 'd',
+ duration_min_max: {
+ daily: {
+ min: 1234,
+ max: 2345,
+ },
+ intraday: {
+ min: 12345,
+ max: 23456,
+ },
+ },
+ onChangeMultiple: jest.fn(),
+ },
+ },
+ }),
+ };
+ default_props = {
+ contract_expiry: 'daily',
+ duration_unit_option: {
+ text: 'Minutes',
+ value: 'm',
+ },
+ duration_values: {
+ t_duration: 5,
+ s_duration: 15,
+ m_duration: 35,
+ h_duration: 1,
+ d_duration: 1,
+ },
+ expiry_epoch: 1703242581,
+ show_expiry: false,
+ setDurationError: jest.fn(),
+ basis_option: 'payout',
+ toggleModal: jest.fn(),
+ has_amount_error: false,
+ payout_value: 10,
+ stake_value: 10,
+ selected_duration: 5,
+ setSelectedDuration: jest.fn(),
+ };
+ });
+ const renderDurationNumbersWidgetMobile = (
+ mock_store: ReturnType,
+ default_props: React.ComponentProps
+ ) => {
+ return render(
+
+
+
+ );
+ };
+ it('should render Mocked duration range text', () => {
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ expect(screen.getByText(/mockeddurationrangetext/i)).toBeInTheDocument();
+ });
+ it('should render mocked expiry text if show_expiry is true', () => {
+ default_props.show_expiry = true;
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ expect(screen.getByText(/mockedexpirytext/i)).toBeInTheDocument();
+ });
+ it('Should show validation messages if selected_duration is less than minimum', () => {
+ default_props.selected_duration = 11;
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ expect(mock_store.ui.addToast).toHaveBeenCalled();
+ expect(default_props.setDurationError).toHaveBeenCalledWith(true);
+ });
+ it('Should show validation messages if selected_duration is empty', () => {
+ default_props.selected_duration = '' as unknown as number;
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ expect(mock_store.ui.addToast).toHaveBeenCalled();
+ expect(default_props.setDurationError).toHaveBeenCalledWith(true);
+ });
+ it('Should call setDurationError with false value and should not call addToast when there is not validation error', () => {
+ default_props.selected_duration = 21;
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ expect(mock_store.ui.addToast).not.toHaveBeenCalled();
+ expect(default_props.setDurationError).toHaveBeenCalledWith(false);
+ });
+ it('should call updateAmountChanges, onChangeMultiple, and toggleModal if there is has_amount_error is false, duration is changed and user clicks ok', () => {
+ default_props.selected_duration = 21;
+ renderDurationNumbersWidgetMobile(mock_store, default_props);
+ userEvent.click(screen.getByText(/ok/i));
+ expect(durationUtils.updateAmountChanges).toHaveBeenCalled();
+ expect(default_props.toggleModal).toHaveBeenCalled();
+ expect(mock_store.modules.trade.onChangeMultiple).toHaveBeenCalledWith({
+ amount: 10,
+ basis: 'payout',
+ duration: 21,
+ duration_unit: 'm',
+ expiry_type: 'duration',
+ });
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-ticks-widget-mobile.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-ticks-widget-mobile.spec.tsx
new file mode 100644
index 000000000000..69198adad93f
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-ticks-widget-mobile.spec.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import TraderProviders from '../../../../../../../trader-providers';
+import DurationTicksWidgetMobile from '../duration-ticks-widget-mobile';
+import { mockStore } from '@deriv/stores';
+import userEvent from '@testing-library/user-event';
+
+describe('', () => {
+ let mock_store: ReturnType, default_props: React.ComponentProps;
+ beforeEach(() => {
+ mock_store = {
+ ...mockStore({
+ modules: {
+ trade: {
+ amount: 123,
+ basis: 'stake',
+ duration: 5,
+ duration_unit: 't',
+ duration_min_max: {
+ daily: {
+ min: 1234,
+ max: 2345,
+ },
+ intraday: {
+ min: 12345,
+ max: 23456,
+ },
+ tick: {
+ min: 1,
+ max: 10,
+ },
+ },
+ onChangeMultiple: jest.fn(),
+ },
+ },
+ }),
+ };
+ default_props = {
+ setDurationError: jest.fn(),
+ basis_option: 'payout',
+ toggleModal: jest.fn(),
+ has_amount_error: false,
+ payout_value: 10,
+ stake_value: 10,
+ selected_duration: 5,
+ setSelectedDuration: jest.fn(),
+ };
+ });
+ const renderDurationTicksWidgetMobile = (
+ mock_store: ReturnType,
+ default_props: React.ComponentProps
+ ) => {
+ return render(
+
+
+
+ );
+ };
+ it('Should call setDurationError on mount', () => {
+ renderDurationTicksWidgetMobile(mock_store, default_props);
+ expect(default_props.setDurationError).toHaveBeenCalled();
+ });
+ it('Should call onChangeMultiple and toggleModal when setTicksDuration is invoked', () => {
+ renderDurationTicksWidgetMobile(mock_store, default_props);
+ const ok_button = screen.getByText('OK');
+ userEvent.click(ok_button);
+
+ expect(mock_store.modules.trade.onChangeMultiple).toHaveBeenCalled();
+ expect(default_props.toggleModal).toHaveBeenCalled();
+ });
+ it('should render items on screen', () => {
+ renderDurationTicksWidgetMobile(mock_store, default_props);
+ expect(screen.getByText('05')).toBeInTheDocument();
+ expect(screen.getByText('Ticks')).toBeInTheDocument();
+ expect(screen.getByText('OK')).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-toggle.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-toggle.spec.tsx
new file mode 100644
index 000000000000..bf9d5e9213c4
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-toggle.spec.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import DurationToggle from '../duration-toggle';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('@deriv/components', () => {
+ return {
+ ...jest.requireActual('@deriv/components'),
+ Icon: jest.fn(() => MockedIcon
),
+ };
+});
+describe('', () => {
+ let mocked_props: React.ComponentProps;
+ beforeEach(() => {
+ mocked_props = {
+ name: 'test_input',
+ onChange: jest.fn(),
+ value: false,
+ };
+ });
+
+ it('Should render toggle button', () => {
+ render();
+ const toggle_button = screen.getByLabelText('Toggle between advanced and simple duration settings');
+ expect(toggle_button).toBeInTheDocument();
+ expect(toggle_button).toHaveClass('advanced-simple-toggle');
+ expect(screen.getByText('MockedIcon')).toBeInTheDocument();
+ });
+ it('Should call onChange when button is clicked', () => {
+ render();
+ userEvent.click(screen.getByLabelText('Toggle between advanced and simple duration settings'));
+ expect(mocked_props.onChange).toHaveBeenCalledWith({ target: { name: 'test_input', value: true } });
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-utils.spec.ts b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-utils.spec.ts
new file mode 100644
index 000000000000..65b3e934f213
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-utils.spec.ts
@@ -0,0 +1,50 @@
+import { updateAmountChanges } from '../duration-utils';
+
+type TUpdateAmountChanges = Parameters;
+
+describe('updateAmountChanges', () => {
+ let obj: TUpdateAmountChanges[0],
+ stake_value: TUpdateAmountChanges[1],
+ payout_value: TUpdateAmountChanges[2],
+ basis: TUpdateAmountChanges[3],
+ trade_basis: TUpdateAmountChanges[4],
+ trade_amount: TUpdateAmountChanges[5];
+ beforeEach(() => {
+ obj = {
+ basis: '',
+ amount: 123,
+ };
+ stake_value = 2;
+ payout_value = 1;
+ basis = '';
+ trade_basis = '';
+ trade_amount = 0;
+ });
+ it('should update basis to "stake" and amount to stake_value when basis is "stake" and stake_value is different from trade_amount', () => {
+ stake_value = 20;
+ trade_amount = 40;
+ basis = 'stake';
+ updateAmountChanges(obj, stake_value, payout_value, basis, trade_basis, trade_amount);
+
+ expect(obj.basis).toBe('stake');
+ expect(obj.amount).toBe(stake_value);
+ });
+ it('should update basis to "payout" and amount to payout_value when basis is "payout" and payout_value is different from trade_amount', () => {
+ basis = 'payout';
+ payout_value = 20;
+ trade_amount = 40;
+ updateAmountChanges(obj, stake_value, payout_value, basis, trade_basis, trade_amount);
+
+ expect(obj.basis).toBe('payout');
+ expect(obj.amount).toBe(payout_value);
+ });
+ it('should update basis to trade_basis and amount to trade_amount when basis is different from trade_basis', () => {
+ trade_basis = 'payout';
+ basis = 'stake';
+ stake_value = 20;
+ trade_amount = 20;
+ updateAmountChanges(obj, stake_value, payout_value, basis, trade_basis, trade_amount);
+ expect(obj.basis).toBe(basis);
+ expect(obj.amount).toBe(trade_amount);
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration.spec.tsx
new file mode 100644
index 000000000000..a5261b71515f
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration.spec.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import Duration from '../duration';
+import moment from 'moment';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('@deriv/components', () => {
+ return {
+ ...jest.requireActual('@deriv/components'),
+ Dropdown: jest.fn(() => 'MockedDropdown'),
+ };
+});
+
+jest.mock('../advanced-duration.tsx', () => jest.fn(() => 'MockedAdvancedDuration'));
+jest.mock('../simple-duration.tsx', () => jest.fn(() => 'MockedSimpleDuration'));
+jest.mock('App/Components/Form/RangeSlider', () => jest.fn(() => 'MockedRangeSlider'));
+
+describe('', () => {
+ let default_props: React.ComponentProps;
+ const duration_toggle_label_text = 'Toggle between advanced and simple duration settings';
+ beforeEach(() => {
+ default_props = {
+ advanced_duration_unit: '',
+ advanced_expiry_type: '',
+ contract_type: '',
+ duration_t: 5,
+ duration_unit: 'd',
+ duration_units_list: [
+ {
+ text: 'Ticks',
+ value: 't',
+ },
+ {
+ text: 'Seconds',
+ value: 's',
+ },
+ {
+ text: 'Minutes',
+ value: 'm',
+ },
+ {
+ text: 'Hours',
+ value: 'h',
+ },
+ {
+ text: 'Days',
+ value: 'd',
+ },
+ ],
+ duration: 12,
+ expiry_date: '2023-11-22',
+ expiry_epoch: 1703057788,
+ expiry_time: '21:00:00',
+ expiry_type: 'duration',
+ getDurationFromUnit: jest.fn(),
+ hasDurationUnit: jest.fn(),
+ is_advanced_duration: false,
+ is_minimized: false,
+ max_value: 1000,
+ min_value: 10,
+ onChange: jest.fn(),
+ onChangeMultiple: jest.fn(),
+ onChangeUiStore: jest.fn(),
+ server_time: moment('2023-11-21T14:30:00'),
+ simple_duration_unit: '',
+ start_date: 0,
+ };
+ });
+ it('Should render Range slider and simple duration widget if is_advanced_duration is false, duration_unit = t and duration_unit_list is length = 1', () => {
+ default_props.duration_units_list = [
+ {
+ text: 'Ticks',
+ value: 't',
+ },
+ ];
+ default_props.duration_unit = 't';
+ render();
+ expect(screen.getByText(/mockeddropdown/i)).toBeInTheDocument();
+ expect(screen.getByText(/mockedrangeslider/i)).toBeInTheDocument();
+ });
+ it('Should render Duration toggle if contract_type is not vanilla ', () => {
+ render();
+ expect(screen.getByLabelText(duration_toggle_label_text)).toBeInTheDocument();
+ });
+ it('Should not render Duration toggle if contract_type is vanilla ', () => {
+ default_props.contract_type = 'vanillalongcall';
+ render();
+ expect(screen.queryByLabelText(duration_toggle_label_text)).not.toBeInTheDocument();
+ });
+ it('Should render AdvanceDuration widget if is_advanced_duration is true and duration_units_list length is > 1', () => {
+ default_props.is_advanced_duration = true;
+ render();
+ expect(screen.getByText(/mockedadvancedduration/i)).toBeInTheDocument();
+ });
+ it('Should render SimpleDuration widget if is_advanced_duration is false and duration_units_list length is > 1', () => {
+ render();
+ expect(screen.getByText(/mockedsimpleduration/i)).toBeInTheDocument();
+ });
+ it('Should call onChangeUiStore and onChangeMultiple when duration toggle is clicked', () => {
+ default_props.is_advanced_duration = true;
+ render();
+ userEvent.click(screen.getByLabelText(duration_toggle_label_text));
+ expect(default_props.onChangeUiStore).toHaveBeenCalled();
+ expect(default_props.onChangeMultiple).toHaveBeenCalled();
+ });
+ it('Should render 12 Days if is_minimized is true and expiry_type is duration', () => {
+ const text_to_get = '12 Days';
+ default_props.is_minimized = true;
+ render();
+ expect(screen.getByText(text_to_get)).toBeInTheDocument();
+ expect(screen.getByText(text_to_get)).toHaveClass('fieldset-minimized');
+ expect(screen.getByText(text_to_get)).toHaveClass('fieldset-minimized__duration');
+ });
+ it('Should render expiry date and time if is_minimized is true and expiry_type is not duration', () => {
+ default_props.expiry_type = 'endtime';
+ default_props.is_minimized = true;
+ render();
+ expect(screen.getByText('Wed - 22 Nov, 2023 21:00:00')).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx
index a96ad8546e85..4cd012df7197 100644
--- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx
@@ -25,7 +25,6 @@ type TAdvancedDuration = Pick<
| 'onChangeUiStore'
| 'server_time'
| 'start_date'
- | 'market_open_times'
> & {
changeDurationUnit: ({ target }: { target: { name: string; value: string } }) => void;
expiry_list: {
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.tsx
index 029f1cff255b..44766f73e38e 100644
--- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.tsx
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.tsx
@@ -1,15 +1,14 @@
import React from 'react';
-import { Tabs, TickPicker, Numpad, RelativeDatepicker } from '@deriv/components';
-import { isEmptyObject, addComma, getDurationMinMaxValues, getUnitMap } from '@deriv/shared';
-import { Localize, localize } from '@deriv/translations';
-import { observer, useStore } from '@deriv/stores';
+import { Tabs, RelativeDatepicker } from '@deriv/components';
+import { getDurationMinMaxValues } from '@deriv/shared';
+import { localize } from '@deriv/translations';
+import { observer } from '@deriv/stores';
import { useTraderStore } from 'Stores/useTraderStores';
-import moment from 'moment';
-import ExpiryText from './expiry-text';
-import DurationRangeText from './duration-range-text';
import type { TTradeParamsMobile } from 'Modules/Trading/Containers/trade-params-mobile';
+import DurationTicksWidgetMobile from './duration-ticks-widget-mobile';
+import DurationNumbersWidgetMobile from './duration-numbers-widget-mobile';
-type TDuration = Pick<
+export type TDurationMobile = Pick<
TTradeParamsMobile,
| 'amount_tab_idx'
| 'd_duration'
@@ -29,251 +28,9 @@ type TDuration = Pick<
expiry_epoch?: string | number;
};
-type TNumber = Pick<
- TDuration,
- | 'expiry_epoch'
- | 'has_amount_error'
- | 'payout_value'
- | 'setDurationError'
- | 'setSelectedDuration'
- | 'stake_value'
- | 'toggleModal'
-> & {
- basis_option: string;
- contract_expiry?: string;
- duration_unit_option: ReturnType['duration_units_list'][0];
- duration_values?: Record;
- selected_duration: number;
- show_expiry?: boolean;
-};
-
-type TTicks = Omit;
-
type TDurationUnit = 't' | 's' | 'm' | 'h' | 'd';
-const submit_label = localize('OK');
-
-const updateAmountChanges = (
- obj: Record,
- stake_value: number,
- payout_value: number,
- basis: string,
- trade_basis: string,
- trade_amount: number
-) => {
- // TODO: Move onChangeMultiple outside of duration and amount
- // and unify all trade parameter components to use same onMultipleChange func onSubmit
- // Checks if Amount tab was changed to stake and stake value was updated
- if (basis === 'stake' && stake_value !== trade_amount) {
- obj.basis = 'stake';
- obj.amount = stake_value;
- // Checks if Amount tab was changed to payout and payout value was updated
- } else if (basis === 'payout' && payout_value !== trade_amount) {
- obj.basis = 'payout';
- obj.amount = payout_value;
- // Checks if Amount tab was changed but payout or stake value was not updated
- } else if (trade_basis !== basis) {
- obj.basis = basis;
- obj.amount = trade_amount;
- }
-};
-
-const Ticks = observer(
- ({
- setDurationError,
- basis_option,
- toggleModal,
- has_amount_error,
- payout_value,
- stake_value,
- selected_duration,
- setSelectedDuration,
- }: TTicks) => {
- const {
- duration_min_max,
- duration: trade_duration,
- duration_unit: trade_duration_unit,
- basis: trade_basis,
- amount: trade_amount,
- onChangeMultiple,
- } = useTraderStore();
- React.useEffect(() => {
- setDurationError(false);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const [min_tick, max_tick] = getDurationMinMaxValues(duration_min_max, 'tick', 't');
-
- const setTickDuration = (value: { target: { value: number; name: string } }) => {
- const { value: duration } = value.target;
- const on_change_obj: Record = {};
-
- // check for any amount changes from Amount trade params tab before submitting onChange object
- if (!has_amount_error)
- updateAmountChanges(on_change_obj, stake_value, payout_value, basis_option, trade_basis, trade_amount);
-
- if (trade_duration !== duration || trade_duration_unit !== 't') {
- on_change_obj.duration_unit = 't';
- on_change_obj.duration = duration;
- }
-
- if (!isEmptyObject(on_change_obj)) onChangeMultiple(on_change_obj);
- toggleModal();
- };
-
- const onTickChange = (tick: number) => setSelectedDuration('t', tick);
- const tick_duration =
- trade_duration < Number(min_tick) && selected_duration < Number(min_tick)
- ? Number(min_tick)
- : selected_duration;
- return (
-
-
-
- );
- }
-);
-
-const Numbers = observer(
- ({
- basis_option,
- contract_expiry = 'intraday',
- duration_unit_option,
- duration_values,
- expiry_epoch,
- has_amount_error,
- payout_value,
- selected_duration,
- setDurationError,
- setSelectedDuration,
- stake_value,
- show_expiry = false,
- toggleModal,
- }: TNumber) => {
- const { ui } = useStore();
- const { addToast } = ui;
- const {
- duration_min_max,
- duration: trade_duration,
- duration_unit: trade_duration_unit,
- basis: trade_basis,
- amount: trade_amount,
- onChangeMultiple,
- } = useTraderStore();
- const { value: duration_unit } = duration_unit_option;
- const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry, duration_unit);
- const [has_error, setHasError] = React.useState(false);
-
- const validateDuration = (value: number | string) => {
- const localized_message = (
-
- );
- if (parseInt(value as string) < Number(min) || Math.trunc(selected_duration) > Number(max)) {
- addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 });
- setDurationError(true);
- setHasError(true);
- return 'error';
- } else if (parseInt(value as string) > Number(max)) {
- addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 });
- setHasError(true);
- return 'error';
- } else if (value.toString().length < 1) {
- addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 });
- setDurationError(true);
- setHasError(true);
- return false;
- }
-
- setDurationError(false);
- setHasError(false);
- return true;
- };
-
- const setDuration = (duration: string | number) => {
- const on_change_obj: Record = {};
-
- // check for any amount changes from Amount trade params tab before submitting onChange object
- if (!has_amount_error)
- updateAmountChanges(on_change_obj, stake_value, payout_value, basis_option, trade_basis, trade_amount);
-
- if (trade_duration !== Number(duration) || trade_duration_unit !== duration_unit) {
- on_change_obj.duration_unit = duration_unit;
- on_change_obj.duration = duration;
- on_change_obj.expiry_type = 'duration';
- }
-
- if (!isEmptyObject(on_change_obj)) onChangeMultiple(on_change_obj);
- toggleModal();
- };
-
- const setExpiryDate = (epoch: number, duration: string | number) => {
- if (trade_duration_unit !== 'd') {
- return moment.utc().add(Number(duration), 'days').format('D MMM YYYY, [23]:[59]:[59] [GMT +0]');
- }
- let expiry_date = new Date((epoch - trade_duration * 24 * 60 * 60) * 1000);
- if (duration) {
- expiry_date = new Date(expiry_date.getTime() + Number(duration) * 24 * 60 * 60 * 1000);
- }
-
- return expiry_date
- .toUTCString()
- .replace('GMT', 'GMT +0')
- .substring(5)
- .replace(/(\d{2}) (\w{3} \d{4})/, '$1 $2,');
- };
-
- const onNumberChange = (num: number | string) => {
- setSelectedDuration(duration_unit, Number(num));
- validateDuration(num);
- };
-
- const fixed_date = !has_error ? setExpiryDate(Number(expiry_epoch), Number(duration_values?.d_duration)) : '';
-
- const { name_plural, name } = getUnitMap()[duration_unit];
- const duration_unit_text = name_plural ?? name;
-
- return (
-
-
-
- {show_expiry && }
-
-
{
- return {v}
;
- }}
- pip_size={0}
- submit_label={submit_label}
- min={Number(min)}
- max={Number(max)}
- reset_press_interval={350}
- reset_value=''
- onValidate={validateDuration}
- onValueChange={onNumberChange}
- />
-
- );
- }
-);
-
-const Duration = observer(
+const DurationMobile = observer(
({
amount_tab_idx,
d_duration,
@@ -290,7 +47,7 @@ const Duration = observer(
stake_value,
t_duration,
toggleModal,
- }: TDuration) => {
+ }: TDurationMobile) => {
const { duration_units_list, duration_min_max, duration_unit, basis: trade_basis } = useTraderStore();
const duration_values = {
t_duration,
@@ -330,7 +87,7 @@ const Duration = observer(
case 't':
return (
-
-
-
-
- & {
+ basis_option: string;
+ contract_expiry?: string;
+ duration_unit_option: ReturnType['duration_units_list'][0];
+ duration_values?: Record;
+ selected_duration: number;
+ show_expiry?: boolean;
+};
+
+const DurationNumbersWidgetMobile = observer(
+ ({
+ basis_option,
+ contract_expiry = 'intraday',
+ duration_unit_option,
+ duration_values,
+ expiry_epoch,
+ has_amount_error,
+ payout_value,
+ selected_duration,
+ setDurationError,
+ setSelectedDuration,
+ stake_value,
+ show_expiry = false,
+ toggleModal,
+ }: TNumber) => {
+ const { ui } = useStore();
+ const { addToast } = ui;
+ const {
+ duration_min_max,
+ duration: trade_duration,
+ duration_unit: trade_duration_unit,
+ basis: trade_basis,
+ amount: trade_amount,
+ onChangeMultiple,
+ } = useTraderStore();
+ const { value: duration_unit } = duration_unit_option;
+ const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry, duration_unit);
+ const [has_error, setHasError] = React.useState(false);
+ const localized_message = (
+
+ );
+ const validateDuration = (value: number | string) => {
+ if (
+ Number(value) < Number(min) ||
+ Math.trunc(selected_duration) > Number(max) ||
+ value.toString().length < 1
+ ) {
+ addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 });
+ setDurationError(true);
+ setHasError(true);
+ return 'error';
+ } else if (Number(value) > Number(max)) {
+ addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 });
+ setHasError(true);
+ return 'error';
+ }
+
+ setDurationError(false);
+ setHasError(false);
+ return true;
+ };
+
+ const setDuration = (duration: string | number) => {
+ const on_change_obj: Partial> = {};
+ // check for any amount changes from Amount trade params tab before submitting onChange object
+ if (!has_amount_error)
+ updateAmountChanges(on_change_obj, stake_value, payout_value, basis_option, trade_basis, trade_amount);
+
+ if (trade_duration !== Number(duration) || trade_duration_unit !== duration_unit) {
+ on_change_obj.duration_unit = duration_unit;
+ on_change_obj.duration = Number(duration);
+ on_change_obj.expiry_type = 'duration';
+ }
+
+ if (!isEmptyObject(on_change_obj)) onChangeMultiple(on_change_obj);
+ toggleModal();
+ };
+
+ const setExpiryDate = (epoch: number, duration: string | number) => {
+ if (trade_duration_unit !== 'd') {
+ return moment.utc().add(Number(duration), 'days').format('D MMM YYYY, [23]:[59]:[59] [GMT +0]');
+ }
+ let expiry_date = new Date((epoch - trade_duration * 24 * 60 * 60) * 1000);
+ if (duration) {
+ expiry_date = new Date(expiry_date.getTime() + Number(duration) * 24 * 60 * 60 * 1000);
+ }
+
+ return expiry_date
+ .toUTCString()
+ .replace('GMT', 'GMT +0')
+ .substring(5)
+ .replace(/(\d{2}) (\w{3} \d{4})/, '$1 $2,');
+ };
+
+ const onNumberChange = (num: number | string) => {
+ setSelectedDuration(duration_unit, Number(num));
+ validateDuration(num);
+ };
+
+ const fixed_date = !has_error ? setExpiryDate(Number(expiry_epoch), Number(duration_values?.d_duration)) : '';
+
+ const { name_plural, name } = getUnitMap()[duration_unit];
+ const duration_unit_text = name_plural ?? name;
+
+ return (
+
+
+
+ {show_expiry && }
+
+
{
+ return {v}
;
+ }}
+ pip_size={0}
+ submit_label={localize('OK')}
+ min={Number(min)}
+ max={Number(max)}
+ reset_press_interval={350}
+ reset_value=''
+ onValidate={validateDuration}
+ onValueChange={onNumberChange}
+ />
+
+ );
+ }
+);
+
+export default DurationNumbersWidgetMobile;
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-ticks-widget-mobile.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-ticks-widget-mobile.tsx
new file mode 100644
index 000000000000..655519b24892
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-ticks-widget-mobile.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { observer } from '@deriv/stores';
+import { useTraderStore } from 'Stores/useTraderStores';
+import { updateAmountChanges } from './duration-utils';
+import { getDurationMinMaxValues, isEmptyObject } from '@deriv/shared';
+import { localize } from '@deriv/translations';
+import { TickPicker } from '@deriv/components';
+import { TDurationMobile } from './duration-mobile';
+
+type TNumber = Pick<
+ TDurationMobile,
+ | 'expiry_epoch'
+ | 'has_amount_error'
+ | 'payout_value'
+ | 'setDurationError'
+ | 'setSelectedDuration'
+ | 'stake_value'
+ | 'toggleModal'
+> & {
+ basis_option: string;
+ contract_expiry?: string;
+ duration_unit_option: ReturnType['duration_units_list'][0];
+ duration_values?: Record;
+ selected_duration: number;
+ show_expiry?: boolean;
+};
+
+type TDurationTicksWidgetMobile = Omit<
+ TNumber,
+ 'expiry_epoch' | 'contract_expiry' | 'duration_unit_option' | 'show_expiry'
+>;
+const DurationTicksWidgetMobile = observer(
+ ({
+ setDurationError,
+ basis_option,
+ toggleModal,
+ has_amount_error,
+ payout_value,
+ stake_value,
+ selected_duration,
+ setSelectedDuration,
+ }: TDurationTicksWidgetMobile) => {
+ const {
+ duration_min_max,
+ duration: trade_duration,
+ duration_unit: trade_duration_unit,
+ basis: trade_basis,
+ amount: trade_amount,
+ onChangeMultiple,
+ } = useTraderStore();
+
+ React.useEffect(() => {
+ setDurationError(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const [min_tick, max_tick] = getDurationMinMaxValues(duration_min_max, 'tick', 't');
+
+ const setTickDuration = (value: { target: { value: number; name: string } }) => {
+ const { value: duration } = value.target;
+ const on_change_obj: Partial> = {};
+
+ // check for any amount changes from Amount trade params tab before submitting onChange object
+ if (!has_amount_error)
+ updateAmountChanges(on_change_obj, stake_value, payout_value, basis_option, trade_basis, trade_amount);
+
+ if (trade_duration !== duration || trade_duration_unit !== 't') {
+ on_change_obj.duration_unit = 't';
+ on_change_obj.duration = duration;
+ }
+
+ if (!isEmptyObject(on_change_obj)) onChangeMultiple(on_change_obj);
+ toggleModal();
+ };
+
+ const onTickChange = (tick: number) => setSelectedDuration('t', tick);
+ const tick_duration =
+ trade_duration < Number(min_tick) && selected_duration < Number(min_tick)
+ ? Number(min_tick)
+ : selected_duration;
+ return (
+
+
+
+ );
+ }
+);
+
+export default DurationTicksWidgetMobile;
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-utils.ts b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-utils.ts
new file mode 100644
index 000000000000..5f08822ee193
--- /dev/null
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-utils.ts
@@ -0,0 +1,26 @@
+import { useTraderStore } from 'Stores/useTraderStores';
+
+export const updateAmountChanges = (
+ obj: Partial>,
+ stake_value: number,
+ payout_value: number,
+ basis: string,
+ trade_basis: string,
+ trade_amount: number
+) => {
+ // TODO: Move onChangeMultiple outside of duration and amount
+ // and unify all trade parameter components to use same onMultipleChange func onSubmit
+ // Checks if Amount tab was changed to stake and stake value was updated
+ if (basis === 'stake' && stake_value !== trade_amount) {
+ obj.basis = 'stake';
+ obj.amount = stake_value;
+ // Checks if Amount tab was changed to payout and payout value was updated
+ } else if (basis === 'payout' && payout_value !== trade_amount) {
+ obj.basis = 'payout';
+ obj.amount = payout_value;
+ // Checks if Amount tab was changed but payout or stake value was not updated
+ } else if (trade_basis !== basis) {
+ obj.basis = basis;
+ obj.amount = trade_amount;
+ }
+};
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.tsx
index 1c60b473d909..ff62ace6ce9e 100644
--- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.tsx
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.tsx
@@ -30,7 +30,6 @@ const DurationWrapper = observer(({ is_minimized }: TDurationWrapper) => {
expiry_epoch,
expiry_time,
start_date,
- market_open_times,
onChange,
onChangeMultiple,
} = useTraderStore();
@@ -52,7 +51,6 @@ const DurationWrapper = observer(({ is_minimized }: TDurationWrapper) => {
getDurationFromUnit,
is_minimized,
is_advanced_duration,
- market_open_times,
onChange,
onChangeMultiple,
onChangeUiStore,
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx
index 9deb9ce9f69d..6b0c0c84ca60 100644
--- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx
+++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx
@@ -29,7 +29,6 @@ export type TDuration = {
hasDurationUnit: (duration_type: string, is_advanced: boolean) => boolean;
is_advanced_duration: TUIStore['is_advanced_duration'];
is_minimized?: boolean;
- market_open_times: TTradeStore['market_open_times'];
max_value: number | null;
min_value: number | null;
onChange: TTradeStore['onChange'];
@@ -56,7 +55,6 @@ const Duration = ({
hasDurationUnit,
is_advanced_duration,
is_minimized,
- market_open_times,
max_value,
min_value,
onChange,
@@ -202,7 +200,6 @@ const Duration = ({
expiry_list={expiry_list}
expiry_type={expiry_type}
getDurationFromUnit={getDurationFromUnit}
- market_open_times={market_open_times}
number_input_props={passthrough_props.number_input}
onChange={onChange}
onChangeUiStore={onChangeUiStore}