shaka-player
Version:
DASH/EME video player library
974 lines (824 loc) • 31.4 kB
JavaScript
/*! @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);
}
});