Files
shaka-player/test/text/text_engine_unit.js
T
Aaron Vaage 60db39bc80 Create Jasmine Matchers for OnceMore Checks
In our tests we regularly have checks that repeatedly check if a
function/spy has been called once more. To check this we would check
the call and then reset the calls each time.

To abstract this out, this CL defines two custom jasmine matchers that
allow us to check if a function/spy has been called once more.

To validate the matchers, TextEngine tests were update to make use of
it.

Change-Id: Ia6bf8d0a585648126881e1713b35be674bd47ba7
2019-01-08 19:58:58 +00:00

464 lines
15 KiB
JavaScript

/**
* @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('TextEngine', function() {
const TextEngine = shaka.text.TextEngine;
const dummyData = new ArrayBuffer(0);
const dummyMimeType = 'text/fake';
/** @type {!Function} */
let mockParserPlugIn;
/** @type {!shaka.test.FakeTextDisplayer} */
let mockDisplayer;
/** @type {!jasmine.Spy} */
let mockParseInit;
/** @type {!jasmine.Spy} */
let mockParseMedia;
/** @type {!shaka.text.TextEngine} */
let textEngine;
beforeEach(function() {
mockParseInit = jasmine.createSpy('mockParseInit');
mockParseMedia = jasmine.createSpy('mockParseMedia');
mockParserPlugIn = function() {
return {
parseInit: mockParseInit,
parseMedia: mockParseMedia,
};
};
mockDisplayer = new shaka.test.FakeTextDisplayer();
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
textEngine = new TextEngine(mockDisplayer);
textEngine.initParser(dummyMimeType);
});
afterEach(function() {
TextEngine.unregisterParser(dummyMimeType);
});
describe('isTypeSupported', function() {
it('reports support only when a parser is installed', function() {
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
TextEngine.registerParser(dummyMimeType, mockParserPlugIn);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(true);
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
});
it('reports support when it\'s closed captions and muxjs is available',
function() {
const closedCaptionsType =
shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE;
const originalMuxjs = window.muxjs;
expect(TextEngine.isTypeSupported(closedCaptionsType)).toBe(true);
try {
window['muxjs'] = null;
expect(TextEngine.isTypeSupported(closedCaptionsType)).toBe(false);
} finally {
window['muxjs'] = originalMuxjs;
}
});
});
describe('appendBuffer', function() {
it('works asynchronously', function(done) {
mockParseMedia.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done);
expect(mockDisplayer.append).not.toHaveBeenCalled();
});
it('calls displayer.append()', async () => {
let cue1 = createFakeCue(1, 2);
let cue2 = createFakeCue(2, 3);
let cue3 = createFakeCue(3, 4);
let cue4 = createFakeCue(4, 5);
mockParseMedia.and.returnValue([cue1, cue2]);
await textEngine.appendBuffer(dummyData, 0, 3);
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 0, segmentEnd: 3},
]);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[cue1, cue2],
]);
expect(mockDisplayer.remove).not.toHaveBeenCalled();
mockParseMedia.and.returnValue([cue3, cue4]);
await textEngine.appendBuffer(dummyData, 3, 5);
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 3, segmentEnd: 5},
]);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[cue3, cue4],
]);
});
it('does not throw if called right before destroy', function(done) {
mockParseMedia.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done);
textEngine.destroy();
});
});
describe('storeAndAppendClosedCaptions', function() {
it('append closed captions with selected id', function() {
const caption = {
startPts: 0,
endPts: 100,
startTime: 0,
endTime: 1,
stream: 'CC1',
text: 'captions',
};
textEngine.setSelectedClosedCaptionId('CC1', 0);
textEngine.storeAndAppendClosedCaptions([caption], 0, 2);
expect(mockDisplayer.append).toHaveBeenCalled();
});
it('does not append closed captions without selected id', function() {
const caption = {
startPts: 0,
endPts: 100,
startTime: 1,
endTime: 2,
stream: 'CC1',
text: 'caption2',
};
textEngine.setSelectedClosedCaptionId('CC3', 0);
textEngine.storeAndAppendClosedCaptions([caption], 0, 2);
expect(mockDisplayer.append).not.toHaveBeenCalled();
});
it('stores closed captions', function() {
const caption0 = {
startPts: 0,
endPts: 100,
startTime: 0,
endTime: 1,
stream: 'CC1',
text: 'caption1',
};
const caption1 = {
startPts: 0,
endPts: 100,
startTime: 1,
endTime: 2,
stream: 'CC1',
text: 'caption2',
};
const caption2 = {
startPts: 0,
endPts: 100,
startTime: 1,
endTime: 2,
stream: 'CC3',
text: 'caption3',
};
textEngine.setSelectedClosedCaptionId('CC1', 0);
// Text Engine stores all the closed captions as a two layer map.
// {closed caption id -> {start and end time -> cues}}
textEngine.storeAndAppendClosedCaptions([caption0], 0, 1);
expect(textEngine.getNumberOfClosedCaptionChannels()).toEqual(1);
expect(textEngine.getNumberOfClosedCaptionsInChannel('CC1')).toEqual(1);
textEngine.storeAndAppendClosedCaptions([caption1], 1, 2);
// Caption1 has the same stream id with caption0, but different start and
// end time. The closed captions map should have 1 key CC1, and two values
// for two start and end times.
expect(textEngine.getNumberOfClosedCaptionChannels()).toEqual(1);
expect(textEngine.getNumberOfClosedCaptionsInChannel('CC1')).toEqual(2);
textEngine.storeAndAppendClosedCaptions([caption2], 1, 2);
// Caption2 has a different stream id CC3, so the closed captions map
// should have two different keys, CC1 and CC3.
expect(textEngine.getNumberOfClosedCaptionChannels()).toEqual(2);
});
});
describe('remove', function() {
let cue1;
let cue2;
let cue3;
beforeEach(function() {
cue1 = createFakeCue(0, 1);
cue2 = createFakeCue(1, 2);
cue3 = createFakeCue(2, 3);
mockParseMedia.and.returnValue([cue1, cue2, cue3]);
});
it('works asynchronously', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
let p = textEngine.remove(0, 1);
expect(mockDisplayer.remove).not.toHaveBeenCalled();
return p;
}).catch(fail).then(done);
});
it('calls displayer.remove()', function(done) {
textEngine.remove(0, 1).then(function() {
expect(mockDisplayer.remove).toHaveBeenCalledWith(0, 1);
}).catch(fail).then(done);
});
it('does not throw if called right before destroy', function(done) {
textEngine.remove(0, 1).catch(fail).then(done);
textEngine.destroy();
});
});
describe('setTimestampOffset', function() {
it('passes the offset to the parser', async () => {
mockParseMedia.and.callFake((data, time) => {
return [
createFakeCue(time.periodStart + 0,
time.periodStart + 1),
createFakeCue(time.periodStart + 2,
time.periodStart + 3),
];
});
await textEngine.appendBuffer(dummyData, 0, 3);
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
new Uint8Array(dummyData),
{periodStart: 0, segmentStart: 0, segmentEnd: 3},
]);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[
createFakeCue(0, 1),
createFakeCue(2, 3),
],
]);
textEngine.setTimestampOffset(4);
await textEngine.appendBuffer(dummyData, 4, 7);
expect(mockParseMedia).toHaveBeenCalledOnceMoreWith([
new Uint8Array(dummyData),
{periodStart: 4, segmentStart: 4, segmentEnd: 7},
]);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[
createFakeCue(4, 5),
createFakeCue(6, 7),
],
]);
});
});
describe('bufferStart/bufferEnd', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('return null when there are no cues', function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
});
it('reflect newly-added cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(3);
return textEngine.appendBuffer(dummyData, 3, 6);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(6);
return textEngine.appendBuffer(dummyData, 6, 10);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
}).catch(fail).then(done);
});
it('reflect newly-removed cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
return textEngine.appendBuffer(dummyData, 3, 6);
}).then(function() {
return textEngine.appendBuffer(dummyData, 6, 10);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(8, 11);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(11, 20);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(0, Infinity);
}).then(function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
}).catch(fail).then(done);
});
it('does not use timestamp offset', async function() {
// The start and end times passed to appendBuffer are now absolute, so
// they already account for timestampOffset and period offset.
// See https://github.com/google/shaka-player/issues/1562
textEngine.setTimestampOffset(60);
await textEngine.appendBuffer(dummyData, 0, 3);
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(3);
await textEngine.appendBuffer(dummyData, 3, 6);
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(6);
});
});
describe('bufferedAheadOf', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('returns 0 when there are no cues', function() {
expect(textEngine.bufferedAheadOf(0)).toBe(0);
});
it('returns 0 if |t| is not buffered', function(done) {
textEngine.appendBuffer(dummyData, 3, 6).then(function() {
expect(textEngine.bufferedAheadOf(6.1)).toBe(0);
}).catch(fail).then(done);
});
it('ignores gaps in the content', function(done) {
textEngine.appendBuffer(dummyData, 3, 6).then(function() {
expect(textEngine.bufferedAheadOf(2)).toBe(3);
}).catch(fail).then(done);
});
it('returns the distance to the end if |t| is buffered', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferedAheadOf(0)).toBe(3);
expect(textEngine.bufferedAheadOf(1)).toBe(2);
expect(textEngine.bufferedAheadOf(2.5)).toBeCloseTo(0.5);
}).catch(fail).then(done);
});
it('does not use timestamp offset', async function() {
// The start and end times passed to appendBuffer are now absolute, so
// they already account for timestampOffset and period offset.
// See https://github.com/google/shaka-player/issues/1562
textEngine.setTimestampOffset(60);
await textEngine.appendBuffer(dummyData, 3, 6);
expect(textEngine.bufferedAheadOf(4)).toBe(2);
expect(textEngine.bufferedAheadOf(64)).toBe(0);
});
});
describe('setAppendWindow', function() {
beforeEach(function() {
mockParseMedia.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('limits appended cues', async () => {
textEngine.setAppendWindow(0, 1.9);
await textEngine.appendBuffer(dummyData, 0, 3);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[
createFakeCue(0, 1),
createFakeCue(1, 2),
],
]);
textEngine.setAppendWindow(1, 2.1);
await textEngine.appendBuffer(dummyData, 0, 3);
expect(mockDisplayer.append).toHaveBeenCalledOnceMoreWith([
[
createFakeCue(1, 2),
createFakeCue(2, 3),
],
]);
});
it('limits bufferStart', function(done) {
textEngine.setAppendWindow(1, 9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferStart()).toBe(1);
return textEngine.remove(0, 9);
}).then(function() {
textEngine.setAppendWindow(2.1, 9);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(2.1);
}).catch(fail).then(done);
});
it('limits bufferEnd', function(done) {
textEngine.setAppendWindow(0, 1.9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferEnd()).toBe(1.9);
textEngine.setAppendWindow(0, 2.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(2.1);
textEngine.setAppendWindow(0, 4.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(3);
}).catch(fail).then(done);
});
});
function createFakeCue(startTime, endTime) {
return {startTime: startTime, endTime: endTime};
}
});