Testing Advanced Plugin System Implementation in video.js
This test suite validates advanced functionality of the Plugin system in Video.js, focusing on plugin lifecycle, state management, and event handling. It ensures proper implementation of plugin registration, initialization, and disposal mechanisms.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
videojs/videoJs
test/unit/plugin-advanced.test.js
/* eslint-env qunit */
import sinon from 'sinon';
import Plugin from '../../src/js/plugin';
import TestHelpers from './test-helpers';
QUnit.module('Plugin: advanced', {
beforeEach() {
this.player = TestHelpers.makePlayer();
const spy = this.spy = sinon.spy();
class MockPlugin extends Plugin {
constructor(...args) {
super(...args);
spy.apply(this, args);
}
}
MockPlugin.VERSION = '1.0.0';
this.MockPlugin = MockPlugin;
Plugin.registerPlugin('mock', MockPlugin);
},
afterEach() {
this.player.dispose();
Object.keys(Plugin.getPlugins()).forEach(key => {
if (key !== Plugin.BASE_PLUGIN_NAME) {
Plugin.deregisterPlugin(key);
}
});
}
});
QUnit.test('pre-setup interface', function(assert) {
assert.strictEqual(typeof this.player.plugin, 'undefined', 'the base Plugin does not add a method to the player');
assert.strictEqual(typeof this.player.mock, 'function', 'plugins are a factory function on a player');
assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
assert.strictEqual(this.player.mock.dispose, undefined, 'class-based plugins are not populated on a player until the factory method creates them');
assert.notOk(this.player.usingPlugin('mock'), 'the player is not using the plugin');
});
QUnit.test('setup', function(assert) {
const instance = this.player.mock({foo: 'bar'}, 123);
assert.strictEqual(this.spy.callCount, 1, 'plugin was set up once');
assert.strictEqual(this.spy.firstCall.thisValue, instance, 'plugin constructor `this` value was the instance');
assert.deepEqual(this.spy.firstCall.args, [this.player, {foo: 'bar'}, 123], 'plugin had the correct arguments');
assert.ok(this.player.usingPlugin('mock'), 'player now recognizes that the plugin was set up');
assert.ok(this.player.hasPlugin('mock'), 'player has the plugin available');
assert.ok(instance instanceof this.MockPlugin, 'plugin instance has the correct constructor');
assert.strictEqual(instance, this.player.mock(), 'factory is replaced by method returning the instance');
assert.strictEqual(instance.player, this.player, 'instance has a reference to the player');
assert.strictEqual(instance.name, 'mock', 'instance knows its name');
assert.strictEqual(typeof instance.state, 'object', 'instance is stateful');
assert.strictEqual(typeof instance.setState, 'function', 'instance is stateful');
assert.strictEqual(typeof instance.off, 'function', 'instance is evented');
assert.strictEqual(typeof instance.on, 'function', 'instance is evented');
assert.strictEqual(typeof instance.one, 'function', 'instance is evented');
assert.strictEqual(typeof instance.trigger, 'function', 'instance is evented');
assert.strictEqual(typeof instance.dispose, 'function', 'instance has dispose method');
assert.strictEqual(typeof instance.version, 'function', 'instance has version method');
assert.strictEqual(instance.version(), '1.0.0', 'version function returns VERSION value');
assert.throws(
() => new Plugin(this.player),
new Error('Plugin must be sub-classed; not directly instantiated.'),
'the Plugin class cannot be directly instantiated'
);
});
QUnit.test('log is added by default', function(assert) {
const instance = this.player.mock();
assert.strictEqual(typeof instance.log, 'function', 'log is a function');
assert.strictEqual(typeof instance.log.debug, 'function', 'log.debug is a function');
assert.strictEqual(typeof instance.log.error, 'function', 'log.error is a function');
assert.strictEqual(typeof instance.log.history, 'function', 'log.history is a function');
assert.strictEqual(typeof instance.log.levels, 'object', 'log.levels is a object');
assert.strictEqual(typeof instance.log.warn, 'function', 'log.warn is a function');
});
QUnit.test('log will not clobber pre-existing log property', function(assert) {
class MockLogPlugin extends Plugin {
log() {}
}
MockLogPlugin.VERSION = '1.0.0';
Plugin.registerPlugin('mockLog', MockLogPlugin);
const instance = this.player.mockLog();
assert.strictEqual(typeof instance.log, 'function', 'log is a function');
assert.strictEqual(instance.log, MockLogPlugin.prototype.log, 'log was not overridden');
});
QUnit.test('all "pluginsetup" events', function(assert) {
const setupSpy = sinon.spy();
const events = [
'beforepluginsetup',
'beforepluginsetup:mock',
'pluginsetup',
'pluginsetup:mock'
];
this.player.on(events, setupSpy);
const instance = this.player.mock();
events.forEach((type, i) => {
const event = setupSpy.getCall(i).args[0];
const hash = setupSpy.getCall(i).args[1];
assert.strictEqual(event.type, type, `the "${type}" event was triggered`);
assert.strictEqual(event.target, this.player.el_, 'the event has the correct target');
assert.deepEqual(hash, {
name: 'mock',
// The "before" events have a `null` instance and the others have the
// return value of the plugin factory.
instance: i < 2 ? null : instance,
plugin: this.MockPlugin
}, 'the event hash object is correct');
});
});
QUnit.test('defaultState static property is used to populate state', function(assert) {
class DefaultStateMock extends Plugin {}
DefaultStateMock.defaultState = {foo: 1, bar: 2};
Plugin.registerPlugin('dsm', DefaultStateMock);
const instance = this.player.dsm();
assert.deepEqual(instance.state, {foo: 1, bar: 2}, 'the plugin state has default properties');
});
QUnit.test('dispose', function(assert) {
const instance = this.player.mock();
instance.dispose();
assert.notOk(this.player.usingPlugin('mock'), 'player recognizes that the plugin is NOT set up');
assert.ok(this.player.hasPlugin('mock'), 'player still has the plugin available');
assert.strictEqual(typeof this.player.mock, 'function', 'instance is replaced by factory');
assert.notStrictEqual(instance, this.player.mock, 'instance is replaced by factory');
assert.strictEqual(instance.player, null, 'instance no longer has a reference to the player');
assert.strictEqual(instance.state, null, 'state is now null');
});
QUnit.test('"dispose" event', function(assert) {
const disposeSpy = sinon.spy();
const instance = this.player.mock();
instance.on('dispose', disposeSpy);
instance.dispose();
assert.strictEqual(disposeSpy.callCount, 1, 'the "dispose" event was triggered');
const event = disposeSpy.firstCall.args[0];
const hash = disposeSpy.firstCall.args[1];
assert.strictEqual(event.type, 'dispose', 'the event has the correct type');
assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');
assert.deepEqual(hash, {
name: 'mock',
instance,
plugin: this.MockPlugin
}, 'the event hash object is correct');
});
QUnit.test('arbitrary events', function(assert) {
const fooSpy = sinon.spy();
const instance = this.player.mock();
instance.on('foo', fooSpy);
instance.trigger('foo');
const event = fooSpy.firstCall.args[0];
const hash = fooSpy.firstCall.args[1];
assert.strictEqual(fooSpy.callCount, 1, 'the "foo" event was triggered');
assert.strictEqual(event.type, 'foo', 'the event has the correct type');
assert.strictEqual(event.target, instance.eventBusEl_, 'the event has the correct target');
assert.deepEqual(hash, {
name: 'mock',
instance,
plugin: this.MockPlugin
}, 'the event hash object is correct');
});
QUnit.test('handleStateChanged() method is automatically bound to the "statechanged" event', function(assert) {
const spy = sinon.spy();
class TestHandler extends Plugin {
handleStateChanged(...args) {
spy.apply(this, args);
}
}
Plugin.registerPlugin('testHandler', TestHandler);
const instance = this.player.testHandler();
instance.setState({foo: 1});
assert.strictEqual(spy.callCount, 1, 'the handleStateChanged listener was called');
assert.strictEqual(spy.firstCall.args[0].type, 'statechanged', 'the event was "statechanged"');
assert.strictEqual(typeof spy.firstCall.args[0].changes, 'object', 'the event included a changes object');
});