From f2fa8f86870ed52ee47a891ced97d4af25bf0814 Mon Sep 17 00:00:00 2001
From: "Pat O&#x27;Neill" <pgoneill@gmail.com>
Date: Mon, 9 Nov 2015 17:43:17 -0500
Subject: [PATCH] @misteroneill exposed DOM helpers. closes #2754

---
 .jshintrc                                    |   1 +
 CHANGELOG.md                                 |   1 +
 src/js/component.js                          |  59 +++++++-
 src/js/player.js                             |   4 +-
 src/js/tech/html5.js                         |   2 +-
 src/js/tracks/text-track-list-converter.js   |   2 +
 src/js/tracks/text-track-settings.js         |  96 ++++++------
 src/js/utils/dom.js                          | 217 +++++++++++++++++++++++----
 src/js/video.js                              | 163 +++++++++++++++++---
 test/unit/component.test.js                  |  19 +++
 test/unit/tracks/text-track-settings.test.js |  74 ++++-----
 test/unit/utils/dom.test.js                  | 194 ++++++++++++++++++++++--
 test/unit/video.test.js                      |  27 ++++
 13 files changed, 707 insertions(+), 152 deletions(-)

diff --git a/.jshintrc b/.jshintrc
index ac1dfa6..826ec9f 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -41,6 +41,7 @@
         "stop",
         "strictEqual",
         "test",
+        "throws",
         "sinon"
     ]
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20efb55..9aafe16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ CHANGELOG
 * @paladox updated grunt-cli dependency ([view](https://github.com/videojs/video.js/pull/2555))
 * @paladox updated grunt-contrib-jshint ([view](https://github.com/videojs/video.js/pull/2554))
 * @siebrand updated dutch translations ([view](https://github.com/videojs/video.js/pull/2556))
+* @misteroneill exposed DOM helpers ([view](https://github.com/videojs/video.js/pull/2754))
 
 --------------------
 
diff --git a/src/js/component.js b/src/js/component.js
index 9fb71ce..7277747 100644
--- a/src/js/component.js
+++ b/src/js/component.js
@@ -831,6 +831,46 @@ class Component {
   }
 
   /**
+   * Finds a single DOM element matching `selector` within the component's
+   * `contentEl` or another custom context.
+   *
+   * @method $
+   * @param  {String} selector
+   *         A valid CSS selector, which will be passed to `querySelector`.
+   *
+   * @param  {Element|String} [context=document]
+   *         A DOM element within which to query. Can also be a selector
+   *         string in which case the first matching element will be used
+   *         as context. If missing (or no element matches selector), falls
+   *         back to `document`.
+   *
+   * @return {Element|null}
+   */
+  $(selector, context) {
+    return Dom.$(selector, context || this.contentEl());
+  }
+
+  /**
+   * Finds a all DOM elements matching `selector` within the component's
+   * `contentEl` or another custom context.
+   *
+   * @method $$
+   * @param  {String} selector
+   *         A valid CSS selector, which will be passed to `querySelectorAll`.
+   *
+   * @param  {Element|String} [context=document]
+   *         A DOM element within which to query. Can also be a selector
+   *         string in which case the first matching element will be used
+   *         as context. If missing (or no element matches selector), falls
+   *         back to `document`.
+   *
+   * @return {NodeList}
+   */
+  $$(selector, context) {
+    return Dom.$$(selector, context || this.contentEl());
+  }
+
+  /**
    * Check if a component's element has a CSS class name
    *
    * @param {String} classToCheck Classname to check
@@ -854,7 +894,7 @@ class Component {
   }
 
   /**
-   * Remove and return a CSS class name from the component's element
+   * Remove a CSS class name from the component's element
    *
    * @param {String} classToRemove Classname to remove
    * @return {Component}
@@ -866,6 +906,23 @@ class Component {
   }
 
   /**
+   * Add or remove a CSS class name from the component's element
+   *
+   * @param  {String} classToToggle
+   * @param  {Boolean|Function} [predicate]
+   *         Can be a function that returns a Boolean. If `true`, the class
+   *         will be added; if `false`, the class will be removed. If not
+   *         given, the class will be added if not present and vice versa.
+   *
+   * @return {Component}
+   * @method toggleClass
+   */
+  toggleClass(classToToggle, predicate) {
+    Dom.toggleElClass(this.el_, classToToggle, predicate);
+    return this;
+  }
+
+  /**
    * Show the component element if hidden
    *
    * @return {Component}
diff --git a/src/js/player.js b/src/js/player.js
index 7d28044..c02d847 100644
--- a/src/js/player.js
+++ b/src/js/player.js
@@ -281,8 +281,8 @@ class Player extends Component {
     // of the player in a way that's still overrideable by CSS, just like the
     // video element
     this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions');
-    let defaultsStyleEl = document.querySelector('.vjs-styles-defaults');
-    let head = document.querySelector('head');
+    let defaultsStyleEl = Dom.$('.vjs-styles-defaults');
+    let head = Dom.$('head');
     head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
 
     // Pass in the width/height/aspectRatio options which will update the style el
diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js
index 8b72a35..b91d691 100644
--- a/src/js/tech/html5.js
+++ b/src/js/tech/html5.js
@@ -771,7 +771,7 @@ class Html5 extends Tech {
 
     this.remoteTextTracks().removeTrack_(track);
 
-    tracks = this.el().querySelectorAll('track');
+    tracks = this.$$('track');
 
     i = tracks.length;
     while (i--) {
diff --git a/src/js/tracks/text-track-list-converter.js b/src/js/tracks/text-track-list-converter.js
index 4e99aba..841b74b 100644
--- a/src/js/tracks/text-track-list-converter.js
+++ b/src/js/tracks/text-track-list-converter.js
@@ -44,6 +44,8 @@ let trackToJson_ = function(track) {
  * @function textTracksToJson
  */
 let textTracksToJson = function(tech) {
+
+  // Cannot use $$ here because it is not an instance of Tech
   let trackEls = tech.el().querySelectorAll('track');
 
   let trackObjs = Array.prototype.map.call(trackEls, (t) => t.track);
diff --git a/src/js/tracks/text-track-settings.js b/src/js/tracks/text-track-settings.js
index 3031822..da7b7fc 100644
--- a/src/js/tracks/text-track-settings.js
+++ b/src/js/tracks/text-track-settings.js
@@ -27,33 +27,33 @@ class TextTrackSettings extends Component {
       this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
     }
 
-    Events.on(this.el().querySelector('.vjs-done-button'), 'click', Fn.bind(this, function() {
+    Events.on(this.$('.vjs-done-button'), 'click', Fn.bind(this, function() {
       this.saveSettings();
       this.hide();
     }));
 
-    Events.on(this.el().querySelector('.vjs-default-button'), 'click', Fn.bind(this, function() {
-      this.el().querySelector('.vjs-fg-color > select').selectedIndex = 0;
-      this.el().querySelector('.vjs-bg-color > select').selectedIndex = 0;
-      this.el().querySelector('.window-color > select').selectedIndex = 0;
-      this.el().querySelector('.vjs-text-opacity > select').selectedIndex = 0;
-      this.el().querySelector('.vjs-bg-opacity > select').selectedIndex = 0;
-      this.el().querySelector('.vjs-window-opacity > select').selectedIndex = 0;
-      this.el().querySelector('.vjs-edge-style select').selectedIndex = 0;
-      this.el().querySelector('.vjs-font-family select').selectedIndex = 0;
-      this.el().querySelector('.vjs-font-percent select').selectedIndex = 2;
+    Events.on(this.$('.vjs-default-button'), 'click', Fn.bind(this, function() {
+      this.$('.vjs-fg-color > select').selectedIndex = 0;
+      this.$('.vjs-bg-color > select').selectedIndex = 0;
+      this.$('.window-color > select').selectedIndex = 0;
+      this.$('.vjs-text-opacity > select').selectedIndex = 0;
+      this.$('.vjs-bg-opacity > select').selectedIndex = 0;
+      this.$('.vjs-window-opacity > select').selectedIndex = 0;
+      this.$('.vjs-edge-style select').selectedIndex = 0;
+      this.$('.vjs-font-family select').selectedIndex = 0;
+      this.$('.vjs-font-percent select').selectedIndex = 2;
       this.updateDisplay();
     }));
 
-    Events.on(this.el().querySelector('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay));
-    Events.on(this.el().querySelector('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay));
+    Events.on(this.$('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay));
 
     if (this.options_.persistTextTrackSettings) {
       this.restoreSettings();
@@ -74,7 +74,7 @@ class TextTrackSettings extends Component {
   }
 
   /**
-   * Get texttrack settings 
+   * Get texttrack settings
    * Settings are
    * .vjs-edge-style
    * .vjs-font-family
@@ -83,23 +83,21 @@ class TextTrackSettings extends Component {
    * .vjs-bg-color
    * .vjs-bg-opacity
    * .window-color
-   * .vjs-window-opacity 
+   * .vjs-window-opacity
    *
-   * @return {Object} 
+   * @return {Object}
    * @method getValues
    */
   getValues() {
-    const el = this.el();
-
-    const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select'));
-    const fontFamily = getSelectedOptionValue(el.querySelector('.vjs-font-family select'));
-    const fgColor = getSelectedOptionValue(el.querySelector('.vjs-fg-color > select'));
-    const textOpacity = getSelectedOptionValue(el.querySelector('.vjs-text-opacity > select'));
-    const bgColor = getSelectedOptionValue(el.querySelector('.vjs-bg-color > select'));
-    const bgOpacity = getSelectedOptionValue(el.querySelector('.vjs-bg-opacity > select'));
-    const windowColor = getSelectedOptionValue(el.querySelector('.window-color > select'));
-    const windowOpacity = getSelectedOptionValue(el.querySelector('.vjs-window-opacity > select'));
-    const fontPercent = window['parseFloat'](getSelectedOptionValue(el.querySelector('.vjs-font-percent > select')));
+    const textEdge = getSelectedOptionValue(this.$('.vjs-edge-style select'));
+    const fontFamily = getSelectedOptionValue(this.$('.vjs-font-family select'));
+    const fgColor = getSelectedOptionValue(this.$('.vjs-fg-color > select'));
+    const textOpacity = getSelectedOptionValue(this.$('.vjs-text-opacity > select'));
+    const bgColor = getSelectedOptionValue(this.$('.vjs-bg-color > select'));
+    const bgOpacity = getSelectedOptionValue(this.$('.vjs-bg-opacity > select'));
+    const windowColor = getSelectedOptionValue(this.$('.window-color > select'));
+    const windowOpacity = getSelectedOptionValue(this.$('.vjs-window-opacity > select'));
+    const fontPercent = window['parseFloat'](getSelectedOptionValue(this.$('.vjs-font-percent > select')));
 
     let result = {
       'backgroundOpacity': bgOpacity,
@@ -121,7 +119,7 @@ class TextTrackSettings extends Component {
   }
 
   /**
-   * Set texttrack settings 
+   * Set texttrack settings
    * Settings are
    * .vjs-edge-style
    * .vjs-font-family
@@ -130,22 +128,20 @@ class TextTrackSettings extends Component {
    * .vjs-bg-color
    * .vjs-bg-opacity
    * .window-color
-   * .vjs-window-opacity 
+   * .vjs-window-opacity
    *
    * @param {Object} values Object with texttrack setting values
    * @method setValues
    */
   setValues(values) {
-    const el = this.el();
-
-    setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle);
-    setSelectedOption(el.querySelector('.vjs-font-family select'), values.fontFamily);
-    setSelectedOption(el.querySelector('.vjs-fg-color > select'), values.color);
-    setSelectedOption(el.querySelector('.vjs-text-opacity > select'), values.textOpacity);
-    setSelectedOption(el.querySelector('.vjs-bg-color > select'), values.backgroundColor);
-    setSelectedOption(el.querySelector('.vjs-bg-opacity > select'), values.backgroundOpacity);
-    setSelectedOption(el.querySelector('.window-color > select'), values.windowColor);
-    setSelectedOption(el.querySelector('.vjs-window-opacity > select'), values.windowOpacity);
+    setSelectedOption(this.$('.vjs-edge-style select'), values.edgeStyle);
+    setSelectedOption(this.$('.vjs-font-family select'), values.fontFamily);
+    setSelectedOption(this.$('.vjs-fg-color > select'), values.color);
+    setSelectedOption(this.$('.vjs-text-opacity > select'), values.textOpacity);
+    setSelectedOption(this.$('.vjs-bg-color > select'), values.backgroundColor);
+    setSelectedOption(this.$('.vjs-bg-opacity > select'), values.backgroundOpacity);
+    setSelectedOption(this.$('.window-color > select'), values.windowColor);
+    setSelectedOption(this.$('.vjs-window-opacity > select'), values.windowOpacity);
 
     let fontPercent = values.fontPercent;
 
@@ -153,11 +149,11 @@ class TextTrackSettings extends Component {
       fontPercent = fontPercent.toFixed(2);
     }
 
-    setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
+    setSelectedOption(this.$('.vjs-font-percent > select'), fontPercent);
   }
 
   /**
-   * Restore texttrack settings 
+   * Restore texttrack settings
    *
    * @method restoreSettings
    */
@@ -174,7 +170,7 @@ class TextTrackSettings extends Component {
   }
 
   /**
-   * Save texttrack settings to local storage 
+   * Save texttrack settings to local storage
    *
    * @method saveSettings
    */
@@ -194,7 +190,7 @@ class TextTrackSettings extends Component {
   }
 
   /**
-   * Update display of texttrack settings  
+   * Update display of texttrack settings
    *
    * @method updateDisplay
    */
diff --git a/src/js/utils/dom.js b/src/js/utils/dom.js
index 8d5da2e..17122b2 100644
--- a/src/js/utils/dom.js
+++ b/src/js/utils/dom.js
@@ -8,6 +8,59 @@ import log from './log.js';
 import tsml from 'tsml';
 
 /**
+ * Detect if a value is a string with any non-whitespace characters.
+ *
+ * @param  {String} str
+ * @return {Boolean}
+ */
+function isNonBlankString(str) {
+  return typeof str === 'string' && /\S/.test(str);
+}
+
+/**
+ * Throws an error if the passed string has whitespace. This is used by
+ * class methods to be relatively consistent with the classList API.
+ *
+ * @param  {String} str
+ * @return {Boolean}
+ */
+function throwIfWhitespace(str) {
+  if (/\s/.test(str)) {
+    throw new Error('class has illegal whitespace characters');
+  }
+}
+
+/**
+ * Produce a regular expression for matching a class name.
+ *
+ * @param  {String} className
+ * @return {RegExp}
+ */
+function classRegExp(className) {
+  return new RegExp('(^|\\s)' + className + '($|\\s)');
+}
+
+/**
+ * Creates functions to query the DOM using a given method.
+ *
+ * @function createQuerier
+ * @private
+ * @param  {String} method
+ * @return {Function}
+ */
+function createQuerier(method) {
+  return function (selector, context) {
+    if (!isNonBlankString(selector)) {
+      return document[method](null);
+    }
+    if (isNonBlankString(context)) {
+      context = document.querySelector(context);
+    }
+    return (isEl(context) ? context : document)[method](selector);
+  };
+}
+
+/**
  * Shorthand for document.getElementById()
  * Also allows for CSS (jQuery) ID syntax. But nothing other than IDs.
  *
@@ -181,47 +234,99 @@ export function removeElData(el) {
 /**
  * Check if an element has a CSS class
  *
+ * @function hasElClass
  * @param {Element} element Element to check
  * @param {String} classToCheck Classname to check
- * @function hasElClass
  */
 export function hasElClass(element, classToCheck) {
-  return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1);
+  if (element.classList) {
+    return element.classList.contains(classToCheck);
+  } else {
+    throwIfWhitespace(classToCheck);
+    return classRegExp(classToCheck).test(element.className);
+  }
 }
 
 /**
  * Add a CSS class name to an element
  *
+ * @function addElClass
  * @param {Element} element    Element to add class name to
  * @param {String} classToAdd Classname to add
- * @function addElClass
  */
 export function addElClass(element, classToAdd) {
-  if (!hasElClass(element, classToAdd)) {
-    element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd;
+  if (element.classList) {
+    element.classList.add(classToAdd);
+
+  // Don't need to `throwIfWhitespace` here because `hasElClass` will do it
+  // in the case of classList not being supported.
+  } else if (!hasElClass(element, classToAdd)) {
+    element.className = (element.className + ' ' + classToAdd).trim();
   }
+
+  return element;
 }
 
 /**
  * Remove a CSS class name from an element
  *
+ * @function removeElClass
  * @param {Element} element    Element to remove from class name
  * @param {String} classToRemove Classname to remove
- * @function removeElClass
  */
 export function removeElClass(element, classToRemove) {
-  if (!hasElClass(element, classToRemove)) {return;}
+  if (element.classList) {
+    element.classList.remove(classToRemove);
+  } else {
+    throwIfWhitespace(classToRemove);
+    element.className = element.className.split(/\s+/).filter(function(c) {
+      return c !== classToRemove;
+    }).join(' ');
+  }
 
-  let classNames = element.className.split(' ');
+  return element;
+}
 
-  // no arr.indexOf in ie8, and we don't want to add a big shim
-  for (let i = classNames.length - 1; i >= 0; i--) {
-    if (classNames[i] === classToRemove) {
-      classNames.splice(i,1);
-    }
+/**
+ * Adds or removes a CSS class name on an element depending on an optional
+ * condition or the presence/absence of the class name.
+ *
+ * @function toggleElClass
+ * @param    {Element} element
+ * @param    {String} classToToggle
+ * @param    {Boolean|Function} [predicate]
+ *           Can be a function that returns a Boolean. If `true`, the class
+ *           will be added; if `false`, the class will be removed. If not
+ *           given, the class will be added if not present and vice versa.
+ */
+export function toggleElClass(element, classToToggle, predicate) {
+
+  // This CANNOT use `classList` internally because IE does not support the
+  // second parameter to the `classList.toggle()` method! Which is fine because
+  // `classList` will be used by the add/remove functions.
+  let has = hasElClass(element, classToToggle);
+
+  if (typeof predicate === 'function') {
+    predicate = predicate(element, classToToggle);
+  }
+
+  if (typeof predicate !== 'boolean') {
+    predicate = !has;
+  }
+
+  // If the necessary class operation matches the current state of the
+  // element, no action is required.
+  if (predicate === has) {
+    return;
+  }
+
+  if (predicate) {
+    addElClass(element, classToToggle);
+  } else {
+    removeElClass(element, classToToggle);
   }
 
-  element.className = classNames.join(' ');
+  return element;
 }
 
 /**
@@ -292,7 +397,7 @@ export function getElAttributes(tag) {
  * Attempt to block the ability to select text while dragging controls
  *
  * @return {Boolean}
- * @method blockTextSelection
+ * @function blockTextSelection
  */
 export function blockTextSelection() {
   document.body.focus();
@@ -305,7 +410,7 @@ export function blockTextSelection() {
  * Turn off text selection blocking
  *
  * @return {Boolean}
- * @method unblockTextSelection
+ * @function unblockTextSelection
  */
 export function unblockTextSelection() {
   document.onselectstart = function() {
@@ -318,9 +423,9 @@ export function unblockTextSelection() {
  * getBoundingClientRect technique from
  * John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
  *
+ * @function findElPosition
  * @param {Element} el Element from which to get offset
- * @return {Object=}
- * @method findElPosition
+ * @return {Object}
  */
 export function findElPosition(el) {
   let box;
@@ -359,10 +464,10 @@ export function findElPosition(el) {
  * Returns an object with x and y coordinates.
  * The base on the coordinates are the bottom left of the element.
  *
+ * @function getPointerPosition
  * @param {Element} el Element on which to get the pointer position on
  * @param {Event} event Event object
- * @return {Object=} position This object will have x and y coordinates corresponding to the mouse position
- * @metho getPointerPosition
+ * @return {Object} This object will have x and y coordinates corresponding to the mouse position
  */
 export function getPointerPosition(el, event) {
   let position = {};
@@ -389,8 +494,9 @@ export function getPointerPosition(el, event) {
 /**
  * Determines, via duck typing, whether or not a value is a DOM element.
  *
- * @param  {Mixed} value
- * @return {Boolean}
+ * @function isEl
+ * @param    {Mixed} value
+ * @return   {Boolean}
  */
 export function isEl(value) {
   return !!value && typeof value === 'object' && value.nodeType === 1;
@@ -427,18 +533,25 @@ export function emptyEl(el) {
  * from falling into the trap of simply writing to `innerHTML`, which is
  * an XSS concern.
  *
- * The content for an element can be passed in multiple types, whose
- * behavior is as follows:
+ * The content for an element can be passed in multiple types and
+ * combinations, whose behavior is as follows:
+ *
+ * - String
+ *   Normalized into a text node.
+ *
+ * - Element, TextNode
+ *   Passed through.
  *
- * - String: Normalized into a text node.
- * - Node: An Element or TextNode is passed through.
- * - Array: A one-dimensional array of strings, nodes, or functions (which
- *   return single strings or nodes).
- * - Function: If the sole argument, is expected to produce a string, node,
- *   or array.
+ * - Array
+ *   A one-dimensional array of strings, elements, nodes, or functions (which
+ *   return single strings, elements, or nodes).
+ *
+ * - Function
+ *   If the sole argument, is expected to produce a string, element,
+ *   node, or array.
  *
  * @function normalizeContent
- * @param    {String|Element|Array|Function} content
+ * @param    {String|Element|TextNode|Array|Function} content
  * @return   {Array}
  */
 export function normalizeContent(content) {
@@ -474,7 +587,8 @@ export function normalizeContent(content) {
  *
  * @function appendContent
  * @param    {Element} el
- * @param    {String|Element|Array|Function} content
+ * @param    {String|Element|TextNode|Array|Function} content
+ *           See: `normalizeContent`
  * @return   {Element}
  */
 export function appendContent(el, content) {
@@ -488,9 +602,46 @@ export function appendContent(el, content) {
  *
  * @function insertContent
  * @param    {Element} el
- * @param    {String|Element|Array|Function} content
+ * @param    {String|Element|TextNode|Array|Function} content
+ *           See: `normalizeContent`
  * @return   {Element}
  */
 export function insertContent(el, content) {
   return appendContent(emptyEl(el), content);
 }
+
+/**
+ * Finds a single DOM element matching `selector` within the optional
+ * `context` of another DOM element (defaulting to `document`).
+ *
+ * @function $
+ * @param    {String} selector
+ *           A valid CSS selector, which will be passed to `querySelector`.
+ *
+ * @param    {Element|String} [context=document]
+ *           A DOM element within which to query. Can also be a selector
+ *           string in which case the first matching element will be used
+ *           as context. If missing (or no element matches selector), falls
+ *           back to `document`.
+ *
+ * @return   {Element|null}
+ */
+export const $ = createQuerier('querySelector');
+
+/**
+ * Finds a all DOM elements matching `selector` within the optional
+ * `context` of another DOM element (defaulting to `document`).
+ *
+ * @function $$
+ * @param    {String} selector
+ *           A valid CSS selector, which will be passed to `querySelectorAll`.
+ *
+ * @param    {Element|String} [context=document]
+ *           A DOM element within which to query. Can also be a selector
+ *           string in which case the first matching element will be used
+ *           as context. If missing (or no element matches selector), falls
+ *           back to `document`.
+ *
+ * @return   {NodeList}
+ */
+export const $$ = createQuerier('querySelectorAll');
diff --git a/src/js/video.js b/src/js/video.js
index a706b0a..101b960 100644
--- a/src/js/video.js
+++ b/src/js/video.js
@@ -99,10 +99,10 @@ var videojs = function(id, options, ready){
 };
 
 // Add default styles
-let style = document.querySelector('.vjs-styles-defaults');
+let style = Dom.$('.vjs-styles-defaults');
 if (!style) {
   style = stylesheet.createStyleElement('vjs-styles-defaults');
-  let head = document.querySelector('head');
+  let head = Dom.$('head');
   head.insertBefore(style, head.firstChild);
   stylesheet.setTextContent(style, `
     .video-js {
@@ -545,22 +545,149 @@ videojs.xhr = xhr;
  */
 videojs.TextTrack = TextTrack;
 
-// REMOVING: We probably should add this to the migration plugin
-// // Expose but deprecate the window[componentName] method for accessing components
-// Object.getOwnPropertyNames(Component.components).forEach(function(name){
-//   let component = Component.components[name];
-//
-//   // A deprecation warning as the constuctor
-//   module.exports[name] = function(player, options, ready){
-//     log.warn('Using videojs.'+name+' to access the '+name+' component has been deprecated. Please use videojs.getComponent("componentName")');
-//
-//     return new Component(player, options, ready);
-//   };
-//
-//   // Allow the prototype and class methods to be accessible still this way
-//   // Though anything that attempts to override class methods will no longer work
-//   assign(module.exports[name], component);
-// });
+/**
+ * Determines, via duck typing, whether or not a value is a DOM element.
+ *
+ * @method isEl
+ * @param  {Mixed} value
+ * @return {Boolean}
+ */
+videojs.isEl = Dom.isEl;
+
+/**
+ * Determines, via duck typing, whether or not a value is a text node.
+ *
+ * @method isTextNode
+ * @param  {Mixed} value
+ * @return {Boolean}
+ */
+videojs.isTextNode = Dom.isTextNode;
+
+/**
+ * Check if an element has a CSS class
+ *
+ * @method hasClass
+ * @param {Element} element Element to check
+ * @param {String} classToCheck Classname to check
+ */
+videojs.hasClass = Dom.hasElClass;
+
+/**
+ * Add a CSS class name to an element
+ *
+ * @method addClass
+ * @param {Element} element    Element to add class name to
+ * @param {String} classToAdd Classname to add
+ */
+videojs.addClass = Dom.addElClass;
+
+/**
+ * Remove a CSS class name from an element
+ *
+ * @method removeClass
+ * @param {Element} element    Element to remove from class name
+ * @param {String} classToRemove Classname to remove
+ */
+videojs.removeClass = Dom.removeElClass;
+
+/**
+ * Adds or removes a CSS class name on an element depending on an optional
+ * condition or the presence/absence of the class name.
+ *
+ * @method toggleElClass
+ * @param  {Element} element
+ * @param  {String} classToToggle
+ * @param  {Boolean|Function} [predicate]
+ *         Can be a function that returns a Boolean. If `true`, the class
+ *         will be added; if `false`, the class will be removed. If not
+ *         given, the class will be added if not present and vice versa.
+ */
+videojs.toggleClass = Dom.toggleElClass;
+
+/**
+ * Apply attributes to an HTML element.
+ *
+ * @method setAttributes
+ * @param  {Element} el         Target element.
+ * @param  {Object=} attributes Element attributes to be applied.
+ */
+videojs.setAttributes = Dom.setElAttributes;
+
+/**
+ * Get an element's attribute values, as defined on the HTML tag
+ * Attributes are not the same as properties. They're defined on the tag
+ * or with setAttribute (which shouldn't be used with HTML)
+ * This will return true or false for boolean attributes.
+ *
+ * @method getAttributes
+ * @param  {Element} tag Element from which to get tag attributes
+ * @return {Object}
+ */
+videojs.getAttributes = Dom.getElAttributes;
+
+/**
+ * Empties the contents of an element.
+ *
+ * @method emptyEl
+ * @param  {Element} el
+ * @return {Element}
+ */
+videojs.emptyEl = Dom.emptyEl;
+
+/**
+ * Normalizes and appends content to an element.
+ *
+ * The content for an element can be passed in multiple types and
+ * combinations, whose behavior is as follows:
+ *
+ * - String
+ *   Normalized into a text node.
+ *
+ * - Element, TextNode
+ *   Passed through.
+ *
+ * - Array
+ *   A one-dimensional array of strings, elements, nodes, or functions (which
+ *   return single strings, elements, or nodes).
+ *
+ * - Function
+ *   If the sole argument, is expected to produce a string, element,
+ *   node, or array.
+ *
+ * @method appendContent
+ * @param  {Element} el
+ * @param  {String|Element|TextNode|Array|Function} content
+ * @return {Element}
+ */
+videojs.appendContent = Dom.appendContent;
+
+/**
+ * Normalizes and inserts content into an element; this is identical to
+ * `appendContent()`, except it empties the element first.
+ *
+ * The content for an element can be passed in multiple types and
+ * combinations, whose behavior is as follows:
+ *
+ * - String
+ *   Normalized into a text node.
+ *
+ * - Element, TextNode
+ *   Passed through.
+ *
+ * - Array
+ *   A one-dimensional array of strings, elements, nodes, or functions (which
+ *   return single strings, elements, or nodes).
+ *
+ * - Function
+ *   If the sole argument, is expected to produce a string, element,
+ *   node, or array.
+ *
+ * @method insertContent
+ * @param  {Element} el
+ * @param  {String|Element|TextNode|Array|Function} content
+ * @return {Element}
+ */
+videojs.insertContent = Dom.insertContent;
 
 /*
  * Custom Universal Module Definition (UMD)
diff --git a/test/unit/component.test.js b/test/unit/component.test.js
index d2c65e3..8315128 100644
--- a/test/unit/component.test.js
+++ b/test/unit/component.test.js
@@ -471,6 +471,10 @@ test('should add and remove a CSS class', function(){
   ok(comp.el().className.indexOf('test-class') !== -1);
   comp.removeClass('test-class');
   ok(comp.el().className.indexOf('test-class') === -1);
+  comp.toggleClass('test-class');
+  ok(comp.el().className.indexOf('test-class') !== -1);
+  comp.toggleClass('test-class');
+  ok(comp.el().className.indexOf('test-class') === -1);
 });
 
 test('should show and hide an element', function(){
@@ -695,3 +699,18 @@ test('should provide interval methods that automatically get cleared on componen
 
   ok(intervalsFired === 5, 'Interval was cleared when component was disposed');
 });
+
+test('$ and $$ functions', function() {
+  var comp = new Component(getFakePlayer());
+  var contentEl = document.createElement('div');
+  var children = [
+    document.createElement('div'),
+    document.createElement('div')
+  ];
+
+  comp.contentEl_ = contentEl;
+  children.forEach(child => contentEl.appendChild(child));
+
+  strictEqual(comp.$('div'), children[0], '$ defaults to contentEl as scope');
+  strictEqual(comp.$$('div').length, children.length, '$$ defaults to contentEl as scope');
+});
diff --git a/test/unit/tracks/text-track-settings.test.js b/test/unit/tracks/text-track-settings.test.js
index 593b4e1..d0e16cd 100644
--- a/test/unit/tracks/text-track-settings.test.js
+++ b/test/unit/tracks/text-track-settings.test.js
@@ -35,17 +35,17 @@ test('should update settings', function() {
   player.textTrackSettings.setValues(newSettings);
   deepEqual(player.textTrackSettings.getValues(), newSettings, 'values are updated');
 
-  equal(player.el().querySelector('.vjs-fg-color > select').selectedIndex, 1, 'fg-color is set to new value');
-  equal(player.el().querySelector('.vjs-bg-color > select').selectedIndex, 1, 'bg-color is set to new value');
-  equal(player.el().querySelector('.window-color > select').selectedIndex, 1, 'window-color is set to new value');
-  equal(player.el().querySelector('.vjs-text-opacity > select').selectedIndex, 1, 'text-opacity is set to new value');
-  equal(player.el().querySelector('.vjs-bg-opacity > select').selectedIndex, 1, 'bg-opacity is set to new value');
-  equal(player.el().querySelector('.vjs-window-opacity > select').selectedIndex, 1, 'window-opacity is set to new value');
-  equal(player.el().querySelector('.vjs-edge-style select').selectedIndex, 1, 'edge-style is set to new value');
-  equal(player.el().querySelector('.vjs-font-family select').selectedIndex, 1, 'font-family is set to new value');
-  equal(player.el().querySelector('.vjs-font-percent select').selectedIndex, 3, 'font-percent is set to new value');
-
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+  equal(player.$('.vjs-fg-color > select').selectedIndex, 1, 'fg-color is set to new value');
+  equal(player.$('.vjs-bg-color > select').selectedIndex, 1, 'bg-color is set to new value');
+  equal(player.$('.window-color > select').selectedIndex, 1, 'window-color is set to new value');
+  equal(player.$('.vjs-text-opacity > select').selectedIndex, 1, 'text-opacity is set to new value');
+  equal(player.$('.vjs-bg-opacity > select').selectedIndex, 1, 'bg-opacity is set to new value');
+  equal(player.$('.vjs-window-opacity > select').selectedIndex, 1, 'window-opacity is set to new value');
+  equal(player.$('.vjs-edge-style select').selectedIndex, 1, 'edge-style is set to new value');
+  equal(player.$('.vjs-font-family select').selectedIndex, 1, 'font-family is set to new value');
+  equal(player.$('.vjs-font-percent select').selectedIndex, 3, 'font-percent is set to new value');
+
+  Events.trigger(player.$('.vjs-done-button'), 'click');
   deepEqual(safeParseTuple(window.localStorage.getItem('vjs-text-track-settings'))[1], newSettings, 'values are saved');
 
   player.dispose();
@@ -57,32 +57,32 @@ test('should restore default settings', function() {
     persistTextTrackSettings: true
   });
 
-  player.el().querySelector('.vjs-fg-color > select').selectedIndex = 1;
-  player.el().querySelector('.vjs-bg-color > select').selectedIndex = 1;
-  player.el().querySelector('.window-color > select').selectedIndex = 1;
-  player.el().querySelector('.vjs-text-opacity > select').selectedIndex = 1;
-  player.el().querySelector('.vjs-bg-opacity > select').selectedIndex = 1;
-  player.el().querySelector('.vjs-window-opacity > select').selectedIndex = 1;
-  player.el().querySelector('.vjs-edge-style select').selectedIndex = 1;
-  player.el().querySelector('.vjs-font-family select').selectedIndex = 1;
-  player.el().querySelector('.vjs-font-percent select').selectedIndex = 3;
+  player.$('.vjs-fg-color > select').selectedIndex = 1;
+  player.$('.vjs-bg-color > select').selectedIndex = 1;
+  player.$('.window-color > select').selectedIndex = 1;
+  player.$('.vjs-text-opacity > select').selectedIndex = 1;
+  player.$('.vjs-bg-opacity > select').selectedIndex = 1;
+  player.$('.vjs-window-opacity > select').selectedIndex = 1;
+  player.$('.vjs-edge-style select').selectedIndex = 1;
+  player.$('.vjs-font-family select').selectedIndex = 1;
+  player.$('.vjs-font-percent select').selectedIndex = 3;
 
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
-  Events.trigger(player.el().querySelector('.vjs-default-button'), 'click');
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+  Events.trigger(player.$('.vjs-done-button'), 'click');
+  Events.trigger(player.$('.vjs-default-button'), 'click');
+  Events.trigger(player.$('.vjs-done-button'), 'click');
 
   deepEqual(player.textTrackSettings.getValues(), {}, 'values are defaulted');
   deepEqual(window.localStorage.getItem('vjs-text-track-settings'), null, 'values are saved');
 
-  equal(player.el().querySelector('.vjs-fg-color > select').selectedIndex, 0, 'fg-color is set to default value');
-  equal(player.el().querySelector('.vjs-bg-color > select').selectedIndex, 0, 'bg-color is set to default value');
-  equal(player.el().querySelector('.window-color > select').selectedIndex, 0, 'window-color is set to default value');
-  equal(player.el().querySelector('.vjs-text-opacity > select').selectedIndex, 0, 'text-opacity is set to default value');
-  equal(player.el().querySelector('.vjs-bg-opacity > select').selectedIndex, 0, 'bg-opacity is set to default value');
-  equal(player.el().querySelector('.vjs-window-opacity > select').selectedIndex, 0, 'window-opacity is set to default value');
-  equal(player.el().querySelector('.vjs-edge-style select').selectedIndex, 0, 'edge-style is set to default value');
-  equal(player.el().querySelector('.vjs-font-family select').selectedIndex, 0, 'font-family is set to default value');
-  equal(player.el().querySelector('.vjs-font-percent select').selectedIndex, 2, 'font-percent is set to default value');
+  equal(player.$('.vjs-fg-color > select').selectedIndex, 0, 'fg-color is set to default value');
+  equal(player.$('.vjs-bg-color > select').selectedIndex, 0, 'bg-color is set to default value');
+  equal(player.$('.window-color > select').selectedIndex, 0, 'window-color is set to default value');
+  equal(player.$('.vjs-text-opacity > select').selectedIndex, 0, 'text-opacity is set to default value');
+  equal(player.$('.vjs-bg-opacity > select').selectedIndex, 0, 'bg-opacity is set to default value');
+  equal(player.$('.vjs-window-opacity > select').selectedIndex, 0, 'window-opacity is set to default value');
+  equal(player.$('.vjs-edge-style select').selectedIndex, 0, 'edge-style is set to default value');
+  equal(player.$('.vjs-font-family select').selectedIndex, 0, 'font-family is set to default value');
+  equal(player.$('.vjs-font-percent select').selectedIndex, 2, 'font-percent is set to default value');
 
   player.dispose();
 });
@@ -91,7 +91,7 @@ test('should open on click', function() {
   var player = TestHelpers.makePlayer({
     tracks: tracks
   });
-  Events.trigger(player.el().querySelector('.vjs-texttrack-settings'), 'click');
+  Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
   ok(!player.textTrackSettings.hasClass('vjs-hidden'), 'settings open');
 
   player.dispose();
@@ -101,8 +101,8 @@ test('should close on done click', function() {
   var player = TestHelpers.makePlayer({
     tracks: tracks
   });
-  Events.trigger(player.el().querySelector('.vjs-texttrack-settings'), 'click');
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+  Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
+  Events.trigger(player.$('.vjs-done-button'), 'click');
   ok(player.textTrackSettings.hasClass('vjs-hidden'), 'settings closed');
 
   player.dispose();
@@ -141,7 +141,7 @@ test('if persist option is set, save settings when "done"', function() {
     save++;
   };
 
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+  Events.trigger(player.$('.vjs-done-button'), 'click');
 
   equal(save, 1, 'save was called');
 
@@ -171,7 +171,7 @@ test('do not try to restore or save settings if persist option is not set', func
 
   equal(restore, 0, 'restore was not called');
 
-  Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+  Events.trigger(player.$('.vjs-done-button'), 'click');
 
   // saveSettings is called but does nothing
   equal(save, 1, 'save was not called');
diff --git a/test/unit/utils/dom.test.js b/test/unit/utils/dom.test.js
index 0c1d82b..5d549ab 100644
--- a/test/unit/utils/dom.test.js
+++ b/test/unit/utils/dom.test.js
@@ -62,23 +62,164 @@ test('should get and remove data from an element', function(){
   ok(!Dom.hasElData(el), 'cached item emptied');
 });
 
-test('should add and remove a class name on an element', function(){
+test('addElClass()', function(){
   var el = document.createElement('div');
+
+  expect(5);
+
   Dom.addElClass(el, 'test-class');
-  ok(el.className === 'test-class', 'class added');
+  strictEqual(el.className, 'test-class', 'adds a single class');
+
   Dom.addElClass(el, 'test-class');
-  ok(el.className === 'test-class', 'same class not duplicated');
-  Dom.addElClass(el, 'test-class2');
-  ok(el.className === 'test-class test-class2', 'added second class');
+  strictEqual(el.className, 'test-class', 'does not duplicate classes');
+
+  throws(function(){
+    Dom.addElClass(el, 'foo foo-bar');
+  }, 'throws when attempting to add a class with whitespace');
+
+  Dom.addElClass(el, 'test2_className');
+  strictEqual(el.className, 'test-class test2_className', 'adds second class');
+
+  Dom.addElClass(el, 'FOO');
+  strictEqual(el.className, 'test-class test2_className FOO', 'adds third class');
+});
+
+test('removeElClass()', function() {
+  var el = document.createElement('div');
+
+  el.className = 'test-class foo foo test2_className FOO bar';
+
+  expect(5);
+
   Dom.removeElClass(el, 'test-class');
-  ok(el.className === 'test-class2', 'removed first class');
+  strictEqual(el.className, 'foo foo test2_className FOO bar', 'removes one class');
+
+  Dom.removeElClass(el, 'foo');
+  strictEqual(el.className, 'test2_className FOO bar', 'removes all instances of a class');
+
+  throws(function(){
+    Dom.removeElClass(el, 'test2_className bar');
+  }, 'throws when attempting to remove a class with whitespace');
+
+  Dom.removeElClass(el, 'test2_className');
+  strictEqual(el.className, 'FOO bar', 'removes another class');
+
+  Dom.removeElClass(el, 'FOO');
+  strictEqual(el.className, 'bar', 'removes another class');
 });
 
-test('should read class names on an element', function(){
+test('hasElClass()', function(){
   var el = document.createElement('div');
-  Dom.addElClass(el, 'test-class1');
-  ok(Dom.hasElClass(el, 'test-class1') === true, 'class detected');
-  ok(Dom.hasElClass(el, 'test-class') === false, 'substring correctly not detected');
+
+  el.className = 'test-class foo foo test2_className FOO bar';
+
+  strictEqual(Dom.hasElClass(el, 'test-class'), true, 'class detected');
+  strictEqual(Dom.hasElClass(el, 'foo'), true, 'class detected');
+  strictEqual(Dom.hasElClass(el, 'test2_className'), true, 'class detected');
+  strictEqual(Dom.hasElClass(el, 'FOO'), true, 'class detected');
+  strictEqual(Dom.hasElClass(el, 'bar'), true, 'class detected');
+  strictEqual(Dom.hasElClass(el, 'test2'), false, 'valid substring - but not a class - correctly not detected');
+  strictEqual(Dom.hasElClass(el, 'className'), false, 'valid substring - but not a class - correctly not detected');
+
+  throws(function(){
+    Dom.hasElClass(el, 'FOO bar');
+  }, 'throws when attempting to detect a class with whitespace');
+});
+
+test('toggleElClass()', function() {
+  let el = Dom.createEl('div', {className: 'foo bar'});
+
+  let predicateToggles = [
+    {
+      toggle: 'foo',
+      predicate: true,
+      className: 'foo bar',
+      message: 'if predicate `true` matches state of the element, do nothing'
+    },
+    {
+      toggle: 'baz',
+      predicate: false,
+      className: 'foo bar',
+      message: 'if predicate `false` matches state of the element, do nothing'
+    },
+    {
+      toggle: 'baz',
+      predicate: true,
+      className: 'foo bar baz',
+      message: 'if predicate `true` differs from state of the element, add the class'
+    },
+    {
+      toggle: 'foo',
+      predicate: false,
+      className: 'bar baz',
+      message: 'if predicate `false` differs from state of the element, remove the class'
+    },
+    {
+      toggle: 'bar',
+      predicate: () => true,
+      className: 'bar baz',
+      message: 'if a predicate function returns `true`, matching the state of the element, do nothing'
+    },
+    {
+      toggle: 'foo',
+      predicate: () => false,
+      className: 'bar baz',
+      message: 'if a predicate function returns `false`, matching the state of the element, do nothing'
+    },
+    {
+      toggle: 'foo',
+      predicate: () => true,
+      className: 'bar baz foo',
+      message: 'if a predicate function returns `true`, differing from state of the element, add the class'
+    },
+    {
+      toggle: 'foo',
+      predicate: () => false,
+      className: 'bar baz',
+      message: 'if a predicate function returns `false`, differing from state of the element, remove the class'
+    },
+    {
+      toggle: 'foo',
+      predicate: Function.prototype,
+      className: 'bar baz foo',
+      message: 'if a predicate function returns `undefined` and the element does not have the class, add the class'
+    },
+    {
+      toggle: 'bar',
+      predicate: Function.prototype,
+      className: 'baz foo',
+      message: 'if a predicate function returns `undefined` and the element has the class, remove the class'
+    },
+    {
+      toggle: 'bar',
+      predicate: () => [],
+      className: 'baz foo bar',
+      message: 'if a predicate function returns a defined non-boolean value and the element does not have the class, add the class'
+    },
+    {
+      toggle: 'baz',
+      predicate: () => 'this is incorrect',
+      className: 'foo bar',
+      message: 'if a predicate function returns a defined non-boolean value and the element has the class, remove the class'
+    },
+  ];
+
+  expect(3 + predicateToggles.length);
+
+  Dom.toggleElClass(el, 'bar');
+  strictEqual(el.className, 'foo', 'toggles a class off, if present');
+
+  Dom.toggleElClass(el, 'bar');
+  strictEqual(el.className, 'foo bar', 'toggles a class on, if absent');
+
+  throws(function(){
+    Dom.toggleElClass(el, 'foo bar');
+  }, 'throws when attempting to toggle a class with whitespace');
+
+  predicateToggles.forEach(x => {
+    Dom.toggleElClass(el, x.toggle, x.predicate);
+    strictEqual(el.className, x.className, x.message);
+  });
 });
 
 test('should set element attributes from object', function(){
@@ -293,4 +434,37 @@ test('Dom.appendContent', function(assert) {
   assert.strictEqual(el.firstChild.nextSibling, p2, 'the second paragraph was appended');
 });
 
+test('$() and $$()', function() {
+  let fixture = document.getElementById('qunit-fixture');
+  let container = document.createElement('div');
+  let children = [
+    document.createElement('div'),
+    document.createElement('div'),
+    document.createElement('div'),
+  ];
 
+  children.forEach(child => container.appendChild(child));
+  fixture.appendChild(container);
+
+  let totalDivCount = document.getElementsByTagName('div').length;
+
+  expect(12);
+
+  strictEqual(Dom.$('#qunit-fixture'), fixture, 'can find an element in the document context');
+  strictEqual(Dom.$$('div').length, totalDivCount, 'finds elements in the document context');
+
+  strictEqual(Dom.$('div', container), children[0], 'can find an element in a DOM element context');
+  strictEqual(Dom.$$('div', container).length, children.length, 'finds elements in a DOM element context');
+
+  strictEqual(Dom.$('#qunit-fixture', document.querySelector('unknown')), fixture, 'falls back to document given a bad context element');
+  strictEqual(Dom.$$('div', document.querySelector('unknown')).length, totalDivCount, 'falls back to document given a bad context element');
+
+  strictEqual(Dom.$('#qunit-fixture', 'body'), fixture, 'can find an element in a selector context');
+  strictEqual(Dom.$$('div', '#qunit-fixture').length, 1 + children.length, 'finds elements in a selector context');
+
+  strictEqual(Dom.$('#qunit-fixture', 'unknown'), fixture, 'falls back to document given a bad context selector');
+  strictEqual(Dom.$$('div', 'unknown').length, totalDivCount, 'falls back to document given a bad context selector');
+
+  strictEqual(Dom.$('div', children[0]), null, 'returns null for missing elements');
+  strictEqual(Dom.$$('div', children[0]).length, 0, 'returns 0 for missing elements');
+});
diff --git a/test/unit/video.test.js b/test/unit/video.test.js
index c0994be..3025474 100644
--- a/test/unit/video.test.js
+++ b/test/unit/video.test.js
@@ -1,6 +1,7 @@
 import videojs from '../../src/js/video.js';
 import TestHelpers from './test-helpers.js';
 import Player from '../../src/js/player.js';
+import * as Dom from '../../src/js/utils/dom.js';
 import log from '../../src/js/utils/log.js';
 import document from 'global/document';
 
@@ -78,3 +79,29 @@ test('should expose options and players properties for backward-compatibility',
   ok(typeof videojs.options, 'object', 'options should be an object');
   ok(typeof videojs.players, 'object', 'players should be an object');
 });
+
+test('should expose DOM functions', function() {
+
+  // Keys are videojs methods, values are Dom methods.
+  let methods = {
+    isEl: 'isEl',
+    isTextNode: 'isTextNode',
+    hasClass: 'hasElClass',
+    addClass: 'addElClass',
+    removeClass: 'removeElClass',
+    toggleClass: 'toggleElClass',
+    setAttributes: 'setElAttributes',
+    getAttributes: 'getElAttributes',
+    emptyEl: 'emptyEl',
+    insertContent: 'insertContent',
+    appendContent: 'appendContent'
+  };
+
+  let keys = Object.keys(methods);
+
+  expect(keys.length);
+  keys.forEach(function(vjsName) {
+    let domName = methods[vjsName];
+    strictEqual(videojs[vjsName], Dom[domName], `videojs.${vjsName} is a reference to Dom.${domName}`);
+  });
+});
