Revision ac01c25a
Von Moritz Bunkus vor etwa 10 Jahren hinzugefügt
SL/Controller/JSTests.pm | ||
---|---|---|
package SL::Controller::JSTests;
|
||
|
||
use strict;
|
||
|
||
use parent qw(SL::Controller::Base);
|
||
|
||
use File::Find ();
|
||
use File::Spec ();
|
||
|
||
use SL::System::Process;
|
||
|
||
use Rose::Object::MakeMethods::Generic
|
||
(
|
||
'scalar --get_set_init' => [ qw(all_scripts scripts_to_run) ],
|
||
);
|
||
|
||
#
|
||
# actions
|
||
#
|
||
|
||
sub action_run {
|
||
my ($self) = @_;
|
||
|
||
$::request->layout->use_stylesheet("css/qunit.css");
|
||
$self->render('js_tests/run', title => $::locale->text('Run JavaScript unit tests'));
|
||
}
|
||
|
||
#
|
||
# helpers
|
||
#
|
||
|
||
sub init_all_scripts {
|
||
my ($self) = @_;
|
||
|
||
my $exe_dir = SL::System::Process->exe_dir;
|
||
|
||
my @scripts;
|
||
my $wanted = sub {
|
||
return if ( ! -f $File::Find::name ) || ($File::Find::name !~ m{\.js$});
|
||
push @scripts, File::Spec->abs2rel($File::Find::name, $exe_dir);
|
||
};
|
||
|
||
File::Find::find($wanted, $exe_dir . '/js/t');
|
||
|
||
return \@scripts;
|
||
}
|
||
|
||
sub init_scripts_to_run {
|
||
my ($self) = @_;
|
||
my $filter = $::form->{file_filter} || '.';
|
||
return [ grep { m{$filter} } @{ $self->all_scripts } ];
|
||
}
|
||
|
||
1;
|
css/qunit-1.17.1.css | ||
---|---|---|
/*!
|
||
* QUnit 1.17.1
|
||
* http://qunitjs.com/
|
||
*
|
||
* Copyright jQuery Foundation and other contributors
|
||
* Released under the MIT license
|
||
* http://jquery.org/license
|
||
*
|
||
* Date: 2015-01-20T19:39Z
|
||
*/
|
||
|
||
/** Font Family and Sizes */
|
||
|
||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
|
||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
||
}
|
||
|
||
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
||
#qunit-tests { font-size: smaller; }
|
||
|
||
|
||
/** Resets */
|
||
|
||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
|
||
/** Header */
|
||
|
||
#qunit-header {
|
||
padding: 0.5em 0 0.5em 1em;
|
||
|
||
color: #8699A4;
|
||
background-color: #0D3349;
|
||
|
||
font-size: 1.5em;
|
||
line-height: 1em;
|
||
font-weight: 400;
|
||
|
||
border-radius: 5px 5px 0 0;
|
||
}
|
||
|
||
#qunit-header a {
|
||
text-decoration: none;
|
||
color: #C2CCD1;
|
||
}
|
||
|
||
#qunit-header a:hover,
|
||
#qunit-header a:focus {
|
||
color: #FFF;
|
||
}
|
||
|
||
#qunit-testrunner-toolbar label {
|
||
display: inline-block;
|
||
padding: 0 0.5em 0 0.1em;
|
||
}
|
||
|
||
#qunit-banner {
|
||
height: 5px;
|
||
}
|
||
|
||
#qunit-testrunner-toolbar {
|
||
padding: 0.5em 1em 0.5em 1em;
|
||
color: #5E740B;
|
||
background-color: #EEE;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#qunit-userAgent {
|
||
padding: 0.5em 1em 0.5em 1em;
|
||
background-color: #2B81AF;
|
||
color: #FFF;
|
||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
||
}
|
||
|
||
#qunit-modulefilter-container {
|
||
float: right;
|
||
padding: 0.2em;
|
||
}
|
||
|
||
.qunit-url-config {
|
||
display: inline-block;
|
||
padding: 0.1em;
|
||
}
|
||
|
||
.qunit-filter {
|
||
display: block;
|
||
float: right;
|
||
margin-left: 1em;
|
||
}
|
||
|
||
/** Tests: Pass/Fail */
|
||
|
||
#qunit-tests {
|
||
list-style-position: inside;
|
||
}
|
||
|
||
#qunit-tests li {
|
||
padding: 0.4em 1em 0.4em 1em;
|
||
border-bottom: 1px solid #FFF;
|
||
list-style-position: inside;
|
||
}
|
||
|
||
#qunit-tests > li {
|
||
display: none;
|
||
}
|
||
|
||
#qunit-tests li.running,
|
||
#qunit-tests li.pass,
|
||
#qunit-tests li.fail,
|
||
#qunit-tests li.skipped {
|
||
display: list-item;
|
||
}
|
||
|
||
#qunit-tests.hidepass li.running,
|
||
#qunit-tests.hidepass li.pass {
|
||
display: none;
|
||
}
|
||
|
||
#qunit-tests li strong {
|
||
cursor: pointer;
|
||
}
|
||
|
||
#qunit-tests li.skipped strong {
|
||
cursor: default;
|
||
}
|
||
|
||
#qunit-tests li a {
|
||
padding: 0.5em;
|
||
color: #C2CCD1;
|
||
text-decoration: none;
|
||
}
|
||
#qunit-tests li a:hover,
|
||
#qunit-tests li a:focus {
|
||
color: #000;
|
||
}
|
||
|
||
#qunit-tests li .runtime {
|
||
float: right;
|
||
font-size: smaller;
|
||
}
|
||
|
||
.qunit-assert-list {
|
||
margin-top: 0.5em;
|
||
padding: 0.5em;
|
||
|
||
background-color: #FFF;
|
||
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.qunit-collapsed {
|
||
display: none;
|
||
}
|
||
|
||
#qunit-tests table {
|
||
border-collapse: collapse;
|
||
margin-top: 0.2em;
|
||
}
|
||
|
||
#qunit-tests th {
|
||
text-align: right;
|
||
vertical-align: top;
|
||
padding: 0 0.5em 0 0;
|
||
}
|
||
|
||
#qunit-tests td {
|
||
vertical-align: top;
|
||
}
|
||
|
||
#qunit-tests pre {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
#qunit-tests del {
|
||
background-color: #E0F2BE;
|
||
color: #374E0C;
|
||
text-decoration: none;
|
||
}
|
||
|
||
#qunit-tests ins {
|
||
background-color: #FFCACA;
|
||
color: #500;
|
||
text-decoration: none;
|
||
}
|
||
|
||
/*** Test Counts */
|
||
|
||
#qunit-tests b.counts { color: #000; }
|
||
#qunit-tests b.passed { color: #5E740B; }
|
||
#qunit-tests b.failed { color: #710909; }
|
||
|
||
#qunit-tests li li {
|
||
padding: 5px;
|
||
background-color: #FFF;
|
||
border-bottom: none;
|
||
list-style-position: inside;
|
||
}
|
||
|
||
/*** Passing Styles */
|
||
|
||
#qunit-tests li li.pass {
|
||
color: #3C510C;
|
||
background-color: #FFF;
|
||
border-left: 10px solid #C6E746;
|
||
}
|
||
|
||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
||
#qunit-tests .pass .test-name { color: #366097; }
|
||
|
||
#qunit-tests .pass .test-actual,
|
||
#qunit-tests .pass .test-expected { color: #999; }
|
||
|
||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
||
|
||
/*** Failing Styles */
|
||
|
||
#qunit-tests li li.fail {
|
||
color: #710909;
|
||
background-color: #FFF;
|
||
border-left: 10px solid #EE5757;
|
||
white-space: pre;
|
||
}
|
||
|
||
#qunit-tests > li:last-child {
|
||
border-radius: 0 0 5px 5px;
|
||
}
|
||
|
||
#qunit-tests .fail { color: #000; background-color: #EE5757; }
|
||
#qunit-tests .fail .test-name,
|
||
#qunit-tests .fail .module-name { color: #000; }
|
||
|
||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
||
#qunit-tests .fail .test-expected { color: #008000; }
|
||
|
||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||
|
||
/*** Skipped tests */
|
||
|
||
#qunit-tests .skipped {
|
||
background-color: #EBECE9;
|
||
}
|
||
|
||
#qunit-tests .qunit-skipped-label {
|
||
background-color: #F4FF77;
|
||
display: inline-block;
|
||
font-style: normal;
|
||
color: #366097;
|
||
line-height: 1.8em;
|
||
padding: 0 0.5em;
|
||
margin: -0.4em 0.4em -0.4em 0;
|
||
}
|
||
|
||
/** Result */
|
||
|
||
#qunit-testresult {
|
||
padding: 0.5em 1em 0.5em 1em;
|
||
|
||
color: #2B81AF;
|
||
background-color: #D2E0E6;
|
||
|
||
border-bottom: 1px solid #FFF;
|
||
}
|
||
#qunit-testresult .module-name {
|
||
font-weight: 700;
|
||
}
|
||
|
||
/** Fixture */
|
||
|
||
#qunit-fixture {
|
||
position: absolute;
|
||
top: -10000px;
|
||
left: -10000px;
|
||
width: 1000px;
|
||
height: 1000px;
|
||
}
|
css/qunit.css | ||
---|---|---|
qunit-1.17.1.css
|
js/qunit-1.17.1.js | ||
---|---|---|
/*!
|
||
* QUnit 1.17.1
|
||
* http://qunitjs.com/
|
||
*
|
||
* Copyright jQuery Foundation and other contributors
|
||
* Released under the MIT license
|
||
* http://jquery.org/license
|
||
*
|
||
* Date: 2015-01-20T19:39Z
|
||
*/
|
||
|
||
(function( window ) {
|
||
|
||
var QUnit,
|
||
config,
|
||
onErrorFnPrev,
|
||
loggingCallbacks = {},
|
||
fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
|
||
toString = Object.prototype.toString,
|
||
hasOwn = Object.prototype.hasOwnProperty,
|
||
// Keep a local reference to Date (GH-283)
|
||
Date = window.Date,
|
||
now = Date.now || function() {
|
||
return new Date().getTime();
|
||
},
|
||
globalStartCalled = false,
|
||
runStarted = false,
|
||
setTimeout = window.setTimeout,
|
||
clearTimeout = window.clearTimeout,
|
||
defined = {
|
||
document: window.document !== undefined,
|
||
setTimeout: window.setTimeout !== undefined,
|
||
sessionStorage: (function() {
|
||
var x = "qunit-test-string";
|
||
try {
|
||
sessionStorage.setItem( x, x );
|
||
sessionStorage.removeItem( x );
|
||
return true;
|
||
} catch ( e ) {
|
||
return false;
|
||
}
|
||
}())
|
||
},
|
||
/**
|
||
* Provides a normalized error string, correcting an issue
|
||
* with IE 7 (and prior) where Error.prototype.toString is
|
||
* not properly implemented
|
||
*
|
||
* Based on http://es5.github.com/#x15.11.4.4
|
||
*
|
||
* @param {String|Error} error
|
||
* @return {String} error message
|
||
*/
|
||
errorString = function( error ) {
|
||
var name, message,
|
||
errorString = error.toString();
|
||
if ( errorString.substring( 0, 7 ) === "[object" ) {
|
||
name = error.name ? error.name.toString() : "Error";
|
||
message = error.message ? error.message.toString() : "";
|
||
if ( name && message ) {
|
||
return name + ": " + message;
|
||
} else if ( name ) {
|
||
return name;
|
||
} else if ( message ) {
|
||
return message;
|
||
} else {
|
||
return "Error";
|
||
}
|
||
} else {
|
||
return errorString;
|
||
}
|
||
},
|
||
/**
|
||
* Makes a clone of an object using only Array or Object as base,
|
||
* and copies over the own enumerable properties.
|
||
*
|
||
* @param {Object} obj
|
||
* @return {Object} New object with only the own properties (recursively).
|
||
*/
|
||
objectValues = function( obj ) {
|
||
var key, val,
|
||
vals = QUnit.is( "array", obj ) ? [] : {};
|
||
for ( key in obj ) {
|
||
if ( hasOwn.call( obj, key ) ) {
|
||
val = obj[ key ];
|
||
vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
|
||
}
|
||
}
|
||
return vals;
|
||
};
|
||
|
||
QUnit = {};
|
||
|
||
/**
|
||
* Config object: Maintain internal state
|
||
* Later exposed as QUnit.config
|
||
* `config` initialized at top of scope
|
||
*/
|
||
config = {
|
||
// The queue of tests to run
|
||
queue: [],
|
||
|
||
// block until document ready
|
||
blocking: true,
|
||
|
||
// by default, run previously failed tests first
|
||
// very useful in combination with "Hide passed tests" checked
|
||
reorder: true,
|
||
|
||
// by default, modify document.title when suite is done
|
||
altertitle: true,
|
||
|
||
// by default, scroll to top of the page when suite is done
|
||
scrolltop: true,
|
||
|
||
// when enabled, all tests must call expect()
|
||
requireExpects: false,
|
||
|
||
// add checkboxes that are persisted in the query-string
|
||
// when enabled, the id is set to `true` as a `QUnit.config` property
|
||
urlConfig: [
|
||
{
|
||
id: "hidepassed",
|
||
label: "Hide passed tests",
|
||
tooltip: "Only show tests and assertions that fail. Stored as query-strings."
|
||
},
|
||
{
|
||
id: "noglobals",
|
||
label: "Check for Globals",
|
||
tooltip: "Enabling this will test if any test introduces new properties on the " +
|
||
"`window` object. Stored as query-strings."
|
||
},
|
||
{
|
||
id: "notrycatch",
|
||
label: "No try-catch",
|
||
tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
|
||
"exceptions in IE reasonable. Stored as query-strings."
|
||
}
|
||
],
|
||
|
||
// Set of all modules.
|
||
modules: [],
|
||
|
||
// The first unnamed module
|
||
currentModule: {
|
||
name: "",
|
||
tests: []
|
||
},
|
||
|
||
callbacks: {}
|
||
};
|
||
|
||
// Push a loose unnamed module to the modules collection
|
||
config.modules.push( config.currentModule );
|
||
|
||
// Initialize more QUnit.config and QUnit.urlParams
|
||
(function() {
|
||
var i, current,
|
||
location = window.location || { search: "", protocol: "file:" },
|
||
params = location.search.slice( 1 ).split( "&" ),
|
||
length = params.length,
|
||
urlParams = {};
|
||
|
||
if ( params[ 0 ] ) {
|
||
for ( i = 0; i < length; i++ ) {
|
||
current = params[ i ].split( "=" );
|
||
current[ 0 ] = decodeURIComponent( current[ 0 ] );
|
||
|
||
// allow just a key to turn on a flag, e.g., test.html?noglobals
|
||
current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
|
||
if ( urlParams[ current[ 0 ] ] ) {
|
||
urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
|
||
} else {
|
||
urlParams[ current[ 0 ] ] = current[ 1 ];
|
||
}
|
||
}
|
||
}
|
||
|
||
if ( urlParams.filter === true ) {
|
||
delete urlParams.filter;
|
||
}
|
||
|
||
QUnit.urlParams = urlParams;
|
||
|
||
// String search anywhere in moduleName+testName
|
||
config.filter = urlParams.filter;
|
||
|
||
config.testId = [];
|
||
if ( urlParams.testId ) {
|
||
|
||
// Ensure that urlParams.testId is an array
|
||
urlParams.testId = [].concat( urlParams.testId );
|
||
for ( i = 0; i < urlParams.testId.length; i++ ) {
|
||
config.testId.push( urlParams.testId[ i ] );
|
||
}
|
||
}
|
||
|
||
// Figure out if we're running the tests from a server or not
|
||
QUnit.isLocal = location.protocol === "file:";
|
||
}());
|
||
|
||
// Root QUnit object.
|
||
// `QUnit` initialized at top of scope
|
||
extend( QUnit, {
|
||
|
||
// call on start of module test to prepend name to all tests
|
||
module: function( name, testEnvironment ) {
|
||
var currentModule = {
|
||
name: name,
|
||
testEnvironment: testEnvironment,
|
||
tests: []
|
||
};
|
||
|
||
// DEPRECATED: handles setup/teardown functions,
|
||
// beforeEach and afterEach should be used instead
|
||
if ( testEnvironment && testEnvironment.setup ) {
|
||
testEnvironment.beforeEach = testEnvironment.setup;
|
||
delete testEnvironment.setup;
|
||
}
|
||
if ( testEnvironment && testEnvironment.teardown ) {
|
||
testEnvironment.afterEach = testEnvironment.teardown;
|
||
delete testEnvironment.teardown;
|
||
}
|
||
|
||
config.modules.push( currentModule );
|
||
config.currentModule = currentModule;
|
||
},
|
||
|
||
// DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
|
||
asyncTest: function( testName, expected, callback ) {
|
||
if ( arguments.length === 2 ) {
|
||
callback = expected;
|
||
expected = null;
|
||
}
|
||
|
||
QUnit.test( testName, expected, callback, true );
|
||
},
|
||
|
||
test: function( testName, expected, callback, async ) {
|
||
var test;
|
||
|
||
if ( arguments.length === 2 ) {
|
||
callback = expected;
|
||
expected = null;
|
||
}
|
||
|
||
test = new Test({
|
||
testName: testName,
|
||
expected: expected,
|
||
async: async,
|
||
callback: callback
|
||
});
|
||
|
||
test.queue();
|
||
},
|
||
|
||
skip: function( testName ) {
|
||
var test = new Test({
|
||
testName: testName,
|
||
skip: true
|
||
});
|
||
|
||
test.queue();
|
||
},
|
||
|
||
// DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
|
||
// In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
|
||
start: function( count ) {
|
||
var globalStartAlreadyCalled = globalStartCalled;
|
||
|
||
if ( !config.current ) {
|
||
globalStartCalled = true;
|
||
|
||
if ( runStarted ) {
|
||
throw new Error( "Called start() outside of a test context while already started" );
|
||
} else if ( globalStartAlreadyCalled || count > 1 ) {
|
||
throw new Error( "Called start() outside of a test context too many times" );
|
||
} else if ( config.autostart ) {
|
||
throw new Error( "Called start() outside of a test context when " +
|
||
"QUnit.config.autostart was true" );
|
||
} else if ( !config.pageLoaded ) {
|
||
|
||
// The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
|
||
config.autostart = true;
|
||
return;
|
||
}
|
||
} else {
|
||
|
||
// If a test is running, adjust its semaphore
|
||
config.current.semaphore -= count || 1;
|
||
|
||
// Don't start until equal number of stop-calls
|
||
if ( config.current.semaphore > 0 ) {
|
||
return;
|
||
}
|
||
|
||
// throw an Error if start is called more often than stop
|
||
if ( config.current.semaphore < 0 ) {
|
||
config.current.semaphore = 0;
|
||
|
||
QUnit.pushFailure(
|
||
"Called start() while already started (test's semaphore was 0 already)",
|
||
sourceFromStacktrace( 2 )
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
resumeProcessing();
|
||
},
|
||
|
||
// DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
|
||
stop: function( count ) {
|
||
|
||
// If there isn't a test running, don't allow QUnit.stop() to be called
|
||
if ( !config.current ) {
|
||
throw new Error( "Called stop() outside of a test context" );
|
||
}
|
||
|
||
// If a test is running, adjust its semaphore
|
||
config.current.semaphore += count || 1;
|
||
|
||
pauseProcessing();
|
||
},
|
||
|
||
config: config,
|
||
|
||
// Safe object type checking
|
||
is: function( type, obj ) {
|
||
return QUnit.objectType( obj ) === type;
|
||
},
|
||
|
||
objectType: function( obj ) {
|
||
if ( typeof obj === "undefined" ) {
|
||
return "undefined";
|
||
}
|
||
|
||
// Consider: typeof null === object
|
||
if ( obj === null ) {
|
||
return "null";
|
||
}
|
||
|
||
var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
|
||
type = match && match[ 1 ] || "";
|
||
|
||
switch ( type ) {
|
||
case "Number":
|
||
if ( isNaN( obj ) ) {
|
||
return "nan";
|
||
}
|
||
return "number";
|
||
case "String":
|
||
case "Boolean":
|
||
case "Array":
|
||
case "Date":
|
||
case "RegExp":
|
||
case "Function":
|
||
return type.toLowerCase();
|
||
}
|
||
if ( typeof obj === "object" ) {
|
||
return "object";
|
||
}
|
||
return undefined;
|
||
},
|
||
|
||
extend: extend,
|
||
|
||
load: function() {
|
||
config.pageLoaded = true;
|
||
|
||
// Initialize the configuration options
|
||
extend( config, {
|
||
stats: { all: 0, bad: 0 },
|
||
moduleStats: { all: 0, bad: 0 },
|
||
started: 0,
|
||
updateRate: 1000,
|
||
autostart: true,
|
||
filter: ""
|
||
}, true );
|
||
|
||
config.blocking = false;
|
||
|
||
if ( config.autostart ) {
|
||
resumeProcessing();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Register logging callbacks
|
||
(function() {
|
||
var i, l, key,
|
||
callbacks = [ "begin", "done", "log", "testStart", "testDone",
|
||
"moduleStart", "moduleDone" ];
|
||
|
||
function registerLoggingCallback( key ) {
|
||
var loggingCallback = function( callback ) {
|
||
if ( QUnit.objectType( callback ) !== "function" ) {
|
||
throw new Error(
|
||
"QUnit logging methods require a callback function as their first parameters."
|
||
);
|
||
}
|
||
|
||
config.callbacks[ key ].push( callback );
|
||
};
|
||
|
||
// DEPRECATED: This will be removed on QUnit 2.0.0+
|
||
// Stores the registered functions allowing restoring
|
||
// at verifyLoggingCallbacks() if modified
|
||
loggingCallbacks[ key ] = loggingCallback;
|
||
|
||
return loggingCallback;
|
||
}
|
||
|
||
for ( i = 0, l = callbacks.length; i < l; i++ ) {
|
||
key = callbacks[ i ];
|
||
|
||
// Initialize key collection of logging callback
|
||
if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
|
||
config.callbacks[ key ] = [];
|
||
}
|
||
|
||
QUnit[ key ] = registerLoggingCallback( key );
|
||
}
|
||
})();
|
||
|
||
// `onErrorFnPrev` initialized at top of scope
|
||
// Preserve other handlers
|
||
onErrorFnPrev = window.onerror;
|
||
|
||
// Cover uncaught exceptions
|
||
// Returning true will suppress the default browser handler,
|
||
// returning false will let it run.
|
||
window.onerror = function( error, filePath, linerNr ) {
|
||
var ret = false;
|
||
if ( onErrorFnPrev ) {
|
||
ret = onErrorFnPrev( error, filePath, linerNr );
|
||
}
|
||
|
||
// Treat return value as window.onerror itself does,
|
||
// Only do our handling if not suppressed.
|
||
if ( ret !== true ) {
|
||
if ( QUnit.config.current ) {
|
||
if ( QUnit.config.current.ignoreGlobalErrors ) {
|
||
return true;
|
||
}
|
||
QUnit.pushFailure( error, filePath + ":" + linerNr );
|
||
} else {
|
||
QUnit.test( "global failure", extend(function() {
|
||
QUnit.pushFailure( error, filePath + ":" + linerNr );
|
||
}, { validTest: true } ) );
|
||
}
|
||
return false;
|
||
}
|
||
|
||
return ret;
|
||
};
|
||
|
||
function done() {
|
||
var runtime, passed;
|
||
|
||
config.autorun = true;
|
||
|
||
// Log the last module results
|
||
if ( config.previousModule ) {
|
||
runLoggingCallbacks( "moduleDone", {
|
||
name: config.previousModule.name,
|
||
tests: config.previousModule.tests,
|
||
failed: config.moduleStats.bad,
|
||
passed: config.moduleStats.all - config.moduleStats.bad,
|
||
total: config.moduleStats.all,
|
||
runtime: now() - config.moduleStats.started
|
||
});
|
||
}
|
||
delete config.previousModule;
|
||
|
||
runtime = now() - config.started;
|
||
passed = config.stats.all - config.stats.bad;
|
||
|
||
runLoggingCallbacks( "done", {
|
||
failed: config.stats.bad,
|
||
passed: passed,
|
||
total: config.stats.all,
|
||
runtime: runtime
|
||
});
|
||
}
|
||
|
||
// Doesn't support IE6 to IE9
|
||
// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
|
||
function extractStacktrace( e, offset ) {
|
||
offset = offset === undefined ? 4 : offset;
|
||
|
||
var stack, include, i;
|
||
|
||
if ( e.stacktrace ) {
|
||
|
||
// Opera 12.x
|
||
return e.stacktrace.split( "\n" )[ offset + 3 ];
|
||
} else if ( e.stack ) {
|
||
|
||
// Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node
|
||
stack = e.stack.split( "\n" );
|
||
if ( /^error$/i.test( stack[ 0 ] ) ) {
|
||
stack.shift();
|
||
}
|
||
if ( fileName ) {
|
||
include = [];
|
||
for ( i = offset; i < stack.length; i++ ) {
|
||
if ( stack[ i ].indexOf( fileName ) !== -1 ) {
|
||
break;
|
||
}
|
||
include.push( stack[ i ] );
|
||
}
|
||
if ( include.length ) {
|
||
return include.join( "\n" );
|
||
}
|
||
}
|
||
return stack[ offset ];
|
||
} else if ( e.sourceURL ) {
|
||
|
||
// Safari < 6
|
||
// exclude useless self-reference for generated Error objects
|
||
if ( /qunit.js$/.test( e.sourceURL ) ) {
|
||
return;
|
||
}
|
||
|
||
// for actual exceptions, this is useful
|
||
return e.sourceURL + ":" + e.line;
|
||
}
|
||
}
|
||
|
||
function sourceFromStacktrace( offset ) {
|
||
var e = new Error();
|
||
if ( !e.stack ) {
|
||
try {
|
||
throw e;
|
||
} catch ( err ) {
|
||
// This should already be true in most browsers
|
||
e = err;
|
||
}
|
||
}
|
||
return extractStacktrace( e, offset );
|
||
}
|
||
|
||
function synchronize( callback, last ) {
|
||
if ( QUnit.objectType( callback ) === "array" ) {
|
||
while ( callback.length ) {
|
||
synchronize( callback.shift() );
|
||
}
|
||
return;
|
||
}
|
||
config.queue.push( callback );
|
||
|
||
if ( config.autorun && !config.blocking ) {
|
||
process( last );
|
||
}
|
||
}
|
||
|
||
function process( last ) {
|
||
function next() {
|
||
process( last );
|
||
}
|
||
var start = now();
|
||
config.depth = ( config.depth || 0 ) + 1;
|
||
|
||
while ( config.queue.length && !config.blocking ) {
|
||
if ( !defined.setTimeout || config.updateRate <= 0 ||
|
||
( ( now() - start ) < config.updateRate ) ) {
|
||
if ( config.current ) {
|
||
|
||
// Reset async tracking for each phase of the Test lifecycle
|
||
config.current.usedAsync = false;
|
||
}
|
||
config.queue.shift()();
|
||
} else {
|
||
setTimeout( next, 13 );
|
||
break;
|
||
}
|
||
}
|
||
config.depth--;
|
||
if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
|
||
done();
|
||
}
|
||
}
|
||
|
||
function begin() {
|
||
var i, l,
|
||
modulesLog = [];
|
||
|
||
// If the test run hasn't officially begun yet
|
||
if ( !config.started ) {
|
||
|
||
// Record the time of the test run's beginning
|
||
config.started = now();
|
||
|
||
verifyLoggingCallbacks();
|
||
|
||
// Delete the loose unnamed module if unused.
|
||
if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
|
||
config.modules.shift();
|
||
}
|
||
|
||
// Avoid unnecessary information by not logging modules' test environments
|
||
for ( i = 0, l = config.modules.length; i < l; i++ ) {
|
||
modulesLog.push({
|
||
name: config.modules[ i ].name,
|
||
tests: config.modules[ i ].tests
|
||
});
|
||
}
|
||
|
||
// The test run is officially beginning now
|
||
runLoggingCallbacks( "begin", {
|
||
totalTests: Test.count,
|
||
modules: modulesLog
|
||
});
|
||
}
|
||
|
||
config.blocking = false;
|
||
process( true );
|
||
}
|
||
|
||
function resumeProcessing() {
|
||
runStarted = true;
|
||
|
||
// A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
|
||
if ( defined.setTimeout ) {
|
||
setTimeout(function() {
|
||
if ( config.current && config.current.semaphore > 0 ) {
|
||
return;
|
||
}
|
||
if ( config.timeout ) {
|
||
clearTimeout( config.timeout );
|
||
}
|
||
|
||
begin();
|
||
}, 13 );
|
||
} else {
|
||
begin();
|
||
}
|
||
}
|
||
|
||
function pauseProcessing() {
|
||
config.blocking = true;
|
||
|
||
if ( config.testTimeout && defined.setTimeout ) {
|
||
clearTimeout( config.timeout );
|
||
config.timeout = setTimeout(function() {
|
||
if ( config.current ) {
|
||
config.current.semaphore = 0;
|
||
QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
|
||
} else {
|
||
throw new Error( "Test timed out" );
|
||
}
|
||
resumeProcessing();
|
||
}, config.testTimeout );
|
||
}
|
||
}
|
||
|
||
function saveGlobal() {
|
||
config.pollution = [];
|
||
|
||
if ( config.noglobals ) {
|
||
for ( var key in window ) {
|
||
if ( hasOwn.call( window, key ) ) {
|
||
// in Opera sometimes DOM element ids show up here, ignore them
|
||
if ( /^qunit-test-output/.test( key ) ) {
|
||
continue;
|
||
}
|
||
config.pollution.push( key );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function checkPollution() {
|
||
var newGlobals,
|
||
deletedGlobals,
|
||
old = config.pollution;
|
||
|
||
saveGlobal();
|
||
|
||
newGlobals = diff( config.pollution, old );
|
||
if ( newGlobals.length > 0 ) {
|
||
QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
|
||
}
|
||
|
||
deletedGlobals = diff( old, config.pollution );
|
||
if ( deletedGlobals.length > 0 ) {
|
||
QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
|
||
}
|
||
}
|
||
|
||
// returns a new Array with the elements that are in a but not in b
|
||
function diff( a, b ) {
|
||
var i, j,
|
||
result = a.slice();
|
||
|
||
for ( i = 0; i < result.length; i++ ) {
|
||
for ( j = 0; j < b.length; j++ ) {
|
||
if ( result[ i ] === b[ j ] ) {
|
||
result.splice( i, 1 );
|
||
i--;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function extend( a, b, undefOnly ) {
|
||
for ( var prop in b ) {
|
||
if ( hasOwn.call( b, prop ) ) {
|
||
|
||
// Avoid "Member not found" error in IE8 caused by messing with window.constructor
|
||
if ( !( prop === "constructor" && a === window ) ) {
|
||
if ( b[ prop ] === undefined ) {
|
||
delete a[ prop ];
|
||
} else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
|
||
a[ prop ] = b[ prop ];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return a;
|
||
}
|
||
|
||
function runLoggingCallbacks( key, args ) {
|
||
var i, l, callbacks;
|
||
|
||
callbacks = config.callbacks[ key ];
|
||
for ( i = 0, l = callbacks.length; i < l; i++ ) {
|
||
callbacks[ i ]( args );
|
||
}
|
||
}
|
||
|
||
// DEPRECATED: This will be removed on 2.0.0+
|
||
// This function verifies if the loggingCallbacks were modified by the user
|
||
// If so, it will restore it, assign the given callback and print a console warning
|
||
function verifyLoggingCallbacks() {
|
||
var loggingCallback, userCallback;
|
||
|
||
for ( loggingCallback in loggingCallbacks ) {
|
||
if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
|
||
|
||
userCallback = QUnit[ loggingCallback ];
|
||
|
||
// Restore the callback function
|
||
QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
|
||
|
||
// Assign the deprecated given callback
|
||
QUnit[ loggingCallback ]( userCallback );
|
||
|
||
if ( window.console && window.console.warn ) {
|
||
window.console.warn(
|
||
"QUnit." + loggingCallback + " was replaced with a new value.\n" +
|
||
"Please, check out the documentation on how to apply logging callbacks.\n" +
|
||
"Reference: http://api.qunitjs.com/category/callbacks/"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// from jquery.js
|
||
function inArray( elem, array ) {
|
||
if ( array.indexOf ) {
|
||
return array.indexOf( elem );
|
||
}
|
||
|
||
for ( var i = 0, length = array.length; i < length; i++ ) {
|
||
if ( array[ i ] === elem ) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
function Test( settings ) {
|
||
var i, l;
|
||
|
||
++Test.count;
|
||
|
||
extend( this, settings );
|
||
this.assertions = [];
|
||
this.semaphore = 0;
|
||
this.usedAsync = false;
|
||
this.module = config.currentModule;
|
||
this.stack = sourceFromStacktrace( 3 );
|
||
|
||
// Register unique strings
|
||
for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
|
||
if ( this.module.tests[ i ].name === this.testName ) {
|
||
this.testName += " ";
|
||
}
|
||
}
|
||
|
||
this.testId = generateHash( this.module.name, this.testName );
|
||
|
||
this.module.tests.push({
|
||
name: this.testName,
|
||
testId: this.testId
|
||
});
|
||
|
||
if ( settings.skip ) {
|
||
|
||
// Skipped tests will fully ignore any sent callback
|
||
this.callback = function() {};
|
||
this.async = false;
|
||
this.expected = 0;
|
||
} else {
|
||
this.assert = new Assert( this );
|
||
}
|
||
}
|
||
|
||
Test.count = 0;
|
||
|
||
Test.prototype = {
|
||
before: function() {
|
||
if (
|
||
|
||
// Emit moduleStart when we're switching from one module to another
|
||
this.module !== config.previousModule ||
|
||
|
||
// They could be equal (both undefined) but if the previousModule property doesn't
|
||
// yet exist it means this is the first test in a suite that isn't wrapped in a
|
||
// module, in which case we'll just emit a moduleStart event for 'undefined'.
|
||
// Without this, reporters can get testStart before moduleStart which is a problem.
|
||
!hasOwn.call( config, "previousModule" )
|
||
) {
|
||
if ( hasOwn.call( config, "previousModule" ) ) {
|
||
runLoggingCallbacks( "moduleDone", {
|
||
name: config.previousModule.name,
|
||
tests: config.previousModule.tests,
|
||
failed: config.moduleStats.bad,
|
||
passed: config.moduleStats.all - config.moduleStats.bad,
|
||
total: config.moduleStats.all,
|
||
runtime: now() - config.moduleStats.started
|
||
});
|
||
}
|
||
config.previousModule = this.module;
|
||
config.moduleStats = { all: 0, bad: 0, started: now() };
|
||
runLoggingCallbacks( "moduleStart", {
|
||
name: this.module.name,
|
||
tests: this.module.tests
|
||
});
|
||
}
|
||
|
||
config.current = this;
|
||
|
||
this.testEnvironment = extend( {}, this.module.testEnvironment );
|
||
delete this.testEnvironment.beforeEach;
|
||
delete this.testEnvironment.afterEach;
|
||
|
||
this.started = now();
|
||
runLoggingCallbacks( "testStart", {
|
||
name: this.testName,
|
||
module: this.module.name,
|
||
testId: this.testId
|
||
});
|
||
|
||
if ( !config.pollution ) {
|
||
saveGlobal();
|
||
}
|
||
},
|
||
|
||
run: function() {
|
||
var promise;
|
||
|
||
config.current = this;
|
||
|
||
if ( this.async ) {
|
||
QUnit.stop();
|
||
}
|
||
|
||
this.callbackStarted = now();
|
||
|
||
if ( config.notrycatch ) {
|
||
promise = this.callback.call( this.testEnvironment, this.assert );
|
||
this.resolvePromise( promise );
|
||
return;
|
||
}
|
||
|
||
try {
|
||
promise = this.callback.call( this.testEnvironment, this.assert );
|
||
this.resolvePromise( promise );
|
||
} catch ( e ) {
|
||
this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
|
||
this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
|
||
|
||
// else next test will carry the responsibility
|
||
saveGlobal();
|
||
|
||
// Restart the tests if they're blocking
|
||
if ( config.blocking ) {
|
||
QUnit.start();
|
||
}
|
||
}
|
||
},
|
||
|
||
after: function() {
|
||
checkPollution();
|
||
},
|
||
|
||
queueHook: function( hook, hookName ) {
|
||
var promise,
|
||
test = this;
|
||
return function runHook() {
|
||
config.current = test;
|
||
if ( config.notrycatch ) {
|
||
promise = hook.call( test.testEnvironment, test.assert );
|
||
test.resolvePromise( promise, hookName );
|
||
return;
|
||
}
|
||
try {
|
||
promise = hook.call( test.testEnvironment, test.assert );
|
||
test.resolvePromise( promise, hookName );
|
||
} catch ( error ) {
|
||
test.pushFailure( hookName + " failed on " + test.testName + ": " +
|
||
( error.message || error ), extractStacktrace( error, 0 ) );
|
||
}
|
||
};
|
||
},
|
||
|
||
// Currently only used for module level hooks, can be used to add global level ones
|
||
hooks: function( handler ) {
|
||
var hooks = [];
|
||
|
||
// Hooks are ignored on skipped tests
|
||
if ( this.skip ) {
|
||
return hooks;
|
||
}
|
||
|
||
if ( this.module.testEnvironment &&
|
||
QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
|
||
hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
|
||
}
|
||
|
||
return hooks;
|
||
},
|
||
|
||
finish: function() {
|
||
config.current = this;
|
||
if ( config.requireExpects && this.expected === null ) {
|
||
this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
|
||
"not called.", this.stack );
|
||
} else if ( this.expected !== null && this.expected !== this.assertions.length ) {
|
||
this.pushFailure( "Expected " + this.expected + " assertions, but " +
|
||
this.assertions.length + " were run", this.stack );
|
||
} else if ( this.expected === null && !this.assertions.length ) {
|
||
this.pushFailure( "Expected at least one assertion, but none were run - call " +
|
||
"expect(0) to accept zero assertions.", this.stack );
|
||
}
|
||
|
||
var i,
|
||
bad = 0;
|
||
|
||
this.runtime = now() - this.started;
|
||
config.stats.all += this.assertions.length;
|
||
config.moduleStats.all += this.assertions.length;
|
||
|
||
for ( i = 0; i < this.assertions.length; i++ ) {
|
||
if ( !this.assertions[ i ].result ) {
|
||
bad++;
|
||
config.stats.bad++;
|
||
config.moduleStats.bad++;
|
||
}
|
||
}
|
||
|
||
runLoggingCallbacks( "testDone", {
|
||
name: this.testName,
|
||
module: this.module.name,
|
||
skipped: !!this.skip,
|
||
failed: bad,
|
||
passed: this.assertions.length - bad,
|
||
total: this.assertions.length,
|
||
runtime: this.runtime,
|
||
|
||
// HTML Reporter use
|
||
assertions: this.assertions,
|
||
testId: this.testId,
|
||
|
||
// DEPRECATED: this property will be removed in 2.0.0, use runtime instead
|
||
duration: this.runtime
|
||
});
|
||
|
||
// QUnit.reset() is deprecated and will be replaced for a new
|
||
// fixture reset function on QUnit 2.0/2.1.
|
||
// It's still called here for backwards compatibility handling
|
||
QUnit.reset();
|
||
|
||
config.current = undefined;
|
||
},
|
||
|
||
queue: function() {
|
||
var bad,
|
||
test = this;
|
||
|
||
if ( !this.valid() ) {
|
||
return;
|
||
}
|
||
|
||
function run() {
|
||
|
||
// each of these can by async
|
||
synchronize([
|
||
function() {
|
||
test.before();
|
||
},
|
||
|
||
test.hooks( "beforeEach" ),
|
||
|
||
function() {
|
||
test.run();
|
||
},
|
||
|
||
test.hooks( "afterEach" ).reverse(),
|
||
|
||
function() {
|
||
test.after();
|
||
},
|
||
function() {
|
||
test.finish();
|
||
}
|
||
]);
|
||
}
|
||
|
||
// `bad` initialized at top of scope
|
||
// defer when previous test run passed, if storage is available
|
||
bad = QUnit.config.reorder && defined.sessionStorage &&
|
||
+sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
|
||
|
||
if ( bad ) {
|
||
run();
|
||
} else {
|
||
synchronize( run, true );
|
||
}
|
||
},
|
||
|
||
push: function( result, actual, expected, message ) {
|
||
var source,
|
||
details = {
|
||
module: this.module.name,
|
||
name: this.testName,
|
||
result: result,
|
||
message: message,
|
||
actual: actual,
|
||
expected: expected,
|
||
testId: this.testId,
|
||
runtime: now() - this.started
|
||
};
|
||
|
||
if ( !result ) {
|
||
source = sourceFromStacktrace();
|
||
|
||
if ( source ) {
|
||
details.source = source;
|
||
}
|
||
}
|
||
|
||
runLoggingCallbacks( "log", details );
|
||
|
||
this.assertions.push({
|
||
result: !!result,
|
||
message: message
|
||
});
|
||
},
|
||
|
||
pushFailure: function( message, source, actual ) {
|
||
if ( !this instanceof Test ) {
|
||
throw new Error( "pushFailure() assertion outside test context, was " +
|
||
sourceFromStacktrace( 2 ) );
|
||
}
|
||
|
||
var details = {
|
||
module: this.module.name,
|
||
name: this.testName,
|
||
result: false,
|
||
message: message || "error",
|
||
actual: actual || null,
|
||
testId: this.testId,
|
||
runtime: now() - this.started
|
||
};
|
||
|
||
if ( source ) {
|
||
details.source = source;
|
||
}
|
||
|
||
runLoggingCallbacks( "log", details );
|
||
|
||
this.assertions.push({
|
||
result: false,
|
||
message: message
|
||
});
|
||
},
|
||
|
||
resolvePromise: function( promise, phase ) {
|
||
var then, message,
|
||
test = this;
|
||
if ( promise != null ) {
|
||
then = promise.then;
|
||
if ( QUnit.objectType( then ) === "function" ) {
|
||
QUnit.stop();
|
||
then.call(
|
||
promise,
|
||
QUnit.start,
|
||
function( error ) {
|
||
message = "Promise rejected " +
|
||
( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
|
||
" " + test.testName + ": " + ( error.message || error );
|
||
test.pushFailure( message, extractStacktrace( error, 0 ) );
|
||
|
||
// else next test will carry the responsibility
|
||
saveGlobal();
|
||
|
||
// Unblock
|
||
QUnit.start();
|
||
}
|
||
);
|
||
}
|
||
}
|
||
},
|
||
|
||
valid: function() {
|
||
var include,
|
||
filter = config.filter,
|
||
module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
|
||
fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
|
||
|
||
// Internally-generated tests are always valid
|
||
if ( this.callback && this.callback.validTest ) {
|
||
return true;
|
||
}
|
||
|
||
if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
|
Auch abrufbar als: Unified diff
JavaScript-Test-Framework auf Basis von QUnit