diff --git a/libraries/common/collections/media-providers/MediaProvider.js b/libraries/common/collections/media-providers/MediaProvider.js new file mode 100644 index 0000000000..e0c76d346a --- /dev/null +++ b/libraries/common/collections/media-providers/MediaProvider.js @@ -0,0 +1,69 @@ +import { logger } from '@shopgate/pwa-core/helpers'; +import styles from './style'; + +/** + * The MediaProvider base class. + */ +class MediaProvider { + /** + * Constructor. + */ + constructor() { + this.containers = new Map(); + } + + /** + * Optimizes video container to make it responsive. + * @param {NodeList} container A DOM container. + * @private + * @returns {MediaProvider} + */ + responsify(container) { + // Remove fixed dimensions from the container. + container.setAttribute('height', ''); + container.setAttribute('width', ''); + + // Create the wrapper and apply styling. + const wrapper = document.createElement('div'); + + wrapper.className = styles; + + // Add the wrapper right before the container into the DOM. + container.parentNode.insertBefore(wrapper, container); + // Move the container into the wrapper. + wrapper.appendChild(container); + + return this; + } + + /** + * Add a DOM container with embedded videos. + * @param {NodeList} container A DOM container. + * @returns {MediaProvider} + */ + add() { + logger.error('MediaProvider.add() needs to be implemented within an inheriting class'); + return this; + } + + /** + * Remove a DOM container. + * @param {NodeList} container A DOM container. + * @returns {MediaProvider} + */ + remove(container) { + this.containers.delete(container); + return this; + } + + /** + * Stops all playing videos within the DOM containers. + * @returns {MediaProvider} + */ + stop() { + logger.error('MediaProvider.stop() needs to be implemented within an inheriting class'); + return this; + } +} + +export default MediaProvider; diff --git a/libraries/common/collections/media-providers/MediaProvider.spec.js b/libraries/common/collections/media-providers/MediaProvider.spec.js new file mode 100644 index 0000000000..97acb30815 --- /dev/null +++ b/libraries/common/collections/media-providers/MediaProvider.spec.js @@ -0,0 +1,76 @@ +import { JSDOM } from 'jsdom'; +import { logger } from '@shopgate/pwa-core/helpers'; +import MediaProvider from './MediaProvider'; + +jest.mock('@shopgate/pwa-core/helpers', () => ({ + logger: { + error: jest.fn(), + }, +})); + +/** + * Creates a DOM container with iframes. + * @param {Array} srcs A list of video URLs. + * @return {Object} + */ +const createContainer = (srcs) => { + const html = srcs.map(src => ``).join(''); + return new JSDOM(html).window.document; +}; + +describe('MediaProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('.constructor()', () => { + it('should construct as expected', () => { + const instance = new MediaProvider(); + expect(instance.containers).toBeInstanceOf(Map); + expect(Array.from(instance.containers)).toHaveLength(0); + }); + }); + + describe('.add()', () => { + it('should log an error when it is not overwritten', () => { + const instance = new MediaProvider(); + instance.add(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + }); + + describe('.stop()', () => { + it('should log an error when it is not overwritten', () => { + const instance = new MediaProvider(); + instance.stop(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + }); + + describe('.remove()', () => { + it('should remove containers as expected', () => { + const instance = new MediaProvider(); + const containerOne = createContainer(['http://www.provider-one.com/video']); + const containerTwo = createContainer(['http://www.provider-two.com/video']); + + instance.containers.set(containerOne, containerOne); + instance.containers.set(containerTwo, containerTwo); + + instance.remove(containerTwo); + + expect(instance.containers.size).toBe(1); + expect(instance.containers.get(containerTwo)).toBeUndefined(); + expect(instance.containers.get(containerOne)).toEqual(containerOne); + + instance.remove(containerOne); + + expect(instance.containers.size).toBe(0); + }); + }); + + describe.skip('.responsify()', () => { + it('should optimize a container to be responsive', () => { + // TODO: Implement the test when a solution for the insertBefore issue was found. + }); + }); +}); diff --git a/libraries/common/collections/media-providers/Vimeo.js b/libraries/common/collections/media-providers/Vimeo.js index 7960285d60..058e1c48dd 100644 --- a/libraries/common/collections/media-providers/Vimeo.js +++ b/libraries/common/collections/media-providers/Vimeo.js @@ -1,14 +1,14 @@ -/* eslint-disable extra-rules/potential-point-free */ +import MediaProvider from './MediaProvider'; /** * The Vimeo media provider class. */ -class VimeoMediaProvider { +class VimeoMediaProvider extends MediaProvider { /** * Constructor. */ constructor() { - this.containers = new Map(); + super(); this.playerReady = false; this.deferred = []; @@ -17,7 +17,8 @@ class VimeoMediaProvider { /** * Checks if the Video player script is already available. - * If not, it injects it into the DOM and adds defferred containers. + * If not, it injects it into the DOM and adds deferred containers. + * @private */ checkPlayer() { if (typeof window.Vimeo !== 'undefined') { @@ -42,39 +43,38 @@ class VimeoMediaProvider { /** * Add a DOM container with embedded videos. + * @override * @param {NodeList} container A DOM container. + * @returns {VimeoMediaProvider} */ add(container) { if (!this.playerReady) { this.deferred.push(container); - return; + return this; } const iframes = container.querySelectorAll('iframe[src*="vimeo.com"]'); if (!iframes.length) { - return; + return this; } const players = []; iframes.forEach((iframe) => { + this.responsify(iframe); players.push(new window.Vimeo.Player(iframe)); }); this.containers.set(container, players); - } - /** - * Remove a DOM container. - * @param {NodeList} container A DOM container. - */ - remove(container) { - this.containers.delete(container); + return this; } /** * Stops all playing videos within the DOM containers. + * @override + * @returns {VimeoMediaProvider} */ stop() { this.containers.forEach((players) => { @@ -82,9 +82,9 @@ class VimeoMediaProvider { player.pause(); }); }); + + return this; } } -/* eslint-enable extra-rules/potential-point-free */ - export default VimeoMediaProvider; diff --git a/libraries/common/collections/media-providers/Vimeo.spec.js b/libraries/common/collections/media-providers/Vimeo.spec.js index 830b410b09..a248b9ed8d 100644 --- a/libraries/common/collections/media-providers/Vimeo.spec.js +++ b/libraries/common/collections/media-providers/Vimeo.spec.js @@ -49,6 +49,9 @@ describe('Vimeo media provider', () => { document.getElementsByTagName('html')[0].innerHTML = ''; instance = new Vimeo(); + // TODO Implement tests for the method when a solution for the insertBefore issue was found. + instance.responsify = jest.fn(); + playerScript = document.querySelector('script[src*="vimeo.com"]'); }; @@ -91,6 +94,8 @@ describe('Vimeo media provider', () => { it('should add multiple containers as expected', () => { const containerOne = createContainer([videos[0]]); const containerTwo = createContainer([videos[1]]); + const iframesOne = containerOne.querySelectorAll('iframe'); + const iframesTwo = containerTwo.querySelectorAll('iframe'); instance.add(containerOne); instance.add(containerTwo); @@ -99,6 +104,8 @@ describe('Vimeo media provider', () => { expect(instance.containers.size).toBe(2); expect(instance.containers.get(containerOne)).toEqual([expect.any(window.Vimeo.Player)]); expect(instance.containers.get(containerTwo)).toEqual([expect.any(window.Vimeo.Player)]); + expect(instance.responsify).toHaveBeenCalledWith(iframesOne[0]); + expect(instance.responsify).toHaveBeenCalledWith(iframesTwo[0]); }); it('should defer addition of a container if the player is not ready', () => { diff --git a/libraries/common/collections/media-providers/YouTube.js b/libraries/common/collections/media-providers/YouTube.js index 58c99651ee..cceb62ef7f 100644 --- a/libraries/common/collections/media-providers/YouTube.js +++ b/libraries/common/collections/media-providers/YouTube.js @@ -1,31 +1,28 @@ -/* eslint-disable extra-rules/potential-point-free */ import URLSearchParams from 'url-search-params'; +import MediaProvider from './MediaProvider'; /** * The YouTube media provider class. */ -class YouTubeMediaProvider { - /** - * Constructor. - */ - constructor() { - this.containers = new Map(); - } - +class YouTubeMediaProvider extends MediaProvider { /** * Add a DOM container with embedded videos. + * @override * @param {NodeList} container A DOM container. + * @returns {YouTubeMediaProvider} */ add(container) { const iframes = container .querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"]'); if (!iframes.length) { - return; + return this; } // Update the video urls to enable stopping videos via the event API. iframes.forEach((iframe, index) => { + this.responsify(iframe); + const { src } = iframe; const [url, query] = src.split('?'); @@ -40,18 +37,14 @@ class YouTubeMediaProvider { }); this.containers.set(container, iframes); - } - /** - * Remove a DOM container. - * @param {NodeList} container A DOM container. - */ - remove(container) { - this.containers.delete(container); + return this; } /** * Stops all playing videos within the DOM containers. + * @override + * @returns {YouTubeMediaProvider} */ stop() { this.containers.forEach((iframes) => { @@ -61,9 +54,9 @@ class YouTubeMediaProvider { } }); }); + + return this; } } -/* eslint-enable extra-rules/potential-point-free */ - export default YouTubeMediaProvider; diff --git a/libraries/common/collections/media-providers/YouTube.spec.js b/libraries/common/collections/media-providers/YouTube.spec.js index 363681b9ba..5f826a2494 100644 --- a/libraries/common/collections/media-providers/YouTube.spec.js +++ b/libraries/common/collections/media-providers/YouTube.spec.js @@ -15,7 +15,8 @@ const videos = [ * @return {Object} */ const createContainer = (srcs) => { - const html = srcs.map(src => ``).join(''); + const iframes = srcs.map(src => ``).join(''); + const html = `