Back to Repositories

Testing Video.js Play Method Implementation in video.js

This test suite thoroughly examines the Video.js player’s play() functionality, focusing on middleware interactions, source loading, and player readiness states. It verifies both promise-based and non-promise play implementations across different initialization scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the Video.js player’s play() method behavior. It tests multiple scenarios including:

  • DOM source loading with async and sync tech ready states
  • Middleware termination handling
  • Promise and non-promise play return values
  • Source changing states and timing
  • Player and tech readiness states

Implementation Analysis

The testing approach uses QUnit with sinon for clock manipulation and fake timers. It implements a modular structure with nested test suites for different play return values and middleware termination scenarios. The tests utilize beforeEach and afterEach hooks for setup and cleanup, with detailed state checking through custom assertion helpers.

Technical Details

  • Testing Framework: QUnit
  • Mocking: Sinon.js for timers and stubs
  • Test Helpers: Custom TestHelpers module
  • Middleware Testing: Custom middleware implementation
  • Clock Management: Fake timers for async testing
  • State Tracking: Custom state verification methods

Best Practices Demonstrated

The test suite exemplifies several testing best practices including isolated test cases, comprehensive state verification, and proper cleanup. It demonstrates thorough edge case handling, clear test organization, and effective use of test hooks for setup and teardown. The modular approach allows for testing multiple combinations of conditions systematically.

videojs/videoJs

test/unit/play.test.js

            
/* eslint-env qunit */
import TestHelpers from './test-helpers.js';
import sinon from 'sinon';
import window from 'global/window';
import * as middleware from '../../src/js/tech/middleware.js';
import {merge} from '../../src/js/utils/obj';

const middleWareTerminations = ['terminates', 'does not-terminate'];
const playReturnValues = ['non-promise', 'promise'];

const mainModule = function(playReturnValue, middlewareTermination, subhooks) {
  subhooks.beforeEach(function(assert) {
    this.clock = sinon.useFakeTimers();
    this.techPlayCalls = 0;
    this.playsTerminated = 0;
    this.playTests = [];
    this.terminate = false;

    if (middlewareTermination === 'terminates') {
      this.terminate = true;
    }
    this.techPlay = () => {
      this.techPlayCalls++;

      if (playReturnValue === 'promise') {
        return window.Promise.resolve('foo');
      }
      return 'foo';
    };

    this.finish = function() {
      const done = assert.async(this.playTests.length);

      const singleFinish = (playValue, assertName) => {
        assert.equal(playValue, 'foo', `play call from - ${assertName} - is correct`);
        done();
      };

      this.playTests.forEach(function(test) {
        const playRetval = test.playRetval;
        const testName = test.assertName;

        if (typeof playRetval === 'string') {
          singleFinish(playRetval, testName);
        } else {
          playRetval.then((v) => {
            singleFinish(v, testName);
          });
        }
      });
    };

    this.checkState = (assertName, options = {}) => {
      const expectedState = merge({
        playCalls: 0,
        techLoaded: false,
        techReady: false,
        playerReady: false,
        changingSrc: false,
        playsTerminated: 0
      }, options);

      if (typeof options.techLoaded === 'undefined' && typeof options.techReady !== 'undefined') {
        expectedState.techLoaded = options.techReady;
      }

      const currentState = {
        playCalls: this.techPlayCalls,
        techLoaded: Boolean(this.player.tech_),
        techReady: Boolean((this.player.tech_ || {}).isReady_),
        playerReady: Boolean(this.player.isReady_),
        changingSrc: Boolean(this.player.changingSrc_),
        playsTerminated: Number(this.playsTerminated)
      };

      assert.deepEqual(currentState, expectedState, assertName);
    };

    this.playTerminatedQueue = () => this.playsTerminated++;

    this.playTest = (assertName, options = {}) => {
      if (this.player.playTerminatedQueue_ !== this.playTerminatedQueue) {
        this.player.runPlayTerminatedQueue_ = this.playTerminatedQueue;
      }
      if (this.player && this.player.tech_ && this.player.tech_.play !== this.techPlay) {
        this.player.tech_.play = this.techPlay;
      }
      this.playTests.push({assertName, playRetval: this.player.play()});
      this.checkState(assertName, options);
    };

    this.middleware = () => {
      return {
        // pass along source
        setSource(srcObj, next) {
          next(null, srcObj);
        },
        callPlay: () => {
          if (this.terminate) {
            return middleware.TERMINATOR;
          }
        }
      };
    };

    middleware.use('*', this.middleware);
  });

  subhooks.afterEach(function() {
    // remove added middleware
    const middlewareList = middleware.getMiddleware('*');

    for (let i = 0; i < middlewareList.length; i++) {
      if (middlewareList[i] === this.middleware) {
        middlewareList.splice(i, 1);
      }
    }
    if (this.player) {
      this.player.dispose();
    }
    this.clock.restore();
  });

  QUnit.test('Player#play() resolves correctly with dom sources and async tech ready', function(assert) {
    // turn of mediaLoader to prevent setting a tech right away
    // similar to settings sources in the DOM
    // turn off autoReady to prevent synchronous ready from the tech
    this.player = TestHelpers.makePlayer({mediaLoader: false, techFaker: {autoReady: false}});

    this.playTest('before anything is ready');

    this.player.src({
      src: 'http://example.com/video.mp4',
      type: 'video/mp4'
    });

    this.playTest('only changingSrc', {
      changingSrc: true
    });

    this.clock.tick(1);

    this.playTest('still changingSrc, tech loaded', {
      techLoaded: true,
      changingSrc: true
    });

    this.player.tech_.triggerReady();
    this.playTest('still changingSrc, tech loaded and ready', {
      techReady: true,
      changingSrc: true
    });
    this.clock.tick(1);

    this.playTest('done changingSrc, tech/player ready', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.clock.tick(1);

    this.checkState('state stays the same', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.playTest('future calls hit tech#play directly, unless terminated', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 2,
      playsTerminated: this.terminate ? 2 : 0
    });

    if (this.terminate) {
      this.terminate = false;

      this.playTest('play works if not terminated', {
        playerReady: true,
        techReady: true,
        playCalls: 1,
        playsTerminated: 2
      });

      this.playTest('future calls hit tech#play directly', {
        playerReady: true,
        techReady: true,
        playCalls: 2,
        playsTerminated: 2
      });
    }

    this.finish(assert);
  });

  QUnit.test('Player#play() resolves correctly with dom sources', function(assert) {
    this.player = TestHelpers.makePlayer({mediaLoader: false});

    this.playTest('before anything is ready');

    this.player.src({
      src: 'http://example.com/video.mp4',
      type: 'video/mp4'
    });

    this.playTest('only changingSrc', {
      changingSrc: true
    });

    this.clock.tick(1);

    this.playTest('still changingSrc, tech/player ready', {
      techLoaded: true,
      changingSrc: true,
      playerReady: true,
      techReady: true
    });

    this.clock.tick(1);

    this.playTest('done changingSrc, tech#play is called', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.clock.tick(1);

    this.checkState('state stays the same', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.playTest('future calls hit tech#play directly', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 2,
      playsTerminated: this.terminate ? 2 : 0
    });

    if (this.terminate) {
      this.terminate = false;

      this.playTest('play works if not terminated', {
        playerReady: true,
        techReady: true,
        playCalls: 1,
        playsTerminated: 2
      });

      this.playTest('future calls hit tech#play directly', {
        playerReady: true,
        techReady: true,
        playCalls: 2,
        playsTerminated: 2
      });
    }

    this.finish(assert);
  });

  QUnit.test('Player#play() resolves correctly with async tech ready', function(assert) {
    this.player = TestHelpers.makePlayer({techFaker: {autoReady: false}});

    this.playTest('before anything is ready', {
      techLoaded: true
    });

    this.player.src({
      src: 'http://example.com/video.mp4',
      type: 'video/mp4'
    });

    this.playTest('tech loaded changingSrc', {
      techLoaded: true,
      changingSrc: true
    });

    this.clock.tick(1);

    this.playTest('still changingSrc, tech loaded', {
      techLoaded: true,
      changingSrc: true
    });

    this.clock.tick(1);
    this.playTest('still changingSrc, tech loaded again', {
      techLoaded: true,
      changingSrc: true
    });

    this.player.tech_.triggerReady();
    this.playTest('still changingSrc, tech loaded and ready', {
      techReady: true,
      changingSrc: true
    });
    this.clock.tick(1);

    this.playTest('still changingSrc tech/player ready', {
      changingSrc: true,
      playerReady: true,
      techReady: true
    });

    // player ready calls fire now
    // which sets changingSrc_ to false
    this.clock.tick(1);

    this.checkState('play was called on ready', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.playTest('future calls hit tech#play directly', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 2,
      playsTerminated: this.terminate ? 2 : 0
    });

    if (this.terminate) {
      this.terminate = false;

      this.playTest('play works if not terminated', {
        playerReady: true,
        techReady: true,
        playCalls: 1,
        playsTerminated: 2
      });

      this.playTest('future calls hit tech#play directly', {
        playerReady: true,
        techReady: true,
        playCalls: 2,
        playsTerminated: 2
      });
    }

    this.finish(assert);
  });

  QUnit.test('Player#play() resolves correctly', function(assert) {
    this.player = TestHelpers.makePlayer();

    this.playTest('player/tech start out ready', {
      techReady: true,
      playerReady: true
    });

    this.player.src({
      src: 'http://example.com/video.mp4',
      type: 'video/mp4'
    });

    this.playTest('now changingSrc', {
      techReady: true,
      playerReady: true,
      changingSrc: true
    });

    this.clock.tick(1);

    this.playTest('done changingSrc, play called if not terminated', {
      techReady: true,
      playerReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.clock.tick(2);

    this.checkState('state stays the same', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 1,
      playsTerminated: this.terminate ? 1 : 0
    });

    this.playTest('future calls hit tech#play directly', {
      playerReady: true,
      techReady: true,
      playCalls: this.terminate ? 0 : 2,
      playsTerminated: this.terminate ? 2 : 0
    });

    if (this.terminate) {
      this.terminate = false;

      this.playTest('play works if not terminated', {
        playerReady: true,
        techReady: true,
        playCalls: 1,
        playsTerminated: 2
      });

      this.playTest('future calls hit tech#play directly', {
        playerReady: true,
        techReady: true,
        playCalls: 2,
        playsTerminated: 2
      });
    }

    this.clock.tick(1000);

    this.finish(assert);
  });

  // without enableSourceset this test will fail.
  QUnit.test('Player#play() resolves correctly on tech el src', function(assert) {
    this.player = TestHelpers.makePlayer({techOrder: ['html5'], enableSourceset: true});

    this.playTest('player/tech start out ready', {
      techReady: true,
      playerReady: true
    });

    this.player.tech_.el_.src = 'http://vjs.zencdn.net/v/oceans.mp4';

    this.player.on('loadstart', () => {
      this.checkState('play should have been called', {
        techReady: true,
        playerReady: true,
        playCalls: 1
      });

      this.finish(assert);
    });
  });
};

QUnit.module('Player#play()', (hooks) => {
  playReturnValues.forEach((playReturnValue) => {
    middleWareTerminations.forEach((middlewareTermination) => {
      QUnit.module(`tech#play() => ${playReturnValue}, middleware ${middlewareTermination}`, (subhooks) => {
        mainModule(playReturnValue, middlewareTermination, subhooks);
      });
    });
  });
});