Skip to content

Commit

Permalink
[DTRA] / Kate / DTRA-253 / Create a custom video player (deriv-com#12443
Browse files Browse the repository at this point in the history
)

* feat: create play stop controller

* refactor: improve volume controller

* fix: ui and volume bugs

* refactor: functions helpers

* refactor: remove extra animation

* refactor: apply first part of suggestions

* refactor: upgrade dropdown component

* refactor: remove inline style

* refactor: add more ref

* refactor: add flag for animation instead of inline style

* refactor: add extra ref and remove inline style for volume control

* fix: revert browser check

* feat: add scroll functionality

* refactor: recalculate scroll heigh

* chore: fix typo

* chore: apply suggestions

* refactor: make srcrollbar visible for player

* refactor: add tests for overlay, volume and playback rate files

* refactor: add tests for video control file and apply suggestion

* refactor: style nesting

* refactor: remove sonarcloud smell

* refactor: get rid of extra ref usage

* refactor: add tests for browser detect function

* refactor: add tests for formatduration function

* refactor: typos

* Maryia/test: VideoPlayer + optimization (#48)

* test: VideoPlayer

* refactor: type TMockedStreamProps

* test: VideoPlayer

* chore: optimization & improvements

* test: add test for video player (replay feature)

* refactor: rename refs

* chore: voluma and animation fixes

* chore: volume control functions refactor

* refactor: change rounding

* fix: autoplaying after switching tabs

* refactor: improve animation

* fix: gap berween drag dot and progress bar

* refactor: reworked animationframe

* refactor: remove use effect

* refactor: improve perfomance

* fix: Safari issues for long living tabs

* refactor: ad debounce and cleaning

* refactor: ad debounce and cleaning

* refactor: fix tests

* fix: mobile control pannel

* fix: first launch time issue

* refactor: change type of th check for rewind

* refactor: apply suggestions

---------

Co-authored-by: Maryia <103177211+maryia-deriv@users.noreply.github.com>
  • Loading branch information
kate-deriv and maryia-deriv authored Feb 16, 2024
1 parent db4e225 commit f8aedb6
Show file tree
Hide file tree
Showing 32 changed files with 1,503 additions and 203 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions packages/components/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type TDropdown = {
onClick?: () => void;
placeholder?: string;
suffix_icon?: string;
should_open_on_hover?: boolean;
should_scroll_to_selected?: boolean;
should_autohide?: boolean;
test_id?: string;
Expand Down Expand Up @@ -275,6 +276,7 @@ const Dropdown = ({
onClick,
placeholder,
suffix_icon,
should_open_on_hover = false,
should_scroll_to_selected,
should_autohide,
test_id,
Expand Down Expand Up @@ -341,7 +343,14 @@ const Dropdown = ({
handleVisibility();
};

const handleVisibility = () => {
const handleVisibility = (e?: React.MouseEvent<HTMLDivElement>) => {
if (e && ['mouseover', 'mouseleave'].includes(e.type)) {
if (!should_open_on_hover) return;
if (e.type === 'mouseover') setIsListVisible(true);
else setIsListVisible(false);
return;
}

if (typeof onClick === 'function') {
onClick();

Expand Down Expand Up @@ -447,7 +456,13 @@ const Dropdown = ({
data-testid={test_id}
value={value || 0}
/>
<div ref={wrapper_ref} className={containerClassName()}>
<div
ref={wrapper_ref}
className={containerClassName()}
onMouseOver={handleVisibility}
onFocus={() => null}
onMouseLeave={handleVisibility}
>
<div
className={classNames('dc-dropdown__container', {
'dc-dropdown__container--suffix-icon': suffix_icon,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/components/src/components/icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ import './common/ic-pix-dark.svg';
import './common/ic-pix-light.svg';
import './common/ic-play-outline.svg';
import './common/ic-play.svg';
import './common/ic-playback-rate.svg';
import './common/ic-poa-error.svg';
import './common/ic-poa-file-clear.svg';
import './common/ic-poa-file-eight-mb.svg';
Expand Down Expand Up @@ -556,6 +557,7 @@ import './common/ic-red-warning.svg';
import './common/ic-redo.svg';
import './common/ic-regulatory-information.svg';
import './common/ic-remove-token.svg';
import './common/ic-replay.svg';
import './common/ic-reports.svg';
import './common/ic-reset.svg';
import './common/ic-sample-credit-card.svg';
Expand All @@ -576,6 +578,8 @@ import './common/ic-share.svg';
import './common/ic-skrill-dark.svg';
import './common/ic-skrill-light.svg';
import './common/ic-sort.svg';
import './common/ic-sound-off.svg';
import './common/ic-sound-on.svg';
import './common/ic-stage1.svg';
import './common/ic-stage2.svg';
import './common/ic-stage3.svg';
Expand Down
4 changes: 4 additions & 0 deletions packages/components/stories/icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ export const icons =
'IcPixLight',
'IcPlayOutline',
'IcPlay',
'IcPlaybackRate',
'IcPoaError',
'IcPoaFileClear',
'IcPoaFileEightMb',
Expand Down Expand Up @@ -565,6 +566,7 @@ export const icons =
'IcRedo',
'IcRegulatoryInformation',
'IcRemoveToken',
'IcReplay',
'IcReports',
'IcReset',
'IcSampleCreditCard',
Expand All @@ -585,6 +587,8 @@ export const icons =
'IcSkrillDark',
'IcSkrillLight',
'IcSort',
'IcSoundOff',
'IcSoundOn',
'IcStage1',
'IcStage2',
'IcStage3',
Expand Down
43 changes: 43 additions & 0 deletions packages/shared/src/utils/browser/__tests__/browser_detect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isSafariBrowser } from '../browser_detect';

describe('isSafariBrowser', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should return false for Chrome browser', () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
configurable: true,
});

expect(isSafariBrowser()).toBe(false);
});

it('should detect Safari browser', () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
configurable: true,
});

expect(isSafariBrowser()).toBe(true);
});

it('should return false for FireFox browser', () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
configurable: true,
});

expect(isSafariBrowser()).toBe(false);
});

it('should return false for Opera browser', () => {
Object.defineProperty(window.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Opera/121.0',
configurable: true,
});

expect(isSafariBrowser()).toBe(false);
});
});
18 changes: 18 additions & 0 deletions packages/shared/src/utils/browser/browser_detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,21 @@ export const isSafari = () => {
})(!window.safari || (typeof window.safari !== 'undefined' && window.safari.pushNotification))
);
};

const getUserBrowser = () => {
// We can't rely only on navigator.userAgent.index, the verification order is also important
if ((navigator.userAgent.indexOf('Opera') || navigator.userAgent.indexOf('OPR')) !== -1) {
return 'Opera';
} else if (navigator.userAgent.indexOf('Edg') !== -1) {
return 'Edge';
} else if (navigator.userAgent.indexOf('Chrome') !== -1) {
return 'Chrome';
} else if (navigator.userAgent.indexOf('Safari') !== -1) {
return 'Safari';
} else if (navigator.userAgent.indexOf('Firefox') !== -1) {
return 'Firefox';
}
return 'unknown';
};

export const isSafariBrowser = () => getUserBrowser() === 'Safari';
17 changes: 17 additions & 0 deletions packages/shared/src/utils/helpers/__tests__/durations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,20 @@ describe('convertDurationLimit', () => {
expect(Duration.convertDurationLimit(86400, 'd')).toEqual(1);
});
});

describe('formatDurationTime', () => {
it('should return dummy duration if time was not passed', () => {
expect(Duration.formatDurationTime()).toBe('00:00');
});

it('should return dummy duration if time is equal to 0', () => {
expect(Duration.formatDurationTime(0)).toBe('00:00');
});

it('should return correct duration', () => {
expect(Duration.formatDurationTime(3)).toBe('00:03');
expect(Duration.formatDurationTime(33)).toBe('00:33');
expect(Duration.formatDurationTime(60)).toBe('01:00');
expect(Duration.formatDurationTime(130)).toBe('02:10');
});
});
14 changes: 14 additions & 0 deletions packages/shared/src/utils/helpers/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,17 @@ export const getDurationMinMaxValues = (

return [min_value, max_value];
};

const formatDisplayedTime = (time_unit: number) => (time_unit < 10 ? `0${time_unit}` : time_unit);

export const formatDurationTime = (time?: number) => {
if (time && !isNaN(time)) {
const minutes = Math.floor(time / 60);
const format_minutes = formatDisplayedTime(minutes);
const seconds = Math.floor(time % 60);
const format_seconds = formatDisplayedTime(seconds);
return `${format_minutes}:${format_seconds}`;
}

return '00:00';
};
1 change: 1 addition & 0 deletions packages/trader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"webpack-node-externals": "^2.5.2"
},
"dependencies": {
"@cloudflare/stream-react": "^1.9.1",
"@deriv-com/analytics": "1.4.10",
"@deriv/components": "^1.0.0",
"@deriv/deriv-api": "^1.0.15",
Expand Down
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 PlaybackRateControl from '../playback-rate-control';

const mocked_props = {
onPlaybackRateChange: jest.fn(),
playback_rate: 1,
};

const default_selected_item = 'Normal';
const playback_rate_list = ['0.25x', '0.5x', '0.75x', default_selected_item, '1.5x', '2.0x'];

describe('<PlaybackRateControl />', () => {
it('should render the component', () => {
const { container } = render(<PlaybackRateControl {...mocked_props} />);

expect(container).not.toBeEmptyDOMElement();
});

it('should render all available options of playback rate if user clicked on default one', () => {
render(<PlaybackRateControl {...mocked_props} />);

playback_rate_list.forEach(item =>
item === default_selected_item
? expect(screen.getByText(item)).toBeInTheDocument()
: expect(screen.queryByText(item)).not.toBeInTheDocument()
);
userEvent.click(screen.getByText(default_selected_item));

playback_rate_list.forEach(item =>
item === default_selected_item
? expect(screen.getAllByText(item)).toHaveLength(2)
: expect(screen.getByText(item)).toBeInTheDocument()
);
});

it('should call onPlaybackRateChange if user chooses another option', () => {
render(<PlaybackRateControl {...mocked_props} />);

expect(mocked_props.onPlaybackRateChange).not.toBeCalled();
userEvent.click(screen.getByText(default_selected_item));
userEvent.click(screen.getByText(playback_rate_list[0]));

expect(mocked_props.onPlaybackRateChange).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import VideoControls from '../video-controls';

const mocked_props: React.ComponentProps<typeof VideoControls> = {
current_time: 3,
dragStartHandler: jest.fn(),
is_animated: true,
is_ended: false,
is_playing: true,
is_muted: false,
onRewind: jest.fn(),
onVolumeChange: jest.fn(),
onPlaybackRateChange: jest.fn(),
progress_bar_filled_ref: { current: null },
progress_bar_ref: { current: null },
progress_dot_ref: { current: null },
show_controls: true,
togglePlay: jest.fn(),
toggleMute: jest.fn(),
video_duration: 33,
volume: 0.5,
playback_rate: 1,
};

const volume_control = 'VolumeControl component';
const playback_rate_control = 'PlaybackRateControl component';
const progress_bar = 'dt_progress_bar';
const progress_bar_filled = 'dt_progress_bar_filled';
const progress_bar_dot = 'dt_progress_bar_dot';

jest.mock('../volume-control', () => jest.fn(() => <div>{volume_control}</div>));
jest.mock('../playback-rate-control', () => jest.fn(() => <div>{playback_rate_control}</div>));

describe('<VideoControls />', () => {
it('should render the component', () => {
render(<VideoControls {...mocked_props} />);

expect(screen.getByTestId(progress_bar)).toBeInTheDocument();
expect(screen.getByTestId(progress_bar_filled)).toBeInTheDocument();
expect(screen.getByText(/00:03 \/ 00:33/i)).toBeInTheDocument();
expect(screen.getByText(volume_control)).toBeInTheDocument();
expect(screen.getByText(playback_rate_control)).toBeInTheDocument();
});

it('should render drag dot on progress bar mouseover event and hide it on mouseleave event', () => {
render(<VideoControls {...mocked_props} />);
const progress = screen.getByTestId(progress_bar);

expect(screen.queryByTestId(progress_bar_dot)).not.toBeInTheDocument();
fireEvent.mouseOver(progress);
expect(screen.getByTestId(progress_bar_dot)).toBeInTheDocument();

fireEvent.mouseLeave(progress);
expect(screen.queryByTestId(progress_bar_dot)).not.toBeInTheDocument();
});

it('should always render drag dot on mobile', () => {
render(<VideoControls {...mocked_props} is_mobile />);

expect(screen.getByTestId(progress_bar_dot)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import VideoOverlay from '../video-overlay';

const mocked_props = {
onClick: jest.fn(),
};

const icon_testid = 'dt_player_overlay_icon';

describe('<VideoOverlay />', () => {
it('should render the component', () => {
const { container } = render(<VideoOverlay {...mocked_props} />);

expect(container).not.toBeEmptyDOMElement();
});

it('should pass correct size to icon for desktop', () => {
render(<VideoOverlay {...mocked_props} />);

expect(screen.getByTestId(icon_testid)).toHaveAttribute('height', '128');
});

it('should pass correct size to icon for mobile', () => {
render(<VideoOverlay {...mocked_props} is_mobile />);

expect(screen.getByTestId(icon_testid)).toHaveAttribute('height', '88');
});
});
Loading

0 comments on commit f8aedb6

Please sign in to comment.