Skip to content

Commit

Permalink
DTRA-1815 / Kate / [DTrader-V2]: Add Onboarding video for Draggable c…
Browse files Browse the repository at this point in the history
…omponent (Trade types selection) (binary-com#16759)

* refactor: prepare folder structure

* refactor: change structure of flags on clocalstorage

* chore: remove code smells

* refactor: create reusable stream iframe component

* refactor: add tests

* chore: remove coed smalls

* refactor: add id for teh video

* refactor: apply suggestions

* chore: remove extra waitfor in tests

* chore: fix test case
  • Loading branch information
kate-deriv committed Sep 10, 2024
1 parent b0aafe6 commit f4f9bf9
Show file tree
Hide file tree
Showing 27 changed files with 315 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AccumulatorStats from '../accumulator-stats';
import TraderProviders from '../../../../trader-providers';
import { mockStore, useStore } from '@deriv/stores';
import { mockStore } from '@deriv/stores';
import { TStores } from '@deriv/stores/types';

describe('AccumulatorStats', () => {
Expand Down Expand Up @@ -91,9 +91,7 @@ describe('AccumulatorStats', () => {
});
test('should set animationClass and isMovingTransition based on rows[0][0] changes', async () => {
renderAccumulatorState(default_mock_store);
jest.advanceTimersByTime(3000);
await waitFor(() => {
expect(screen.getByTestId('accumulator-first-stat')).toHaveClass('animate-success');
});
await waitFor(() => jest.advanceTimersByTime(300));
expect(screen.getByTestId('accumulator-first-stat')).toHaveClass('animate-success');
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import React from 'react';
import { ActionSheet, Text } from '@deriv-com/quill-ui';
import { Localize } from '@deriv/translations';
import { DESCRIPTION_VIDEO_ID, UNIFIED_MODE_VIDEO_ID } from 'Modules/Trading/Helpers/video-config';
import { UNIFIED_MODE_VIDEO_ID } from 'Modules/Trading/Helpers/video-config';
import StreamIframe from '../StreamIframe';

const AccumulatorStatsDescription = ({ onActionSheetClose }: { onActionSheetClose: () => void }) => {
return (
<ActionSheet.Portal showHandlebar={false}>
<div className='stats-description'>
<div className='stats-description__player-wrapper'>
<div className='stats-description__player-wrapper__player'>
<iframe
allowFullScreen={false}
width='100%'
height='100%'
src={`https://iframe.cloudflarestream.com/${UNIFIED_MODE_VIDEO_ID.accumulator_stats}?muted=true&controls=false&autoplay=true&loop=true&preload=auto`}
/>
</div>
</div>
<StreamIframe src={UNIFIED_MODE_VIDEO_ID.accumulator_stats} title='accumulator_stats' />
<div className='stats-description__content'>
<div className='stats-description__content__title'>
<Text size='lg' bold>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,20 +172,7 @@
}

.stats-description {
text-align: left;
&__player-wrapper {
position: relative;
width: 100%;
padding-top: 56.25%; // 16:9 aspect ratio (9/16 = 0.5625 or 56.25%)
overflow: hidden;
&__player {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
text-align: start;

&__content {
margin: var(--core-spacing-1200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import OnboardingGuide from '../onboarding-guide';
const trading_modal_text = 'Welcome to the new Deriv Trader';
const positions_modal_text = 'View your positions';
const guide_container = 'GuideContainer';
const localStorage_key = 'guide_dtrader_v2';

jest.mock('../guide-container', () =>
jest.fn(({ should_run }: { should_run?: boolean }) => <div>{should_run && guide_container}</div>)
Expand Down Expand Up @@ -62,43 +63,43 @@ describe('OnboardingGuide', () => {
});

it('should close the Modal for positions page, set flag to localStorage equal to true and do NOT start the guide after user clicks on "Got it" button', async () => {
const key = 'guide_dtrader_v2_positions_page';
const field = 'positions_page';
jest.useFakeTimers();
render(<OnboardingGuide type='positions_page' />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(localStorage.getItem(key)).toBe('false');
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);

userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));

expect(screen.queryByText(positions_modal_text)).not.toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(localStorage.getItem(key)).toBe('true');
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);

jest.useRealTimers();
});

it('should close the Modal for trading page and set flag to localStorage equal to true if user clicks on overlay and do NOT start the guide', async () => {
const key = 'guide_dtrader_v2_trade_page';
const field = 'trade_page';
jest.useFakeTimers();
render(<OnboardingGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(localStorage.getItem(key)).toBe('false');
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);

userEvent.click(screen.getByTestId('dt-actionsheet-overlay'));
await waitFor(() => jest.advanceTimersByTime(300));

expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(localStorage.getItem(key)).toBe('true');
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);

jest.useRealTimers();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ const OnboardingGuide = ({ type = 'trade_page' }: TOnboardingGuideProps) => {
const guide_timeout_ref = React.useRef<ReturnType<typeof setTimeout>>();
const is_button_clicked_ref = React.useRef(false);

const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<boolean>(`guide_dtrader_v2_${type}`, false);
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2', {
trade_types_selection: false,
trade_page: false,
positions_page: false,
});

const is_trade_page_guide = type === 'trade_page';

const onFinishGuide = React.useCallback(() => {
setShouldRunGuide(false);
setGuideDtraderV2(true);
setGuideDtraderV2({ ...guide_dtrader_v2, [type]: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setGuideDtraderV2]);

const onGuideSkip = () => {
Expand Down Expand Up @@ -57,10 +63,11 @@ const OnboardingGuide = ({ type = 'trade_page' }: TOnboardingGuideProps) => {
};

React.useEffect(() => {
if (!guide_dtrader_v2) guide_timeout_ref.current = setTimeout(() => setIsModalOpen(true), 800);
if (!guide_dtrader_v2?.[type]) guide_timeout_ref.current = setTimeout(() => setIsModalOpen(true), 800);

return () => clearTimeout(guide_timeout_ref.current);
}, [guide_dtrader_v2]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [guide_dtrader_v2?.[type]]);

return (
<React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TradeTypesSelectionGuide from '../trade-types-selection-guide';

const modal_text = 'Pin, rearrange, or remove your favorite trade types for easy access.';
const localStorage_key = 'guide_dtrader_v2';
const video = 'Video';

jest.mock('../../../StreamIframe', () => jest.fn(() => <div>{video}</div>));

describe('TradeTypesSelectionGuide', () => {
beforeEach(() => {
localStorage.clear();
});

it('should render Modal with correct content after 800ms after mounting', async () => {
jest.useFakeTimers();
render(<TradeTypesSelectionGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(video)).toBeInTheDocument();
expect(screen.getByText(modal_text)).toBeInTheDocument();

jest.useRealTimers();
});

it('should not render Modal if there is a flag in the localStorage equal to true', async () => {
const field = { trade_types_selection: true };
localStorage.setItem(localStorage_key, JSON.stringify(field));

jest.useFakeTimers();
render(<TradeTypesSelectionGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.queryByText(video)).not.toBeInTheDocument();
expect(screen.queryByText(modal_text)).not.toBeInTheDocument();

jest.useRealTimers();
});

it('should close the Modal and set flag to localStorage equal to true after user clicks on "Got it" button', async () => {
const field = 'trade_types_selection';
jest.useFakeTimers();
render(<TradeTypesSelectionGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(video)).toBeInTheDocument();
expect(screen.getByText(modal_text)).toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);

userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));

expect(screen.queryByText(video)).not.toBeInTheDocument();
expect(screen.queryByText(modal_text)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);

jest.useRealTimers();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TradeTypesSelectionGuide from './trade-types-selection-guide';

export default TradeTypesSelectionGuide;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { Modal } from '@deriv-com/quill-ui';
import { useLocalStorageData } from '@deriv/hooks';
import { Localize } from '@deriv/translations';
import { UNIFIED_MODE_VIDEO_ID } from 'Modules/Trading/Helpers/video-config';
import StreamIframe from '../../StreamIframe';

const TradeTypesSelectionGuide = () => {
const [is_modal_open, setIsModalOpen] = React.useState(false);
const guide_timeout_ref = React.useRef<ReturnType<typeof setTimeout>>();

const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2', {
trade_types_selection: false,
trade_page: false,
positions_page: false,
});
const { trade_types_selection } = guide_dtrader_v2 || {};

const onFinishGuide = () => {
setIsModalOpen(false);
setGuideDtraderV2({ ...guide_dtrader_v2, trade_types_selection: true });
};

React.useEffect(() => {
if (!trade_types_selection) guide_timeout_ref.current = setTimeout(() => setIsModalOpen(true), 800);

return () => clearTimeout(guide_timeout_ref.current);
}, [trade_types_selection]);

if (trade_types_selection) return null;

return (
<Modal
isOpened={is_modal_open}
isNonExpandable
isMobile
showHandleBar={false}
toggleModal={onFinishGuide}
primaryButtonLabel={<Localize i18n_default_text='Got it' />}
primaryButtonCallback={onFinishGuide}
>
<Modal.Header
image={<StreamIframe src={UNIFIED_MODE_VIDEO_ID.trade_type_selection} title='trade_types_selection' />}
title={<Localize i18n_default_text='Manage your trade types' />}
/>
<Modal.Body>
<Localize i18n_default_text='Pin, rearrange, or remove your favorite trade types for easy access.' />
</Modal.Body>
</Modal>
);
};

export default React.memo(TradeTypesSelectionGuide);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { UNIFIED_MODE_VIDEO_ID } from 'Modules/Trading/Helpers/video-config';
import StreamIframe from '../stream-iframe';

const video_stream_testid = 'dt_video_stream';

const mocked_props = {
src: UNIFIED_MODE_VIDEO_ID.accumulator_stats,
test_id: video_stream_testid,
title: 'accumulator_stats',
};

describe('StreamIframe component', () => {
it('should render iframe with a video with no controls & autoplay & looped & muted & preloaded & letterbox_color === transparent & width and height === 100% by default', () => {
render(<StreamIframe {...mocked_props} />);
const iframe = screen.getByTestId(video_stream_testid);
const iframe_src = iframe.getAttribute('src');

expect(
iframe_src?.includes(
'letterboxColor=transparent&muted=true&preload=auto&loop=true&autoplay=true&controls=false'
)
).toBeTruthy();
expect(iframe).toHaveAttribute('width', '100%');
expect(iframe).toHaveAttribute('height', '100%');
});

it('should render iframe with a video with controls if a proper prop was passed', () => {
render(<StreamIframe {...mocked_props} controls />);
const iframe_src = screen.getByTestId(video_stream_testid).getAttribute('src');

expect(iframe_src?.includes('controls=true')).toBeTruthy();
});

it('should render iframe with a non-looped video if a proper prop was passed', () => {
render(<StreamIframe {...mocked_props} loop={false} />);
const iframe_src = screen.getByTestId(video_stream_testid).getAttribute('src');

expect(iframe_src?.includes('loop=false')).toBeTruthy();
});

it('should render iframe with a non-autoplaying video if a proper prop was passed', () => {
render(<StreamIframe {...mocked_props} autoplay={false} />);
const iframe_src = screen.getByTestId(video_stream_testid).getAttribute('src');

expect(iframe_src?.includes('autoplay=false')).toBeTruthy();
});

it('should render iframe with a non-muted video if a proper prop was passed', () => {
render(<StreamIframe {...mocked_props} muted={false} />);
const iframe_src = screen.getByTestId(video_stream_testid).getAttribute('src');

expect(iframe_src?.includes('muted=false')).toBeTruthy();
});
});
4 changes: 4 additions & 0 deletions packages/trader/src/AppV2/Components/StreamIframe/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './stream-iframe.scss';
import StreamIframe from './stream-iframe';

export default StreamIframe;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.stream {
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__wrapper {
position: relative;
width: 100%;
padding-top: 56.25%; // 16:9 aspect ratio (9/16 = 0.5625 or 56.25%)
overflow: hidden;
}
}
Loading

0 comments on commit f4f9bf9

Please sign in to comment.