diff --git a/lib/offline/offline_utils.js b/lib/offline/offline_utils.js index aaa2abb6c..8bc723805 100644 --- a/lib/offline/offline_utils.js +++ b/lib/offline/offline_utils.js @@ -67,6 +67,7 @@ shaka.offline.OfflineUtils.getStoredContent = function(manifest) { */ shaka.offline.OfflineUtils.reconstructPeriod = function( period, drmInfos, timeline) { + // TODO(modmaker): Add unit tests for this method. var OfflineUtils = shaka.offline.OfflineUtils; var textStreamsDb = period.streams.filter(function(streamDb) { return streamDb.contentType == 'text'; diff --git a/lib/util/data_view_reader.js b/lib/util/data_view_reader.js index 8b0b4b2d6..6889456fb 100644 --- a/lib/util/data_view_reader.js +++ b/lib/util/data_view_reader.js @@ -200,18 +200,13 @@ shaka.util.DataViewReader.prototype.skip = function(bytes) { * Keeps reading until it reaches a byte that equals to zero. The text is * assumed to be UTF-8. * @return {string} - * @throws {shaka.util.Error} when reading past the end of the data view. */ shaka.util.DataViewReader.prototype.readTerminatedString = function() { var start = this.position_; - try { - while (this.hasMoreData()) { - var value = this.dataView_.getUint8(this.position_); - if (value == 0) break; - this.position_ += 1; - } - } catch (exception) { - this.throwOutOfBounds_(); + while (this.hasMoreData()) { + var value = this.dataView_.getUint8(this.position_); + if (value == 0) break; + this.position_ += 1; } var ret = this.dataView_.buffer.slice(start, this.position_); diff --git a/lib/util/event_manager.js b/lib/util/event_manager.js index 7fdb06acb..a5c517c68 100644 --- a/lib/util/event_manager.js +++ b/lib/util/event_manager.js @@ -17,6 +17,7 @@ goog.provide('shaka.util.EventManager'); +goog.require('goog.asserts'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.MultiMap'); @@ -129,9 +130,7 @@ shaka.util.EventManager.Binding_ = function(target, type, listener) { * event listener is already detached. */ shaka.util.EventManager.Binding_.prototype.unlisten = function() { - if (!this.target) - return; - + goog.asserts.assert(this.target, 'Missing target'); this.target.removeEventListener(this.type, this.listener, false); this.target = null; diff --git a/lib/util/map_utils.js b/lib/util/map_utils.js index aa70af447..18465c1bf 100644 --- a/lib/util/map_utils.js +++ b/lib/util/map_utils.js @@ -65,24 +65,6 @@ shaka.util.MapUtils.map = function(object, callback) { }; -/** - * Creates a new object where the values are filtered out according to a - * predicate. - * - * @param {!Object.} object - * @param {function(KEY, VALUE):boolean} callback - * @return {!Object.} - * @template KEY,VALUE - */ -shaka.util.MapUtils.filter = function(object, callback) { - return Object.keys(object).reduce(function(ret, key) { - if (callback(key, object[key])) - ret[key] = object[key]; - return ret; - }, {}); -}; - - /** * Returns true if every entry matches the predicate. * @@ -96,18 +78,3 @@ shaka.util.MapUtils.every = function(object, callback) { return callback(key, object[key]); }); }; - - -/** - * Returns true if any entry matches the predicate. - * - * @param {!Object.} object - * @param {function(KEY, VALUE):boolean} callback - * @return {boolean} - * @template KEY,VALUE - */ -shaka.util.MapUtils.some = function(object, callback) { - return Object.keys(object).some(function(key) { - return callback(key, object[key]); - }); -}; diff --git a/lib/util/multi_map.js b/lib/util/multi_map.js index 31a1097fc..56cfc851c 100644 --- a/lib/util/multi_map.js +++ b/lib/util/multi_map.js @@ -45,26 +45,6 @@ shaka.util.MultiMap.prototype.push = function(key, value) { }; -/** - * Set an array of values for the key, overwriting any previous data. - * @param {string} key - * @param {!Array.} values - */ -shaka.util.MultiMap.prototype.set = function(key, values) { - this.map_[key] = values; -}; - - -/** - * Check for a key. - * @param {string} key - * @return {boolean} true if the key exists. - */ -shaka.util.MultiMap.prototype.has = function(key) { - return this.map_.hasOwnProperty(key); -}; - - /** * Get a list of values by key. * @param {string} key @@ -108,19 +88,6 @@ shaka.util.MultiMap.prototype.remove = function(key, value) { }; -/** - * Get all keys from the multimap. - * @return {!Array.} - */ -shaka.util.MultiMap.prototype.keys = function() { - var result = []; - for (var key in this.map_) { - result.push(key); - } - return result; -}; - - /** * Clear all keys and values from the multimap. */ diff --git a/lib/util/timer.js b/lib/util/timer.js index 437d67c8c..ce2f37c8e 100644 --- a/lib/util/timer.js +++ b/lib/util/timer.js @@ -29,9 +29,6 @@ shaka.util.Timer = function(callback) { /** @private {?number} */ this.id_ = null; - /** @private {number} */ - this.timeoutSeconds_ = 0; - /** @private {Function} */ this.callback_ = (function() { this.id_ = null; @@ -57,22 +54,5 @@ shaka.util.Timer.prototype.cancel = function() { */ shaka.util.Timer.prototype.schedule = function(seconds) { this.cancel(); - this.timeoutSeconds_ = seconds; this.id_ = setTimeout(this.callback_, seconds * 1000); }; - - -/** - * If the timer is running, reschedule it using the previous scheduled timeout. - * @example - * If scheduled for 5 seconds, and rescheduled 3 seconds later, - * the timer will fire 8 seconds after the original scheduling. - * @example - * If scheduled for 5 seconds, and rescheduled 6 seconds later, - * the timer will already have fired and will not be rescheduled. - */ -shaka.util.Timer.prototype.rescheduleIfRunning = function() { - if (this.id_ != null) { - this.schedule(this.timeoutSeconds_); - } -}; diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index b33dbe98c..15c0ef914 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -22,6 +22,7 @@ describe('MediaSourceEngine', function() { var videoSourceBuffer; var mockVideo; var mockMediaSource; + var mockTextEngine; var mediaSourceEngine; var Util; @@ -70,6 +71,10 @@ describe('MediaSourceEngine', function() { new shaka.media.MediaSourceEngine(video, mockMediaSource, null); }); + afterEach(function() { + mockTextEngine = null; + }); + describe('init', function() { it('creates SourceBuffers for the given types', function() { mediaSourceEngine.init({'audio': 'audio/foo', 'video': 'video/foo'}); @@ -87,7 +92,7 @@ describe('MediaSourceEngine', function() { describe('bufferStart and bufferEnd', function() { beforeEach(function() { - mediaSourceEngine.init({'audio': 'audio/foo'}); + mediaSourceEngine.init({'audio': 'audio/foo', 'text': 'text/foo'}); }); it('returns correct timestamps for one range', function() { @@ -124,11 +129,24 @@ describe('MediaSourceEngine', function() { expect(mediaSourceEngine.bufferStart('audio', 0)).toBeNull(); expect(mediaSourceEngine.bufferEnd('audio', 0)).toBeNull(); }); + + it('will forward to TextEngine', function() { + mockTextEngine.bufferStart.and.returnValue(10); + mockTextEngine.bufferEnd.and.returnValue(20); + + expect(mockTextEngine.bufferStart).not.toHaveBeenCalled(); + expect(mediaSourceEngine.bufferStart('text')).toBe(10); + expect(mockTextEngine.bufferStart).toHaveBeenCalled(); + + expect(mockTextEngine.bufferEnd).not.toHaveBeenCalled(); + expect(mediaSourceEngine.bufferEnd('text')).toBe(20); + expect(mockTextEngine.bufferEnd).toHaveBeenCalled(); + }); }); describe('bufferedAheadOf', function() { beforeEach(function() { - mediaSourceEngine.init({'audio': 'audio/foo'}); + mediaSourceEngine.init({'audio': 'audio/foo', 'text': 'text/foo'}); }); it('returns the amount of data ahead of the given position', function() { @@ -188,13 +206,34 @@ describe('MediaSourceEngine', function() { expect(mediaSourceEngine.bufferedAheadOf('audio', 6.98)) .toBeCloseTo(4.02); }); + + it('will forward to TextEngine', function() { + mockTextEngine.bufferedAheadOf.and.returnValue(10); + + expect(mockTextEngine.bufferedAheadOf).not.toHaveBeenCalled(); + expect(mediaSourceEngine.bufferedAheadOf('text', 5)).toBe(10); + expect(mockTextEngine.bufferedAheadOf).toHaveBeenCalledWith(5); + + // This should get called with 25, return null, then MediaSourceEngine + // should retry at |25 + 5|. + mockTextEngine.bufferedAheadOf.calls.reset(); + mockTextEngine.bufferedAheadOf.and.callFake(function(time) { + if (time < 30) + return null; + else + return 15; + }); + expect(mediaSourceEngine.bufferedAheadOf('text', 25, 5)).toBe(20); + expect(mockTextEngine.bufferedAheadOf).toHaveBeenCalled(); + }); }); describe('appendBuffer', function() { beforeEach(function() { captureEvents(audioSourceBuffer, ['updateend', 'error']); captureEvents(videoSourceBuffer, ['updateend', 'error']); - mediaSourceEngine.init({'audio': 'audio/foo', 'video': 'video/foo'}); + mediaSourceEngine.init( + {'audio': 'audio/foo', 'video': 'video/foo', 'text': 'text/foo'}); }); it('appends the given data', function(done) { @@ -338,13 +377,22 @@ describe('MediaSourceEngine', function() { done(); }); }); + + it('forwards to TextEngine', function(done) { + var data = new ArrayBuffer(0); + expect(mockTextEngine.appendBuffer).not.toHaveBeenCalled(); + mediaSourceEngine.appendBuffer('text', data, 0, 10).then(function() { + expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith(data, 0, 10); + }).catch(fail).then(done); + }); }); describe('remove', function() { beforeEach(function() { captureEvents(audioSourceBuffer, ['updateend', 'error']); captureEvents(videoSourceBuffer, ['updateend', 'error']); - mediaSourceEngine.init({'audio': 'audio/foo', 'video': 'video/foo'}); + mediaSourceEngine.init( + {'audio': 'audio/foo', 'video': 'video/foo', 'text': 'text/foo'}); }); it('removes the given data', function(done) { @@ -469,13 +517,21 @@ describe('MediaSourceEngine', function() { done(); }); }); + + it('will forward to TextEngine', function(done) { + expect(mockTextEngine.remove).not.toHaveBeenCalled(); + mediaSourceEngine.remove('text', 10, 20).then(function() { + expect(mockTextEngine.remove).toHaveBeenCalledWith(10, 20); + }).catch(fail).then(done); + }); }); describe('clear', function() { beforeEach(function() { captureEvents(audioSourceBuffer, ['updateend', 'error']); captureEvents(videoSourceBuffer, ['updateend', 'error']); - mediaSourceEngine.init({'audio': 'audio/foo', 'video': 'video/foo'}); + mediaSourceEngine.init( + {'audio': 'audio/foo', 'video': 'video/foo', 'text': 'text/foo'}); }); it('clears the given data', function(done) { @@ -522,11 +578,18 @@ describe('MediaSourceEngine', function() { }); audioSourceBuffer.updateend(); }); + + it('will forward to TextEngine', function(done) { + expect(mockTextEngine.remove).not.toHaveBeenCalled(); + mediaSourceEngine.clear('text').then(function() { + expect(mockTextEngine.remove).toHaveBeenCalledWith(0, Infinity); + }).catch(fail).then(done); + }); }); describe('setTimestampOffset', function() { beforeEach(function() { - mediaSourceEngine.init({'audio': 'audio/foo'}); + mediaSourceEngine.init({'audio': 'audio/foo', 'text': 'text/foo'}); }); it('sets the timestamp offset', function(done) { @@ -536,11 +599,18 @@ describe('MediaSourceEngine', function() { done(); }); }); + + it('will forward to TextEngine', function(done) { + expect(mockTextEngine.setTimestampOffset).not.toHaveBeenCalled(); + mediaSourceEngine.setTimestampOffset('text', 10).then(function() { + expect(mockTextEngine.setTimestampOffset).toHaveBeenCalledWith(10); + }).catch(fail).then(done); + }); }); describe('setAppendWindowEnd', function() { beforeEach(function() { - mediaSourceEngine.init({'audio': 'audio/foo'}); + mediaSourceEngine.init({'audio': 'audio/foo', 'text': 'text/foo'}); }); it('sets the append window end', function(done) { @@ -553,6 +623,13 @@ describe('MediaSourceEngine', function() { done(); }); }); + + it('will forward to TextEngine', function(done) { + expect(mockTextEngine.setAppendWindowEnd).not.toHaveBeenCalled(); + mediaSourceEngine.setAppendWindowEnd('text', 5).then(function() { + expect(mockTextEngine.setAppendWindowEnd).toHaveBeenCalledWith(5); + }).catch(fail).then(done); + }); }); describe('endOfStream', function() { @@ -867,6 +944,15 @@ describe('MediaSourceEngine', function() { done(); }); }); + + it('destroys text engines', function(done) { + mediaSourceEngine.reinitText('text/vtt'); + + mediaSourceEngine.destroy().then(function() { + expect(mockTextEngine).toBeTruthy(); + expect(mockTextEngine.destroy).toHaveBeenCalled(); + }).catch(fail).then(done); + }); }); function createMockMediaSource() { @@ -905,9 +991,19 @@ describe('MediaSourceEngine', function() { function createMockTextEngineCtor() { var ctor = jasmine.createSpy('TextEngine'); ctor.isTypeSupported = function() { return true; }; - ctor.prototype.initParser = function() {}; - ctor.prototype.addEventListener = function() {}; - ctor.prototype.removeEventListener = function() {}; + ctor.and.callFake(function() { + expect(mockTextEngine).toBeFalsy(); + mockTextEngine = jasmine.createSpyObj('TextEngine', [ + 'initParser', 'destroy', 'appendBuffer', 'remove', 'setTimestampOffset', + 'setAppendWindowEnd', 'bufferStart', 'bufferEnd', 'bufferedAheadOf' + ]); + + var resolve = Promise.resolve.bind(Promise); + mockTextEngine.destroy.and.callFake(resolve); + mockTextEngine.appendBuffer.and.callFake(resolve); + mockTextEngine.remove.and.callFake(resolve); + return mockTextEngine; + }); return ctor; } diff --git a/test/offline/db_engine_unit.js b/test/offline/db_engine_unit.js index 8fee3d8c8..e20c91efc 100644 --- a/test/offline/db_engine_unit.js +++ b/test/offline/db_engine_unit.js @@ -104,6 +104,27 @@ describe('DBEngine', function() { }).catch(fail).then(done); }); + it('supports iterating over each element', function(done) { + var testData = [ + {key: 1, i: 4}, + {key: 2, i: 1}, + {key: 3, i: 2}, + {key: 4, i: 9} + ]; + var spy = jasmine.createSpy('forEach'); + Promise.all(testData.map(db.insert.bind(db, 'test'))) + .then(function() { + return db.forEach('test', spy); + }) + .then(function() { + expect(spy).toHaveBeenCalledTimes(testData.length); + for (var i = 0; i < testData.length; i++) + expect(spy).toHaveBeenCalledWith(testData[i]); + }) + .catch(fail) + .then(done); + }); + it('aborts transactions on destroy()', function(done) { var expectedError = new shaka.util.Error( shaka.util.Error.Category.STORAGE, diff --git a/test/offline/offline_manifest_parser_unit.js b/test/offline/offline_manifest_parser_unit.js new file mode 100644 index 000000000..405aa0de0 --- /dev/null +++ b/test/offline/offline_manifest_parser_unit.js @@ -0,0 +1,247 @@ +/** + * @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('OfflineManifestParser', function() { + var originalDbEngineCtor; + var fakeDbEngineCtor; + var parser; + var dbEngine; + + beforeAll(function() { + originalDbEngineCtor = shaka.offline.DBEngine; + }); + + afterAll(function() { + shaka.offline.DBEngine = originalDbEngineCtor; + }); + + beforeEach(function() { + dbEngine = createFakeDbEngine(); + fakeDbEngineCtor = jasmine.createSpy('DBEngine'); + fakeDbEngineCtor.and.returnValue(dbEngine); + shaka.offline.DBEngine = fakeDbEngineCtor; + + parser = new shaka.offline.OfflineManifestParser(); + }); + + afterEach(function() { + parser.stop(); + }); + + it('will query DBEngine for the manifest', function(done) { + var uri = 'offline:123'; + dbEngine.get.and.returnValue(Promise.resolve({ + key: 0, + originalManifestUri: '', + duration: 60, + size: 100, + periods: [], + sessionIds: [], + drmInfo: null, + appMetadata: null + })); + + parser.start(uri, null, null, null) + .then(function(manifest) { + expect(manifest).toBeTruthy(); + + expect(fakeDbEngineCtor).toHaveBeenCalledTimes(1); + expect(dbEngine.init).toHaveBeenCalledTimes(1); + expect(dbEngine.destroy).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledWith('manifest', 123); + }) + .catch(fail) + .then(done); + }); + + it('will fail if manifest not found', function(done) { + var uri = 'offline:123'; + dbEngine.get.and.returnValue(Promise.resolve(null)); + + parser.start(uri, null, null, null) + .then(fail) + .catch(function(err) { + shaka.test.Util.expectToEqualError( + err, + new shaka.util.Error( + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, 123)); + + expect(fakeDbEngineCtor).toHaveBeenCalledTimes(1); + expect(dbEngine.init).toHaveBeenCalledTimes(1); + expect(dbEngine.destroy).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledWith('manifest', 123); + }) + .then(done); + }); + + it('still calls destroy on error', function(done) { + var uri = 'offline:123'; + dbEngine.get.and.returnValue(Promise.reject()); + + parser.start(uri, null, null, null) + .then(fail) + .catch(function(err) { + expect(fakeDbEngineCtor).toHaveBeenCalledTimes(1); + expect(dbEngine.init).toHaveBeenCalledTimes(1); + expect(dbEngine.destroy).toHaveBeenCalledTimes(1); + }) + .then(done); + }); + + it('will fail for invalid URI', function(done) { + var uri = 'offline:abc'; + parser.start(uri, null, null, null) + .then(fail) + .catch(function(err) { + shaka.test.Util.expectToEqualError( + err, + new shaka.util.Error( + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri)); + }) + .then(done); + }); + + describe('reconstructing manifest', function() { + var originalReconstructPeriod; + + beforeAll(function() { + originalReconstructPeriod = shaka.offline.OfflineUtils.reconstructPeriod; + }); + + afterAll(function() { + shaka.offline.OfflineUtils.reconstructPeriod = originalReconstructPeriod; + }); + + it('converts non-Period members correctly', function(done) { + var uri = 'offline:123'; + var data = { + key: 123, + originalManifestUri: 'https://example.com/manifest', + duration: 60, + size: 100, + periods: [], + sessionIds: ['abc', '123'], + drmInfo: null, + appMetadata: null + }; + dbEngine.get.and.returnValue(Promise.resolve(data)); + + parser.start(uri, null, null, null) + .then(function(manifest) { + expect(manifest).toBeTruthy(); + expect(manifest.minBufferTime).toEqual(jasmine.any(Number)); + expect(manifest.offlineSessionIds).toEqual(data.sessionIds); + expect(manifest.periods).toEqual([]); + + var timeline = manifest.presentationTimeline; + expect(timeline).toBeTruthy(); + expect(timeline.isLive()).toBe(false); + expect(timeline.getPresentationStartTime()).toBe(null); + expect(timeline.getDuration()).toBe(data.duration); + }) + .catch(fail) + .then(done); + }); + + it('will accept DrmInfo', function(done) { + var uri = 'offline:123'; + var drmInfo = { + keySystem: 'com.example.drm', + licenseServerUri: 'https://example.com/drm', + distinctiveIdentifierRequired: false, + persistentStateRequired: true, + audioRobustness: 'weak', + videoRobustness: 'awesome', + serverCertificate: null, + initData: [{initData: new Uint8Array([1]), initDataType: 'foo'}], + keyIds: ['key1', 'key2'] + }; + var period = {}; + var data = { + key: 123, + originalManifestUri: 'https://example.com/manifest', + duration: 60, + size: 100, + periods: [period], + sessionIds: ['abc', '123'], + drmInfo: drmInfo, + appMetadata: null + }; + dbEngine.get.and.returnValue(Promise.resolve(data)); + + var spy = jasmine.createSpy('reconstructPeriod'); + shaka.offline.OfflineUtils.reconstructPeriod = spy; + + parser.start(uri, null, null, null) + .then(function(manifest) { + expect(manifest).toBeTruthy(); + + expect(spy).toHaveBeenCalled(); + expect(spy.calls.argsFor(0)[1]).toEqual([drmInfo]); + }) + .catch(fail) + .then(done); + }); + + it('will call reconstructPeriod for each Period', function(done) { + var uri = 'offline:123'; + var data = { + key: 123, + originalManifestUri: 'https://example.com/manifest', + duration: 60, + size: 100, + periods: [{id: 1}, {id: 2}, {id: 3}], + sessionIds: ['abc', '123'], + drmInfo: null, + appMetadata: null + }; + dbEngine.get.and.returnValue(Promise.resolve(data)); + + var spy = jasmine.createSpy('reconstructPeriod'); + shaka.offline.OfflineUtils.reconstructPeriod = spy; + + parser.start(uri, null, null, null) + .then(function(manifest) { + expect(manifest).toBeTruthy(); + + expect(spy).toHaveBeenCalledTimes(3); + for (var i = 0; i < data.periods.length; i++) { + expect(spy.calls.argsFor(i)[0]).toBe(data.periods[i]); + expect(spy.calls.argsFor(i)[1]).toEqual([]); // drmInfos + } + }) + .catch(fail) + .then(done); + }); + }); + + function createFakeDbEngine() { + var resolve = Promise.resolve.bind(Promise); + + var fake = jasmine.createSpyObj('DBEngine', ['init', 'destroy', 'get']); + fake.init.and.callFake(resolve); + fake.destroy.and.callFake(resolve); + fake.get.and.callFake(function() { + return Promise.resolve({data: new ArrayBuffer(0)}); + }); + return fake; + } +}); diff --git a/test/offline/offline_scheme_unit.js b/test/offline/offline_scheme_unit.js new file mode 100644 index 000000000..81fc231a2 --- /dev/null +++ b/test/offline/offline_scheme_unit.js @@ -0,0 +1,127 @@ +/** + * @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('OfflineScheme', function() { + var OfflineScheme; + var originalDbEngineCtor; + var fakeDbEngineCtor; + var dbEngine; + var request; + + beforeAll(function() { + OfflineScheme = shaka.offline.OfflineScheme; + originalDbEngineCtor = shaka.offline.DBEngine; + }); + + afterAll(function() { + shaka.offline.DBEngine = originalDbEngineCtor; + }); + + beforeEach(function() { + dbEngine = createFakeDbEngine(); + fakeDbEngineCtor = jasmine.createSpy('DBEngine'); + fakeDbEngineCtor.and.returnValue(dbEngine); + shaka.offline.DBEngine = fakeDbEngineCtor; + + // The whole request is ignored by the OfflineScheme. + var retry = shaka.net.NetworkingEngine.defaultRetryParameters(); + request = shaka.net.NetworkingEngine.makeRequest([], retry); + }); + + it('will return special content-type header for manifests', function(done) { + var uri = 'offline:123'; + fakeDbEngineCtor.and.throwError(); + OfflineScheme(uri, request) + .then(function(response) { + expect(response).toBeTruthy(); + expect(response.uri).toBe(uri); + expect(response.headers['content-type']) + .toBe('application/x-offline-manifest'); + }) + .catch(fail) + .then(done); + }); + + it('will query DBEngine for segments', function(done) { + var uri = 'offline:123/456/789'; + + OfflineScheme(uri, request) + .then(function(response) { + expect(response).toBeTruthy(); + expect(response.uri).toBe(uri); + expect(response.data).toBeTruthy(); + + expect(fakeDbEngineCtor).toHaveBeenCalledTimes(1); + expect(dbEngine.init).toHaveBeenCalledTimes(1); + expect(dbEngine.destroy).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledWith('segment', 789); + }) + .catch(fail) + .then(done); + }); + + it('will fail if segment not found', function(done) { + var uri = 'offline:123/456/789'; + dbEngine.get.and.returnValue(Promise.resolve(null)); + + OfflineScheme(uri, request) + .then(fail) + .catch(function(err) { + shaka.test.Util.expectToEqualError( + err, + new shaka.util.Error( + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, 789)); + + expect(fakeDbEngineCtor).toHaveBeenCalledTimes(1); + expect(dbEngine.init).toHaveBeenCalledTimes(1); + expect(dbEngine.destroy).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledTimes(1); + expect(dbEngine.get).toHaveBeenCalledWith('segment', 789); + }) + .catch(fail) + .then(done); + }); + + it('will fail for invalid URI', function(done) { + var uri = 'offline:abc'; + fakeDbEngineCtor.and.throwError(); + OfflineScheme(uri, request) + .then(fail) + .catch(function(err) { + shaka.test.Util.expectToEqualError( + err, + new shaka.util.Error( + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri)); + }) + .then(done); + }); + + function createFakeDbEngine() { + var resolve = Promise.resolve.bind(Promise); + + var fake = jasmine.createSpyObj('DBEngine', ['init', 'destroy', 'get']); + fake.init.and.callFake(resolve); + fake.destroy.and.callFake(resolve); + fake.get.and.callFake(function() { + return Promise.resolve({data: new ArrayBuffer(0)}); + }); + return fake; + } +}); diff --git a/test/test/externs/jasmine.js b/test/test/externs/jasmine.js index 8a3663c76..456554aa3 100644 --- a/test/test/externs/jasmine.js +++ b/test/test/externs/jasmine.js @@ -130,6 +130,10 @@ jasmine.Matchers.prototype.toHaveBeenCalled = function(opt_value) {}; jasmine.Matchers.prototype.toHaveBeenCalledWith = function(var_args) {}; +/** @param {number} times */ +jasmine.Matchers.prototype.toHaveBeenCalledTimes = function(times) {}; + + /** @param {string|RegExp} value */ jasmine.Matchers.prototype.toMatch = function(value) {}; diff --git a/test/util/array_utils_unit.js b/test/util/array_utils_unit.js new file mode 100644 index 000000000..60f57b430 --- /dev/null +++ b/test/util/array_utils_unit.js @@ -0,0 +1,57 @@ +/** + * @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('ArrayUtils', function() { + var ArrayUtils; + + beforeAll(function() { + ArrayUtils = shaka.util.ArrayUtils; + }); + + describe('removeDuplicates', function() { + it('will remove duplicate elements', function() { + var arr = [1, 2, 2, 5, 6, 3, 1, 2]; + expect(ArrayUtils.removeDuplicates(arr)).toEqual([1, 2, 5, 6, 3]); + }); + + it('does nothing if no duplicates', function() { + var arr = [1, 2, 3, 6, 5, 4]; + expect(ArrayUtils.removeDuplicates(arr)).toEqual(arr); + }); + + it('accepts an optional comparator', function() { + var arr = ['aaa', 'abc', 'bat', 'car', 'cat']; + var comparator = function(a, b) { return a[0] === b[0]; }; + expect(ArrayUtils.removeDuplicates(arr, comparator)) + .toEqual(['aaa', 'bat', 'car']); + }); + }); + + describe('indexOf', function() { + it('will find a matching element', function() { + var arr = ['aaa', 'bbb', 'ccc']; + var comparator = function(a, b) { return a[0] === b[0]; }; + expect(ArrayUtils.indexOf(arr, 'bat', comparator)).toBe(1); + }); + + it('will return -1 if not found', function() { + var arr = ['aaa', 'bbb', 'ccc']; + var comparator = function(a, b) { return a[0] === b[0]; }; + expect(ArrayUtils.indexOf(arr, 'zoo', comparator)).toBe(-1); + }); + }); +}); diff --git a/test/util/cancelable_chain_unit.js b/test/util/cancelable_chain_unit.js index b58da04b5..a8719baf8 100644 --- a/test/util/cancelable_chain_unit.js +++ b/test/util/cancelable_chain_unit.js @@ -79,6 +79,11 @@ describe('CancelableChain', function() { p.catch(fail).then(done); }); + it('returns the same promise after being finalized', function() { + var p = chain.then(function() { return 1; }).finalize(); + expect(chain.finalize()).toBe(p); + }); + describe('cancel', function() { var cannedError; diff --git a/test/util/data_view_reader_unit.js b/test/util/data_view_reader_unit.js index ccb675671..5da2c2082 100644 --- a/test/util/data_view_reader_unit.js +++ b/test/util/data_view_reader_unit.js @@ -168,69 +168,48 @@ describe('DataViewReader', function() { expect(bigEndianReader.getPosition()).toBe(8); }); - it('detects end-of-stream when reading a uint8', function() { - bigEndianReader.skip(7); - bigEndianReader.readUint8(); - - var exception = null; - - try { + describe('end-of-stream', function() { + it('detects when reading a uint8', function() { + bigEndianReader.skip(7); bigEndianReader.readUint8(); - } catch (e) { - exception = e; + runTest(function() { bigEndianReader.readUint8(); }); + }); + + it('detects when reading a uint16', function() { + bigEndianReader.skip(7); + runTest(function() { bigEndianReader.readUint16(); }); + }); + + it('detects when reading a uint32', function() { + bigEndianReader.skip(5); + runTest(function() { bigEndianReader.readUint32(); }); + }); + + it('detects when readinga uint64', function() { + bigEndianReader.skip(3); + runTest(function() { bigEndianReader.readUint64(); }); + }); + + it('detects when skipping bytes', function() { + bigEndianReader.skip(8); + runTest(function() { bigEndianReader.skip(1); }); + }); + + it('detects when reading bytes', function() { + bigEndianReader.skip(8); + runTest(function() { bigEndianReader.readBytes(1); }); + }); + + function runTest(test) { + try { + test(); + fail('Should throw exception'); + } catch (e) { + expect(e).not.toBeNull(); + expect(e instanceof shaka.util.Error).toBe(true); + expect(e.code).toBe(Code.BUFFER_READ_OUT_OF_BOUNDS); + } } - - expect(exception).not.toBeNull(); - expect(exception instanceof shaka.util.Error).toBe(true); - expect(exception.code).toBe(Code.BUFFER_READ_OUT_OF_BOUNDS); - }); - - it('detects end-of-stream when reading a uint16', function() { - bigEndianReader.skip(7); - - var exception = null; - - try { - bigEndianReader.readUint16(); - } catch (e) { - exception = e; - } - - expect(exception).not.toBeNull(); - expect(exception instanceof shaka.util.Error).toBe(true); - expect(exception.code).toBe(Code.BUFFER_READ_OUT_OF_BOUNDS); - }); - - it('detects end-of-stream when reading a uint32', function() { - bigEndianReader.skip(5); - - var exception = null; - - try { - bigEndianReader.readUint32(); - } catch (e) { - exception = e; - } - - expect(exception).not.toBeNull(); - expect(exception instanceof shaka.util.Error).toBe(true); - expect(exception.code).toBe(Code.BUFFER_READ_OUT_OF_BOUNDS); - }); - - it('detects end-of-stream when skipping bytes', function() { - bigEndianReader.skip(8); - - var exception = null; - - try { - bigEndianReader.skip(1); - } catch (e) { - exception = e; - } - - expect(exception).not.toBeNull(); - expect(exception instanceof shaka.util.Error).toBe(true); - expect(exception.code).toBe(Code.BUFFER_READ_OUT_OF_BOUNDS); }); it('detects uint64s too large for JavaScript', function() { diff --git a/test/util/event_manager_unit.js b/test/util/event_manager_unit.js index 6c770e161..8c07b032f 100644 --- a/test/util/event_manager_unit.js +++ b/test/util/event_manager_unit.js @@ -113,6 +113,19 @@ describe('EventManager', function() { expect(listener).not.toHaveBeenCalled(); }); + it('ignores other targets when removing listeners', function() { + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target1, 'eventtype1', listener1); + eventManager.listen(target2, 'eventtype1', listener2); + eventManager.unlisten(target2, 'eventtype1'); + + target1.dispatchEvent(event1); + + expect(listener1).toHaveBeenCalled(); + }); + it('stops listening to multiple events', function() { var listener1 = jasmine.createSpy('listener1'); var listener2 = jasmine.createSpy('listener2');