aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lib
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lib')
-rw-r--r--tests/lib/bootstrap.js4
-rw-r--r--tests/lib/qunit-assert-domequal.js23
-rw-r--r--tests/lib/vendor/qunit-assert-classes/LICENSE.txt22
-rw-r--r--tests/lib/vendor/qunit-assert-classes/qunit-assert-classes.js245
-rw-r--r--tests/lib/vendor/qunit-assert-close/MIT-LICENSE.txt21
-rw-r--r--tests/lib/vendor/qunit-assert-close/qunit-assert-close.js209
-rw-r--r--tests/lib/vendor/qunit-composite/LICENSE.txt36
-rw-r--r--tests/lib/vendor/qunit-composite/qunit-composite.css47
-rw-r--r--tests/lib/vendor/qunit-composite/qunit-composite.js236
9 files changed, 834 insertions, 9 deletions
diff --git a/tests/lib/bootstrap.js b/tests/lib/bootstrap.js
index 277fecca7..a5bb1711b 100644
--- a/tests/lib/bootstrap.js
+++ b/tests/lib/bootstrap.js
@@ -12,8 +12,8 @@ requirejs.config( {
"jquery-simulate": "../../../external/jquery-simulate/jquery.simulate",
"lib": "../../lib",
"phantom-bridge": "../../../node_modules/grunt-contrib-qunit/phantomjs/bridge",
- "qunit-assert-classes": "../../../external/qunit-assert-classes/qunit-assert-classes",
- "qunit-assert-close": "../../../external/qunit-assert-close/qunit-assert-close",
+ "qunit-assert-classes": "../../lib/vendor/qunit-assert-classes/qunit-assert-classes",
+ "qunit-assert-close": "../../lib/vendor/qunit-assert-close/qunit-assert-close",
"qunit": "../../../external/qunit/qunit",
"testswarm": "https://swarm.jquery.org/js/inject.js?" + ( new Date() ).getTime(),
"ui": "../../../ui"
diff --git a/tests/lib/qunit-assert-domequal.js b/tests/lib/qunit-assert-domequal.js
index bdad9844c..066eb203b 100644
--- a/tests/lib/qunit-assert-domequal.js
+++ b/tests/lib/qunit-assert-domequal.js
@@ -15,11 +15,16 @@ var domEqual = QUnit.assert.domEqual = function( selector, modifier, message ) {
var assert = this;
// Get current state prior to modifier
- var expected = extract( selector, message );
+ var expected = extract( assert, selector, message );
function done() {
- var actual = extract( selector, message );
- assert.push( QUnit.equiv( actual, expected ), actual, expected, message );
+ var actual = extract( assert, selector, message );
+ assert.pushResult( {
+ result: QUnit.equiv( actual, expected ),
+ actual: actual,
+ expected: expected,
+ message: message
+ } );
}
// Run modifier (async or sync), then compare state via done()
@@ -116,11 +121,15 @@ function jQueryVersionSince( version ) {
return compareVersions( $.fn.jquery, version ) >= 0;
}
-function extract( selector, message ) {
+function extract( assert, selector, message ) {
var elem = $( selector );
if ( !elem.length ) {
- QUnit.push( false, null, null,
- "domEqual failed, can't extract " + selector + ", message was: " + message );
+ assert.pushResult( {
+ result: false,
+ actual: null,
+ expected: null,
+ message: "domEqual failed, can't extract " + selector + ", message was: " + message
+ } );
return;
}
@@ -190,7 +199,7 @@ function extract( selector, message ) {
children = elem.children();
if ( children.length ) {
result.children = elem.children().map( function() {
- return extract( $( this ) );
+ return extract( assert, $( this ) );
} ).get();
} else {
result.text = elem.text();
diff --git a/tests/lib/vendor/qunit-assert-classes/LICENSE.txt b/tests/lib/vendor/qunit-assert-classes/LICENSE.txt
new file mode 100644
index 000000000..938db0368
--- /dev/null
+++ b/tests/lib/vendor/qunit-assert-classes/LICENSE.txt
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Alexander Schmitz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/tests/lib/vendor/qunit-assert-classes/qunit-assert-classes.js b/tests/lib/vendor/qunit-assert-classes/qunit-assert-classes.js
new file mode 100644
index 000000000..3461f1d90
--- /dev/null
+++ b/tests/lib/vendor/qunit-assert-classes/qunit-assert-classes.js
@@ -0,0 +1,245 @@
+// With custom modifications - all are marked with
+// a "Custom modification" comment.
+( function( factory ) {
+ if ( typeof define === "function" && define.amd ) {
+
+ // AMD. Register as an anonymous module.
+ define( [
+ "qunit"
+ ], factory );
+ } else {
+
+ // Browser globals
+ factory( QUnit );
+ }
+}( function( QUnit ) {
+
+ function inArray( haystack, needle ) {
+ for ( var i = 0; i < haystack.length; i++ ) {
+ if (
+ ( needle instanceof RegExp && needle.test( haystack[ i ] ) )||
+ ( typeof needle === "string" && haystack[ i ] === needle )
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function check( element, search ) {
+ var i, classAttribute, elementClassArray,
+ missing = [],
+ found = [];
+
+ if ( element.jquery && element.length !== 1 ) {
+ throw new Error( "Class checks can only be performed on a single element on a collection" );
+ }
+
+ element = element.jquery ? element[ 0 ] : element;
+ classAttribute = element.getAttribute( "class" );
+
+ if ( classAttribute ) {
+ elementClassArray = splitClasses( classAttribute );
+ if ( search instanceof RegExp ) {
+ if ( inArray( elementClassArray, search ) ) {
+ found.push( search );
+ } else {
+ missing.push( search );
+ }
+ } else {
+ for( i = 0; i < search.length; i++ ) {
+ if ( !inArray( elementClassArray, search[ i ] ) ) {
+ missing.push( search[ i ] );
+ } else {
+ found.push( search[ i ] );
+ }
+ }
+ }
+ } else {
+ missing = search;
+ }
+
+ return {
+ missing: missing,
+ found: found,
+ element: element,
+ classAttribute: classAttribute
+ };
+ }
+
+ function splitClasses( classes ) {
+ return classes.match( /\S+/g ) || [];
+ }
+
+ function pluralize( message, classes ) {
+ return message + ( classes.length > 1 ? "es" : "" );
+ }
+
+ // Custom modification: removing QUnit.extend
+ var key;
+ var qunitAssertExtensions = {
+ hasClasses: function( element, classes, message ) {
+ var classArray = splitClasses( classes ),
+ results = check( element, classArray );
+
+ message = message || pluralize( "Element must have class", classArray );
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: !results.missing.length,
+ actual: results.found.join( " " ),
+ expected: classes,
+ message: message
+ } );
+ },
+ lacksClasses: function( element, classes, message ) {
+ var classArray = splitClasses( classes ),
+ results = check( element, classArray );
+
+ message = message || pluralize( "Element must not have class", classArray );
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: !results.found.length,
+ actual: results.found.join( " " ),
+ expected: classes,
+ message: message
+ } );
+ },
+ hasClassesStrict: function( element, classes, message ) {
+ var result,
+ classArray = splitClasses( classes ),
+ results = check( element, classArray );
+
+ message = message || pluralize( "Element must only have class", classArray );
+
+ result = !results.missing.length && results.element.getAttribute( "class" ) &&
+ splitClasses( results.element.getAttribute( "class" ) ).length ===
+ results.found.length;
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: result,
+ actual: results.found.join( " " ),
+ expected: classes,
+ message: message
+ } );
+ },
+ hasClassRegex: function( element, regex, message ) {
+ var results = check( element, regex );
+
+ message = message || "Element must have class matching " + regex;
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: !!results.found.length,
+ actual: results.found.join( " " ),
+ expected: regex,
+ message: message
+ } );
+ },
+ lacksClassRegex: function( element, regex, message ) {
+ var results = check( element, regex );
+
+ message = message || "Element must not have class matching " + regex;
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: results.missing.length,
+ actual: results.missing.join( " " ),
+ expected: regex,
+ message: message
+ } );
+ },
+ hasClassStart: function( element, partialClass, message ) {
+ var results = check( element, new RegExp( "^" + partialClass ) );
+
+ message = message || "Element must have class starting with " + partialClass;
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: results.found.length,
+ actual: results.found.join( " " ),
+ expected: partialClass,
+ message: message
+ } );
+ },
+ lacksClassStart: function( element, partialClass, message ) {
+ var results = check( element, new RegExp( "^" + partialClass ) );
+
+ message = message || "Element must not have class starting with " + partialClass;
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: results.missing.length,
+ actual: results.missing.join( " " ),
+ expected: partialClass,
+ message: message
+ } );
+ },
+ hasClassPartial: function( element, partialClass, message ) {
+ var results = check( element, new RegExp( partialClass ) );
+
+ message = message || "Element must have class containing '" + partialClass + "'";
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: results.found.length,
+ actual: results.found.join( " " ),
+ expected: partialClass,
+ message: message
+ } );
+ },
+ lacksClassPartial: function( element, partialClass, message ) {
+ var results = check( element, new RegExp( partialClass ) );
+
+ message = message || "Element must not have class containing '" + partialClass + "'";
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: results.missing.length,
+ actual: results.missing.join( " " ),
+ expected: partialClass,
+ message: message
+ } );
+ },
+ lacksAllClasses: function( element, message ) {
+ element = element.jquery ? element[ 0 ] : element;
+
+ var classAttribute = element.getAttribute( "class" ) || "",
+ classes = splitClasses( classAttribute );
+
+ message = message || "Element must not have any classes";
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: !classes.length,
+ actual: !classes.length,
+ expected: true,
+ message: message
+ } );
+ },
+ hasSomeClass: function( element, message ) {
+ element = element.jquery ? element[ 0 ] : element;
+
+ var classAttribute = element.getAttribute( "class" ) || "",
+ classes = splitClasses( classAttribute );
+
+ message = message || "Element must have a class";
+
+ // Custom modification: push -> pushResult
+ this.pushResult( {
+ result: classes.length,
+ actual: classes.length,
+ expected: true,
+ message: message
+ } );
+ }
+ };
+
+ // Custom modification: removing QUnit.extend
+ for ( key in qunitAssertExtensions ) {
+ QUnit.assert[ key ] = qunitAssertExtensions[ key ];
+ }
+
+} ) );
diff --git a/tests/lib/vendor/qunit-assert-close/MIT-LICENSE.txt b/tests/lib/vendor/qunit-assert-close/MIT-LICENSE.txt
new file mode 100644
index 000000000..aed5dc97e
--- /dev/null
+++ b/tests/lib/vendor/qunit-assert-close/MIT-LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright jQuery Foundation and other contributors
+http://jquery.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/tests/lib/vendor/qunit-assert-close/qunit-assert-close.js b/tests/lib/vendor/qunit-assert-close/qunit-assert-close.js
new file mode 100644
index 000000000..69d405205
--- /dev/null
+++ b/tests/lib/vendor/qunit-assert-close/qunit-assert-close.js
@@ -0,0 +1,209 @@
+// With custom modifications - all are marked with
+// a "Custom modification" comment.
+(function(factory) {
+
+ // NOTE:
+ // All techniques except for the "browser globals" fallback will extend the
+ // provided QUnit object but return the isolated API methods
+
+ // For AMD: Register as an anonymous AMD module with a named dependency on "qunit".
+ if (typeof define === "function" && define.amd) {
+ define(["qunit"], factory);
+ }
+ // For Node.js
+ else if (typeof module !== "undefined" && module && module.exports && typeof require === "function") {
+ module.exports = factory(require("qunitjs"));
+ }
+
+ // Custom modification: remove the non-Node.js CommonJS part due to its
+ // usage of QUnit.extend.
+ //
+ // For browser globals
+ else {
+ factory(QUnit);
+ }
+
+}(function(QUnit) {
+
+ /**
+ * Find an appropriate `Assert` context to `push` results to.
+ * @param * context - An unknown context, possibly `Assert`, `Test`, or neither
+ * @private
+ */
+ function _getPushContext(context) {
+ var pushContext;
+
+ if (context && typeof context.push === "function") {
+ // `context` is an `Assert` context
+ pushContext = context;
+ }
+ else if (context && context.assert && typeof context.assert.push === "function") {
+ // `context` is a `Test` context
+ pushContext = context.assert;
+ }
+ else if (
+ QUnit && QUnit.config && QUnit.config.current && QUnit.config.current.assert &&
+ typeof QUnit.config.current.assert.push === "function"
+ ) {
+ // `context` is an unknown context but we can find the `Assert` context via QUnit
+ pushContext = QUnit.config.current.assert;
+ }
+ else if (QUnit && typeof QUnit.push === "function") {
+ pushContext = QUnit.push;
+ }
+ else {
+ throw new Error("Could not find the QUnit `Assert` context to push results");
+ }
+
+ return pushContext;
+ }
+
+ /**
+ * Checks that the first two arguments are equal, or are numbers close enough to be considered equal
+ * based on a specified maximum allowable difference.
+ *
+ * @example assert.close(3.141, Math.PI, 0.001);
+ *
+ * @param Number actual
+ * @param Number expected
+ * @param Number maxDifference (the maximum inclusive difference allowed between the actual and expected numbers)
+ * @param String message (optional)
+ */
+ function close(actual, expected, maxDifference, message) {
+ var actualDiff = (actual === expected) ? 0 : Math.abs(actual - expected),
+ result = actualDiff <= maxDifference,
+ pushContext = _getPushContext(this);
+
+ message = message || (actual + " should be within " + maxDifference + " (inclusive) of " + expected + (result ? "" : ". Actual: " + actualDiff));
+
+ // Custom modification: push -> pushResult
+ pushContext.pushResult({
+ result: result,
+ actual: actual,
+ expected: expected,
+ message: message
+ });
+ }
+
+
+ /**
+ * Checks that the first two arguments are equal, or are numbers close enough to be considered equal
+ * based on a specified maximum allowable difference percentage.
+ *
+ * @example assert.close.percent(155, 150, 3.4); // Difference is ~3.33%
+ *
+ * @param Number actual
+ * @param Number expected
+ * @param Number maxPercentDifference (the maximum inclusive difference percentage allowed between the actual and expected numbers)
+ * @param String message (optional)
+ */
+ close.percent = function closePercent(actual, expected, maxPercentDifference, message) {
+ var actualDiff, result,
+ pushContext = _getPushContext(this);
+
+ if (actual === expected) {
+ actualDiff = 0;
+ result = actualDiff <= maxPercentDifference;
+ }
+ else if (actual !== 0 && expected !== 0 && expected !== Infinity && expected !== -Infinity) {
+ actualDiff = Math.abs(100 * (actual - expected) / expected);
+ result = actualDiff <= maxPercentDifference;
+ }
+ else {
+ // Dividing by zero (0)! Should return `false` unless the max percentage was `Infinity`
+ actualDiff = Infinity;
+ result = maxPercentDifference === Infinity;
+ }
+ message = message || (actual + " should be within " + maxPercentDifference + "% (inclusive) of " + expected + (result ? "" : ". Actual: " + actualDiff + "%"));
+
+ // Custom modification: push -> pushResult
+ pushContext.pushResult({
+ result: result,
+ actual: actual,
+ expected: expected,
+ message: message
+ });
+ };
+
+
+ /**
+ * Checks that the first two arguments are numbers with differences greater than the specified
+ * minimum difference.
+ *
+ * @example assert.notClose(3.1, Math.PI, 0.001);
+ *
+ * @param Number actual
+ * @param Number expected
+ * @param Number minDifference (the minimum exclusive difference allowed between the actual and expected numbers)
+ * @param String message (optional)
+ */
+ function notClose(actual, expected, minDifference, message) {
+ var actualDiff = Math.abs(actual - expected),
+ result = actualDiff > minDifference,
+ pushContext = _getPushContext(this);
+
+ message = message || (actual + " should not be within " + minDifference + " (exclusive) of " + expected + (result ? "" : ". Actual: " + actualDiff));
+
+ // Custom modification: push -> pushResult
+ pushContext.pushResult({
+ result: result,
+ actual: actual,
+ expected: expected,
+ message: message
+ });
+ }
+
+
+ /**
+ * Checks that the first two arguments are numbers with differences greater than the specified
+ * minimum difference percentage.
+ *
+ * @example assert.notClose.percent(156, 150, 3.5); // Difference is 4.0%
+ *
+ * @param Number actual
+ * @param Number expected
+ * @param Number minPercentDifference (the minimum exclusive difference percentage allowed between the actual and expected numbers)
+ * @param String message (optional)
+ */
+ notClose.percent = function notClosePercent(actual, expected, minPercentDifference, message) {
+ var actualDiff, result,
+ pushContext = _getPushContext(this);
+
+ if (actual === expected) {
+ actualDiff = 0;
+ result = actualDiff > minPercentDifference;
+ }
+ else if (actual !== 0 && expected !== 0 && expected !== Infinity && expected !== -Infinity) {
+ actualDiff = Math.abs(100 * (actual - expected) / expected);
+ result = actualDiff > minPercentDifference;
+ }
+ else {
+ // Dividing by zero (0)! Should only return `true` if the min percentage was `Infinity`
+ actualDiff = Infinity;
+ result = minPercentDifference !== Infinity;
+ }
+ message = message || (actual + " should not be within " + minPercentDifference + "% (exclusive) of " + expected + (result ? "" : ". Actual: " + actualDiff + "%"));
+
+ // Custom modification: push -> pushResult
+ pushContext.pushResult({
+ result: result,
+ actual: actual,
+ expected: expected,
+ message: message
+ });
+ };
+
+ var key;
+ var api = {
+ close: close,
+ notClose: notClose,
+ closePercent: close.percent,
+ notClosePercent: notClose.percent
+ };
+
+ for (key in api) {
+ QUnit.assert[key] = api[key];
+ }
+
+ return api;
+}));
diff --git a/tests/lib/vendor/qunit-composite/LICENSE.txt b/tests/lib/vendor/qunit-composite/LICENSE.txt
new file mode 100644
index 000000000..155d8e869
--- /dev/null
+++ b/tests/lib/vendor/qunit-composite/LICENSE.txt
@@ -0,0 +1,36 @@
+Copyright jQuery Foundation and other contributors, https://jquery.org/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/JamesMGreene/qunit-composite
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+All files located in the node_modules directory are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
diff --git a/tests/lib/vendor/qunit-composite/qunit-composite.css b/tests/lib/vendor/qunit-composite/qunit-composite.css
new file mode 100644
index 000000000..a5d06ce77
--- /dev/null
+++ b/tests/lib/vendor/qunit-composite/qunit-composite.css
@@ -0,0 +1,47 @@
+.qunit-composite-suite {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+
+ margin: 0;
+ padding: 0;
+ border-width: 1px 0 0;
+ height: 45%;
+ width: 100%;
+
+ background: #fff;
+}
+
+#qunit-testsuites {
+ margin: 0;
+ padding: 0.5em 1.0em;
+ font-family: "Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial,sans-serif;
+ font-size: small;
+ background-color: #d2e0e6;
+ border-bottom: 1px solid #fff;
+}
+
+#qunit-testsuites a {
+ color: #00c;
+ text-decoration: none;
+}
+
+#qunit-testsuites a:hover {
+ text-decoration: underline;
+}
+
+#qunit-testsuites > li {
+ display: inline-block;
+}
+
+#qunit-testsuites > li:first-child::before {
+ content: "Suites: ";
+}
+
+#qunit-testsuites > li + li::before {
+ content: "|\a0";
+}
+
+#qunit-testsuites > li::after {
+ content: "\a0";
+}
diff --git a/tests/lib/vendor/qunit-composite/qunit-composite.js b/tests/lib/vendor/qunit-composite/qunit-composite.js
new file mode 100644
index 000000000..042d024b4
--- /dev/null
+++ b/tests/lib/vendor/qunit-composite/qunit-composite.js
@@ -0,0 +1,236 @@
+/**
+ * QUnit Composite
+ *
+ * With custom modifications - all are marked with
+ * a "Custom modification" comment.
+ *
+ * https://github.com/JamesMGreene/qunit-composite
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * https://jquery.org/license/
+ */
+(function( factory ) {
+ if ( typeof define === "function" && define.amd ) {
+ define( [ "qunit" ], factory );
+ } else {
+ factory( QUnit );
+ }
+}(function( QUnit ) {
+var iframe, hasBound, resumeTests, suiteAssert,
+ modules = 1,
+ executingComposite = false;
+
+function hasClass( elem, name ) {
+ return ( " " + elem.className + " " ).indexOf( " " + name + " " ) > -1;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += ( elem.className ? " " : "" ) + name;
+ }
+}
+
+function addEvent( elem, type, fn ) {
+ if ( elem.addEventListener ) {
+ // Standards-based browsers
+ elem.addEventListener( type, fn, false );
+ } else if ( elem.attachEvent ) {
+ // support: IE <9
+ elem.attachEvent( "on" + type, fn );
+ }
+}
+
+function runSuite( suite ) {
+ var path;
+
+ if ( QUnit.is( "object", suite ) ) {
+ path = suite.path;
+ suite = suite.name;
+ } else {
+ path = suite;
+ }
+
+ QUnit.test( suite, function( assert ) {
+ resumeTests = assert.async();
+ suiteAssert = assert;
+ iframe.setAttribute( "src", path );
+ // QUnit.start is called from the child iframe's QUnit.done hook.
+ });
+}
+
+function initIframe() {
+ var iframeWin,
+ body = document.body;
+
+ function onIframeLoad() {
+ var moduleName, testName,
+ count = 0;
+
+ if ( !iframe.src ) {
+ return;
+ }
+
+ // Deal with QUnit being loaded asynchronously via AMD
+ if ( !iframeWin.QUnit && iframeWin.define && iframeWin.define.amd ) {
+ return iframeWin.require( [ "qunit" ], onIframeLoad );
+ }
+
+ iframeWin.QUnit.moduleStart(function( data ) {
+ // Capture module name for messages
+ moduleName = data.name;
+ });
+
+ iframeWin.QUnit.testStart(function( data ) {
+ // Capture test name for messages
+ testName = data.name;
+ });
+ iframeWin.QUnit.testDone(function() {
+ testName = undefined;
+ });
+
+ iframeWin.QUnit.log(function( data ) {
+ if (testName === undefined) {
+ return;
+ }
+ // Pass all test details through to the main page
+ var message = ( moduleName ? moduleName + ": " : "" ) + testName + ": " + ( data.message || ( data.result ? "okay" : "failed" ) );
+ suiteAssert.expect( ++count );
+ suiteAssert.pushResult( {
+ result: data.result,
+ actual: data.actual,
+ expected: data.expected,
+ message: message
+ } );
+ });
+
+ // Continue the outer test when the iframe's test is done
+ iframeWin.QUnit.done(function() {
+ resumeTests();
+ });
+ }
+
+ iframe = document.createElement( "iframe" );
+ iframe.className = "qunit-composite-suite";
+ body.appendChild( iframe );
+
+ addEvent( iframe, "load", onIframeLoad );
+
+ iframeWin = iframe.contentWindow;
+}
+
+function appendSuitesToHeader( suites ) {
+ var i, suitesLen, suite, path, name, suitesEl, testResultEl,
+ newSuiteListItemEl, newSuiteLinkEl;
+
+ suitesEl = document.getElementById("qunit-testsuites");
+
+ if (!suitesEl) {
+ testResultEl = document.getElementById("qunit-testresult");
+
+ if (!testResultEl) {
+ // QUnit has not been set up yet. Defer until QUnit is ready.
+ QUnit.begin(function () {
+ appendSuitesToHeader(suites);
+ });
+ return;
+ }
+
+ suitesEl = document.createElement("ul");
+ suitesEl.id = "qunit-testsuites";
+ testResultEl.parentNode.insertBefore(suitesEl, testResultEl);
+ }
+
+ for (i = 0, suitesLen = suites.length; i < suitesLen; ++i) {
+ suite = suites[i];
+ newSuiteLinkEl = document.createElement("a");
+ newSuiteLinkEl.innerHTML = suite.name || suite;
+ newSuiteLinkEl.href = suite.path || suite;
+
+ newSuiteListItemEl = document.createElement("li");
+ newSuiteListItemEl.appendChild(newSuiteLinkEl);
+
+ suitesEl.appendChild(newSuiteListItemEl);
+ }
+}
+
+/**
+ * @param {string} [name] Module name to group these test suites.
+ * @param {Array} suites List of suites where each suite
+ * may either be a string (path to the html test page),
+ * or an object with a path and name property.
+ */
+QUnit.testSuites = function( name, suites ) {
+ var i, suitesLen;
+
+ if ( arguments.length === 1 ) {
+ suites = name;
+ name = "Composition #" + modules++;
+ }
+ suitesLen = suites.length;
+
+ appendSuitesToHeader(suites);
+
+ if ( !hasBound ) {
+ hasBound = true;
+ QUnit.begin( initIframe );
+
+ // TODO: Would be better to use something like QUnit.once( 'moduleDone' )
+ // after the last test suite.
+ QUnit.moduleDone( function () {
+ executingComposite = false;
+ } );
+
+ QUnit.done(function() {
+ iframe.style.display = "none";
+ });
+ }
+
+ QUnit.module( name, {
+ beforeEach: function () {
+ executingComposite = true;
+ }
+ });
+
+ for ( i = 0; i < suitesLen; i++ ) {
+ runSuite( suites[ i ] );
+ }
+};
+
+QUnit.testDone(function( data ) {
+ if ( !executingComposite ) {
+ return;
+ }
+
+ var i, len,
+ testId = data.testId,
+ current = document.getElementById( "qunit-test-output-" + testId ),
+ children = current && current.children,
+ src = iframe.src;
+
+ if (!(current && children)) {
+ return;
+ }
+
+ addEvent( current, "dblclick", function( e ) {
+ var target = e && e.target ? e.target : window.event.srcElement;
+ if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
+ target = target.parentNode;
+ }
+ if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
+ window.location = src;
+ }
+ });
+
+ // Undo QUnit's auto-expansion for bad tests
+ for ( i = 0, len = children.length; i < len; i++ ) {
+ if ( children[ i ].nodeName.toLowerCase() === "ol" ) {
+ addClass( children[ i ], "qunit-collapsed" );
+ }
+ }
+
+ // Update Rerun link to point to the standalone test suite page
+ current.getElementsByTagName( "a" )[ 0 ].href = src;
+});
+
+}));