diff --git a/client/cypress/tests/players/MediaPlayerContainer.cy.js b/client/cypress/tests/players/MediaPlayerContainer.cy.js new file mode 100644 index 00000000..2e1a39d1 --- /dev/null +++ b/client/cypress/tests/players/MediaPlayerContainer.cy.js @@ -0,0 +1,281 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import MediaPlayerContainer from '../../../components/app/MediaPlayerContainer.vue' +import * as rootStore from '../../../store/index' +import * as userStore from '../../../store/user' +import * as globalsStore from '../../../store/globals' +import * as librariesStore from '../../../store/libraries' + +Vue.use(Vuex) + +const FIXTURE_URL = '/__cypress/fixtures/test-audio.wav' +const TEST_LIBRARY_ID = 'lib-test' +const TEST_ITEM_ID = 'item-test' +const TEST_SESSION_ID = 'session-test' + +const makeLibraryItem = () => ({ + id: TEST_ITEM_ID, + libraryId: TEST_LIBRARY_ID, + mediaType: 'book', + updatedAt: 1714608000000, + media: { + coverPath: null, + duration: 4, + metadata: { + title: 'Smart Speed Harness Fixture', + authors: [{ id: 'author-1', name: 'Harness Author' }], + explicit: false + }, + chapters: [{ id: 'chapter-1', start: 0, end: 4, title: 'Fixture Chapter' }] + } +}) + +const buildStore = () => { + return new Vuex.Store({ + state: rootStore.state(), + getters: rootStore.getters, + mutations: rootStore.mutations, + modules: { + user: { + namespaced: true, + state: userStore.state(), + getters: userStore.getters, + mutations: userStore.mutations, + actions: userStore.actions + }, + globals: { + namespaced: true, + state: globalsStore.state(), + getters: globalsStore.getters, + mutations: globalsStore.mutations + }, + libraries: { + namespaced: true, + state: librariesStore.state(), + getters: librariesStore.getters, + mutations: librariesStore.mutations + } + } + }) +} + +const createAudioContextStub = () => { + const sourceNode = { + connect: cy.stub().as('audioSourceConnect'), + disconnect: cy.stub().as('audioSourceDisconnect') + } + + const audioContext = { + destination: { label: 'destination' }, + state: 'running', + currentTime: 0, + resume: cy.stub().callsFake(() => { + audioContext.state = 'running' + return Promise.resolve() + }).as('audioContextResume'), + suspend: cy.stub().callsFake(() => { + audioContext.state = 'suspended' + return Promise.resolve() + }).as('audioContextSuspend'), + close: cy.stub().resolves().as('audioContextClose'), + createMediaElementSource: cy.stub().returns(sourceNode).as('createMediaElementSource'), + audioWorklet: { + addModule: cy.stub().resolves().as('audioWorkletAddModule') + } + } + + return { audioContext } +} + +describe('MediaPlayerContainer', () => { + beforeEach(() => { + cy.viewport(1280, 900) + + cy.window().then((win) => { + win.MediaMetadata = function MediaMetadata(metadata) { + Object.assign(this, metadata) + } + win.navigator.mediaSession = { + playbackState: 'none', + metadata: null, + setActionHandler: cy.stub().as('setActionHandler') + } + }) + }) + + it('starts playback through the real container session path', () => { + const store = buildStore() + const eventBus = new Vue() + const libraryItem = makeLibraryItem() + const { audioContext } = createAudioContextStub() + + store.commit('setRouterBasePath', '') + store.commit('libraries/addUpdate', { + id: TEST_LIBRARY_ID, + mediaType: 'book', + settings: { coverAspectRatio: 0 } + }) + store.commit('libraries/setCurrentLibrary', { id: TEST_LIBRARY_ID }) + store.commit('user/setUser', { + id: 'user-1', + type: 'root', + mediaProgress: [], + bookmarks: [], + permissions: { update: true, delete: true, download: true, upload: true, accessAllLibraries: true }, + librariesAccessible: [TEST_LIBRARY_ID] + }) + store.commit('user/setSettings', { + ...store.state.user.settings, + enableSmartSpeed: false, + smartSpeedRatio: 2.5, + playbackRate: 1, + playbackRateIncrementDecrement: 0.1, + jumpForwardAmount: 10, + jumpBackwardAmount: 10, + useChapterTrack: false + }) + + cy.intercept('GET', `/api/items/${TEST_ITEM_ID}?expanded=1`, { + statusCode: 200, + body: libraryItem + }).as('getLibraryItem') + + cy.intercept('POST', `/api/items/${TEST_ITEM_ID}/play`, (req) => { + expect(req.body.mediaPlayer).to.equal('html5') + expect(req.body.forceTranscode).to.equal(false) + req.reply({ + statusCode: 200, + body: { + id: TEST_SESSION_ID, + libraryItem, + episodeId: null, + displayTitle: 'Smart Speed Harness Fixture', + displayAuthor: 'Harness Author', + currentTime: 0, + playMethod: 0, + audioTracks: [ + { + index: 0, + startOffset: 0, + duration: 4, + contentUrl: FIXTURE_URL, + mimeType: 'audio/wav' + } + ] + } + }) + }).as('startPlaybackSession') + + cy.intercept('POST', `/api/session/${TEST_SESSION_ID}/close`, { + statusCode: 200, + body: {} + }).as('closePlaybackSession') + + cy.intercept('POST', `/api/session/${TEST_SESSION_ID}/sync`, { + statusCode: 200, + body: {} + }).as('syncPlaybackSession') + + cy.mount(MediaPlayerContainer, { + store, + stubs: { + 'covers-book-cover': { template: '
' }, + 'ui-tooltip': { template: '