setIsSliderHovered(false)}
+ onMouseOver={(): void => setIsSliderHovered(true)}
+ onMove={handleMouseMove}
onUpdate={onTimeChange}
step={5}
title={__('media_time_slider')}
diff --git a/src/lib/viewers/controls/media/__tests__/Filmstrip-test.tsx b/src/lib/viewers/controls/media/__tests__/Filmstrip-test.tsx
new file mode 100644
index 000000000..1e2023fa0
--- /dev/null
+++ b/src/lib/viewers/controls/media/__tests__/Filmstrip-test.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { shallow, ShallowWrapper } from 'enzyme';
+import Filmstrip from '../Filmstrip';
+
+describe('Filmstrip', () => {
+ const getWrapper = (props = {}): ShallowWrapper =>
+ shallow();
+
+ describe('render', () => {
+ test('should return a valid wrapper', () => {
+ const wrapper = getWrapper();
+ expect(wrapper.hasClass('bp-Filmstrip')).toBe(true);
+ });
+
+ test.each`
+ time | left | top
+ ${0} | ${-0} | ${-0}
+ ${1} | ${-180} | ${-0}
+ ${10} | ${-1800} | ${-0}
+ ${30} | ${-5400} | ${-0}
+ ${60} | ${-10800} | ${-0}
+ ${100} | ${-0} | ${-90}
+ ${110} | ${-1800} | ${-90}
+ ${500} | ${-0} | ${-450}
+ ${510} | ${-1800} | ${-450}
+ `('should display the frame position for time $time as $left/$top', ({ left, time, top }) => {
+ const wrapper = getWrapper({ time });
+ expect(wrapper.find('[data-testid="bp-Filmstrip-frame"]').prop('style')).toMatchObject({
+ backgroundImage: "url('https://app.box.com')",
+ backgroundPositionX: left,
+ backgroundPositionY: top,
+ });
+ });
+
+ test.each`
+ aspectRatio | width
+ ${undefined} | ${160}
+ ${0} | ${160}
+ ${1} | ${90}
+ ${2} | ${180}
+ `('should display the frame size for aspect ratio $aspectRatio as $width', ({ aspectRatio, width }) => {
+ const wrapper = getWrapper({ aspectRatio });
+ expect(wrapper.find('[data-testid="bp-Filmstrip-frame"]').prop('style')).toMatchObject({
+ height: 90,
+ width,
+ });
+ });
+
+ test('should display the correct filmstrip time', () => {
+ const wrapper = getWrapper({ time: 120 });
+ expect(wrapper.find('[data-testid="bp-Filmstrip-time"]').text()).toBe('2:00');
+ });
+
+ test('should display the crawler while the filmstrip image loads', done => {
+ const mockImage = document.createElement('img');
+
+ Object.defineProperty(mockImage, 'src', {
+ set() {
+ setTimeout(() => {
+ this.onload();
+ done();
+ });
+ },
+ });
+
+ jest.useFakeTimers();
+ jest.spyOn(document, 'createElement').mockImplementation(() => mockImage);
+ jest.spyOn(React, 'useEffect').mockImplementationOnce(func => func());
+
+ const wrapper = getWrapper();
+ expect(wrapper.exists('[data-testid="bp-Filmstrip-crawler"]')).toBe(true);
+
+ jest.advanceTimersByTime(0); // Simulate loading complete
+ expect(wrapper.exists('[data-testid="bp-Filmstrip-crawler"]')).toBe(false);
+ });
+ });
+});
diff --git a/src/lib/viewers/controls/media/__tests__/TimeControls-test.tsx b/src/lib/viewers/controls/media/__tests__/TimeControls-test.tsx
index 5bf186278..9795be555 100644
--- a/src/lib/viewers/controls/media/__tests__/TimeControls-test.tsx
+++ b/src/lib/viewers/controls/media/__tests__/TimeControls-test.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
+import Filmstrip from '../Filmstrip';
import SliderControl from '../../slider';
import TimeControls from '../TimeControls';
@@ -12,6 +13,30 @@ describe('TimeControls', () => {
const getWrapper = (props = {}): ShallowWrapper =>
shallow();
+ describe('event handlers', () => {
+ test('should update the slider hover state on mousemove', () => {
+ const wrapper = getWrapper({ filmstripInterval: 1 });
+
+ wrapper.find(SliderControl).simulate('move', 100, 1000, 10000); // Time, position, max position
+
+ expect(wrapper.find(Filmstrip).props()).toMatchObject({
+ position: 1000,
+ positionMax: 10000,
+ time: 100,
+ });
+ });
+
+ test('should update the slider hover state on mouseover and mouseout', () => {
+ const wrapper = getWrapper({ filmstripInterval: 1 });
+
+ wrapper.find(SliderControl).simulate('mouseover');
+ expect(wrapper.find(Filmstrip).prop('isShown')).toBe(true);
+
+ wrapper.find(SliderControl).simulate('mouseout');
+ expect(wrapper.find(Filmstrip).prop('isShown')).toBe(false);
+ });
+ });
+
describe('render', () => {
test('should return a valid wrapper', () => {
const wrapper = getWrapper();
@@ -47,5 +72,28 @@ describe('TimeControls', () => {
const wrapper = getWrapper({ bufferedRange: buffer, currentTime });
expect(wrapper.find(SliderControl).prop('track')).toEqual(track);
});
+
+ test('should render the filmstrip with the correct props', () => {
+ const wrapper = getWrapper({
+ aspectRatio: 1.5,
+ filmstripInterval: 2,
+ filmstripUrl: 'https://app.box.com',
+ });
+
+ expect(wrapper.find(Filmstrip).props()).toMatchObject({
+ aspectRatio: 1.5,
+ imageUrl: 'https://app.box.com',
+ interval: 2,
+ });
+ });
+
+ test('should not render the filmstrip if the interval is missing', () => {
+ const wrapper = getWrapper({
+ aspectRatio: 1.5,
+ imageUrl: 'https://app.box.com',
+ });
+
+ expect(wrapper.exists(Filmstrip)).toBe(false);
+ });
});
});
diff --git a/src/lib/viewers/media/DashControls.tsx b/src/lib/viewers/media/DashControls.tsx
index ac3739467..fecbe206c 100644
--- a/src/lib/viewers/media/DashControls.tsx
+++ b/src/lib/viewers/media/DashControls.tsx
@@ -16,15 +16,20 @@ export type Props = DurationLabelsProps &
PlayControlsProps &
SubtitlesToggleProps &
TimeControlsProps &
- VolumeControlsProps & { isPlayingHD?: boolean };
+ VolumeControlsProps & {
+ isPlayingHD?: boolean;
+ };
export default function DashControls({
audioTrack,
audioTracks,
autoplay,
+ aspectRatio,
bufferedRange,
currentTime,
durationTime,
+ filmstripInterval,
+ filmstripUrl,
isAutoGeneratedSubtitles,
isHDSupported,
isPlaying,
@@ -49,9 +54,12 @@ export default function DashControls({
return (
diff --git a/src/lib/viewers/media/DashViewer.js b/src/lib/viewers/media/DashViewer.js
index cc8733185..ac883f73f 100644
--- a/src/lib/viewers/media/DashViewer.js
+++ b/src/lib/viewers/media/DashViewer.js
@@ -28,6 +28,15 @@ class DashViewer extends VideoBaseViewer {
/** @property {Array