summaryrefslogtreecommitdiff
path: root/javascript/videojs/test/unit/tech/tech.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/test/unit/tech/tech.test.js')
-rw-r--r--javascript/videojs/test/unit/tech/tech.test.js776
1 files changed, 776 insertions, 0 deletions
diff --git a/javascript/videojs/test/unit/tech/tech.test.js b/javascript/videojs/test/unit/tech/tech.test.js
new file mode 100644
index 0000000..a64ca57
--- /dev/null
+++ b/javascript/videojs/test/unit/tech/tech.test.js
@@ -0,0 +1,776 @@
+/* eslint-env qunit */
+import Tech from '../../../src/js/tech/tech.js';
+import Html5 from '../../../src/js/tech/html5.js';
+import Button from '../../../src/js/button.js';
+import { createTimeRange } from '../../../src/js/utils/time.js';
+import MediaError from '../../../src/js/media-error.js';
+import AudioTrack from '../../../src/js/tracks/audio-track';
+import VideoTrack from '../../../src/js/tracks/video-track';
+import TextTrack from '../../../src/js/tracks/text-track';
+import AudioTrackList from '../../../src/js/tracks/audio-track-list';
+import VideoTrackList from '../../../src/js/tracks/video-track-list';
+import TextTrackList from '../../../src/js/tracks/text-track-list';
+import sinon from 'sinon';
+import log from '../../../src/js/utils/log.js';
+import TestHelpers from '../test-helpers.js';
+
+function stubbedSourceHandler(handler) {
+ return {
+ canPlayType() {
+ return true;
+ },
+ canHandleSource() {
+ return true;
+ },
+ handleSource(source, tech, options) {
+ return handler;
+ }
+ };
+}
+
+QUnit.module('Media Tech', {
+ beforeEach(assert) {
+ this.noop = function() {};
+ this.clock = sinon.useFakeTimers();
+ this.featuresProgessEvents = Tech.prototype.featuresProgessEvents;
+ Tech.prototype.featuresProgressEvents = false;
+ },
+ afterEach(assert) {
+ this.clock.restore();
+ Tech.prototype.featuresProgressEvents = this.featuresProgessEvents;
+ }
+});
+
+QUnit.test('Tech.registerTech and Tech.getTech', function(assert) {
+ class MyTech extends Tech {}
+ const oldTechs = Tech.techs_;
+ const oldDefaultTechOrder = Tech.defaultTechOrder_;
+
+ Tech.registerTech('MyTech', MyTech);
+
+ assert.ok(Tech.techs_.MyTech, 'Tech is stored in the global list');
+ assert.notEqual(Tech.defaultTechOrder_.indexOf('MyTech'), -1, 'Tech is stored in the defaultTechOrder array');
+ assert.strictEqual(Tech.getTech('myTech'), MyTech, 'can get a tech using `camelCase` name');
+ assert.strictEqual(Tech.getTech('MyTech'), MyTech, 'can get a tech using `titleCase` name');
+
+ // reset techs and defaultTechOrder
+ Tech.techs_ = oldTechs;
+ Tech.defaultTechOrder_ = oldDefaultTechOrder;
+});
+
+QUnit.test('should synthesize timeupdate events by default', function(assert) {
+ let timeupdates = 0;
+ const tech = new Tech();
+
+ tech.on('timeupdate', function() {
+ timeupdates++;
+ });
+
+ tech.trigger('play');
+
+ this.clock.tick(250);
+ assert.equal(timeupdates, 1, 'triggered at least one timeupdate');
+
+ tech.dispose();
+});
+
+QUnit.test('stops manual timeupdates while paused', function(assert) {
+ let timeupdates = 0;
+ const tech = new Tech();
+
+ tech.on('timeupdate', function() {
+ timeupdates++;
+ });
+
+ tech.trigger('play');
+ this.clock.tick(10 * 250);
+ assert.ok(timeupdates > 0, 'timeupdates fire during playback');
+
+ tech.trigger('pause');
+ timeupdates = 0;
+ this.clock.tick(10 * 250);
+ assert.equal(timeupdates, 0, 'timeupdates do not fire when paused');
+
+ tech.trigger('play');
+ this.clock.tick(10 * 250);
+ assert.ok(timeupdates > 0, 'timeupdates fire when playback resumes');
+ tech.dispose();
+});
+
+QUnit.test('should synthesize progress events by default', function(assert) {
+ let progresses = 0;
+ let bufferedPercent = 0.5;
+ const tech = new Tech();
+
+ tech.on('progress', function() {
+ progresses++;
+ });
+ tech.bufferedPercent = function() {
+ return bufferedPercent;
+ };
+
+ this.clock.tick(500);
+ assert.equal(progresses, 0, 'waits until ready');
+
+ tech.trigger('ready');
+ this.clock.tick(500);
+ assert.equal(progresses, 1, 'triggered one event');
+
+ tech.trigger('ready');
+ bufferedPercent = 0.75;
+ this.clock.tick(500);
+ assert.equal(progresses, 2, 'repeated readies are ok');
+ tech.dispose();
+});
+
+QUnit.test('dispose() should stop time tracking', function(assert) {
+ const tech = new Tech();
+
+ tech.dispose();
+
+ // progress and timeupdate events will throw exceptions after the
+ // tech is disposed
+ try {
+ this.clock.tick(10 * 1000);
+ } catch (e) {
+ return assert.equal(e, undefined, 'threw an exception');
+ }
+ assert.ok(true, 'no exception was thrown');
+});
+
+QUnit.test('dispose() should clear all tracks that are passed when its created', function(assert) {
+ const audioTracks = new AudioTrackList([new AudioTrack(), new AudioTrack()]);
+ const videoTracks = new VideoTrackList([new VideoTrack(), new VideoTrack()]);
+ const pretech = new Tech();
+ const textTracks = new TextTrackList([new TextTrack({tech: pretech}),
+ new TextTrack({tech: pretech})]);
+
+ assert.equal(audioTracks.length, 2, 'should have two audio tracks at the start');
+ assert.equal(videoTracks.length, 2, 'should have two video tracks at the start');
+ assert.equal(textTracks.length, 2, 'should have two text tracks at the start');
+
+ const tech = new Tech({audioTracks, videoTracks, textTracks});
+
+ assert.equal(
+ tech.videoTracks().length,
+ videoTracks.length,
+ 'should hold video tracks that we passed'
+ );
+ assert.equal(
+ tech.audioTracks().length,
+ audioTracks.length,
+ 'should hold audio tracks that we passed'
+ );
+ assert.equal(
+ tech.textTracks().length,
+ textTracks.length,
+ 'should hold text tracks that we passed'
+ );
+
+ pretech.dispose();
+ tech.dispose();
+
+ assert.equal(audioTracks.length, 0, 'should have zero audio tracks after dispose');
+ assert.equal(videoTracks.length, 0, 'should have zero video tracks after dispose');
+ assert.equal(textTracks.length, 0, 'should have zero text tracks after dispose');
+});
+
+QUnit.test('dispose() should clear all tracks that are added after creation', function(assert) {
+ const tech = new Tech();
+
+ tech.addRemoteTextTrack({}, true);
+ tech.addRemoteTextTrack({}, true);
+
+ tech.audioTracks().addTrack(new AudioTrack());
+ tech.audioTracks().addTrack(new AudioTrack());
+
+ tech.videoTracks().addTrack(new VideoTrack());
+ tech.videoTracks().addTrack(new VideoTrack());
+
+ assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
+ assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
+ assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start');
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 2,
+ 'should have two remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
+
+ tech.dispose();
+
+ assert.equal(
+ tech.audioTracks().length,
+ 0,
+ 'should have zero audio tracks after dispose'
+ );
+ assert.equal(
+ tech.videoTracks().length,
+ 0,
+ 'should have zero video tracks after dispose'
+ );
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 0,
+ 'should have zero remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 0, 'should have zero remote text tracks');
+ assert.equal(tech.textTracks().length, 0, 'should have zero video tracks after dispose');
+});
+
+QUnit.test('switching sources should clear all remote tracks that are added with the default manualCleanup = false', function(assert) {
+ const oldLogWarn = log.warn;
+
+ // Define a new tech class
+ class MyTech extends Tech {}
+
+ // Create source handler
+ const handler = {
+ canPlayType: () => 'probably',
+ canHandleSource: () => 'probably',
+ handleSource: () => {
+ return {
+ dispose: () => {}
+ };
+ }
+ };
+
+ // Extend Tech with source handlers
+ Tech.withSourceHandlers(MyTech);
+
+ MyTech.registerSourceHandler(handler);
+
+ const tech = new MyTech();
+
+ tech.triggerReady();
+
+ // set the initial source
+ tech.setSource({src: 'foo.mp4', type: 'mp4'});
+
+ // should not be automatically cleaned up when source changes
+ tech.addRemoteTextTrack({}, true);
+ // should be automatically cleaned up when source changes
+ tech.addRemoteTextTrack({});
+ this.clock.tick(1);
+
+ assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start');
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 2,
+ 'should have two remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
+ assert.equal(
+ tech.autoRemoteTextTracks_.length,
+ 1,
+ 'should have one auto-cleanup remote text track'
+ );
+
+ // change source to force cleanup of auto remote text tracks
+ tech.setSource({src: 'bar.mp4', type: 'mp4'});
+ this.clock.tick(1);
+
+ assert.equal(
+ tech.textTracks().length,
+ 1,
+ 'should have one text track after source change'
+ );
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 1,
+ 'should have one remote remote text track els after source change'
+ );
+ assert.equal(
+ tech.remoteTextTracks().length,
+ 1,
+ 'should have one remote text track after source change'
+ );
+ assert.equal(
+ tech.autoRemoteTextTracks_.length,
+ 0,
+ 'should have zero auto-cleanup remote text tracks'
+ );
+
+ log.warn = oldLogWarn;
+ tech.dispose();
+});
+
+QUnit.test('should add the source handler interface to a tech', function(assert) {
+ const sourceA = { src: 'foo.mp4', type: 'video/mp4' };
+ const sourceB = { src: 'no-support', type: 'no-support' };
+
+ // Define a new tech class
+ class MyTech extends Tech {}
+
+ // Extend Tech with source handlers
+ Tech.withSourceHandlers(MyTech);
+
+ // Check for the expected class methods
+ assert.ok(
+ MyTech.registerSourceHandler,
+ 'added a registerSourceHandler function to the Tech'
+ );
+ assert.ok(
+ MyTech.selectSourceHandler,
+ 'added a selectSourceHandler function to the Tech'
+ );
+
+ // Create an instance of Tech
+ const tech = new MyTech();
+
+ // Check for the expected instance methods
+ assert.ok(tech.setSource, 'added a setSource function to the tech instance');
+
+ // Create an internal state class for the source handler
+ // The internal class would be used by a source handler to maintain state
+ // and provde a dispose method for the handler.
+ // This is optional for source handlers
+ let disposeCalled = false;
+
+ class HandlerInternalState {
+ dispose() {
+ disposeCalled = true;
+ }
+ }
+
+ // Create source handlers
+ const handlerOne = {
+ canPlayType(type) {
+ if (type !== 'no-support') {
+ return 'probably';
+ }
+ return '';
+ },
+ canHandleSource(source, options) {
+ assert.strictEqual(
+ tech.options_,
+ options,
+ 'tech options passed to canHandleSource'
+ );
+ if (source.type !== 'no-support') {
+ return 'probably';
+ }
+ return '';
+ },
+ handleSource(s, t, o) {
+ assert.strictEqual(
+ tech,
+ t,
+ 'tech instance passed to source handler'
+ );
+ assert.strictEqual(
+ sourceA,
+ s,
+ 'tech instance passed to the source handler'
+ );
+ assert.strictEqual(
+ tech.options_,
+ o,
+ 'tech options passed to the source handler handleSource'
+ );
+ return new HandlerInternalState();
+ }
+ };
+
+ const handlerTwo = {
+ canPlayType(type) {
+ // no support
+ return '';
+ },
+ canHandleSource(source, options) {
+ // no support
+ return '';
+ },
+ handleSource(source, tech_, options) {
+ assert.ok(false, 'handlerTwo supports nothing and should never be called');
+ }
+ };
+
+ // Test registering source handlers
+ MyTech.registerSourceHandler(handlerOne);
+ assert.strictEqual(
+ MyTech.sourceHandlers[0],
+ handlerOne,
+ 'handlerOne was added to the source handler array'
+ );
+ MyTech.registerSourceHandler(handlerTwo, 0);
+ assert.strictEqual(
+ MyTech.sourceHandlers[0],
+ handlerTwo,
+ 'handlerTwo was registered at the correct index (0)'
+ );
+
+ // Test handler selection
+ assert.strictEqual(
+ MyTech.selectSourceHandler(sourceA, tech.options_),
+ handlerOne,
+ 'handlerOne was selected to handle the valid source'
+ );
+ assert.strictEqual(
+ MyTech.selectSourceHandler(sourceB, tech.options_),
+ null,
+ 'no handler was selected to handle the invalid source'
+ );
+
+ // Test canPlayType return values
+ assert.strictEqual(
+ MyTech.canPlayType(sourceA.type),
+ 'probably',
+ 'the Tech returned probably for the valid source'
+ );
+ assert.strictEqual(
+ MyTech.canPlayType(sourceB.type),
+ '',
+ 'the Tech returned an empty string for the invalid source'
+ );
+
+ // Test canPlaySource return values
+ assert.strictEqual(
+ MyTech.canPlaySource(sourceA, tech.options_),
+ 'probably',
+ 'the Tech returned probably for the valid source'
+ );
+ assert.strictEqual(
+ MyTech.canPlaySource(sourceB, tech.options_),
+ '',
+ 'the Tech returned an empty string for the invalid source'
+ );
+
+ tech.addRemoteTextTrack({}, true);
+ tech.addRemoteTextTrack({}, true);
+
+ tech.audioTracks().addTrack(new AudioTrack());
+ tech.audioTracks().addTrack(new AudioTrack());
+
+ tech.videoTracks().addTrack(new VideoTrack());
+ tech.videoTracks().addTrack(new VideoTrack());
+
+ assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
+ assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
+ assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 2,
+ 'should have two remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
+
+ // Pass a source through the source handler process of a tech instance
+ tech.setSource(sourceA);
+
+ // verify that the Tracks are still there
+ assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
+ assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
+ assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 2,
+ 'should have two remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
+
+ assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
+ assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');
+
+ // Pass a second source
+ tech.setSource(sourceA);
+ assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
+ assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');
+
+ // verify that all the tracks were removed as we got a new source
+ assert.equal(tech.audioTracks().length, 0, 'should have zero audio tracks');
+ assert.equal(tech.videoTracks().length, 0, 'should have zero video tracks');
+ assert.equal(tech.textTracks().length, 2, 'should have two text tracks');
+ assert.equal(
+ tech.remoteTextTrackEls().length,
+ 2,
+ 'should have two remote text tracks els'
+ );
+ assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
+
+ // Check that the handler dispose method works
+ assert.ok(disposeCalled, 'dispose has been called for the handler yet');
+ disposeCalled = false;
+ tech.dispose();
+ assert.ok(
+ disposeCalled,
+ 'the handler dispose method was called when the tech was disposed'
+ );
+});
+
+QUnit.test('should handle unsupported sources with the source handler API', function(assert) {
+ // Define a new tech class
+ class MyTech extends Tech {}
+
+ // Extend Tech with source handlers
+ Tech.withSourceHandlers(MyTech);
+ // Create an instance of Tech
+ const tech = new MyTech();
+ let usedNative;
+
+ MyTech.nativeSourceHandler = {
+ handleSource() {
+ usedNative = true;
+ }
+ };
+
+ tech.setSource('');
+ assert.ok(
+ usedNative,
+ 'native source handler was used when an unsupported source was set'
+ );
+ tech.dispose();
+});
+
+QUnit.test('should allow custom error events to be set', function(assert) {
+ const tech = new Tech();
+ const errors = [];
+
+ tech.on('error', function() {
+ errors.push(tech.error());
+ });
+
+ assert.equal(tech.error(), null, 'error is null by default');
+
+ tech.error(new MediaError(1));
+ assert.equal(errors.length, 1, 'triggered an error event');
+ assert.equal(errors[0].code, 1, 'set the proper code');
+
+ tech.error(2);
+ assert.equal(errors.length, 2, 'triggered an error event');
+ assert.equal(errors[1].code, 2, 'wrapped the error code');
+ tech.dispose();
+});
+
+QUnit.test('should track whether a video has played', function(assert) {
+ const tech = new Tech();
+
+ assert.equal(tech.played().length, 0, 'starts with zero length');
+ tech.trigger('playing');
+ assert.equal(tech.played().length, 1, 'has length after playing');
+ tech.dispose();
+});
+
+QUnit.test('delegates deferrables to the source handler', function(assert) {
+ class MyTech extends Tech {
+ seekable() {
+ throw new Error('You should not be calling me!');
+ }
+ seeking() {
+ throw new Error('You should not be calling me!');
+ }
+ duration() {
+ throw new Error('You should not be calling me!');
+ }
+ }
+
+ Tech.withSourceHandlers(MyTech);
+
+ let seekableCount = 0;
+ let seekingCount = 0;
+ let durationCount = 0;
+ const handler = {
+ seekable() {
+ seekableCount++;
+ return createTimeRange(0, 0);
+ },
+ seeking() {
+ seekingCount++;
+ return false;
+ },
+ duration() {
+ durationCount++;
+ return 0;
+ }
+ };
+
+ MyTech.registerSourceHandler(stubbedSourceHandler(handler));
+
+ const tech = new MyTech();
+
+ tech.setSource({
+ src: 'example.mp4',
+ type: 'video/mp4'
+ });
+ tech.seekable();
+ tech.seeking();
+ tech.duration();
+ assert.equal(seekableCount, 1, 'called the source handler');
+ assert.equal(seekingCount, 1, 'called the source handler');
+ assert.equal(durationCount, 1, 'called the source handler');
+ tech.dispose();
+});
+
+QUnit.test('delegates only deferred deferrables to the source handler', function(assert) {
+ let seekingCount = 0;
+
+ class MyTech extends Tech {
+ seekable() {
+ throw new Error('You should not be calling me!');
+ }
+ seeking() {
+ seekingCount++;
+ return false;
+ }
+ duration() {
+ throw new Error('You should not be calling me!');
+ }
+ }
+
+ Tech.withSourceHandlers(MyTech);
+
+ let seekableCount = 0;
+ let durationCount = 0;
+ const handler = {
+ seekable() {
+ seekableCount++;
+ return createTimeRange(0, 0);
+ },
+ duration() {
+ durationCount++;
+ return 0;
+ }
+ };
+
+ MyTech.registerSourceHandler(stubbedSourceHandler(handler));
+
+ const tech = new MyTech();
+
+ tech.setSource({
+ src: 'example.mp4',
+ type: 'video/mp4'
+ });
+ tech.seekable();
+ tech.seeking();
+ tech.duration();
+ assert.equal(seekableCount, 1, 'called the source handler');
+ assert.equal(seekingCount, 1, 'called the tech itself');
+ assert.equal(durationCount, 1, 'called the source handler');
+ tech.dispose();
+});
+
+QUnit.test('Tech.isTech returns correct answers for techs and components', function(assert) {
+ const isTech = Tech.isTech;
+ const tech = new Html5({}, {});
+ const button = new Button(TestHelpers.makePlayer(), {});
+
+ assert.ok(isTech(Tech), 'Tech is a Tech');
+ assert.ok(isTech(Html5), 'Html5 is a Tech');
+ assert.ok(isTech(tech), 'An html5 instance is a Tech');
+ assert.ok(!isTech(5), 'A number is not a Tech');
+ assert.ok(!isTech('this is a tech'), 'A string is not a Tech');
+ assert.ok(!isTech(Button), 'A Button is not a Tech');
+ assert.ok(!isTech(button), 'A Button instance is not a Tech');
+ assert.ok(!isTech(isTech), 'A function is not a Tech');
+
+ tech.dispose();
+ button.dispose();
+});
+
+QUnit.test('setSource after tech dispose should dispose source handler once', function(assert) {
+ class MyTech extends Tech {}
+
+ Tech.withSourceHandlers(MyTech);
+
+ let disposeCount = 0;
+ const handler = {
+ dispose() {
+ disposeCount++;
+ }
+ };
+
+ MyTech.registerSourceHandler({
+ canPlayType() {
+ return true;
+ },
+ canHandleSource() {
+ return true;
+ },
+ handleSource(source, tech, options) {
+ return handler;
+ }
+ });
+
+ const tech = new MyTech();
+
+ tech.setSource('test');
+
+ assert.equal(disposeCount, 0, 'did not call sourceHandler_ dispose for initial dispose');
+ tech.dispose();
+ assert.ok(!tech.sourceHandler_, 'sourceHandler should be unset');
+ assert.equal(disposeCount, 1, 'called the source handler dispose');
+
+ // this would normally be done above tech on src after dispose
+ tech.el_ = tech.createEl();
+
+ tech.setSource('test');
+ assert.equal(disposeCount, 1, 'did not dispose after initial setSource');
+
+ tech.setSource('test');
+ assert.equal(disposeCount, 2, 'did dispose on second setSource');
+ tech.dispose();
+
+});
+
+QUnit.test('setSource after previous setSource should dispose source handler once', function(assert) {
+ class MyTech extends Tech {}
+
+ Tech.withSourceHandlers(MyTech);
+
+ let disposeCount = 0;
+ const handler = {
+ dispose() {
+ disposeCount++;
+ }
+ };
+
+ MyTech.registerSourceHandler({
+ canPlayType() {
+ return true;
+ },
+ canHandleSource() {
+ return true;
+ },
+ handleSource(source, tech, options) {
+ return handler;
+ }
+ });
+
+ const tech = new MyTech();
+
+ tech.setSource('test');
+ assert.equal(disposeCount, 0, 'did not call dispose for initial setSource');
+
+ tech.setSource('test');
+ assert.equal(disposeCount, 1, 'did dispose for second setSource');
+
+ tech.setSource('test');
+ assert.equal(disposeCount, 2, 'did dispose for third setSource');
+ tech.dispose();
+
+});
+
+QUnit.test('returns an empty object for getVideoPlaybackQuality', function(assert) {
+ const tech = new Tech();
+
+ assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'returns an empty object');
+ tech.dispose();
+});
+
+QUnit.test('requestVideoFrameCallback waits if tech not ready', function(assert) {
+ const tech = new Tech();
+ const cbSpy = sinon.spy();
+
+ tech.paused = sinon.spy();
+ tech.isReady_ = false;
+
+ tech.requestVideoFrameCallback(cbSpy);
+
+ assert.notOk(tech.paused.called, 'paused not called on tech that is not ready');
+
+ tech.trigger('playing');
+
+ assert.ok(cbSpy.called, 'callback was called on tech playing');
+
+ tech.dispose();
+});