shaka-player
Version:
DASH/EME video player library
625 lines (529 loc) • 22.5 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('AbortableOperation', () => {
const Util = shaka.test.Util;
describe('promise', () => {
it('is resolved by the constructor argument', async () => {
const promise = new shaka.util.PublicPromise();
const abort = () => Promise.resolve();
const operation = new shaka.util.AbortableOperation(promise, abort);
promise.resolve(100);
const value = await operation.promise;
expect(value).toBe(100);
});
});
describe('abort', () => {
it('calls the abort argument from the constructor', () => {
const promise = Promise.resolve();
const abort =
jasmine.createSpy('abort').and.returnValue(Promise.resolve());
const operation = new shaka.util.AbortableOperation(
promise, shaka.test.Util.spyFunc(abort));
operation.abort();
expect(abort).toHaveBeenCalled();
});
it('is resolved when the underlying abort() is resolved', async () => {
/** @type {!shaka.util.PublicPromise} */
const p = new shaka.util.PublicPromise();
const abort = jasmine.createSpy('abort').and.returnValue(p);
const operation = new shaka.util.AbortableOperation(
new shaka.util.PublicPromise(), shaka.test.Util.spyFunc(abort));
const abortComplete = jasmine.createSpy('abort complete');
operation.abort().then(shaka.test.Util.spyFunc(abortComplete), fail);
expect(abortComplete).not.toHaveBeenCalled();
await shaka.test.Util.shortDelay();
// Nothing has happened yet, so abort is not complete.
expect(abortComplete).not.toHaveBeenCalled();
// Resolve the underlying Promise.
p.resolve();
await shaka.test.Util.shortDelay();
// The abort is now complete.
expect(abortComplete).toHaveBeenCalled();
});
});
describe('failed', () => {
it('creates a failed operation with the given error', async () => {
const error = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const operation = shaka.util.AbortableOperation.failed(error);
await expectAsync(operation.promise)
.toBeRejectedWith(Util.jasmineError(error));
});
});
describe('aborted', () => {
it('creates a failed operation with OPERATION_ABORTED', async () => {
const error = Util.jasmineError(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.OPERATION_ABORTED));
const operation = shaka.util.AbortableOperation.aborted();
await expectAsync(operation.promise).toBeRejectedWith(error);
});
});
describe('completed', () => {
it('creates a completed operation with the given value', async () => {
const operation = shaka.util.AbortableOperation.completed(100);
const value = await operation.promise;
expect(value).toBe(100);
});
});
describe('notAbortable', () => {
it('creates an operation from the given promise', async () => {
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
const operation = shaka.util.AbortableOperation.notAbortable(promise);
let isAborted = false;
operation.abort().then(() => {
isAborted = true;
});
let isComplete = false;
operation.promise.catch(fail).then((value) => {
isComplete = true;
expect(value).toBe(100);
});
await shaka.test.Util.shortDelay();
// Even though we called abort(), the operation hasn't completed
// because it isn't abortable. The abort() Promise hasn't been
// resolved yet, either.
expect(isComplete).toBe(false);
expect(isAborted).toBe(false);
promise.resolve(100);
await shaka.test.Util.shortDelay();
// Now that we resolved the underlying promise, the operation is
// complete, and so is the abort() Promise.
expect(isComplete).toBe(true);
expect(isAborted).toBe(true);
});
}); // describe('notAbortable')
describe('all', () => {
it('creates a successful operation when all succeed', async () => {
/** @type {!shaka.util.PublicPromise} */
const p1 = new shaka.util.PublicPromise();
const op1 = shaka.util.AbortableOperation.notAbortable(p1);
/** @type {!shaka.util.PublicPromise} */
const p2 = new shaka.util.PublicPromise();
const op2 = shaka.util.AbortableOperation.notAbortable(p2);
/** @type {!shaka.util.PublicPromise} */
const p3 = new shaka.util.PublicPromise();
const op3 = shaka.util.AbortableOperation.notAbortable(p3);
const all = shaka.util.AbortableOperation.all([op1, op2, op3]);
const onSuccessSpy = jasmine.createSpy('onSuccess');
const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
const onErrorSpy = jasmine.createSpy('onError');
const onError = shaka.test.Util.spyFunc(onErrorSpy);
all.promise.then(onSuccess, onError);
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
p1.resolve();
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
p2.resolve();
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
p3.resolve();
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
});
it('creates a failed operation when any fail', async () => {
/** @type {!shaka.util.PublicPromise} */
const p1 = new shaka.util.PublicPromise();
const op1 = shaka.util.AbortableOperation.notAbortable(p1);
/** @type {!shaka.util.PublicPromise} */
const p2 = new shaka.util.PublicPromise();
const op2 = shaka.util.AbortableOperation.notAbortable(p2);
const p3 = new shaka.util.PublicPromise();
const op3 = shaka.util.AbortableOperation.notAbortable(p3);
const all = shaka.util.AbortableOperation.all([op1, op2, op3]);
const onSuccessSpy = jasmine.createSpy('onSuccess');
const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
const onErrorSpy = jasmine.createSpy('onError');
const onError = shaka.test.Util.spyFunc(onErrorSpy);
all.promise.then(onSuccess, onError);
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
p1.resolve();
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
p2.reject('error');
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).toHaveBeenCalledWith('error');
});
it('aborts all operations on abort', async () => {
/** @type {!shaka.util.PublicPromise} */
const p1 = new shaka.util.PublicPromise();
const abort1Spy = jasmine.createSpy('abort1')
.and.callFake(() => p1.reject());
const abort1 = shaka.test.Util.spyFunc(abort1Spy);
const op1 = new shaka.util.AbortableOperation(p1, abort1);
/** @type {!shaka.util.PublicPromise} */
const p2 = new shaka.util.PublicPromise();
const abort2Spy = jasmine.createSpy('abort2')
.and.callFake(() => p2.reject());
const abort2 = shaka.test.Util.spyFunc(abort2Spy);
const op2 = new shaka.util.AbortableOperation(p2, abort2);
/** @type {!shaka.util.PublicPromise} */
const p3 = new shaka.util.PublicPromise();
const abort3Spy = jasmine.createSpy('abort3')
.and.callFake(() => p3.reject());
const abort3 = shaka.test.Util.spyFunc(abort3Spy);
const op3 = new shaka.util.AbortableOperation(p3, abort3);
/** @type {!shaka.util.AbortableOperation} */
const all = shaka.util.AbortableOperation.all([op1, op2, op3]);
const onSuccessSpy = jasmine.createSpy('onSuccess');
const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
const onErrorSpy = jasmine.createSpy('onError');
const onError = shaka.test.Util.spyFunc(onErrorSpy);
all.promise.then(onSuccess, onError);
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).not.toHaveBeenCalled();
expect(abort1Spy).not.toHaveBeenCalled();
expect(abort2Spy).not.toHaveBeenCalled();
expect(abort3Spy).not.toHaveBeenCalled();
all.abort();
await shaka.test.Util.shortDelay();
expect(onSuccessSpy).not.toHaveBeenCalled();
expect(onErrorSpy).toHaveBeenCalled();
expect(abort1Spy).toHaveBeenCalled();
expect(abort2Spy).toHaveBeenCalled();
expect(abort3Spy).toHaveBeenCalled();
});
}); // describe('all')
describe('finally', () => {
it('executes after the operation is successful', async () => {
let isDone = false;
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
shaka.util.AbortableOperation.notAbortable(promise).finally((ok) => {
expect(ok).toBe(true);
isDone = true;
});
await shaka.test.Util.shortDelay();
expect(isDone).toBe(false);
promise.resolve(100);
await shaka.test.Util.shortDelay();
expect(isDone).toBe(true);
});
it('executes after the operation fails', async () => {
let isDone = false;
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
shaka.util.AbortableOperation.notAbortable(promise).finally((ok) => {
expect(ok).toBe(false);
isDone = true;
});
await shaka.test.Util.shortDelay();
expect(isDone).toBe(false);
promise.reject(0);
await shaka.test.Util.shortDelay();
expect(isDone).toBe(true);
});
it('executes after the chain is successful', async () => {
let isDone = false;
/** @type {!shaka.util.PublicPromise} */
const promise1 = new shaka.util.PublicPromise();
/** @type {!shaka.util.PublicPromise} */
const promise2 = new shaka.util.PublicPromise();
shaka.util.AbortableOperation.notAbortable(promise1).chain(() => {
return shaka.util.AbortableOperation.notAbortable(promise2);
}).finally((ok) => {
expect(ok).toBe(true);
isDone = true;
});
await shaka.test.Util.shortDelay();
expect(isDone).toBe(false);
promise1.resolve();
await shaka.test.Util.shortDelay();
expect(isDone).toBe(false);
promise2.resolve();
await shaka.test.Util.shortDelay();
expect(isDone).toBe(true);
});
it('executes after the chain fails', async () => {
let isDone = false;
/** @type {!shaka.util.PublicPromise} */
const promise1 = new shaka.util.PublicPromise();
const promise2 = new shaka.util.PublicPromise();
shaka.util.AbortableOperation.notAbortable(promise1).chain(() => {
return shaka.util.AbortableOperation.notAbortable(promise2);
}).finally((ok) => {
expect(ok).toBe(false);
isDone = true;
});
await shaka.test.Util.shortDelay();
expect(isDone).toBe(false);
promise1.reject(0);
await shaka.test.Util.shortDelay();
expect(isDone).toBe(true);
});
it('executes after a complex chain', async () => {
let isDone = false;
shaka.util.AbortableOperation.completed(0).chain(() => {
return shaka.util.AbortableOperation.aborted();
}).chain(() => {
fail('Should not be reachable');
}, (e) => {
return shaka.util.AbortableOperation.completed(100);
}).finally((ok) => {
expect(ok).toBe(true);
isDone = true;
});
await shaka.test.Util.shortDelay();
expect(isDone).toBe(true);
});
}); // describe('finally')
describe('chain', () => {
it('passes the value to the next operation on success', async () => {
/** @type {!Array.<number>} */
const values = [];
const op = shaka.util.AbortableOperation.completed(100).chain((value) => {
values.push(value);
expect(value).toBe(100);
// Plain value
return 200;
}).chain((value) => {
values.push(value);
expect(value).toBe(200);
// Resolved Promise
return Promise.resolve(300);
}).chain((value) => {
values.push(value);
expect(value).toBe(300);
// Delayed Promise
return shaka.test.Util.shortDelay().then(() => 400);
}).chain((value) => {
values.push(value);
expect(value).toBe(400);
// Abortable operation
return shaka.util.AbortableOperation.completed(500);
}).chain((value) => {
values.push(value);
expect(value).toBe(500);
}).finally((ok) => {
expect(ok).toBe(true);
// The bug https://github.com/shaka-project/shaka-player/issues/1260
// makes this expectation fail because some stages were skipped.
// Without this check, the test would pass, even though the bug shows
// up first in the basic functionality of 'chain'.
expect(values).toEqual([100, 200, 300, 400, 500]);
});
await op.promise;
});
it('skips the onSuccess callbacks on error', async () => {
const error = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const op = shaka.util.AbortableOperation.failed(error)
.chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error);
throw error; // rethrow
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error);
throw error; // rethrow
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error);
}).finally((ok) => {
expect(ok).toBe(true); // Last stage did not rethrow
});
await op.promise;
});
it('can fall back to other operations in onError callback', async () => {
const error1 = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const error2 = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
const error3 = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.EBML_BAD_FLOATING_POINT_SIZE);
const op = shaka.util.AbortableOperation.failed(error1)
.chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error1);
return shaka.util.AbortableOperation.failed(error2);
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error2);
return shaka.util.AbortableOperation.failed(error3);
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error3);
return shaka.util.AbortableOperation.completed(400);
}).chain((value) => {
expect(value).toBe(400);
}).finally((ok) => {
expect(ok).toBe(true);
});
await op.promise;
});
it('fails when an error is thrown', async () => {
const error1 = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const error2 = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
const op = shaka.util.AbortableOperation.completed(100).chain((value) => {
throw error1;
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error1);
throw error2;
}).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error2);
}).finally((ok) => {
expect(ok).toBe(true); // Last stage did not rethrow
});
await op.promise;
});
it('goes to success state when onError returns undefined', async () => {
const error = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const op = shaka.util.AbortableOperation.failed(error)
.chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error);
// no return value
}).chain((value) => {
expect(value).toBe(undefined);
}, fail).finally((ok) => {
expect(ok).toBe(true);
});
await op.promise;
});
it('does not need return when onSuccess omitted', async () => {
const operation = shaka.util.AbortableOperation.completed(100)
.chain(undefined, fail).chain(undefined, fail).chain((value) => {
expect(value).toBe(100);
}).finally((ok) => {
expect(ok).toBe(true);
});
await operation.promise;
});
it('does not need rethrow when onError omitted', async () => {
const error = new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI);
const operation = shaka.util.AbortableOperation.failed(error)
.chain(fail).chain(fail).chain(fail).chain(fail, (e) => {
shaka.test.Util.expectToEqualError(e, error);
}).finally((ok) => {
expect(ok).toBe(true); // Last stage did not rethrow
});
await operation.promise;
});
it('ensures abort is called with the correct "this"', async () => {
// During testing and development, an early version of chain() would
// sometimes unbind an abort method from an earlier stage of the chain.
// Make sure this doesn't happen.
let innerOperation;
/** @type {!shaka.util.PublicPromise} */
const p = new shaka.util.PublicPromise();
let abortCalled = false;
/**
* NOTE: This is a subtle thing, but this must be an ES5 anonymous
* function for the test to work. ES6 arrow functions would always be
* called with the "this" of the test itself, regardless of what the
* library is doing.
*
* @this {shaka.util.AbortableOperation}
* @return {!Promise}
*/
function abort() {
expect(this).toBe(innerOperation);
abortCalled = true;
return Promise.resolve();
}
// Since the issue was with the calling of operation.abort, rather than
// the onAbort_ callback, we make an operation-like thing instead of using
// the AbortableOperation constructor.
innerOperation = {promise: p, abort: abort};
// The second stage of the chain returns innerOperation. A brief moment
// later, the outer chain is aborted.
const operation =
shaka.util.AbortableOperation.completed(100)
.chain(() => {
shaka.test.Util.shortDelay().then(() => {
operation.abort();
p.resolve();
});
return innerOperation;
})
.finally((ok) => {
// We resolved the non-abortable inner operation
expect(ok).toBe(true);
expect(abortCalled).toBe(true);
});
await operation.promise;
});
it('aborts nested AbortableOperation objects', async () => {
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
const abort = jasmine.createSpy('abort');
/** @type {!shaka.util.AbortableOperation} */
const operation = shaka.util.AbortableOperation.completed(0).chain(() => {
return shaka.util.AbortableOperation.completed(1)
.chain(() => 2)
.chain(() => {
return new shaka.util.AbortableOperation(
promise, Util.spyFunc(abort));
})
.chain(() => 3);
});
await Util.shortDelay(); // Ensure we are waiting on the "promise".
operation.abort();
promise.resolve();
await expectAsync(operation.promise).toBeRejected();
expect(abort).toHaveBeenCalled();
});
it('aborts even with a failure callback', async () => {
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
/** @type {!shaka.util.AbortableOperation} */
const operation = shaka.util.AbortableOperation.completed(0)
.chain(() => promise)
.chain(
() => fail('Promise should be rejected'),
() => {});
await Util.shortDelay(); // Ensure we are waiting on the "promise".
operation.abort();
promise.reject();
await expectAsync(operation.promise).toBeRejected();
});
it('abort waits for plain Promise to be resolved', async () => {
/** @type {!shaka.util.PublicPromise} */
const promise = new shaka.util.PublicPromise();
const resolved = jasmine.createSpy('resolve');
/** @type {!shaka.util.AbortableOperation} */
const operation = shaka.util.AbortableOperation.completed(0)
.chain(() => promise);
await Util.shortDelay(); // Ensure we are waiting on the "promise".
const p = operation.abort().then(
Util.spyFunc(resolved), Util.spyFunc(resolved));
// Even after waiting for some Promises, the abort() promise should not
// get resolved until the "promise" is finished.
await Util.shortDelay();
expect(resolved).not.toHaveBeenCalled();
// Now the abort() can get resolved.
promise.resolve();
await p;
});
}); // describe('chain')
}); // describe('AbortableOperation')