WordPress.org

Make WordPress Core

Changeset 27679


Ignore:
Timestamp:
03/24/2014 05:59:45 AM (7 years ago)
Author:
azaozz
Message:

Update the TinyMCE tests.

In 4.0.20 all tests were reworked. The 'testrunner' was removed and the PhantomJS Runner QUnit plugin was added making it possible to run the tests from cli. However it is still necessary to run the tests in all supported browsers to test the fixes for all browser quirks and normalization. Also all tests are loaded in one html file.

See #27014

Location:
trunk/tests/qunit/editor
Files:
57 added
116 deleted
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/qunit/editor/index.html

    r27155 r27679  
    22<html>
    33<head>
    4 <meta charset="UTF-8" />
    5 <title>Test Runner</title>
    6 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    7 <link rel="stylesheet" href="js/qunit/testrunner.css" type="text/css" />
    8 <script src="js/qunit/testrunner.js"></script>
    9 <script>
    10 TestRunner.addSuites([
    11     "tinymce/tests.js",
    12     "tinymce/dom/tests.js",
    13     "tinymce/html/tests.js",
    14     "tinymce/ui/tests.js",
    15     "tinymce/util/tests.js",
    16     "plugins/tests.js"
    17 ]);
    18 </script>
     4    <meta charset="UTF-8" />
     5    <title>TinyMCE QUnit tests</title>
     6    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     7    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0" />
     8    <title>QUnit tests</title>
     9    <link rel="stylesheet" href="js/qunit/qunit.css" type="text/css" />
     10    <link rel="stylesheet" href="../../../src/wp-includes/js/tinymce/skins/lightgray/skin.min.css" type="text/css" />
     11    <link rel="stylesheet" href="tinymce/ui/css/ui-overrides.css" type="text/css" />
     12    <style>
     13        #qunit-modulefilter-container { float: none; }
     14    </style>
    1915</head>
    2016<body>
     17    <div id="qunit"></div>
     18    <div id="qunit-fixture"></div>
     19    <div id="view" style="position: absolute; right: 0; top: 0"></div>
     20
     21    <script src="http://www.google.com/jsapi"></script>
     22    <script>google.load("jquery", "1");</script>
     23    <script src="js/qunit/qunit.js"></script>
     24    <script src="../../../src/wp-includes/js/tinymce/tinymce.min.js"></script>
     25<!--<script src="../js/tinymce/classes/jquery.tinymce.js"></script> -->
     26    <script src="js/utils.js"></script>
     27    <script src="js/init.js"></script>
     28
     29    <!-- tinymce.dom.* -->
     30    <script src="tinymce/dom/DOMUtils.js"></script>
     31    <script src="tinymce/dom/EventUtils.js"></script>
     32    <script src="tinymce/dom/Range.js"></script>
     33    <script src="tinymce/dom/Selection.js"></script>
     34    <script src="tinymce/dom/Serializer.js"></script>
     35    <script src="tinymce/dom/TridentSelection.js"></script>
     36
     37    <!-- tinymce.html.* -->
     38    <script src="tinymce/html/DomParser.js"></script>
     39    <script src="tinymce/html/Entities.js"></script>
     40    <script src="tinymce/html/Node.js"></script>
     41    <script src="tinymce/html/SaxParser.js"></script>
     42    <script src="tinymce/html/Schema.js"></script>
     43    <script src="tinymce/html/Obsolete.js"></script>
     44    <script src="tinymce/html/Styles.js"></script>
     45    <script src="tinymce/html/Writer.js"></script>
     46
     47    <!-- tnymce.ui.* -->
     48    <script src="tinymce/ui/AbsoluteLayout.js"></script>
     49    <script src="tinymce/ui/Button.js"></script>
     50    <script src="tinymce/ui/Collection.js"></script>
     51    <script src="tinymce/ui/ColorButton.js"></script>
     52    <script src="tinymce/ui/Control.js"></script>
     53    <script src="tinymce/ui/FitLayout.js"></script>
     54    <script src="tinymce/ui/FlexLayout.js"></script>
     55    <script src="tinymce/ui/GridLayout.js"></script>
     56    <script src="tinymce/ui/MenuButton.js"></script>
     57    <script src="tinymce/ui/Panel.js"></script>
     58    <script src="tinymce/ui/Selector.js"></script>
     59    <script src="tinymce/ui/SplitButton.js"></script>
     60    <script src="tinymce/ui/TabPanel.js"></script>
     61    <script src="tinymce/ui/TextBox.js"></script>
     62    <script src="tinymce/ui/Window.js"></script>
     63
     64    <!-- tinymce.util.* -->
     65    <script src="tinymce/util/JSON.js"></script>
     66    <script src="tinymce/util/JSONRequest.js"></script>
     67    <script src="tinymce/util/LocalStorage.js"></script>
     68    <script src="tinymce/util/Quirks_webkit.js"></script>
     69    <script src="tinymce/util/URI.js"></script>
     70    <script src="tinymce/util/XHR.js"></script>
     71
     72    <!-- tinymce.* -->
     73    <script src="tinymce/Editor.js"></script>
     74    <script src="tinymce/EditorCommands.js"></script>
     75    <script src="tinymce/EnterKey.js"></script>
     76    <script src="tinymce/ForceBlocks.js"></script>
     77    <script src="tinymce/Formatter_apply.js"></script>
     78    <script src="tinymce/Formatter_check.js"></script>
     79    <script src="tinymce/Formatter_remove.js"></script>
     80    <script src="tinymce/UndoManager.js"></script>
     81
     82    <!-- tinymce.plugins.* -->
     83<!--<script src="plugins/autosave.js"></script>
     84    <script src="plugins/fullpage.js"></script>
     85    <script src="plugins/jquery_plugin.js"></script>
     86    <script src="plugins/legacyoutput.js"></script>
     87    <script src="plugins/lists.js"></script> -->
     88    <script src="plugins/media.js"></script>
     89<!--<script src="plugins/noneditable.js"></script> -->
     90    <script src="plugins/paste.js"></script>
     91<!--<script src="plugins/searchreplace.js"></script>
     92    <script src="plugins/spellchecker.js"></script>
     93    <script src="plugins/table.js"></script>
     94    <script src="plugins/wordcount.js"></script> -->
    2195</body>
    2296</html>
  • trunk/tests/qunit/editor/js/qunit/qunit.css

    r27155 r27679  
    1 
    2 ol#qunit-tests {
    3     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    4     margin:0;
    5     padding:0;
    6     list-style-position:inside;
    7 
     1/*!
     2 * QUnit 1.14.0
     3 * http://qunitjs.com/
     4 *
     5 * Copyright 2013 jQuery Foundation and other contributors
     6 * Released under the MIT license
     7 * http://jquery.org/license
     8 *
     9 * Date: 2014-01-31T16:40Z
     10 */
     11
     12/** Font Family and Sizes */
     13
     14#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
     15    font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
     16}
     17
     18#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
     19#qunit-tests { font-size: smaller; }
     20
     21
     22/** Resets */
     23
     24#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
     25    margin: 0;
     26    padding: 0;
     27}
     28
     29
     30/** Header */
     31
     32#qunit-header {
     33    padding: 0.5em 0 0.5em 1em;
     34
     35    color: #8699A4;
     36    background-color: #0D3349;
     37
     38    font-size: 1.5em;
     39    line-height: 1em;
     40    font-weight: 400;
     41
     42    border-radius: 5px 5px 0 0;
     43}
     44
     45#qunit-header a {
     46    text-decoration: none;
     47    color: #C2CCD1;
     48}
     49
     50#qunit-header a:hover,
     51#qunit-header a:focus {
     52    color: #FFF;
     53}
     54
     55#qunit-testrunner-toolbar label {
     56    display: inline-block;
     57    padding: 0 0.5em 0 0.1em;
     58}
     59
     60#qunit-banner {
     61    height: 5px;
     62}
     63
     64#qunit-testrunner-toolbar {
     65    padding: 0.5em 0 0.5em 2em;
     66    color: #5E740B;
     67    background-color: #EEE;
     68    overflow: hidden;
     69}
     70
     71#qunit-userAgent {
     72    padding: 0.5em 0 0.5em 2.5em;
     73    background-color: #2B81AF;
     74    color: #FFF;
     75    text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
     76}
     77
     78#qunit-modulefilter-container {
     79    float: right;
     80}
     81
     82/** Tests: Pass/Fail */
     83
     84#qunit-tests {
     85    list-style-position: inside;
     86}
     87
     88#qunit-tests li {
     89    padding: 0.4em 0.5em 0.4em 2.5em;
     90    border-bottom: 1px solid #FFF;
     91    list-style-position: inside;
     92}
     93
     94#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running  {
     95    display: none;
     96}
     97
     98#qunit-tests li strong {
     99    cursor: pointer;
     100}
     101
     102#qunit-tests li a {
     103    padding: 0.5em;
     104    color: #C2CCD1;
     105    text-decoration: none;
     106}
     107#qunit-tests li a:hover,
     108#qunit-tests li a:focus {
     109    color: #000;
     110}
     111
     112#qunit-tests li .runtime {
     113    float: right;
    8114    font-size: smaller;
    9115}
    10 ol#qunit-tests li{
    11     padding:0.4em 0.5em 0.4em 2.5em;
    12     border-bottom:1px solid #fff;
    13     font-size:small;
    14     list-style-position:inside;
    15 }
    16 ol#qunit-tests li ol{
    17     box-shadow: inset 0px 2px 13px #999;
    18     -moz-box-shadow: inset 0px 2px 13px #999;
    19     -webkit-box-shadow: inset 0px 2px 13px #999;
    20     margin-top:0.5em;
    21     margin-left:0;
    22     padding:0.5em;
    23     background-color:#fff;
    24     border-radius:15px;
    25     -moz-border-radius: 15px;
    26     -webkit-border-radius: 15px;
    27 }
    28 ol#qunit-tests li li{
    29     border-bottom:none;
    30     margin:0.5em;
    31     background-color:#fff;
     116
     117.qunit-assert-list {
     118    margin-top: 0.5em;
     119    padding: 0.5em;
     120
     121    background-color: #FFF;
     122
     123    border-radius: 5px;
     124}
     125
     126.qunit-collapsed {
     127    display: none;
     128}
     129
     130#qunit-tests table {
     131    border-collapse: collapse;
     132    margin-top: 0.2em;
     133}
     134
     135#qunit-tests th {
     136    text-align: right;
     137    vertical-align: top;
     138    padding: 0 0.5em 0 0;
     139}
     140
     141#qunit-tests td {
     142    vertical-align: top;
     143}
     144
     145#qunit-tests pre {
     146    margin: 0;
     147    white-space: pre-wrap;
     148    word-wrap: break-word;
     149}
     150
     151#qunit-tests del {
     152    background-color: #E0F2BE;
     153    color: #374E0C;
     154    text-decoration: none;
     155}
     156
     157#qunit-tests ins {
     158    background-color: #FFCACA;
     159    color: #500;
     160    text-decoration: none;
     161}
     162
     163/*** Test Counts */
     164
     165#qunit-tests b.counts                       { color: #000; }
     166#qunit-tests b.passed                       { color: #5E740B; }
     167#qunit-tests b.failed                       { color: #710909; }
     168
     169#qunit-tests li li {
     170    padding: 5px;
     171    background-color: #FFF;
     172    border-bottom: none;
    32173    list-style-position: inside;
    33     padding:0.4em 0.5em 0.4em 0.5em;
    34 }
    35 
    36 ol#qunit-tests li li.pass{
    37     border-left:26px solid #C6E746;
    38     background-color:#fff;
    39     color:#5E740B;
    40     }
    41 ol#qunit-tests li li.fail{
    42     border-left:26px solid #EE5757;
    43     background-color:#fff;
    44     color:#710909;
    45 }
    46 ol#qunit-tests li.pass{
    47     background-color:#D2E0E6;
    48     color:#528CE0;
    49 }
    50 ol#qunit-tests li.fail{
    51     background-color:#EE5757;
    52     color:#000;
    53 }
    54 ol#qunit-tests li strong {
    55     cursor:pointer;
    56 }
    57 h1#qunit-header{
    58     background-color:#0d3349;
    59     margin:0;
    60     padding:0.5em 0 0.5em 1em;
    61     color:#fff;
    62     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    63     border-top-right-radius:15px;
    64     border-top-left-radius:15px;
    65     -moz-border-radius-topright:15px;
    66     -moz-border-radius-topleft:15px;
    67     -webkit-border-top-right-radius:15px;
    68     -webkit-border-top-left-radius:15px;
    69     text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
    70 }
    71 h2#qunit-banner{
    72     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    73     height:5px;
    74     margin:0;
    75     padding:0;
    76 }
    77 h2#qunit-banner.qunit-pass{
    78     background-color:#C6E746;
    79 }
    80 h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
    81     background-color:#EE5757;
    82 }
    83 #qunit-testrunner-toolbar {
    84     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    85     padding:0;
    86     /*width:80%;*/
    87     padding:0em 0 0.5em 2em;
    88     font-size: small;
    89 }
    90 h2#qunit-userAgent {
    91     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    92     background-color:#2b81af;
    93     margin:0;
    94     padding:0;
    95     color:#fff;
    96     font-size: small;
    97     padding:0.5em 0 0.5em 2.5em;
    98     text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
    99 }
    100 p#qunit-testresult{
    101     font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
    102     margin:0;
    103     font-size: small;
    104     color:#2b81af;
    105     border-bottom-right-radius:15px;
    106     border-bottom-left-radius:15px;
    107     -moz-border-radius-bottomright:15px;
    108     -moz-border-radius-bottomleft:15px;
    109     -webkit-border-bottom-right-radius:15px;
    110     -webkit-border-bottom-left-radius:15px;
    111     background-color:#D2E0E6;
    112     padding:0.5em 0.5em 0.5em 2.5em;
    113 }
    114 strong b.fail{
    115     color:#710909;
    116     }
    117 strong b.pass{
    118     color:#5E740B;
    119     }
     174}
     175
     176/*** Passing Styles */
     177
     178#qunit-tests li li.pass {
     179    color: #3C510C;
     180    background-color: #FFF;
     181    border-left: 10px solid #C6E746;
     182}
     183
     184#qunit-tests .pass                          { color: #528CE0; background-color: #D2E0E6; }
     185#qunit-tests .pass .test-name               { color: #366097; }
     186
     187#qunit-tests .pass .test-actual,
     188#qunit-tests .pass .test-expected           { color: #999; }
     189
     190#qunit-banner.qunit-pass                    { background-color: #C6E746; }
     191
     192/*** Failing Styles */
     193
     194#qunit-tests li li.fail {
     195    color: #710909;
     196    background-color: #FFF;
     197    border-left: 10px solid #EE5757;
     198    white-space: pre;
     199}
     200
     201#qunit-tests > li:last-child {
     202    border-radius: 0 0 5px 5px;
     203}
     204
     205#qunit-tests .fail                          { color: #000; background-color: #EE5757; }
     206#qunit-tests .fail .test-name,
     207#qunit-tests .fail .module-name             { color: #000; }
     208
     209#qunit-tests .fail .test-actual             { color: #EE5757; }
     210#qunit-tests .fail .test-expected           { color: #008000; }
     211
     212#qunit-banner.qunit-fail                    { background-color: #EE5757; }
     213
     214
     215/** Result */
     216
     217#qunit-testresult {
     218    padding: 0.5em 0.5em 0.5em 2.5em;
     219
     220    color: #2B81AF;
     221    background-color: #D2E0E6;
     222
     223    border-bottom: 1px solid #FFF;
     224}
     225#qunit-testresult .module-name {
     226    font-weight: 700;
     227}
     228
     229/** Fixture */
     230
     231#qunit-fixture {
     232    position: absolute;
     233    top: -10000px;
     234    left: -10000px;
     235    width: 1000px;
     236    height: 1000px;
     237}
  • trunk/tests/qunit/editor/js/qunit/qunit.js

    r27155 r27679  
    1 /*
    2  * QUnit - A JavaScript Unit Testing Framework
    3  *
    4  * http://docs.jquery.com/QUnit
     1/*!
     2 * QUnit 1.14.0
     3 * http://qunitjs.com/
    54 *
    6  * Copyright (c) 2009 John Resig, Jörn Zaefferer
    7  * Dual licensed under the MIT (MIT-LICENSE.txt)
    8  * and GPL (GPL-LICENSE.txt) licenses.
     5 * Copyright 2013 jQuery Foundation and other contributors
     6 * Released under the MIT license
     7 * http://jquery.org/license
     8 *
     9 * Date: 2014-01-31T16:40Z
    910 */
    1011
    11 (function(window) {
    12 
    13 var QUnit = {
     12(function( window ) {
     13
     14var QUnit,
     15    assert,
     16    config,
     17    onErrorFnPrev,
     18    testId = 0,
     19    fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
     20    toString = Object.prototype.toString,
     21    hasOwn = Object.prototype.hasOwnProperty,
     22    // Keep a local reference to Date (GH-283)
     23    Date = window.Date,
     24    setTimeout = window.setTimeout,
     25    clearTimeout = window.clearTimeout,
     26    defined = {
     27        document: typeof window.document !== "undefined",
     28        setTimeout: typeof window.setTimeout !== "undefined",
     29        sessionStorage: (function() {
     30            var x = "qunit-test-string";
     31            try {
     32                sessionStorage.setItem( x, x );
     33                sessionStorage.removeItem( x );
     34                return true;
     35            } catch( e ) {
     36                return false;
     37            }
     38        }())
     39    },
     40    /**
     41     * Provides a normalized error string, correcting an issue
     42     * with IE 7 (and prior) where Error.prototype.toString is
     43     * not properly implemented
     44     *
     45     * Based on http://es5.github.com/#x15.11.4.4
     46     *
     47     * @param {String|Error} error
     48     * @return {String} error message
     49     */
     50    errorString = function( error ) {
     51        var name, message,
     52            errorString = error.toString();
     53        if ( errorString.substring( 0, 7 ) === "[object" ) {
     54            name = error.name ? error.name.toString() : "Error";
     55            message = error.message ? error.message.toString() : "";
     56            if ( name && message ) {
     57                return name + ": " + message;
     58            } else if ( name ) {
     59                return name;
     60            } else if ( message ) {
     61                return message;
     62            } else {
     63                return "Error";
     64            }
     65        } else {
     66            return errorString;
     67        }
     68    },
     69    /**
     70     * Makes a clone of an object using only Array or Object as base,
     71     * and copies over the own enumerable properties.
     72     *
     73     * @param {Object} obj
     74     * @return {Object} New object with only the own properties (recursively).
     75     */
     76    objectValues = function( obj ) {
     77        // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
     78        /*jshint newcap: false */
     79        var key, val,
     80            vals = QUnit.is( "array", obj ) ? [] : {};
     81        for ( key in obj ) {
     82            if ( hasOwn.call( obj, key ) ) {
     83                val = obj[key];
     84                vals[key] = val === Object(val) ? objectValues(val) : val;
     85            }
     86        }
     87        return vals;
     88    };
     89
     90
     91// Root QUnit object.
     92// `QUnit` initialized at top of scope
     93QUnit = {
    1494
    1595    // call on start of module test to prepend name to all tests
    16     module: function(name, testEnvironment) {
     96    module: function( name, testEnvironment ) {
    1797        config.currentModule = name;
    18 
    19         synchronize(function() {
    20             if ( config.currentModule ) {
    21                 QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
    22             }
    23 
    24             config.currentModule = name;
    25             config.moduleTestEnvironment = testEnvironment;
    26             config.moduleStats = { all: 0, bad: 0 };
    27 
    28             QUnit.moduleStart( name, testEnvironment );
    29         });
    30     },
    31 
    32     asyncTest: function(testName, expected, callback) {
    33         if ( arguments.length === 2 ) {
    34             callback = expected;
    35             expected = 0;
    36         }
    37 
    38         QUnit.test(testName, expected, callback, true);
    39     },
    40    
    41     test: function(testName, expected, callback, async) {
    42         var name = '<span class="test-name">' + testName + '</span>', testEnvironment, testEnvironmentArg;
    43 
     98        config.currentModuleTestEnvironment = testEnvironment;
     99        config.modules[name] = true;
     100    },
     101
     102    asyncTest: function( testName, expected, callback ) {
    44103        if ( arguments.length === 2 ) {
    45104            callback = expected;
    46105            expected = null;
    47106        }
    48         // is 2nd argument a testEnvironment?
    49         if ( expected && typeof expected === 'object') {
    50             testEnvironmentArg =  expected;
     107
     108        QUnit.test( testName, expected, callback, true );
     109    },
     110
     111    test: function( testName, expected, callback, async ) {
     112        var test,
     113            nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>";
     114
     115        if ( arguments.length === 2 ) {
     116            callback = expected;
    51117            expected = null;
    52118        }
    53119
    54120        if ( config.currentModule ) {
    55             name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
    56         }
    57 
    58         if ( !validTest(config.currentModule + ": " + testName) ) {
     121            nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml;
     122        }
     123
     124        test = new Test({
     125            nameHtml: nameHtml,
     126            testName: testName,
     127            expected: expected,
     128            async: async,
     129            callback: callback,
     130            module: config.currentModule,
     131            moduleTestEnvironment: config.currentModuleTestEnvironment,
     132            stack: sourceFromStacktrace( 2 )
     133        });
     134
     135        if ( !validTest( test ) ) {
    59136            return;
    60137        }
    61138
    62         synchronize(function() {
    63 
    64             testEnvironment = extend({
    65                 setup: function() {},
    66                 teardown: function() {}
    67             }, config.moduleTestEnvironment);
    68             if (testEnvironmentArg) {
    69                 extend(testEnvironment,testEnvironmentArg);
    70             }
    71 
    72             QUnit.testStart( testName, testEnvironment );
    73 
    74             // allow utility functions to access the current test environment
    75             QUnit.current_testEnvironment = testEnvironment;
    76            
    77             config.assertions = [];
    78             config.expected = expected;
    79            
    80             var tests = id("qunit-tests");
    81             if (tests) {
    82                 var b = document.createElement("strong");
    83                     b.innerHTML = "Running " + name;
    84                 var li = document.createElement("li");
    85                     li.appendChild( b );
    86                     li.id = "current-test-output";
    87                 tests.appendChild( li )
    88             }
    89 
    90             try {
    91                 if ( !config.pollution ) {
    92                     saveGlobal();
    93                 }
    94 
    95                 testEnvironment.setup.call(testEnvironment);
    96             } catch(e) {
    97                 QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
    98             }
    99         });
    100    
    101         synchronize(function() {
    102             if ( async ) {
    103                 QUnit.stop();
    104             }
    105 
    106             try {
    107                 callback.call(testEnvironment);
    108             } catch(e) {
    109                 fail("Test " + name + " died, exception and test follows", e, callback);
    110                 QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
    111                 // else next test will carry the responsibility
    112                 saveGlobal();
    113 
    114                 // Restart the tests if they're blocking
    115                 if ( config.blocking ) {
    116                     start();
    117                 }
    118             }
    119         });
    120 
    121         synchronize(function() {
    122             try {
    123                 checkPollution();
    124                 testEnvironment.teardown.call(testEnvironment);
    125             } catch(e) {
    126                 QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
    127             }
    128         });
    129    
    130         synchronize(function() {
    131             try {
    132                 QUnit.reset();
    133             } catch(e) {
    134                 fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
    135             }
    136 
    137             if ( config.expected && config.expected != config.assertions.length ) {
    138                 QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
    139             }
    140 
    141             var good = 0, bad = 0,
    142                 tests = id("qunit-tests");
    143 
    144             config.stats.all += config.assertions.length;
    145             config.moduleStats.all += config.assertions.length;
    146 
    147             if ( tests ) {
    148                 var ol  = document.createElement("ol");
    149 
    150                 for ( var i = 0; i < config.assertions.length; i++ ) {
    151                     var assertion = config.assertions[i];
    152 
    153                     var li = document.createElement("li");
    154                     li.className = assertion.result ? "pass" : "fail";
    155                     li.innerHTML = assertion.message || "(no message)";
    156                     ol.appendChild( li );
    157 
    158                     if ( assertion.result ) {
    159                         good++;
    160                     } else {
    161                         bad++;
    162                         config.stats.bad++;
    163                         config.moduleStats.bad++;
    164                     }
    165                 }
    166                 if (bad == 0) {
    167                     ol.style.display = "none";
    168                 }
    169 
    170                 var b = document.createElement("strong");
    171                 b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
    172                
    173                 addEvent(b, "click", function() {
    174                     var next = b.nextSibling, display = next.style.display;
    175                     next.style.display = display === "none" ? "block" : "none";
     139        test.queue();
     140    },
     141
     142    // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
     143    expect: function( asserts ) {
     144        if (arguments.length === 1) {
     145            config.current.expected = asserts;
     146        } else {
     147            return config.current.expected;
     148        }
     149    },
     150
     151    start: function( count ) {
     152        // QUnit hasn't been initialized yet.
     153        // Note: RequireJS (et al) may delay onLoad
     154        if ( config.semaphore === undefined ) {
     155            QUnit.begin(function() {
     156                // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
     157                setTimeout(function() {
     158                    QUnit.start( count );
    176159                });
    177                
    178                 addEvent(b, "dblclick", function(e) {
    179                     var target = e && e.target ? e.target : window.event.srcElement;
    180                     if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
    181                         target = target.parentNode;
    182                     }
    183                     if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
    184                         window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, ""));
    185                     }
    186                 });
    187 
    188                 var li = id("current-test-output");
    189                 li.id = "";
    190                 li.className = bad ? "fail" : "pass";
    191                 li.removeChild( li.firstChild );
    192                 li.appendChild( b );
    193                 li.appendChild( ol );
    194 
    195                 if ( bad ) {
    196                     var toolbar = id("qunit-testrunner-toolbar");
    197                     if ( toolbar ) {
    198                         toolbar.style.display = "block";
    199                         id("qunit-filter-pass").disabled = null;
    200                         id("qunit-filter-missing").disabled = null;
    201                     }
    202                 }
    203 
    204             } else {
    205                 for ( var i = 0; i < config.assertions.length; i++ ) {
    206                     if ( !config.assertions[i].result ) {
    207                         bad++;
    208                         config.stats.bad++;
    209                         config.moduleStats.bad++;
    210                     }
    211                 }
    212             }
    213 
    214             QUnit.testDone( testName, bad, config.assertions.length );
    215 
    216             if ( !window.setTimeout && !config.queue.length ) {
    217                 done();
    218             }
    219         });
    220 
    221         if ( window.setTimeout && !config.doneTimer ) {
    222             config.doneTimer = window.setTimeout(function(){
    223                 if ( !config.queue.length ) {
    224                     done();
    225                 } else {
    226                     synchronize( done );
    227                 }
    228             }, 13);
    229         }
    230     },
    231    
    232     /**
    233      * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
    234      */
    235     expect: function(asserts) {
    236         config.expected = asserts;
    237     },
    238 
    239     /**
    240      * Asserts true.
    241      * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
    242      */
    243     ok: function(a, msg) {
    244         msg = escapeHtml(msg);
    245         QUnit.log(a, msg);
    246 
    247         config.assertions.push({
    248             result: !!a,
    249             message: msg
    250         });
    251     },
    252 
    253     /**
    254      * Checks that the first two arguments are equal, with an optional message.
    255      * Prints out both actual and expected values.
    256      *
    257      * Prefered to ok( actual == expected, message )
    258      *
    259      * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
    260      *
    261      * @param Object actual
    262      * @param Object expected
    263      * @param String message (optional)
    264      */
    265     equal: function(actual, expected, message) {
    266         push(expected == actual, actual, expected, message);
    267     },
    268 
    269     notEqual: function(actual, expected, message) {
    270         push(expected != actual, actual, expected, message);
    271     },
    272    
    273     deepEqual: function(actual, expected, message) {
    274         push(QUnit.equiv(actual, expected), actual, expected, message);
    275     },
    276 
    277     notDeepEqual: function(actual, expected, message) {
    278         push(!QUnit.equiv(actual, expected), actual, expected, message);
    279     },
    280 
    281     strictEqual: function(actual, expected, message) {
    282         push(expected === actual, actual, expected, message);
    283     },
    284 
    285     notStrictEqual: function(actual, expected, message) {
    286         push(expected !== actual, actual, expected, message);
    287     },
    288 
    289     raises: function(fn,  message) {
    290         try {
    291             fn();
    292             ok( false, message );
    293         }
    294         catch (e) {
    295             ok( true, message );
    296         }
    297     },
    298 
    299     start: function() {
     160            });
     161            return;
     162        }
     163
     164        config.semaphore -= count || 1;
     165        // don't start until equal number of stop-calls
     166        if ( config.semaphore > 0 ) {
     167            return;
     168        }
     169        // ignore if start is called more often then stop
     170        if ( config.semaphore < 0 ) {
     171            config.semaphore = 0;
     172            QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
     173            return;
     174        }
    300175        // A slight delay, to avoid any current callbacks
    301         if ( window.setTimeout ) {
    302             window.setTimeout(function() {
     176        if ( defined.setTimeout ) {
     177            setTimeout(function() {
     178                if ( config.semaphore > 0 ) {
     179                    return;
     180                }
    303181                if ( config.timeout ) {
    304                     clearTimeout(config.timeout);
     182                    clearTimeout( config.timeout );
    305183                }
    306184
    307185                config.blocking = false;
    308                 process();
     186                process( true );
    309187            }, 13);
    310188        } else {
    311189            config.blocking = false;
    312             process();
    313         }
    314     },
    315    
    316     stop: function(timeout) {
     190            process( true );
     191        }
     192    },
     193
     194    stop: function( count ) {
     195        config.semaphore += count || 1;
    317196        config.blocking = true;
    318197
    319         if ( timeout && window.setTimeout ) {
    320             config.timeout = window.setTimeout(function() {
     198        if ( config.testTimeout && defined.setTimeout ) {
     199            clearTimeout( config.timeout );
     200            config.timeout = setTimeout(function() {
    321201                QUnit.ok( false, "Test timed out" );
     202                config.semaphore = 1;
    322203                QUnit.start();
    323             }, timeout);
    324         }
    325     }
    326 
     204            }, config.testTimeout );
     205        }
     206    }
    327207};
    328208
    329 // Backwards compatibility, deprecated
    330 QUnit.equals = QUnit.equal;
    331 QUnit.same = QUnit.deepEqual;
    332 
    333 // Maintain internal state
    334 var config = {
     209// We use the prototype to distinguish between properties that should
     210// be exposed as globals (and in exports) and those that shouldn't
     211(function() {
     212    function F() {}
     213    F.prototype = QUnit;
     214    QUnit = new F();
     215    // Make F QUnit's constructor so that we can add to the prototype later
     216    QUnit.constructor = F;
     217}());
     218
     219/**
     220 * Config object: Maintain internal state
     221 * Later exposed as QUnit.config
     222 * `config` initialized at top of scope
     223 */
     224config = {
    335225    // The queue of tests to run
    336226    queue: [],
    337227
    338228    // block until document ready
    339     blocking: true
     229    blocking: true,
     230
     231    // when enabled, show only failing tests
     232    // gets persisted through sessionStorage and can be changed in UI via checkbox
     233    hidepassed: false,
     234
     235    // by default, run previously failed tests first
     236    // very useful in combination with "Hide passed tests" checked
     237    reorder: true,
     238
     239    // by default, modify document.title when suite is done
     240    altertitle: true,
     241
     242    // by default, scroll to top of the page when suite is done
     243    scrolltop: true,
     244
     245    // when enabled, all tests must call expect()
     246    requireExpects: false,
     247
     248    // add checkboxes that are persisted in the query-string
     249    // when enabled, the id is set to `true` as a `QUnit.config` property
     250    urlConfig: [
     251        {
     252            id: "noglobals",
     253            label: "Check for Globals",
     254            tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
     255        },
     256        {
     257            id: "notrycatch",
     258            label: "No try-catch",
     259            tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
     260        }
     261    ],
     262
     263    // Set of all modules.
     264    modules: {},
     265
     266    // logging callback queues
     267    begin: [],
     268    done: [],
     269    log: [],
     270    testStart: [],
     271    testDone: [],
     272    moduleStart: [],
     273    moduleDone: []
    340274};
    341275
    342 // Load parameters
     276// Initialize more QUnit.config and QUnit.urlParams
    343277(function() {
    344     var location = window.location || { search: "", protocol: "file:" },
    345         GETParams = location.search.slice(1).split('&');
    346 
    347     for ( var i = 0; i < GETParams.length; i++ ) {
    348         GETParams[i] = decodeURIComponent( GETParams[i] );
    349         if ( GETParams[i] === "noglobals" ) {
    350             GETParams.splice( i, 1 );
    351             i--;
    352             config.noglobals = true;
    353         } else if ( GETParams[i].search('=') > -1 ) {
    354             GETParams.splice( i, 1 );
    355             i--;
    356         }
    357     }
    358    
    359     // restrict modules/tests by get parameters
    360     config.filters = GETParams;
    361    
     278    var i, current,
     279        location = window.location || { search: "", protocol: "file:" },
     280        params = location.search.slice( 1 ).split( "&" ),
     281        length = params.length,
     282        urlParams = {};
     283
     284    if ( params[ 0 ] ) {
     285        for ( i = 0; i < length; i++ ) {
     286            current = params[ i ].split( "=" );
     287            current[ 0 ] = decodeURIComponent( current[ 0 ] );
     288
     289            // allow just a key to turn on a flag, e.g., test.html?noglobals
     290            current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
     291            if ( urlParams[ current[ 0 ] ] ) {
     292                urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
     293            } else {
     294                urlParams[ current[ 0 ] ] = current[ 1 ];
     295            }
     296        }
     297    }
     298
     299    QUnit.urlParams = urlParams;
     300
     301    // String search anywhere in moduleName+testName
     302    config.filter = urlParams.filter;
     303
     304    // Exact match of the module name
     305    config.module = urlParams.module;
     306
     307    config.testNumber = [];
     308    if ( urlParams.testNumber ) {
     309
     310        // Ensure that urlParams.testNumber is an array
     311        urlParams.testNumber = [].concat( urlParams.testNumber );
     312        for ( i = 0; i < urlParams.testNumber.length; i++ ) {
     313            current = urlParams.testNumber[ i ];
     314            config.testNumber.push( parseInt( current, 10 ) );
     315        }
     316    }
     317
    362318    // Figure out if we're running the tests from a server or not
    363     QUnit.isLocal = !!(location.protocol === 'file:');
    364 })();
    365 
    366 // Expose the API as global variables, unless an 'exports'
    367 // object exists, in that case we assume we're in CommonJS
    368 if ( typeof exports === "undefined" || typeof require === "undefined" ) {
    369     extend(window, QUnit);
    370     window.QUnit = QUnit;
    371 } else {
    372     extend(exports, QUnit);
    373     exports.QUnit = QUnit;
    374 }
    375 
    376 // define these after exposing globals to keep them in these QUnit namespace only
    377 extend(QUnit, {
     319    QUnit.isLocal = location.protocol === "file:";
     320}());
     321
     322extend( QUnit, {
     323
    378324    config: config,
    379325
    380326    // Initialize the configuration options
    381327    init: function() {
    382         extend(config, {
     328        extend( config, {
    383329            stats: { all: 0, bad: 0 },
    384330            moduleStats: { all: 0, bad: 0 },
    385             started: +new Date,
     331            started: +new Date(),
    386332            updateRate: 1000,
    387333            blocking: false,
    388334            autostart: true,
    389335            autorun: false,
    390             assertions: [],
    391             filters: [],
    392             queue: []
     336            filter: "",
     337            queue: [],
     338            semaphore: 1
    393339        });
    394340
    395         var tests = id("qunit-tests"),
    396             banner = id("qunit-banner"),
    397             result = id("qunit-testresult");
     341        var tests, banner, result,
     342            qunit = id( "qunit" );
     343
     344        if ( qunit ) {
     345            qunit.innerHTML =
     346                "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
     347                "<h2 id='qunit-banner'></h2>" +
     348                "<div id='qunit-testrunner-toolbar'></div>" +
     349                "<h2 id='qunit-userAgent'></h2>" +
     350                "<ol id='qunit-tests'></ol>";
     351        }
     352
     353        tests = id( "qunit-tests" );
     354        banner = id( "qunit-banner" );
     355        result = id( "qunit-testresult" );
    398356
    399357        if ( tests ) {
     
    408366            result.parentNode.removeChild( result );
    409367        }
    410     },
    411    
    412     /**
    413      * Resets the test setup. Useful for tests that modify the DOM.
    414      */
     368
     369        if ( tests ) {
     370            result = document.createElement( "p" );
     371            result.id = "qunit-testresult";
     372            result.className = "result";
     373            tests.parentNode.insertBefore( result, tests );
     374            result.innerHTML = "Running...<br/>&nbsp;";
     375        }
     376    },
     377
     378    // Resets the test setup. Useful for tests that modify the DOM.
     379    /*
     380    DEPRECATED: Use multiple tests instead of resetting inside a test.
     381    Use testStart or testDone for custom cleanup.
     382    This method will throw an error in 2.0, and will be removed in 2.1
     383    */
    415384    reset: function() {
    416         if ( window.jQuery ) {
    417             jQuery("#main, #qunit-fixture").html( config.fixture );
    418         }
    419     },
    420    
    421     /**
    422      * Trigger an event on an element.
    423      *
    424      * @example triggerEvent( document.body, "click" );
    425      *
    426      * @param DOMElement elem
    427      * @param String type
    428      */
    429     triggerEvent: function( elem, type, event ) {
    430         if ( document.createEvent ) {
    431             event = document.createEvent("MouseEvents");
    432             event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
    433                 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    434             elem.dispatchEvent( event );
    435 
    436         } else if ( elem.fireEvent ) {
    437             elem.fireEvent("on"+type);
    438         }
    439     },
    440    
     385        var fixture = id( "qunit-fixture" );
     386        if ( fixture ) {
     387            fixture.innerHTML = config.fixture;
     388        }
     389    },
     390
    441391    // Safe object type checking
    442392    is: function( type, obj ) {
    443         return QUnit.objectType( obj ) == type;
    444     },
    445    
     393        return QUnit.objectType( obj ) === type;
     394    },
     395
    446396    objectType: function( obj ) {
    447         if (typeof obj === "undefined") {
    448                 return "undefined";
    449 
    450         // consider: typeof null === object
    451         }
    452         if (obj === null) {
    453                 return "null";
    454         }
    455 
    456         var type = Object.prototype.toString.call( obj )
    457             .match(/^\[object\s(.*)\]$/)[1] || '';
    458 
    459         switch (type) {
    460                 case 'Number':
    461                         if (isNaN(obj)) {
    462                                 return "nan";
    463                         } else {
    464                                 return "number";
    465                         }
    466                 case 'String':
    467                 case 'Boolean':
    468                 case 'Array':
    469                 case 'Date':
    470                 case 'RegExp':
    471                 case 'Function':
    472                         return type.toLowerCase();
    473         }
    474         if (typeof obj === "object") {
    475                 return "object";
     397        if ( typeof obj === "undefined" ) {
     398            return "undefined";
     399        }
     400
     401        // Consider: typeof null === object
     402        if ( obj === null ) {
     403            return "null";
     404        }
     405
     406        var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
     407            type = match && match[1] || "";
     408
     409        switch ( type ) {
     410            case "Number":
     411                if ( isNaN(obj) ) {
     412                    return "nan";
     413                }
     414                return "number";
     415            case "String":
     416            case "Boolean":
     417            case "Array":
     418            case "Date":
     419            case "RegExp":
     420            case "Function":
     421                return type.toLowerCase();
     422        }
     423        if ( typeof obj === "object" ) {
     424            return "object";
    476425        }
    477426        return undefined;
    478427    },
    479    
    480     // Logging callbacks
    481     begin: function() {},
    482     done: function(failures, total) {},
    483     log: function(result, message) {},
    484     testStart: function(name, testEnvironment) {},
    485     testDone: function(name, failures, total) {},
    486     moduleStart: function(name, testEnvironment) {},
    487     moduleDone: function(name, failures, total) {}
     428
     429    push: function( result, actual, expected, message ) {
     430        if ( !config.current ) {
     431            throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
     432        }
     433
     434        var output, source,
     435            details = {
     436                module: config.current.module,
     437                name: config.current.testName,
     438                result: result,
     439                message: message,
     440                actual: actual,
     441                expected: expected
     442            };
     443
     444        message = escapeText( message ) || ( result ? "okay" : "failed" );
     445        message = "<span class='test-message'>" + message + "</span>";
     446        output = message;
     447
     448        if ( !result ) {
     449            expected = escapeText( QUnit.jsDump.parse(expected) );
     450            actual = escapeText( QUnit.jsDump.parse(actual) );
     451            output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>";
     452
     453            if ( actual !== expected ) {
     454                output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>";
     455                output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>";
     456            }
     457
     458            source = sourceFromStacktrace();
     459
     460            if ( source ) {
     461                details.source = source;
     462                output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
     463            }
     464
     465            output += "</table>";
     466        }
     467
     468        runLoggingCallbacks( "log", QUnit, details );
     469
     470        config.current.assertions.push({
     471            result: !!result,
     472            message: output
     473        });
     474    },
     475
     476    pushFailure: function( message, source, actual ) {
     477        if ( !config.current ) {
     478            throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
     479        }
     480
     481        var output,
     482            details = {
     483                module: config.current.module,
     484                name: config.current.testName,
     485                result: false,
     486                message: message
     487            };
     488
     489        message = escapeText( message ) || "error";
     490        message = "<span class='test-message'>" + message + "</span>";
     491        output = message;
     492
     493        output += "<table>";
     494
     495        if ( actual ) {
     496            output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>";
     497        }
     498
     499        if ( source ) {
     500            details.source = source;
     501            output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
     502        }
     503
     504        output += "</table>";
     505
     506        runLoggingCallbacks( "log", QUnit, details );
     507
     508        config.current.assertions.push({
     509            result: false,
     510            message: output
     511        });
     512    },
     513
     514    url: function( params ) {
     515        params = extend( extend( {}, QUnit.urlParams ), params );
     516        var key,
     517            querystring = "?";
     518
     519        for ( key in params ) {
     520            if ( hasOwn.call( params, key ) ) {
     521                querystring += encodeURIComponent( key ) + "=" +
     522                    encodeURIComponent( params[ key ] ) + "&";
     523            }
     524        }
     525        return window.location.protocol + "//" + window.location.host +
     526            window.location.pathname + querystring.slice( 0, -1 );
     527    },
     528
     529    extend: extend,
     530    id: id,
     531    addEvent: addEvent,
     532    addClass: addClass,
     533    hasClass: hasClass,
     534    removeClass: removeClass
     535    // load, equiv, jsDump, diff: Attached later
    488536});
    489537
    490 if ( typeof document === "undefined" || document.readyState === "complete" ) {
     538/**
     539 * @deprecated: Created for backwards compatibility with test runner that set the hook function
     540 * into QUnit.{hook}, instead of invoking it and passing the hook function.
     541 * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
     542 * Doing this allows us to tell if the following methods have been overwritten on the actual
     543 * QUnit object.
     544 */
     545extend( QUnit.constructor.prototype, {
     546
     547    // Logging callbacks; all receive a single argument with the listed properties
     548    // run test/logs.html for any related changes
     549    begin: registerLoggingCallback( "begin" ),
     550
     551    // done: { failed, passed, total, runtime }
     552    done: registerLoggingCallback( "done" ),
     553
     554    // log: { result, actual, expected, message }
     555    log: registerLoggingCallback( "log" ),
     556
     557    // testStart: { name }
     558    testStart: registerLoggingCallback( "testStart" ),
     559
     560    // testDone: { name, failed, passed, total, runtime }
     561    testDone: registerLoggingCallback( "testDone" ),
     562
     563    // moduleStart: { name }
     564    moduleStart: registerLoggingCallback( "moduleStart" ),
     565
     566    // moduleDone: { name, failed, passed, total }
     567    moduleDone: registerLoggingCallback( "moduleDone" )
     568});
     569
     570if ( !defined.document || document.readyState === "complete" ) {
    491571    config.autorun = true;
    492572}
    493573
    494 addEvent(window, "load", function() {
    495     QUnit.begin();
    496    
     574QUnit.load = function() {
     575    runLoggingCallbacks( "begin", QUnit, {} );
     576
    497577    // Initialize the config, saving the execution queue
    498     var oldconfig = extend({}, config);
     578    var banner, filter, i, j, label, len, main, ol, toolbar, val, selection,
     579        urlConfigContainer, moduleFilter, userAgent,
     580        numModules = 0,
     581        moduleNames = [],
     582        moduleFilterHtml = "",
     583        urlConfigHtml = "",
     584        oldconfig = extend( {}, config );
     585
    499586    QUnit.init();
    500587    extend(config, oldconfig);
     
    502589    config.blocking = false;
    503590
    504     var userAgent = id("qunit-userAgent");
     591    len = config.urlConfig.length;
     592
     593    for ( i = 0; i < len; i++ ) {
     594        val = config.urlConfig[i];
     595        if ( typeof val === "string" ) {
     596            val = {
     597                id: val,
     598                label: val
     599            };
     600        }
     601        config[ val.id ] = QUnit.urlParams[ val.id ];
     602        if ( !val.value || typeof val.value === "string" ) {
     603            urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) +
     604                "' name='" + escapeText( val.id ) +
     605                "' type='checkbox'" +
     606                ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
     607                ( config[ val.id ] ? " checked='checked'" : "" ) +
     608                " title='" + escapeText( val.tooltip ) +
     609                "'><label for='qunit-urlconfig-" + escapeText( val.id ) +
     610                "' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>";
     611        } else {
     612            urlConfigHtml += "<label for='qunit-urlconfig-" + escapeText( val.id ) +
     613                "' title='" + escapeText( val.tooltip ) +
     614                "'>" + val.label +
     615                ": </label><select id='qunit-urlconfig-" + escapeText( val.id ) +
     616                "' name='" + escapeText( val.id ) +
     617                "' title='" + escapeText( val.tooltip ) +
     618                "'><option></option>";
     619            selection = false;
     620            if ( QUnit.is( "array", val.value ) ) {
     621                for ( j = 0; j < val.value.length; j++ ) {
     622                    urlConfigHtml += "<option value='" + escapeText( val.value[j] ) + "'" +
     623                        ( config[ val.id ] === val.value[j] ?
     624                            (selection = true) && " selected='selected'" :
     625                            "" ) +
     626                        ">" + escapeText( val.value[j] ) + "</option>";
     627                }
     628            } else {
     629                for ( j in val.value ) {
     630                    if ( hasOwn.call( val.value, j ) ) {
     631                        urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
     632                            ( config[ val.id ] === j ?
     633                                (selection = true) && " selected='selected'" :
     634                                "" ) +
     635                            ">" + escapeText( val.value[j] ) + "</option>";
     636                    }
     637                }
     638            }
     639            if ( config[ val.id ] && !selection ) {
     640                urlConfigHtml += "<option value='" + escapeText( config[ val.id ] ) +
     641                    "' selected='selected' disabled='disabled'>" +
     642                    escapeText( config[ val.id ] ) +
     643                    "</option>";
     644            }
     645            urlConfigHtml += "</select>";
     646        }
     647    }
     648    for ( i in config.modules ) {
     649        if ( config.modules.hasOwnProperty( i ) ) {
     650            moduleNames.push(i);
     651        }
     652    }
     653    numModules = moduleNames.length;
     654    moduleNames.sort( function( a, b ) {
     655        return a.localeCompare( b );
     656    });
     657    moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " +
     658        ( config.module === undefined  ? "selected='selected'" : "" ) +
     659        ">< All Modules ></option>";
     660
     661
     662    for ( i = 0; i < numModules; i++) {
     663            moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(moduleNames[i]) ) + "' " +
     664                ( config.module === moduleNames[i] ? "selected='selected'" : "" ) +
     665                ">" + escapeText(moduleNames[i]) + "</option>";
     666    }
     667    moduleFilterHtml += "</select>";
     668
     669    // `userAgent` initialized at top of scope
     670    userAgent = id( "qunit-userAgent" );
    505671    if ( userAgent ) {
    506672        userAgent.innerHTML = navigator.userAgent;
    507673    }
    508    
    509     var toolbar = id("qunit-testrunner-toolbar");
     674
     675    // `banner` initialized at top of scope
     676    banner = id( "qunit-header" );
     677    if ( banner ) {
     678        banner.innerHTML = "<a href='" + QUnit.url({ filter: undefined, module: undefined, testNumber: undefined }) + "'>" + banner.innerHTML + "</a> ";
     679    }
     680
     681    // `toolbar` initialized at top of scope
     682    toolbar = id( "qunit-testrunner-toolbar" );
    510683    if ( toolbar ) {
    511         toolbar.style.display = "none";
    512        
    513         var filter = document.createElement("input");
     684        // `filter` initialized at top of scope
     685        filter = document.createElement( "input" );
    514686        filter.type = "checkbox";
    515687        filter.id = "qunit-filter-pass";
    516         filter.disabled = true;
     688
    517689        addEvent( filter, "click", function() {
    518             var li = document.getElementsByTagName("li");
    519             for ( var i = 0; i < li.length; i++ ) {
    520                 if ( li[i].className.indexOf("pass") > -1 ) {
    521                     li[i].style.display = filter.checked ? "none" : "";
     690            var tmp,
     691                ol = id( "qunit-tests" );
     692
     693            if ( filter.checked ) {
     694                ol.className = ol.className + " hidepass";
     695            } else {
     696                tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
     697                ol.className = tmp.replace( / hidepass /, " " );
     698            }
     699            if ( defined.sessionStorage ) {
     700                if (filter.checked) {
     701                    sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
     702                } else {
     703                    sessionStorage.removeItem( "qunit-filter-passed-tests" );
    522704                }
    523705            }
    524706        });
     707
     708        if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
     709            filter.checked = true;
     710            // `ol` initialized at top of scope
     711            ol = id( "qunit-tests" );
     712            ol.className = ol.className + " hidepass";
     713        }
    525714        toolbar.appendChild( filter );
    526715
    527         var label = document.createElement("label");
    528         label.setAttribute("for", "qunit-filter-pass");
     716        // `label` initialized at top of scope
     717        label = document.createElement( "label" );
     718        label.setAttribute( "for", "qunit-filter-pass" );
     719        label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." );
    529720        label.innerHTML = "Hide passed tests";
    530721        toolbar.appendChild( label );
    531722
    532         var missing = document.createElement("input");
    533         missing.type = "checkbox";
    534         missing.id = "qunit-filter-missing";
    535         missing.disabled = true;
    536         addEvent( missing, "click", function() {
    537             var li = document.getElementsByTagName("li");
    538             for ( var i = 0; i < li.length; i++ ) {
    539                 if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
    540                     li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
    541                 }
    542             }
     723        urlConfigContainer = document.createElement("span");
     724        urlConfigContainer.innerHTML = urlConfigHtml;
     725        // For oldIE support:
     726        // * Add handlers to the individual elements instead of the container
     727        // * Use "click" instead of "change" for checkboxes
     728        // * Fallback from event.target to event.srcElement
     729        addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) {
     730            var params = {},
     731                target = event.target || event.srcElement;
     732            params[ target.name ] = target.checked ?
     733                target.defaultValue || true :
     734                undefined;
     735            window.location = QUnit.url( params );
    543736        });
    544         toolbar.appendChild( missing );
    545 
    546         label = document.createElement("label");
    547         label.setAttribute("for", "qunit-filter-missing");
    548         label.innerHTML = "Hide missing tests (untested code is broken code)";
    549         toolbar.appendChild( label );
    550     }
    551 
    552     var main = id('main') || id('qunit-fixture');
     737        addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) {
     738            var params = {},
     739                target = event.target || event.srcElement;
     740            params[ target.name ] = target.options[ target.selectedIndex ].value || undefined;
     741            window.location = QUnit.url( params );
     742        });
     743        toolbar.appendChild( urlConfigContainer );
     744
     745        if (numModules > 1) {
     746            moduleFilter = document.createElement( "span" );
     747            moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
     748            moduleFilter.innerHTML = moduleFilterHtml;
     749            addEvent( moduleFilter.lastChild, "change", function() {
     750                var selectBox = moduleFilter.getElementsByTagName("select")[0],
     751                    selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
     752
     753                window.location = QUnit.url({
     754                    module: ( selectedModule === "" ) ? undefined : selectedModule,
     755                    // Remove any existing filters
     756                    filter: undefined,
     757                    testNumber: undefined
     758                });
     759            });
     760            toolbar.appendChild(moduleFilter);
     761        }
     762    }
     763
     764    // `main` initialized at top of scope
     765    main = id( "qunit-fixture" );
    553766    if ( main ) {
    554767        config.fixture = main.innerHTML;
    555768    }
    556769
    557     if (config.autostart) {
     770    if ( config.autostart ) {
    558771        QUnit.start();
    559772    }
    560 });
     773};
     774
     775if ( defined.document ) {
     776    addEvent( window, "load", QUnit.load );
     777}
     778
     779// `onErrorFnPrev` initialized at top of scope
     780// Preserve other handlers
     781onErrorFnPrev = window.onerror;
     782
     783// Cover uncaught exceptions
     784// Returning true will suppress the default browser handler,
     785// returning false will let it run.
     786window.onerror = function ( error, filePath, linerNr ) {
     787    var ret = false;
     788    if ( onErrorFnPrev ) {
     789        ret = onErrorFnPrev( error, filePath, linerNr );
     790    }
     791
     792    // Treat return value as window.onerror itself does,
     793    // Only do our handling if not suppressed.
     794    if ( ret !== true ) {
     795        if ( QUnit.config.current ) {
     796            if ( QUnit.config.current.ignoreGlobalErrors ) {
     797                return true;
     798            }
     799            QUnit.pushFailure( error, filePath + ":" + linerNr );
     800        } else {
     801            QUnit.test( "global failure", extend( function() {
     802                QUnit.pushFailure( error, filePath + ":" + linerNr );
     803            }, { validTest: validTest } ) );
     804        }
     805        return false;
     806    }
     807
     808    return ret;
     809};
    561810
    562811function done() {
    563     if ( config.doneTimer && window.clearTimeout ) {
    564         window.clearTimeout( config.doneTimer );
    565         config.doneTimer = null;
    566     }
    567 
    568     if ( config.queue.length ) {
    569         config.doneTimer = window.setTimeout(function(){
    570             if ( !config.queue.length ) {
    571                 done();
    572             } else {
    573                 synchronize( done );
    574             }
    575         }, 13);
    576 
    577         return;
    578     }
    579 
    580812    config.autorun = true;
    581813
    582814    // Log the last module results
    583     if ( config.currentModule ) {
    584         QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
    585     }
    586 
    587     var banner = id("qunit-banner"),
    588         tests = id("qunit-tests"),
    589         html = ['Tests completed in ',
    590         +new Date - config.started, ' milliseconds.<br/>',
    591         '<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
     815    if ( config.previousModule ) {
     816        runLoggingCallbacks( "moduleDone", QUnit, {
     817            name: config.previousModule,
     818            failed: config.moduleStats.bad,
     819            passed: config.moduleStats.all - config.moduleStats.bad,
     820            total: config.moduleStats.all
     821        });
     822    }
     823    delete config.previousModule;
     824
     825    var i, key,
     826        banner = id( "qunit-banner" ),
     827        tests = id( "qunit-tests" ),
     828        runtime = +new Date() - config.started,
     829        passed = config.stats.all - config.stats.bad,
     830        html = [
     831            "Tests completed in ",
     832            runtime,
     833            " milliseconds.<br/>",
     834            "<span class='passed'>",
     835            passed,
     836            "</span> assertions of <span class='total'>",
     837            config.stats.all,
     838            "</span> passed, <span class='failed'>",
     839            config.stats.bad,
     840            "</span> failed."
     841        ].join( "" );
    592842
    593843    if ( banner ) {
    594         banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
    595     }
    596 
    597     if ( tests ) { 
    598         var result = id("qunit-testresult");
    599 
    600         if ( !result ) {
    601             result = document.createElement("p");
    602             result.id = "qunit-testresult";
    603             result.className = "result";
    604             tests.parentNode.insertBefore( result, tests.nextSibling );
    605         }
    606 
    607         result.innerHTML = html;
    608     }
    609 
    610     QUnit.done( config.stats.bad, config.stats.all );
    611 }
    612 
    613 function validTest( name ) {
    614     var i = config.filters.length,
    615         run = false;
    616 
    617     if ( !i ) {
     844        banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
     845    }
     846
     847    if ( tests ) {
     848        id( "qunit-testresult" ).innerHTML = html;
     849    }
     850
     851    if ( config.altertitle && defined.document && document.title ) {
     852        // show ✖ for good, ✔ for bad suite result in title
     853        // use escape sequences in case file gets loaded with non-utf-8-charset
     854        document.title = [
     855            ( config.stats.bad ? "\u2716" : "\u2714" ),
     856            document.title.replace( /^[\u2714\u2716] /i, "" )
     857        ].join( " " );
     858    }
     859
     860    // clear own sessionStorage items if all tests passed
     861    if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
     862        // `key` & `i` initialized at top of scope
     863        for ( i = 0; i < sessionStorage.length; i++ ) {
     864            key = sessionStorage.key( i++ );
     865            if ( key.indexOf( "qunit-test-" ) === 0 ) {
     866                sessionStorage.removeItem( key );
     867            }
     868        }
     869    }
     870
     871    // scroll back to top to show results
     872    if ( config.scrolltop && window.scrollTo ) {
     873        window.scrollTo(0, 0);
     874    }
     875
     876    runLoggingCallbacks( "done", QUnit, {
     877        failed: config.stats.bad,
     878        passed: passed,
     879        total: config.stats.all,
     880        runtime: runtime
     881    });
     882}
     883
     884/** @return Boolean: true if this test should be ran */
     885function validTest( test ) {
     886    var include,
     887        filter = config.filter && config.filter.toLowerCase(),
     888        module = config.module && config.module.toLowerCase(),
     889        fullName = ( test.module + ": " + test.testName ).toLowerCase();
     890
     891    // Internally-generated tests are always valid
     892    if ( test.callback && test.callback.validTest === validTest ) {
     893        delete test.callback.validTest;
    618894        return true;
    619895    }
    620    
    621     while ( i-- ) {
    622         var filter = config.filters[i],
    623             not = filter.charAt(0) == '!';
    624 
    625         if ( not ) {
    626             filter = filter.slice(1);
    627         }
    628 
    629         if ( name.indexOf(filter) !== -1 ) {
    630             return !not;
    631         }
    632 
    633         if ( not ) {
    634             run = true;
    635         }
    636     }
    637 
    638     return run;
    639 }
    640 
    641 function escapeHtml(s) {
    642     s = s === null ? "" : s + "";
    643     return s.replace(/[\&"<>\\]/g, function(s) {
    644         switch(s) {
    645             case "&": return "&amp;";
    646             case "\\": return "\\\\";
    647             case '"': return '\"';
    648             case "<": return "&lt;";
    649             case ">": return "&gt;";
    650             default: return s;
     896
     897    if ( config.testNumber.length > 0 ) {
     898        if ( inArray( test.testNumber, config.testNumber ) < 0 ) {
     899            return false;
     900        }
     901    }
     902
     903    if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
     904        return false;
     905    }
     906
     907    if ( !filter ) {
     908        return true;
     909    }
     910
     911    include = filter.charAt( 0 ) !== "!";
     912    if ( !include ) {
     913        filter = filter.slice( 1 );
     914    }
     915
     916    // If the filter matches, we need to honour include
     917    if ( fullName.indexOf( filter ) !== -1 ) {
     918        return include;
     919    }
     920
     921    // Otherwise, do the opposite
     922    return !include;
     923}
     924
     925// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
     926// Later Safari and IE10 are supposed to support error.stack as well
     927// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
     928function extractStacktrace( e, offset ) {
     929    offset = offset === undefined ? 3 : offset;
     930
     931    var stack, include, i;
     932
     933    if ( e.stacktrace ) {
     934        // Opera
     935        return e.stacktrace.split( "\n" )[ offset + 3 ];
     936    } else if ( e.stack ) {
     937        // Firefox, Chrome
     938        stack = e.stack.split( "\n" );
     939        if (/^error$/i.test( stack[0] ) ) {
     940            stack.shift();
     941        }
     942        if ( fileName ) {
     943            include = [];
     944            for ( i = offset; i < stack.length; i++ ) {
     945                if ( stack[ i ].indexOf( fileName ) !== -1 ) {
     946                    break;
     947                }
     948                include.push( stack[ i ] );
     949            }
     950            if ( include.length ) {
     951                return include.join( "\n" );
     952            }
     953        }
     954        return stack[ offset ];
     955    } else if ( e.sourceURL ) {
     956        // Safari, PhantomJS
     957        // hopefully one day Safari provides actual stacktraces
     958        // exclude useless self-reference for generated Error objects
     959        if ( /qunit.js$/.test( e.sourceURL ) ) {
     960            return;
     961        }
     962        // for actual exceptions, this is useful
     963        return e.sourceURL + ":" + e.line;
     964    }
     965}
     966function sourceFromStacktrace( offset ) {
     967    try {
     968        throw new Error();
     969    } catch ( e ) {
     970        return extractStacktrace( e, offset );
     971    }
     972}
     973
     974/**
     975 * Escape text for attribute or text content.
     976 */
     977function escapeText( s ) {
     978    if ( !s ) {
     979        return "";
     980    }
     981    s = s + "";
     982    // Both single quotes and double quotes (for attributes)
     983    return s.replace( /['"<>&]/g, function( s ) {
     984        switch( s ) {
     985            case "'":
     986                return "&#039;";
     987            case "\"":
     988                return "&quot;";
     989            case "<":
     990                return "&lt;";
     991            case ">":
     992                return "&gt;";
     993            case "&":
     994                return "&amp;";
    651995        }
    652996    });
    653997}
    654998
    655 function push(result, actual, expected, message) {
    656     message = escapeHtml(message) || (result ? "okay" : "failed");
    657     message = '<span class="test-message">' + message + "</span>";
    658     expected = escapeHtml(QUnit.jsDump.parse(expected));
    659     actual = escapeHtml(QUnit.jsDump.parse(actual));
    660     var output = message + ', expected: <span class="test-expected">' + expected + '</span>';
    661     if (actual != expected) {
    662         output += ' result: <span class="test-actual">' + actual + '</span>, diff: ' + QUnit.diff(expected, actual);
    663     }
    664    
    665     // can't use ok, as that would double-escape messages
    666     QUnit.log(result, output);
    667     config.assertions.push({
    668         result: !!result,
    669         message: output
    670     });
    671 }
    672 
    673 function synchronize( callback ) {
     999function synchronize( callback, last ) {
    6741000    config.queue.push( callback );
    6751001
    6761002    if ( config.autorun && !config.blocking ) {
    677         process();
    678     }
    679 }
    680 
    681 function process() {
    682     var start = (new Date()).getTime();
     1003        process( last );
     1004    }
     1005}
     1006
     1007function process( last ) {
     1008    function next() {
     1009        process( last );
     1010    }
     1011    var start = new Date().getTime();
     1012    config.depth = config.depth ? config.depth + 1 : 1;
    6831013
    6841014    while ( config.queue.length && !config.blocking ) {
    685         if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
     1015        if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
    6861016            config.queue.shift()();
    687 
    6881017        } else {
    689             setTimeout( process, 13 );
     1018            setTimeout( next, 13 );
    6901019            break;
    6911020        }
     1021    }
     1022    config.depth--;
     1023    if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
     1024        done();
    6921025    }
    6931026}
     
    6951028function saveGlobal() {
    6961029    config.pollution = [];
    697    
     1030
    6981031    if ( config.noglobals ) {
    6991032        for ( var key in window ) {
    700             config.pollution.push( key );
    701         }
    702     }
    703 }
    704 
    705 function checkPollution( name ) {
    706     var old = config.pollution;
     1033            if ( hasOwn.call( window, key ) ) {
     1034                // in Opera sometimes DOM element ids show up here, ignore them
     1035                if ( /^qunit-test-output/.test( key ) ) {
     1036                    continue;
     1037                }
     1038                config.pollution.push( key );
     1039            }
     1040        }
     1041    }
     1042}
     1043
     1044function checkPollution() {
     1045    var newGlobals,
     1046        deletedGlobals,
     1047        old = config.pollution;
     1048
    7071049    saveGlobal();
    708    
    709     var newGlobals = diff( old, config.pollution );
     1050
     1051    newGlobals = diff( config.pollution, old );
    7101052    if ( newGlobals.length > 0 ) {
    711         ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
    712         config.expected++;
    713     }
    714 
    715     var deletedGlobals = diff( config.pollution, old );
     1053        QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
     1054    }
     1055
     1056    deletedGlobals = diff( old, config.pollution );
    7161057    if ( deletedGlobals.length > 0 ) {
    717         ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
    718         config.expected++;
     1058        QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
    7191059    }
    7201060}
     
    7221062// returns a new Array with the elements that are in a but not in b
    7231063function diff( a, b ) {
    724     var result = a.slice();
    725     for ( var i = 0; i < result.length; i++ ) {
    726         for ( var j = 0; j < b.length; j++ ) {
     1064    var i, j,
     1065        result = a.slice();
     1066
     1067    for ( i = 0; i < result.length; i++ ) {
     1068        for ( j = 0; j < b.length; j++ ) {
    7271069            if ( result[i] === b[j] ) {
    728                 result.splice(i, 1);
     1070                result.splice( i, 1 );
    7291071                i--;
    7301072                break;
     
    7351077}
    7361078
    737 function fail(message, exception, callback) {
    738     if ( typeof console !== "undefined" && console.error && console.warn ) {
    739         console.error(message);
    740         console.error(exception);
    741         console.warn(callback.toString());
    742 
    743     } else if ( window.opera && opera.postError ) {
    744         opera.postError(message, exception, callback.toString);
    745     }
    746 }
    747 
    748 function extend(a, b) {
     1079function extend( a, b ) {
    7491080    for ( var prop in b ) {
    750         a[prop] = b[prop];
     1081        if ( hasOwn.call( b, prop ) ) {
     1082            // Avoid "Member not found" error in IE8 caused by messing with window.constructor
     1083            if ( !( prop === "constructor" && a === window ) ) {
     1084                if ( b[ prop ] === undefined ) {
     1085                    delete a[ prop ];
     1086                } else {
     1087                    a[ prop ] = b[ prop ];
     1088                }
     1089            }
     1090        }
    7511091    }
    7521092
     
    7541094}
    7551095
    756 function addEvent(elem, type, fn) {
     1096/**
     1097 * @param {HTMLElement} elem
     1098 * @param {string} type
     1099 * @param {Function} fn
     1100 */
     1101function addEvent( elem, type, fn ) {
    7571102    if ( elem.addEventListener ) {
     1103
     1104        // Standards-based browsers
    7581105        elem.addEventListener( type, fn, false );
    7591106    } else if ( elem.attachEvent ) {
     1107
     1108        // support: IE <9
    7601109        elem.attachEvent( "on" + type, fn );
    7611110    } else {
    762         fn();
    763     }
    764 }
    765 
    766 function id(name) {
    767     return !!(typeof document !== "undefined" && document && document.getElementById) &&
    768         document.getElementById( name );
    769 }
     1111
     1112        // Caller must ensure support for event listeners is present
     1113        throw new Error( "addEvent() was called in a context without event listener support" );
     1114    }
     1115}
     1116
     1117/**
     1118 * @param {Array|NodeList} elems
     1119 * @param {string} type
     1120 * @param {Function} fn
     1121 */
     1122function addEvents( elems, type, fn ) {
     1123    var i = elems.length;
     1124    while ( i-- ) {
     1125        addEvent( elems[i], type, fn );
     1126    }
     1127}
     1128
     1129function hasClass( elem, name ) {
     1130    return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
     1131}
     1132
     1133function addClass( elem, name ) {
     1134    if ( !hasClass( elem, name ) ) {
     1135        elem.className += (elem.className ? " " : "") + name;
     1136    }
     1137}
     1138
     1139function removeClass( elem, name ) {
     1140    var set = " " + elem.className + " ";
     1141    // Class name may appear multiple times
     1142    while ( set.indexOf(" " + name + " ") > -1 ) {
     1143        set = set.replace(" " + name + " " , " ");
     1144    }
     1145    // If possible, trim it for prettiness, but not necessarily
     1146    elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, "");
     1147}
     1148
     1149function id( name ) {
     1150    return defined.document && document.getElementById && document.getElementById( name );
     1151}
     1152
     1153function registerLoggingCallback( key ) {
     1154    return function( callback ) {
     1155        config[key].push( callback );
     1156    };
     1157}
     1158
     1159// Supports deprecated method of completely overwriting logging callbacks
     1160function runLoggingCallbacks( key, scope, args ) {
     1161    var i, callbacks;
     1162    if ( QUnit.hasOwnProperty( key ) ) {
     1163        QUnit[ key ].call(scope, args );
     1164    } else {
     1165        callbacks = config[ key ];
     1166        for ( i = 0; i < callbacks.length; i++ ) {
     1167            callbacks[ i ].call( scope, args );
     1168        }
     1169    }
     1170}
     1171
     1172// from jquery.js
     1173function inArray( elem, array ) {
     1174    if ( array.indexOf ) {
     1175        return array.indexOf( elem );
     1176    }
     1177
     1178    for ( var i = 0, length = array.length; i < length; i++ ) {
     1179        if ( array[ i ] === elem ) {
     1180            return i;
     1181        }
     1182    }
     1183
     1184    return -1;
     1185}
     1186
     1187function Test( settings ) {
     1188    extend( this, settings );
     1189    this.assertions = [];
     1190    this.testNumber = ++Test.count;
     1191}
     1192
     1193Test.count = 0;
     1194
     1195Test.prototype = {
     1196    init: function() {
     1197        var a, b, li,
     1198            tests = id( "qunit-tests" );
     1199
     1200        if ( tests ) {
     1201            b = document.createElement( "strong" );
     1202            b.innerHTML = this.nameHtml;
     1203
     1204            // `a` initialized at top of scope
     1205            a = document.createElement( "a" );
     1206            a.innerHTML = "Rerun";
     1207            a.href = QUnit.url({ testNumber: this.testNumber });
     1208
     1209            li = document.createElement( "li" );
     1210            li.appendChild( b );
     1211            li.appendChild( a );
     1212            li.className = "running";
     1213            li.id = this.id = "qunit-test-output" + testId++;
     1214
     1215            tests.appendChild( li );
     1216        }
     1217    },
     1218    setup: function() {
     1219        if (
     1220            // Emit moduleStart when we're switching from one module to another
     1221            this.module !== config.previousModule ||
     1222                // They could be equal (both undefined) but if the previousModule property doesn't
     1223                // yet exist it means this is the first test in a suite that isn't wrapped in a
     1224                // module, in which case we'll just emit a moduleStart event for 'undefined'.
     1225                // Without this, reporters can get testStart before moduleStart  which is a problem.
     1226                !hasOwn.call( config, "previousModule" )
     1227        ) {
     1228            if ( hasOwn.call( config, "previousModule" ) ) {
     1229                runLoggingCallbacks( "moduleDone", QUnit, {
     1230                    name: config.previousModule,
     1231                    failed: config.moduleStats.bad,
     1232                    passed: config.moduleStats.all - config.moduleStats.bad,
     1233                    total: config.moduleStats.all
     1234                });
     1235            }
     1236            config.previousModule = this.module;
     1237            config.moduleStats = { all: 0, bad: 0 };
     1238            runLoggingCallbacks( "moduleStart", QUnit, {
     1239                name: this.module
     1240            });
     1241        }
     1242
     1243        config.current = this;
     1244
     1245        this.testEnvironment = extend({
     1246            setup: function() {},
     1247            teardown: function() {}
     1248        }, this.moduleTestEnvironment );
     1249
     1250        this.started = +new Date();
     1251        runLoggingCallbacks( "testStart", QUnit, {
     1252            name: this.testName,
     1253            module: this.module
     1254        });
     1255
     1256        /*jshint camelcase:false */
     1257
     1258
     1259        /**
     1260         * Expose the current test environment.
     1261         *
     1262         * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
     1263         */
     1264        QUnit.current_testEnvironment = this.testEnvironment;
     1265
     1266        /*jshint camelcase:true */
     1267
     1268        if ( !config.pollution ) {
     1269            saveGlobal();
     1270        }
     1271        if ( config.notrycatch ) {
     1272            this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
     1273            return;
     1274        }
     1275        try {
     1276            this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
     1277        } catch( e ) {
     1278            QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
     1279        }
     1280    },
     1281    run: function() {
     1282        config.current = this;
     1283
     1284        var running = id( "qunit-testresult" );
     1285
     1286        if ( running ) {
     1287            running.innerHTML = "Running: <br/>" + this.nameHtml;
     1288        }
     1289
     1290        if ( this.async ) {
     1291            QUnit.stop();
     1292        }
     1293
     1294        this.callbackStarted = +new Date();
     1295
     1296        if ( config.notrycatch ) {
     1297            this.callback.call( this.testEnvironment, QUnit.assert );
     1298            this.callbackRuntime = +new Date() - this.callbackStarted;
     1299            return;
     1300        }
     1301
     1302        try {
     1303            this.callback.call( this.testEnvironment, QUnit.assert );
     1304            this.callbackRuntime = +new Date() - this.callbackStarted;
     1305        } catch( e ) {
     1306            this.callbackRuntime = +new Date() - this.callbackStarted;
     1307
     1308            QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
     1309            // else next test will carry the responsibility
     1310            saveGlobal();
     1311
     1312            // Restart the tests if they're blocking
     1313            if ( config.blocking ) {
     1314                QUnit.start();
     1315            }
     1316        }
     1317    },
     1318    teardown: function() {
     1319        config.current = this;
     1320        if ( config.notrycatch ) {
     1321            if ( typeof this.callbackRuntime === "undefined" ) {
     1322                this.callbackRuntime = +new Date() - this.callbackStarted;
     1323            }
     1324            this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
     1325            return;
     1326        } else {
     1327            try {
     1328                this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
     1329            } catch( e ) {
     1330                QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
     1331            }
     1332        }
     1333        checkPollution();
     1334    },
     1335    finish: function() {
     1336        config.current = this;
     1337        if ( config.requireExpects && this.expected === null ) {
     1338            QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
     1339        } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
     1340            QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
     1341        } else if ( this.expected === null && !this.assertions.length ) {
     1342            QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
     1343        }
     1344
     1345        var i, assertion, a, b, time, li, ol,
     1346            test = this,
     1347            good = 0,
     1348            bad = 0,
     1349            tests = id( "qunit-tests" );
     1350
     1351        this.runtime = +new Date() - this.started;
     1352        config.stats.all += this.assertions.length;
     1353        config.moduleStats.all += this.assertions.length;
     1354
     1355        if ( tests ) {
     1356            ol = document.createElement( "ol" );
     1357            ol.className = "qunit-assert-list";
     1358
     1359            for ( i = 0; i < this.assertions.length; i++ ) {
     1360                assertion = this.assertions[i];
     1361
     1362                li = document.createElement( "li" );
     1363                li.className = assertion.result ? "pass" : "fail";
     1364                li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
     1365                ol.appendChild( li );
     1366
     1367                if ( assertion.result ) {
     1368                    good++;
     1369                } else {
     1370                    bad++;
     1371                    config.stats.bad++;
     1372                    config.moduleStats.bad++;
     1373                }
     1374            }
     1375
     1376            // store result when possible
     1377            if ( QUnit.config.reorder && defined.sessionStorage ) {
     1378                if ( bad ) {
     1379                    sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
     1380                } else {
     1381                    sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
     1382                }
     1383            }
     1384
     1385            if ( bad === 0 ) {
     1386                addClass( ol, "qunit-collapsed" );
     1387            }
     1388
     1389            // `b` initialized at top of scope
     1390            b = document.createElement( "strong" );
     1391            b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
     1392
     1393            addEvent(b, "click", function() {
     1394                var next = b.parentNode.lastChild,
     1395                    collapsed = hasClass( next, "qunit-collapsed" );
     1396                ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
     1397            });
     1398
     1399            addEvent(b, "dblclick", function( e ) {
     1400                var target = e && e.target ? e.target : window.event.srcElement;
     1401                if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
     1402                    target = target.parentNode;
     1403                }
     1404                if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
     1405                    window.location = QUnit.url({ testNumber: test.testNumber });
     1406                }
     1407            });
     1408
     1409            // `time` initialized at top of scope
     1410            time = document.createElement( "span" );
     1411            time.className = "runtime";
     1412            time.innerHTML = this.runtime + " ms";
     1413
     1414            // `li` initialized at top of scope
     1415            li = id( this.id );
     1416            li.className = bad ? "fail" : "pass";
     1417            li.removeChild( li.firstChild );
     1418            a = li.firstChild;
     1419            li.appendChild( b );
     1420            li.appendChild( a );
     1421            li.appendChild( time );
     1422            li.appendChild( ol );
     1423
     1424        } else {
     1425            for ( i = 0; i < this.assertions.length; i++ ) {
     1426                if ( !this.assertions[i].result ) {
     1427                    bad++;
     1428                    config.stats.bad++;
     1429                    config.moduleStats.bad++;
     1430                }
     1431            }
     1432        }
     1433
     1434        runLoggingCallbacks( "testDone", QUnit, {
     1435            name: this.testName,
     1436            module: this.module,
     1437            failed: bad,
     1438            passed: this.assertions.length - bad,
     1439            total: this.assertions.length,
     1440            runtime: this.runtime,
     1441            // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
     1442            duration: this.runtime
     1443        });
     1444
     1445        QUnit.reset();
     1446
     1447        config.current = undefined;
     1448    },
     1449
     1450    queue: function() {
     1451        var bad,
     1452            test = this;
     1453
     1454        synchronize(function() {
     1455            test.init();
     1456        });
     1457        function run() {
     1458            // each of these can by async
     1459            synchronize(function() {
     1460                test.setup();
     1461            });
     1462            synchronize(function() {
     1463                test.run();
     1464            });
     1465            synchronize(function() {
     1466                test.teardown();
     1467            });
     1468            synchronize(function() {
     1469                test.finish();
     1470            });
     1471        }
     1472
     1473        // `bad` initialized at top of scope
     1474        // defer when previous test run passed, if storage is available
     1475        bad = QUnit.config.reorder && defined.sessionStorage &&
     1476                        +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
     1477
     1478        if ( bad ) {
     1479            run();
     1480        } else {
     1481            synchronize( run, true );
     1482        }
     1483    }
     1484};
     1485
     1486// `assert` initialized at top of scope
     1487// Assert helpers
     1488// All of these must either call QUnit.push() or manually do:
     1489// - runLoggingCallbacks( "log", .. );
     1490// - config.current.assertions.push({ .. });
     1491assert = QUnit.assert = {
     1492    /**
     1493     * Asserts rough true-ish result.
     1494     * @name ok
     1495     * @function
     1496     * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
     1497     */
     1498    ok: function( result, msg ) {
     1499        if ( !config.current ) {
     1500            throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
     1501        }
     1502        result = !!result;
     1503        msg = msg || ( result ? "okay" : "failed" );
     1504
     1505        var source,
     1506            details = {
     1507                module: config.current.module,
     1508                name: config.current.testName,
     1509                result: result,
     1510                message: msg
     1511            };
     1512
     1513        msg = "<span class='test-message'>" + escapeText( msg ) + "</span>";
     1514
     1515        if ( !result ) {
     1516            source = sourceFromStacktrace( 2 );
     1517            if ( source ) {
     1518                details.source = source;
     1519                msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" +
     1520                    escapeText( source ) +
     1521                    "</pre></td></tr></table>";
     1522            }
     1523        }
     1524        runLoggingCallbacks( "log", QUnit, details );
     1525        config.current.assertions.push({
     1526            result: result,
     1527            message: msg
     1528        });
     1529    },
     1530
     1531    /**
     1532     * Assert that the first two arguments are equal, with an optional message.
     1533     * Prints out both actual and expected values.
     1534     * @name equal
     1535     * @function
     1536     * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
     1537     */
     1538    equal: function( actual, expected, message ) {
     1539        /*jshint eqeqeq:false */
     1540        QUnit.push( expected == actual, actual, expected, message );
     1541    },
     1542
     1543    /**
     1544     * @name notEqual
     1545     * @function
     1546     */
     1547    notEqual: function( actual, expected, message ) {
     1548        /*jshint eqeqeq:false */
     1549        QUnit.push( expected != actual, actual, expected, message );
     1550    },
     1551
     1552    /**
     1553     * @name propEqual
     1554     * @function
     1555     */
     1556    propEqual: function( actual, expected, message ) {
     1557        actual = objectValues(actual);
     1558        expected = objectValues(expected);
     1559        QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
     1560    },
     1561
     1562    /**
     1563     * @name notPropEqual
     1564     * @function
     1565     */
     1566    notPropEqual: function( actual, expected, message ) {
     1567        actual = objectValues(actual);
     1568        expected = objectValues(expected);
     1569        QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
     1570    },
     1571
     1572    /**
     1573     * @name deepEqual
     1574     * @function
     1575     */
     1576    deepEqual: function( actual, expected, message ) {
     1577        QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
     1578    },
     1579
     1580    /**
     1581     * @name notDeepEqual
     1582     * @function
     1583     */
     1584    notDeepEqual: function( actual, expected, message ) {
     1585        QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
     1586    },
     1587
     1588    /**
     1589     * @name strictEqual
     1590     * @function
     1591     */
     1592    strictEqual: function( actual, expected, message ) {
     1593        QUnit.push( expected === actual, actual, expected, message );
     1594    },
     1595
     1596    /**
     1597     * @name notStrictEqual
     1598     * @function
     1599     */
     1600    notStrictEqual: function( actual, expected, message ) {
     1601        QUnit.push( expected !== actual, actual, expected, message );
     1602    },
     1603
     1604    "throws": function( block, expected, message ) {
     1605        var actual,
     1606            expectedOutput = expected,
     1607            ok = false;
     1608
     1609        // 'expected' is optional
     1610        if ( !message && typeof expected === "string" ) {
     1611            message = expected;
     1612            expected = null;
     1613        }
     1614
     1615        config.current.ignoreGlobalErrors = true;
     1616        try {
     1617            block.call( config.current.testEnvironment );
     1618        } catch (e) {
     1619            actual = e;
     1620        }
     1621        config.current.ignoreGlobalErrors = false;
     1622
     1623        if ( actual ) {
     1624
     1625            // we don't want to validate thrown error
     1626            if ( !expected ) {
     1627                ok = true;
     1628                expectedOutput = null;
     1629
     1630            // expected is an Error object
     1631            } else if ( expected instanceof Error ) {
     1632                ok = actual instanceof Error &&
     1633                     actual.name === expected.name &&
     1634                     actual.message === expected.message;
     1635
     1636            // expected is a regexp
     1637            } else if ( QUnit.objectType( expected ) === "regexp" ) {
     1638                ok = expected.test( errorString( actual ) );
     1639
     1640            // expected is a string
     1641            } else if ( QUnit.objectType( expected ) === "string" ) {
     1642                ok = expected === errorString( actual );
     1643
     1644            // expected is a constructor
     1645            } else if ( actual instanceof expected ) {
     1646                ok = true;
     1647
     1648            // expected is a validation function which returns true is validation passed
     1649            } else if ( expected.call( {}, actual ) === true ) {
     1650                expectedOutput = null;
     1651                ok = true;
     1652            }
     1653
     1654            QUnit.push( ok, actual, expectedOutput, message );
     1655        } else {
     1656            QUnit.pushFailure( message, null, "No exception was thrown." );
     1657        }
     1658    }
     1659};
     1660
     1661/**
     1662 * @deprecated since 1.8.0
     1663 * Kept assertion helpers in root for backwards compatibility.
     1664 */
     1665extend( QUnit.constructor.prototype, assert );
     1666
     1667/**
     1668 * @deprecated since 1.9.0
     1669 * Kept to avoid TypeErrors for undefined methods.
     1670 */
     1671QUnit.constructor.prototype.raises = function() {
     1672    QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" );
     1673};
     1674
     1675/**
     1676 * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
     1677 * Kept to avoid TypeErrors for undefined methods.
     1678 */
     1679QUnit.constructor.prototype.equals = function() {
     1680    QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
     1681};
     1682QUnit.constructor.prototype.same = function() {
     1683    QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
     1684};
    7701685
    7711686// Test for equality any JavaScript type.
    772 // Discussions and reference: http://philrathe.com/articles/equiv
    773 // Test suites: http://philrathe.com/tests/equiv
    7741687// Author: Philippe Rathé <prathe@gmail.com>
    775 QUnit.equiv = function () {
    776 
    777     var innerEquiv; // the real equiv function
    778     var callers = []; // stack to decide between skip/abort functions
    779     var parents = []; // stack to avoiding loops from circular referencing
    780 
    781     // Call the o related callback with the given arguments.
    782     function bindCallbacks(o, callbacks, args) {
    783         var prop = QUnit.objectType(o);
    784         if (prop) {
    785             if (QUnit.objectType(callbacks[prop]) === "function") {
    786                 return callbacks[prop].apply(callbacks, args);
    787             } else {
    788                 return callbacks[prop]; // or undefined
    789             }
    790         }
    791     }
    792    
    793     var callbacks = function () {
    794 
    795         // for string, boolean, number and null
    796         function useStrictEquality(b, a) {
    797             if (b instanceof a.constructor || a instanceof b.constructor) {
    798                 // to catch short annotaion VS 'new' annotation of a declaration
    799                 // e.g. var i = 1;
    800                 //      var j = new Number(1);
    801                 return a == b;
    802             } else {
    803                 return a === b;
    804             }
    805         }
    806 
    807         return {
    808             "string": useStrictEquality,
    809             "boolean": useStrictEquality,
    810             "number": useStrictEquality,
    811             "null": useStrictEquality,
    812             "undefined": useStrictEquality,
    813 
    814             "nan": function (b) {
    815                 return isNaN(b);
    816             },
    817 
    818             "date": function (b, a) {
    819                 return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf();
    820             },
    821 
    822             "regexp": function (b, a) {
    823                 return QUnit.objectType(b) === "regexp" &&
    824                     a.source === b.source && // the regex itself
    825                     a.global === b.global && // and its modifers (gmi) ...
    826                     a.ignoreCase === b.ignoreCase &&
    827                     a.multiline === b.multiline;
    828             },
    829 
    830             // - skip when the property is a method of an instance (OOP)
    831             // - abort otherwise,
    832             //   initial === would have catch identical references anyway
    833             "function": function () {
    834                 var caller = callers[callers.length - 1];
    835                 return caller !== Object &&
    836                         typeof caller !== "undefined";
    837             },
    838 
    839             "array": function (b, a) {
    840                 var i, j, loop;
    841                 var len;
    842 
    843                 // b could be an object literal here
    844                 if ( ! (QUnit.objectType(b) === "array")) {
    845                     return false;
    846                 }   
    847                
    848                 len = a.length;
    849                 if (len !== b.length) { // safe and faster
    850                     return false;
    851                 }
    852                
    853                 //track reference to avoid circular references
    854                 parents.push(a);
    855                 for (i = 0; i < len; i++) {
    856                     loop = false;
    857                     for(j=0;j<parents.length;j++){
    858                         if(parents[j] === a[i]){
    859                             loop = true;//dont rewalk array
    860                         }
    861                     }
    862                     if (!loop && ! innerEquiv(a[i], b[i])) {
    863                         parents.pop();
    864                         return false;
    865                     }
    866                 }
    867                 parents.pop();
    868                 return true;
    869             },
    870 
    871             "object": function (b, a) {
    872                 var i, j, loop;
    873                 var eq = true; // unless we can proove it
    874                 var aProperties = [], bProperties = []; // collection of strings
    875 
    876                 // comparing constructors is more strict than using instanceof
    877                 if ( a.constructor !== b.constructor) {
    878                     return false;
    879                 }
    880 
    881                 // stack constructor before traversing properties
    882                 callers.push(a.constructor);
    883                 //track reference to avoid circular references
    884                 parents.push(a);
    885                
    886                 for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
    887                     loop = false;
    888                     for(j=0;j<parents.length;j++){
    889                         if(parents[j] === a[i])
    890                             loop = true; //don't go down the same path twice
    891                     }
    892                     aProperties.push(i); // collect a's properties
    893 
    894                     if (!loop && ! innerEquiv(a[i], b[i])) {
    895                         eq = false;
    896                         break;
    897                     }
    898                 }
    899 
    900                 callers.pop(); // unstack, we are done
    901                 parents.pop();
    902 
    903                 for (i in b) {
    904                     bProperties.push(i); // collect b's properties
    905                 }
    906 
    907                 // Ensures identical properties name
    908                 return eq && innerEquiv(aProperties.sort(), bProperties.sort());
    909             }
    910         };
    911     }();
    912 
    913     innerEquiv = function () { // can take multiple arguments
    914         var args = Array.prototype.slice.apply(arguments);
    915         if (args.length < 2) {
    916             return true; // end transition
    917         }
    918 
    919         return (function (a, b) {
    920             if (a === b) {
    921                 return true; // catch the most you can
    922             } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType(a) !== QUnit.objectType(b)) {
    923                 return false; // don't lose time with error prone cases
    924             } else {
    925                 return bindCallbacks(a, callbacks, [b, a]);
    926             }
    927 
    928         // apply transition with (1..n) arguments
    929         })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
    930     };
    931 
    932     return innerEquiv;
    933 
    934 }();
     1688QUnit.equiv = (function() {
     1689
     1690    // Call the o related callback with the given arguments.
     1691    function bindCallbacks( o, callbacks, args ) {
     1692        var prop = QUnit.objectType( o );
     1693        if ( prop ) {
     1694            if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
     1695                return callbacks[ prop ].apply( callbacks, args );
     1696            } else {
     1697                return callbacks[ prop ]; // or undefined
     1698            }
     1699        }
     1700    }
     1701
     1702    // the real equiv function
     1703    var innerEquiv,
     1704        // stack to decide between skip/abort functions
     1705        callers = [],
     1706        // stack to avoiding loops from circular referencing
     1707        parents = [],
     1708        parentsB = [],
     1709
     1710        getProto = Object.getPrototypeOf || function ( obj ) {
     1711            /*jshint camelcase:false */
     1712            return obj.__proto__;
     1713        },
     1714        callbacks = (function () {
     1715
     1716            // for string, boolean, number and null
     1717            function useStrictEquality( b, a ) {
     1718                /*jshint eqeqeq:false */
     1719                if ( b instanceof a.constructor || a instanceof b.constructor ) {
     1720                    // to catch short annotation VS 'new' annotation of a
     1721                    // declaration
     1722                    // e.g. var i = 1;
     1723                    // var j = new Number(1);
     1724                    return a == b;
     1725                } else {
     1726                    return a === b;
     1727                }
     1728            }
     1729
     1730            return {
     1731                "string": useStrictEquality,
     1732                "boolean": useStrictEquality,
     1733                "number": useStrictEquality,
     1734                "null": useStrictEquality,
     1735                "undefined": useStrictEquality,
     1736
     1737                "nan": function( b ) {
     1738                    return isNaN( b );
     1739                },
     1740
     1741                "date": function( b, a ) {
     1742                    return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
     1743                },
     1744
     1745                "regexp": function( b, a ) {
     1746                    return QUnit.objectType( b ) === "regexp" &&
     1747                        // the regex itself
     1748                        a.source === b.source &&
     1749                        // and its modifiers
     1750                        a.global === b.global &&
     1751                        // (gmi) ...
     1752                        a.ignoreCase === b.ignoreCase &&
     1753                        a.multiline === b.multiline &&
     1754                        a.sticky === b.sticky;
     1755                },
     1756
     1757                // - skip when the property is a method of an instance (OOP)
     1758                // - abort otherwise,
     1759                // initial === would have catch identical references anyway
     1760                "function": function() {
     1761                    var caller = callers[callers.length - 1];
     1762                    return caller !== Object && typeof caller !== "undefined";
     1763                },
     1764
     1765                "array": function( b, a ) {
     1766                    var i, j, len, loop, aCircular, bCircular;
     1767
     1768                    // b could be an object literal here
     1769                    if ( QUnit.objectType( b ) !== "array" ) {
     1770                        return false;
     1771                    }
     1772
     1773                    len = a.length;
     1774                    if ( len !== b.length ) {
     1775                        // safe and faster
     1776                        return false;
     1777                    }
     1778
     1779                    // track reference to avoid circular references
     1780                    parents.push( a );
     1781                    parentsB.push( b );
     1782                    for ( i = 0; i < len; i++ ) {
     1783                        loop = false;
     1784                        for ( j = 0; j < parents.length; j++ ) {
     1785                            aCircular = parents[j] === a[i];
     1786                            bCircular = parentsB[j] === b[i];
     1787                            if ( aCircular || bCircular ) {
     1788                                if ( a[i] === b[i] || aCircular && bCircular ) {
     1789                                    loop = true;
     1790                                } else {
     1791                                    parents.pop();
     1792                                    parentsB.pop();
     1793                                    return false;
     1794                                }
     1795                            }
     1796                        }
     1797                        if ( !loop && !innerEquiv(a[i], b[i]) ) {
     1798                            parents.pop();
     1799                            parentsB.pop();
     1800                            return false;
     1801                        }
     1802                    }
     1803                    parents.pop();
     1804                    parentsB.pop();
     1805                    return true;
     1806                },
     1807
     1808                "object": function( b, a ) {
     1809                    /*jshint forin:false */
     1810                    var i, j, loop, aCircular, bCircular,
     1811                        // Default to true
     1812                        eq = true,
     1813                        aProperties = [],
     1814                        bProperties = [];
     1815
     1816                    // comparing constructors is more strict than using
     1817                    // instanceof
     1818                    if ( a.constructor !== b.constructor ) {
     1819                        // Allow objects with no prototype to be equivalent to
     1820                        // objects with Object as their constructor.
     1821                        if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
     1822                            ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
     1823                                return false;
     1824                        }
     1825                    }
     1826
     1827                    // stack constructor before traversing properties
     1828                    callers.push( a.constructor );
     1829
     1830                    // track reference to avoid circular references
     1831                    parents.push( a );
     1832                    parentsB.push( b );
     1833
     1834                    // be strict: don't ensure hasOwnProperty and go deep
     1835                    for ( i in a ) {
     1836                        loop = false;
     1837                        for ( j = 0; j < parents.length; j++ ) {
     1838                            aCircular = parents[j] === a[i];
     1839                            bCircular = parentsB[j] === b[i];
     1840                            if ( aCircular || bCircular ) {
     1841                                if ( a[i] === b[i] || aCircular && bCircular ) {
     1842                                    loop = true;
     1843                                } else {
     1844                                    eq = false;
     1845                                    break;
     1846                                }
     1847                            }
     1848                        }
     1849                        aProperties.push(i);
     1850                        if ( !loop && !innerEquiv(a[i], b[i]) ) {
     1851                            eq = false;
     1852                            break;
     1853                        }
     1854                    }
     1855
     1856                    parents.pop();
     1857                    parentsB.pop();
     1858                    callers.pop(); // unstack, we are done
     1859
     1860                    for ( i in b ) {
     1861                        bProperties.push( i ); // collect b's properties
     1862                    }
     1863
     1864                    // Ensures identical properties name
     1865                    return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
     1866                }
     1867            };
     1868        }());
     1869
     1870    innerEquiv = function() { // can take multiple arguments
     1871        var args = [].slice.apply( arguments );
     1872        if ( args.length < 2 ) {
     1873            return true; // end transition
     1874        }
     1875
     1876        return (function( a, b ) {
     1877            if ( a === b ) {
     1878                return true; // catch the most you can
     1879            } else if ( a === null || b === null || typeof a === "undefined" ||
     1880                    typeof b === "undefined" ||
     1881                    QUnit.objectType(a) !== QUnit.objectType(b) ) {
     1882                return false; // don't lose time with error prone cases
     1883            } else {
     1884                return bindCallbacks(a, callbacks, [ b, a ]);
     1885            }
     1886
     1887            // apply transition with (1..n) arguments
     1888        }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) );
     1889    };
     1890
     1891    return innerEquiv;
     1892}());
    9351893
    9361894/**
    937  * jsDump
    938  * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
    939  * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
    940  * Date: 5/15/2008
     1895 * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
     1896 * http://flesler.blogspot.com Licensed under BSD
     1897 * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
     1898 *
    9411899 * @projectDescription Advanced and extensible data dumping for Javascript.
    9421900 * @version 1.0.0
     
    9461904QUnit.jsDump = (function() {
    9471905    function quote( str ) {
    948         return '"' + str.toString().replace(/"/g, '\\"') + '"';
    949     };
     1906        return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
     1907    }
    9501908    function literal( o ) {
    951         return o + ''; 
    952     };
     1909        return o + "";
     1910    }
    9531911    function join( pre, arr, post ) {
    9541912        var s = jsDump.separator(),
    9551913            base = jsDump.indent(),
    9561914            inner = jsDump.indent(1);
    957         if ( arr.join )
    958             arr = arr.join( ',' + s + inner );
    959         if ( !arr )
     1915        if ( arr.join ) {
     1916            arr = arr.join( "," + s + inner );
     1917        }
     1918        if ( !arr ) {
    9601919            return pre + post;
     1920        }
    9611921        return [ pre, inner + arr, base + post ].join(s);
    962     };
    963     function array( arr ) {
    964         var i = arr.length, ret = Array(i);                 
     1922    }
     1923    function array( arr, stack ) {
     1924        var i = arr.length, ret = new Array(i);
    9651925        this.up();
    966         while ( i-- )
    967             ret[i] = this.parse( arr[i] );             
     1926        while ( i-- ) {
     1927            ret[i] = this.parse( arr[i] , undefined , stack);
     1928        }
    9681929        this.down();
    969         return join( '[', ret, ']' );
    970     };
    971    
    972     var reName = /^function (\w+)/;
    973    
    974     var jsDump = {
    975         parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
    976             var parser = this.parsers[ type || this.typeOf(obj) ];
    977             type = typeof parser;           
    978            
    979             return type == 'function' ? parser.call( this, obj ) :
    980                    type == 'string' ? parser :
    981                    this.parsers.error;
    982         },
    983         typeOf:function( obj ) {
    984             var type;
    985             if ( obj === null ) {
    986                 type = "null";
    987             } else if (typeof obj === "undefined") {
    988                 type = "undefined";
    989             } else if (QUnit.is("RegExp", obj)) {
    990                 type = "regexp";
    991             } else if (QUnit.is("Date", obj)) {
    992                 type = "date";
    993             } else if (QUnit.is("Function", obj)) {
    994                 type = "function";
    995             } else if (obj.setInterval && obj.document && !obj.nodeType) {
    996                 type = "window";
    997             } else if (obj.nodeType === 9) {
    998                 type = "document";
    999             } else if (obj.nodeType) {
    1000                 type = "node";
    1001             } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
    1002                 type = "array";
    1003             } else {
    1004                 type = typeof obj;
    1005             }
    1006             return type;
    1007         },
    1008         separator:function() {
    1009             return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
    1010         },
    1011         indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
    1012             if ( !this.multiline )
    1013                 return '';
    1014             var chr = this.indentChar;
    1015             if ( this.HTML )
    1016                 chr = chr.replace(/\t/g,'   ').replace(/ /g,'&nbsp;');
    1017             return Array( this._depth_ + (extra||0) ).join(chr);
    1018         },
    1019         up:function( a ) {
    1020             this._depth_ += a || 1;
    1021         },
    1022         down:function( a ) {
    1023             this._depth_ -= a || 1;
    1024         },
    1025         setParser:function( name, parser ) {
    1026             this.parsers[name] = parser;
    1027         },
    1028         // The next 3 are exposed so you can use them
    1029         quote:quote,
    1030         literal:literal,
    1031         join:join,
    1032         //
    1033         _depth_: 1,
    1034         // This is the list of parsers, to modify them, use jsDump.setParser
    1035         parsers:{
    1036             window: '[Window]',
    1037             document: '[Document]',
    1038             error:'[ERROR]', //when no parser is found, shouldn't happen
    1039             unknown: '[Unknown]',
    1040             'null':'null',
    1041             undefined:'undefined',
    1042             'function':function( fn ) {
    1043                 var ret = 'function',
    1044                     name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
    1045                 if ( name )
    1046                     ret += ' ' + name;
    1047                 ret += '(';
    1048                
    1049                 ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
    1050                 return join( ret, this.parse(fn,'functionCode'), '}' );
     1930        return join( "[", ret, "]" );
     1931    }
     1932
     1933    var reName = /^function (\w+)/,
     1934        jsDump = {
     1935            // type is used mostly internally, you can fix a (custom)type in advance
     1936            parse: function( obj, type, stack ) {
     1937                stack = stack || [ ];
     1938                var inStack, res,
     1939                    parser = this.parsers[ type || this.typeOf(obj) ];
     1940
     1941                type = typeof parser;
     1942                inStack = inArray( obj, stack );
     1943
     1944                if ( inStack !== -1 ) {
     1945                    return "recursion(" + (inStack - stack.length) + ")";
     1946                }
     1947                if ( type === "function" )  {
     1948                    stack.push( obj );
     1949                    res = parser.call( this, obj, stack );
     1950                    stack.pop();
     1951                    return res;
     1952                }
     1953                return ( type === "string" ) ? parser : this.parsers.error;
    10511954            },
    1052             array: array,
    1053             nodelist: array,
    1054             arguments: array,
    1055             object:function( map ) {
    1056                 var ret = [ ];
    1057                 this.up();
    1058                 for ( var key in map )
    1059                     ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
    1060                 this.down();
    1061                 return join( '{', ret, '}' );
     1955            typeOf: function( obj ) {
     1956                var type;
     1957                if ( obj === null ) {
     1958                    type = "null";
     1959                } else if ( typeof obj === "undefined" ) {
     1960                    type = "undefined";
     1961                } else if ( QUnit.is( "regexp", obj) ) {
     1962                    type = "regexp";
     1963                } else if ( QUnit.is( "date", obj) ) {
     1964                    type = "date";
     1965                } else if ( QUnit.is( "function", obj) ) {
     1966                    type = "function";
     1967                } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
     1968                    type = "window";
     1969                } else if ( obj.nodeType === 9 ) {
     1970                    type = "document";
     1971                } else if ( obj.nodeType ) {
     1972                    type = "node";
     1973                } else if (
     1974                    // native arrays
     1975                    toString.call( obj ) === "[object Array]" ||
     1976                    // NodeList objects
     1977                    ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
     1978                ) {
     1979                    type = "array";
     1980                } else if ( obj.constructor === Error.prototype.constructor ) {
     1981                    type = "error";
     1982                } else {
     1983                    type = typeof obj;
     1984                }
     1985                return type;
    10621986            },
    1063             node:function( node ) {
    1064                 var open = this.HTML ? '&lt;' : '<',
    1065                     close = this.HTML ? '&gt;' : '>';
    1066                    
    1067                 var tag = node.nodeName.toLowerCase(),
    1068                     ret = open + tag;
    1069                    
    1070                 for ( var a in this.DOMAttrs ) {
    1071                     var val = node[this.DOMAttrs[a]];
    1072                     if ( val )
    1073                         ret += ' ' + a + '=' + this.parse( val, 'attribute' );
    1074                 }
    1075                 return ret + close + open + '/' + tag + close;
     1987            separator: function() {
     1988                return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? "&nbsp;" : " ";
    10761989            },
    1077             functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
    1078                 var l = fn.length;
    1079                 if ( !l ) return '';               
    1080                
    1081                 var args = Array(l);
    1082                 while ( l-- )
    1083                     args[l] = String.fromCharCode(97+l);//97 is 'a'
    1084                 return ' ' + args.join(', ') + ' ';
     1990            // extra can be a number, shortcut for increasing-calling-decreasing
     1991            indent: function( extra ) {
     1992                if ( !this.multiline ) {
     1993                    return "";
     1994                }
     1995                var chr = this.indentChar;
     1996                if ( this.HTML ) {
     1997                    chr = chr.replace( /\t/g, "   " ).replace( / /g, "&nbsp;" );
     1998                }
     1999                return new Array( this.depth + ( extra || 0 ) ).join(chr);
    10852000            },
    1086             key:quote, //object calls it internally, the key part of an item in a map
    1087             functionCode:'[code]', //function calls it internally, it's the content of the function
    1088             attribute:quote, //node calls it internally, it's an html attribute value
    1089             string:quote,
    1090             date:quote,
    1091             regexp:literal, //regex
    1092             number:literal,
    1093             'boolean':literal
    1094         },
    1095         DOMAttrs:{//attributes to dump from nodes, name=>realName
    1096             id:'id',
    1097             name:'name',
    1098             'class':'className'
    1099         },
    1100         HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
    1101         indentChar:'   ',//indentation unit
    1102         multiline:false //if true, items in a collection, are separated by a \n, else just a space.
    1103     };
     2001            up: function( a ) {
     2002                this.depth += a || 1;
     2003            },
     2004            down: function( a ) {
     2005                this.depth -= a || 1;
     2006            },
     2007            setParser: function( name, parser ) {
     2008                this.parsers[name] = parser;
     2009            },
     2010            // The next 3 are exposed so you can use them
     2011            quote: quote,
     2012            literal: literal,
     2013            join: join,
     2014            //
     2015            depth: 1,
     2016            // This is the list of parsers, to modify them, use jsDump.setParser
     2017            parsers: {
     2018                window: "[Window]",
     2019                document: "[Document]",
     2020                error: function(error) {
     2021                    return "Error(\"" + error.message + "\")";
     2022                },
     2023                unknown: "[Unknown]",
     2024                "null": "null",
     2025                "undefined": "undefined",
     2026                "function": function( fn ) {
     2027                    var ret = "function",
     2028                        // functions never have name in IE
     2029                        name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
     2030
     2031                    if ( name ) {
     2032                        ret += " " + name;
     2033                    }
     2034                    ret += "( ";
     2035
     2036                    ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
     2037                    return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
     2038                },
     2039                array: array,
     2040                nodelist: array,
     2041                "arguments": array,
     2042                object: function( map, stack ) {
     2043                    /*jshint forin:false */
     2044                    var ret = [ ], keys, key, val, i;
     2045                    QUnit.jsDump.up();
     2046                    keys = [];
     2047                    for ( key in map ) {
     2048                        keys.push( key );
     2049                    }
     2050                    keys.sort();
     2051                    for ( i = 0; i < keys.length; i++ ) {
     2052                        key = keys[ i ];
     2053                        val = map[ key ];
     2054                        ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
     2055                    }
     2056                    QUnit.jsDump.down();
     2057                    return join( "{", ret, "}" );
     2058                },
     2059                node: function( node ) {
     2060                    var len, i, val,
     2061                        open = QUnit.jsDump.HTML ? "&lt;" : "<",
     2062                        close = QUnit.jsDump.HTML ? "&gt;" : ">",
     2063                        tag = node.nodeName.toLowerCase(),
     2064                        ret = open + tag,
     2065                        attrs = node.attributes;
     2066
     2067                    if ( attrs ) {
     2068                        for ( i = 0, len = attrs.length; i < len; i++ ) {
     2069                            val = attrs[i].nodeValue;
     2070                            // IE6 includes all attributes in .attributes, even ones not explicitly set.
     2071                            // Those have values like undefined, null, 0, false, "" or "inherit".
     2072                            if ( val && val !== "inherit" ) {
     2073                                ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
     2074                            }
     2075                        }
     2076                    }
     2077                    ret += close;
     2078
     2079                    // Show content of TextNode or CDATASection
     2080                    if ( node.nodeType === 3 || node.nodeType === 4 ) {
     2081                        ret += node.nodeValue;
     2082                    }
     2083
     2084                    return ret + open + "/" + tag + close;
     2085                },
     2086                // function calls it internally, it's the arguments part of the function
     2087                functionArgs: function( fn ) {
     2088                    var args,
     2089                        l = fn.length;
     2090
     2091                    if ( !l ) {
     2092                        return "";
     2093                    }
     2094
     2095                    args = new Array(l);
     2096                    while ( l-- ) {
     2097                        // 97 is 'a'
     2098                        args[l] = String.fromCharCode(97+l);
     2099                    }
     2100                    return " " + args.join( ", " ) + " ";
     2101                },
     2102                // object calls it internally, the key part of an item in a map
     2103                key: quote,
     2104                // function calls it internally, it's the content of the function
     2105                functionCode: "[code]",
     2106                // node calls it internally, it's an html attribute value
     2107                attribute: quote,
     2108                string: quote,
     2109                date: quote,
     2110                regexp: literal,
     2111                number: literal,
     2112                "boolean": literal
     2113            },
     2114            // if true, entities are escaped ( <, >, \t, space and \n )
     2115            HTML: false,
     2116            // indentation unit
     2117            indentChar: "  ",
     2118            // if true, items in a collection, are separated by a \n, else just a space.
     2119            multiline: true
     2120        };
    11042121
    11052122    return jsDump;
    1106 })();
    1107 
    1108 // from Sizzle.js
    1109 function getText( elems ) {
    1110     var ret = "", elem;
    1111 
    1112     for ( var i = 0; elems[i]; i++ ) {
    1113         elem = elems[i];
    1114 
    1115         // Get the text from text nodes and CDATA nodes
    1116         if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
    1117             ret += elem.nodeValue;
    1118 
    1119         // Traverse everything else, except comment nodes
    1120         } else if ( elem.nodeType !== 8 ) {
    1121             ret += getText( elem.childNodes );
    1122         }
    1123     }
    1124 
    1125     return ret;
    1126 };
     2123}());
    11272124
    11282125/*
     
    11352132 * More Info:
    11362133 *  http://ejohn.org/projects/javascript-diff-algorithm/
    1137  * 
     2134 *
    11382135 * Usage: QUnit.diff(expected, actual)
    1139  * 
    1140  * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the  quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
     2136 *
     2137 * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the  quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
    11412138 */
    11422139QUnit.diff = (function() {
    1143     function diff(o, n){
    1144         var ns = new Object();
    1145         var os = new Object();
    1146        
    1147         for (var i = 0; i < n.length; i++) {
    1148             if (ns[n[i]] == null)
    1149                 ns[n[i]] = {
    1150                     rows: new Array(),
     2140    /*jshint eqeqeq:false, eqnull:true */
     2141    function diff( o, n ) {
     2142        var i,
     2143            ns = {},
     2144            os = {};
     2145
     2146        for ( i = 0; i < n.length; i++ ) {
     2147            if ( !hasOwn.call( ns, n[i] ) ) {
     2148                ns[ n[i] ] = {
     2149                    rows: [],
    11512150                    o: null
    11522151                };
    1153             ns[n[i]].rows.push(i);
    1154         }
    1155        
    1156         for (var i = 0; i < o.length; i++) {
    1157             if (os[o[i]] == null)
    1158                 os[o[i]] = {
    1159                     rows: new Array(),
     2152            }
     2153            ns[ n[i] ].rows.push( i );
     2154        }
     2155
     2156        for ( i = 0; i < o.length; i++ ) {
     2157            if ( !hasOwn.call( os, o[i] ) ) {
     2158                os[ o[i] ] = {
     2159                    rows: [],
    11602160                    n: null
    11612161                };
    1162             os[o[i]].rows.push(i);
    1163         }
    1164        
    1165         for (var i in ns) {
    1166             if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
    1167                 n[ns[i].rows[0]] = {
    1168                     text: n[ns[i].rows[0]],
    1169                     row: os[i].rows[0]
    1170                 };
    1171                 o[os[i].rows[0]] = {
    1172                     text: o[os[i].rows[0]],
    1173                     row: ns[i].rows[0]
    1174                 };
    1175             }
    1176         }
    1177        
    1178         for (var i = 0; i < n.length - 1; i++) {
    1179             if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
    1180             n[i + 1] == o[n[i].row + 1]) {
    1181                 n[i + 1] = {
    1182                     text: n[i + 1],
     2162            }
     2163            os[ o[i] ].rows.push( i );
     2164        }
     2165
     2166        for ( i in ns ) {
     2167            if ( hasOwn.call( ns, i ) ) {
     2168                if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
     2169                    n[ ns[i].rows[0] ] = {
     2170                        text: n[ ns[i].rows[0] ],
     2171                        row: os[i].rows[0]
     2172                    };
     2173                    o[ os[i].rows[0] ] = {
     2174                        text: o[ os[i].rows[0] ],
     2175                        row: ns[i].rows[0]
     2176                    };
     2177                }
     2178            }
     2179        }
     2180
     2181        for ( i = 0; i < n.length - 1; i++ ) {
     2182            if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
     2183                        n[ i + 1 ] == o[ n[i].row + 1 ] ) {
     2184
     2185                n[ i + 1 ] = {
     2186                    text: n[ i + 1 ],
    11832187                    row: n[i].row + 1
    11842188                };
    1185                 o[n[i].row + 1] = {
    1186                     text: o[n[i].row + 1],
     2189                o[ n[i].row + 1 ] = {
     2190                    text: o[ n[i].row + 1 ],
    11872191                    row: i + 1
    11882192                };
    11892193            }
    11902194        }
    1191        
    1192         for (var i = n.length - 1; i > 0; i--) {
    1193             if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
    1194             n[i - 1] == o[n[i].row - 1]) {
    1195                 n[i - 1] = {
    1196                     text: n[i - 1],
     2195
     2196        for ( i = n.length - 1; i > 0; i-- ) {
     2197            if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
     2198                        n[ i - 1 ] == o[ n[i].row - 1 ]) {
     2199
     2200                n[ i - 1 ] = {
     2201                    text: n[ i - 1 ],
    11972202                    row: n[i].row - 1
    11982203                };
    1199                 o[n[i].row - 1] = {
    1200                     text: o[n[i].row - 1],
     2204                o[ n[i].row - 1 ] = {
     2205                    text: o[ n[i].row - 1 ],
    12012206                    row: i - 1
    12022207                };
    12032208            }
    12042209        }
    1205        
     2210
    12062211        return {
    12072212            o: o,
     
    12092214        };
    12102215    }
    1211    
    1212     return function(o, n){
    1213         o = o.replace(/\s+$/, '');
    1214         n = n.replace(/\s+$/, '');
    1215         var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/));
    1216 
    1217         var str = "";
    1218        
    1219         var oSpace = o.match(/\s+/g);
    1220         if (oSpace == null) {
    1221             oSpace = [" "];
     2216
     2217    return function( o, n ) {
     2218        o = o.replace( /\s+$/, "" );
     2219        n = n.replace( /\s+$/, "" );
     2220
     2221        var i, pre,
     2222            str = "",
     2223            out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
     2224            oSpace = o.match(/\s+/g),
     2225            nSpace = n.match(/\s+/g);
     2226
     2227        if ( oSpace == null ) {
     2228            oSpace = [ " " ];
    12222229        }
    12232230        else {
    1224             oSpace.push(" ");
    1225         }
    1226         var nSpace = n.match(/\s+/g);
    1227         if (nSpace == null) {
    1228             nSpace = [" "];
     2231            oSpace.push( " " );
     2232        }
     2233
     2234        if ( nSpace == null ) {
     2235            nSpace = [ " " ];
    12292236        }
    12302237        else {
    1231             nSpace.push(" ");
    1232         }
    1233        
    1234         if (out.n.length == 0) {
    1235             for (var i = 0; i < out.o.length; i++) {
    1236                 str += '<del>' + out.o[i] + oSpace[i] + "</del>";
     2238            nSpace.push( " " );
     2239        }
     2240
     2241        if ( out.n.length === 0 ) {
     2242            for ( i = 0; i < out.o.length; i++ ) {
     2243                str += "<del>" + out.o[i] + oSpace[i] + "</del>";
    12372244            }
    12382245        }
    12392246        else {
    1240             if (out.n[0].text == null) {
    1241                 for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
    1242                     str += '<del>' + out.o[n] + oSpace[n] + "</del>";
    1243                 }
    1244             }
    1245            
    1246             for (var i = 0; i < out.n.length; i++) {
     2247            if ( out.n[0].text == null ) {
     2248                for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
     2249                    str += "<del>" + out.o[n] + oSpace[n] + "</del>";
     2250                }
     2251            }
     2252
     2253            for ( i = 0; i < out.n.length; i++ ) {
    12472254                if (out.n[i].text == null) {
    1248                     str += '<ins>' + out.n[i] + nSpace[i] + "</ins>";
     2255                    str += "<ins>" + out.n[i] + nSpace[i] + "</ins>";
    12492256                }
    12502257                else {
    1251                     var pre = "";
    1252                    
    1253                     for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
    1254                         pre += '<del>' + out.o[n] + oSpace[n] + "</del>";
     2258                    // `pre` initialized at top of scope
     2259                    pre = "";
     2260
     2261                    for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
     2262                        pre += "<del>" + out.o[n] + oSpace[n] + "</del>";
    12552263                    }
    12562264                    str += " " + out.n[i].text + nSpace[i] + pre;
     
    12582266            }
    12592267        }
    1260        
     2268
    12612269        return str;
    1262     }
    1263 })();
    1264 
    1265 })(this);
     2270    };
     2271}());
     2272
     2273// For browser, export only select globals
     2274if ( typeof window !== "undefined" ) {
     2275    extend( window, QUnit.constructor.prototype );
     2276    window.QUnit = QUnit;
     2277}
     2278
     2279// For CommonJS environments, export everything
     2280if ( typeof module !== "undefined" && module.exports ) {
     2281    module.exports = QUnit;
     2282}
     2283
     2284
     2285// Get a reference to the global object, like window in browsers
     2286}( (function() {
     2287    return this;
     2288})() ));
  • trunk/tests/qunit/editor/js/utils.js

    r27155 r27679  
    1 function fontFace(face) {
    2     if (tinymce.isOpera) {
    3         return "'" + face + "'";
    4     } else {
    5         return face;
    6     }
    7 }
    8 
    9 function findContainer(selector) {
    10     var container;
    11     if (tinymce.is(selector, 'string')) {
    12         container = editor.dom.select(selector)[0];
    13     } else {
    14         container = selector;
    15     }
    16     if (container.firstChild) {
    17         container = container.firstChild;
    18     }
    19     return container;
    20 }
    21 
    22 function setSelection(startSelector, startOffset, endSelector, endOffset) {
    23     if (!endSelector) {
    24         endSelector = startSelector;
    25         endOffset = startOffset;
    26     }
    27     var startContainer = findContainer(startSelector);
    28     var endContainer = findContainer(endSelector);
    29     var rng = editor.dom.createRng();
    30 
    31     function setRange(container, offset, start) {
    32         offset = offset || 0;
    33 
    34         if (offset === 'after') {
     1(function() {
     2    function fontFace(face) {
     3        if (tinymce.isOpera) {
     4            return "'" + face + "'";
     5        } else {
     6            return face;
     7        }
     8    }
     9
     10    function findContainer(selector) {
     11        var container;
     12        if (tinymce.is(selector, 'string')) {
     13            container = editor.dom.select(selector)[0];
     14        } else {
     15            container = selector;
     16        }
     17        if (container.firstChild) {
     18            container = container.firstChild;
     19        }
     20        return container;
     21    }
     22
     23    function setSelection(startSelector, startOffset, endSelector, endOffset) {
     24        if (!endSelector) {
     25            endSelector = startSelector;
     26            endOffset = startOffset;
     27        }
     28        var startContainer = findContainer(startSelector);
     29        var endContainer = findContainer(endSelector);
     30        var rng = editor.dom.createRng();
     31
     32        function setRange(container, offset, start) {
     33            offset = offset || 0;
     34
     35            if (offset === 'after') {
     36                if (start) {
     37                    rng.setStartAfter(container);
     38                } else {
     39                    rng.setEndAfter(container);
     40                }
     41                return;
     42            } else if (offset === 'afterNextCharacter') {
     43                container = container.nextSibling;
     44                offset = 1;
     45            }
    3546            if (start) {
    36                 rng.setStartAfter(container);
     47                rng.setStart(container, offset);
    3748            } else {
    38                 rng.setEndAfter(container);
    39             }
     49                rng.setEnd(container, offset);
     50            }
     51        }
     52
     53        setRange(startContainer, startOffset, true);
     54        setRange(endContainer, endOffset, false);
     55        editor.selection.setRng(rng);
     56    }
     57
     58    function trimContent(content) {
     59        return content.replace(/^<p>&nbsp;<\/p>\n?/, '').replace(/\n?<p>&nbsp;<\/p>$/, '');
     60    }
     61
     62    /**
     63     * Fakes a key event.
     64     *
     65     * @param {Element/String} e DOM element object or element id to send fake event to.
     66     * @param {String} na Event name to fake like "keydown".
     67     * @param {Object} o Optional object with data to send with the event like keyCode and charCode.
     68     */
     69    function fakeKeyEvent(e, na, o) {
     70        var ev;
     71
     72        o = tinymce.extend({
     73            keyCode : 13,
     74            charCode : 0
     75        }, o);
     76
     77        e = tinymce.DOM.get(e);
     78
     79        if (e.fireEvent) {
     80            ev = document.createEventObject();
     81            tinymce.extend(ev, o);
     82            e.fireEvent('on' + na, ev);
    4083            return;
    41         } else if (offset === 'afterNextCharacter') {
    42             container = container.nextSibling;
    43             offset = 1;
    44         }
    45         if (start) {
    46             rng.setStart(container, offset);
    47         } else {
    48             rng.setEnd(container, offset);
    49         }
    50     }
    51 
    52     setRange(startContainer, startOffset, true);
    53     setRange(endContainer, endOffset, false);
    54     editor.selection.setRng(rng);
    55 }
    56 
    57 function initWhenTinyAndRobotAreReady(initTinyFunction) {
    58     function loaded() {
    59         QUnit.start();
    60     }
    61 
    62     tinymce.on('AddEditor', function(e) {
    63         e.editor.on('Init', function() {
    64             loaded();
    65         });
    66     });
    67 
    68     window.robot.onload(initTinyFunction);
    69 }
    70 
    71 function trimContent(content) {
    72     return content.replace(/^<p>&nbsp;<\/p>\n?/, '').replace(/\n?<p>&nbsp;<\/p>$/, '');
    73 }
    74 
    75 /**
    76  * Fakes a key event.
    77  *
    78  * @param {Element/String} e DOM element object or element id to send fake event to.
    79  * @param {String} na Event name to fake like "keydown".
    80  * @param {Object} o Optional object with data to send with the event like keyCode and charCode.
    81  */
    82 function fakeKeyEvent(e, na, o) {
    83     var ev;
    84 
    85     o = tinymce.extend({
    86         keyCode : 13,
    87         charCode : 0
    88     }, o);
    89 
    90     e = tinymce.DOM.get(e);
    91 
    92     if (e.fireEvent) {
    93         ev = document.createEventObject();
    94         tinymce.extend(ev, o);
    95         e.fireEvent('on' + na, ev);
    96         return;
    97     }
    98 
    99     if (document.createEvent) {
    100         try {
    101             // Fails in Safari
    102             ev = document.createEvent('KeyEvents');
    103             ev.initKeyEvent(na, true, true, window, false, false, false, false, o.keyCode, o.charCode);
    104         } catch (ex) {
    105             ev = document.createEvent('Events');
    106             ev.initEvent(na, true, true);
     84        }
     85
     86        if (document.createEvent) {
     87            try {
     88                // Fails in Safari
     89                ev = document.createEvent('KeyEvents');
     90                ev.initKeyEvent(na, true, true, window, false, false, false, false, o.keyCode, o.charCode);
     91            } catch (ex) {
     92                ev = document.createEvent('Events');
     93                ev.initEvent(na, true, true);
     94
     95                ev.keyCode = o.keyCode;
     96                ev.charCode = o.charCode;
     97            }
     98        } else {
     99            ev = document.createEvent('UIEvents');
     100
     101            if (ev.initUIEvent)
     102                ev.initUIEvent(na, true, true, window, 1);
    107103
    108104            ev.keyCode = o.keyCode;
    109105            ev.charCode = o.charCode;
    110106        }
    111     } else {
    112         ev = document.createEvent('UIEvents');
    113 
    114         if (ev.initUIEvent)
    115             ev.initUIEvent(na, true, true, window, 1);
    116 
    117         ev.keyCode = o.keyCode;
    118         ev.charCode = o.charCode;
    119     }
    120 
    121     e.dispatchEvent(ev);
    122 }
    123 
    124 function normalizeRng(rng) {
    125     if (rng.startContainer.nodeType == 3) {
    126         if (rng.startOffset == 0)
    127             rng.setStartBefore(rng.startContainer);
    128         else if (rng.startOffset >= rng.startContainer.nodeValue.length - 1)
    129             rng.setStartAfter(rng.startContainer);
    130     }
    131 
    132     if (rng.endContainer.nodeType == 3) {
    133         if (rng.endOffset == 0)
    134             rng.setEndBefore(rng.endContainer);
    135         else if (rng.endOffset >= rng.endContainer.nodeValue.length - 1)
    136             rng.setEndAfter(rng.endContainer);
    137     }
    138 
    139     return rng;
    140 }
    141 
    142 // TODO: Replace this with the new event logic in 3.5
    143 function type(chr) {
    144     var editor = tinymce.activeEditor, keyCode, charCode, event = tinymce.dom.Event, evt, startElm, rng;
    145 
    146     function fakeEvent(target, type, evt) {
    147         editor.dom.fire(target, type, evt);
    148     }
    149 
    150     // Numeric keyCode
    151     if (typeof(chr) == "number") {
    152         charCode = keyCode = chr;
    153     } else if (typeof(chr) == "string") {
    154         // String value
    155         if (chr == '\b') {
    156             keyCode = 8;
    157             charCode = chr.charCodeAt(0);
    158         } else if (chr == '\n') {
    159             keyCode = 13;
    160             charCode = chr.charCodeAt(0);
    161         } else {
    162             charCode = chr.charCodeAt(0);
    163             keyCode = charCode;
    164         }
    165     } else {
    166         evt = chr;
    167     }
    168 
    169     evt = evt || {keyCode: keyCode, charCode: charCode};
    170 
    171     startElm = editor.selection.getStart();
    172     fakeEvent(startElm, 'keydown', evt);
    173     fakeEvent(startElm, 'keypress', evt);
    174 
    175     if (!evt.isDefaultPrevented()) {
    176         if (keyCode == 8) {
    177             if (editor.getDoc().selection) {
    178                 rng = editor.getDoc().selection.createRange();
    179 
    180                 if (rng.text.length === 0) {
    181                     rng.moveStart('character', -1);
    182                     rng.select();
     107
     108        e.dispatchEvent(ev);
     109    }
     110
     111    function normalizeRng(rng) {
     112        if (rng.startContainer.nodeType == 3) {
     113            if (rng.startOffset == 0)
     114                rng.setStartBefore(rng.startContainer);
     115            else if (rng.startOffset >= rng.startContainer.nodeValue.length - 1)
     116                rng.setStartAfter(rng.startContainer);
     117        }
     118
     119        if (rng.endContainer.nodeType == 3) {
     120            if (rng.endOffset == 0)
     121                rng.setEndBefore(rng.endContainer);
     122            else if (rng.endOffset >= rng.endContainer.nodeValue.length - 1)
     123                rng.setEndAfter(rng.endContainer);
     124        }
     125
     126        return rng;
     127    }
     128
     129    // TODO: Replace this with the new event logic in 3.5
     130    function type(chr) {
     131        var editor = tinymce.activeEditor, keyCode, charCode, event = tinymce.dom.Event, evt, startElm, rng;
     132
     133        function fakeEvent(target, type, evt) {
     134            editor.dom.fire(target, type, evt);
     135        }
     136
     137        // Numeric keyCode
     138        if (typeof(chr) == "number") {
     139            charCode = keyCode = chr;
     140        } else if (typeof(chr) == "string") {
     141            // String value
     142            if (chr == '\b') {
     143                keyCode = 8;
     144                charCode = chr.charCodeAt(0);
     145            } else if (chr == '\n') {
     146                keyCode = 13;
     147                charCode = chr.charCodeAt(0);
     148            } else {
     149                charCode = chr.charCodeAt(0);
     150                keyCode = charCode;
     151            }
     152        } else {
     153            evt = chr;
     154        }
     155
     156        evt = evt || {keyCode: keyCode, charCode: charCode};
     157
     158        startElm = editor.selection.getStart();
     159        fakeEvent(startElm, 'keydown', evt);
     160        fakeEvent(startElm, 'keypress', evt);
     161
     162        if (!evt.isDefaultPrevented()) {
     163            if (keyCode == 8) {
     164                if (editor.getDoc().selection) {
     165                    rng = editor.getDoc().selection.createRange();
     166
     167                    if (rng.text.length === 0) {
     168                        rng.moveStart('character', -1);
     169                        rng.select();
     170                    }
     171
     172                    rng.execCommand('Delete', false, null);
     173                } else {
     174                    rng = editor.selection.getRng();
     175
     176                    if (rng.startContainer.nodeType == 1 && rng.collapsed) {
     177                        var nodes = rng.startContainer.childNodes, lastNode = nodes[nodes.length - 1];
     178
     179                        // If caret is at <p>abc|</p> and after the abc text node then move it to the end of the text node
     180                        // Expand the range to include the last char <p>ab[c]</p> since IE 11 doesn't delete otherwise
     181                        if (rng.startOffset >= nodes.length - 1 && lastNode && lastNode.nodeType == 3 && lastNode.data.length > 0) {
     182                            rng.setStart(lastNode, lastNode.data.length - 1);
     183                            rng.setEnd(lastNode, lastNode.data.length);
     184                            editor.selection.setRng(rng);
     185                        }
     186                    }
     187
     188                    editor.getDoc().execCommand('Delete', false, null);
    183189                }
    184 
    185                 rng.execCommand('Delete', false, null);
    186             } else {
    187                 rng = editor.selection.getRng();
    188 
    189                 if (rng.startContainer.nodeType == 1 && rng.collapsed) {
    190                     var nodes = rng.startContainer.childNodes, lastNode = nodes[nodes.length - 1];
    191 
    192                     // If caret is at <p>abc|</p> and after the abc text node then move it to the end of the text node
    193                     // Expand the range to include the last char <p>ab[c]</p> since IE 11 doesn't delete otherwise
    194                     if (rng.startOffset >= nodes.length - 1 && lastNode && lastNode.nodeType == 3 && lastNode.data.length > 0) {
    195                         rng.setStart(lastNode, lastNode.data.length - 1);
    196                         rng.setEnd(lastNode, lastNode.data.length);
    197                         editor.selection.setRng(rng);
     190            } else if (typeof(chr) == 'string') {
     191                rng = editor.selection.getRng(true);
     192
     193                if (rng.startContainer.nodeType == 3 && rng.collapsed) {
     194                    rng.startContainer.insertData(rng.startOffset, chr);
     195                    rng.setStart(rng.startContainer, rng.startOffset + 1);
     196                    rng.collapse(true);
     197                    editor.selection.setRng(rng);
     198                } else {
     199                    rng.insertNode(editor.getDoc().createTextNode(chr));
     200                }
     201            }
     202        }
     203
     204        fakeEvent(startElm, 'keyup', evt);
     205    }
     206
     207    function cleanHtml(html) {
     208        html = html.toLowerCase().replace(/[\r\n]+/gi, '');
     209        html = html.replace(/ (sizcache[0-9]+|sizcache|nodeindex|sizset[0-9]+|sizset|data\-mce\-expando|data\-mce\-selected)="[^"]*"/gi, '');
     210        html = html.replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>|<div[^>]+data-mce-bogus[^>]+><\/div>/gi, '');
     211        html = html.replace(/ style="([^"]+)"/gi, function(val1, val2) {
     212            val2 = val2.replace(/;$/, '');
     213            return ' style="' + val2.replace(/\:([^ ])/g, ': $1') + ';"';
     214        });
     215
     216        return html;
     217    }
     218
     219    function normalizeHtml(html) {
     220        var writer = new tinymce.html.Writer();
     221
     222        new tinymce.html.SaxParser({
     223            validate: false,
     224            comment: writer.comment,
     225            cdata: writer.cdata,
     226            text: writer.text,
     227            end: writer.end,
     228            pi: writer.pi,
     229            doctype: writer.doctype,
     230
     231            start: function(name, attrs, empty) {
     232                attrs.sort(function(a, b) {
     233                    if (a.name === b.name) {
     234                        return 0;
    198235                    }
    199                 }
    200 
    201                 editor.getDoc().execCommand('Delete', false, null);
    202             }
    203         } else if (typeof(chr) == 'string') {
    204             rng = editor.selection.getRng(true);
    205 
    206             if (rng.startContainer.nodeType == 3 && rng.collapsed) {
    207                 rng.startContainer.insertData(rng.startOffset, chr);
    208                 rng.setStart(rng.startContainer, rng.startOffset + 1);
    209                 rng.collapse(true);
    210                 editor.selection.setRng(rng);
    211             } else {
    212                 rng.insertNode(editor.getDoc().createTextNode(chr));
    213             }
    214         }
    215     }
    216 
    217     fakeEvent(startElm, 'keyup', evt);
    218 }
    219 
    220 function cleanHtml(html) {
    221     html = html.toLowerCase().replace(/[\r\n]+/gi, '');
    222     html = html.replace(/ (sizcache[0-9]+|sizcache|nodeindex|sizset[0-9]+|sizset|data\-mce\-expando|data\-mce\-selected)="[^"]*"/gi, '');
    223     html = html.replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>|<div[^>]+data-mce-bogus[^>]+><\/div>/gi, '');
    224 
    225     return html;
    226 }
    227 
    228 function normalizeHtml(html) {
    229     var writer = new tinymce.html.Writer();
    230 
    231     new tinymce.html.SaxParser({
    232         validate: false,
    233         comment: writer.comment,
    234         cdata: writer.cdata,
    235         text: writer.text,
    236         end: writer.end,
    237         pi: writer.pi,
    238         doctype: writer.doctype,
    239 
    240         start: function(name, attrs, empty) {
    241             attrs.sort(function(a, b) {
    242                 if (a.name === b.name) {
    243                     return 0;
    244                 }
    245 
    246                 return a.name > b.name ? 1 : -1;
    247             });
    248 
    249             writer.start(name, attrs, empty);
    250         }
    251     }).parse(html);
    252 
    253     return writer.getContent();
    254 }
    255 
    256 /**
    257  * Measures the x, y, w, h of the specified element/control relative to the view element.
    258  */
    259 function rect(ctrl) {
    260     var outerRect, innerRect;
    261 
    262     if (ctrl.nodeType) {
    263         innerRect = ctrl.getBoundingClientRect();
    264     } else {
    265         innerRect = ctrl.getEl().getBoundingClientRect();
    266     }
    267 
    268     outerRect = document.getElementById('view').getBoundingClientRect();
    269 
    270     return [
    271         Math.round(innerRect.left - outerRect.left),
    272         Math.round(innerRect.top - outerRect.top),
    273         Math.round(innerRect.right - innerRect.left),
    274         Math.round(innerRect.bottom - innerRect.top)
    275     ];
    276 }
    277 
    278 function size(ctrl) {
    279     return rect(ctrl).slice(2);
    280 }
    281 
    282 function resetScroll(elm) {
    283     elm.scrollTop = 0;
    284     elm.scrollLeft = 0;
    285 }
    286 
    287 // Needed since fonts render differently on different platforms
    288 function nearlyEqualRects(rect1, rect2, diff) {
    289     diff = diff || 1;
    290 
    291     for (var i = 0; i < 4; i++) {
    292         if (Math.abs(rect1[i] - rect2[i]) > diff) {
    293             deepEqual(rect1, rect2);
    294             return;
    295         }
    296     }
    297 
    298     ok(true);
    299 }
     236
     237                    return a.name > b.name ? 1 : -1;
     238                });
     239
     240                writer.start(name, attrs, empty);
     241            }
     242        }).parse(html);
     243
     244        return writer.getContent();
     245    }
     246
     247    /**
     248     * Measures the x, y, w, h of the specified element/control relative to the view element.
     249     */
     250    function rect(ctrl) {
     251        var outerRect, innerRect;
     252
     253        if (ctrl.nodeType) {
     254            innerRect = ctrl.getBoundingClientRect();
     255        } else {
     256            innerRect = ctrl.getEl().getBoundingClientRect();
     257        }
     258
     259        outerRect = document.getElementById('view').getBoundingClientRect();
     260
     261        return [
     262            Math.round(innerRect.left - outerRect.left),
     263            Math.round(innerRect.top - outerRect.top),
     264            Math.round(innerRect.right - innerRect.left),
     265            Math.round(innerRect.bottom - innerRect.top)
     266        ];
     267    }
     268
     269    function size(ctrl) {
     270        return rect(ctrl).slice(2);
     271    }
     272
     273    function resetScroll(elm) {
     274        elm.scrollTop = 0;
     275        elm.scrollLeft = 0;
     276    }
     277
     278    // Needed since fonts render differently on different platforms
     279    function nearlyEqualRects(rect1, rect2, diff) {
     280        diff = diff || 1;
     281
     282        for (var i = 0; i < 4; i++) {
     283            if (Math.abs(rect1[i] - rect2[i]) > diff) {
     284                deepEqual(rect1, rect2);
     285                return;
     286            }
     287        }
     288
     289        ok(true);
     290    }
     291
     292    function getFontmostWindow() {
     293        return editor.windowManager.windows[editor.windowManager.windows.length - 1];
     294    }
     295
     296    function pressArrowKey(evt) {
     297        var dom = editor.dom, target = editor.selection.getNode();
     298
     299        evt = tinymce.extend({keyCode: 37}, evt);
     300
     301        dom.fire(target, 'keydown', evt);
     302        dom.fire(target, 'keypress', evt);
     303        dom.fire(target, 'keyup', evt);
     304    }
     305
     306    function pressEnter(evt) {
     307        var dom = editor.dom, target = editor.selection.getNode();
     308
     309        evt = tinymce.extend({keyCode: 13}, evt);
     310
     311        dom.fire(target, 'keydown', evt);
     312        dom.fire(target, 'keypress', evt);
     313        dom.fire(target, 'keyup', evt);
     314    }
     315
     316    function trimBrsOnIE(html) {
     317        return html.replace(/<br[^>]*>/gi, '');
     318    }
     319
     320    window.Utils = {
     321        fontFace: fontFace,
     322        findContainer: findContainer,
     323        setSelection: setSelection,
     324        trimContent: trimContent,
     325        fakeKeyEvent: fakeKeyEvent,
     326        normalizeRng: normalizeRng,
     327        type: type,
     328        cleanHtml: cleanHtml,
     329        normalizeHtml: normalizeHtml,
     330        rect: rect,
     331        size: size,
     332        resetScroll: resetScroll,
     333        nearlyEqualRects: nearlyEqualRects,
     334        getFontmostWindow: getFontmostWindow,
     335        pressArrowKey: pressArrowKey,
     336        pressEnter: pressEnter,
     337        trimBrsOnIE: trimBrsOnIE
     338    };
     339})();
  • trunk/tests/qunit/editor/tinymce/dom/DOMUtils.js

    r27155 r27679  
    11(function() {
     2    module("tinymce.dom.DOMUtils");
     3
    24    var DOM = new tinymce.dom.DOMUtils(document, {keep_values : true, schema : new tinymce.html.Schema()});
    35
     
    79        DOM.add(document.body, 'div', {id : 'test'});
    810
    9         dom = new tinymce.dom.DOMUtils(document, {hex_colors : true, keep_values : true, url_converter : function(u, n, e) {
     11        dom = new tinymce.dom.DOMUtils(document, {hex_colors : true, keep_values : true, url_converter : function(u) {
    1012            return 'X' + u + 'Y';
    1113        }});
     
    219221        ok(DOM.select('div', 'test').reverse);
    220222
    221         DOM.setHTML('test', '<div class="test1 test2 test3">test 1</div><div class="test2">test 2 <div>test 3</div></div><div>test 4</div>')
     223        DOM.setHTML('test', '<div class="test1 test2 test3">test 1</div><div class="test2">test 2 <div>test 3</div></div><div>test 4</div>');
    222224        equal(DOM.select('div.test2', 'test').length, 2);
    223225
    224         DOM.setHTML('test', '<div class="test1 test2 test3">test 1</div><div class="test2">test 2 <div>test 3</div></div><div>test 4</div>')
     226        DOM.setHTML('test', '<div class="test1 test2 test3">test 1</div><div class="test2">test 2 <div>test 3</div></div><div>test 4</div>');
    225227        equal(DOM.select('div div', 'test').length, 1, null, tinymce.isWebKit); // Issue: http://bugs.webkit.org/show_bug.cgi?id=17461
    226228        //alert(DOM.select('div div', 'test').length +","+DOM.get('test').querySelectorAll('div div').length);
     
    265267        equal(DOM.getAttrib('test', 'title'), 'abc');
    266268
    267         dom = new tinymce.dom.DOMUtils(document, {keep_values : true, url_converter : function(u, n, e) {
     269        dom = new tinymce.dom.DOMUtils(document, {keep_values : true, url_converter : function(u, n) {
    268270            return '&<>"' + u + '&<>"' + n;
    269271        }});
     
    287289
    288290    test('getAttribs', 2, function() {
    289         var dom;
    290 
    291291        function check(obj, val) {
    292292            var count = 0;
     
    295295
    296296            tinymce.each(obj, function(o) {
    297                 if (tinymce.inArray(val, o.nodeName.toLowerCase()) != -1 && o.specified)
     297                if (tinymce.inArray(val, o.nodeName.toLowerCase()) != -1 && o.specified) {
    298298                    count++;
     299                }
    299300            });
    300301
    301302            return count == obj.length;
    302         };
     303        }
    303304
    304305        DOM.add(document.body, 'div', {id : 'test'});
     
    357358        equal(DOM.getParent('test2', function(n) {return n.nodeName == 'BODY';}).nodeName, 'BODY');
    358359        equal(DOM.getParent('test2', function(n) {return n.nodeName == 'BODY';}, document.body), null);
    359         equal(DOM.getParent('test2', function(n) {return false;}), null);
     360        equal(DOM.getParent('test2', function() {return false;}), null);
    360361        equal(DOM.getParent('test2', 'SPAN').nodeName, 'SPAN');
    361362        equal(DOM.getParent('test2', 'body', DOM.get('test')), null);
     
    436437
    437438    test('getNext', 5, function() {
    438         var r;
    439 
    440439        DOM.add(document.body, 'div', {id : 'test'});
    441440
     
    445444        equal(DOM.getNext(DOM.get('test').firstChild, 'div'), null);
    446445        equal(DOM.getNext(null, 'div'), null);
    447         equal(DOM.getNext(DOM.get('test').firstChild, function(n) {return n.nodeName == 'EM'}).nodeName, 'EM');
     446        equal(DOM.getNext(DOM.get('test').firstChild, function(n) {return n.nodeName == 'EM';}).nodeName, 'EM');
    448447
    449448        DOM.remove('test');
     
    451450
    452451    test('getPrev', 5, function() {
    453         var r;
    454 
    455452        DOM.add(document.body, 'div', {id : 'test'});
    456453
     
    460457        equal(DOM.getPrev(DOM.get('test').lastChild, 'div'), null);
    461458        equal(DOM.getPrev(null, 'div'), null);
    462         equal(DOM.getPrev(DOM.get('test').lastChild, function(n) {return n.nodeName == 'STRONG'}).nodeName, 'STRONG');
     459        equal(DOM.getPrev(DOM.get('test').lastChild, function(n) {return n.nodeName == 'STRONG';}).nodeName, 'STRONG');
    463460
    464461        DOM.remove('test');
     
    468465        var c = 0;
    469466
    470         DOM.loadCSS('css/test.css?a=1,css/test.css?a=2,css/test.css?a=3');
     467        DOM.loadCSS('tinymce/dom/test.css?a=1,tinymce/dom/test.css?a=2,tinymce/dom/test.css?a=3');
    471468
    472469        tinymce.each(document.getElementsByTagName('link'), function(n) {
    473             if (n.href.indexOf('test.css?a=') != -1)
     470            if (n.href.indexOf('test.css?a=') != -1) {
    474471                c++;
     472            }
    475473        });
    476474
     
    577575        point = DOM.select('ul li:nth-child(2)', DOM.get('test'))[0];
    578576        DOM.split(parent, point);
    579         equal(cleanHtml(DOM.get('test').innerHTML), '<ul><li>first line<br><ul><li><span>second</span> <span>line</span></li></ul></li><li>third line<br></li></ul>');
     577        equal(Utils.cleanHtml(DOM.get('test').innerHTML), '<ul><li>first line<br><ul><li><span>second</span> <span>line</span></li></ul></li><li>third line<br></li></ul>');
    580578
    581579        DOM.remove('test');
     
    631629        ok(!DOM.isEmpty(DOM.get('test')), 'Non empty complex HTML with achor name');
    632630
    633         DOM.setHTML('test', '<img src="x">');
     631        DOM.setHTML('test', '<img src="tinymce/ui/img/raster.gif">');
    634632        ok(!DOM.isEmpty(DOM.get('test')), 'Non empty html with img element');
    635633
Note: See TracChangeset for help on using the changeset viewer.