Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DTRA / Kate / DTRA-1475 / Implement Take profit trade param functionality #16224

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CarouselHeader from '../carousel-header';

jest.mock('@deriv/quill-icons', () => ({
...jest.requireActual('@deriv/quill-icons'),
LabelPairedArrowLeftMdRegularIcon: jest.fn(({ onClick }) => (
<button onClick={onClick}>LabelPairedArrowLeftMdRegularIcon</button>
)),
LabelPairedCircleInfoMdRegularIcon: jest.fn(({ onClick }) => (
<button onClick={onClick}>LabelPairedCircleInfoMdRegularIcon</button>
)),
}));

const mock_props = {
current_index: 0,
onNextClick: jest.fn(),
onPrevClick: jest.fn(),
title: <div>Title</div>,
};

describe('CarouselHeader', () => {
it('should render passed title and correct icon for passed index. If user clicks on info icon, onNextClick should be called', () => {
render(<CarouselHeader {...mock_props} />);

expect(screen.getByText('Title')).toBeInTheDocument();

const info_icon = screen.getByText('LabelPairedCircleInfoMdRegularIcon');
expect(info_icon).toBeInTheDocument();

expect(mock_props.onNextClick).not.toBeCalled();
userEvent.click(info_icon);
expect(mock_props.onNextClick).toBeCalled();
});

it('should render correct icon for passed index. If user clicks on arrow icon, onPrevClick should be called', () => {
render(<CarouselHeader {...mock_props} current_index={1} />);

const arrow_icon = screen.getByText('LabelPairedArrowLeftMdRegularIcon');
expect(arrow_icon).toBeInTheDocument();

expect(mock_props.onPrevClick).not.toBeCalled();
userEvent.click(arrow_icon);
expect(mock_props.onPrevClick).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { ActionSheet } from '@deriv-com/quill-ui';
import { LabelPairedArrowLeftMdRegularIcon, LabelPairedCircleInfoMdRegularIcon } from '@deriv/quill-icons';

type TCarouselHeaderProps = {
current_index: number;
onNextClick: () => void;
onPrevClick: () => void;
title?: React.ReactNode;
};

const CarouselHeader = ({ current_index, onNextClick, onPrevClick, title }: TCarouselHeaderProps) => (
<ActionSheet.Header
className='carousel-controls'
title={title}
icon={
current_index ? (
<LabelPairedArrowLeftMdRegularIcon onClick={onPrevClick} />
) : (
<LabelPairedCircleInfoMdRegularIcon onClick={onNextClick} />
)
}
iconPosition={current_index ? 'left' : 'right'}
/>
);

export default CarouselHeader;
6 changes: 6 additions & 0 deletions packages/trader/src/AppV2/Components/Carousel/carousel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@
transition: transform 0.24s ease-in-out;
}
}

.carousel-controls {
.quill-action-sheet--title--icon {
z-index: 2;
}
}
18 changes: 10 additions & 8 deletions packages/trader/src/AppV2/Components/Carousel/carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import React from 'react';
import CarouselHeader from './carousel-header';

type THeaderProps = {
current_index: number;
onNextClick: () => void;
onPrevClick: () => void;
};
type TCarousel = {
header: ({ current_index, onNextClick, onPrevClick }: THeaderProps) => JSX.Element;
header: typeof CarouselHeader;
pages: { id: number; component: JSX.Element }[];
title?: React.ReactNode;
};

const Carousel = ({ header, pages }: TCarousel) => {
const Carousel = ({ header, pages, title }: TCarousel) => {
const [current_index, setCurrentIndex] = React.useState(0);

const HeaderComponent = header;
Expand All @@ -20,7 +17,12 @@ const Carousel = ({ header, pages }: TCarousel) => {

return (
<React.Fragment>
<HeaderComponent current_index={current_index} onNextClick={onNextClick} onPrevClick={onPrevClick} />
<HeaderComponent
current_index={current_index}
onNextClick={onNextClick}
onPrevClick={onPrevClick}
title={title}
/>
<ul className='carousel'>
{pages.map(({ component, id }) => (
<li
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockStore } from '@deriv/stores';
import ModulesProvider from 'Stores/Providers/modules-providers';
import TraderProviders from '../../../../../trader-providers';
import TakeProfit from '../take-profit';

const take_profit_trade_param = 'Take profit';

const mediaQueryList = {
matches: true,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};

window.matchMedia = jest.fn().mockImplementation(() => mediaQueryList);

describe('TakeProfit', () => {
let default_mock_store: ReturnType<typeof mockStore>;

beforeEach(() => (default_mock_store = mockStore({})));

afterEach(() => jest.clearAllMocks());

const mockTakeProfit = () =>
render(
<TraderProviders store={default_mock_store}>
<ModulesProvider store={default_mock_store}>
<TakeProfit is_minimized />
</ModulesProvider>
</TraderProviders>
);

it('should render trade param with "Take profit" label', () => {
mockTakeProfit();

expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByText(take_profit_trade_param)).toBeInTheDocument();
});

it('should open ActionSheet with input, "Save" button and text content with definition if user clicks on trade param', () => {
mockTakeProfit();

expect(screen.queryByTestId('dt-actionsheet-overlay')).not.toBeInTheDocument();

userEvent.click(screen.getByText(take_profit_trade_param));

expect(screen.getByTestId('dt-actionsheet-overlay')).toBeInTheDocument();
const input = screen.getByRole('spinbutton');
expect(input).toBeInTheDocument();
expect(screen.getByText('Save')).toBeInTheDocument();
expect(
screen.getByText(
'When your profit reaches or exceeds the set amount, your trade will be closed automatically.'
)
).toBeInTheDocument();
});

it('should render alternative text content with definition for Accumulators', () => {
default_mock_store.modules.trade.is_accumulator = true;
mockTakeProfit();

userEvent.click(screen.getByText(take_profit_trade_param));

expect(screen.getByText('Note: Cannot be adjusted for ongoing accumulator contracts.')).toBeInTheDocument();
});

it('should enable input, when user clicks on ToggleSwitch', () => {
mockTakeProfit();

userEvent.click(screen.getByText(take_profit_trade_param));

const toggle_switcher = screen.getAllByRole('button')[0];
const input = screen.getByRole('spinbutton');

expect(input).toBeDisabled();
userEvent.click(toggle_switcher);
expect(input).toBeEnabled();
});

it('should enable input, when user clicks on Take Profit overlay', () => {
mockTakeProfit();

userEvent.click(screen.getByText(take_profit_trade_param));

const take_profit_overlay = screen.getByTestId('dt_take_profit_overlay');
const input = screen.getByRole('spinbutton');

expect(input).toBeDisabled();
userEvent.click(take_profit_overlay);
expect(input).toBeEnabled();
});

it('should validate values, that user typed, and show error text if they are out of acceptable range. If values are wrong, when user clicks on "Save" button onChangeMultiple and onChange will not be called', () => {
default_mock_store.modules.trade.validation_params = {
take_profit: {
min: '0.01',
max: '100',
},
};
mockTakeProfit();

userEvent.click(screen.getByText(take_profit_trade_param));

const toggle_switcher = screen.getAllByRole('button')[0];
userEvent.click(toggle_switcher);

const input = screen.getByRole('spinbutton');
userEvent.type(input, ' ');
expect(screen.getByText('Please enter a take profit amount.'));

const save_button = screen.getByText('Save');
userEvent.click(save_button);
expect(default_mock_store.modules.trade.onChangeMultiple).not.toBeCalled();
expect(default_mock_store.modules.trade.onChange).not.toBeCalled();

userEvent.type(input, '0.0002');
expect(screen.getByText('Acceptable range: 0.01 to 100'));

userEvent.click(save_button);
expect(default_mock_store.modules.trade.onChangeMultiple).not.toBeCalled();
expect(default_mock_store.modules.trade.onChange).not.toBeCalled();
});

it('should validate values, that user typed. In case if values are correct, when user clicks on "Save" button onChangeMultiple and onChange will be called', () => {
default_mock_store.modules.trade.validation_params = {
take_profit: {
min: '0.01',
max: '100',
},
};
mockTakeProfit();

userEvent.click(screen.getByText(take_profit_trade_param));

const toggle_switcher = screen.getAllByRole('button')[0];
userEvent.click(toggle_switcher);

const input = screen.getByRole('spinbutton');
userEvent.type(input, '2');
expect(screen.getByText('Acceptable range: 0.01 to 100'));

userEvent.click(screen.getByText('Save'));
expect(default_mock_store.modules.trade.onChangeMultiple).toBeCalled();
expect(default_mock_store.modules.trade.onChange).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './take-profit.scss';
import TakeProfit from './take-profit';

export default TakeProfit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { ActionSheet, Text } from '@deriv-com/quill-ui';
import { Localize } from '@deriv/translations';

const TakeProfitDescription = () => (
<ActionSheet.Content className='take-profit__wrapper--definition'>
<Text>
<Localize i18n_default_text='When your profit reaches or exceeds the set amount, your trade will be closed automatically.' />
</Text>
</ActionSheet.Content>
);

export default TakeProfitDescription;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { ActionSheet, CaptionText, Text, ToggleSwitch, TextFieldWithSteppers } from '@deriv-com/quill-ui';
import { Localize, localize } from '@deriv/translations';

type TTakeProfitInputProps = {
currency?: string;
decimals?: number;
error_message?: React.ReactNode;
is_enabled?: boolean;
is_accumulator?: boolean;
message?: React.ReactNode;
onToggleSwitch: (new_value: boolean) => void;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSave: () => void;
take_profit_value?: string | number;
};

const TakeProfitInput = React.forwardRef(
(
{
currency,
decimals,
error_message,
is_enabled,
is_accumulator,
message,
onToggleSwitch,
onInputChange,
onSave,
take_profit_value,
}: TTakeProfitInputProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
return (
<React.Fragment>
<ActionSheet.Content className='take-profit__wrapper'>
<div className='take-profit__content'>
<Text>
<Localize i18n_default_text='Take profit' />
</Text>
<ToggleSwitch checked={is_enabled} onChange={onToggleSwitch} />
</div>
<TextFieldWithSteppers
allowDecimals
disabled={!is_enabled}
decimals={decimals}
message={message}
name='take_profit'
onChange={onInputChange}
placeholder={localize('Amount')}
ref={ref}
status={error_message ? 'error' : 'neutral'}
textAlignment='center'
unitLeft={currency}
variant='fill'
value={take_profit_value}
/>
{!is_enabled && (
<button
className='take-profit__overlay'
onClick={() => onToggleSwitch(true)}
data-testid='dt_take_profit_overlay'
/>
)}
{is_accumulator && (
<CaptionText color='quill-typography__color--subtle' className='take-profit__accu-information'>
<Localize i18n_default_text='Note: Cannot be adjusted for ongoing accumulator contracts.' />
</CaptionText>
)}
</ActionSheet.Content>
<ActionSheet.Footer
alignment='vertical'
primaryAction={{
content: <Localize i18n_default_text='Save' />,
onAction: onSave,
}}
shouldCloseOnPrimaryButtonClick={false}
/>
</React.Fragment>
);
}
);

TakeProfitInput.displayName = 'TakeProfitInput';

export default TakeProfitInput;
Loading
Loading