├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── jquery.truncate.js ├── package.json └── test ├── index.html ├── index.js └── vendor ├── jquery.js ├── qunit.css ├── qunit.js └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2012 Pathable 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/pathable/truncate.png?branch=master)](https://travis-ci.org/pathable/truncate) 2 | 3 | ## Usage 4 | 5 | ### jQuery.truncate(html, options) 6 | 7 | ```javascript 8 | > jQuery.truncate('

Stuff and Nonsense

', { 9 | length: 13 10 | }); 11 | '

Stuff and No…

' 12 | ``` 13 | 14 | ### $el.truncate(options) 15 | 16 | ```javascript 17 | > jQuery('

Stuff and Nonsense

').truncate({ 18 | length: 13 19 | }).html(); 20 | '

Stuff and No…

' 21 | ``` 22 | 23 | ## Options 24 | 25 | Default options are stored on `jQuery.truncate.defaults`. 26 | 27 | ### length 28 | 29 | *Default: Infinity* 30 | 31 | The maximum length (including the ellipsis) of the truncated html. 32 | 33 | ### stripTags 34 | 35 | *Default: false* 36 | 37 | If `stripTags` is truthy all html tags will be stripped, leaving only the text. 38 | 39 | ```javascript 40 | > jQuery.truncate('

Stuff and Nonsense

', { 41 | length: 13, 42 | stripTags: true 43 | }); 44 | 'Stuff and No…' 45 | ``` 46 | 47 | ### words 48 | 49 | *Default: false* 50 | 51 | If `words` is truthy the input will only be truncated at word boundaries. 52 | 53 | ```javascript 54 | > jQuery.truncate('

Stuff and Nonsense

', { 55 | length: 13, 56 | words: true 57 | }); 58 | '

Stuff and…

' 59 | ``` 60 | 61 | ### keepFirstWord 62 | 63 | *Default: false* 64 | 65 | When `words` and `keepFirstWord` are both truthy the input will contain 66 | at least one word beside the ellipsis even if it's longer than 67 | a target length. 68 | 69 | ```javascript 70 | > jQuery.truncate('

Stuff and Nonsense

', { 71 | length: 4, 72 | words: true, 73 | keepFirstWord: true 74 | }); 75 | '

Stuff…

' 76 | ``` 77 | 78 | ### finishBlock 79 | 80 | *Default: false* 81 | 82 | When `finishBlock` is true it will always truncate the content at the end of the block. 83 | 84 | ```javascript 85 | > jQuery.truncate('

Stuff and Nonsense and something else

', { 86 | length: 13, 87 | finishBlock: true, 88 | }); 89 | '

Stuff and Nonsense…

' 90 | ``` 91 | 92 | ### noBreaks 93 | 94 | *Default: false* 95 | 96 | If `noBreaks` is truthy the input will contain no break elements. 97 | 98 | ```javascript 99 | > jQuery.truncate('

Stuff and
Nonsense

', { 100 | length: 13, 101 | noBreaks: true 102 | }); 103 | '

Stuff and No…

' 104 | ``` 105 | 106 | ### ellipsis 107 | 108 | *Default: '…'* 109 | 110 | The `ellipsis` setting is used to provide a different character for the ellipsis. 111 | 112 | ```javascript 113 | > jQuery.truncate('

Stuff and Nonsense

', { 114 | length: 13, 115 | ellipsis: '~' 116 | }); 117 | '

Stuff and No~

' 118 | ``` 119 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-truncate-html", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /jquery.truncate.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // Matches trailing non-space characters. 4 | var chop = /(\s*\S+|\s)$/; 5 | 6 | // Matches the first word in the string. 7 | var start = /^(\S*)/; 8 | 9 | // Return a truncated html string. Delegates to $.fn.truncate. 10 | $.truncate = function(html, options) { 11 | return $('
').append(html).truncate(options).html(); 12 | }; 13 | 14 | // Truncate the contents of an element in place. 15 | $.fn.truncate = function(options) { 16 | if ($.isNumeric(options)) options = {length: options}; 17 | var o = $.extend({}, $.truncate.defaults, options); 18 | 19 | return this.each(function() { 20 | var self = $(this); 21 | 22 | if (o.noBreaks) self.find('br').replaceWith(' '); 23 | 24 | var text = self.text(); 25 | var excess = text.length - o.length; 26 | 27 | if (o.stripTags) self.text(text); 28 | 29 | // Chop off any partial words if appropriate. 30 | if (o.words && excess > 0) { 31 | var truncated = text.slice(0, o.length).replace(chop, '').length; 32 | 33 | if (o.keepFirstWord && truncated === 0) { 34 | excess = text.length - start.exec(text)[0].length - 1; 35 | } else { 36 | excess = text.length - truncated - 1; 37 | } 38 | } 39 | 40 | if (excess < 0 || !excess && !o.truncated) return; 41 | 42 | // Iterate over each child node in reverse, removing excess text. 43 | $.each(self.contents().get().reverse(), function(i, el) { 44 | var $el = $(el); 45 | var text = $el.text(); 46 | var length = text.length; 47 | 48 | // If the text is longer than the excess, remove the node and continue. 49 | if (length <= excess) { 50 | o.truncated = true; 51 | excess -= length; 52 | $el.remove(); 53 | return; 54 | } 55 | 56 | // Remove the excess text and append the ellipsis. 57 | if (el.nodeType === 3) { 58 | // should we finish the block anyway? 59 | if (o.finishBlock) { 60 | $(el.splitText(length)).replaceWith(o.ellipsis); 61 | } else { 62 | $(el.splitText(length - excess - 1)).replaceWith(o.ellipsis); 63 | } 64 | return false; 65 | } 66 | 67 | // Recursively truncate child nodes. 68 | $el.truncate($.extend(o, {length: length - excess})); 69 | return false; 70 | }); 71 | }); 72 | }; 73 | 74 | $.truncate.defaults = { 75 | 76 | // Strip all html elements, leaving only plain text. 77 | stripTags: false, 78 | 79 | // Only truncate at word boundaries. 80 | words: false, 81 | 82 | // When 'words' is active, keeps the first word in the string 83 | // even if it's longer than a target length. 84 | keepFirstWord: false, 85 | 86 | // Replace instances of
with a single space. 87 | noBreaks: false, 88 | 89 | // if true always truncate the content at the end of the block. 90 | finishBlock: false, 91 | 92 | // The maximum length of the truncated html. 93 | length: Infinity, 94 | 95 | // The character to use as the ellipsis. The word joiner (U+2060) can be 96 | // used to prevent a hanging ellipsis, but displays incorrectly in Chrome 97 | // on Windows 7. 98 | // http://code.google.com/p/chromium/issues/detail?id=68323 99 | ellipsis: '\u2026' // '\u2060\u2026' 100 | 101 | }; 102 | 103 | })(jQuery); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truncate", 3 | "version": "0.0.1", 4 | "repository": "https://github.com/pathable/truncate.git", 5 | "scripts": { 6 | "test": "phantomjs ./test/vendor/runner.js ./test/index.html?noglobals=true" 7 | }, 8 | "devDependencies": { 9 | "phantomjs": "1.9.0-1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Truncate Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | module('truncate'); 4 | 5 | var txt = 'stuff and nonsense'; 6 | var html = 'stuff and nonsense'; 7 | 8 | test('Accept a number as options.', function() { 9 | strictEqual($.truncate('four', 3), 'fo…'); 10 | }); 11 | 12 | test('plain text', function() { 13 | strictEqual($.truncate(txt, {length: 20}), txt); 14 | strictEqual($.truncate(txt, {length: 18}), txt); 15 | strictEqual($.truncate(txt, {length: 9}), 'stuff an…'); 16 | }); 17 | 18 | test('tags', function() { 19 | strictEqual($.truncate(html, {length: 20}), html); 20 | strictEqual($.truncate(html, {length: 18}), html); 21 | strictEqual($.truncate(html, {length: 13}), 'stuff and no…'); 22 | strictEqual($.truncate(html, {length: 9}), 'stuff an…'); 23 | strictEqual($.truncate(html, {length: 8}), 'stuff a…'); 24 | strictEqual($.truncate(html, {length: 7}), 'stuff'); 25 | strictEqual($.truncate(html, {length: 6}), 'stuff'); 26 | strictEqual($.truncate(html, {length: 5}), 'stuf…'); 27 | strictEqual($.truncate(html, {length: 4}), 'stu…'); 28 | }); 29 | 30 | test('words', function() { 31 | strictEqual($.truncate(txt, {length: 20, words: true}), txt); 32 | strictEqual($.truncate(txt, {length: 18, words: true}), txt); 33 | strictEqual($.truncate(txt, {length: 14, words: true}), 'stuff and…'); 34 | strictEqual($.truncate(txt, {length: 10, words: true}), 'stuff and…'); 35 | strictEqual($.truncate(txt, {length: 9, words: true}), 'stuff…'); 36 | strictEqual($.truncate(txt, {length: 4, words: true}), '…'); 37 | }); 38 | 39 | test('keepLastWord', function () { 40 | strictEqual($.truncate(txt, {length: 4, words: true, keepFirstWord: true}), 'stuff…'); 41 | }); 42 | 43 | test('finishBlock', function () { 44 | var html = '

stuff and nonsense and something else

'; 45 | strictEqual($.truncate(html, {length: 13, finishBlock: true}), '

stuff and nonsense…

'); 46 | strictEqual($.truncate(html, {length: 23, finishBlock: true}), '

stuff and nonsense and something else…

'); 47 | }); 48 | 49 | test('noBreaks', function() { 50 | strictEqual($.truncate('foo
bar baz', {length: 8, noBreaks: true}), 'foo bar…'); 51 | }); 52 | 53 | test('stripTags', function() { 54 | strictEqual($.truncate('foo bar baz', {stripTags: true}), 'foo bar baz'); 55 | strictEqual($.truncate('foo bar baz', {length: 8, stripTags: true}), 'foo bar…'); 56 | }); 57 | 58 | test('entities', function() { 59 | var html = '
foo<bar<baz
'; 60 | strictEqual($.truncate(html, {length: 5}), '
foo<…
'); 61 | strictEqual($.truncate(html, {length: 6}), '
foo<b…
'); 62 | strictEqual($.truncate(html, {length: 8}), '
foo<bar…
'); 63 | strictEqual($.truncate(html, {length: 9}), '
foo<bar<…
'); 64 | }); 65 | 66 | test('html ellipsis', function() { 67 | var ellipsis = '… '; 68 | strictEqual($.truncate(txt, {length: 9, ellipsis: ellipsis}), 'stuff an… '); 69 | strictEqual($.truncate(html, {length: 4, ellipsis: ellipsis}), 'stu… '); 70 | }); 71 | 72 | })(jQuery); 73 | -------------------------------------------------------------------------------- /test/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /test/vendor/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2013 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * https://jquery.org/license/ 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | setTimeout = window.setTimeout, 24 | defined = { 25 | setTimeout: typeof window.setTimeout !== "undefined", 26 | sessionStorage: (function() { 27 | var x = "qunit-test-string"; 28 | try { 29 | sessionStorage.setItem( x, x ); 30 | sessionStorage.removeItem( x ); 31 | return true; 32 | } catch( e ) { 33 | return false; 34 | } 35 | }()) 36 | }, 37 | /** 38 | * Provides a normalized error string, correcting an issue 39 | * with IE 7 (and prior) where Error.prototype.toString is 40 | * not properly implemented 41 | * 42 | * Based on http://es5.github.com/#x15.11.4.4 43 | * 44 | * @param {String|Error} error 45 | * @return {String} error message 46 | */ 47 | errorString = function( error ) { 48 | var name, message, 49 | errorString = error.toString(); 50 | if ( errorString.substring( 0, 7 ) === "[object" ) { 51 | name = error.name ? error.name.toString() : "Error"; 52 | message = error.message ? error.message.toString() : ""; 53 | if ( name && message ) { 54 | return name + ": " + message; 55 | } else if ( name ) { 56 | return name; 57 | } else if ( message ) { 58 | return message; 59 | } else { 60 | return "Error"; 61 | } 62 | } else { 63 | return errorString; 64 | } 65 | }, 66 | /** 67 | * Makes a clone of an object using only Array or Object as base, 68 | * and copies over the own enumerable properties. 69 | * 70 | * @param {Object} obj 71 | * @return {Object} New object with only the own properties (recursively). 72 | */ 73 | objectValues = function( obj ) { 74 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 75 | /*jshint newcap: false */ 76 | var key, val, 77 | vals = QUnit.is( "array", obj ) ? [] : {}; 78 | for ( key in obj ) { 79 | if ( hasOwn.call( obj, key ) ) { 80 | val = obj[key]; 81 | vals[key] = val === Object(val) ? objectValues(val) : val; 82 | } 83 | } 84 | return vals; 85 | }; 86 | 87 | function Test( settings ) { 88 | extend( this, settings ); 89 | this.assertions = []; 90 | this.testNumber = ++Test.count; 91 | } 92 | 93 | Test.count = 0; 94 | 95 | Test.prototype = { 96 | init: function() { 97 | var a, b, li, 98 | tests = id( "qunit-tests" ); 99 | 100 | if ( tests ) { 101 | b = document.createElement( "strong" ); 102 | b.innerHTML = this.nameHtml; 103 | 104 | // `a` initialized at top of scope 105 | a = document.createElement( "a" ); 106 | a.innerHTML = "Rerun"; 107 | a.href = QUnit.url({ testNumber: this.testNumber }); 108 | 109 | li = document.createElement( "li" ); 110 | li.appendChild( b ); 111 | li.appendChild( a ); 112 | li.className = "running"; 113 | li.id = this.id = "qunit-test-output" + testId++; 114 | 115 | tests.appendChild( li ); 116 | } 117 | }, 118 | setup: function() { 119 | if ( 120 | // Emit moduleStart when we're switching from one module to another 121 | this.module !== config.previousModule || 122 | // They could be equal (both undefined) but if the previousModule property doesn't 123 | // yet exist it means this is the first test in a suite that isn't wrapped in a 124 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 125 | // Without this, reporters can get testStart before moduleStart which is a problem. 126 | !hasOwn.call( config, "previousModule" ) 127 | ) { 128 | if ( hasOwn.call( config, "previousModule" ) ) { 129 | runLoggingCallbacks( "moduleDone", QUnit, { 130 | name: config.previousModule, 131 | failed: config.moduleStats.bad, 132 | passed: config.moduleStats.all - config.moduleStats.bad, 133 | total: config.moduleStats.all 134 | }); 135 | } 136 | config.previousModule = this.module; 137 | config.moduleStats = { all: 0, bad: 0 }; 138 | runLoggingCallbacks( "moduleStart", QUnit, { 139 | name: this.module 140 | }); 141 | } 142 | 143 | config.current = this; 144 | 145 | this.testEnvironment = extend({ 146 | setup: function() {}, 147 | teardown: function() {} 148 | }, this.moduleTestEnvironment ); 149 | 150 | this.started = +new Date(); 151 | runLoggingCallbacks( "testStart", QUnit, { 152 | name: this.testName, 153 | module: this.module 154 | }); 155 | 156 | /*jshint camelcase:false */ 157 | 158 | 159 | /** 160 | * Expose the current test environment. 161 | * 162 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. 163 | */ 164 | QUnit.current_testEnvironment = this.testEnvironment; 165 | 166 | /*jshint camelcase:true */ 167 | 168 | if ( !config.pollution ) { 169 | saveGlobal(); 170 | } 171 | if ( config.notrycatch ) { 172 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 173 | return; 174 | } 175 | try { 176 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 177 | } catch( e ) { 178 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 179 | } 180 | }, 181 | run: function() { 182 | config.current = this; 183 | 184 | var running = id( "qunit-testresult" ); 185 | 186 | if ( running ) { 187 | running.innerHTML = "Running:
" + this.nameHtml; 188 | } 189 | 190 | if ( this.async ) { 191 | QUnit.stop(); 192 | } 193 | 194 | this.callbackStarted = +new Date(); 195 | 196 | if ( config.notrycatch ) { 197 | this.callback.call( this.testEnvironment, QUnit.assert ); 198 | this.callbackRuntime = +new Date() - this.callbackStarted; 199 | return; 200 | } 201 | 202 | try { 203 | this.callback.call( this.testEnvironment, QUnit.assert ); 204 | this.callbackRuntime = +new Date() - this.callbackStarted; 205 | } catch( e ) { 206 | this.callbackRuntime = +new Date() - this.callbackStarted; 207 | 208 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 209 | // else next test will carry the responsibility 210 | saveGlobal(); 211 | 212 | // Restart the tests if they're blocking 213 | if ( config.blocking ) { 214 | QUnit.start(); 215 | } 216 | } 217 | }, 218 | teardown: function() { 219 | config.current = this; 220 | if ( config.notrycatch ) { 221 | if ( typeof this.callbackRuntime === "undefined" ) { 222 | this.callbackRuntime = +new Date() - this.callbackStarted; 223 | } 224 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 225 | return; 226 | } else { 227 | try { 228 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 229 | } catch( e ) { 230 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 231 | } 232 | } 233 | checkPollution(); 234 | }, 235 | finish: function() { 236 | config.current = this; 237 | if ( config.requireExpects && this.expected === null ) { 238 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 239 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 240 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 241 | } else if ( this.expected === null && !this.assertions.length ) { 242 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 243 | } 244 | 245 | var i, assertion, a, b, time, li, ol, 246 | test = this, 247 | good = 0, 248 | bad = 0, 249 | tests = id( "qunit-tests" ); 250 | 251 | this.runtime = +new Date() - this.started; 252 | config.stats.all += this.assertions.length; 253 | config.moduleStats.all += this.assertions.length; 254 | 255 | if ( tests ) { 256 | ol = document.createElement( "ol" ); 257 | ol.className = "qunit-assert-list"; 258 | 259 | for ( i = 0; i < this.assertions.length; i++ ) { 260 | assertion = this.assertions[i]; 261 | 262 | li = document.createElement( "li" ); 263 | li.className = assertion.result ? "pass" : "fail"; 264 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 265 | ol.appendChild( li ); 266 | 267 | if ( assertion.result ) { 268 | good++; 269 | } else { 270 | bad++; 271 | config.stats.bad++; 272 | config.moduleStats.bad++; 273 | } 274 | } 275 | 276 | // store result when possible 277 | if ( QUnit.config.reorder && defined.sessionStorage ) { 278 | if ( bad ) { 279 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 280 | } else { 281 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 282 | } 283 | } 284 | 285 | if ( bad === 0 ) { 286 | addClass( ol, "qunit-collapsed" ); 287 | } 288 | 289 | // `b` initialized at top of scope 290 | b = document.createElement( "strong" ); 291 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 292 | 293 | addEvent(b, "click", function() { 294 | var next = b.parentNode.lastChild, 295 | collapsed = hasClass( next, "qunit-collapsed" ); 296 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 297 | }); 298 | 299 | addEvent(b, "dblclick", function( e ) { 300 | var target = e && e.target ? e.target : window.event.srcElement; 301 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 302 | target = target.parentNode; 303 | } 304 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 305 | window.location = QUnit.url({ testNumber: test.testNumber }); 306 | } 307 | }); 308 | 309 | // `time` initialized at top of scope 310 | time = document.createElement( "span" ); 311 | time.className = "runtime"; 312 | time.innerHTML = this.runtime + " ms"; 313 | 314 | // `li` initialized at top of scope 315 | li = id( this.id ); 316 | li.className = bad ? "fail" : "pass"; 317 | li.removeChild( li.firstChild ); 318 | a = li.firstChild; 319 | li.appendChild( b ); 320 | li.appendChild( a ); 321 | li.appendChild( time ); 322 | li.appendChild( ol ); 323 | 324 | } else { 325 | for ( i = 0; i < this.assertions.length; i++ ) { 326 | if ( !this.assertions[i].result ) { 327 | bad++; 328 | config.stats.bad++; 329 | config.moduleStats.bad++; 330 | } 331 | } 332 | } 333 | 334 | runLoggingCallbacks( "testDone", QUnit, { 335 | name: this.testName, 336 | module: this.module, 337 | failed: bad, 338 | passed: this.assertions.length - bad, 339 | total: this.assertions.length, 340 | duration: this.runtime 341 | }); 342 | 343 | QUnit.reset(); 344 | 345 | config.current = undefined; 346 | }, 347 | 348 | queue: function() { 349 | var bad, 350 | test = this; 351 | 352 | synchronize(function() { 353 | test.init(); 354 | }); 355 | function run() { 356 | // each of these can by async 357 | synchronize(function() { 358 | test.setup(); 359 | }); 360 | synchronize(function() { 361 | test.run(); 362 | }); 363 | synchronize(function() { 364 | test.teardown(); 365 | }); 366 | synchronize(function() { 367 | test.finish(); 368 | }); 369 | } 370 | 371 | // `bad` initialized at top of scope 372 | // defer when previous test run passed, if storage is available 373 | bad = QUnit.config.reorder && defined.sessionStorage && 374 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 375 | 376 | if ( bad ) { 377 | run(); 378 | } else { 379 | synchronize( run, true ); 380 | } 381 | } 382 | }; 383 | 384 | // Root QUnit object. 385 | // `QUnit` initialized at top of scope 386 | QUnit = { 387 | 388 | // call on start of module test to prepend name to all tests 389 | module: function( name, testEnvironment ) { 390 | config.currentModule = name; 391 | config.currentModuleTestEnvironment = testEnvironment; 392 | config.modules[name] = true; 393 | }, 394 | 395 | asyncTest: function( testName, expected, callback ) { 396 | if ( arguments.length === 2 ) { 397 | callback = expected; 398 | expected = null; 399 | } 400 | 401 | QUnit.test( testName, expected, callback, true ); 402 | }, 403 | 404 | test: function( testName, expected, callback, async ) { 405 | var test, 406 | nameHtml = "" + escapeText( testName ) + ""; 407 | 408 | if ( arguments.length === 2 ) { 409 | callback = expected; 410 | expected = null; 411 | } 412 | 413 | if ( config.currentModule ) { 414 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 415 | } 416 | 417 | test = new Test({ 418 | nameHtml: nameHtml, 419 | testName: testName, 420 | expected: expected, 421 | async: async, 422 | callback: callback, 423 | module: config.currentModule, 424 | moduleTestEnvironment: config.currentModuleTestEnvironment, 425 | stack: sourceFromStacktrace( 2 ) 426 | }); 427 | 428 | if ( !validTest( test ) ) { 429 | return; 430 | } 431 | 432 | test.queue(); 433 | }, 434 | 435 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 436 | expect: function( asserts ) { 437 | if (arguments.length === 1) { 438 | config.current.expected = asserts; 439 | } else { 440 | return config.current.expected; 441 | } 442 | }, 443 | 444 | start: function( count ) { 445 | // QUnit hasn't been initialized yet. 446 | // Note: RequireJS (et al) may delay onLoad 447 | if ( config.semaphore === undefined ) { 448 | QUnit.begin(function() { 449 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 450 | setTimeout(function() { 451 | QUnit.start( count ); 452 | }); 453 | }); 454 | return; 455 | } 456 | 457 | config.semaphore -= count || 1; 458 | // don't start until equal number of stop-calls 459 | if ( config.semaphore > 0 ) { 460 | return; 461 | } 462 | // ignore if start is called more often then stop 463 | if ( config.semaphore < 0 ) { 464 | config.semaphore = 0; 465 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 466 | return; 467 | } 468 | // A slight delay, to avoid any current callbacks 469 | if ( defined.setTimeout ) { 470 | setTimeout(function() { 471 | if ( config.semaphore > 0 ) { 472 | return; 473 | } 474 | if ( config.timeout ) { 475 | clearTimeout( config.timeout ); 476 | } 477 | 478 | config.blocking = false; 479 | process( true ); 480 | }, 13); 481 | } else { 482 | config.blocking = false; 483 | process( true ); 484 | } 485 | }, 486 | 487 | stop: function( count ) { 488 | config.semaphore += count || 1; 489 | config.blocking = true; 490 | 491 | if ( config.testTimeout && defined.setTimeout ) { 492 | clearTimeout( config.timeout ); 493 | config.timeout = setTimeout(function() { 494 | QUnit.ok( false, "Test timed out" ); 495 | config.semaphore = 1; 496 | QUnit.start(); 497 | }, config.testTimeout ); 498 | } 499 | } 500 | }; 501 | 502 | // `assert` initialized at top of scope 503 | // Assert helpers 504 | // All of these must either call QUnit.push() or manually do: 505 | // - runLoggingCallbacks( "log", .. ); 506 | // - config.current.assertions.push({ .. }); 507 | // We attach it to the QUnit object *after* we expose the public API, 508 | // otherwise `assert` will become a global variable in browsers (#341). 509 | assert = { 510 | /** 511 | * Asserts rough true-ish result. 512 | * @name ok 513 | * @function 514 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 515 | */ 516 | ok: function( result, msg ) { 517 | if ( !config.current ) { 518 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 519 | } 520 | result = !!result; 521 | msg = msg || (result ? "okay" : "failed" ); 522 | 523 | var source, 524 | details = { 525 | module: config.current.module, 526 | name: config.current.testName, 527 | result: result, 528 | message: msg 529 | }; 530 | 531 | msg = "" + escapeText( msg ) + ""; 532 | 533 | if ( !result ) { 534 | source = sourceFromStacktrace( 2 ); 535 | if ( source ) { 536 | details.source = source; 537 | msg += "
Source:
" + escapeText( source ) + "
"; 538 | } 539 | } 540 | runLoggingCallbacks( "log", QUnit, details ); 541 | config.current.assertions.push({ 542 | result: result, 543 | message: msg 544 | }); 545 | }, 546 | 547 | /** 548 | * Assert that the first two arguments are equal, with an optional message. 549 | * Prints out both actual and expected values. 550 | * @name equal 551 | * @function 552 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 553 | */ 554 | equal: function( actual, expected, message ) { 555 | /*jshint eqeqeq:false */ 556 | QUnit.push( expected == actual, actual, expected, message ); 557 | }, 558 | 559 | /** 560 | * @name notEqual 561 | * @function 562 | */ 563 | notEqual: function( actual, expected, message ) { 564 | /*jshint eqeqeq:false */ 565 | QUnit.push( expected != actual, actual, expected, message ); 566 | }, 567 | 568 | /** 569 | * @name propEqual 570 | * @function 571 | */ 572 | propEqual: function( actual, expected, message ) { 573 | actual = objectValues(actual); 574 | expected = objectValues(expected); 575 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 576 | }, 577 | 578 | /** 579 | * @name notPropEqual 580 | * @function 581 | */ 582 | notPropEqual: function( actual, expected, message ) { 583 | actual = objectValues(actual); 584 | expected = objectValues(expected); 585 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 586 | }, 587 | 588 | /** 589 | * @name deepEqual 590 | * @function 591 | */ 592 | deepEqual: function( actual, expected, message ) { 593 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 594 | }, 595 | 596 | /** 597 | * @name notDeepEqual 598 | * @function 599 | */ 600 | notDeepEqual: function( actual, expected, message ) { 601 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 602 | }, 603 | 604 | /** 605 | * @name strictEqual 606 | * @function 607 | */ 608 | strictEqual: function( actual, expected, message ) { 609 | QUnit.push( expected === actual, actual, expected, message ); 610 | }, 611 | 612 | /** 613 | * @name notStrictEqual 614 | * @function 615 | */ 616 | notStrictEqual: function( actual, expected, message ) { 617 | QUnit.push( expected !== actual, actual, expected, message ); 618 | }, 619 | 620 | "throws": function( block, expected, message ) { 621 | var actual, 622 | expectedOutput = expected, 623 | ok = false; 624 | 625 | // 'expected' is optional 626 | if ( typeof expected === "string" ) { 627 | message = expected; 628 | expected = null; 629 | } 630 | 631 | config.current.ignoreGlobalErrors = true; 632 | try { 633 | block.call( config.current.testEnvironment ); 634 | } catch (e) { 635 | actual = e; 636 | } 637 | config.current.ignoreGlobalErrors = false; 638 | 639 | if ( actual ) { 640 | // we don't want to validate thrown error 641 | if ( !expected ) { 642 | ok = true; 643 | expectedOutput = null; 644 | // expected is a regexp 645 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 646 | ok = expected.test( errorString( actual ) ); 647 | // expected is a constructor 648 | } else if ( actual instanceof expected ) { 649 | ok = true; 650 | // expected is a validation function which returns true is validation passed 651 | } else if ( expected.call( {}, actual ) === true ) { 652 | expectedOutput = null; 653 | ok = true; 654 | } 655 | 656 | QUnit.push( ok, actual, expectedOutput, message ); 657 | } else { 658 | QUnit.pushFailure( message, null, "No exception was thrown." ); 659 | } 660 | } 661 | }; 662 | 663 | /** 664 | * @deprecated since 1.8.0 665 | * Kept assertion helpers in root for backwards compatibility. 666 | */ 667 | extend( QUnit, assert ); 668 | 669 | /** 670 | * @deprecated since 1.9.0 671 | * Kept root "raises()" for backwards compatibility. 672 | * (Note that we don't introduce assert.raises). 673 | */ 674 | QUnit.raises = assert[ "throws" ]; 675 | 676 | /** 677 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 678 | * Kept to avoid TypeErrors for undefined methods. 679 | */ 680 | QUnit.equals = function() { 681 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 682 | }; 683 | QUnit.same = function() { 684 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 685 | }; 686 | 687 | // We want access to the constructor's prototype 688 | (function() { 689 | function F() {} 690 | F.prototype = QUnit; 691 | QUnit = new F(); 692 | // Make F QUnit's constructor so that we can add to the prototype later 693 | QUnit.constructor = F; 694 | }()); 695 | 696 | /** 697 | * Config object: Maintain internal state 698 | * Later exposed as QUnit.config 699 | * `config` initialized at top of scope 700 | */ 701 | config = { 702 | // The queue of tests to run 703 | queue: [], 704 | 705 | // block until document ready 706 | blocking: true, 707 | 708 | // when enabled, show only failing tests 709 | // gets persisted through sessionStorage and can be changed in UI via checkbox 710 | hidepassed: false, 711 | 712 | // by default, run previously failed tests first 713 | // very useful in combination with "Hide passed tests" checked 714 | reorder: true, 715 | 716 | // by default, modify document.title when suite is done 717 | altertitle: true, 718 | 719 | // when enabled, all tests must call expect() 720 | requireExpects: false, 721 | 722 | // add checkboxes that are persisted in the query-string 723 | // when enabled, the id is set to `true` as a `QUnit.config` property 724 | urlConfig: [ 725 | { 726 | id: "noglobals", 727 | label: "Check for Globals", 728 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 729 | }, 730 | { 731 | id: "notrycatch", 732 | label: "No try-catch", 733 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 734 | } 735 | ], 736 | 737 | // Set of all modules. 738 | modules: {}, 739 | 740 | // logging callback queues 741 | begin: [], 742 | done: [], 743 | log: [], 744 | testStart: [], 745 | testDone: [], 746 | moduleStart: [], 747 | moduleDone: [] 748 | }; 749 | 750 | // Export global variables, unless an 'exports' object exists, 751 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 752 | if ( typeof exports === "undefined" ) { 753 | extend( window, QUnit.constructor.prototype ); 754 | 755 | // Expose QUnit object 756 | window.QUnit = QUnit; 757 | } 758 | 759 | // Initialize more QUnit.config and QUnit.urlParams 760 | (function() { 761 | var i, 762 | location = window.location || { search: "", protocol: "file:" }, 763 | params = location.search.slice( 1 ).split( "&" ), 764 | length = params.length, 765 | urlParams = {}, 766 | current; 767 | 768 | if ( params[ 0 ] ) { 769 | for ( i = 0; i < length; i++ ) { 770 | current = params[ i ].split( "=" ); 771 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 772 | // allow just a key to turn on a flag, e.g., test.html?noglobals 773 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 774 | urlParams[ current[ 0 ] ] = current[ 1 ]; 775 | } 776 | } 777 | 778 | QUnit.urlParams = urlParams; 779 | 780 | // String search anywhere in moduleName+testName 781 | config.filter = urlParams.filter; 782 | 783 | // Exact match of the module name 784 | config.module = urlParams.module; 785 | 786 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 787 | 788 | // Figure out if we're running the tests from a server or not 789 | QUnit.isLocal = location.protocol === "file:"; 790 | }()); 791 | 792 | // Extend QUnit object, 793 | // these after set here because they should not be exposed as global functions 794 | extend( QUnit, { 795 | assert: assert, 796 | 797 | config: config, 798 | 799 | // Initialize the configuration options 800 | init: function() { 801 | extend( config, { 802 | stats: { all: 0, bad: 0 }, 803 | moduleStats: { all: 0, bad: 0 }, 804 | started: +new Date(), 805 | updateRate: 1000, 806 | blocking: false, 807 | autostart: true, 808 | autorun: false, 809 | filter: "", 810 | queue: [], 811 | semaphore: 1 812 | }); 813 | 814 | var tests, banner, result, 815 | qunit = id( "qunit" ); 816 | 817 | if ( qunit ) { 818 | qunit.innerHTML = 819 | "

" + escapeText( document.title ) + "

" + 820 | "

" + 821 | "
" + 822 | "

" + 823 | "
    "; 824 | } 825 | 826 | tests = id( "qunit-tests" ); 827 | banner = id( "qunit-banner" ); 828 | result = id( "qunit-testresult" ); 829 | 830 | if ( tests ) { 831 | tests.innerHTML = ""; 832 | } 833 | 834 | if ( banner ) { 835 | banner.className = ""; 836 | } 837 | 838 | if ( result ) { 839 | result.parentNode.removeChild( result ); 840 | } 841 | 842 | if ( tests ) { 843 | result = document.createElement( "p" ); 844 | result.id = "qunit-testresult"; 845 | result.className = "result"; 846 | tests.parentNode.insertBefore( result, tests ); 847 | result.innerHTML = "Running...
     "; 848 | } 849 | }, 850 | 851 | // Resets the test setup. Useful for tests that modify the DOM. 852 | /* 853 | DEPRECATED: Use multiple tests instead of resetting inside a test. 854 | Use testStart or testDone for custom cleanup. 855 | This method will throw an error in 2.0, and will be removed in 2.1 856 | */ 857 | reset: function() { 858 | var fixture = id( "qunit-fixture" ); 859 | if ( fixture ) { 860 | fixture.innerHTML = config.fixture; 861 | } 862 | }, 863 | 864 | // Trigger an event on an element. 865 | // @example triggerEvent( document.body, "click" ); 866 | triggerEvent: function( elem, type, event ) { 867 | if ( document.createEvent ) { 868 | event = document.createEvent( "MouseEvents" ); 869 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 870 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 871 | 872 | elem.dispatchEvent( event ); 873 | } else if ( elem.fireEvent ) { 874 | elem.fireEvent( "on" + type ); 875 | } 876 | }, 877 | 878 | // Safe object type checking 879 | is: function( type, obj ) { 880 | return QUnit.objectType( obj ) === type; 881 | }, 882 | 883 | objectType: function( obj ) { 884 | if ( typeof obj === "undefined" ) { 885 | return "undefined"; 886 | // consider: typeof null === object 887 | } 888 | if ( obj === null ) { 889 | return "null"; 890 | } 891 | 892 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 893 | type = match && match[1] || ""; 894 | 895 | switch ( type ) { 896 | case "Number": 897 | if ( isNaN(obj) ) { 898 | return "nan"; 899 | } 900 | return "number"; 901 | case "String": 902 | case "Boolean": 903 | case "Array": 904 | case "Date": 905 | case "RegExp": 906 | case "Function": 907 | return type.toLowerCase(); 908 | } 909 | if ( typeof obj === "object" ) { 910 | return "object"; 911 | } 912 | return undefined; 913 | }, 914 | 915 | push: function( result, actual, expected, message ) { 916 | if ( !config.current ) { 917 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 918 | } 919 | 920 | var output, source, 921 | details = { 922 | module: config.current.module, 923 | name: config.current.testName, 924 | result: result, 925 | message: message, 926 | actual: actual, 927 | expected: expected 928 | }; 929 | 930 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 931 | message = "" + message + ""; 932 | output = message; 933 | 934 | if ( !result ) { 935 | expected = escapeText( QUnit.jsDump.parse(expected) ); 936 | actual = escapeText( QUnit.jsDump.parse(actual) ); 937 | output += ""; 938 | 939 | if ( actual !== expected ) { 940 | output += ""; 941 | output += ""; 942 | } 943 | 944 | source = sourceFromStacktrace(); 945 | 946 | if ( source ) { 947 | details.source = source; 948 | output += ""; 949 | } 950 | 951 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 952 | } 953 | 954 | runLoggingCallbacks( "log", QUnit, details ); 955 | 956 | config.current.assertions.push({ 957 | result: !!result, 958 | message: output 959 | }); 960 | }, 961 | 962 | pushFailure: function( message, source, actual ) { 963 | if ( !config.current ) { 964 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 965 | } 966 | 967 | var output, 968 | details = { 969 | module: config.current.module, 970 | name: config.current.testName, 971 | result: false, 972 | message: message 973 | }; 974 | 975 | message = escapeText( message ) || "error"; 976 | message = "" + message + ""; 977 | output = message; 978 | 979 | output += ""; 980 | 981 | if ( actual ) { 982 | output += ""; 983 | } 984 | 985 | if ( source ) { 986 | details.source = source; 987 | output += ""; 988 | } 989 | 990 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 991 | 992 | runLoggingCallbacks( "log", QUnit, details ); 993 | 994 | config.current.assertions.push({ 995 | result: false, 996 | message: output 997 | }); 998 | }, 999 | 1000 | url: function( params ) { 1001 | params = extend( extend( {}, QUnit.urlParams ), params ); 1002 | var key, 1003 | querystring = "?"; 1004 | 1005 | for ( key in params ) { 1006 | if ( hasOwn.call( params, key ) ) { 1007 | querystring += encodeURIComponent( key ) + "=" + 1008 | encodeURIComponent( params[ key ] ) + "&"; 1009 | } 1010 | } 1011 | return window.location.protocol + "//" + window.location.host + 1012 | window.location.pathname + querystring.slice( 0, -1 ); 1013 | }, 1014 | 1015 | extend: extend, 1016 | id: id, 1017 | addEvent: addEvent, 1018 | addClass: addClass, 1019 | hasClass: hasClass, 1020 | removeClass: removeClass 1021 | // load, equiv, jsDump, diff: Attached later 1022 | }); 1023 | 1024 | /** 1025 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1026 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1027 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1028 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1029 | * QUnit object. 1030 | */ 1031 | extend( QUnit.constructor.prototype, { 1032 | 1033 | // Logging callbacks; all receive a single argument with the listed properties 1034 | // run test/logs.html for any related changes 1035 | begin: registerLoggingCallback( "begin" ), 1036 | 1037 | // done: { failed, passed, total, runtime } 1038 | done: registerLoggingCallback( "done" ), 1039 | 1040 | // log: { result, actual, expected, message } 1041 | log: registerLoggingCallback( "log" ), 1042 | 1043 | // testStart: { name } 1044 | testStart: registerLoggingCallback( "testStart" ), 1045 | 1046 | // testDone: { name, failed, passed, total, duration } 1047 | testDone: registerLoggingCallback( "testDone" ), 1048 | 1049 | // moduleStart: { name } 1050 | moduleStart: registerLoggingCallback( "moduleStart" ), 1051 | 1052 | // moduleDone: { name, failed, passed, total } 1053 | moduleDone: registerLoggingCallback( "moduleDone" ) 1054 | }); 1055 | 1056 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1057 | config.autorun = true; 1058 | } 1059 | 1060 | QUnit.load = function() { 1061 | runLoggingCallbacks( "begin", QUnit, {} ); 1062 | 1063 | // Initialize the config, saving the execution queue 1064 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1065 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1066 | numModules = 0, 1067 | moduleNames = [], 1068 | moduleFilterHtml = "", 1069 | urlConfigHtml = "", 1070 | oldconfig = extend( {}, config ); 1071 | 1072 | QUnit.init(); 1073 | extend(config, oldconfig); 1074 | 1075 | config.blocking = false; 1076 | 1077 | len = config.urlConfig.length; 1078 | 1079 | for ( i = 0; i < len; i++ ) { 1080 | val = config.urlConfig[i]; 1081 | if ( typeof val === "string" ) { 1082 | val = { 1083 | id: val, 1084 | label: val, 1085 | tooltip: "[no tooltip available]" 1086 | }; 1087 | } 1088 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1089 | urlConfigHtml += ""; 1095 | } 1096 | for ( i in config.modules ) { 1097 | if ( config.modules.hasOwnProperty( i ) ) { 1098 | moduleNames.push(i); 1099 | } 1100 | } 1101 | numModules = moduleNames.length; 1102 | moduleNames.sort( function( a, b ) { 1103 | return a.localeCompare( b ); 1104 | }); 1105 | moduleFilterHtml += ""; 1116 | 1117 | // `userAgent` initialized at top of scope 1118 | userAgent = id( "qunit-userAgent" ); 1119 | if ( userAgent ) { 1120 | userAgent.innerHTML = navigator.userAgent; 1121 | } 1122 | 1123 | // `banner` initialized at top of scope 1124 | banner = id( "qunit-header" ); 1125 | if ( banner ) { 1126 | banner.innerHTML = "" + banner.innerHTML + " "; 1127 | } 1128 | 1129 | // `toolbar` initialized at top of scope 1130 | toolbar = id( "qunit-testrunner-toolbar" ); 1131 | if ( toolbar ) { 1132 | // `filter` initialized at top of scope 1133 | filter = document.createElement( "input" ); 1134 | filter.type = "checkbox"; 1135 | filter.id = "qunit-filter-pass"; 1136 | 1137 | addEvent( filter, "click", function() { 1138 | var tmp, 1139 | ol = document.getElementById( "qunit-tests" ); 1140 | 1141 | if ( filter.checked ) { 1142 | ol.className = ol.className + " hidepass"; 1143 | } else { 1144 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1145 | ol.className = tmp.replace( / hidepass /, " " ); 1146 | } 1147 | if ( defined.sessionStorage ) { 1148 | if (filter.checked) { 1149 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1150 | } else { 1151 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1152 | } 1153 | } 1154 | }); 1155 | 1156 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1157 | filter.checked = true; 1158 | // `ol` initialized at top of scope 1159 | ol = document.getElementById( "qunit-tests" ); 1160 | ol.className = ol.className + " hidepass"; 1161 | } 1162 | toolbar.appendChild( filter ); 1163 | 1164 | // `label` initialized at top of scope 1165 | label = document.createElement( "label" ); 1166 | label.setAttribute( "for", "qunit-filter-pass" ); 1167 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); 1168 | label.innerHTML = "Hide passed tests"; 1169 | toolbar.appendChild( label ); 1170 | 1171 | urlConfigCheckboxesContainer = document.createElement("span"); 1172 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1173 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1174 | // For oldIE support: 1175 | // * Add handlers to the individual elements instead of the container 1176 | // * Use "click" instead of "change" 1177 | // * Fallback from event.target to event.srcElement 1178 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1179 | var params = {}, 1180 | target = event.target || event.srcElement; 1181 | params[ target.name ] = target.checked ? true : undefined; 1182 | window.location = QUnit.url( params ); 1183 | }); 1184 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1185 | 1186 | if (numModules > 1) { 1187 | moduleFilter = document.createElement( "span" ); 1188 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 1189 | moduleFilter.innerHTML = moduleFilterHtml; 1190 | addEvent( moduleFilter.lastChild, "change", function() { 1191 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1192 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1193 | 1194 | window.location = QUnit.url({ 1195 | module: ( selectedModule === "" ) ? undefined : selectedModule, 1196 | // Remove any existing filters 1197 | filter: undefined, 1198 | testNumber: undefined 1199 | }); 1200 | }); 1201 | toolbar.appendChild(moduleFilter); 1202 | } 1203 | } 1204 | 1205 | // `main` initialized at top of scope 1206 | main = id( "qunit-fixture" ); 1207 | if ( main ) { 1208 | config.fixture = main.innerHTML; 1209 | } 1210 | 1211 | if ( config.autostart ) { 1212 | QUnit.start(); 1213 | } 1214 | }; 1215 | 1216 | addEvent( window, "load", QUnit.load ); 1217 | 1218 | // `onErrorFnPrev` initialized at top of scope 1219 | // Preserve other handlers 1220 | onErrorFnPrev = window.onerror; 1221 | 1222 | // Cover uncaught exceptions 1223 | // Returning true will suppress the default browser handler, 1224 | // returning false will let it run. 1225 | window.onerror = function ( error, filePath, linerNr ) { 1226 | var ret = false; 1227 | if ( onErrorFnPrev ) { 1228 | ret = onErrorFnPrev( error, filePath, linerNr ); 1229 | } 1230 | 1231 | // Treat return value as window.onerror itself does, 1232 | // Only do our handling if not suppressed. 1233 | if ( ret !== true ) { 1234 | if ( QUnit.config.current ) { 1235 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1236 | return true; 1237 | } 1238 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1239 | } else { 1240 | QUnit.test( "global failure", extend( function() { 1241 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1242 | }, { validTest: validTest } ) ); 1243 | } 1244 | return false; 1245 | } 1246 | 1247 | return ret; 1248 | }; 1249 | 1250 | function done() { 1251 | config.autorun = true; 1252 | 1253 | // Log the last module results 1254 | if ( config.currentModule ) { 1255 | runLoggingCallbacks( "moduleDone", QUnit, { 1256 | name: config.currentModule, 1257 | failed: config.moduleStats.bad, 1258 | passed: config.moduleStats.all - config.moduleStats.bad, 1259 | total: config.moduleStats.all 1260 | }); 1261 | } 1262 | delete config.previousModule; 1263 | 1264 | var i, key, 1265 | banner = id( "qunit-banner" ), 1266 | tests = id( "qunit-tests" ), 1267 | runtime = +new Date() - config.started, 1268 | passed = config.stats.all - config.stats.bad, 1269 | html = [ 1270 | "Tests completed in ", 1271 | runtime, 1272 | " milliseconds.
    ", 1273 | "", 1274 | passed, 1275 | " assertions of ", 1276 | config.stats.all, 1277 | " passed, ", 1278 | config.stats.bad, 1279 | " failed." 1280 | ].join( "" ); 1281 | 1282 | if ( banner ) { 1283 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1284 | } 1285 | 1286 | if ( tests ) { 1287 | id( "qunit-testresult" ).innerHTML = html; 1288 | } 1289 | 1290 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1291 | // show ✖ for good, ✔ for bad suite result in title 1292 | // use escape sequences in case file gets loaded with non-utf-8-charset 1293 | document.title = [ 1294 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1295 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1296 | ].join( " " ); 1297 | } 1298 | 1299 | // clear own sessionStorage items if all tests passed 1300 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1301 | // `key` & `i` initialized at top of scope 1302 | for ( i = 0; i < sessionStorage.length; i++ ) { 1303 | key = sessionStorage.key( i++ ); 1304 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1305 | sessionStorage.removeItem( key ); 1306 | } 1307 | } 1308 | } 1309 | 1310 | // scroll back to top to show results 1311 | if ( window.scrollTo ) { 1312 | window.scrollTo(0, 0); 1313 | } 1314 | 1315 | runLoggingCallbacks( "done", QUnit, { 1316 | failed: config.stats.bad, 1317 | passed: passed, 1318 | total: config.stats.all, 1319 | runtime: runtime 1320 | }); 1321 | } 1322 | 1323 | /** @return Boolean: true if this test should be ran */ 1324 | function validTest( test ) { 1325 | var include, 1326 | filter = config.filter && config.filter.toLowerCase(), 1327 | module = config.module && config.module.toLowerCase(), 1328 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1329 | 1330 | // Internally-generated tests are always valid 1331 | if ( test.callback && test.callback.validTest === validTest ) { 1332 | delete test.callback.validTest; 1333 | return true; 1334 | } 1335 | 1336 | if ( config.testNumber ) { 1337 | return test.testNumber === config.testNumber; 1338 | } 1339 | 1340 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1341 | return false; 1342 | } 1343 | 1344 | if ( !filter ) { 1345 | return true; 1346 | } 1347 | 1348 | include = filter.charAt( 0 ) !== "!"; 1349 | if ( !include ) { 1350 | filter = filter.slice( 1 ); 1351 | } 1352 | 1353 | // If the filter matches, we need to honour include 1354 | if ( fullName.indexOf( filter ) !== -1 ) { 1355 | return include; 1356 | } 1357 | 1358 | // Otherwise, do the opposite 1359 | return !include; 1360 | } 1361 | 1362 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1363 | // Later Safari and IE10 are supposed to support error.stack as well 1364 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1365 | function extractStacktrace( e, offset ) { 1366 | offset = offset === undefined ? 3 : offset; 1367 | 1368 | var stack, include, i; 1369 | 1370 | if ( e.stacktrace ) { 1371 | // Opera 1372 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1373 | } else if ( e.stack ) { 1374 | // Firefox, Chrome 1375 | stack = e.stack.split( "\n" ); 1376 | if (/^error$/i.test( stack[0] ) ) { 1377 | stack.shift(); 1378 | } 1379 | if ( fileName ) { 1380 | include = []; 1381 | for ( i = offset; i < stack.length; i++ ) { 1382 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1383 | break; 1384 | } 1385 | include.push( stack[ i ] ); 1386 | } 1387 | if ( include.length ) { 1388 | return include.join( "\n" ); 1389 | } 1390 | } 1391 | return stack[ offset ]; 1392 | } else if ( e.sourceURL ) { 1393 | // Safari, PhantomJS 1394 | // hopefully one day Safari provides actual stacktraces 1395 | // exclude useless self-reference for generated Error objects 1396 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1397 | return; 1398 | } 1399 | // for actual exceptions, this is useful 1400 | return e.sourceURL + ":" + e.line; 1401 | } 1402 | } 1403 | function sourceFromStacktrace( offset ) { 1404 | try { 1405 | throw new Error(); 1406 | } catch ( e ) { 1407 | return extractStacktrace( e, offset ); 1408 | } 1409 | } 1410 | 1411 | /** 1412 | * Escape text for attribute or text content. 1413 | */ 1414 | function escapeText( s ) { 1415 | if ( !s ) { 1416 | return ""; 1417 | } 1418 | s = s + ""; 1419 | // Both single quotes and double quotes (for attributes) 1420 | return s.replace( /['"<>&]/g, function( s ) { 1421 | switch( s ) { 1422 | case "'": 1423 | return "'"; 1424 | case "\"": 1425 | return """; 1426 | case "<": 1427 | return "<"; 1428 | case ">": 1429 | return ">"; 1430 | case "&": 1431 | return "&"; 1432 | } 1433 | }); 1434 | } 1435 | 1436 | function synchronize( callback, last ) { 1437 | config.queue.push( callback ); 1438 | 1439 | if ( config.autorun && !config.blocking ) { 1440 | process( last ); 1441 | } 1442 | } 1443 | 1444 | function process( last ) { 1445 | function next() { 1446 | process( last ); 1447 | } 1448 | var start = new Date().getTime(); 1449 | config.depth = config.depth ? config.depth + 1 : 1; 1450 | 1451 | while ( config.queue.length && !config.blocking ) { 1452 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1453 | config.queue.shift()(); 1454 | } else { 1455 | setTimeout( next, 13 ); 1456 | break; 1457 | } 1458 | } 1459 | config.depth--; 1460 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1461 | done(); 1462 | } 1463 | } 1464 | 1465 | function saveGlobal() { 1466 | config.pollution = []; 1467 | 1468 | if ( config.noglobals ) { 1469 | for ( var key in window ) { 1470 | if ( hasOwn.call( window, key ) ) { 1471 | // in Opera sometimes DOM element ids show up here, ignore them 1472 | if ( /^qunit-test-output/.test( key ) ) { 1473 | continue; 1474 | } 1475 | config.pollution.push( key ); 1476 | } 1477 | } 1478 | } 1479 | } 1480 | 1481 | function checkPollution() { 1482 | var newGlobals, 1483 | deletedGlobals, 1484 | old = config.pollution; 1485 | 1486 | saveGlobal(); 1487 | 1488 | newGlobals = diff( config.pollution, old ); 1489 | if ( newGlobals.length > 0 ) { 1490 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1491 | } 1492 | 1493 | deletedGlobals = diff( old, config.pollution ); 1494 | if ( deletedGlobals.length > 0 ) { 1495 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1496 | } 1497 | } 1498 | 1499 | // returns a new Array with the elements that are in a but not in b 1500 | function diff( a, b ) { 1501 | var i, j, 1502 | result = a.slice(); 1503 | 1504 | for ( i = 0; i < result.length; i++ ) { 1505 | for ( j = 0; j < b.length; j++ ) { 1506 | if ( result[i] === b[j] ) { 1507 | result.splice( i, 1 ); 1508 | i--; 1509 | break; 1510 | } 1511 | } 1512 | } 1513 | return result; 1514 | } 1515 | 1516 | function extend( a, b ) { 1517 | for ( var prop in b ) { 1518 | if ( hasOwn.call( b, prop ) ) { 1519 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 1520 | if ( !( prop === "constructor" && a === window ) ) { 1521 | if ( b[ prop ] === undefined ) { 1522 | delete a[ prop ]; 1523 | } else { 1524 | a[ prop ] = b[ prop ]; 1525 | } 1526 | } 1527 | } 1528 | } 1529 | 1530 | return a; 1531 | } 1532 | 1533 | /** 1534 | * @param {HTMLElement} elem 1535 | * @param {string} type 1536 | * @param {Function} fn 1537 | */ 1538 | function addEvent( elem, type, fn ) { 1539 | // Standards-based browsers 1540 | if ( elem.addEventListener ) { 1541 | elem.addEventListener( type, fn, false ); 1542 | // IE 1543 | } else { 1544 | elem.attachEvent( "on" + type, fn ); 1545 | } 1546 | } 1547 | 1548 | /** 1549 | * @param {Array|NodeList} elems 1550 | * @param {string} type 1551 | * @param {Function} fn 1552 | */ 1553 | function addEvents( elems, type, fn ) { 1554 | var i = elems.length; 1555 | while ( i-- ) { 1556 | addEvent( elems[i], type, fn ); 1557 | } 1558 | } 1559 | 1560 | function hasClass( elem, name ) { 1561 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1562 | } 1563 | 1564 | function addClass( elem, name ) { 1565 | if ( !hasClass( elem, name ) ) { 1566 | elem.className += (elem.className ? " " : "") + name; 1567 | } 1568 | } 1569 | 1570 | function removeClass( elem, name ) { 1571 | var set = " " + elem.className + " "; 1572 | // Class name may appear multiple times 1573 | while ( set.indexOf(" " + name + " ") > -1 ) { 1574 | set = set.replace(" " + name + " " , " "); 1575 | } 1576 | // If possible, trim it for prettiness, but not necessarily 1577 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); 1578 | } 1579 | 1580 | function id( name ) { 1581 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1582 | document.getElementById( name ); 1583 | } 1584 | 1585 | function registerLoggingCallback( key ) { 1586 | return function( callback ) { 1587 | config[key].push( callback ); 1588 | }; 1589 | } 1590 | 1591 | // Supports deprecated method of completely overwriting logging callbacks 1592 | function runLoggingCallbacks( key, scope, args ) { 1593 | var i, callbacks; 1594 | if ( QUnit.hasOwnProperty( key ) ) { 1595 | QUnit[ key ].call(scope, args ); 1596 | } else { 1597 | callbacks = config[ key ]; 1598 | for ( i = 0; i < callbacks.length; i++ ) { 1599 | callbacks[ i ].call( scope, args ); 1600 | } 1601 | } 1602 | } 1603 | 1604 | // Test for equality any JavaScript type. 1605 | // Author: Philippe Rathé 1606 | QUnit.equiv = (function() { 1607 | 1608 | // Call the o related callback with the given arguments. 1609 | function bindCallbacks( o, callbacks, args ) { 1610 | var prop = QUnit.objectType( o ); 1611 | if ( prop ) { 1612 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1613 | return callbacks[ prop ].apply( callbacks, args ); 1614 | } else { 1615 | return callbacks[ prop ]; // or undefined 1616 | } 1617 | } 1618 | } 1619 | 1620 | // the real equiv function 1621 | var innerEquiv, 1622 | // stack to decide between skip/abort functions 1623 | callers = [], 1624 | // stack to avoiding loops from circular referencing 1625 | parents = [], 1626 | parentsB = [], 1627 | 1628 | getProto = Object.getPrototypeOf || function ( obj ) { 1629 | /*jshint camelcase:false */ 1630 | return obj.__proto__; 1631 | }, 1632 | callbacks = (function () { 1633 | 1634 | // for string, boolean, number and null 1635 | function useStrictEquality( b, a ) { 1636 | /*jshint eqeqeq:false */ 1637 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1638 | // to catch short annotation VS 'new' annotation of a 1639 | // declaration 1640 | // e.g. var i = 1; 1641 | // var j = new Number(1); 1642 | return a == b; 1643 | } else { 1644 | return a === b; 1645 | } 1646 | } 1647 | 1648 | return { 1649 | "string": useStrictEquality, 1650 | "boolean": useStrictEquality, 1651 | "number": useStrictEquality, 1652 | "null": useStrictEquality, 1653 | "undefined": useStrictEquality, 1654 | 1655 | "nan": function( b ) { 1656 | return isNaN( b ); 1657 | }, 1658 | 1659 | "date": function( b, a ) { 1660 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1661 | }, 1662 | 1663 | "regexp": function( b, a ) { 1664 | return QUnit.objectType( b ) === "regexp" && 1665 | // the regex itself 1666 | a.source === b.source && 1667 | // and its modifiers 1668 | a.global === b.global && 1669 | // (gmi) ... 1670 | a.ignoreCase === b.ignoreCase && 1671 | a.multiline === b.multiline && 1672 | a.sticky === b.sticky; 1673 | }, 1674 | 1675 | // - skip when the property is a method of an instance (OOP) 1676 | // - abort otherwise, 1677 | // initial === would have catch identical references anyway 1678 | "function": function() { 1679 | var caller = callers[callers.length - 1]; 1680 | return caller !== Object && typeof caller !== "undefined"; 1681 | }, 1682 | 1683 | "array": function( b, a ) { 1684 | var i, j, len, loop, aCircular, bCircular; 1685 | 1686 | // b could be an object literal here 1687 | if ( QUnit.objectType( b ) !== "array" ) { 1688 | return false; 1689 | } 1690 | 1691 | len = a.length; 1692 | if ( len !== b.length ) { 1693 | // safe and faster 1694 | return false; 1695 | } 1696 | 1697 | // track reference to avoid circular references 1698 | parents.push( a ); 1699 | parentsB.push( b ); 1700 | for ( i = 0; i < len; i++ ) { 1701 | loop = false; 1702 | for ( j = 0; j < parents.length; j++ ) { 1703 | aCircular = parents[j] === a[i]; 1704 | bCircular = parentsB[j] === b[i]; 1705 | if ( aCircular || bCircular ) { 1706 | if ( a[i] === b[i] || aCircular && bCircular ) { 1707 | loop = true; 1708 | } else { 1709 | parents.pop(); 1710 | parentsB.pop(); 1711 | return false; 1712 | } 1713 | } 1714 | } 1715 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1716 | parents.pop(); 1717 | parentsB.pop(); 1718 | return false; 1719 | } 1720 | } 1721 | parents.pop(); 1722 | parentsB.pop(); 1723 | return true; 1724 | }, 1725 | 1726 | "object": function( b, a ) { 1727 | /*jshint forin:false */ 1728 | var i, j, loop, aCircular, bCircular, 1729 | // Default to true 1730 | eq = true, 1731 | aProperties = [], 1732 | bProperties = []; 1733 | 1734 | // comparing constructors is more strict than using 1735 | // instanceof 1736 | if ( a.constructor !== b.constructor ) { 1737 | // Allow objects with no prototype to be equivalent to 1738 | // objects with Object as their constructor. 1739 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1740 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1741 | return false; 1742 | } 1743 | } 1744 | 1745 | // stack constructor before traversing properties 1746 | callers.push( a.constructor ); 1747 | 1748 | // track reference to avoid circular references 1749 | parents.push( a ); 1750 | parentsB.push( b ); 1751 | 1752 | // be strict: don't ensure hasOwnProperty and go deep 1753 | for ( i in a ) { 1754 | loop = false; 1755 | for ( j = 0; j < parents.length; j++ ) { 1756 | aCircular = parents[j] === a[i]; 1757 | bCircular = parentsB[j] === b[i]; 1758 | if ( aCircular || bCircular ) { 1759 | if ( a[i] === b[i] || aCircular && bCircular ) { 1760 | loop = true; 1761 | } else { 1762 | eq = false; 1763 | break; 1764 | } 1765 | } 1766 | } 1767 | aProperties.push(i); 1768 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1769 | eq = false; 1770 | break; 1771 | } 1772 | } 1773 | 1774 | parents.pop(); 1775 | parentsB.pop(); 1776 | callers.pop(); // unstack, we are done 1777 | 1778 | for ( i in b ) { 1779 | bProperties.push( i ); // collect b's properties 1780 | } 1781 | 1782 | // Ensures identical properties name 1783 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1784 | } 1785 | }; 1786 | }()); 1787 | 1788 | innerEquiv = function() { // can take multiple arguments 1789 | var args = [].slice.apply( arguments ); 1790 | if ( args.length < 2 ) { 1791 | return true; // end transition 1792 | } 1793 | 1794 | return (function( a, b ) { 1795 | if ( a === b ) { 1796 | return true; // catch the most you can 1797 | } else if ( a === null || b === null || typeof a === "undefined" || 1798 | typeof b === "undefined" || 1799 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1800 | return false; // don't lose time with error prone cases 1801 | } else { 1802 | return bindCallbacks(a, callbacks, [ b, a ]); 1803 | } 1804 | 1805 | // apply transition with (1..n) arguments 1806 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); 1807 | }; 1808 | 1809 | return innerEquiv; 1810 | }()); 1811 | 1812 | /** 1813 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1814 | * http://flesler.blogspot.com Licensed under BSD 1815 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1816 | * 1817 | * @projectDescription Advanced and extensible data dumping for Javascript. 1818 | * @version 1.0.0 1819 | * @author Ariel Flesler 1820 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1821 | */ 1822 | QUnit.jsDump = (function() { 1823 | function quote( str ) { 1824 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1825 | } 1826 | function literal( o ) { 1827 | return o + ""; 1828 | } 1829 | function join( pre, arr, post ) { 1830 | var s = jsDump.separator(), 1831 | base = jsDump.indent(), 1832 | inner = jsDump.indent(1); 1833 | if ( arr.join ) { 1834 | arr = arr.join( "," + s + inner ); 1835 | } 1836 | if ( !arr ) { 1837 | return pre + post; 1838 | } 1839 | return [ pre, inner + arr, base + post ].join(s); 1840 | } 1841 | function array( arr, stack ) { 1842 | var i = arr.length, ret = new Array(i); 1843 | this.up(); 1844 | while ( i-- ) { 1845 | ret[i] = this.parse( arr[i] , undefined , stack); 1846 | } 1847 | this.down(); 1848 | return join( "[", ret, "]" ); 1849 | } 1850 | 1851 | var reName = /^function (\w+)/, 1852 | jsDump = { 1853 | // type is used mostly internally, you can fix a (custom)type in advance 1854 | parse: function( obj, type, stack ) { 1855 | stack = stack || [ ]; 1856 | var inStack, res, 1857 | parser = this.parsers[ type || this.typeOf(obj) ]; 1858 | 1859 | type = typeof parser; 1860 | inStack = inArray( obj, stack ); 1861 | 1862 | if ( inStack !== -1 ) { 1863 | return "recursion(" + (inStack - stack.length) + ")"; 1864 | } 1865 | if ( type === "function" ) { 1866 | stack.push( obj ); 1867 | res = parser.call( this, obj, stack ); 1868 | stack.pop(); 1869 | return res; 1870 | } 1871 | return ( type === "string" ) ? parser : this.parsers.error; 1872 | }, 1873 | typeOf: function( obj ) { 1874 | var type; 1875 | if ( obj === null ) { 1876 | type = "null"; 1877 | } else if ( typeof obj === "undefined" ) { 1878 | type = "undefined"; 1879 | } else if ( QUnit.is( "regexp", obj) ) { 1880 | type = "regexp"; 1881 | } else if ( QUnit.is( "date", obj) ) { 1882 | type = "date"; 1883 | } else if ( QUnit.is( "function", obj) ) { 1884 | type = "function"; 1885 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1886 | type = "window"; 1887 | } else if ( obj.nodeType === 9 ) { 1888 | type = "document"; 1889 | } else if ( obj.nodeType ) { 1890 | type = "node"; 1891 | } else if ( 1892 | // native arrays 1893 | toString.call( obj ) === "[object Array]" || 1894 | // NodeList objects 1895 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1896 | ) { 1897 | type = "array"; 1898 | } else if ( obj.constructor === Error.prototype.constructor ) { 1899 | type = "error"; 1900 | } else { 1901 | type = typeof obj; 1902 | } 1903 | return type; 1904 | }, 1905 | separator: function() { 1906 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1907 | }, 1908 | // extra can be a number, shortcut for increasing-calling-decreasing 1909 | indent: function( extra ) { 1910 | if ( !this.multiline ) { 1911 | return ""; 1912 | } 1913 | var chr = this.indentChar; 1914 | if ( this.HTML ) { 1915 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1916 | } 1917 | return new Array( this.depth + ( extra || 0 ) ).join(chr); 1918 | }, 1919 | up: function( a ) { 1920 | this.depth += a || 1; 1921 | }, 1922 | down: function( a ) { 1923 | this.depth -= a || 1; 1924 | }, 1925 | setParser: function( name, parser ) { 1926 | this.parsers[name] = parser; 1927 | }, 1928 | // The next 3 are exposed so you can use them 1929 | quote: quote, 1930 | literal: literal, 1931 | join: join, 1932 | // 1933 | depth: 1, 1934 | // This is the list of parsers, to modify them, use jsDump.setParser 1935 | parsers: { 1936 | window: "[Window]", 1937 | document: "[Document]", 1938 | error: function(error) { 1939 | return "Error(\"" + error.message + "\")"; 1940 | }, 1941 | unknown: "[Unknown]", 1942 | "null": "null", 1943 | "undefined": "undefined", 1944 | "function": function( fn ) { 1945 | var ret = "function", 1946 | // functions never have name in IE 1947 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1948 | 1949 | if ( name ) { 1950 | ret += " " + name; 1951 | } 1952 | ret += "( "; 1953 | 1954 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1955 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1956 | }, 1957 | array: array, 1958 | nodelist: array, 1959 | "arguments": array, 1960 | object: function( map, stack ) { 1961 | /*jshint forin:false */ 1962 | var ret = [ ], keys, key, val, i; 1963 | QUnit.jsDump.up(); 1964 | keys = []; 1965 | for ( key in map ) { 1966 | keys.push( key ); 1967 | } 1968 | keys.sort(); 1969 | for ( i = 0; i < keys.length; i++ ) { 1970 | key = keys[ i ]; 1971 | val = map[ key ]; 1972 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1973 | } 1974 | QUnit.jsDump.down(); 1975 | return join( "{", ret, "}" ); 1976 | }, 1977 | node: function( node ) { 1978 | var len, i, val, 1979 | open = QUnit.jsDump.HTML ? "<" : "<", 1980 | close = QUnit.jsDump.HTML ? ">" : ">", 1981 | tag = node.nodeName.toLowerCase(), 1982 | ret = open + tag, 1983 | attrs = node.attributes; 1984 | 1985 | if ( attrs ) { 1986 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1987 | val = attrs[i].nodeValue; 1988 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1989 | // Those have values like undefined, null, 0, false, "" or "inherit". 1990 | if ( val && val !== "inherit" ) { 1991 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1992 | } 1993 | } 1994 | } 1995 | ret += close; 1996 | 1997 | // Show content of TextNode or CDATASection 1998 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1999 | ret += node.nodeValue; 2000 | } 2001 | 2002 | return ret + open + "/" + tag + close; 2003 | }, 2004 | // function calls it internally, it's the arguments part of the function 2005 | functionArgs: function( fn ) { 2006 | var args, 2007 | l = fn.length; 2008 | 2009 | if ( !l ) { 2010 | return ""; 2011 | } 2012 | 2013 | args = new Array(l); 2014 | while ( l-- ) { 2015 | // 97 is 'a' 2016 | args[l] = String.fromCharCode(97+l); 2017 | } 2018 | return " " + args.join( ", " ) + " "; 2019 | }, 2020 | // object calls it internally, the key part of an item in a map 2021 | key: quote, 2022 | // function calls it internally, it's the content of the function 2023 | functionCode: "[code]", 2024 | // node calls it internally, it's an html attribute value 2025 | attribute: quote, 2026 | string: quote, 2027 | date: quote, 2028 | regexp: literal, 2029 | number: literal, 2030 | "boolean": literal 2031 | }, 2032 | // if true, entities are escaped ( <, >, \t, space and \n ) 2033 | HTML: false, 2034 | // indentation unit 2035 | indentChar: " ", 2036 | // if true, items in a collection, are separated by a \n, else just a space. 2037 | multiline: true 2038 | }; 2039 | 2040 | return jsDump; 2041 | }()); 2042 | 2043 | // from jquery.js 2044 | function inArray( elem, array ) { 2045 | if ( array.indexOf ) { 2046 | return array.indexOf( elem ); 2047 | } 2048 | 2049 | for ( var i = 0, length = array.length; i < length; i++ ) { 2050 | if ( array[ i ] === elem ) { 2051 | return i; 2052 | } 2053 | } 2054 | 2055 | return -1; 2056 | } 2057 | 2058 | /* 2059 | * Javascript Diff Algorithm 2060 | * By John Resig (http://ejohn.org/) 2061 | * Modified by Chu Alan "sprite" 2062 | * 2063 | * Released under the MIT license. 2064 | * 2065 | * More Info: 2066 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2067 | * 2068 | * Usage: QUnit.diff(expected, actual) 2069 | * 2070 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2071 | */ 2072 | QUnit.diff = (function() { 2073 | /*jshint eqeqeq:false, eqnull:true */ 2074 | function diff( o, n ) { 2075 | var i, 2076 | ns = {}, 2077 | os = {}; 2078 | 2079 | for ( i = 0; i < n.length; i++ ) { 2080 | if ( !hasOwn.call( ns, n[i] ) ) { 2081 | ns[ n[i] ] = { 2082 | rows: [], 2083 | o: null 2084 | }; 2085 | } 2086 | ns[ n[i] ].rows.push( i ); 2087 | } 2088 | 2089 | for ( i = 0; i < o.length; i++ ) { 2090 | if ( !hasOwn.call( os, o[i] ) ) { 2091 | os[ o[i] ] = { 2092 | rows: [], 2093 | n: null 2094 | }; 2095 | } 2096 | os[ o[i] ].rows.push( i ); 2097 | } 2098 | 2099 | for ( i in ns ) { 2100 | if ( hasOwn.call( ns, i ) ) { 2101 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2102 | n[ ns[i].rows[0] ] = { 2103 | text: n[ ns[i].rows[0] ], 2104 | row: os[i].rows[0] 2105 | }; 2106 | o[ os[i].rows[0] ] = { 2107 | text: o[ os[i].rows[0] ], 2108 | row: ns[i].rows[0] 2109 | }; 2110 | } 2111 | } 2112 | } 2113 | 2114 | for ( i = 0; i < n.length - 1; i++ ) { 2115 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2116 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2117 | 2118 | n[ i + 1 ] = { 2119 | text: n[ i + 1 ], 2120 | row: n[i].row + 1 2121 | }; 2122 | o[ n[i].row + 1 ] = { 2123 | text: o[ n[i].row + 1 ], 2124 | row: i + 1 2125 | }; 2126 | } 2127 | } 2128 | 2129 | for ( i = n.length - 1; i > 0; i-- ) { 2130 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2131 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2132 | 2133 | n[ i - 1 ] = { 2134 | text: n[ i - 1 ], 2135 | row: n[i].row - 1 2136 | }; 2137 | o[ n[i].row - 1 ] = { 2138 | text: o[ n[i].row - 1 ], 2139 | row: i - 1 2140 | }; 2141 | } 2142 | } 2143 | 2144 | return { 2145 | o: o, 2146 | n: n 2147 | }; 2148 | } 2149 | 2150 | return function( o, n ) { 2151 | o = o.replace( /\s+$/, "" ); 2152 | n = n.replace( /\s+$/, "" ); 2153 | 2154 | var i, pre, 2155 | str = "", 2156 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2157 | oSpace = o.match(/\s+/g), 2158 | nSpace = n.match(/\s+/g); 2159 | 2160 | if ( oSpace == null ) { 2161 | oSpace = [ " " ]; 2162 | } 2163 | else { 2164 | oSpace.push( " " ); 2165 | } 2166 | 2167 | if ( nSpace == null ) { 2168 | nSpace = [ " " ]; 2169 | } 2170 | else { 2171 | nSpace.push( " " ); 2172 | } 2173 | 2174 | if ( out.n.length === 0 ) { 2175 | for ( i = 0; i < out.o.length; i++ ) { 2176 | str += "" + out.o[i] + oSpace[i] + ""; 2177 | } 2178 | } 2179 | else { 2180 | if ( out.n[0].text == null ) { 2181 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2182 | str += "" + out.o[n] + oSpace[n] + ""; 2183 | } 2184 | } 2185 | 2186 | for ( i = 0; i < out.n.length; i++ ) { 2187 | if (out.n[i].text == null) { 2188 | str += "" + out.n[i] + nSpace[i] + ""; 2189 | } 2190 | else { 2191 | // `pre` initialized at top of scope 2192 | pre = ""; 2193 | 2194 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2195 | pre += "" + out.o[n] + oSpace[n] + ""; 2196 | } 2197 | str += " " + out.n[i].text + nSpace[i] + pre; 2198 | } 2199 | } 2200 | } 2201 | 2202 | return str; 2203 | }; 2204 | }()); 2205 | 2206 | // for CommonJS environments, export everything 2207 | if ( typeof exports !== "undefined" ) { 2208 | extend( exports, QUnit.constructor.prototype ); 2209 | } 2210 | 2211 | // get at whatever the global object is, like window in browsers 2212 | }( (function() {return this;}.call()) )); 2213 | -------------------------------------------------------------------------------- /test/vendor/runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QtWebKit-powered headless test runner using PhantomJS 3 | * 4 | * PhantomJS binaries: http://phantomjs.org/download.html 5 | * Requires PhantomJS 1.6+ (1.7+ recommended) 6 | * 7 | * Run with: 8 | * phantomjs runner.js [url-of-your-qunit-testsuite] 9 | * 10 | * e.g. 11 | * phantomjs runner.js http://localhost/qunit/test/index.html 12 | */ 13 | 14 | /*global phantom:false, require:false, console:false, window:false, QUnit:false */ 15 | 16 | (function() { 17 | 'use strict'; 18 | 19 | var url, page, timeout, 20 | args = require('system').args; 21 | 22 | // arg[0]: scriptName, args[1...]: arguments 23 | if (args.length < 2 || args.length > 3) { 24 | console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); 25 | phantom.exit(1); 26 | } 27 | 28 | url = args[1]; 29 | page = require('webpage').create(); 30 | if (args[2] !== undefined) { 31 | timeout = parseInt(args[2], 10); 32 | } 33 | 34 | // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) 35 | page.onConsoleMessage = function(msg) { 36 | console.log(msg); 37 | }; 38 | 39 | page.onInitialized = function() { 40 | page.evaluate(addLogging); 41 | }; 42 | 43 | page.onCallback = function(message) { 44 | var result, 45 | failed; 46 | 47 | if (message) { 48 | if (message.name === 'QUnit.done') { 49 | result = message.data; 50 | failed = !result || result.failed; 51 | 52 | phantom.exit(failed ? 1 : 0); 53 | } 54 | } 55 | }; 56 | 57 | page.open(url, function(status) { 58 | if (status !== 'success') { 59 | console.error('Unable to access network: ' + status); 60 | phantom.exit(1); 61 | } else { 62 | // Cannot do this verification with the 'DOMContentLoaded' handler because it 63 | // will be too late to attach it if a page does not have any script tags. 64 | var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); 65 | if (qunitMissing) { 66 | console.error('The `QUnit` object is not present on this page.'); 67 | phantom.exit(1); 68 | } 69 | 70 | // Set a timeout on the test running, otherwise tests with async problems will hang forever 71 | if (typeof timeout === 'number') { 72 | setTimeout(function() { 73 | console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); 74 | phantom.exit(1); 75 | }, timeout * 1000); 76 | } 77 | 78 | // Do nothing... the callback mechanism will handle everything! 79 | } 80 | }); 81 | 82 | function addLogging() { 83 | window.document.addEventListener('DOMContentLoaded', function() { 84 | var currentTestAssertions = []; 85 | 86 | QUnit.log(function(details) { 87 | var response; 88 | 89 | // Ignore passing assertions 90 | if (details.result) { 91 | return; 92 | } 93 | 94 | response = details.message || ''; 95 | 96 | if (typeof details.expected !== 'undefined') { 97 | if (response) { 98 | response += ', '; 99 | } 100 | 101 | response += 'expected: ' + details.expected + ', but was: ' + details.actual; 102 | } 103 | 104 | if (details.source) { 105 | response += "\n" + details.source; 106 | } 107 | 108 | currentTestAssertions.push('Failed assertion: ' + response); 109 | }); 110 | 111 | QUnit.testDone(function(result) { 112 | var i, 113 | len, 114 | name = result.module + ': ' + result.name; 115 | 116 | if (result.failed) { 117 | console.log('Test failed: ' + name); 118 | 119 | for (i = 0, len = currentTestAssertions.length; i < len; i++) { 120 | console.log(' ' + currentTestAssertions[i]); 121 | } 122 | } 123 | 124 | currentTestAssertions.length = 0; 125 | }); 126 | 127 | QUnit.done(function(result) { 128 | console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); 129 | 130 | if (typeof window.callPhantom === 'function') { 131 | window.callPhantom({ 132 | 'name': 'QUnit.done', 133 | 'data': result 134 | }); 135 | } 136 | }); 137 | }, false); 138 | } 139 | })(); 140 | --------------------------------------------------------------------------------