Back to Repositories

Validating Source Management and Event Handling in video.js

This unit test suite validates the sourceset handling functionality in video.js, focusing on source changes, tech initialization, and event handling across different media elements.

Test Coverage Overview

The test suite provides comprehensive coverage of sourceset event handling and source management in video.js.

  • Tests source initialization with different media elements (video, audio, video-js)
  • Validates source changes through various methods (player.src, mediaElement.src, setAttribute)
  • Covers edge cases like blob sources and relative URLs
  • Tests integration with different tech implementations (HTML5, custom tech)

Implementation Analysis

The testing approach uses QUnit with extensive setup and validation helpers.

Key patterns include:
  • Source validation across player cache, tech element, and events
  • Async testing with promises and timeouts
  • Mock tech implementation for custom source handling
  • Systematic test organization with nested modules

Technical Details

Testing infrastructure includes:

  • QUnit test framework with sinon for mocking
  • Custom validation helpers for source checking
  • Environment setup with DOM fixtures
  • Tech faker for simulating different playback technologies

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices:

  • Thorough setup and teardown between tests
  • Comprehensive validation of source states
  • Proper async handling with done() callbacks
  • Clear test organization and naming
  • Extensive edge case coverage

videojs/videoJs

test/unit/sourceset.test.js

            
/* eslint-env qunit */
import videojs from '../../src/js/video.js';
import document from 'global/document';
import window from 'global/window';
import log from '../../src/js/utils/log.js';
import sinon from 'sinon';
import {getAbsoluteURL} from '../../src/js/utils/url.js';

const Html5 = videojs.getTech('Html5');
const wait = 1;
let qunitFn = 'module';
const blobSrc = {
  src: 'blob:something',
  type: 'video/mp4'
};
const testSrc = {
  src: 'http://example.com/testSrc.mp4',
  type: 'video/mp4'
};
// Using a real URL here makes the tests work with retryOnError by default
// however, this means that it may fail offline
const sourceOne = {src: 'https://vjs.zencdn.net/v/oceans.mp4?one', type: 'video/mp4'};
const sourceTwo = {src: 'https://vjs.zencdn.net/v/oceans.mp4?two', type: 'video/mp4'};
const sourceThree = {src: 'https://vjs.zencdn.net/v/oceans.mp4?three', type: 'video/mp4'};

if (!Html5.canOverrideAttributes()) {
  qunitFn = 'skip';
}

const oldMovingMedia = Html5.prototype.movingMediaElementInDOM;
const validateSource = function(player, expectedSources, event, srcOverrides = {}) {
  expectedSources = Array.isArray(expectedSources) ? expectedSources : [expectedSources];
  const mediaEl = player.tech_.el();
  const assert = QUnit.assert;
  const expected = {
    // player cache checks
    currentSources: expectedSources, currentSource: expectedSources[0], src: expectedSources[0].src,
    // tech checks
    event: expectedSources[0].src, attr: expectedSources[0].src, prop: expectedSources[0].src
  };

  Object.keys(srcOverrides).forEach((k) => {
    // only override known properties
    if (!expected.hasOwnProperty(k)) {
      return;
    }

    expected[k] = srcOverrides[k];
  });

  assert.deepEqual(player.currentSource(), expected.currentSource, 'player.currentSource() is correct');
  assert.deepEqual(player.currentSources(), expected.currentSources, 'player.currentSources() is correct');
  assert.equal(player.src(), expected.src, 'player.src() is correct');

  assert.equal(event.src, expected.event, 'event src is correct');

  // if we expect a blank attr it will be null instead
  assert.equal(mediaEl.getAttribute('src'), expected.attr || null, 'mediaEl attribute is correct');

  // mediaEl.src source is always absolute, but can be empty string
  // getAbsoluteURL would return the current url of the page for empty string
  // so we have to check
  expected.prop = expected.prop ? getAbsoluteURL(expected.prop) : expected.prop;
  assert.equal(mediaEl.src, expected.prop, 'mediaEl src property is correct');

};

const setupEnv = function(env, testName) {
  sinon.stub(log, 'error');
  env.fixture = document.getElementById('qunit-fixture');

  if ((/^change/i).test(testName)) {
    Html5.prototype.movingMediaElementInDOM = false;
  }

  env.sourcesets = 0;
  env.hook = (player) => player.on('sourceset', (e) => {
    env.sourcesets++;
  });
  videojs.hook('setup', env.hook);

  if ((/video-js/i).test(testName)) {
    env.mediaEl = document.createElement('video-js');
  } else if ((/audio/i).test(testName)) {
    env.mediaEl = document.createElement('audio');
  } else {
    env.mediaEl = document.createElement('video');
  }
  env.mediaEl.className = 'video-js';
  env.fixture.appendChild(env.mediaEl);
};

const setupAfterEach = function(totalSourcesets) {
  return function(assert) {
    const done = assert.async();

    if (typeof this.totalSourcesets === 'undefined') {
      this.totalSourcesets = totalSourcesets;
    }

    window.setTimeout(() => {
      assert.equal(this.sourcesets, this.totalSourcesets, 'no additional sourcesets');

      this.player.dispose();
      assert.equal(this.sourcesets, this.totalSourcesets, 'no source set on dispose');

      videojs.removeHook('setup', this.hook);
      Html5.prototype.movingMediaElementInDOM = oldMovingMedia;
      log.error.restore();
      done();
    }, wait);
  };
};

const testTypes = ['video el', 'change video el', 'audio el', 'change audio el', 'video-js', 'change video-js el'];

QUnit[qunitFn]('sourceset', function(hooks) {
  QUnit.module('sourceset option', (subhooks) => testTypes.forEach((testName) => {
    QUnit.module(testName, {
      beforeEach() {

        setupEnv(this, testName);
      },
      afterEach: setupAfterEach(1)
    });

    QUnit.test('sourceset enabled by default', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('data-setup', JSON.stringify({sources: [testSrc]}));
      this.player = videojs(this.mediaEl, {});

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('sourceset not triggered if turned off', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: false
      });

      this.totalSourcesets = 0;

      this.player.one('sourceset', (e) => {
        this.totalSourcesets = 1;
      });

      this.player.on('loadstart', () => {
        done();
      });

      this.player.src(testSrc);

    });
  }));

  QUnit.module('source before player', (subhooks) => testTypes.forEach((testName) => {
    QUnit.module(testName, {
      beforeEach() {

        setupEnv(this, testName);
      },
      afterEach: setupAfterEach(1)
    });

    QUnit.test('data-setup one source', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('data-setup', JSON.stringify({sources: [testSrc]}));
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('data-setup one blob', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('data-setup', JSON.stringify({sources: [blobSrc]}));
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [blobSrc], e);
        done();
      });
    });

    QUnit.test('data-setup preload auto', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('data-setup', JSON.stringify({sources: [testSrc]}));
      this.mediaEl.setAttribute('preload', 'auto');
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('data-setup two sources', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('data-setup', JSON.stringify({sources: [sourceOne, sourceTwo]}));
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [sourceOne, sourceTwo], e);
        done();
      });
    });

    QUnit.test('videojs({sources: [...]}) one source', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true,
        sources: [testSrc]
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('videojs({sources: [...]}) one blob', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true,
        sources: [blobSrc]
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [blobSrc], e);
        done();
      });
    });

    QUnit.test('videojs({sources: [...]}) two sources', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true,
        sources: [sourceOne, sourceTwo]
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [sourceOne, sourceTwo], e);
        done();
      });
    });

    QUnit.test('mediaEl.src = ...;', function(assert) {
      const done = assert.async();

      this.mediaEl.src = testSrc.src;
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('mediaEl.src = blob;', function(assert) {
      const done = assert.async();

      this.mediaEl.src = blobSrc.src;
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [{src: blobSrc.src, type: ''}], e);
        done();
      });
    });

    QUnit.test('mediaEl.setAttribute("src", ...)"', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('src', testSrc.src);
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('mediaEl.setAttribute("src", blob)', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('src', blobSrc.src);
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [{src: blobSrc.src, type: ''}], e);
        done();
      });
    });

    QUnit.test('<source> one source', function(assert) {
      const done = assert.async();

      this.source = document.createElement('source');
      this.source.src = testSrc.src;
      this.source.type = testSrc.type;

      this.mediaEl.appendChild(this.source);

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });
      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });
    });

    QUnit.test('<source> two sources', function(assert) {
      const done = assert.async();

      this.source = document.createElement('source');
      this.source.src = sourceOne.src;
      this.source.type = sourceOne.type;

      this.source2 = document.createElement('source');
      this.source2.src = sourceTwo.src;
      this.source2.type = sourceTwo.type;

      this.mediaEl.appendChild(this.source);
      this.mediaEl.appendChild(this.source2);

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [sourceOne, sourceTwo], e);
        done();
      });
    });

    QUnit.test('no source', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.totalSourcesets = 0;

      window.setTimeout(() => {
        assert.equal(this.sourcesets, 0, 'no sourceset');
        done();
      }, wait);
    });

    QUnit.test('relative sources are handled correctly', function(assert) {
      const done = assert.async();
      const one = {src: 'relative-one.mp4', type: 'video/mp4'};
      const two = {src: '../relative-two.mp4', type: 'video/mp4'};
      const three = {src: './relative-three.mp4?test=test', type: 'video/mp4'};

      const source = document.createElement('source');

      source.src = one.src;
      source.type = one.type;

      this.mediaEl.appendChild(source);
      this.player = videojs(this.mediaEl, {enableSourceset: true});

      // mediaEl changes on ready
      this.player.ready(() => {
        this.mediaEl = this.player.tech_.el();
      });

      this.totalSourcesets = 3;
      this.player.one('sourceset', (e) => {
        assert.ok(true, '** sourceset with relative source and <source> el');
        // mediaEl attr is relative
        validateSource(this.player, {src: getAbsoluteURL(one.src), type: one.type}, e, {attr: one.src});

        this.player.one('sourceset', (e2) => {
          assert.ok(true, '** sourceset with relative source and mediaEl.src');
          // mediaEl attr is relative
          validateSource(this.player, {src: getAbsoluteURL(two.src), type: two.type}, e2, {attr: two.src});

          // setAttribute makes the source absolute
          this.player.one('sourceset', (e3) => {
            assert.ok(true, '** sourceset with relative source and mediaEl.setAttribute');
            validateSource(this.player, {src: getAbsoluteURL(three.src), type: three.type}, e3, {attr: three.src});
            done();
          });

          this.mediaEl.setAttribute('src', three.src);
        });

        this.mediaEl.src = two.src;
      });

    });
  }));

  QUnit.module('source after player', (subhooks) => testTypes.forEach((testName) => {
    QUnit.module(testName, {
      beforeEach() {

        setupEnv(this, testName);
      },
      afterEach: setupAfterEach(1)
    });

    QUnit.test('player.src({...}) one source', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });
      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });

      this.player.src(testSrc);
    });

    QUnit.test('player.src({...}) one blob', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });
      this.player.one('sourceset', (e) => {
        validateSource(this.player, [blobSrc], e);
        done();
      });

      this.player.src(blobSrc);
    });

    QUnit.test('player.src({...}) preload auto', function(assert) {
      const done = assert.async();

      this.mediaEl.setAttribute('preload', 'auto');
      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });

      this.player.src(testSrc);
    });

    QUnit.test('player.src({...}) two sources', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {
        enableSourceset: true
      });

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [sourceOne, sourceTwo], e);
        done();
      });

      this.player.src([sourceOne, sourceTwo]);
    });

    QUnit.test('mediaEl.src = ...;', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {enableSourceset: true});

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });

      this.player.tech_.el_.src = testSrc.src;
    });

    QUnit.test('mediaEl.src = blob', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {enableSourceset: true});

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [{src: blobSrc.src, type: ''}], e);
        done();
      });

      this.player.tech_.el_.src = blobSrc.src;
    });

    QUnit.test('mediaEl.setAttribute("src", ...)"', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {enableSourceset: true});

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [testSrc], e);
        done();
      });

      this.player.tech_.el_.setAttribute('src', testSrc.src);
    });

    QUnit.test('mediaEl.setAttribute("src", blob)"', function(assert) {
      const done = assert.async();

      this.player = videojs(this.mediaEl, {enableSourceset: true});

      this.player.one('sourceset', (e) => {
        validateSource(this.player, [{src: blobSrc.src, type: ''}], e);
        done();
      });

      this.player.tech_.el_.setAttribute('src', blobSrc.src);
    });

    const appendTypes = [
      {name: 'appendChild', fn: (el, obj) => el.appendChild(obj)},
      {name: 'innerHTML', fn: (el, obj) => {el.innerHTML = obj.outerHTML;}}, // eslint-disable-line
    ];

    // ie does not support this and safari < 10 does not either
    if (window.Element.prototype.append) {
      appendTypes.push({name: 'append', fn: (el, obj) => el.append(obj)});
    }

    if (window.Element.prototype.insertAdjacentHTML) {
      appendTypes.push({name: 'insertAdjacentHTML', fn: (el, obj) => el.insertAdjacentHTML('afterbegin', obj.outerHTML)});
    }

    appendTypes.forEach((appendObj) => {

      QUnit.test(`<source> one source through ${appendObj.name}`, function(assert) {
        const done = assert.async();

        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e) => {
          validateSource(this.player, testSrc, e, {prop: '', attr: ''});
          done();
        });

        // since the media el has no source, just appending will
        // change the source without calling load
        appendObj.fn(this.player.tech_.el_, this.source);
      });

      QUnit.test(`<source> one source through ${appendObj.name} and load`, function(assert) {
        const done = assert.async();

        this.totalSourcesets = 2;
        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e1) => {
          validateSource(this.player, testSrc, e1, {prop: '', attr: ''});

          this.player.one('sourceset', (e2) => {
            validateSource(this.player, testSrc, e2, {prop: '', attr: ''});
            done();
          });
        });

        // since the media el has no source, just appending will
        // change the source without calling load
        appendObj.fn(this.player.tech_.el_, this.source);

        // should fire an additional sourceset
        this.player.tech_.el_.load();
      });

      QUnit.test(`one <source> through ${appendObj.name} and then mediaEl.src`, function(assert) {
        const done = assert.async();

        this.totalSourcesets = 2;
        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e1) => {
          validateSource(this.player, testSrc, e1, {prop: '', attr: ''});

          this.player.one('sourceset', (e2) => {
            validateSource(this.player, [sourceOne], e2);

            done();
          });
        });

        // since the media el has no source, just appending will
        // change the source without calling load
        appendObj.fn(this.player.tech_.el_, this.source);

        // should fire an additional sourceset
        this.player.tech_.el_.src = sourceOne.src;
      });

      QUnit.test(`one <source> through ${appendObj.name} and then mediaEl.setAttribute`, function(assert) {
        const done = assert.async();

        this.totalSourcesets = 2;
        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e1) => {
          validateSource(this.player, testSrc, e1, {prop: '', attr: ''});

          this.player.one('sourceset', (e2) => {
            validateSource(this.player, [sourceOne], e2);

            done();
          });
        });

        // since the media el has no source, just appending will
        // change the source without calling load
        appendObj.fn(this.player.tech_.el_, this.source);

        // should fire an additional sourceset
        this.player.tech_.el_.setAttribute('src', sourceOne.src);
      });

      QUnit.test(`mediaEl.src and then <source> through ${appendObj.name}`, function(assert) {
        const done = assert.async();

        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e) => {
          validateSource(this.player, [sourceOne], e);

          done();
        });

        this.player.tech_.el_.src = sourceOne.src;

        // should not fire sourceset
        appendObj.fn(this.player.tech_.el_, this.source);
      });

      QUnit.test(`mediaEl.setAttribute and then <source> through ${appendObj.name}`, function(assert) {
        const done = assert.async();

        this.source = document.createElement('source');
        this.source.src = testSrc.src;
        this.source.type = testSrc.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e) => {
          validateSource(this.player, [sourceOne], e);

          done();
        });

        this.player.tech_.el_.setAttribute('src', sourceOne.src);

        // should not fire sourceset
        appendObj.fn(this.player.tech_.el_, this.source);
      });

      QUnit.test(`<source> two sources through ${appendObj.name}`, function(assert) {
        const done = assert.async();

        this.source = document.createElement('source');
        this.source.src = sourceOne.src;
        this.source.type = sourceOne.type;

        this.source2 = document.createElement('source');
        this.source2.src = sourceTwo.src;
        this.source2.type = sourceTwo.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e) => {
          validateSource(this.player, sourceOne, e, {prop: '', attr: ''});
          done();
        });

        // since the media el has no source, just appending will
        // change the source without calling load
        appendObj.fn(this.player.tech_.el_, this.source);

        // this should not be in the source list or fire a sourceset
        appendObj.fn(this.player.tech_.el_, this.source2);
      });

      QUnit.test(`set, remove, load, and set again through ${appendObj.name}`, function(assert) {
        const done = assert.async();

        this.totalSourcesets = 3;
        this.source = document.createElement('source');
        this.source.src = sourceTwo.src;
        this.source.type = sourceTwo.type;

        this.player = videojs(this.mediaEl, {enableSourceset: true});

        this.player.one('sourceset', (e1) => {
          validateSource(this.player, [sourceOne], e1);

          this.player.one('sourceset', (e2) => {
            validateSource(this.player, [{src: '', type: ''}], e2);

            this.player.one('sourceset', (e3) => {
              validateSource(this.player, sourceTwo, e3, {prop: '', attr: ''});
              done();
            });
          });

          // reset to no source
          this.player.tech_.el_.removeAttribute('src');
          this.player.tech_.el_.load();

          // since the media el has no source, just appending will
          // change the source without calling load
          appendObj.fn(this.player.tech_.el_, this.source);

        });

        this.player.tech_.el_.setAttribute('src', sourceOne.src);
      });
    });

    QUnit.test('no source and load', function(assert) {
      this.player = videojs(this.mediaEl, {enableSourceset: true});
      this.player.tech_.el_.load();

      this.totalSourcesets = 1;
    });
  }));

  QUnit.module('source change', (subhooks) => testTypes.forEach((testName) => {
    QUnit.module(testName, {
      beforeEach(assert) {
        const done = assert.async();

        setupEnv(this, testName);

        this.mediaEl.src = testSrc.src;
        this.player = videojs(this.mediaEl, {
          enableSourceset: true
        });

        this.player.ready(() => {
          this.mediaEl = this.player.tech_.el();
        });

        // initial sourceset should happen on player.ready
        this.player.one('sourceset', (e) => {
          validateSource(this.player, [testSrc], e);
          done();
        });
      },
      afterEach: setupAfterEach(3)
    });

    QUnit.test('player.src({...})', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [testSrc], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceOne], e2);
          done();
        });

        this.player.src(sourceOne);
      });

      this.player.src(testSrc);
    });

    QUnit.test('hls -> hls -> blob -> hls', function(assert) {
      this.totalSourcesets = 5;
      // we have to force techFaker here as some browsers, ie edge/safari support
      // native HLS.
      this.player.options_.techOrder = ['techFaker'];
      this.player.options_.techFaker = this.player.options_.techFaker || {};
      const done = assert.async();
      const m3u8One = {
        src: 'http://vjs.zencdn.net/v/oceans.m3u8',
        type: 'application/x-mpegURL'
      };
      const blobOne = 'blob:one';
      const m3u8Two = {
        src: 'http://vjs.zencdn.net/v/oceans-two.m3u8',
        type: 'application/x-mpegURL'
      };
      const blobTwo = 'blob:two';
      const setTechFaker = (src) => {
        this.player.options_.techFaker = this.player.options_.techFaker || {};
        this.player.tech_.options_ = this.player.tech_.options_ || {};
        this.player.tech_.options_.sourceset = src;
        this.player.options_.techFaker.sourceset = src;
      };

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [m3u8One], e1, {event: blobOne, attr: blobOne, prop: blobOne});

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [m3u8Two], e2, {event: blobTwo, attr: blobTwo, prop: blobTwo});

          // should change to blobSrc now
          this.player.one('sourceset', (e3) => {
            validateSource(this.player, [blobSrc], e3);

            this.player.one('sourceset', (e4) => {
              validateSource(this.player, [m3u8Two], e2, {event: blobTwo, attr: blobTwo, prop: blobTwo});

              done();
            });

            setTechFaker(blobTwo);
            this.player.src(m3u8Two);
          });

          setTechFaker(blobSrc.src);
          this.player.src(blobSrc);
        });

        setTechFaker(blobTwo);
        this.player.src(m3u8Two);
      });

      setTechFaker(blobOne);
      this.player.src(m3u8One);
    });

    QUnit.test('hls -> mp4 -> hls -> blob', function(assert) {
      this.totalSourcesets = 5;
      // we have to force techFaker here as some browsers, ie edge/safari support
      // native HLS.
      this.player.options_.techOrder = ['techFaker'];
      this.player.options_.techFaker = this.player.options_.techFaker || {};
      const done = assert.async();
      const m3u8One = {
        src: 'http://vjs.zencdn.net/v/oceans.m3u8',
        type: 'application/x-mpegURL'
      };
      const blobOne = 'blob:one';
      const setTechFaker = (src) => {
        this.player.options_.techFaker = this.player.options_.techFaker || {};
        this.player.tech_.options_ = this.player.tech_.options_ || {};
        this.player.tech_.options_.sourceset = src;
        this.player.options_.techFaker.sourceset = src;
      };

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [m3u8One], e1, {event: blobOne, attr: blobOne, prop: blobOne});

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [testSrc], e2);

          // should change to blobSrc now
          this.player.one('sourceset', (e3) => {
            validateSource(this.player, [m3u8One], e3, {event: blobOne, attr: blobOne, prop: blobOne});

            this.player.one('sourceset', (e4) => {
              validateSource(this.player, [blobSrc], e4);

              done();
            });

            setTechFaker(blobSrc.src);
            this.player.src(blobSrc);
          });

          setTechFaker(blobOne);
          this.player.src(m3u8One);
        });

        setTechFaker(testSrc.src);
        this.player.src(testSrc);
      });

      setTechFaker(blobOne);
      this.player.src(m3u8One);
    });

    QUnit.test('player.src({...}) x2 at the same time', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [sourceOne], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceTwo], e2);
          done();
        });
      });

      this.player.src(sourceOne);
      this.player.src(sourceTwo);
    });

    QUnit.test('player.src({...}) x3 at the same time', function(assert) {
      const done = assert.async();

      // we have one more sourceset then other tests
      this.totalSourcesets = 4;

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, sourceOne, e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, sourceTwo, e2);

          this.player.one('sourceset', (e3) => {
            validateSource(this.player, sourceThree, e3);
            done();
          });
        });
      });

      this.player.src(sourceOne);
      this.player.src(sourceTwo);
      this.player.src(sourceThree);
    });

    QUnit.test('mediaEl.src = ...', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [testSrc], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceOne], e2);
          done();
        });

        this.mediaEl.src = sourceOne.src;
      });

      this.mediaEl.src = testSrc.src;
    });

    QUnit.test('mediaEl.src = ... x2 at the same time', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [sourceOne], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceTwo], e2);
          done();
        });
      });

      this.mediaEl.src = sourceOne.src;
      this.mediaEl.src = sourceTwo.src;
    });

    QUnit.test('mediaEl.setAttribute("src", ...)', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [testSrc], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceOne], e2);
          done();
        });

        this.mediaEl.setAttribute('src', sourceOne.src);
      });

      this.mediaEl.setAttribute('src', testSrc.src);
    });

    QUnit.test('mediaEl.setAttribute("src", ...) x2 at the same time', function(assert) {
      const done = assert.async();

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [sourceOne], e1);

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceTwo], e2);
          done();
        });
      });

      this.mediaEl.setAttribute('src', sourceOne.src);
      this.mediaEl.setAttribute('src', sourceTwo.src);
    });

    QUnit.test('mediaEl.load() with a src attribute', function(assert) {
      const done = assert.async();

      this.totalSourcesets = 1;

      window.setTimeout(() => {
        this.sourcesets = 0;
        this.totalSourcesets = 1;

        this.player.one('sourceset', (e) => {
          validateSource(this.player, [testSrc], e);
          done();
        });

        this.player.load();
      }, wait);
    });

    QUnit.test('mediaEl.load()', function(assert) {
      const source = document.createElement('source');

      source.src = testSrc.src;
      source.type = testSrc.type;

      // the only way to unset a source, so that we use the source
      // elements instead
      this.mediaEl.removeAttribute('src');

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [testSrc], e1, {attr: '', prop: ''});

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceOne], e2, {attr: '', prop: ''});
        });

        source.src = sourceOne.src;
        source.type = sourceOne.type;

        this.mediaEl.load();
      });

      this.mediaEl.appendChild(source);
      this.mediaEl.load();
    });

    QUnit.test('mediaEl.load() x2 at the same time', function(assert) {
      const source = document.createElement('source');

      source.src = sourceOne.src;
      source.type = sourceOne.type;

      this.player.one('sourceset', (e1) => {
        validateSource(this.player, [sourceOne], e1, {attr: '', prop: ''});

        this.player.one('sourceset', (e2) => {
          validateSource(this.player, [sourceTwo], e2, {attr: '', prop: ''});
        });
      });

      // the only way to unset a source, so that we use the source
      // elements instead
      this.mediaEl.removeAttribute('src');
      this.mediaEl.appendChild(source);
      this.mediaEl.load();

      source.src = sourceTwo.src;
      source.type = sourceTwo.type;
      this.mediaEl.load();
    });

    QUnit.test('adding a <source> without load()', function(assert) {
      const done = assert.async();
      const source = document.createElement('source');

      source.src = testSrc.src;
      source.type = testSrc.type;

      this.mediaEl.appendChild(source);

      this.totalSourcesets = 1;

      window.setTimeout(() => {
        assert.equal(this.sourcesets, 1, 'does not trigger sourceset');
        done();
      }, wait);
    });

    QUnit.test('changing a <source>s src without load()', function(assert) {
      const done = assert.async();
      const source = document.createElement('source');

      source.src = testSrc.src;
      source.type = testSrc.type;

      this.mediaEl.appendChild(source);

      source.src = testSrc.src;

      this.totalSourcesets = 1;

      window.setTimeout(() => {
        assert.equal(this.sourcesets, 1, 'does not trigger sourceset');
        done();
      }, wait);
    });
  }));

  QUnit.test('sourceset event object has a src property', function(assert) {
    const done = assert.async();
    const fixture = document.querySelector('#qunit-fixture');
    const vid = document.createElement('video');
    const Tech = videojs.getTech('Tech');
    const youtubeSrc = {
      src: 'https://www.youtube.com/watch?v=C0DPdy98e4c',
      type: 'video/youtube'
    };
    const sourcesets = [];

    class FakeYoutube extends Html5 {
      static isSupported() {
        return true;
      }

      static canPlayType(type) {
        return type === 'video/youtube' ? 'maybe' : '';
      }

      static canPlaySource(srcObj) {
        return srcObj.type === 'video/youtube';
      }
    }

    videojs.registerTech('FakeYoutube', FakeYoutube);

    fixture.appendChild(vid);

    const player = videojs(vid, {
      enableSourceset: true,
      techOrder: ['fakeYoutube', 'html5']
    });

    player.ready(function() {
      // the first sourceset ends up being the second source because when the first source is set
      // the tech isn't ready so we delay it, then the second source comes and the tech is ready
      // so it ends up being triggered immediately.
      player.on('sourceset', (e) => {
        sourcesets.push(e.src);

        if (sourcesets.length === 3) {
          assert.deepEqual([youtubeSrc.src, sourceTwo.src, sourceOne.src], sourcesets, 'sourceset as expected');

          player.dispose();
          delete Tech.techs_.FakeYoutube;
          done();
        }
      });

      player.src(sourceOne);
      player.src(sourceTwo);
    });

    player.src(youtubeSrc);

  });

  QUnit.test('tech sourceset update sources cache when changingSrc_ is false', function(assert) {
    const clock = sinon.useFakeTimers();
    const fixture = document.querySelector('#qunit-fixture');
    const vid = document.createElement('video');

    fixture.appendChild(vid);

    const player = videojs(vid);

    player.src(sourceOne);

    clock.tick(1);

    // Declaring the spies here avoids listening to the changes that took place when loading the source.
    const handleTechSourceset_ = sinon.spy(player, 'handleTechSourceset_');
    const techGet_ = sinon.spy(player, 'techGet_');
    const updateSourceCaches_ = sinon.spy(player, 'updateSourceCaches_');

    player.tech_.trigger('sourceset');

    assert.ok(handleTechSourceset_.calledOnce, 'handleTechSourceset_ was called');
    assert.ok(!techGet_.called, 'techGet_ was not called');
    assert.ok(updateSourceCaches_.calledOnce, 'updateSourceCaches_ was called');
    assert.equal(player.cache_.src, '', 'player.cache_.src was reset');

    player.tech_.trigger('loadstart');

    assert.ok(techGet_.called, 'techGet_ was called');
    assert.equal(updateSourceCaches_.callCount, 2, 'updateSourceCaches_ was called twice');

    handleTechSourceset_.restore();
    techGet_.restore();
    updateSourceCaches_.restore();
    player.dispose();
    clock.restore();
  });
});