UNPKG

shaka-player

Version:
993 lines (863 loc) 33.4 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ describe('CastSender', function() { const CastSender = shaka.cast.CastSender; const CastUtils = shaka.cast.CastUtils; const Util = shaka.test.Util; const originalChrome = window['chrome']; let fakeAppId = 'asdf'; let 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(function() { 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)); }); afterEach(async () => { await sender.destroy(); resetClassVariables(); }); afterAll(function() { window['chrome'] = originalChrome; }); describe('init', function() { it('installs a callback if the cast API is not available', function() { // 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); expect(mockCastApi.initialize).toHaveBeenCalled(); }); it('sets up cast API right away if it is available', function() { 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); expect(mockCastApi.initialize).toHaveBeenCalled(); }); }); describe('hasReceivers', function() { it('reflects the most recent receiver status', function() { 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', function(done) { sender.init(); fakeReceiverAvailability(true); sender.destroy().then(function() { sender = new CastSender( fakeAppId, Util.spyFunc(onStatusChanged), Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent), Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired)); sender.init(); // You get an initial call to onStatusChanged when it initializes. expect(onStatusChanged).toHaveBeenCalledTimes(3); return Util.delay(0.25); }).then(function() { // And then you get another call after it has 'discovered' the // existing receivers. expect(sender.hasReceivers()).toBe(true); expect(onStatusChanged).toHaveBeenCalledTimes(4); }).catch(fail).then(done); }); }); describe('cast', function() { it('fails when the cast API is not ready', function(done) { mockCastApi.isAvailable = false; sender.init(); expect(sender.apiReady()).toBe(false); sender.cast(fakeInitState).then(fail).catch(function(error) { expect(error.category).toBe(shaka.util.Error.Category.CAST); expect(error.code).toBe(shaka.util.Error.Code.CAST_API_UNAVAILABLE); }).then(done); }); it('fails when there are no receivers', function(done) { sender.init(); expect(sender.apiReady()).toBe(true); expect(sender.hasReceivers()).toBe(false); sender.cast(fakeInitState).then(fail).catch(function(error) { expect(error.category).toBe(shaka.util.Error.Category.CAST); expect(error.code).toBe(shaka.util.Error.Code.NO_CAST_RECEIVERS); }).then(done); }); it('creates a session and sends an "init" message', function(done) { sender.init(); expect(sender.apiReady()).toBe(true); fakeReceiverAvailability(true); expect(sender.hasReceivers()).toBe(true); let p = sender.cast(fakeInitState); fakeSessionConnection(); p.then(function() { expect(onStatusChanged).toHaveBeenCalled(); expect(sender.isCasting()).toBe(true); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', initState: fakeInitState, })); }).catch(fail).then(done); }); // 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. let connectionFailures = [ { condition: 'canceled by the user', castErrorCode: 'cancel', shakaErrorCode: 8004, // Code.CAST_CANCELED_BY_USER }, { condition: 'the connection times out', castErrorCode: 'timeout', shakaErrorCode: 8005, // Code.CAST_CONNECTION_TIMED_OUT }, { condition: 'the receiver is unavailable', castErrorCode: 'receiver_unavailable', shakaErrorCode: 8006, // Code.CAST_RECEIVER_APP_UNAVAILABLE }, { condition: 'an unexpected error occurs', castErrorCode: 'anything else', shakaErrorCode: 8003, // Code.UNEXPECTED_CAST_ERROR }, ]; connectionFailures.forEach(function(metadata) { it('fails when ' + metadata.condition, function(done) { sender.init(); fakeReceiverAvailability(true); let p = sender.cast(fakeInitState); fakeSessionConnectionFailure(metadata.castErrorCode); p.then(fail).catch(function(error) { expect(error.category).toBe(shaka.util.Error.Category.CAST); expect(error.code).toBe(metadata.shakaErrorCode); }).then(done); }); }); it('fails when we are already casting', function(done) { sender.init(); fakeReceiverAvailability(true); let p = sender.cast(fakeInitState); fakeSessionConnection(); p.catch(fail).then(function() { return sender.cast(fakeInitState); }).then(fail).catch(function(error) { expect(error.category).toBe(shaka.util.Error.Category.CAST); expect(error.code).toBe(shaka.util.Error.Code.ALREADY_CASTING); }).then(done); }); }); it('re-uses old sessions', function(done) { sender.init(); fakeReceiverAvailability(true); let p = sender.cast(fakeInitState); fakeSessionConnection(); let oldMockSession = mockSession; p.then(function() { return sender.destroy(); }).then(function() { // 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)); 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, })); }).catch(fail).then(done); }); it('doesn\'t re-use stopped sessions', function(done) { sender.init(); fakeReceiverAvailability(true); let p = sender.cast(fakeInitState); fakeSessionConnection(); p.then(function() { return sender.destroy(); }).then(function() { 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)); sender.init(); expect(sender.isCasting()).toBe(false); }).catch(fail).then(done); }); it('joins existing sessions automatically', function(done) { sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); Util.delay(0.1).then(function() { expect(onStatusChanged).toHaveBeenCalled(); expect(sender.isCasting()).toBe(true); expect(onInitStateRequired).toHaveBeenCalled(); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', initState: fakeInitState, })); }).catch(fail).then(done); }); describe('setAppData', function() { let fakeAppData = { myKey1: 'myValue1', myKey2: 'myValue2', }; it('sets "appData" for "init" message if not casting', function(done) { sender.init(); fakeReceiverAvailability(true); sender.setAppData(fakeAppData); sender.cast(fakeInitState).then(function() { expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'init', appData: fakeAppData, })); }).catch(fail).then(done); fakeSessionConnection(); }); it('sends a special "appData" message if casting', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { // 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, })); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('onFirstCastStateUpdate', function() { it('is triggered by an "update" message', function(done) { // You have to join an existing session for it to work. sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); Util.delay(0.1).then(function() { expect(onFirstCastStateUpdate).not.toHaveBeenCalled(); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).toHaveBeenCalled(); }).catch(fail).then(done); }); it('is not triggered if making a new session', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).not.toHaveBeenCalled(); }).catch(fail).then(done); fakeSessionConnection(); }); it('is triggered once per existing session', function(done) { sender.init(); fakeReceiverAvailability(true); fakeJoinExistingSession(); Util.delay(0.1).then(function() { 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(); return Util.delay(0.1); }).then(function() { fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(onFirstCastStateUpdate).toHaveBeenCalled(); }).catch(fail).then(done); }); }); describe('onRemoteEvent', function() { it('is triggered by an "event" message', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { let fakeEvent = { type: 'eventName', detail: {key1: 'value1'}, }; fakeSessionMessage({ type: 'event', targetName: 'video', event: fakeEvent, }); expect(onRemoteEvent).toHaveBeenCalledWith( 'video', jasmine.objectContaining(fakeEvent)); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('onResumeLocal', function() { it('is triggered when casting ends', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.isCasting()).toBe(true); expect(onResumeLocal).not.toHaveBeenCalled(); fakeRemoteDisconnect(); expect(sender.isCasting()).toBe(false); expect(onResumeLocal).toHaveBeenCalled(); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('showDisconnectDialog', function() { it('opens the dialog if we are casting', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { 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(); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('get', function() { it('returns most recent properties from "update" messages', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { let 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); }).catch(fail).then(done); fakeSessionConnection(); }); it('returns functions for video and player methods', function() { 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', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { let method = sender.get('video', 'play'); let retval = method(123, 'abc'); expect(retval).toBe(undefined); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'call', targetName: 'video', methodName: 'play', args: [123, 'abc'], })); }).catch(fail).then(done); fakeSessionConnection(); }); describe('async player methods', function() { let method; beforeEach(function(done) { method = null; sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { method = sender.get('player', 'load'); }).catch(fail).then(done); fakeSessionConnection(); }); it('return Promises', function() { let p = method(); expect(p).toEqual(jasmine.any(Promise)); p.catch(function() {}); // silence logs about uncaught rejections }); it('trigger "asyncCall" messages', function() { let p = method(123, 'abc'); p.catch(function() {}); // 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', function(done) { let p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); let id = mockSession.messages[mockSession.messages.length - 1].id; fakeSessionMessage({ type: 'asyncComplete', id: id, error: null, }); // Wait a tick for the Promise status to change. return Util.delay(0.1); }).then(function() { expect(p.status).toBe('resolved'); }).catch(fail).then(done); }); it('reject when "asyncComplete" messages have an error', function(done) { let 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'); let p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); let id = mockSession.messages[mockSession.messages.length - 1].id; fakeSessionMessage({ type: 'asyncComplete', id: id, error: originalError, }); // Wait a tick for the Promise status to change. return Util.delay(0.1); }).then(function() { expect(p.status).toBe('rejected'); return p.catch(function(error) { Util.expectToEqualError(error, originalError); }); }).catch(fail).then(done); }); it('reject when disconnected remotely', function(done) { let p = new shaka.test.StatusPromise(method(123, 'abc')); // Wait a tick for the Promise status to be set. Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); fakeRemoteDisconnect(); // Wait a tick for the Promise status to change. return Util.delay(0.1); }).then(function() { expect(p.status).toBe('rejected'); return p.catch(function(error) { Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); }); }).catch(fail).then(done); }); }); }); describe('set', function() { it('overrides any cached properties', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { let 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); }).catch(fail).then(done); fakeSessionConnection(); }); it('causes a "set" message to be sent', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { sender.set('video', 'muted', true); expect(mockSession.messages).toContain(jasmine.objectContaining({ type: 'set', targetName: 'video', property: 'muted', value: true, })); }).catch(fail).then(done); fakeSessionConnection(); }); it('can be used before we have an "update" message', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.get('video', 'muted')).toBe(undefined); sender.set('video', 'muted', true); expect(sender.get('video', 'muted')).toBe(true); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('hasRemoteProperties', function() { it('is true only after we have an "update" message', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.hasRemoteProperties()).toBe(false); fakeSessionMessage({ type: 'update', update: {video: {currentTime: 12}, player: {isLive: false}}, }); expect(sender.hasRemoteProperties()).toBe(true); }).catch(fail).then(done); fakeSessionConnection(); }); }); describe('forceDisconnect', function() { it('disconnects and cancels all async operations', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { 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(); let method = sender.get('player', 'load'); let p = new shaka.test.StatusPromise(method()); // Wait a tick for the Promise status to be set. return Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); sender.forceDisconnect(); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).toHaveBeenCalled(); expect(mockSession.removeUpdateListener).toHaveBeenCalled(); expect(mockSession.removeMessageListener).toHaveBeenCalled(); // Wait a tick for the Promise status to change. return Util.delay(0.1); }).then(function() { expect(p.status).toBe('rejected'); return p.catch(function(error) { Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); }); }); }).catch(fail).then(done); fakeSessionConnection(); }); 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 = /** @type {Object} */(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', function() { it('cancels all async operations', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.isCasting()).toBe(true); expect(mockSession.stop).not.toHaveBeenCalled(); expect(mockSession.removeUpdateListener).not.toHaveBeenCalled(); expect(mockSession.removeMessageListener).not.toHaveBeenCalled(); let method = sender.get('player', 'load'); let p = new shaka.test.StatusPromise(method()); // Wait a tick for the Promise status to be set. return Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); sender.destroy().catch(fail); expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); expect(mockSession.removeUpdateListener).toHaveBeenCalled(); expect(mockSession.removeMessageListener).toHaveBeenCalled(); // Wait a tick for the Promise status to change. return Util.delay(0.1); }).then(function() { expect(p.status).toBe('rejected'); return p.catch(function(error) { Util.expectToEqualError(error, new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.LOAD_INTERRUPTED)); }); }); }).catch(fail).then(done); fakeSessionConnection(); }); }); 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() { let 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( function(namespace, message, successCallback, errorCallback) { session.messages.push(CastUtils.deserialize(message)); }); return session; } /** * @param {boolean} yes If true, simulate receivers being available. */ function fakeReceiverAvailability(yes) { let calls = mockCastApi.ApiConfig.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let onReceiverStatusChanged = calls.argsFor(0)[2]; onReceiverStatusChanged(yes ? 'available' : 'unavailable'); } } function fakeSessionConnection() { let calls = mockCastApi.requestSession.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let onSessionInitiated = calls.argsFor(0)[0]; mockSession = createMockCastSession(); onSessionInitiated(mockSession); } } /** * @param {string} code */ function fakeSessionConnectionFailure(code) { let calls = mockCastApi.requestSession.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let onSessionError = calls.argsFor(0)[1]; onSessionError({code: code}); } } /** * @param {?} message */ function fakeSessionMessage(message) { let calls = mockSession.addMessageListener.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let namespace = calls.argsFor(0)[0]; let listener = calls.argsFor(0)[1]; let serialized = CastUtils.serialize(message); listener(namespace, serialized); } } function fakeRemoteDisconnect() { mockSession.status = 'disconnected'; let calls = mockSession.addUpdateListener.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let onConnectionStatus = calls.argsFor(0)[0]; onConnectionStatus(); } } function fakeJoinExistingSession() { let calls = mockCastApi.ApiConfig.calls; expect(calls.count()).toEqual(1); if (calls.count()) { let onJoinExistingSession = 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); } });