UNPKG

shaka-player

Version:
974 lines (824 loc) 31.4 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('CastSender', () => { const CastSender = shaka.cast.CastSender; const CastUtils = shaka.cast.CastUtils; const Util = shaka.test.Util; const originalChrome = window['chrome']; const originalStatusDelay = shaka.cast.CastSender.STATUS_DELAY; const fakeAppId = 'asdf'; const fakeAndroidReceiverCompatible = false; const fakeInitState = { manifest: null, player: null, startTime: null, video: null, }; /** @type {!jasmine.Spy} */ let onStatusChanged; /** @type {!jasmine.Spy} */ let onFirstCastStateUpdate; let onRemoteEvent; let onResumeLocal; let onInitStateRequired; let mockCastApi; let mockSession; /** @type {shaka.cast.CastSender} */ let sender; beforeEach(() => { onStatusChanged = jasmine.createSpy('onStatusChanged'); onFirstCastStateUpdate = jasmine.createSpy('onFirstCastStateUpdate'); onRemoteEvent = jasmine.createSpy('onRemoteEvent'); onResumeLocal = jasmine.createSpy('onResumeLocal'); onInitStateRequired = jasmine.createSpy('onInitStateRequired') .and.returnValue(fakeInitState); mockCastApi = createMockCastApi(); // We're using quotes to access window.chrome because the compiler // knows about lots of Chrome-specific APIs we aren't mocking. We // don't need this mock strictly type-checked. window['chrome'] = {cast: mockCastApi}; mockSession = null; sender = new CastSender( fakeAppId, Util.spyFunc(onStatusChanged), Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent), Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired), fakeAndroidReceiverCompatible); }); afterEach(async () => { await sender.destroy(); resetClassVariables(); }); beforeAll(() => { shaka.cast.CastSender.STATUS_DELAY = 0; }); afterAll(() => { window['chrome'] = originalChrome; shaka.cast.CastSender.STATUS_DELAY = originalStatusDelay; }); describe('init', () => { it('installs a callback if the cast API is not available', () => { // Remove the mock cast API. delete window['chrome'].cast; // Init and expect that apiReady is false and no status is available. sender.init(); expect(sender.apiReady()).toBe(false); expect(onStatusChanged).not.toHaveBeenCalled(); // Restore the mock cast API. window['chrome'].cast = mockCastApi; simulateSdkLoaded(); // Expect the API to be ready and initialized. expect(sender.apiReady()).toBe(true); expect(sender.hasReceivers()).toBe(false); expect(onStatusChanged).toHaveBeenCalled(); expect(mockCastApi.SessionRequest).toHaveBeenCalledWith( fakeAppId, /* capabilities= */ [], /* timeout= */ null, fakeAndroidReceiverCompatible, /* credentialsData= */ null); expect(mockCastApi.initialize).toHaveBeenCalled(); }); it('sets up cast API right away if it is available', () => { sender.init(); // Expect the API to be ready and initialized. expect(sender.apiReady()).toBe(true); expect(sender.hasReceivers()).toBe(false); expect(onStatusChanged).toHaveBeenCalled(); expect(mockCastApi.SessionRequest).toHaveBeenCalledWith( fakeAppId, /* capabilities= */ [], /* timeout= */ null, fakeAndroidReceiverCompatible, /* credentialsData= */ null); expect(mockCastApi.initialize).toHaveBeenCalled(); }); }); describe('hasReceivers', () => { it('reflects the most recent receiver status', () => { sender.init(); expect(sender.hasReceivers()).toBe(false); fakeReceiverAvailability(true); expect(sender.hasReceivers()).toBe(true); fakeReceiverAvailability(false); expect(sender.hasReceivers()).toBe(false); }); it('remembers status from previous senders', async () => { sender.init(); fakeReceiverAvailability(true); await sender.destroy(); sender = new CastSender( fakeAppId, Util.spyFunc(onStatusChanged), Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent), Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired), /* androidReceiverCompatible= */ false); sender.init(); // You get an initial call to onStatusChanged when it initializes. expect(onStatusChanged).toHaveBeenCalledTimes(3); await Util.shortDelay(); // And then you get another call after it has 'discovered' the // existing receivers. expect(sender.hasReceivers()).toBe(true); expect(onStatusChanged).toHaveBeenCalledTimes(4); }); }); describe('cast', () => { it('fails when the cast API is not ready', async () => { mockCastApi.isAvailable = false; sender.init(); expect(sender.apiReady()).toBe(false); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.CAST, shaka.util.Error.Code.CAST_API_UNAVAILABLE)); await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected); }); it('fails when there are no receivers', async () => { sender.init(); expect(sender.apiReady()).toBe(true); expect(sender.hasReceivers()).toBe(false); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.CAST, shaka.util.Error.Code.NO_CAST_RECEIVERS)); await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected); }); it('creates a session and sends an "init" message', async () => { sender.init(); expect(sender.apiReady()).toBe(true); fakeReceiverAvailability(true); expect(sender.hasReceivers()).toBe(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(onStatusChanged).toHaveBeenCalled(); expect(sender.isCasting()).toBe(true); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', initState: fakeInitState, })); }); // The library is not loaded yet during describe(), so we can't refer to // Shaka error codes by name here. Instead, we use the numeric value and // put the name in a comment. const connectionFailures = [ { condition: 'canceled by the user', castErrorCode: 'cancel', shakaErrorCode: shaka.util.Error.Code.CAST_CANCELED_BY_USER, }, { condition: 'the connection times out', castErrorCode: 'timeout', shakaErrorCode: shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT, }, { condition: 'the receiver is unavailable', castErrorCode: 'receiver_unavailable', shakaErrorCode: shaka.util.Error.Code.CAST_RECEIVER_APP_UNAVAILABLE, }, { condition: 'an unexpected error occurs', castErrorCode: 'anything else', shakaErrorCode: shaka.util.Error.Code.UNEXPECTED_CAST_ERROR, }, ]; for (const metadata of connectionFailures) { it('fails when ' + metadata.condition, async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnectionFailure(metadata.castErrorCode); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.CAST, metadata.shakaErrorCode, jasmine.anything())); await expectAsync(p).toBeRejectedWith(expected); }); } it('fails when we are already casting', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.CAST, shaka.util.Error.Code.ALREADY_CASTING)); await expectAsync(sender.cast(fakeInitState)).toBeRejectedWith(expected); }); }); it('re-uses old sessions', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); const oldMockSession = mockSession; await p; await sender.destroy(); // Reset tracking variables. mockCastApi.ApiConfig.calls.reset(); onStatusChanged.calls.reset(); oldMockSession.messages = []; // Make a new session, to ensure that the sender is correctly using // the previous mock session. mockSession = createMockCastSession(); sender = new CastSender( fakeAppId, Util.spyFunc(onStatusChanged), Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent), Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired), /* androidReceiverCompatible= */ false); sender.init(); // The sender should automatically rejoin the session, without needing // to be told to cast. expect(onStatusChanged).toHaveBeenCalled(); expect(sender.isCasting()).toBe(true); // The message should be on the old session, instead of the new one. expect(mockSession.messages.length).toBe(0); expect(oldMockSession.messages).toContain(jasmine.objectContaining({ type: 'init', initState: fakeInitState, })); }); it('doesn\'t re-use stopped sessions', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; await sender.destroy(); mockCastApi.ApiConfig.calls.reset(); // The session is stopped in the meantime. mockSession.status = chrome.cast.SessionStatus.STOPPED; sender = new CastSender( fakeAppId, Util.spyFunc(onStatusChanged), Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent), Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired), /* androidReceiverCompatible= */ false); sender.init(); expect(sender.isCasting()).toBe(false); }); it('joins existing sessions automatically', async () => { sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); await Util.shortDelay(); expect(onStatusChanged).toHaveBeenCalled(); expect(sender.isCasting()).toBe(true); expect(onInitStateRequired).toHaveBeenCalled(); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', initState: fakeInitState, })); }); describe('setAppData', () => { const fakeAppData = { myKey1: 'myValue1', myKey2: 'myValue2', }; it('sets "appData" for "init" message if not casting', async () => { sender.init(); fakeReceiverAvailability(true); sender.setAppData(fakeAppData); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', appData: fakeAppData, })); }); it('sends a special "appData" message if casting', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; // init message has no appData expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', appData: null, })); // no appData message yet expect(mockSession.messages).not.toContain(jasmine.objectContaining({ type: 'appData', })); sender.setAppData(fakeAppData); // now there is an appData message expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'appData', appData: fakeAppData, })); }); }); describe('onFirstCastStateUpdate', () => { it('is triggered by an "update" message', async () => { // You have to join an existing session for it to work. sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); await Util.shortDelay(); expect(onFirstCastStateUpdate).not.toHaveBeenCalled(); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).toHaveBeenCalled(); }); it('is not triggered if making a new session', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).not.toHaveBeenCalled(); }); it('is triggered once per existing session', async () => { sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); await Util.shortDelay(); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).toHaveBeenCalled(); onFirstCastStateUpdate.calls.reset(); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).not.toHaveBeenCalled(); onFirstCastStateUpdate.calls.reset(); // Disconnect and then connect to another existing session. fakeJoinExistingSession(); await Util.shortDelay(); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).toHaveBeenCalled(); }); }); describe('onRemoteEvent', () => { it('is triggered by an "event" message', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; const fakeEvent = { type: 'eventName', detail: {key1: 'value1'}, }; fakeSessionMessage({ type: 'event', targetName: 'video', event: fakeEvent, }); expect(onRemoteEvent).toHaveBeenCalledWith( 'video', jasmine.objectContaining(fakeEvent)); }); }); describe('onResumeLocal', () => { it('is triggered when casting ends', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(sender.isCasting()).toBe(true); expect(onResumeLocal).not.toHaveBeenCalled(); fakeRemoteDisconnect(); expect(sender.isCasting()).toBe(false); expect(onResumeLocal).toHaveBeenCalled(); }); }); describe('showDisconnectDialog', () => { it('opens the dialog if we are casting', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(sender.isCasting()).toBe(true); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); mockCastApi.requestSession.calls.reset(); sender.showDisconnectDialog(); // this call opens the dialog: expect(mockCastApi.requestSession).toHaveBeenCalled(); // these were not used: expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); fakeRemoteDisconnect(); }); }); describe('get', () => { it('returns most recent properties from "update" messages', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; const update = { video: { currentTime: 12, paused: false, }, player: { isBuffering: true, seekRange: {start: 5, end: 17}, }, }; fakeSessionMessage({ type: 'update', update: update, }); // These are properties: expect(sender.get('video', 'currentTime')).toBe( update.video.currentTime); expect(sender.get('video', 'paused')).toBe( update.video.paused); // These are getter methods: expect(sender.get('player', 'isBuffering')()).toBe( update.player.isBuffering); expect(sender.get('player', 'seekRange')()).toEqual( update.player.seekRange); }); it('returns functions for video and player methods', () => { sender.init(); expect(sender.get('video', 'play')).toEqual(jasmine.any(Function)); expect(sender.get('player', 'isLive')).toEqual(jasmine.any(Function)); expect(sender.get('player', 'configure')).toEqual(jasmine.any(Function)); expect(sender.get('player', 'load')).toEqual(jasmine.any(Function)); }); it('simple methods trigger "call" messages', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; const method = sender.get('video', 'play'); const retval = method(123, 'abc'); expect(retval).toBe(undefined); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'call', targetName: 'video', methodName: 'play', args: [123, 'abc'], })); }); describe('async player methods', () => { let method; beforeEach(async () => { method = null; sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; method = sender.get('player', 'load'); }); it('return Promises', () => { const p = method(); expect(p).toEqual(jasmine.any(Promise)); p.catch(() => {}); // silence logs about uncaught rejections }); it('trigger "asyncCall" messages', () => { const p = method(123, 'abc'); p.catch(() => {}); // silence logs about uncaught rejections expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'asyncCall', targetName: 'player', methodName: 'load', args: [123, 'abc'], id: jasmine.any(String), })); }); it('resolve when "asyncComplete" messages are received', async () => { /** @type {!shaka.test.StatusPromise} */ const p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. await Util.shortDelay(); expect(p.status).toBe('pending'); const id = mockSession.messages[mockSession.messages.length - 1].id; fakeSessionMessage({ type: 'asyncComplete', id: id, error: null, }); await expectAsync(p).toBeResolved(); }); it('reject when "asyncComplete" messages have an error', async () => { const originalError = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE, 'foo://bar'); /** @type {!shaka.test.StatusPromise} */ const p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. await Util.shortDelay(); expect(p.status).toBe('pending'); const id = mockSession.messages[mockSession.messages.length - 1].id; fakeSessionMessage({ type: 'asyncComplete', id: id, error: originalError, }); await expectAsync(p).toBeRejectedWith(Util.jasmineError(originalError)); }); it('reject when disconnected remotely', async () => { /** @type {!shaka.test.StatusPromise} */ const p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. await Util.shortDelay(); expect(p.status).toBe('pending'); fakeRemoteDisconnect(); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); await expectAsync(p).toBeRejectedWith(expected); }); }); }); describe('set', () => { it('overrides any cached properties', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; const update = { video: {muted: false}, }; fakeSessionMessage({ type: 'update', update: update, }); expect(sender.get('video', 'muted')).toBe(false); sender.set('video', 'muted', true); expect(sender.get('video', 'muted')).toBe(true); }); it('causes a "set" message to be sent', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; sender.set('video', 'muted', true); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'set', targetName: 'video', property: 'muted', value: true, })); }); it('can be used before we have an "update" message', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(sender.get('video', 'muted')).toBe(undefined); sender.set('video', 'muted', true); expect(sender.get('video', 'muted')).toBe(true); }); }); describe('hasRemoteProperties', () => { it('is true only after we have an "update" message', async () => { sender.init(); fakeReceiverAvailability(true); const p = sender.cast(fakeInitState); fakeSessionConnection(); await p; expect(sender.hasRemoteProperties()).toBe(false); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(sender.hasRemoteProperties()).toBe(true); }); }); describe('forceDisconnect', () => { it('disconnects and cancels all async operations', async () => { sender.init(); fakeReceiverAvailability(true); const cast = sender.cast(fakeInitState); fakeSessionConnection(); await cast; expect(sender.isCasting()).toBe(true); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); expect(mockSession.removeUpdateListener).not.toHaveBeenCalled(); expect(mockSession.removeMessageListener).not.toHaveBeenCalled(); const method = sender.get('player', 'load'); /** @type {!shaka.test.StatusPromise} */ const p = new shaka.test.StatusPromise(method()); // Wait a tick for the Promise status to be set. await Util.shortDelay(); expect(p.status).toBe('pending'); sender.forceDisconnect(); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).toHaveBeenCalled(); expect(mockSession.removeUpdateListener).toHaveBeenCalled(); expect(mockSession.removeMessageListener).toHaveBeenCalled(); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); await expectAsync(p).toBeRejectedWith(expected); }); it('transfers playback to local device', async () => { sender.init(); fakeReceiverAvailability(true); const cast = sender.cast(fakeInitState); fakeSessionConnection(); await cast; expect(sender.isCasting()).toBe(true); expect(onResumeLocal).not.toHaveBeenCalled(); sender.forceDisconnect(); expect(sender.isCasting()).toBe(false); expect(onResumeLocal).toHaveBeenCalled(); }); it('succeeds even if session.stop() throws', async () => { sender.init(); fakeReceiverAvailability(true); const cast = sender.cast(fakeInitState); fakeSessionConnection(); await cast; mockSession.stop.and.throwError(new Error('DISCONNECTED!')); expect(() => sender.forceDisconnect()).not.toThrow(jasmine.anything()); }); }); describe('sendMessage exception', () => { /** @type {Error} */ let originalException; /** @type {Object} */ let expectedError; beforeEach(async () => { sender.init(); fakeReceiverAvailability(true); const cast = sender.cast(fakeInitState); fakeSessionConnection(); await cast; originalException = new Error('DISCONNECTED!'); expectedError = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.CAST, shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT, originalException)); mockSession.sendMessage.and.throwError(originalException); }); it('propagates to the caller', () => { expect(() => sender.set('video', 'muted', true)).toThrow(expectedError); }); it('triggers an error event on Player', () => { expect(() => sender.set('video', 'muted', true)).toThrow(expectedError); const expectedEvent = jasmine.objectContaining({ type: 'error', detail: expectedError, }); expect(onRemoteEvent).toHaveBeenCalledWith('player', expectedEvent); }); it('disconnects the sender', () => { expect(sender.isCasting()).toBe(true); expect(onResumeLocal).not.toHaveBeenCalled(); expect(() => sender.set('video', 'muted', true)).toThrow(expectedError); expect(sender.isCasting()).toBe(false); expect(onResumeLocal).toHaveBeenCalled(); }); }); describe('destroy', () => { it('cancels all async operations', async () => { sender.init(); fakeReceiverAvailability(true); const cast = sender.cast(fakeInitState); fakeSessionConnection(); await cast; expect(sender.isCasting()).toBe(true); expect(mockSession.stop).not.toHaveBeenCalled(); expect(mockSession.removeUpdateListener).not.toHaveBeenCalled(); expect(mockSession.removeMessageListener).not.toHaveBeenCalled(); const method = sender.get('player', 'load'); /** @type {!shaka.test.StatusPromise} */ const p = new shaka.test.StatusPromise(method()); // Wait a tick for the Promise status to be set. await Util.shortDelay(); expect(p.status).toBe('pending'); const destroy = sender.destroy(); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); expect(mockSession.removeUpdateListener).toHaveBeenCalled(); expect(mockSession.removeMessageListener).toHaveBeenCalled(); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); await expectAsync(p).toBeRejectedWith(expected); await destroy; }); }); function createMockCastApi() { return { isAvailable: true, SessionRequest: jasmine.createSpy('chrome.cast.SessionRequest'), SessionStatus: {STOPPED: 'stopped'}, ApiConfig: jasmine.createSpy('chrome.cast.ApiConfig'), initialize: jasmine.createSpy('chrome.cast.initialize'), requestSession: jasmine.createSpy('chrome.cast.requestSession'), }; } function createMockCastSession() { const session = { messages: [], status: 'connected', receiver: {friendlyName: 'SomeDevice'}, addUpdateListener: jasmine.createSpy('Session.addUpdateListener'), removeUpdateListener: jasmine.createSpy('Session.removeUpdateListener'), addMessageListener: jasmine.createSpy('Session.addMessageListener'), removeMessageListener: jasmine.createSpy('Session.removeMessageListener'), leave: jasmine.createSpy('Session.leave'), sendMessage: jasmine.createSpy('Session.sendMessage'), stop: jasmine.createSpy('Session.stop'), }; // For convenience, deserialize and store sent messages. session.sendMessage.and.callFake( (namespace, message, successCallback, errorCallback) => { session.messages.push(CastUtils.deserialize(message)); }); return session; } /** * @param {boolean} yes If true, simulate receivers being available. */ function fakeReceiverAvailability(yes) { expect(mockCastApi.ApiConfig).toHaveBeenCalledTimes(1); const onReceiverStatusChanged = mockCastApi.ApiConfig.calls.argsFor(0)[2]; onReceiverStatusChanged(yes ? 'available' : 'unavailable'); } function fakeSessionConnection() { expect(mockCastApi.requestSession).toHaveBeenCalledTimes(1); const onSessionInitiated = mockCastApi.requestSession.calls.argsFor(0)[0]; mockSession = createMockCastSession(); onSessionInitiated(mockSession); } /** * @param {string} code */ function fakeSessionConnectionFailure(code) { expect(mockCastApi.requestSession).toHaveBeenCalledTimes(1); const onSessionError = mockCastApi.requestSession.calls.argsFor(0)[1]; onSessionError({code: code}); } /** * @param {?} message */ function fakeSessionMessage(message) { expect(mockSession.addMessageListener).toHaveBeenCalledTimes(1); const namespace = mockSession.addMessageListener.calls.argsFor(0)[0]; const listener = mockSession.addMessageListener.calls.argsFor(0)[1]; const serialized = CastUtils.serialize(message); listener(namespace, serialized); } function fakeRemoteDisconnect() { mockSession.status = 'disconnected'; expect(mockSession.addUpdateListener).toHaveBeenCalledTimes(1); const onConnectionStatus = mockSession.addUpdateListener.calls.argsFor(0)[0]; onConnectionStatus(); } function fakeJoinExistingSession() { expect(mockCastApi.ApiConfig).toHaveBeenCalledTimes(1); const onJoinExistingSession = mockCastApi.ApiConfig.calls.argsFor(0)[1]; mockSession = createMockCastSession(); onJoinExistingSession(mockSession); } /** * @suppress {visibility} * "suppress visibility" has function scope, so this is a mini-function that * exists solely to suppress visibility rules for these actions. */ function resetClassVariables() { CastSender.hasReceivers_ = false; CastSender.session_ = null; } /** * @suppress {visibility} * "suppress visibility" has function scope, so this is a mini-function that * exists solely to suppress visibility rules for these actions. */ function simulateSdkLoaded() { shaka.cast.CastSender.onSdkLoaded_(true); } });