From 700562996114be92e5cb03d42b33dce575f64d7d Mon Sep 17 00:00:00 2001 From: Josep Boix Requesens Date: Wed, 5 Jun 2024 10:59:51 +0200 Subject: [PATCH] feat(playlist-plugin): introduce repeat one mode - Introduced a new "repeat one" mode to the playlist plugin. - Modified the UI to include a toggle button that cycles through three modes: no repeat, repeat all, and repeat one. BREAKING CHANGE: - Changed the API to toggle the repeat mode and the initialization options. - The repeat mode is no longer a boolean. It is now a number representing each mode (0: no repeat, 1: repeat all, 2: repeat one). --- packages/pillarbox-playlist/README.md | 51 ++- packages/pillarbox-playlist/index.html | 3 +- .../scss/pillarbox-playlist.scss | 18 ++ .../modal/pillarbox-playlist-repeat-button.js | 18 +- .../components/pillarbox-playlist-button.js | 2 - packages/pillarbox-playlist/src/lang/de.json | 6 +- packages/pillarbox-playlist/src/lang/en.json | 4 +- packages/pillarbox-playlist/src/lang/fr.json | 4 +- packages/pillarbox-playlist/src/lang/it.json | 4 +- packages/pillarbox-playlist/src/lang/rm.json | 4 +- .../src/pillarbox-playlist-ui.js | 2 + .../src/pillarbox-playlist.js | 78 ++++- .../test/pillarbox-playlist-ui.spec.js | 93 ++++-- .../test/pillarbox-playlist.spec.js | 305 +++++++++++++----- 14 files changed, 442 insertions(+), 150 deletions(-) diff --git a/packages/pillarbox-playlist/README.md b/packages/pillarbox-playlist/README.md index 0692e95..9073d04 100644 --- a/packages/pillarbox-playlist/README.md +++ b/packages/pillarbox-playlist/README.md @@ -25,19 +25,34 @@ Once the player is installed you can activate the plugin as follows: ```javascript import Pillarbox from '@srgssr/pillarbox-web'; -import '@srgssr/pillarbox-playlist'; +import { RepeatMode } from '@srgssr/pillarbox-playlist'; import '@srgssr/pillarbox-playlist/ui'; const player = new Pillarbox('my-player', { - plugins: { - pillarboxPlaylist: { autoadvance: true, repeat: true }, + plugins: { + pillarboxPlaylist: { + autoadvance: true, + repeat: RepeatMode.REPEAT_ALL + }, pillarboxPlaylistUI: true } }); const playlist = [ - { sources: [{ src: 'video1.mp4', type: 'video/mp4' }], poster: 'poster1.jpg' }, - { sources: [{ src: 'video2.mp4', type: 'video/mp4' }], poster: 'poster2.jpg' } + { + sources: [{ + src: 'video1.mp4', + type: 'video/mp4' + }], + poster: 'poster1.jpg' + }, + { + sources: [{ + src: 'video2.mp4', + type: 'video/mp4' + }], + poster: 'poster2.jpg' + } ]; player.playlistPlugin().load(playlist); @@ -71,7 +86,7 @@ The following table outlines the key methods available in the this plugin: | `previous()` | Navigates to the previous item or restarts the current item based on playback position and threshold. | | `shuffle()` | Randomizes the order of the playlist items using the Fisher-Yates shuffle algorithm. | | `select(index)` | Selects and plays the item at the specified index in the playlist. | -| `toggleRepeat(force?)` | Toggles the repeat mode of the player to the opposite of its current state, or sets it to the specified boolean value if provided. | +| `toggleRepeat(force?)` | Cycles through the repeat mode of the player, or sets it to the specified value if provided. | | `toggleAutoadvance(force?)` | Toggles the auto-advance mode of the player to the opposite of its current state, or sets it to the specified boolean value if provided. | #### Options @@ -82,7 +97,7 @@ behavior of the plugin. Here are the available options: | Option | Type | Default | Description | |-------------------------------|---------|---------|---------------------------------------------------------------------------------------------| | `playlist` | Array | `[]` | An array of playlist items to be initially loaded into the player. | -| `repeat` | Boolean | `false` | If true, the playlist will start over automatically after the last item ends. | +| `repeat` | Number | 0 | Set the repeat mode of the playlist: 0 - No Repeat, 1 - Repeat All, 2 - Repeat one. | | `autoadvance` | Boolean | `false` | If enabled, the player will automatically move to the next item after the current one ends. | | `previousNavigationThreshold` | Number | 3 | Threshold in seconds for determining the behavior when navigating to the previous item. | @@ -91,11 +106,11 @@ behavior of the plugin. Here are the available options: After initializing the plugin, you can modify or read these properties to control playlist behavior dynamically: -| Property | Type | Description | -|-------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `repeat` | Boolean | Enables or disables repeating the playlist once the last item has played. Changes take effect immediately and apply to subsequent operations. | -| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | -| `previousNavigationThreshold` | Number | Threshold in seconds for determining the behavior when navigating to the previous item. | +| Property | Type | Description | +|-------------------------------|---------|----------------------------------------------------------------------------------------------| +| `repeat` | Number | Changes the repeat mode of the playlist: 0 - No Repeat, 1 - Repeat All, 2 - Repeat one. . | +| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | +| `previousNavigationThreshold` | Number | Threshold in seconds for determining the behavior when navigating to the previous item. | The following properties are read-only: @@ -105,6 +120,16 @@ The following properties are read-only: | `currentItem` | Object | Retrieves the currently playing item. | | `items` | Array | Retrieves all items in the playlist. Modifications to the returned array will not affect the internal state of the playlist. | +#### Constants + +The following table outlines the key constants available in this plugin: + +| Constant | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `RepeatMode.NO_REPEAT` | Disables repeat mode. | +| `RepeatMode.REPEAT_ALL` | Loops the entire playlist. Once the last element of the playlist ends, the next element will be the first one. This mode only works forwards, i.e., when advancing to the next element. | +| `RepeatMode.REPEAT_ONE` | Loops the currently playing item in the playlist. | + #### Events The following event is emitted by the playlist plugin: @@ -158,7 +183,7 @@ import '@srgssr/pillarbox-playlist'; import '@srgssr/pillarbox-playlist/ui'; const player = new Pillarbox('my-player', { - plugins: { + plugins: { // Include the playlist plugin pillarboxPlaylist: true, // Include the playlist UI plugin diff --git a/packages/pillarbox-playlist/index.html b/packages/pillarbox-playlist/index.html index 11f8a6c..04ac432 100644 --- a/packages/pillarbox-playlist/index.html +++ b/packages/pillarbox-playlist/index.html @@ -26,6 +26,7 @@ import { default as pillarbox, SrgSsr} from '@srgssr/pillarbox-web'; import './src/pillarbox-playlist.js'; import './src/pillarbox-playlist-ui.js'; + import { RepeatMode } from './src/pillarbox-playlist.js'; // Handle URL parameters const searchParams = new URLSearchParams(location.search); @@ -40,7 +41,7 @@ autoplay: true, srgOptions: { dataProviderHost: ilHost }, plugins: { - pillarboxPlaylist: { repeat: true }, + pillarboxPlaylist: { autoadvance: true, repeat: RepeatMode.REPEAT_ALL }, pillarboxPlaylistUI: true } }); diff --git a/packages/pillarbox-playlist/scss/pillarbox-playlist.scss b/packages/pillarbox-playlist/scss/pillarbox-playlist.scss index b109e6e..2d53c71 100644 --- a/packages/pillarbox-playlist/scss/pillarbox-playlist.scss +++ b/packages/pillarbox-playlist/scss/pillarbox-playlist.scss @@ -51,4 +51,22 @@ border-radius: 0.5em; } } + + .pbw-repeat-one { + position: relative; + } + + .pbw-repeat-one::after { + position: absolute; + right: 0.5em; + bottom: 0.5em; + display: flex; + align-items: center; + justify-content: center; + width: 1em; + height: 1em; + font-size: 1em; + border-radius: 50%; + content: "1"; + } } diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js index c2ca385..6a8375c 100644 --- a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js @@ -1,4 +1,5 @@ import videojs from 'video.js'; +import { RepeatMode } from '../../pillarbox-playlist.js'; /** * @ignore @@ -14,6 +15,7 @@ class PillarboxPlaylistRepeatButton extends Button { constructor(player, options) { options = videojs.mergeOptions({ controlText: 'Repeat' }, options); super(player, options); + this.controlText(this.repeatModeAsString()); this.setIcon('repeat'); } @@ -30,6 +32,17 @@ class PillarboxPlaylistRepeatButton extends Button { this.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-repeat`, true); } + repeatModeAsString() { + switch (this.playlist().repeat) { + case RepeatMode.NO_REPEAT: + return 'No Repeat'; + case RepeatMode.REPEAT_ALL: + return 'Repeat All'; + case RepeatMode.REPEAT_ONE: + return 'Repeat One'; + } + } + /** * Builds the CSS class string for the button. * @@ -47,7 +60,10 @@ class PillarboxPlaylistRepeatButton extends Button { handleClick(event) { super.handleClick(event); this.playlist().toggleRepeat(); - this.toggleClass('vjs-selected', this.playlist().repeat); + this.toggleClass('vjs-selected', !this.playlist().isNoRepeatMode()); + this.toggleClass('pbw-repeat-one', this.playlist().isRepeatOneMode()); + this.controlText(this.repeatModeAsString()); + this.setAttribute('aria-pressed', !this.playlist().isNoRepeatMode()); } } diff --git a/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js b/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js index f5bf1b3..d2a471f 100644 --- a/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js +++ b/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js @@ -1,6 +1,4 @@ import videojs from 'video.js'; -import './modal/pillarbox-playlist-modal.js'; -import '../lang/index.js'; /** * @ignore diff --git a/packages/pillarbox-playlist/src/lang/de.json b/packages/pillarbox-playlist/src/lang/de.json index eafdbf7..6886804 100644 --- a/packages/pillarbox-playlist/src/lang/de.json +++ b/packages/pillarbox-playlist/src/lang/de.json @@ -1,7 +1,9 @@ { - "Next Item": "Nächstes Element", + "Next Item": "Nächstes element", + "No Repeat": "Keine Wiederholung", "Playlist": "Wiedergabeliste", "Previous Item": "Vorheriges Element", - "Repeat": "Wiederholen", + "Repeat All": "Alle Wiederholen", + "Repeat One": "Einzelnes Wiederholen", "Shuffle": "Mischen" } diff --git a/packages/pillarbox-playlist/src/lang/en.json b/packages/pillarbox-playlist/src/lang/en.json index 5d72728..c93ef6d 100644 --- a/packages/pillarbox-playlist/src/lang/en.json +++ b/packages/pillarbox-playlist/src/lang/en.json @@ -1,7 +1,9 @@ { "Next Item": "Next Item", + "No Repeat": "No Repeat", "Playlist": "Playlist", "Previous Item": "Previous Item", - "Repeat": "Repeat", + "Repeat All": "Repeat All", + "Repeat One": "Repeat One", "Shuffle": "Shuffle" } diff --git a/packages/pillarbox-playlist/src/lang/fr.json b/packages/pillarbox-playlist/src/lang/fr.json index c1b8861..fa49bd2 100644 --- a/packages/pillarbox-playlist/src/lang/fr.json +++ b/packages/pillarbox-playlist/src/lang/fr.json @@ -1,7 +1,9 @@ { "Next Item": "Élément suivant", + "No Repeat": "Pas de Répétition", "Playlist": "Liste de lecture", "Previous Item": "Élément précédent", - "Repeat": "Répéter", + "Repeat All": "Répéter Tout", + "Repeat One": "Répéter Un", "Shuffle": "Mélanger" } diff --git a/packages/pillarbox-playlist/src/lang/it.json b/packages/pillarbox-playlist/src/lang/it.json index f903a1a..320d7e5 100644 --- a/packages/pillarbox-playlist/src/lang/it.json +++ b/packages/pillarbox-playlist/src/lang/it.json @@ -1,7 +1,9 @@ { "Next Item": "Elemento successivo", + "No Repeat": "Nessuna Ripetizione", "Playlist": "Playlist", "Previous Item": "Elemento precedente", - "Repeat": "Ripeti", + "Repeat All": "Ripeti Tutto", + "Repeat One": "Ripeti Uno", "Shuffle": "Mescola" } diff --git a/packages/pillarbox-playlist/src/lang/rm.json b/packages/pillarbox-playlist/src/lang/rm.json index 1c34ac1..f8d9339 100644 --- a/packages/pillarbox-playlist/src/lang/rm.json +++ b/packages/pillarbox-playlist/src/lang/rm.json @@ -1,7 +1,9 @@ { "Next Item": "Element proxim", + "No Repeat": "Nagins Repeter", "Playlist": "Glista da reprodukziun", "Previous Item": "Element precedent", - "Repeat": "Repeter", + "Repeat All": "Repeter Tut", + "Repeat One": "Repeter In", "Shuffle": "Maschadar" } diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js index 4a2c3d5..c05c9ff 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js @@ -1,5 +1,7 @@ import videojs from 'video.js'; import './components/pillarbox-playlist-button.js'; +import './components/modal/pillarbox-playlist-modal.js'; +import './lang'; /** * @ignore diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist.js b/packages/pillarbox-playlist/src/pillarbox-playlist.js index 0cac8ab..e536fa6 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js @@ -7,10 +7,32 @@ import videojs from 'video.js'; const Plugin = videojs.getPlugin('plugin'); const log = videojs.log.createLogger('pillarbox-playlist'); +/** + * Defines the available repeat modes for the playlist. + * + * @enum {number} + */ +export const RepeatMode = { + /** + * Disables repeat mode. + */ + NO_REPEAT: 0, + /** + * Loops the entire playlist. Once the last element of the playlist ends the n + * ext element will be the first one. This mode only works forwards, + * i.e. when advancing to the next element. + */ + REPEAT_ALL: 1, + /** + * Loops the currently playing item in the playlist. + */ + REPEAT_ONE: 2, +}; + /** * Represents a Plugin that allows control over a playlist. */ -class PillarboxPlaylist extends Plugin { +export class PillarboxPlaylist extends Plugin { /** * The items in the playlist. * @@ -45,22 +67,48 @@ class PillarboxPlaylist extends Plugin { previousNavigationThreshold = 3; /** - * Whether the repeat is enabled or not. If repeat is enabled once the last - * element of the playlist ends the next element will be the first one. This - * mode only works forwards, i.e. when advancing to the next element. + * The current repeat mode of the player. By default, repeat is disabled. * - * @type boolean + * @type {RepeatMode} */ - repeat = false; + repeat = RepeatMode.NO_REPEAT; /** * Toggles the repeat mode of the player to the opposite of its current state. * - * @param {boolean} [force] Optional. If provided, sets the repeat mode to the specified boolean value (true or false). - * If omitted, the repeat mode will toggle to the opposite of its current state. + * @param {RepeatMode} [force] Optional. + * If provided, sets the repeat mode to the specified state. + * If omitted, the repeat mode will cycle in order through: no repeat, repeat all and repeat one. */ toggleRepeat(force = undefined) { - this.repeat = force ?? !this.repeat; + this.repeat = force ?? (this.repeat + 1) % 3; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.REPEAT_ONE}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.REPEAT_ONE}, false otherwise. + */ + isRepeatOneMode() { + return this.repeat === RepeatMode.REPEAT_ONE; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.REPEAT_ALL}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.REPEAT_ALL}, false otherwise. + */ + isRepeatAllMode() { + return this.repeat === RepeatMode.REPEAT_ALL; + } + + /** + * Checks if the repeat mode is set to {@link RepeatMode.NO_REPEAT}. + * + * @returns {boolean} True if the repeat mode is {@link RepeatMode.NO_REPEAT}, false otherwise. + */ + isNoRepeatMode() { + return this.repeat === RepeatMode.NO_REPEAT; } /** @@ -110,7 +158,7 @@ class PillarboxPlaylist extends Plugin { } this.autoadvance = Boolean(options.autoadvance); - this.repeat = Boolean(options.repeat); + this.repeat = options.repeat ?? this.repeat; this.previousNavigationThreshold = Number.isFinite(options.previousNavigationThreshold) ? options.previousNavigationThreshold : @@ -312,7 +360,7 @@ class PillarboxPlaylist extends Plugin { return; } - if (this.repeat) this.select(0); + if (this.repeat === RepeatMode.REPEAT_ALL) this.select(0); } /** @@ -369,6 +417,12 @@ class PillarboxPlaylist extends Plugin { * will be played, otherwise nothing happens. */ handleEnded() { + if (this.repeat === RepeatMode.REPEAT_ONE) { + this.player.play().then(() => {}); + + return; + } + if (!this.autoadvance) { return; } @@ -422,8 +476,6 @@ PillarboxPlaylist.prototype.options_ = { videojs.registerPlugin('pillarboxPlaylist', PillarboxPlaylist); -export default PillarboxPlaylist; - /** * Represents a single item in the playlist. * diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js index ba0f01e..d9a6c36 100644 --- a/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js +++ b/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js @@ -2,32 +2,57 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite import pillarbox from '@srgssr/pillarbox-web'; import '../src/pillarbox-playlist.js'; import PillarboxPlaylistUi from '../src/pillarbox-playlist-ui.js'; +import { RepeatMode } from '../src/pillarbox-playlist.js'; const playlist = [ { - sources: [{ src: 'first-source', type: 'test' }], + sources: [{ + src: 'first-source', + type: 'test' + }], poster: 'first-poster', - data: { title: 'first-source', duration: 180 } + data: { + title: 'first-source', + duration: 180 + } }, { - sources: [{ src: 'second-source', type: 'test' }], + sources: [{ + src: 'second-source', + type: 'test' + }], poster: 'second-poster', - data: { title: 'second-source', duration: 150 } + data: { + title: 'second-source', + duration: 150 + } }, { - sources: [{ src: 'third-source', type: 'test' }], + sources: [{ + src: 'third-source', + type: 'test' + }], poster: 'third-poster', - data: { title: 'third-source', duration: 120 } + data: { + title: 'third-source', + duration: 120 + } }, { - sources: [{ src: 'fourth-source', type: 'test' }], + sources: [{ + src: 'fourth-source', + type: 'test' + }], poster: 'fourth-poster', - data: { title: 'fourth-source', duration: 210 } + data: { + title: 'fourth-source', + duration: 210 + } } ]; - -window.HTMLMediaElement.prototype.load = () => {}; +window.HTMLMediaElement.prototype.load = () => { +}; describe('PillarboxPlaylist', () => { let videoElement, player; @@ -84,7 +109,7 @@ describe('PillarboxPlaylist', () => { const fullscreenBtnIndex = controlBarChildIndex('FullscreenToggle'); const playlistBtnIndex = controlBarChildIndex('PillarboxPlaylistButton'); - expect(playlistBtnIndex).toBe(fullscreenBtnIndex-1); + expect(playlistBtnIndex).toBe(fullscreenBtnIndex - 1); }); it('should insert the playlist button at the last position if no sibling was found', () => { @@ -112,7 +137,7 @@ describe('PillarboxPlaylist', () => { it('should not insert the playlist button if the control bar is disabled', () => { player = pillarbox(videoElement, { - controlBar:false, + controlBar: false, plugins: { pillarboxPlaylist: true, pillarboxPlaylistUI: true @@ -125,8 +150,7 @@ describe('PillarboxPlaylist', () => { }); }); - - describe('User interface',() => { + describe('User interface', () => { let pillarboxPlaylist, dialog, controls, items, button; beforeEach(async() => { @@ -139,8 +163,10 @@ describe('PillarboxPlaylist', () => { pillarboxPlaylist = player.pillarboxPlaylist(); - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); pillarboxPlaylist.load(playlist); await new Promise((resolve) => player.ready(() => resolve())); @@ -163,7 +189,7 @@ describe('PillarboxPlaylist', () => { expect(dialog.hasClass('vjs-hidden')).toBeTruthy(); }); - it('should open the playlist dialog', ()=> { + it('should open the playlist dialog', () => { // When button.handleClick(); @@ -171,7 +197,7 @@ describe('PillarboxPlaylist', () => { expect(dialog.hasClass('vjs-hidden')).toBeFalsy(); }); - it('should hide the button when the playlist is empty', ()=> { + it('should hide the button when the playlist is empty', () => { // When pillarboxPlaylist.clear(); @@ -179,8 +205,7 @@ describe('PillarboxPlaylist', () => { expect(button.hasClass('vjs-hidden')).toBeTruthy(); }); - - it('should select an item when clicked', ()=> { + it('should select an item when clicked', () => { // Given pillarboxPlaylist.select(0); @@ -194,15 +219,29 @@ describe('PillarboxPlaylist', () => { .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); }); - it('should toggle repeat mode through the dialog controls', ()=> { - // When + it('should toggle repeat mode through the dialog controls', () => { + pillarboxPlaylist.toggleRepeat(RepeatMode.NO_REPEAT); + controls.getChild('PillarboxPlaylistRepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.REPEAT_ALL); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeTruthy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeFalsy(); - // Then - expect(pillarboxPlaylist.repeat).toBeTruthy(); + controls.getChild('PillarboxPlaylistRepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.REPEAT_ONE); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeTruthy(); + + controls.getChild('PillarboxPlaylistRepeatButton').handleClick(); + expect(pillarboxPlaylist.repeat).toBe(RepeatMode.NO_REPEAT); + expect(pillarboxPlaylist.isNoRepeatMode()).toBeTruthy(); + expect(pillarboxPlaylist.isRepeatAllMode()).toBeFalsy(); + expect(pillarboxPlaylist.isRepeatOneMode()).toBeFalsy(); }); - it('should go the next item through the dialog controls', ()=> { + it('should go the next item through the dialog controls', () => { // Given pillarboxPlaylist.select(0); @@ -216,7 +255,7 @@ describe('PillarboxPlaylist', () => { .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); }); - it('should go the previous item through the dialog controls', ()=> { + it('should go the previous item through the dialog controls', () => { // Given pillarboxPlaylist.select(2); @@ -230,7 +269,7 @@ describe('PillarboxPlaylist', () => { .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); }); - it('should shuffle the items through the dialog controls', ()=> { + it('should shuffle the items through the dialog controls', () => { // When controls.getChild('PillarboxPlaylistShuffleButton').handleClick(); diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js index 44ff89b..2c382fc 100644 --- a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js +++ b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js @@ -1,39 +1,54 @@ -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - vi -} from 'vitest'; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import pillarbox from '@srgssr/pillarbox-web'; -import PillarboxPlaylist from '../src/pillarbox-playlist.js'; +import { PillarboxPlaylist, RepeatMode } from '../src/pillarbox-playlist.js'; const playlist = [ { - sources: [{ src: 'first-source', type: 'test' }], + sources: [{ + src: 'first-source', + type: 'test' + }], poster: 'first-poster', - data: { title: 'first-source', duration: 180 } + data: { + title: 'first-source', + duration: 180 + } }, { - sources: [{ src: 'second-source', type: 'test' }], + sources: [{ + src: 'second-source', + type: 'test' + }], poster: 'second-poster', - data: { title: 'second-source', duration: 150 } + data: { + title: 'second-source', + duration: 150 + } }, { - sources: [{ src: 'third-source', type: 'test' }], + sources: [{ + src: 'third-source', + type: 'test' + }], poster: 'third-poster', - data: { title: 'third-source', duration: 120 } + data: { + title: 'third-source', + duration: 120 + } }, { - sources: [{ src: 'fourth-source', type: 'test' }], + sources: [{ + src: 'fourth-source', + type: 'test' + }], poster: 'fourth-poster', - data: { title: 'fourth-source', duration: 210 } + data: { + title: 'fourth-source', + duration: 210 + } } ]; - window.HTMLMediaElement.prototype.load = () => { }; @@ -67,8 +82,10 @@ describe('PillarboxPlaylist', () => { describe('load', () => { it('should load a playlist', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -87,8 +104,10 @@ describe('PillarboxPlaylist', () => { describe('select', () => { it('should select an item by index', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -103,8 +122,10 @@ describe('PillarboxPlaylist', () => { it('should not load an item if its already selected', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -121,8 +142,10 @@ describe('PillarboxPlaylist', () => { it('should not load an item if its outside of the playlist range', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -141,8 +164,10 @@ describe('PillarboxPlaylist', () => { describe('next', () => { it('should play next on a registered playlist', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -160,8 +185,10 @@ describe('PillarboxPlaylist', () => { it('should not play next if the current index is the last of the playlist', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -177,34 +204,15 @@ describe('PillarboxPlaylist', () => { expect(srcSpy).toHaveBeenLastCalledWith(playlist[3].sources); expect(posterSpy).toHaveBeenLastCalledWith(playlist[3].poster); }); - - it('should play the first element if repeat is true when next is called and the current index is the last of the playlist', () => { - // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); - - // When - pillarboxPlaylist.toggleRepeat(true); - pillarboxPlaylist.load(playlist); - pillarboxPlaylist.select(3); - pillarboxPlaylist.next(); - - // Then - expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); - expect(pillarboxPlaylist.hasNext()).toBeTruthy(); - expect(pillarboxPlaylist.items.length).toBe(4); - expect(pillarboxPlaylist.currentIndex).toBe(0); - expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); - expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources); - expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster); - }); }); describe('previous', () => { it('should play previous on a registered playlist', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -243,8 +251,10 @@ describe('PillarboxPlaylist', () => { describe('autoadvance', () => { it('should play next element on ended if autoadvance is enabled', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.toggleAutoadvance(true); @@ -263,8 +273,10 @@ describe('PillarboxPlaylist', () => { it('should not play next element on ended if autoadvance is disabled', () => { // Given - const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {}); - const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {}); + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.toggleAutoadvance(false); @@ -282,11 +294,57 @@ describe('PillarboxPlaylist', () => { }); }); + describe('repeat', () => { + it('should play the same element if repeat mode is "repeat one"', () => { + // Given + const playSpy = vi.spyOn(player, 'play') + .mockImplementation(() => Promise.resolve()); + + // When + pillarboxPlaylist.toggleRepeat(RepeatMode.REPEAT_ONE); + pillarboxPlaylist.load(playlist); + pillarboxPlaylist.handleEnded(); + + // Then + expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); + expect(pillarboxPlaylist.hasNext()).toBeTruthy(); + expect(pillarboxPlaylist.items.length).toBe(4); + expect(pillarboxPlaylist.currentIndex).toBe(0); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + expect(playSpy).toHaveBeenCalled(); + }); + + it('should play the first element if repeat is true when next is called and the current index is the last of the playlist', () => { + // Given + const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => { + }); + const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => { + }); + + // When + pillarboxPlaylist.toggleRepeat(RepeatMode.REPEAT_ALL); + pillarboxPlaylist.load(playlist); + pillarboxPlaylist.select(3); + pillarboxPlaylist.next(); + + // Then + expect(pillarboxPlaylist.hasPrevious()).toBeFalsy(); + expect(pillarboxPlaylist.hasNext()).toBeTruthy(); + expect(pillarboxPlaylist.items.length).toBe(4); + expect(pillarboxPlaylist.currentIndex).toBe(0); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources); + expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster); + }); + }); + describe('shuffle', () => { it('should randomize the order of playlist items', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -301,11 +359,25 @@ describe('PillarboxPlaylist', () => { describe('push', () => { it('should push new items at the end of the playlist', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); const items = [ - { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' }, - { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' } + { + sources: [{ + src: 'fifth-source', + type: 'test' + }], + poster: 'fifth-poster' + }, + { + sources: [{ + src: 'sixth-source', + type: 'test' + }], + poster: 'sixth-poster' + } ]; // When @@ -322,12 +394,26 @@ describe('PillarboxPlaylist', () => { describe('splice', () => { it('should push new items at any point of the playlist', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); const items = [ - { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' }, - { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' } + { + sources: [{ + src: 'fifth-source', + type: 'test' + }], + poster: 'fifth-poster' + }, + { + sources: [{ + src: 'sixth-source', + type: 'test' + }], + poster: 'sixth-poster' + } ]; // When @@ -342,8 +428,10 @@ describe('PillarboxPlaylist', () => { it('should delete items at any point of the playlist', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -358,11 +446,25 @@ describe('PillarboxPlaylist', () => { it('should push and delete items at any point of the playlist', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); const items = [ - { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' }, - { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' } + { + sources: [{ + src: 'fifth-source', + type: 'test' + }], + poster: 'fifth-poster' + }, + { + sources: [{ + src: 'sixth-source', + type: 'test' + }], + poster: 'sixth-poster' + } ]; // When @@ -378,8 +480,10 @@ describe('PillarboxPlaylist', () => { it('should lose track of current item when deleted', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist); @@ -393,11 +497,25 @@ describe('PillarboxPlaylist', () => { it('should lose track of current item when deleted even when items are added', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); const items = [ - { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' }, - { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' } + { + sources: [{ + src: 'fifth-source', + type: 'test' + }], + poster: 'fifth-poster' + }, + { + sources: [{ + src: 'sixth-source', + type: 'test' + }], + poster: 'sixth-poster' + } ]; // When @@ -441,9 +559,15 @@ describe('PillarboxPlaylist', () => { it('should reverse a single-item playlist without changing the index', () => { // Given const items = [{ - sources: [{ src: 'first-source', type: 'test' }], + sources: [{ + src: 'first-source', + type: 'test' + }], poster: 'first-poster', - data: { title: 'first-source', duration: 120 } + data: { + title: 'first-source', + duration: 120 + } }]; pillarboxPlaylist.load(items); @@ -457,7 +581,6 @@ describe('PillarboxPlaylist', () => { }); }); - describe('sort', () => { it('should sort items by duration and update currentIndex correctly', () => { // Given @@ -492,9 +615,15 @@ describe('PillarboxPlaylist', () => { it('should sort a single-item playlist without changing the index', () => { // Given const items = [{ - sources: [{ src: 'first-source', type: 'test' }], + sources: [{ + src: 'first-source', + type: 'test' + }], poster: 'first-poster', - data: { title: 'first-source', duration: 120 } + data: { + title: 'first-source', + duration: 120 + } }]; pillarboxPlaylist.load(items); @@ -511,8 +640,10 @@ describe('PillarboxPlaylist', () => { describe('clear', () => { it('should clear all the items of the playlist', () => { // Given - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); + vi.spyOn(player, 'src').mockImplementation(() => { + }); + vi.spyOn(player, 'poster').mockImplementation(() => { + }); // When pillarboxPlaylist.load(playlist);