From 7aaea7fd37fb414f5d24172cedc22eb7548d2463 Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Sat, 16 Sep 2017 13:08:51 +0300 Subject: [PATCH 1/3] Merge more testing coverage from henrikingo fork * core_tests.js and navigation_tests.js have more tests * qunit_test_runner.html runs QUnit in a browser (without karma) and provides a generic iframe based platform to test different presentations. * copy qunit.js and syn.js into the repo. This allows qunit_test_runner.html to work without any dependency on node/npm. (Karma obviously does need them.) * Move jscs cli option to .jscsrc instead --- .jscsrc | 5 + karma.conf.js | 37 +- package.json | 4 +- qunit_test_runner.html | 26 + test/bootstrap.js | 33 +- test/core_tests.js | 378 ++- test/core_tests_presentation.html | 24 + test/helpers.js | 110 + test/navigation_tests.js | 169 ++ test/qunit/qunit.css | 301 +++ test/qunit/qunit.js | 4158 +++++++++++++++++++++++++++++ test/syn/syn.js | 2422 +++++++++++++++++ 12 files changed, 7511 insertions(+), 156 deletions(-) create mode 100644 .jscsrc create mode 100644 qunit_test_runner.html create mode 100644 test/core_tests_presentation.html create mode 100644 test/helpers.js create mode 100644 test/navigation_tests.js create mode 100644 test/qunit/qunit.css create mode 100644 test/qunit/qunit.js create mode 100644 test/syn/syn.js diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..576f754 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,5 @@ +{ + "preset": "jquery", + // Since we check quotemarks already in jshint, this can be turned off + "validateQuoteMarks": false +} diff --git a/karma.conf.js b/karma.conf.js index 4844c94..a99351a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -7,11 +7,25 @@ module.exports = function( config ) { // Frameworks to use frameworks: [ "qunit" ], + proxies : { + '/test/' : '/base/test/', + '/js/' : '/base/js/' + }, + // List of files / patterns to load in the browser files: [ - "test/bootstrap.js", - "js/impress.js", - "test/core_tests.js" + // The QUnit tests + "test/helpers.js", + "test/core_tests.js", + "test/navigation_tests.js", + // Presentation files, for the iframe + //"test/core_tests_presentation.html" + //{pattern: "test/core_tests_presentation.html", watched: true, served: true, included: false} + {pattern: "test/*.html", watched: true, served: true, included: false}, + {pattern: "test/plugins/*/*.html", watched: true, served: true, included: false}, + // JS files for iframe + {pattern: "js/impress.js", watched: true, served: true, included: false}, + {pattern: "test/syn/syn.js", watched: true, served: true, included: false} ], // List of files to exclude @@ -43,11 +57,22 @@ module.exports = function( config ) { // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) // - PhantomJS // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) - browsers: [ "Chrome" ], + //browsers: [ "Chrome" ], + //browsers: [ "Firefox" ], + browsers: [ "Chrome", "Firefox" ], - // If browser does not capture in given timeout [ms], kill it - captureTimeout: 60000, + client: { + clearContext: false, + qunit: { + showUI: true, + testTimeout: 120*1000 + } + }, + // If browser does not capture, or produce output, in given timeout [ms], kill it + captureTimeout: 60*1000, + browserNoActivityTimeout: 60*1000, + // Continuous Integration mode // if true, it capture browsers, run tests and exit singleRun: false diff --git a/package.json b/package.json index 34c40a7..982a5c8 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,20 @@ "url": "https://github.com/bartaz/impress.js/issues" }, "scripts": { - "lint": "jshint js/impress.js *.js test/bootstrap.js && jscs js/impress.js *.js test/bootstrap.js --preset=jquery", + "lint": "jshint js/impress.js test/*.js && jscs js/impress.js test/*.js", "test": "karma start --single-run", "test:dev": "karma start", "test:sauce": "karma start karma.conf-sauce.js" }, "devDependencies": { "chrome": "0.1.0", + "firefox": "*", "jscs": "2.11.0", "jshint": "2.9.1", "karma": "0.13.22", "karma-chrome-launcher": "1.0.1", "karma-cli": "1.0.0", + "karma-firefox-launcher": "~0.1", "karma-qunit": "1.0.0", "karma-sauce-launcher": "1.0.0", "qunitjs": "2.0.0-rc1" diff --git a/qunit_test_runner.html b/qunit_test_runner.html new file mode 100644 index 0000000..cc316bf --- /dev/null +++ b/qunit_test_runner.html @@ -0,0 +1,26 @@ + + + + + + QUnit tests for impress.js + + + +
+ +
+ + + + + + + + + + + diff --git a/test/bootstrap.js b/test/bootstrap.js index 6a315e8..774d62f 100644 --- a/test/bootstrap.js +++ b/test/bootstrap.js @@ -1,12 +1,25 @@ /*jshint browser:true */ -var root = document.createElement( "div" ); -root.innerHTML = [ - "
", - "
First slide
", - "
Second slide
", - "
Third slide
", - "
Fourth slide
", - "
" -].join( "" ); -document.body.appendChild( root ); +// TODO: This is the bootstrap file for *karma*. Poorly named (since karma is +// only one option, in this repo) but keeping the same name now to avoid +// unnecessary deviation with upstream. +// If you just want to run the tests locally, you can open test/index.html in Firefox. + +// That's annoying: karma-qunit doesn't provide the qunit-fixture element +// https://github.com/karma-runner/karma-qunit/issues/18 + +// This file contains so much HTML, that we will just respectfully disagree about js +/* jshint quotmark:single */ +/* global document */ + +var fix = document.createElement( 'div' ); +fix.id = 'qunit-fixture'; +fix.innerHTML = [ +'\n', +' ' +].join( '' ); diff --git a/test/core_tests.js b/test/core_tests.js index 12d2378..5a61e74 100644 --- a/test/core_tests.js +++ b/test/core_tests.js @@ -1,166 +1,266 @@ -(function() { - var registerEventListener = function( target, eventType, callback ) { - target.addEventListener( eventType, callback ); - return function removeRegisteredEventListener() { - target.removeEventListener( eventType, callback ); +/* + * Copyright 2016 Henrik Ingo (@henrikingo) + * + * Released under the MIT license. See LICENSE file. + */ + +/* global document, console, setTimeout, loadIframe, initPresentation, _impressSupported, QUnit */ + +QUnit.module( "Core Tests" ); + +QUnit.test( "Initialize Impress.js", function( assert ) { + console.log( "Begin init() test" ); + + // Init triggers impress:init and impress:stepenter events, which we want to catch. + var doneInit = assert.async(); + var doneStepEnter = assert.async(); + var doneSync = assert.async(); + + loadIframe( "test/core_tests_presentation.html", assert, function() { + var iframe = document.getElementById( "presentation-iframe" ); + var iframeDoc = iframe.contentDocument; + var iframeWin = iframe.contentWindow; + var root = iframeDoc.querySelector( "div#impress" ); + + // Catch events triggered by init() + var assertInit = function() { + assert.ok( true, "impress:init event triggered." ); + + var canvas = iframeDoc.querySelector( "div#impress > div" ); + + // Delay and duration don't become set before the first transition actually happened + assert.equal( canvas.style.transitionDelay, + "0ms", + "canvas.style.transitionDelay initialized correctly" ); + assert.equal( canvas.style.transitionDuration, + "0ms", + "canvas.style.transitionDuration initialized correctly" ); + + doneInit(); + console.log( "End init() test (async)" ); }; - }; - QUnit.begin(function() { - impress().init(); - }); + var assertInitWrapper = function() { + setTimeout( function() { assertInit(); }, 10 ); + }; + root.addEventListener( "impress:init", assertInitWrapper ); - QUnit.module( "Initialization" ); + root.addEventListener( "impress:stepenter", function( event ) { + assert.ok( true, "impress:stepenter event triggered." ); + var step1 = iframeDoc.querySelector( "div#step-1" ); + assert.equal( event.target, step1, + event.target.id + " triggered impress:stepenter event." ); + doneStepEnter(); + } ); - QUnit.test( "Global Scope", function( assert ) { - assert.expect( 1 ); - assert.ok( impress, "impress declared in global scope" ); - }); + // Synchronous code and assertions + assert.ok( iframeWin.impress, + "impress declared in global scope" ); + assert.strictEqual( iframeWin.impress().init(), undefined, + "impress().init() called." ); + assert.strictEqual( iframeWin.impress().init(), undefined, + "It's ok to call impress().init() a second time, it's a no-op." ); - QUnit.test( "Multiple API instantiation", function( assert ) { - assert.expect( 0 ); - impress().init(); - }); + // The asserts below are true immediately after impress().init() returns. + // Therefore we test them here, not in an event handler. + var notSupportedClass = iframeDoc.body.classList.contains( "impress-not-supported" ); + var yesSupportedClass = iframeDoc.body.classList.contains( "impress-supported" ); + if ( !_impressSupported() ) { + assert.ok( notSupportedClass, + "body.impress-not-supported class still there." ); + assert.ok( !yesSupportedClass, + "body.impress-supported class was NOT added." ); + } else { + assert.ok( !notSupportedClass, + "body.impress-not-supported class was removed." ); + assert.ok( yesSupportedClass, + "body.impress-supported class was added." ); - QUnit.test( "Support Markup", function( assert ) { - assert.expect( 4 ); + assert.ok( !iframeDoc.body.classList.contains( "impress-disabled" ), + "body.impress-disabled is removed." ); + assert.ok( iframeDoc.body.classList.contains( "impress-enabled" ), + "body.impress-enabled is added." ); - var impressNotSupported = document.body.classList.contains( "impress-not-supported" ); - var impressSupported = document.body.classList.contains( "impress-supported" ); - assert.ok( impressSupported, "Have class .impress-supported" ); - assert.notOk( impressNotSupported, "Don't have class .impress-not-supported" ); + var canvas = iframeDoc.querySelector( "div#impress > div" ); + assert.ok( !canvas.classList.contains( "step" ) && canvas.id === "", + "Additional 'canvas' div inserted between div#impress root and steps." ); + assert.equal( canvas.style.transform, + "rotateZ(0deg) rotateY(0deg) rotateX(0deg) translate3d(1000px, 0px, 0px)", + "canvas.style.transform initialized correctly" ); + assert.equal( canvas.style.transformOrigin, + "left top 0px", + "canvas.style.transformOrigin initialized correctly" ); + assert.equal( canvas.style.transformStyle, + "preserve-3d", + "canvas.style.transformStyle initialized correctly" ); + assert.equal( canvas.style.transitionProperty, + "all", + "canvas.style.transitionProperty initialized correctly" ); + assert.equal( canvas.style.transitionTimingFunction, + "ease-in-out", + "canvas.style.transitionTimingFunction initialized correctly" ); - var impressDisabled = document.body.classList.contains( "impress-disabled" ); - var impressEnabled = document.body.classList.contains( "impress-enabled" ); - assert.ok( impressEnabled, "Have class .impress-enabled" ); - assert.notOk( impressDisabled, "Don't have class .impress-disabled" ); - }); + assert.equal( iframeDoc.documentElement.style.height, + "100%", + "documentElement.style.height is 100%" ); - QUnit.test( "Attributes", function( assert ) { - assert.expect( 10 ); + // Steps initialization + var step1 = iframeDoc.querySelector( "div#step-1" ); + assert.equal( step1.style.position, + "absolute", + "Step position is 'absolute'." ); - var actual, expected; - var root = document.querySelector( "#impress" ); - var canvas = document.querySelector( "div#impress > div" ); + assert.ok( step1.classList.contains( "active" ), + "Step 1 has active css class." ); - var canvasIsNotAStep = !canvas.classList.contains("step") && canvas.id === ""; - assert.ok( canvasIsNotAStep, "Canvas do not have step element data" ); + } + doneSync(); + console.log( "End init() test (sync)" ); + } ); // LoadIframe() - actual = canvas.style.webkitTransform || canvas.style.transform; - expected = "rotateZ(0deg) rotateY(0deg) rotateX(0deg) translate3d(1000px, 0px, 0px)"; - assert.strictEqual( actual, expected, "canvas.style.transform initialized correctly" ); +} ); - // Normalize result for IE 11 and Safari. - actual = canvas.style.webkitTransformOrigin || canvas.style.transformOrigin; - expected = "left top 0px"; +// Note: Here we focus on testing the core functionality of moving between +// steps, the css classes set and unset, and events triggered. +// TODO: more complex animations and check position, transitions, delays, etc... +QUnit.test( "Impress Core API", function( assert ) { + console.log( "Begin core api test" ); + var done = assert.async(); + loadIframe( "test/core_tests_presentation.html", assert, function() { + initPresentation( assert, function() { + var iframe = document.getElementById( "presentation-iframe" ); + var iframeDoc = iframe.contentDocument; + var iframeWin = iframe.contentWindow; - if ( actual === "left top" || actual === "0% 0%" ) { - actual = expected; - } - assert.strictEqual( actual, expected, "canvas.style.transformOrigin initialized correctly" ); + // Impress.js itself uses event listeners to manipulate most CSS classes. + // Wait a short while before checking, to avoid race. + // (See assertStepEnterWrapper and assertStepLeaveWrapper.) + var wait = 5; // Milliseconds - actual = canvas.style.webkitTransformStyle || canvas.style.transformStyle; - expected = "preserve-3d"; - assert.strictEqual( actual, expected, "canvas.style.transformStyle initialized correctly" ); + var step1 = iframeDoc.querySelector( "div#step-1" ); + var step2 = iframeDoc.querySelector( "div#step-2" ); + var step3 = iframeDoc.querySelector( "div#step-3" ); + var step4 = iframeDoc.querySelector( "div#fourth" ); + var root = iframeDoc.querySelector( "div#impress" ); - actual = canvas.style.transitionDelay; - expected = "0ms"; - assert.strictEqual( actual, expected, "canvas.style.transitionDelay initialized correctly" ); + // On impress:stepenter, we do some assertions on the "entered" object. + // On impress:stepleave, we do some assertions on the "left" object. + // Finally we call next() to initialize the next transition, and it starts all over again. + var i = 0; + var sequence = [ { left: step1, + entered: step2, + next: function() { return iframeWin.impress().goto( 2 ); }, + text: "goto() called and returns ok (2->3)" }, + { left: step2, + entered: step3, + next: function() { return iframeWin.impress().goto( "fourth" ); }, + text: "goto() called and returns ok (3->4)" }, + { left: step3, + entered: step4, + next: function() { return iframeWin.impress().next(); }, + text: "next() wraps around to first step (4->1)" }, + { left: step4, + entered: step1, + next: function() { return iframeWin.impress().prev(); }, + text: "prev() wraps around to last step (1->4)" }, + { left: step1, + entered: step4, + next: function() { return iframeWin.impress().prev(); }, + text: "prev() called and returns ok (4->3)" }, + { left: step4, + entered: step3, + next: function() { return iframeWin.impress().goto( 0 ); }, + text: "End of test suite, return to first step with goto(0)." }, + { left: step3, + entered: step1, + next: false } // False = end of sequence + ]; - actual = canvas.style.transitionDuration; - expected = "0ms"; - assert.strictEqual( actual, expected, "canvas.style.transitionDuration initialized correctly" ); + // When both assertStepEnter and assertStepLeave are done, we can go to next step in sequence. + var readyCount = 0; + var readyForNext = function() { + readyCount++; + if ( readyCount % 2 === 0 ) { + if ( sequence[ i ].next ) { + assert.ok( sequence[ i ].next(), sequence[ i ].text ); + i++; + assertImmediately(); + } else { + done(); + console.log( "End core api test" ); + } + } + }; - actual = canvas.style.transitionProperty; - expected = "all"; - assert.strictEqual( actual, expected, "canvas.style.transitionProperty initialized correctly" ); + // Things to check on impress:stepenter event -----------------------------// + var assertStepEnter = function( event ) { + assert.equal( event.target, sequence[ i ].entered, + event.target.id + " triggered impress:stepenter event." ); + assert.ok( event.target.classList.contains( "present" ), + event.target.id + " set present css class." ); + assert.ok( !event.target.classList.contains( "future" ), + event.target.id + " unset future css class." ); + assert.ok( !event.target.classList.contains( "past" ), + event.target.id + " unset past css class." ); + assert.equal( "#/" + event.target.id, iframeWin.location.hash, + "Hash is " + "#/" + event.target.id ); - actual = canvas.style.transitionTimingFunction; - expected = "ease-in-out"; - assert.strictEqual( actual, expected, "canvas.style.transitionTimingFunction initialized correctly" ); + // Just by way of comparison, check transitionDuration again, in a non-init transition + var canvas = iframeDoc.querySelector( "div#impress > div" ); + assert.equal( canvas.style.transitionDelay, + "0ms", + "canvas.style.transitionDelay set correctly" ); + assert.equal( canvas.style.transitionDuration, + "1000ms", + "canvas.style.transitionDuration set correctly" ); - actual = root.style.perspective; - expected = ""; - assert.notStrictEqual( actual, expected, "root.style.perspective should be set explicitly for IE 11" ); + readyForNext(); + }; - actual = document.documentElement.style.height; - expected = "100%"; - assert.strictEqual( actual, expected, "documentElement.style.height is 100%" ); - }); + var assertStepEnterWrapper = function( event ) { + setTimeout( function() { assertStepEnter( event ); }, wait ); + }; + root.addEventListener( "impress:stepenter", assertStepEnterWrapper ); - QUnit.test( "Steps", function( assert ) { - assert.expect( 2 ); + // Things to check on impress:stepleave event -----------------------------// + var assertStepLeave = function( event ) { + assert.equal( event.target, sequence[ i ].left, + event.target.id + " triggered impress:stepleave event." ); + assert.ok( !event.target.classList.contains( "present" ), + event.target.id + " unset present css class." ); + assert.ok( !event.target.classList.contains( "future" ), + event.target.id + " unset future css class." ); + assert.ok( event.target.classList.contains( "past" ), + event.target.id + " set past css class." ); + readyForNext(); + }; - var actual, expected; - var step1 = document.querySelector( "div#step-1" ); + var assertStepLeaveWrapper = function( event ) { + setTimeout( function() { assertStepLeave( event ); }, wait ); + }; + root.addEventListener( "impress:stepleave", assertStepLeaveWrapper ); - actual = step1.style.position; - expected = "absolute"; - assert.strictEqual( actual, expected, "Step position is 'absolute'" ); + // Things to check immediately after impress().goto() ---------------------------// + var assertImmediately = function() { + assert.ok( sequence[ i ].entered.classList.contains( "active" ), + sequence[ i ].entered.id + " set active css class." ); + assert.ok( !sequence[ i ].left.classList.contains( "active" ), + sequence[ i ].left.id + " unset active css class." ); + }; - assert.ok( step1.classList.contains( "active" ), "Step 1 has active css class." ); - }); + // Done with setup. Start testing! -----------------------------------------------// + // Do no-op tests first, then trigger the sequence of transitions we setup above. // - QUnit.module( "Core API" ); + assert.strictEqual( iframeWin.impress().goto( iframeDoc.querySelector( "div#impress" ) ), + false, + "goto() to a non-step element fails, as it should." ); + assert.strictEqual( iframeWin.impress().goto(), + false, + "goto() fails, as it should." ); - QUnit.test( ".next()", function( assert ) { - assert.expect( 2 ); - - var root = document.querySelector( "div#impress" ); - var step1 = document.querySelector( "div#step-1" ); - var step2 = document.querySelector( "div#step-2" ); - var done = assert.async(); - - impress().next(); - - assert.ok( step2.classList.contains( "active" ), step2.id + " add active css class." ); - assert.notOk( step1.classList.contains( "active" ), step1.id + " remove active css class." ); - - // Reset to original state - var removeStepEnterEvent = registerEventListener( root, "impress:stepenter", function() { - removeStepEnterEvent(); - done(); - }); - impress().goto( step1 ); - }); - - QUnit.test( "impress:stepenter event", function( assert ) { - assert.expect( 4 ); - - var actual, expected; - var root = document.querySelector( "div#impress" ); - var step1 = document.querySelector( "div#step-1" ); - var step2 = document.querySelector( "div#step-2" ); - var done = assert.async(); - - var removeTestEvent = registerEventListener( root, "impress:stepenter", function( event ) { - actual = event.target; - expected = step2; - assert.strictEqual( actual, expected, "Triggered event for the second step" ); - - assert.ok( step2.classList.contains( "present" ), event.target.id + " add present css class" ); - assert.notOk( step2.classList.contains( "future" ), event.target.id + " remove future css class" ); - assert.notOk( step2.classList.contains( "past" ), event.target.id + " remove active css class." ); - - removeTestEvent(); - - // Reset to original state - var removeCleanupEvent = registerEventListener( root, "impress:stepenter", function() { - removeCleanupEvent(); - done(); - }); - - impress().goto( step1 ); - }); - - impress().next(); - }); - - QUnit.done(function( details ) { - // Impress.js will set the hash part of the url, we want to unset it when finished - // Otherwise a refresh of browser page would not start tests from the last step step - window.location.hash = ""; - // Add back vertical scrollbar so we can read results if there were failures. - document.body.style.overflow = 'auto'; - }); -}()); + // This starts executing the sequence above + assert.ok( iframeWin.impress().next(), + "next() called and returns ok (1->2)" ); + } ); // InitPresentation() + } ); // LoadIframe() +} ); diff --git a/test/core_tests_presentation.html b/test/core_tests_presentation.html new file mode 100644 index 0000000..52152ef --- /dev/null +++ b/test/core_tests_presentation.html @@ -0,0 +1,24 @@ + + + + + + The presentation steps used in an iframe in core_tests.js + + + +
+
First slide
+ + +
Fourth slide
+
+ + + + + + diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..746facd --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,110 @@ +// This file contains so much HTML, that we will just respectfully disagree about js +/* jshint quotmark:single */ +/* global document, console, setTimeout, navigator */ +/* exported loadIframe, initPresentation, _impressSupported */ + +var loadIframe = function( src, assert, callback ) { + console.log( 'Begin loadIframe' ); + + // When running in Karma, the #qunit-fixture appears from somewhere and we can't set its + // contents in advance. + var fix = document.getElementById( 'qunit-fixture' ); + fix.innerHTML = [ + '\n', + ' ' + ].join( '' ); + + var iframe = document.getElementById( 'presentation-iframe' ); + + var onLoad = function() { + assert.ok( true, + 'Presentation loaded. iframe.src = ' + iframe.src ); + try { + assert.ok( iframe.contentDocument, + 'Verifying that tests can access the presentation inside the iframe. ' + + 'Note: On Firefox this fails when using paths with "../" parts for the iframe.' ); + } + catch ( err ) { + assert.ok( false, + 'Error when trying to access presentation in iframe. Note: When using Chrome with ' + + 'local files (file:///) this will fail with SecurityError. ' + + 'You can however use Chrome over Karma.' ); + } + console.log( 'End loadIframe' ); + callback(); + }; + + // FIXME: Seems to be some race in loading js files inside the iframe (in CircleCI). + // The error that happens is that window.impress isn't set yet, even if onLoad event triggered. + // Needs more investigation. + var onLoadWrapper = function( event ) { + setTimeout( function() { onLoad( event ); }, 1000 ); + }; + iframe.addEventListener( 'load', onLoadWrapper ); + + assert.ok( true, + 'Setting iframe.src = ' + src ); + iframe.src = src; +}; + +var initPresentation = function( assert, callback ) { + console.log( 'Begin initPresentation' ); + var iframe = document.getElementById( 'presentation-iframe' ); + var iframeDoc = iframe.contentDocument; + var iframeWin = iframe.contentWindow; + + // Impress:stepenter is the last event triggered in init(), so we wait for that. + var waitForStepEnter = function( event ) { + assert.ok( true, 'impress (' + event.target.id + ') is now initialized.' ); + iframeDoc.removeEventListener( 'impress:stepenter', waitForStepEnterWrapper ); + console.log( 'End initPresentation' ); + callback(); + }; + + // Unfortunately, impress.js uses the impress:stepenter event internally to + // do some things related to entering a step. This causes a race condition when + // we listen for the same event and expect it to be done with everything. + // We wait 5 ms to resolve the race condition, then it's safe to start testing. + var waitForStepEnterWrapper = function( event ) { + setTimeout( function() { waitForStepEnter( event ); }, 5 ); + }; + iframeDoc.addEventListener( 'impress:stepenter', waitForStepEnterWrapper ); + + assert.strictEqual( iframeWin.impress().init(), undefined, 'Initializing impress.' ); +}; + +// Helper function to determine whether this browser is supported by +// impress.js or not. Copied from impress.js itself. +var _impressSupported = function() { + var pfx = ( function() { + var style = document.createElement( 'dummy' ).style, + prefixes = 'Webkit Moz O ms Khtml'.split( ' ' ), + memory = {}; + return function( prop ) { + if ( typeof memory[ prop ] === 'undefined' ) { + var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), + props = ( prop + ' ' + prefixes.join( ucProp + ' ' ) + ucProp ).split( ' ' ); + memory[ prop ] = null; + for ( var i in props ) { + if ( style[ props[ i ] ] !== undefined ) { + memory[ prop ] = props[ i ]; + break; + } + } + } + return memory[ prop ]; + }; + } )(); + + var ua = navigator.userAgent.toLowerCase(); + return ( pfx( 'perspective' ) !== null ) && + ( document.body.classList ) && + ( document.body.dataset ) && + ( ua.search( /(iphone)|(ipod)|(android)/ ) === -1 ); +}; + diff --git a/test/navigation_tests.js b/test/navigation_tests.js new file mode 100644 index 0000000..fd4a05b --- /dev/null +++ b/test/navigation_tests.js @@ -0,0 +1,169 @@ +/* + * Copyright 2016 Henrik Ingo (@henrikingo) + * + * Released under the MIT license. See LICENSE file. + */ +/* global QUnit, loadIframe, initPresentation, document, window */ + +QUnit.module( "Navigation plugin" ); + +QUnit.test( "Navigation Plugin", function( assert ) { + window.console.log( "Begin navigation plugin" ); + var done = assert.async(); + + loadIframe( "test/core_tests_presentation.html", assert, function() { + initPresentation( assert, function() { + var iframe = document.getElementById( "presentation-iframe" ); + var iframeDoc = iframe.contentDocument; + var iframeWin = iframe.contentWindow; + + var wait = 5; // Milliseconds + + var step1 = iframeDoc.querySelector( "div#step-1" ); + var step2 = iframeDoc.querySelector( "div#step-2" ); + var step3 = iframeDoc.querySelector( "div#step-3" ); + var step4 = iframeDoc.querySelector( "div#fourth" ); + var root = iframeDoc.querySelector( "div#impress" ); + + var i = 0; + var sequence = [ { left: step1, + entered: step2, + next: function() { return iframeWin.syn.type( "bodyid", " " ); }, + text: "space (2->3)" }, + { left: step2, + entered: step3, + next: function() { return iframeWin.syn.type( "bodyid", "[right]" ); }, + text: "[right] (3->4)" }, + { left: step3, + entered: step4, + next: function() { return iframeWin.syn.type( "bodyid", "\t" ); }, + text: "tab (4->1)" }, + { left: step4, + entered: step1, + next: function() { return iframeWin.syn.type( "bodyid", "[down]" ); }, + text: "[down] (1->2)" }, + { left: step1, + entered: step2, + next: function() { return iframeWin.syn.type( "bodyid", "[page-down]" ); }, + text: "[page-down] (2->3)" }, + { left: step2, + entered: step3, + next: function() { return iframeWin.syn.type( "bodyid", "[page-up]" ); }, + text: "[page-up] (3->2)" }, + { left: step3, + entered: step2, + next: function() { return iframeWin.syn.type( "bodyid", "[left]" ); }, + text: "[left] (2->1)" }, + { left: step2, + entered: step1, + next: function() { return iframeWin.syn.type( "bodyid", "[up]" ); }, + text: "[up] (1->4)" }, + { left: step1, + entered: step4, + next: function() { return iframeWin.syn.click( "step-2", {} ); }, + text: "click on 2 (4->2)" }, + { left: step4, + entered: step2, + next: function() { return iframeWin.syn.click( "linktofourth", {} ); }, + text: "click on link with href to id=fourth (2->4)" }, + { left: step2, + entered: step4, + next: function() { return iframeWin.impress().goto( 0 ); }, + text: "Return to first step with goto(0)." }, + { left: step4, + entered: step1, + next: false } + ]; + + var readyCount = 0; + var readyForNext = function() { + readyCount++; + if ( readyCount % 2 === 0 ) { + if ( sequence[ i ].next ) { + assert.ok( sequence[ i ].next(), sequence[ i ].text ); + i++; + } else { + window.console.log( "End navigation plugin" ); + done(); + } + } + }; + + // Things to check on impress:stepenter event -----------------------------// + var assertStepEnter = function( event ) { + assert.equal( event.target, sequence[ i ].entered, + event.target.id + " triggered impress:stepenter event." ); + readyForNext(); + }; + + var assertStepEnterWrapper = function( event ) { + window.setTimeout( function() { assertStepEnter( event ); }, wait ); + }; + root.addEventListener( "impress:stepenter", assertStepEnterWrapper ); + + // Things to check on impress:stepleave event -----------------------------// + var assertStepLeave = function( event ) { + assert.equal( event.target, sequence[ i ].left, + event.target.id + " triggered impress:stepleave event." ); + readyForNext(); + }; + + var assertStepLeaveWrapper = function( event ) { + window.setTimeout( function() { assertStepLeave( event ); }, wait ); + }; + root.addEventListener( "impress:stepleave", assertStepLeaveWrapper ); + + assert.ok( iframeWin.impress().next(), "next() called and returns ok (1->2)" ); + } ); // InitPresentation() + } ); // LoadIframe() +} ); + +QUnit.test( "Navigation Plugin - No-op tests", function( assert ) { + window.console.log( "Begin navigation no-op" ); + var done = assert.async(); + + loadIframe( "test/core_tests_presentation.html", assert, function() { + initPresentation( assert, function() { + var iframe = document.getElementById( "presentation-iframe" ); + var iframeDoc = iframe.contentDocument; + var iframeWin = iframe.contentWindow; + + var wait = 5; // Milliseconds + + var root = iframeDoc.querySelector( "div#impress" ); + + // This should never happen -----------------------------// + var assertStepEnter = function( event ) { + assert.ok( false, + event.target.id + " triggered impress:stepenter event." ); + }; + + var assertStepEnterWrapper = function( event ) { + window.setTimeout( function() { assertStepEnter( event ); }, wait ); + }; + root.addEventListener( "impress:stepenter", assertStepEnterWrapper ); + + // This should never happen -----------------------------// + var assertStepLeave = function( event ) { + assert.ok( false, + event.target.id + " triggered impress:stepleave event." ); + }; + + var assertStepLeaveWrapper = function( event ) { + window.setTimeout( function() { assertStepLeave( event ); }, wait ); + }; + root.addEventListener( "impress:stepleave", assertStepLeaveWrapper ); + + // These are no-op actions, we're already in step-1 -----------------------// + assert.ok( iframeWin.syn.click( "step-1", {} ), + "Click on step that is currently active, should do nothing." ); + assert.ok( iframeWin.syn.click( "linktofirst", {} ), + "Click on link pointing to step that is currently active, should do nothing." ); + + // After delay, if no event triggers are called. We're done. + window.setTimeout( function() { + window.console.log( "End navigation no-op" ); done(); + }, 3000 ); + } ); // InitPresentation() + } ); // LoadIframe() +} ); diff --git a/test/qunit/qunit.css b/test/qunit/qunit.css new file mode 100644 index 0000000..a59e282 --- /dev/null +++ b/test/qunit/qunit.css @@ -0,0 +1,301 @@ +/*! + * QUnit 1.20.0 + * http://qunitjs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-27T17:53Z + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #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-filteredTest { + padding: 0.5em 1em 0.5em 1em; + background-color: #F4FF77; + color: #366097; +} + +#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 { + visibility: hidden; + position: absolute; + width: 0; + height: 0; + padding: 0; + border: 0; + margin: 0; +} + +#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 p a { + padding: 0.25em; + color: #6B6464; +} +#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-source { + margin: 0.6em 0 0.3em; +} + +.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; +} diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js new file mode 100644 index 0000000..904943f --- /dev/null +++ b/test/qunit/qunit.js @@ -0,0 +1,4158 @@ +/*! + * QUnit 1.20.0 + * http://qunitjs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-27T17:53Z + */ + +(function( global ) { + +var QUnit = {}; + +var Date = global.Date; +var now = Date.now || function() { + return new Date().getTime(); +}; + +var setTimeout = global.setTimeout; +var clearTimeout = global.clearTimeout; + +// Store a local window from the global to allow direct references. +var window = global.window; + +var defined = { + document: window && window.document !== undefined, + setTimeout: setTimeout !== undefined, + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch ( e ) { + return false; + } + }() ) +}; + +var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); +var globalStartCalled = false; +var runStarted = false; + +var toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty; + +// 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; +} + +// 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; +} + +/** + * 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). + */ +function objectValues ( 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; +} + +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 + // This block runs on every environment, so `global` is being used instead of `window` + // to avoid errors on node. + if ( prop !== "constructor" || a !== global ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +function objectType( 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 "Set": + case "Map": + case "Date": + case "RegExp": + case "Function": + case "Symbol": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } +} + +// Safe object type checking +function is( type, obj ) { + return QUnit.objectType( obj ) === type; +} + +var getUrlParams = function() { + var i, current; + var urlParams = {}; + var location = window.location; + var params = location.search.slice( 1 ).split( "&" ); + var length = params.length; + + 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 ]; + } + } + } + + return urlParams; +}; + +// Doesn't support IE6 to IE9, it will return undefined on these browsers +// 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.stack ) { + 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 ]; + + // Support: Safari <=6 only + } else if ( e.sourceURL ) { + + // 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 error = new Error(); + + // Support: Safari <=7 only, IE <=10 - 11 only + // Not all browsers generate the `stack` property for `new Error()`, see also #636 + if ( !error.stack ) { + try { + throw error; + } catch ( err ) { + error = err; + } + } + + return extractStacktrace( error, offset ); +} + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +var 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, + + // HTML Reporter: collapse every test except the first failing test + // If false, all failing tests will be expanded + collapse: true, + + // by default, scroll to top of the page when suite is done + scrolltop: true, + + // depth up-to which object will be dumped + maxDepth: 5, + + // 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 " + + "global object (`window` in Browsers). 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: [], + + // Stack of nested modules + moduleStack: [], + + // The first unnamed module + currentModule: { + name: "", + tests: [] + }, + + callbacks: {} +}; + +var urlParams = defined.document ? getUrlParams() : {}; + +// Push a loose unnamed module to the modules collection +config.modules.push( config.currentModule ); + +if ( urlParams.filter === true ) { + delete urlParams.filter; +} + +// String search anywhere in moduleName+testName +config.filter = urlParams.filter; + +config.testId = []; +if ( urlParams.testId ) { + // Ensure that urlParams.testId is an array + urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); + for (var i = 0; i < urlParams.testId.length; i++ ) { + config.testId.push( urlParams.testId[ i ] ); + } +} + +var loggingCallbacks = {}; + +// Register logging callbacks +function registerLoggingCallbacks( obj ) { + var i, l, key, + callbackNames = [ "begin", "done", "log", "testStart", "testDone", + "moduleStart", "moduleDone" ]; + + function registerLoggingCallback( key ) { + var loggingCallback = function( callback ) { + if ( 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 = callbackNames.length; i < l; i++ ) { + key = callbackNames[ i ]; + + // Initialize key collection of logging callback + if ( objectType( config.callbacks[ key ] ) === "undefined" ) { + config.callbacks[ key ] = []; + } + + obj[ key ] = registerLoggingCallback( key ); + } +} + +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 ( global.console && global.console.warn ) { + global.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/" + ); + } + } + } +} + +( function() { + if ( !defined.document ) { + return; + } + + // `onErrorFnPrev` initialized at top of scope + // Preserve other handlers + var 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; + }; +} )(); + +QUnit.urlParams = urlParams; + +// Figure out if we're running the tests from a server or not +QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); + +// Expose the current QUnit version +QUnit.version = "1.20.0"; + +extend( QUnit, { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment, executeNow ) { + var module, moduleFns; + var currentModule = config.currentModule; + + if ( arguments.length === 2 ) { + if ( testEnvironment instanceof Function ) { + executeNow = testEnvironment; + testEnvironment = undefined; + } + } + + // 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; + } + + module = createModule(); + + moduleFns = { + beforeEach: setHook( module, "beforeEach" ), + afterEach: setHook( module, "afterEach" ) + }; + + if ( executeNow instanceof Function ) { + config.moduleStack.push( module ); + setCurrentModule( module ); + executeNow.call( module.testEnvironment, moduleFns ); + config.moduleStack.pop(); + module = module.parentModule || currentModule; + } + + setCurrentModule( module ); + + function createModule() { + var parentModule = config.moduleStack.length ? + config.moduleStack.slice( -1 )[ 0 ] : null; + var moduleName = parentModule !== null ? + [ parentModule.name, name ].join( " > " ) : name; + var module = { + name: moduleName, + parentModule: parentModule, + tests: [] + }; + + var env = {}; + if ( parentModule ) { + extend( env, parentModule.testEnvironment ); + delete env.beforeEach; + delete env.afterEach; + } + extend( env, testEnvironment ); + module.testEnvironment = env; + + config.modules.push( module ); + return module; + } + + function setCurrentModule( module ) { + config.currentModule = module; + } + + }, + + // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. + asyncTest: asyncTest, + + test: test, + + skip: skip, + + only: only, + + // 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; + + // If semaphore is non-numeric, throw error + if ( isNaN( config.current.semaphore ) ) { + config.current.semaphore = 0; + + QUnit.pushFailure( + "Called start() with a non-numeric decrement.", + sourceFromStacktrace( 2 ) + ); + return; + } + + // 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, + + is: is, + + objectType: objectType, + + 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(); + } + }, + + stack: function( offset ) { + offset = ( offset || 0 ) + 2; + return sourceFromStacktrace( offset ); + } +}); + +registerLoggingCallbacks( QUnit ); + +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 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 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 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 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 + }); +} + +function setHook( module, hookName ) { + if ( module.testEnvironment === undefined ) { + module.testEnvironment = {}; + } + + return function( callback ) { + module.testEnvironment[ hookName ] = callback; + }; +} + +var focused = false; + +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; + + if ( this.module.testEnvironment ) { + delete this.module.testEnvironment.beforeEach; + delete this.module.testEnvironment.afterEach; + } + this.testEnvironment = extend( {}, this.module.testEnvironment ); + + 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 ) { + runTest( this ); + return; + } + + try { + runTest( this ); + } 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(); + } + } + + function runTest( test ) { + promise = test.callback.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise ); + } + }, + + after: function() { + checkPollution(); + }, + + queueHook: function( hook, hookName ) { + var promise, + test = this; + return function runHook() { + config.current = test; + if ( config.notrycatch ) { + callHook(); + return; + } + try { + callHook(); + } catch ( error ) { + test.pushFailure( hookName + " failed on " + test.testName + ": " + + ( error.message || error ), extractStacktrace( error, 0 ) ); + } + + function callHook() { + promise = hook.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise, hookName ); + } + }; + }, + + // Currently only used for module level hooks, can be used to add global level ones + hooks: function( handler ) { + var hooks = []; + + function processHooks( test, module ) { + if ( module.parentModule ) { + processHooks( test, module.parentModule ); + } + if ( module.testEnvironment && + QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { + hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); + } + } + + // Hooks are ignored on skipped tests + if ( !this.skip ) { + processHooks( this, this.module ); + } + 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, + + // Source of Test + source: this.stack, + + // 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 priority, + 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(); + } + ]); + } + + // Prioritize previously failed tests, detected from sessionStorage + priority = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); + + return synchronize( run, priority ); + }, + + push: function( result, actual, expected, message, negative ) { + var source, + details = { + module: this.module.name, + name: this.testName, + result: result, + message: message, + actual: actual, + expected: expected, + testId: this.testId, + negative: negative || false, + 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, + function() { 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 && config.filter.toLowerCase(), + module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), + fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); + + function testInModuleChain( testModule ) { + var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; + if ( testModuleName === module ) { + return true; + } else if ( testModule.parentModule ) { + return testInModuleChain( testModule.parentModule ); + } else { + return false; + } + } + + // 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 ) { + return false; + } + + if ( module && !testInModuleChain( this.module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; + } +}; + +// Resets the test setup. Useful for tests that modify the DOM. +/* +DEPRECATED: Use multiple tests instead of resetting inside a test. +Use testStart or testDone for custom cleanup. +This method will throw an error in 2.0, and will be removed in 2.1 +*/ +QUnit.reset = function() { + + // Return on non-browser environments + // This is necessary to not break on node tests + if ( !defined.document ) { + return; + } + + var fixture = defined.document && document.getElementById && + document.getElementById( "qunit-fixture" ); + + if ( fixture ) { + fixture.innerHTML = config.fixture; + } +}; + +QUnit.pushFailure = function() { + if ( !QUnit.config.current ) { + throw new Error( "pushFailure() assertion outside test context, in " + + sourceFromStacktrace( 2 ) ); + } + + // Gets current test obj + var currentTest = QUnit.config.current; + + return currentTest.pushFailure.apply( currentTest, arguments ); +}; + +// Based on Java's String.hashCode, a simple but not +// rigorously collision resistant hashing function +function generateHash( module, testName ) { + var hex, + i = 0, + hash = 0, + str = module + "\x1C" + testName, + len = str.length; + + for ( ; i < len; i++ ) { + hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); + hash |= 0; + } + + // Convert the possibly negative integer hash code into an 8 character hex string, which isn't + // strictly necessary but increases user understanding that the id is a SHA-like hash + hex = ( 0x100000000 + hash ).toString( 16 ); + if ( hex.length < 8 ) { + hex = "0000000" + hex; + } + + return hex.slice( -8 ); +} + +function synchronize( callback, priority ) { + var last = !priority; + + if ( QUnit.objectType( callback ) === "array" ) { + while ( callback.length ) { + synchronize( callback.shift() ); + } + return; + } + + if ( priority ) { + priorityFill( callback ); + } else { + config.queue.push( callback ); + } + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +// Place previously failed tests on a queue priority line, respecting the order they get assigned. +function priorityFill( callback ) { + var queue, prioritizedQueue; + + queue = config.queue.slice( priorityFill.pos ); + prioritizedQueue = config.queue.slice( 0, -config.queue.length + priorityFill.pos ); + + queue.unshift( callback ); + queue.unshift.apply( queue, prioritizedQueue ); + + config.queue = queue; + + priorityFill.pos += 1; +} +priorityFill.pos = 0; + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in global ) { + if ( hasOwn.call( global, 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( ", " ) ); + } +} + +// Will be exposed as QUnit.asyncTest +function asyncTest( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); +} + +// Will be exposed as QUnit.test +function test( testName, expected, callback, async ) { + if ( focused ) { return; } + + var newTest; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + newTest = new Test({ + testName: testName, + expected: expected, + async: async, + callback: callback + }); + + newTest.queue(); +} + +// Will be exposed as QUnit.skip +function skip( testName ) { + if ( focused ) { return; } + + var test = new Test({ + testName: testName, + skip: true + }); + + test.queue(); +} + +// Will be exposed as QUnit.only +function only( testName, expected, callback, async ) { + var newTest; + + if ( focused ) { return; } + + QUnit.config.queue.length = 0; + focused = true; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + newTest = new Test({ + testName: testName, + expected: expected, + async: async, + callback: callback + }); + + newTest.queue(); +} + +function Assert( testContext ) { + this.test = testContext; +} + +// Assert helpers +QUnit.assert = Assert.prototype = { + + // Specify the number of expected assertions to guarantee that failed test + // (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if ( arguments.length === 1 ) { + this.test.expected = asserts; + } else { + return this.test.expected; + } + }, + + // Increment this Test's semaphore counter, then return a function that + // decrements that counter a maximum of once. + async: function( count ) { + var test = this.test, + popped = false, + acceptCallCount = count; + + if ( typeof acceptCallCount === "undefined" ) { + acceptCallCount = 1; + } + + test.semaphore += 1; + test.usedAsync = true; + pauseProcessing(); + + return function done() { + + if ( popped ) { + test.pushFailure( "Too many calls to the `assert.async` callback", + sourceFromStacktrace( 2 ) ); + return; + } + acceptCallCount -= 1; + if ( acceptCallCount > 0 ) { + return; + } + + test.semaphore -= 1; + popped = true; + resumeProcessing(); + }; + }, + + // Exports test.push() to the user API + push: function( /* result, actual, expected, message, negative */ ) { + var assert = this, + currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; + + // Backwards compatibility fix. + // Allows the direct use of global exported assertions and QUnit.assert.* + // Although, it's use is not recommended as it can leak assertions + // to other tests from async tests, because we only get a reference to the current test, + // not exactly the test where assertion were intended to be called. + if ( !currentTest ) { + throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); + } + + if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { + currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", + sourceFromStacktrace( 2 ) ); + + // Allow this assertion to continue running anyway... + } + + if ( !( assert instanceof Assert ) ) { + assert = currentTest.assert; + } + return assert.test.push.apply( assert.test, arguments ); + }, + + ok: function( result, message ) { + message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + + QUnit.dump.parse( result ) ); + this.push( !!result, result, true, message ); + }, + + notOk: function( result, message ) { + message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + + QUnit.dump.parse( result ) ); + this.push( !result, result, false, message, true ); + }, + + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + this.push( expected == actual, actual, expected, message ); + }, + + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + this.push( expected != actual, actual, expected, message, true ); + }, + + propEqual: function( actual, expected, message ) { + actual = objectValues( actual ); + expected = objectValues( expected ); + this.push( QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + notPropEqual: function( actual, expected, message ) { + actual = objectValues( actual ); + expected = objectValues( expected ); + this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); + }, + + deepEqual: function( actual, expected, message ) { + this.push( QUnit.equiv( actual, expected ), actual, expected, message ); + }, + + notDeepEqual: function( actual, expected, message ) { + this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); + }, + + strictEqual: function( actual, expected, message ) { + this.push( expected === actual, actual, expected, message ); + }, + + notStrictEqual: function( actual, expected, message ) { + this.push( expected !== actual, actual, expected, message, true ); + }, + + "throws": function( block, expected, message ) { + var actual, expectedType, + expectedOutput = expected, + ok = false, + currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; + + // 'expected' is optional unless doing string comparison + if ( message == null && typeof expected === "string" ) { + message = expected; + expected = null; + } + + currentTest.ignoreGlobalErrors = true; + try { + block.call( currentTest.testEnvironment ); + } catch (e) { + actual = e; + } + currentTest.ignoreGlobalErrors = false; + + if ( actual ) { + expectedType = QUnit.objectType( expected ); + + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + + // expected is a regexp + } else if ( expectedType === "regexp" ) { + ok = expected.test( errorString( actual ) ); + + // expected is a string + } else if ( expectedType === "string" ) { + ok = expected === errorString( actual ); + + // expected is a constructor, maybe an Error constructor + } else if ( expectedType === "function" && actual instanceof expected ) { + ok = true; + + // expected is an Error object + } else if ( expectedType === "object" ) { + ok = actual instanceof expected.constructor && + actual.name === expected.name && + actual.message === expected.message; + + // expected is a validation function which returns true if validation passed + } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + } + + currentTest.assert.push( ok, actual, expectedOutput, message ); + } +}; + +// Provide an alternative to assert.throws(), for environments that consider throws a reserved word +// Known to us are: Closure Compiler, Narwhal +(function() { + /*jshint sub:true */ + Assert.prototype.raises = Assert.prototype[ "throws" ]; +}()); + +function errorString( error ) { + var name, message, + resultErrorString = error.toString(); + if ( resultErrorString.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 resultErrorString; + } +} + +// Test for equality any JavaScript type. +// Author: Philippe Rathé +QUnit.equiv = (function() { + + // Stack to decide between skip/abort functions + var callers = []; + + // Stack to avoiding loops from circular referencing + var parents = []; + var parentsB = []; + + function useStrictEquality( b, a ) { + + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + + // To catch short annotation VS 'new' annotation of a declaration. e.g.: + // `var i = 1;` + // `var j = new Number(1);` + return a == b; + } else { + return a === b; + } + } + + function compareConstructors( a, b ) { + var getProto = Object.getPrototypeOf || function( obj ) { + + /*jshint proto: true */ + return obj.__proto__; + }; + var protoA = getProto( a ); + var protoB = getProto( b ); + + // Comparing constructors is more strict than using `instanceof` + if ( a.constructor === b.constructor ) { + return true; + } + + // Ref #851 + // If the obj prototype descends from a null constructor, treat it + // as a null prototype. + if ( protoA && protoA.constructor === null ) { + protoA = null; + } + if ( protoB && protoB.constructor === null ) { + protoB = null; + } + + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( ( protoA === null && protoB === Object.prototype ) || + ( protoB === null && protoA === Object.prototype ) ) { + return true; + } + + return false; + } + + var callbacks = { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + "symbol": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + + // The regex itself + a.source === b.source && + + // And its modifiers + a.global === b.global && + + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[ callers.length - 1 ]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // Track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { + parents.pop(); + parentsB.pop(); + return false; + } + } + } + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + parents.pop(); + parentsB.pop(); + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "set": function( b, a ) { + var aArray, bArray; + + // `b` could be any object here + if ( QUnit.objectType( b ) !== "set" ) { + return false; + } + + aArray = []; + a.forEach( function( v ) { + aArray.push( v ); + }); + bArray = []; + b.forEach( function( v ) { + bArray.push( v ); + }); + + return innerEquiv( bArray, aArray ); + }, + + "map": function( b, a ) { + var aArray, bArray; + + // `b` could be any object here + if ( QUnit.objectType( b ) !== "map" ) { + return false; + } + + aArray = []; + a.forEach( function( v, k ) { + aArray.push( [ k, v ] ); + }); + bArray = []; + b.forEach( function( v, k ) { + bArray.push( [ k, v ] ); + }); + + return innerEquiv( bArray, aArray ); + }, + + "object": function( b, a ) { + var i, j, loop, aCircular, bCircular; + + // Default to true + var eq = true; + var aProperties = []; + var bProperties = []; + + if ( compareConstructors( a, b ) === false ) { + return false; + } + + // Stack constructor before traversing properties + callers.push( a.constructor ); + + // Track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // Be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { + eq = false; + break; + } + } + } + aProperties.push( i ); + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + eq = false; + break; + } + } + + parents.pop(); + parentsB.pop(); + + // Unstack, we are done + callers.pop(); + + for ( i in b ) { + + // Collect b's properties + bProperties.push( i ); + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + + function typeEquiv( a, b ) { + var prop = QUnit.objectType( a ); + return callbacks[ prop ]( b, a ); + } + + // The real equiv function + function innerEquiv() { + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + + // End transition + return true; + } + + return ( (function( a, b ) { + if ( a === b ) { + + // Catch the most you can + return true; + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType( a ) !== QUnit.objectType( b ) ) { + + // Don't lose time with error prone cases + return false; + } else { + return typeEquiv( a, b ); + } + + // Apply transition with (1..n) arguments + }( args[ 0 ], args[ 1 ] ) ) && + innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); + } + + return innerEquiv; +}()); + +// Based on jsDump by Ariel Flesler +// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html +QUnit.dump = (function() { + function quote( str ) { + return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = dump.separator(), + base = dump.indent(), + inner = dump.indent( 1 ); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join( s ); + } + function array( arr, stack ) { + var i = arr.length, + ret = new Array( i ); + + if ( dump.maxDepth && dump.depth > dump.maxDepth ) { + return "[object Array]"; + } + + this.up(); + while ( i-- ) { + ret[ i ] = this.parse( arr[ i ], undefined, stack ); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + dump = { + + // objType is used mostly internally, you can fix a (custom) type in advance + parse: function( obj, objType, stack ) { + stack = stack || []; + var res, parser, parserType, + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + ( inStack - stack.length ) + ")"; + } + + objType = objType || this.typeOf( obj ); + parser = this.parsers[ objType ]; + parserType = typeof parser; + + if ( parserType === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( parserType === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj ) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj ) ) { + type = "date"; + } else if ( QUnit.is( "function", obj ) ) { + type = "function"; + } else if ( obj.setInterval !== undefined && + obj.document !== undefined && + obj.nodeType === undefined ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + + // native arrays + toString.call( obj ) === "[object Array]" || + + // NodeList objects + ( typeof obj.length === "number" && obj.item !== undefined && + ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && + obj[ 0 ] === undefined ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this.depth + ( extra || 0 ) ).join( chr ); + }, + up: function( a ) { + this.depth += a || 1; + }, + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[ name ] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + maxDepth: QUnit.config.maxDepth, + + // This is the list of parsers, to modify them, use dump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function( error ) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + + // functions never have name in IE + name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, dump.parse( fn, "functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + var keys, key, val, i, nonEnumerableProperties, + ret = []; + + if ( dump.maxDepth && dump.depth > dump.maxDepth ) { + return "[object Object]"; + } + + dump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + + // Some properties are not always enumerable on Error objects. + nonEnumerableProperties = [ "message", "name" ]; + for ( i in nonEnumerableProperties ) { + key = nonEnumerableProperties[ i ]; + if ( key in map && inArray( key, keys ) < 0 ) { + keys.push( key ); + } + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( dump.parse( key, "key" ) + ": " + + dump.parse( val, undefined, stack ) ); + } + dump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = dump.HTML ? "<" : "<", + close = dump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[ i ].nodeValue; + + // IE6 includes all attributes in .attributes, even ones not explicitly + // set. Those have values like undefined, null, 0, false, "" or + // "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[ i ].nodeName + "=" + + dump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array( l ); + while ( l-- ) { + + // 97 is 'a' + args[ l ] = String.fromCharCode( 97 + l ); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return dump; +}()); + +// back compat +QUnit.jsDump = QUnit.dump; + +// For browser, export only select globals +if ( defined.document ) { + + // Deprecated + // Extend assert methods to QUnit and Global scope through Backwards compatibility + (function() { + var i, + assertions = Assert.prototype; + + function applyCurrent( current ) { + return function() { + var assert = new Assert( QUnit.config.current ); + current.apply( assert, arguments ); + }; + } + + for ( i in assertions ) { + QUnit[ i ] = applyCurrent( assertions[ i ] ); + } + })(); + + (function() { + var i, l, + keys = [ + "test", + "module", + "expect", + "asyncTest", + "start", + "stop", + "ok", + "notOk", + "equal", + "notEqual", + "propEqual", + "notPropEqual", + "deepEqual", + "notDeepEqual", + "strictEqual", + "notStrictEqual", + "throws", + "raises" + ]; + + for ( i = 0, l = keys.length; i < l; i++ ) { + window[ keys[ i ] ] = QUnit[ keys[ i ] ]; + } + })(); + + window.QUnit = QUnit; +} + +// For nodejs +if ( typeof module !== "undefined" && module && module.exports ) { + module.exports = QUnit; + + // For consistency with CommonJS environments' exports + module.exports.QUnit = QUnit; +} + +// For CommonJS with exports, but without module.exports, like Rhino +if ( typeof exports !== "undefined" && exports ) { + exports.QUnit = QUnit; +} + +if ( typeof define === "function" && define.amd ) { + define( function() { + return QUnit; + } ); + QUnit.config.autostart = false; +} + +/* + * This file is a modified version of google-diff-match-patch's JavaScript implementation + * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), + * modifications are licensed as more fully set forth in LICENSE.txt. + * + * The original source of google-diff-match-patch is attributable and licensed as follows: + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * More Info: + * https://code.google.com/p/google-diff-match-patch/ + * + * Usage: QUnit.diff(expected, actual) + * + */ +QUnit.diff = ( function() { + function DiffMatchPatch() { + } + + // DIFF FUNCTIONS + + /** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ + var DIFF_DELETE = -1, + DIFF_INSERT = 1, + DIFF_EQUAL = 0; + + /** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} optChecklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @return {!Array.} Array of diff tuples. + */ + DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { + var deadline, checklines, commonlength, + commonprefix, commonsuffix, diffs; + + // The diff must be complete in up to 1 second. + deadline = ( new Date() ).getTime() + 1000; + + // Check for null inputs. + if ( text1 === null || text2 === null ) { + throw new Error( "Null input. (DiffMain)" ); + } + + // Check for equality (speedup). + if ( text1 === text2 ) { + if ( text1 ) { + return [ + [ DIFF_EQUAL, text1 ] + ]; + } + return []; + } + + if ( typeof optChecklines === "undefined" ) { + optChecklines = true; + } + + checklines = optChecklines; + + // Trim off common prefix (speedup). + commonlength = this.diffCommonPrefix( text1, text2 ); + commonprefix = text1.substring( 0, commonlength ); + text1 = text1.substring( commonlength ); + text2 = text2.substring( commonlength ); + + // Trim off common suffix (speedup). + commonlength = this.diffCommonSuffix( text1, text2 ); + commonsuffix = text1.substring( text1.length - commonlength ); + text1 = text1.substring( 0, text1.length - commonlength ); + text2 = text2.substring( 0, text2.length - commonlength ); + + // Compute the diff on the middle block. + diffs = this.diffCompute( text1, text2, checklines, deadline ); + + // Restore the prefix and suffix. + if ( commonprefix ) { + diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); + } + if ( commonsuffix ) { + diffs.push( [ DIFF_EQUAL, commonsuffix ] ); + } + this.diffCleanupMerge( diffs ); + return diffs; + }; + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { + var changes, equalities, equalitiesLength, lastequality, + pointer, preIns, preDel, postIns, postDel; + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + preIns = false; + // Is there a deletion operation before the last equality. + preDel = false; + // Is there an insertion operation after the last equality. + postIns = false; + // Is there a deletion operation after the last equality. + postDel = false; + while ( pointer < diffs.length ) { + + // Equality found. + if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { + if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { + + // Candidate found. + equalities[ equalitiesLength++ ] = pointer; + preIns = postIns; + preDel = postDel; + lastequality = diffs[ pointer ][ 1 ]; + } else { + + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = null; + } + postIns = postDel = false; + + // An insertion or deletion. + } else { + + if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { + postDel = true; + } else { + postIns = true; + } + + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || + ( ( lastequality.length < 2 ) && + ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { + + // Duplicate record. + diffs.splice( + equalities[ equalitiesLength - 1 ], + 0, + [ DIFF_DELETE, lastequality ] + ); + + // Change second copy to insert. + diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = null; + if ( preIns && preDel ) { + // No changes made which could affect previous entry, keep going. + postIns = postDel = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; + postIns = postDel = false; + } + changes = true; + } + } + pointer++; + } + + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + }; + + /** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @param {integer} string to be beautified. + * @return {string} HTML representation. + */ + DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { + var op, data, x, + html = []; + for ( x = 0; x < diffs.length; x++ ) { + op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) + data = diffs[ x ][ 1 ]; // Text of change. + switch ( op ) { + case DIFF_INSERT: + html[ x ] = "" + data + ""; + break; + case DIFF_DELETE: + html[ x ] = "" + data + ""; + break; + case DIFF_EQUAL: + html[ x ] = "" + data + ""; + break; + } + } + return html.join( "" ); + }; + + /** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ + DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerstart; + // Quick check for common null cases. + if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min( text1.length, text2.length ); + pointermid = pointermax; + pointerstart = 0; + while ( pointermin < pointermid ) { + if ( text1.substring( pointerstart, pointermid ) === + text2.substring( pointerstart, pointermid ) ) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ + DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerend; + // Quick check for common null cases. + if ( !text1 || + !text2 || + text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min( text1.length, text2.length ); + pointermid = pointermax; + pointerend = 0; + while ( pointermin < pointermid ) { + if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === + text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { + var diffs, longtext, shorttext, i, hm, + text1A, text2A, text1B, text2B, + midCommon, diffsA, diffsB; + + if ( !text1 ) { + // Just add some text (speedup). + return [ + [ DIFF_INSERT, text2 ] + ]; + } + + if ( !text2 ) { + // Just delete some text (speedup). + return [ + [ DIFF_DELETE, text1 ] + ]; + } + + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + i = longtext.indexOf( shorttext ); + if ( i !== -1 ) { + // Shorter text is inside the longer text (speedup). + diffs = [ + [ DIFF_INSERT, longtext.substring( 0, i ) ], + [ DIFF_EQUAL, shorttext ], + [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] + ]; + // Swap insertions for deletions if diff is reversed. + if ( text1.length > text2.length ) { + diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; + } + return diffs; + } + + if ( shorttext.length === 1 ) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + } + + // Check to see if the problem can be split in two. + hm = this.diffHalfMatch( text1, text2 ); + if ( hm ) { + // A half-match was found, sort out the return data. + text1A = hm[ 0 ]; + text1B = hm[ 1 ]; + text2A = hm[ 2 ]; + text2B = hm[ 3 ]; + midCommon = hm[ 4 ]; + // Send both pairs off for separate processing. + diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); + diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); + // Merge the results. + return diffsA.concat( [ + [ DIFF_EQUAL, midCommon ] + ], diffsB ); + } + + if ( checklines && text1.length > 100 && text2.length > 100 ) { + return this.diffLineMode( text1, text2, deadline ); + } + + return this.diffBisect( text1, text2, deadline ); + }; + + /** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ + DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { + var longtext, shorttext, dmp, + text1A, text2B, text2A, text1B, midCommon, + hm1, hm2, hm; + + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { + return null; // Pointless. + } + dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diffHalfMatchI( longtext, shorttext, i ) { + var seed, j, bestCommon, prefixLength, suffixLength, + bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; + // Start with a 1/4 length substring at position i as a seed. + seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); + j = -1; + bestCommon = ""; + while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { + prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), + shorttext.substring( j ) ); + suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), + shorttext.substring( 0, j ) ); + if ( bestCommon.length < suffixLength + prefixLength ) { + bestCommon = shorttext.substring( j - suffixLength, j ) + + shorttext.substring( j, j + prefixLength ); + bestLongtextA = longtext.substring( 0, i - suffixLength ); + bestLongtextB = longtext.substring( i + prefixLength ); + bestShorttextA = shorttext.substring( 0, j - suffixLength ); + bestShorttextB = shorttext.substring( j + prefixLength ); + } + } + if ( bestCommon.length * 2 >= longtext.length ) { + return [ bestLongtextA, bestLongtextB, + bestShorttextA, bestShorttextB, bestCommon + ]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + hm1 = diffHalfMatchI( longtext, shorttext, + Math.ceil( longtext.length / 4 ) ); + // Check again based on the third quarter. + hm2 = diffHalfMatchI( longtext, shorttext, + Math.ceil( longtext.length / 2 ) ); + if ( !hm1 && !hm2 ) { + return null; + } else if ( !hm2 ) { + hm = hm1; + } else if ( !hm1 ) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + text1A, text1B, text2A, text2B; + if ( text1.length > text2.length ) { + text1A = hm[ 0 ]; + text1B = hm[ 1 ]; + text2A = hm[ 2 ]; + text2B = hm[ 3 ]; + } else { + text2A = hm[ 0 ]; + text2B = hm[ 1 ]; + text1A = hm[ 2 ]; + text1B = hm[ 3 ]; + } + midCommon = hm[ 4 ]; + return [ text1A, text1B, text2A, text2B, midCommon ]; + }; + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { + var a, diffs, linearray, pointer, countInsert, + countDelete, textInsert, textDelete, j; + // Scan the text on a line-by-line basis first. + a = this.diffLinesToChars( text1, text2 ); + text1 = a.chars1; + text2 = a.chars2; + linearray = a.lineArray; + + diffs = this.DiffMain( text1, text2, false, deadline ); + + // Convert the diff back to original text. + this.diffCharsToLines( diffs, linearray ); + // Eliminate freak matches (e.g. blank lines) + this.diffCleanupSemantic( diffs ); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push( [ DIFF_EQUAL, "" ] ); + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + while ( pointer < diffs.length ) { + switch ( diffs[ pointer ][ 0 ] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[ pointer ][ 1 ]; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[ pointer ][ 1 ]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if ( countDelete >= 1 && countInsert >= 1 ) { + // Delete the offending records and add the merged ones. + diffs.splice( pointer - countDelete - countInsert, + countDelete + countInsert ); + pointer = pointer - countDelete - countInsert; + a = this.DiffMain( textDelete, textInsert, false, deadline ); + for ( j = a.length - 1; j >= 0; j-- ) { + diffs.splice( pointer, 0, a[ j ] ); + } + pointer = pointer + a.length; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; + }; + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { + var text1Length, text2Length, maxD, vOffset, vLength, + v1, v2, x, delta, front, k1start, k1end, k2start, + k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); + vOffset = maxD; + vLength = 2 * maxD; + v1 = new Array( vLength ); + v2 = new Array( vLength ); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for ( x = 0; x < vLength; x++ ) { + v1[ x ] = -1; + v2[ x ] = -1; + } + v1[ vOffset + 1 ] = 0; + v2[ vOffset + 1 ] = 0; + delta = text1Length - text2Length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + front = ( delta % 2 !== 0 ); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + k1start = 0; + k1end = 0; + k2start = 0; + k2end = 0; + for ( d = 0; d < maxD; d++ ) { + // Bail out if deadline is reached. + if ( ( new Date() ).getTime() > deadline ) { + break; + } + + // Walk the front path one step. + for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { + k1Offset = vOffset + k1; + if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { + x1 = v1[ k1Offset + 1 ]; + } else { + x1 = v1[ k1Offset - 1 ] + 1; + } + y1 = x1 - k1; + while ( x1 < text1Length && y1 < text2Length && + text1.charAt( x1 ) === text2.charAt( y1 ) ) { + x1++; + y1++; + } + v1[ k1Offset ] = x1; + if ( x1 > text1Length ) { + // Ran off the right of the graph. + k1end += 2; + } else if ( y1 > text2Length ) { + // Ran off the bottom of the graph. + k1start += 2; + } else if ( front ) { + k2Offset = vOffset + delta - k1; + if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - v2[ k2Offset ]; + if ( x1 >= x2 ) { + // Overlap detected. + return this.diffBisectSplit( text1, text2, x1, y1, deadline ); + } + } + } + } + + // Walk the reverse path one step. + for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { + k2Offset = vOffset + k2; + if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { + x2 = v2[ k2Offset + 1 ]; + } else { + x2 = v2[ k2Offset - 1 ] + 1; + } + y2 = x2 - k2; + while ( x2 < text1Length && y2 < text2Length && + text1.charAt( text1Length - x2 - 1 ) === + text2.charAt( text2Length - y2 - 1 ) ) { + x2++; + y2++; + } + v2[ k2Offset ] = x2; + if ( x2 > text1Length ) { + // Ran off the left of the graph. + k2end += 2; + } else if ( y2 > text2Length ) { + // Ran off the top of the graph. + k2start += 2; + } else if ( !front ) { + k1Offset = vOffset + delta - k2; + if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { + x1 = v1[ k1Offset ]; + y1 = vOffset + x1 - k1Offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - x2; + if ( x1 >= x2 ) { + // Overlap detected. + return this.diffBisectSplit( text1, text2, x1, y1, deadline ); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + }; + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { + var text1a, text1b, text2a, text2b, diffs, diffsb; + text1a = text1.substring( 0, x ); + text2a = text2.substring( 0, y ); + text1b = text1.substring( x ); + text2b = text2.substring( y ); + + // Compute both diffs serially. + diffs = this.DiffMain( text1a, text2a, false, deadline ); + diffsb = this.DiffMain( text1b, text2b, false, deadline ); + + return diffs.concat( diffsb ); + }; + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { + var changes, equalities, equalitiesLength, lastequality, + pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, + lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + // Number of characters that changed after the equality. + lengthInsertions2 = 0; + lengthDeletions2 = 0; + while ( pointer < diffs.length ) { + if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. + equalities[ equalitiesLength++ ] = pointer; + lengthInsertions1 = lengthInsertions2; + lengthDeletions1 = lengthDeletions2; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = diffs[ pointer ][ 1 ]; + } else { // An insertion or deletion. + if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { + lengthInsertions2 += diffs[ pointer ][ 1 ].length; + } else { + lengthDeletions2 += diffs[ pointer ][ 1 ].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if ( lastequality && ( lastequality.length <= + Math.max( lengthInsertions1, lengthDeletions1 ) ) && + ( lastequality.length <= Math.max( lengthInsertions2, + lengthDeletions2 ) ) ) { + + // Duplicate record. + diffs.splice( + equalities[ equalitiesLength - 1 ], + 0, + [ DIFF_DELETE, lastequality ] + ); + + // Change second copy to insert. + diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; + + // Throw away the equality we just deleted. + equalitiesLength--; + + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; + + // Reset the counters. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while ( pointer < diffs.length ) { + if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && + diffs[ pointer ][ 0 ] === DIFF_INSERT ) { + deletion = diffs[ pointer - 1 ][ 1 ]; + insertion = diffs[ pointer ][ 1 ]; + overlapLength1 = this.diffCommonOverlap( deletion, insertion ); + overlapLength2 = this.diffCommonOverlap( insertion, deletion ); + if ( overlapLength1 >= overlapLength2 ) { + if ( overlapLength1 >= deletion.length / 2 || + overlapLength1 >= insertion.length / 2 ) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice( + pointer, + 0, + [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] + ); + diffs[ pointer - 1 ][ 1 ] = + deletion.substring( 0, deletion.length - overlapLength1 ); + diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); + pointer++; + } + } else { + if ( overlapLength2 >= deletion.length / 2 || + overlapLength2 >= insertion.length / 2 ) { + + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice( + pointer, + 0, + [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] + ); + + diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; + diffs[ pointer - 1 ][ 1 ] = + insertion.substring( 0, insertion.length - overlapLength2 ); + diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; + diffs[ pointer + 1 ][ 1 ] = + deletion.substring( overlapLength2 ); + pointer++; + } + } + pointer++; + } + pointer++; + } + }; + + /** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ + DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { + var text1Length, text2Length, textLength, + best, length, pattern, found; + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + // Eliminate the null case. + if ( text1Length === 0 || text2Length === 0 ) { + return 0; + } + // Truncate the longer string. + if ( text1Length > text2Length ) { + text1 = text1.substring( text1Length - text2Length ); + } else if ( text1Length < text2Length ) { + text2 = text2.substring( 0, text1Length ); + } + textLength = Math.min( text1Length, text2Length ); + // Quick check for the worst case. + if ( text1 === text2 ) { + return textLength; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + best = 0; + length = 1; + while ( true ) { + pattern = text1.substring( textLength - length ); + found = text2.indexOf( pattern ); + if ( found === -1 ) { + return best; + } + length += found; + if ( found === 0 || text1.substring( textLength - length ) === + text2.substring( 0, length ) ) { + best = length; + length++; + } + } + }; + + /** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ + DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { + var lineArray, lineHash, chars1, chars2; + lineArray = []; // e.g. lineArray[4] === 'Hello\n' + lineHash = {}; // e.g. lineHash['Hello\n'] === 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[ 0 ] = ""; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diffLinesToCharsMunge( text ) { + var chars, lineStart, lineEnd, lineArrayLength, line; + chars = ""; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + lineStart = 0; + lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + lineArrayLength = lineArray.length; + while ( lineEnd < text.length - 1 ) { + lineEnd = text.indexOf( "\n", lineStart ); + if ( lineEnd === -1 ) { + lineEnd = text.length - 1; + } + line = text.substring( lineStart, lineEnd + 1 ); + lineStart = lineEnd + 1; + + if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : + ( lineHash[ line ] !== undefined ) ) { + chars += String.fromCharCode( lineHash[ line ] ); + } else { + chars += String.fromCharCode( lineArrayLength ); + lineHash[ line ] = lineArrayLength; + lineArray[ lineArrayLength++ ] = line; + } + } + return chars; + } + + chars1 = diffLinesToCharsMunge( text1 ); + chars2 = diffLinesToCharsMunge( text2 ); + return { + chars1: chars1, + chars2: chars2, + lineArray: lineArray + }; + }; + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ + DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { + var x, chars, text, y; + for ( x = 0; x < diffs.length; x++ ) { + chars = diffs[ x ][ 1 ]; + text = []; + for ( y = 0; y < chars.length; y++ ) { + text[ y ] = lineArray[ chars.charCodeAt( y ) ]; + } + diffs[ x ][ 1 ] = text.join( "" ); + } + }; + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { + var pointer, countDelete, countInsert, textInsert, textDelete, + commonlength, changes, diffPointer, position; + diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + commonlength; + while ( pointer < diffs.length ) { + switch ( diffs[ pointer ][ 0 ] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[ pointer ][ 1 ]; + pointer++; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[ pointer ][ 1 ]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if ( countDelete + countInsert > 1 ) { + if ( countDelete !== 0 && countInsert !== 0 ) { + // Factor out any common prefixies. + commonlength = this.diffCommonPrefix( textInsert, textDelete ); + if ( commonlength !== 0 ) { + if ( ( pointer - countDelete - countInsert ) > 0 && + diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === + DIFF_EQUAL ) { + diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += + textInsert.substring( 0, commonlength ); + } else { + diffs.splice( 0, 0, [ DIFF_EQUAL, + textInsert.substring( 0, commonlength ) + ] ); + pointer++; + } + textInsert = textInsert.substring( commonlength ); + textDelete = textDelete.substring( commonlength ); + } + // Factor out any common suffixies. + commonlength = this.diffCommonSuffix( textInsert, textDelete ); + if ( commonlength !== 0 ) { + diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - + commonlength ) + diffs[ pointer ][ 1 ]; + textInsert = textInsert.substring( 0, textInsert.length - + commonlength ); + textDelete = textDelete.substring( 0, textDelete.length - + commonlength ); + } + } + // Delete the offending records and add the merged ones. + if ( countDelete === 0 ) { + diffs.splice( pointer - countInsert, + countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); + } else if ( countInsert === 0 ) { + diffs.splice( pointer - countDelete, + countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); + } else { + diffs.splice( + pointer - countDelete - countInsert, + countDelete + countInsert, + [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] + ); + } + pointer = pointer - countDelete - countInsert + + ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; + } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { + + // Merge this equality with the previous one. + diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; + diffs.splice( pointer, 1 ); + } else { + pointer++; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + } + if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + changes = false; + pointer = 1; + + // Intentionally ignore the first and last element (don't need checking). + while ( pointer < diffs.length - 1 ) { + if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && + diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { + + diffPointer = diffs[ pointer ][ 1 ]; + position = diffPointer.substring( + diffPointer.length - diffs[ pointer - 1 ][ 1 ].length + ); + + // This is a single edit surrounded by equalities. + if ( position === diffs[ pointer - 1 ][ 1 ] ) { + + // Shift the edit over the previous equality. + diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + + diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - + diffs[ pointer - 1 ][ 1 ].length ); + diffs[ pointer + 1 ][ 1 ] = + diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; + diffs.splice( pointer - 1, 1 ); + changes = true; + } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === + diffs[ pointer + 1 ][ 1 ] ) { + + // Shift the edit over the next equality. + diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; + diffs[ pointer ][ 1 ] = + diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + + diffs[ pointer + 1 ][ 1 ]; + diffs.splice( pointer + 1, 1 ); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + }; + + return function( o, n ) { + var diff, output, text; + diff = new DiffMatchPatch(); + output = diff.DiffMain( o, n ); + diff.diffCleanupEfficiency( output ); + text = diff.diffPrettyHtml( output ); + + return text; + }; +}() ); + +// Get a reference to the global object, like window in browsers +}( (function() { + return this; +})() )); + +(function() { + +// Don't load the HTML Reporter on non-Browser environments +if ( typeof window === "undefined" || !window.document ) { + return; +} + +// Deprecated QUnit.init - Ref #530 +// Re-initialize the configuration options +QUnit.init = function() { + var tests, banner, result, qunit, + config = QUnit.config; + + config.stats = { all: 0, bad: 0 }; + config.moduleStats = { all: 0, bad: 0 }; + config.started = 0; + config.updateRate = 1000; + config.blocking = false; + config.autostart = true; + config.autorun = false; + config.filter = ""; + config.queue = []; + + // Return on non-browser environments + // This is necessary to not break on node tests + if ( typeof window === "undefined" ) { + return; + } + + qunit = id( "qunit" ); + if ( qunit ) { + qunit.innerHTML = + "

" + escapeText( document.title ) + "

" + + "

" + + "
" + + "

" + + "
    "; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } +}; + +var config = QUnit.config, + collapseNext = false, + hasOwn = Object.prototype.hasOwnProperty, + defined = { + document: window.document !== undefined, + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch ( e ) { + return false; + } + }()) + }, + modulesList = []; + +/** +* Escape text for attribute or text content. +*/ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch ( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } + }); +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + if ( elem.addEventListener ) { + + // Standards-based browsers + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + + // support: IE <9 + elem.attachEvent( "on" + type, function() { + var event = window.event; + if ( !event.target ) { + event.target = event.srcElement || document; + } + + fn.call( elem, event ); + }); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[ i ], type, fn ); + } +} + +function hasClass( elem, name ) { + return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += ( elem.className ? " " : "" ) + name; + } +} + +function toggleClass( elem, name ) { + if ( hasClass( elem, name ) ) { + removeClass( elem, name ); + } else { + addClass( elem, name ); + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + + // Class name may appear multiple times + while ( set.indexOf( " " + name + " " ) >= 0 ) { + set = set.replace( " " + name + " ", " " ); + } + + // trim for prettiness + elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); +} + +function id( name ) { + return defined.document && document.getElementById && document.getElementById( name ); +} + +function getUrlConfigHtml() { + var i, j, val, + escaped, escapedTooltip, + selection = false, + len = config.urlConfig.length, + urlConfigHtml = ""; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[ i ]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val + }; + } + + escaped = escapeText( val.id ); + escapedTooltip = escapeText( val.tooltip ); + + if ( config[ val.id ] === undefined ) { + config[ val.id ] = QUnit.urlParams[ val.id ]; + } + + if ( !val.value || typeof val.value === "string" ) { + urlConfigHtml += ""; + } else { + urlConfigHtml += ""; + } + } + + return urlConfigHtml; +} + +// Handle "click" events on toolbar checkboxes and "change" for select menus. +// Updates the URL with the new state of `config.urlConfig` values. +function toolbarChanged() { + var updatedUrl, value, + field = this, + params = {}; + + // Detect if field is a select menu or a checkbox + if ( "selectedIndex" in field ) { + value = field.options[ field.selectedIndex ].value || undefined; + } else { + value = field.checked ? ( field.defaultValue || true ) : undefined; + } + + params[ field.name ] = value; + updatedUrl = setUrl( params ); + + if ( "hidepassed" === field.name && "replaceState" in window.history ) { + config[ field.name ] = value || false; + if ( value ) { + addClass( id( "qunit-tests" ), "hidepass" ); + } else { + removeClass( id( "qunit-tests" ), "hidepass" ); + } + + // It is not necessary to refresh the whole page + window.history.replaceState( null, "", updatedUrl ); + } else { + window.location = updatedUrl; + } +} + +function setUrl( params ) { + var key, + querystring = "?"; + + params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + if ( params[ key ] === undefined ) { + continue; + } + querystring += encodeURIComponent( key ); + if ( params[ key ] !== true ) { + querystring += "=" + encodeURIComponent( params[ key ] ); + } + querystring += "&"; + } + } + return location.protocol + "//" + location.host + + location.pathname + querystring.slice( 0, -1 ); +} + +function applyUrlParams() { + var selectedModule, + modulesList = id( "qunit-modulefilter" ), + filter = id( "qunit-filter-input" ).value; + + selectedModule = modulesList ? + decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : + undefined; + + window.location = setUrl({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + filter: ( filter === "" ) ? undefined : filter, + + // Remove testId filter + testId: undefined + }); +} + +function toolbarUrlConfigContainer() { + var urlConfigContainer = document.createElement( "span" ); + + urlConfigContainer.innerHTML = getUrlConfigHtml(); + addClass( urlConfigContainer, "qunit-url-config" ); + + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" for checkboxes + addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); + addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); + + return urlConfigContainer; +} + +function toolbarLooseFilter() { + var filter = document.createElement( "form" ), + label = document.createElement( "label" ), + input = document.createElement( "input" ), + button = document.createElement( "button" ); + + addClass( filter, "qunit-filter" ); + + label.innerHTML = "Filter: "; + + input.type = "text"; + input.value = config.filter || ""; + input.name = "filter"; + input.id = "qunit-filter-input"; + + button.innerHTML = "Go"; + + label.appendChild( input ); + + filter.appendChild( label ); + filter.appendChild( button ); + addEvent( filter, "submit", function( ev ) { + applyUrlParams(); + + if ( ev && ev.preventDefault ) { + ev.preventDefault(); + } + + return false; + }); + + return filter; +} + +function toolbarModuleFilterHtml() { + var i, + moduleFilterHtml = ""; + + if ( !modulesList.length ) { + return false; + } + + modulesList.sort(function( a, b ) { + return a.localeCompare( b ); + }); + + moduleFilterHtml += "" + + ""; + + return moduleFilterHtml; +} + +function toolbarModuleFilter() { + var toolbar = id( "qunit-testrunner-toolbar" ), + moduleFilter = document.createElement( "span" ), + moduleFilterHtml = toolbarModuleFilterHtml(); + + if ( !toolbar || !moduleFilterHtml ) { + return false; + } + + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + + addEvent( moduleFilter.lastChild, "change", applyUrlParams ); + + toolbar.appendChild( moduleFilter ); +} + +function appendToolbar() { + var toolbar = id( "qunit-testrunner-toolbar" ); + + if ( toolbar ) { + toolbar.appendChild( toolbarUrlConfigContainer() ); + toolbar.appendChild( toolbarLooseFilter() ); + } +} + +function appendHeader() { + var header = id( "qunit-header" ); + + if ( header ) { + header.innerHTML = "" + header.innerHTML + " "; + } +} + +function appendBanner() { + var banner = id( "qunit-banner" ); + + if ( banner ) { + banner.className = ""; + } +} + +function appendTestResults() { + var tests = id( "qunit-tests" ), + result = id( "qunit-testresult" ); + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + tests.innerHTML = ""; + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } +} + +function storeFixture() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + config.fixture = fixture.innerHTML; + } +} + +function appendFilteredTest() { + var testId = QUnit.config.testId; + if ( !testId || testId.length <= 0 ) { + return ""; + } + return "
    Rerunning selected tests: " + testId.join(", ") + + " " + "Run all tests" + "
    "; +} + +function appendUserAgent() { + var userAgent = id( "qunit-userAgent" ); + + if ( userAgent ) { + userAgent.innerHTML = ""; + userAgent.appendChild( + document.createTextNode( + "QUnit " + QUnit.version + "; " + navigator.userAgent + ) + ); + } +} + +function appendTestsList( modules ) { + var i, l, x, z, test, moduleObj; + + for ( i = 0, l = modules.length; i < l; i++ ) { + moduleObj = modules[ i ]; + + if ( moduleObj.name ) { + modulesList.push( moduleObj.name ); + } + + for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { + test = moduleObj.tests[ x ]; + + appendTest( test.name, test.testId, moduleObj.name ); + } + } +} + +function appendTest( name, testId, moduleName ) { + var title, rerunTrigger, testBlock, assertList, + tests = id( "qunit-tests" ); + + if ( !tests ) { + return; + } + + title = document.createElement( "strong" ); + title.innerHTML = getNameHtml( name, moduleName ); + + rerunTrigger = document.createElement( "a" ); + rerunTrigger.innerHTML = "Rerun"; + rerunTrigger.href = setUrl({ testId: testId }); + + testBlock = document.createElement( "li" ); + testBlock.appendChild( title ); + testBlock.appendChild( rerunTrigger ); + testBlock.id = "qunit-test-output-" + testId; + + assertList = document.createElement( "ol" ); + assertList.className = "qunit-assert-list"; + + testBlock.appendChild( assertList ); + + tests.appendChild( testBlock ); +} + +// HTML Reporter initialization and load +QUnit.begin(function( details ) { + var qunit = id( "qunit" ); + + // Fixture is the only one necessary to run without the #qunit element + storeFixture(); + + if ( qunit ) { + qunit.innerHTML = + "

    " + escapeText( document.title ) + "

    " + + "

    " + + "
    " + + appendFilteredTest() + + "

    " + + "
      "; + } + + appendHeader(); + appendBanner(); + appendTestResults(); + appendUserAgent(); + appendToolbar(); + appendTestsList( details.modules ); + toolbarModuleFilter(); + + if ( qunit && config.hidepassed ) { + addClass( qunit.lastChild, "hidepass" ); + } +}); + +QUnit.done(function( details ) { + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + html = [ + "Tests completed in ", + details.runtime, + " milliseconds.
      ", + "", + details.passed, + " assertions of ", + details.total, + " passed, ", + details.failed, + " failed." + ].join( "" ); + + if ( banner ) { + banner.className = details.failed ? "qunit-fail" : "qunit-pass"; + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && defined.document && document.title ) { + + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( details.failed ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( config.scrolltop && window.scrollTo ) { + window.scrollTo( 0, 0 ); + } +}); + +function getNameHtml( name, module ) { + var nameHtml = ""; + + if ( module ) { + nameHtml = "" + escapeText( module ) + ": "; + } + + nameHtml += "" + escapeText( name ) + ""; + + return nameHtml; +} + +QUnit.testStart(function( details ) { + var running, testBlock, bad; + + testBlock = id( "qunit-test-output-" + details.testId ); + if ( testBlock ) { + testBlock.className = "running"; + } else { + + // Report later registered tests + appendTest( details.name, details.testId, details.module ); + } + + running = id( "qunit-testresult" ); + if ( running ) { + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); + + running.innerHTML = ( bad ? + "Rerunning previously failed test:
      " : + "Running:
      " ) + + getNameHtml( details.name, details.module ); + } + +}); + +function stripHtml( string ) { + // strip tags, html entity and whitespaces + return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); +} + +QUnit.log(function( details ) { + var assertList, assertLi, + message, expected, actual, diff, + showDiff = false, + testItem = id( "qunit-test-output-" + details.testId ); + + if ( !testItem ) { + return; + } + + message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); + message = "" + message + ""; + message += "@ " + details.runtime + " ms"; + + // pushFailure doesn't provide details.expected + // when it calls, it's implicit to also not show expected and diff stuff + // Also, we need to check details.expected existence, as it can exist and be undefined + if ( !details.result && hasOwn.call( details, "expected" ) ) { + if ( details.negative ) { + expected = escapeText( "NOT " + QUnit.dump.parse( details.expected ) ); + } else { + expected = escapeText( QUnit.dump.parse( details.expected ) ); + } + + actual = escapeText( QUnit.dump.parse( details.actual ) ); + message += ""; + + if ( actual !== expected ) { + + message += ""; + + // Don't show diff if actual or expected are booleans + if ( !( /^(true|false)$/.test( actual ) ) && + !( /^(true|false)$/.test( expected ) ) ) { + diff = QUnit.diff( expected, actual ); + showDiff = stripHtml( diff ).length !== + stripHtml( expected ).length + + stripHtml( actual ).length; + } + + // Don't show diff if expected and actual are totally different + if ( showDiff ) { + message += ""; + } + } else if ( expected.indexOf( "[object Array]" ) !== -1 || + expected.indexOf( "[object Object]" ) !== -1 ) { + message += ""; + } + + if ( details.source ) { + message += ""; + } + + message += "
      Expected:
      " +
      +			expected +
      +			"
      Result:
      " +
      +				actual + "
      Diff:
      " +
      +					diff + "
      Message: " + + "Diff suppressed as the depth of object is more than current max depth (" + + QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + + " run with a higher max depth or " + + "Rerun without max depth.

      Source:
      " +
      +				escapeText( details.source ) + "
      "; + + // this occours when pushFailure is set and we have an extracted stack trace + } else if ( !details.result && details.source ) { + message += "" + + "" + + "
      Source:
      " +
      +			escapeText( details.source ) + "
      "; + } + + assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; + + assertLi = document.createElement( "li" ); + assertLi.className = details.result ? "pass" : "fail"; + assertLi.innerHTML = message; + assertList.appendChild( assertLi ); +}); + +QUnit.testDone(function( details ) { + var testTitle, time, testItem, assertList, + good, bad, testCounts, skipped, sourceName, + tests = id( "qunit-tests" ); + + if ( !tests ) { + return; + } + + testItem = id( "qunit-test-output-" + details.testId ); + + assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; + + good = details.passed; + bad = details.failed; + + // store result when possible + if ( config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); + } + } + + if ( bad === 0 ) { + + // Collapse the passing tests + addClass( assertList, "qunit-collapsed" ); + } else if ( bad && config.collapse && !collapseNext ) { + + // Skip collapsing the first failing test + collapseNext = true; + } else { + + // Collapse remaining tests + addClass( assertList, "qunit-collapsed" ); + } + + // testItem.firstChild is the test name + testTitle = testItem.firstChild; + + testCounts = bad ? + "" + bad + ", " + "" + good + ", " : + ""; + + testTitle.innerHTML += " (" + testCounts + + details.assertions.length + ")"; + + if ( details.skipped ) { + testItem.className = "skipped"; + skipped = document.createElement( "em" ); + skipped.className = "qunit-skipped-label"; + skipped.innerHTML = "skipped"; + testItem.insertBefore( skipped, testTitle ); + } else { + addEvent( testTitle, "click", function() { + toggleClass( assertList, "qunit-collapsed" ); + }); + + testItem.className = bad ? "fail" : "pass"; + + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = details.runtime + " ms"; + testItem.insertBefore( time, assertList ); + } + + // Show the source of the test when showing assertions + if ( details.source ) { + sourceName = document.createElement( "p" ); + sourceName.innerHTML = "Source: " + details.source; + addClass( sourceName, "qunit-source" ); + if ( bad === 0 ) { + addClass( sourceName, "qunit-collapsed" ); + } + addEvent( testTitle, "click", function() { + toggleClass( sourceName, "qunit-collapsed" ); + }); + testItem.appendChild( sourceName ); + } +}); + +if ( defined.document ) { + + // Avoid readyState issue with phantomjs + // Ref: #818 + var notPhantom = ( function( p ) { + return !( p && p.version && p.version.major > 0 ); + } )( window.phantom ); + + if ( notPhantom && document.readyState === "complete" ) { + QUnit.load(); + } else { + addEvent( window, "load", QUnit.load ); + } +} else { + config.pageLoaded = true; + config.autorun = true; +} + +})(); diff --git a/test/syn/syn.js b/test/syn/syn.js new file mode 100644 index 0000000..188e04a --- /dev/null +++ b/test/syn/syn.js @@ -0,0 +1,2422 @@ +/** + * syn - 0.0.3 + * + * @copyright 2015 Bitovi + * Fri, 06 Mar 2015 01:47:33 GMT + * @license MIT + */ + +/*[global-shim]*/ +(function (exports, global){ + var origDefine = global.define; + + var get = function(name){ + var parts = name.split("."), + cur = global, + i; + for(i = 0 ; i < parts.length; i++){ + if(!cur) { + break; + } + cur = cur[parts[i]]; + } + return cur; + }; + var modules = global.define && global.define.modules || {}; + var ourDefine = global.define = function(moduleName, deps, callback){ + var module; + if(typeof deps === "function") { + callback = deps; + deps = []; + } + var args = [], + i; + for(i =0; i < deps.length; i++) { + args.push( exports[deps[i]] ? get(exports[deps[i]]) : ( modules[deps[i]] || get(deps[i]) ) ); + } + // CJS has no dependencies but 3 callback arguments + if(!deps.length && callback.length) { + module = { exports: {} }; + var require = function(name) { + return exports[name] ? get(exports[name]) : modules[name]; + }; + args.push(require, module.exports, module); + } + + global.define = origDefine; + var result = callback ? callback.apply(null, args) : undefined; + global.define = ourDefine; + + // Favor CJS module.exports over the return value + modules[moduleName] = module && module.exports ? module.exports : result; + }; + global.define.modules = modules; + global.define.amd = true; + global.System = { + define: function(__name, __code){ + global.define = origDefine; + eval("(function() { " + __code + " \n }).call(global);"); + global.define = ourDefine; + } + }; +})({},window) +/*syn@0.1.1#synthetic*/ +define('syn/synthetic', function(require, exports, module) { +var opts = window.syn ? window.syn : {}; +var extend = function (d, s) { + var p; + for (p in s) { + d[p] = s[p]; + } + return d; + }, browser = { + msie: !!(window.attachEvent && !window.opera), + opera: !!window.opera, + webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') === -1, + gecko: navigator.userAgent.indexOf('Gecko') > -1, + mobilesafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/), + rhino: navigator.userAgent.match(/Rhino/) && true + }, createEventObject = function (type, options, element) { + var event = element.ownerDocument.createEventObject(); + return extend(event, options); + }, data = {}, id = 1, expando = '_synthetic' + new Date().getTime(), bind, unbind, schedule, key = /keypress|keyup|keydown/, page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/, activeElement, syn = function (type, element, options, callback) { + return new syn.init(type, element, options, callback); + }; +syn.config = opts; +syn.__tryFocus = function tryFocus(element) { + try { + element.focus(); + } catch (e) { + } +}; +bind = function (el, ev, f) { + return el.addEventListener ? el.addEventListener(ev, f, false) : el.attachEvent('on' + ev, f); +}; +unbind = function (el, ev, f) { + return el.addEventListener ? el.removeEventListener(ev, f, false) : el.detachEvent('on' + ev, f); +}; +schedule = syn.config.schedule || function (fn, ms) { + setTimeout(fn, ms); +}; +extend(syn, { + init: function (type, element, options, callback) { + var args = syn.args(options, element, callback), self = this; + this.queue = []; + this.element = args.element; + if (typeof this[type] === 'function') { + this[type](args.element, args.options, function (defaults, el) { + if (args.callback) { + args.callback.apply(self, arguments); + } + self.done.apply(self, arguments); + }); + } else { + this.result = syn.trigger(args.element, type, args.options); + if (args.callback) { + args.callback.call(this, args.element, this.result); + } + } + }, + jquery: function (el, fast) { + if (window.FuncUnit && window.FuncUnit.jQuery) { + return window.FuncUnit.jQuery; + } + if (el) { + return syn.helpers.getWindow(el).jQuery || window.jQuery; + } else { + return window.jQuery; + } + }, + args: function () { + var res = {}, i = 0; + for (; i < arguments.length; i++) { + if (typeof arguments[i] === 'function') { + res.callback = arguments[i]; + } else if (arguments[i] && arguments[i].jquery) { + res.element = arguments[i][0]; + } else if (arguments[i] && arguments[i].nodeName) { + res.element = arguments[i]; + } else if (res.options && typeof arguments[i] === 'string') { + res.element = document.getElementById(arguments[i]); + } else if (arguments[i]) { + res.options = arguments[i]; + } + } + return res; + }, + click: function (element, options, callback) { + syn('click!', element, options, callback); + }, + defaults: { + focus: function focus() { + if (!syn.support.focusChanges) { + var element = this, nodeName = element.nodeName.toLowerCase(); + syn.data(element, 'syntheticvalue', element.value); + if (nodeName === 'input' || nodeName === 'textarea') { + bind(element, 'blur', function blur() { + if (syn.data(element, 'syntheticvalue') !== element.value) { + syn.trigger(element, 'change', {}); + } + unbind(element, 'blur', blur); + }); + } + } + }, + submit: function () { + syn.onParents(this, function (el) { + if (el.nodeName.toLowerCase() === 'form') { + el.submit(); + return false; + } + }); + } + }, + changeOnBlur: function (element, prop, value) { + bind(element, 'blur', function onblur() { + if (value !== element[prop]) { + syn.trigger(element, 'change', {}); + } + unbind(element, 'blur', onblur); + }); + }, + closest: function (el, type) { + while (el && el.nodeName.toLowerCase() !== type.toLowerCase()) { + el = el.parentNode; + } + return el; + }, + data: function (el, key, value) { + var d; + if (!el[expando]) { + el[expando] = id++; + } + if (!data[el[expando]]) { + data[el[expando]] = {}; + } + d = data[el[expando]]; + if (value) { + data[el[expando]][key] = value; + } else { + return data[el[expando]][key]; + } + }, + onParents: function (el, func) { + var res; + while (el && res !== false) { + res = func(el); + el = el.parentNode; + } + return el; + }, + focusable: /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i, + isFocusable: function (elem) { + var attributeNode; + if (elem.getAttributeNode) { + attributeNode = elem.getAttributeNode('tabIndex'); + } + return this.focusable.test(elem.nodeName) || attributeNode && attributeNode.specified && syn.isVisible(elem); + }, + isVisible: function (elem) { + return elem.offsetWidth && elem.offsetHeight || elem.clientWidth && elem.clientHeight; + }, + tabIndex: function (elem) { + var attributeNode = elem.getAttributeNode('tabIndex'); + return attributeNode && attributeNode.specified && (parseInt(elem.getAttribute('tabIndex')) || 0); + }, + bind: bind, + unbind: unbind, + schedule: schedule, + browser: browser, + helpers: { + createEventObject: createEventObject, + createBasicStandardEvent: function (type, defaults, doc) { + var event; + try { + event = doc.createEvent('Events'); + } catch (e2) { + event = doc.createEvent('UIEvents'); + } finally { + event.initEvent(type, true, true); + extend(event, defaults); + } + return event; + }, + inArray: function (item, array) { + var i = 0; + for (; i < array.length; i++) { + if (array[i] === item) { + return i; + } + } + return -1; + }, + getWindow: function (element) { + if (element.ownerDocument) { + return element.ownerDocument.defaultView || element.ownerDocument.parentWindow; + } + }, + extend: extend, + scrollOffset: function (win, set) { + var doc = win.document.documentElement, body = win.document.body; + if (set) { + window.scrollTo(set.left, set.top); + } else { + return { + left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0), + top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0) + }; + } + }, + scrollDimensions: function (win) { + var doc = win.document.documentElement, body = win.document.body, docWidth = doc.clientWidth, docHeight = doc.clientHeight, compat = win.document.compatMode === 'CSS1Compat'; + return { + height: compat && docHeight || body.clientHeight || docHeight, + width: compat && docWidth || body.clientWidth || docWidth + }; + }, + addOffset: function (options, el) { + var jq = syn.jquery(el), off; + if (typeof options === 'object' && options.clientX === undefined && options.clientY === undefined && options.pageX === undefined && options.pageY === undefined && jq) { + el = jq(el); + off = el.offset(); + options.pageX = off.left + el.width() / 2; + options.pageY = off.top + el.height() / 2; + } + } + }, + key: { + ctrlKey: null, + altKey: null, + shiftKey: null, + metaKey: null + }, + dispatch: function (event, element, type, autoPrevent) { + if (element.dispatchEvent && event) { + var preventDefault = event.preventDefault, prevents = autoPrevent ? -1 : 0; + if (autoPrevent) { + bind(element, type, function ontype(ev) { + ev.preventDefault(); + unbind(this, type, ontype); + }); + } + event.preventDefault = function () { + prevents++; + if (++prevents > 0) { + preventDefault.apply(this, []); + } + }; + element.dispatchEvent(event); + return prevents <= 0; + } else { + try { + window.event = event; + } catch (e) { + } + return element.sourceIndex <= 0 || element.fireEvent && element.fireEvent('on' + type, event); + } + }, + create: { + page: { + event: function (type, options, element) { + var doc = syn.helpers.getWindow(element).document || document, event; + if (doc.createEvent) { + event = doc.createEvent('Events'); + event.initEvent(type, true, true); + return event; + } else { + try { + event = createEventObject(type, options, element); + } catch (e) { + } + return event; + } + } + }, + focus: { + event: function (type, options, element) { + syn.onParents(element, function (el) { + if (syn.isFocusable(el)) { + if (el.nodeName.toLowerCase() !== 'html') { + syn.__tryFocus(el); + activeElement = el; + } else if (activeElement) { + var doc = syn.helpers.getWindow(element).document; + if (doc !== window.document) { + return false; + } else if (doc.activeElement) { + doc.activeElement.blur(); + activeElement = null; + } else { + activeElement.blur(); + activeElement = null; + } + } + return false; + } + }); + return true; + } + } + }, + support: { + clickChanges: false, + clickSubmits: false, + keypressSubmits: false, + mouseupSubmits: false, + radioClickChanges: false, + focusChanges: false, + linkHrefJS: false, + keyCharacters: false, + backspaceWorks: false, + mouseDownUpClicks: false, + tabKeyTabs: false, + keypressOnAnchorClicks: false, + optionClickBubbles: false, + ready: 0 + }, + trigger: function (element, type, options) { + if (!options) { + options = {}; + } + var create = syn.create, setup = create[type] && create[type].setup, kind = key.test(type) ? 'key' : page.test(type) ? 'page' : 'mouse', createType = create[type] || {}, createKind = create[kind], event, ret, autoPrevent, dispatchEl = element; + if (syn.support.ready === 2 && setup) { + setup(type, options, element); + } + autoPrevent = options._autoPrevent; + delete options._autoPrevent; + if (createType.event) { + ret = createType.event(type, options, element); + } else { + options = createKind.options ? createKind.options(type, options, element) : options; + if (!syn.support.changeBubbles && /option/i.test(element.nodeName)) { + dispatchEl = element.parentNode; + } + event = createKind.event(type, options, dispatchEl); + ret = syn.dispatch(event, dispatchEl, type, autoPrevent); + } + if (ret && syn.support.ready === 2 && syn.defaults[type]) { + syn.defaults[type].call(element, options, autoPrevent); + } + return ret; + }, + eventSupported: function (eventName) { + var el = document.createElement('div'); + eventName = 'on' + eventName; + var isSupported = eventName in el; + if (!isSupported) { + el.setAttribute(eventName, 'return;'); + isSupported = typeof el[eventName] === 'function'; + } + el = null; + return isSupported; + } +}); +extend(syn.init.prototype, { + then: function (type, element, options, callback) { + if (syn.autoDelay) { + this.delay(); + } + var args = syn.args(options, element, callback), self = this; + this.queue.unshift(function (el, prevented) { + if (typeof this[type] === 'function') { + this.element = args.element || el; + this[type](this.element, args.options, function (defaults, el) { + if (args.callback) { + args.callback.apply(self, arguments); + } + self.done.apply(self, arguments); + }); + } else { + this.result = syn.trigger(args.element, type, args.options); + if (args.callback) { + args.callback.call(this, args.element, this.result); + } + return this; + } + }); + return this; + }, + delay: function (timeout, callback) { + if (typeof timeout === 'function') { + callback = timeout; + timeout = null; + } + timeout = timeout || 600; + var self = this; + this.queue.unshift(function () { + schedule(function () { + if (callback) { + callback.apply(self, []); + } + self.done.apply(self, arguments); + }, timeout); + }); + return this; + }, + done: function (defaults, el) { + if (el) { + this.element = el; + } + if (this.queue.length) { + this.queue.pop().call(this, this.element, defaults); + } + }, + '_click': function (element, options, callback, force) { + syn.helpers.addOffset(options, element); + syn.trigger(element, 'mousedown', options); + schedule(function () { + syn.trigger(element, 'mouseup', options); + if (!syn.support.mouseDownUpClicks || force) { + syn.trigger(element, 'click', options); + callback(true); + } else { + syn.create.click.setup('click', options, element); + syn.defaults.click.call(element); + schedule(function () { + callback(true); + }, 1); + } + }, 1); + }, + '_rightClick': function (element, options, callback) { + syn.helpers.addOffset(options, element); + var mouseopts = extend(extend({}, syn.mouse.browser.right.mouseup), options); + syn.trigger(element, 'mousedown', mouseopts); + schedule(function () { + syn.trigger(element, 'mouseup', mouseopts); + if (syn.mouse.browser.right.contextmenu) { + syn.trigger(element, 'contextmenu', extend(extend({}, syn.mouse.browser.right.contextmenu), options)); + } + callback(true); + }, 1); + }, + '_dblclick': function (element, options, callback) { + syn.helpers.addOffset(options, element); + var self = this; + this._click(element, options, function () { + schedule(function () { + self._click(element, options, function () { + syn.trigger(element, 'dblclick', options); + callback(true); + }, true); + }, 2); + }); + } +}); +var actions = [ + 'click', + 'dblclick', + 'move', + 'drag', + 'key', + 'type', + 'rightClick' + ], makeAction = function (name) { + syn[name] = function (element, options, callback) { + return syn('_' + name, element, options, callback); + }; + syn.init.prototype[name] = function (element, options, callback) { + return this.then('_' + name, element, options, callback); + }; + }, i = 0; +for (; i < actions.length; i++) { + makeAction(actions[i]); +} +module.exports = syn; +}); +/*syn@0.1.1#mouse*/ +define('syn/mouse', function(require, exports, module) { +var syn = require('syn/synthetic'); +var h = syn.helpers, getWin = h.getWindow; +syn.mouse = {}; +h.extend(syn.defaults, { + mousedown: function (options) { + syn.trigger(this, 'focus', {}); + }, + click: function () { + var element = this, href, type, createChange, radioChanged, nodeName, scope; + try { + href = element.href; + type = element.type; + createChange = syn.data(element, 'createChange'); + radioChanged = syn.data(element, 'radioChanged'); + scope = getWin(element); + nodeName = element.nodeName.toLowerCase(); + } catch (e) { + return; + } + if (!syn.support.linkHrefJS && /^\s*javascript:/.test(href)) { + var code = href.replace(/^\s*javascript:/, ''); + if (code !== '//' && code.indexOf('void(0)') === -1) { + if (window.selenium) { + eval('with(selenium.browserbot.getCurrentWindow()){' + code + '}'); + } else { + eval('with(scope){' + code + '}'); + } + } + } + if (!syn.support.clickSubmits && (nodeName === 'input' && type === 'submit') || nodeName === 'button') { + var form = syn.closest(element, 'form'); + if (form) { + syn.trigger(form, 'submit', {}); + } + } + if (nodeName === 'a' && element.href && !/^\s*javascript:/.test(href)) { + scope.location.href = href; + } + if (nodeName === 'input' && type === 'checkbox') { + if (!syn.support.clickChanges) { + syn.trigger(element, 'change', {}); + } + } + if (nodeName === 'input' && type === 'radio') { + if (radioChanged && !syn.support.radioClickChanges) { + syn.trigger(element, 'change', {}); + } + } + if (nodeName === 'option' && createChange) { + syn.trigger(element.parentNode, 'change', {}); + syn.data(element, 'createChange', false); + } + } +}); +h.extend(syn.create, { + mouse: { + options: function (type, options, element) { + var doc = document.documentElement, body = document.body, center = [ + options.pageX || 0, + options.pageY || 0 + ], left = syn.mouse.browser && syn.mouse.browser.left[type], right = syn.mouse.browser && syn.mouse.browser.right[type]; + return h.extend({ + bubbles: true, + cancelable: true, + view: window, + detail: 1, + screenX: 1, + screenY: 1, + clientX: options.clientX || center[0] - (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0), + clientY: options.clientY || center[1] - (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0), + ctrlKey: !!syn.key.ctrlKey, + altKey: !!syn.key.altKey, + shiftKey: !!syn.key.shiftKey, + metaKey: !!syn.key.metaKey, + button: left && left.button !== null ? left.button : right && right.button || (type === 'contextmenu' ? 2 : 0), + relatedTarget: document.documentElement + }, options); + }, + event: function (type, defaults, element) { + var doc = getWin(element).document || document, event; + if (doc.createEvent) { + try { + event = doc.createEvent('MouseEvents'); + event.initMouseEvent(type, defaults.bubbles, defaults.cancelable, defaults.view, defaults.detail, defaults.screenX, defaults.screenY, defaults.clientX, defaults.clientY, defaults.ctrlKey, defaults.altKey, defaults.shiftKey, defaults.metaKey, defaults.button, defaults.relatedTarget); + } catch (e) { + event = h.createBasicStandardEvent(type, defaults, doc); + } + event.synthetic = true; + return event; + } else { + try { + event = h.createEventObject(type, defaults, element); + } catch (e) { + } + return event; + } + } + }, + click: { + setup: function (type, options, element) { + var nodeName = element.nodeName.toLowerCase(); + if (!syn.support.clickChecks && !syn.support.changeChecks && nodeName === 'input') { + type = element.type.toLowerCase(); + if (type === 'checkbox') { + element.checked = !element.checked; + } + if (type === 'radio') { + if (!element.checked) { + try { + syn.data(element, 'radioChanged', true); + } catch (e) { + } + element.checked = true; + } + } + } + if (nodeName === 'a' && element.href && !/^\s*javascript:/.test(element.href)) { + syn.data(element, 'href', element.href); + } + if (/option/i.test(element.nodeName)) { + var child = element.parentNode.firstChild, i = -1; + while (child) { + if (child.nodeType === 1) { + i++; + if (child === element) { + break; + } + } + child = child.nextSibling; + } + if (i !== element.parentNode.selectedIndex) { + element.parentNode.selectedIndex = i; + syn.data(element, 'createChange', true); + } + } + } + }, + mousedown: { + setup: function (type, options, element) { + var nn = element.nodeName.toLowerCase(); + if (syn.browser.safari && (nn === 'select' || nn === 'option')) { + options._autoPrevent = true; + } + } + } +}); +}); +/*syn@0.1.1#mouse.support*/ +define('syn/mouse.support', function(require, exports, module) { +var syn = require('syn/synthetic'); +require('syn/mouse'); +if (!document.body) { + syn.schedule(function () { + checkSupport(syn); + }, 1); +} else { + window.__synthTest = function () { + syn.support.linkHrefJS = true; + }; + var div = document.createElement('div'), checkbox, submit, form, select; + div.innerHTML = '
      ' + '' + '' + '' + '' + '' + '' + '' + '' + '
      '; + document.documentElement.appendChild(div); + form = div.firstChild; + checkbox = form.childNodes[0]; + submit = form.childNodes[2]; + select = form.getElementsByTagName('select')[0]; + syn.trigger(form.childNodes[6], 'click', {}); + checkbox.checked = false; + checkbox.onchange = function () { + syn.support.clickChanges = true; + }; + syn.trigger(checkbox, 'click', {}); + syn.support.clickChecks = checkbox.checked; + checkbox.checked = false; + syn.trigger(checkbox, 'change', {}); + syn.support.changeChecks = checkbox.checked; + form.onsubmit = function (ev) { + if (ev.preventDefault) { + ev.preventDefault(); + } + syn.support.clickSubmits = true; + return false; + }; + syn.trigger(submit, 'click', {}); + form.childNodes[1].onchange = function () { + syn.support.radioClickChanges = true; + }; + syn.trigger(form.childNodes[1], 'click', {}); + syn.bind(div, 'click', function onclick() { + syn.support.optionClickBubbles = true; + syn.unbind(div, 'click', onclick); + }); + syn.trigger(select.firstChild, 'click', {}); + syn.support.changeBubbles = syn.eventSupported('change'); + div.onclick = function () { + syn.support.mouseDownUpClicks = true; + }; + syn.trigger(div, 'mousedown', {}); + syn.trigger(div, 'mouseup', {}); + document.documentElement.removeChild(div); + syn.support.ready++; +} +}); +/*syn@0.1.1#browsers*/ +define('syn/browsers', function(require, exports, module) { +var syn = require('syn/synthetic'); +require('syn/mouse'); +syn.key.browsers = { + webkit: { + 'prevent': { + 'keyup': [], + 'keydown': [ + 'char', + 'keypress' + ], + 'keypress': ['char'] + }, + 'character': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 'char', + 'char' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'specialChars': { + 'keydown': [ + 0, + 'char' + ], + 'keyup': [ + 0, + 'char' + ] + }, + 'navigation': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'special': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'tab': { + 'keydown': [ + 0, + 'char' + ], + 'keyup': [ + 0, + 'char' + ] + }, + 'pause-break': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'caps': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'escape': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'num-lock': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'scroll-lock': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'print': { + 'keyup': [ + 0, + 'key' + ] + }, + 'function': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + '\r': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 'char', + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + } + }, + gecko: { + 'prevent': { + 'keyup': [], + 'keydown': ['char'], + 'keypress': ['char'] + }, + 'character': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 'char', + 0 + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'specialChars': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'navigation': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'special': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + '\t': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'pause-break': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'caps': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'escape': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'num-lock': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'scroll-lock': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + 'print': { + 'keyup': [ + 0, + 'key' + ] + }, + 'function': { + 'keydown': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + }, + '\r': { + 'keydown': [ + 0, + 'key' + ], + 'keypress': [ + 0, + 'key' + ], + 'keyup': [ + 0, + 'key' + ] + } + }, + msie: { + 'prevent': { + 'keyup': [], + 'keydown': [ + 'char', + 'keypress' + ], + 'keypress': ['char'] + }, + 'character': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'char' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'specialChars': { + 'keydown': [ + null, + 'char' + ], + 'keyup': [ + null, + 'char' + ] + }, + 'navigation': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'special': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'tab': { + 'keydown': [ + null, + 'char' + ], + 'keyup': [ + null, + 'char' + ] + }, + 'pause-break': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'caps': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'escape': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'num-lock': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'scroll-lock': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'print': { + 'keyup': [ + null, + 'key' + ] + }, + 'function': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + '\r': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + } + }, + opera: { + 'prevent': { + 'keyup': [], + 'keydown': [], + 'keypress': ['char'] + }, + 'character': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'char' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'specialChars': { + 'keydown': [ + null, + 'char' + ], + 'keypress': [ + null, + 'char' + ], + 'keyup': [ + null, + 'char' + ] + }, + 'navigation': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ] + }, + 'special': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'tab': { + 'keydown': [ + null, + 'char' + ], + 'keypress': [ + null, + 'char' + ], + 'keyup': [ + null, + 'char' + ] + }, + 'pause-break': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'caps': { + 'keydown': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'escape': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ] + }, + 'num-lock': { + 'keyup': [ + null, + 'key' + ], + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ] + }, + 'scroll-lock': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + 'print': {}, + 'function': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + }, + '\r': { + 'keydown': [ + null, + 'key' + ], + 'keypress': [ + null, + 'key' + ], + 'keyup': [ + null, + 'key' + ] + } + } +}; +syn.mouse.browsers = { + webkit: { + 'right': { + 'mousedown': { + 'button': 2, + 'which': 3 + }, + 'mouseup': { + 'button': 2, + 'which': 3 + }, + 'contextmenu': { + 'button': 2, + 'which': 3 + } + }, + 'left': { + 'mousedown': { + 'button': 0, + 'which': 1 + }, + 'mouseup': { + 'button': 0, + 'which': 1 + }, + 'click': { + 'button': 0, + 'which': 1 + } + } + }, + opera: { + 'right': { + 'mousedown': { + 'button': 2, + 'which': 3 + }, + 'mouseup': { + 'button': 2, + 'which': 3 + } + }, + 'left': { + 'mousedown': { + 'button': 0, + 'which': 1 + }, + 'mouseup': { + 'button': 0, + 'which': 1 + }, + 'click': { + 'button': 0, + 'which': 1 + } + } + }, + msie: { + 'right': { + 'mousedown': { 'button': 2 }, + 'mouseup': { 'button': 2 }, + 'contextmenu': { 'button': 0 } + }, + 'left': { + 'mousedown': { 'button': 1 }, + 'mouseup': { 'button': 1 }, + 'click': { 'button': 0 } + } + }, + chrome: { + 'right': { + 'mousedown': { + 'button': 2, + 'which': 3 + }, + 'mouseup': { + 'button': 2, + 'which': 3 + }, + 'contextmenu': { + 'button': 2, + 'which': 3 + } + }, + 'left': { + 'mousedown': { + 'button': 0, + 'which': 1 + }, + 'mouseup': { + 'button': 0, + 'which': 1 + }, + 'click': { + 'button': 0, + 'which': 1 + } + } + }, + gecko: { + 'left': { + 'mousedown': { + 'button': 0, + 'which': 1 + }, + 'mouseup': { + 'button': 0, + 'which': 1 + }, + 'click': { + 'button': 0, + 'which': 1 + } + }, + 'right': { + 'mousedown': { + 'button': 2, + 'which': 3 + }, + 'mouseup': { + 'button': 2, + 'which': 3 + }, + 'contextmenu': { + 'button': 2, + 'which': 3 + } + } + } +}; +syn.key.browser = function () { + if (syn.key.browsers[window.navigator.userAgent]) { + return syn.key.browsers[window.navigator.userAgent]; + } + for (var browser in syn.browser) { + if (syn.browser[browser] && syn.key.browsers[browser]) { + return syn.key.browsers[browser]; + } + } + return syn.key.browsers.gecko; +}(); +syn.mouse.browser = function () { + if (syn.mouse.browsers[window.navigator.userAgent]) { + return syn.mouse.browsers[window.navigator.userAgent]; + } + for (var browser in syn.browser) { + if (syn.browser[browser] && syn.mouse.browsers[browser]) { + return syn.mouse.browsers[browser]; + } + } + return syn.mouse.browsers.gecko; +}(); +}); +/*syn@0.1.1#typeable*/ +define('syn/typeable', function(require, exports, module) { +var syn = require('syn/synthetic'); +var typeables = []; +var __indexOf = [].indexOf || function (item) { + for (var i = 0, l = this.length; i < l; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + }; +syn.typeable = function (fn) { + if (__indexOf.call(typeables, fn) === -1) { + typeables.push(fn); + } +}; +syn.typeable.test = function (el) { + for (var i = 0, len = typeables.length; i < len; i++) { + if (typeables[i](el)) { + return true; + } + } + return false; +}; +var type = syn.typeable; +var typeableExp = /input|textarea/i; +type(function (el) { + return typeableExp.test(el.nodeName); +}); +type(function (el) { + return __indexOf.call([ + '', + 'true' + ], el.getAttribute('contenteditable')) !== -1; +}); +}); +/*syn@0.1.1#key*/ +define('syn/key', function(require, exports, module) { +var syn = require('syn/synthetic'); +require('syn/typeable'); +require('syn/browsers'); +var h = syn.helpers, getSelection = function (el) { + var real, r, start; + if (el.selectionStart !== undefined) { + if (document.activeElement && document.activeElement !== el && el.selectionStart === el.selectionEnd && el.selectionStart === 0) { + return { + start: el.value.length, + end: el.value.length + }; + } + return { + start: el.selectionStart, + end: el.selectionEnd + }; + } else { + try { + if (el.nodeName.toLowerCase() === 'input') { + real = h.getWindow(el).document.selection.createRange(); + r = el.createTextRange(); + r.setEndPoint('EndToStart', real); + start = r.text.length; + return { + start: start, + end: start + real.text.length + }; + } else { + real = h.getWindow(el).document.selection.createRange(); + r = real.duplicate(); + var r2 = real.duplicate(), r3 = real.duplicate(); + r2.collapse(); + r3.collapse(false); + r2.moveStart('character', -1); + r3.moveStart('character', -1); + r.moveToElementText(el); + r.setEndPoint('EndToEnd', real); + start = r.text.length - real.text.length; + var end = r.text.length; + if (start !== 0 && r2.text === '') { + start += 2; + } + if (end !== 0 && r3.text === '') { + end += 2; + } + return { + start: start, + end: end + }; + } + } catch (e) { + var prop = formElExp.test(el.nodeName) ? 'value' : 'textContent'; + return { + start: el[prop].length, + end: el[prop].length + }; + } + } + }, getFocusable = function (el) { + var document = h.getWindow(el).document, res = []; + var els = document.getElementsByTagName('*'), len = els.length; + for (var i = 0; i < len; i++) { + if (syn.isFocusable(els[i]) && els[i] !== document.documentElement) { + res.push(els[i]); + } + } + return res; + }, formElExp = /input|textarea/i, textProperty = function () { + var el = document.createElement('span'); + return el.textContent != null ? 'textContent' : 'innerText'; + }(), getText = function (el) { + if (formElExp.test(el.nodeName)) { + return el.value; + } + return el[textProperty]; + }, setText = function (el, value) { + if (formElExp.test(el.nodeName)) { + el.value = value; + } else { + el[textProperty] = value; + } + }; +h.extend(syn, { + keycodes: { + '\b': 8, + '\t': 9, + '\r': 13, + 'shift': 16, + 'ctrl': 17, + 'alt': 18, + 'pause-break': 19, + 'caps': 20, + 'escape': 27, + 'num-lock': 144, + 'scroll-lock': 145, + 'print': 44, + 'page-up': 33, + 'page-down': 34, + 'end': 35, + 'home': 36, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + 'insert': 45, + 'delete': 46, + ' ': 32, + '0': 48, + '1': 49, + '2': 50, + '3': 51, + '4': 52, + '5': 53, + '6': 54, + '7': 55, + '8': 56, + '9': 57, + 'a': 65, + 'b': 66, + 'c': 67, + 'd': 68, + 'e': 69, + 'f': 70, + 'g': 71, + 'h': 72, + 'i': 73, + 'j': 74, + 'k': 75, + 'l': 76, + 'm': 77, + 'n': 78, + 'o': 79, + 'p': 80, + 'q': 81, + 'r': 82, + 's': 83, + 't': 84, + 'u': 85, + 'v': 86, + 'w': 87, + 'x': 88, + 'y': 89, + 'z': 90, + 'num0': 96, + 'num1': 97, + 'num2': 98, + 'num3': 99, + 'num4': 100, + 'num5': 101, + 'num6': 102, + 'num7': 103, + 'num8': 104, + 'num9': 105, + '*': 106, + '+': 107, + 'subtract': 109, + 'decimal': 110, + 'divide': 111, + ';': 186, + '=': 187, + ',': 188, + 'dash': 189, + '-': 189, + 'period': 190, + '.': 190, + 'forward-slash': 191, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + '\'': 222, + 'left window key': 91, + 'right window key': 92, + 'select key': 93, + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123 + }, + selectText: function (el, start, end) { + if (el.setSelectionRange) { + if (!end) { + syn.__tryFocus(el); + el.setSelectionRange(start, start); + } else { + el.selectionStart = start; + el.selectionEnd = end; + } + } else if (el.createTextRange) { + var r = el.createTextRange(); + r.moveStart('character', start); + end = end || start; + r.moveEnd('character', end - el.value.length); + r.select(); + } + }, + getText: function (el) { + if (syn.typeable.test(el)) { + var sel = getSelection(el); + return el.value.substring(sel.start, sel.end); + } + var win = syn.helpers.getWindow(el); + if (win.getSelection) { + return win.getSelection().toString(); + } else if (win.document.getSelection) { + return win.document.getSelection().toString(); + } else { + return win.document.selection.createRange().text; + } + }, + getSelection: getSelection +}); +h.extend(syn.key, { + data: function (key) { + if (syn.key.browser[key]) { + return syn.key.browser[key]; + } + for (var kind in syn.key.kinds) { + if (h.inArray(key, syn.key.kinds[kind]) > -1) { + return syn.key.browser[kind]; + } + } + return syn.key.browser.character; + }, + isSpecial: function (keyCode) { + var specials = syn.key.kinds.special; + for (var i = 0; i < specials.length; i++) { + if (syn.keycodes[specials[i]] === keyCode) { + return specials[i]; + } + } + }, + options: function (key, event) { + var keyData = syn.key.data(key); + if (!keyData[event]) { + return null; + } + var charCode = keyData[event][0], keyCode = keyData[event][1], result = {}; + if (keyCode === 'key') { + result.keyCode = syn.keycodes[key]; + } else if (keyCode === 'char') { + result.keyCode = key.charCodeAt(0); + } else { + result.keyCode = keyCode; + } + if (charCode === 'char') { + result.charCode = key.charCodeAt(0); + } else if (charCode !== null) { + result.charCode = charCode; + } + if (result.keyCode) { + result.which = result.keyCode; + } else { + result.which = result.charCode; + } + return result; + }, + kinds: { + special: [ + 'shift', + 'ctrl', + 'alt', + 'caps' + ], + specialChars: ['\b'], + navigation: [ + 'page-up', + 'page-down', + 'end', + 'home', + 'left', + 'up', + 'right', + 'down', + 'insert', + 'delete' + ], + 'function': [ + 'f1', + 'f2', + 'f3', + 'f4', + 'f5', + 'f6', + 'f7', + 'f8', + 'f9', + 'f10', + 'f11', + 'f12' + ] + }, + getDefault: function (key) { + if (syn.key.defaults[key]) { + return syn.key.defaults[key]; + } + for (var kind in syn.key.kinds) { + if (h.inArray(key, syn.key.kinds[kind]) > -1 && syn.key.defaults[kind]) { + return syn.key.defaults[kind]; + } + } + return syn.key.defaults.character; + }, + defaults: { + 'character': function (options, scope, key, force, sel) { + if (/num\d+/.test(key)) { + key = key.match(/\d+/)[0]; + } + if (force || !syn.support.keyCharacters && syn.typeable.test(this)) { + var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end), character = key; + setText(this, before + character + after); + var charLength = character === '\n' && syn.support.textareaCarriage ? 2 : character.length; + syn.selectText(this, before.length + charLength); + } + }, + 'c': function (options, scope, key, force, sel) { + if (syn.key.ctrlKey) { + syn.key.clipboard = syn.getText(this); + } else { + syn.key.defaults.character.apply(this, arguments); + } + }, + 'v': function (options, scope, key, force, sel) { + if (syn.key.ctrlKey) { + syn.key.defaults.character.call(this, options, scope, syn.key.clipboard, true, sel); + } else { + syn.key.defaults.character.apply(this, arguments); + } + }, + 'a': function (options, scope, key, force, sel) { + if (syn.key.ctrlKey) { + syn.selectText(this, 0, getText(this).length); + } else { + syn.key.defaults.character.apply(this, arguments); + } + }, + 'home': function () { + syn.onParents(this, function (el) { + if (el.scrollHeight !== el.clientHeight) { + el.scrollTop = 0; + return false; + } + }); + }, + 'end': function () { + syn.onParents(this, function (el) { + if (el.scrollHeight !== el.clientHeight) { + el.scrollTop = el.scrollHeight; + return false; + } + }); + }, + 'page-down': function () { + syn.onParents(this, function (el) { + if (el.scrollHeight !== el.clientHeight) { + var ch = el.clientHeight; + el.scrollTop += ch; + return false; + } + }); + }, + 'page-up': function () { + syn.onParents(this, function (el) { + if (el.scrollHeight !== el.clientHeight) { + var ch = el.clientHeight; + el.scrollTop -= ch; + return false; + } + }); + }, + '\b': function (options, scope, key, force, sel) { + if (!syn.support.backspaceWorks && syn.typeable.test(this)) { + var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end); + if (sel.start === sel.end && sel.start > 0) { + setText(this, before.substring(0, before.length - 1) + after); + syn.selectText(this, sel.start - 1); + } else { + setText(this, before + after); + syn.selectText(this, sel.start); + } + } + }, + 'delete': function (options, scope, key, force, sel) { + if (!syn.support.backspaceWorks && syn.typeable.test(this)) { + var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end); + if (sel.start === sel.end && sel.start <= getText(this).length - 1) { + setText(this, before + after.substring(1)); + } else { + setText(this, before + after); + } + syn.selectText(this, sel.start); + } + }, + '\r': function (options, scope, key, force, sel) { + var nodeName = this.nodeName.toLowerCase(); + if (nodeName === 'input') { + syn.trigger(this, 'change', {}); + } + if (!syn.support.keypressSubmits && nodeName === 'input') { + var form = syn.closest(this, 'form'); + if (form) { + syn.trigger(form, 'submit', {}); + } + } + if (!syn.support.keyCharacters && nodeName === 'textarea') { + syn.key.defaults.character.call(this, options, scope, '\n', undefined, sel); + } + if (!syn.support.keypressOnAnchorClicks && nodeName === 'a') { + syn.trigger(this, 'click', {}); + } + }, + '\t': function (options, scope) { + var focusEls = getFocusable(this), current = null, i = 0, el, firstNotIndexed, orders = []; + for (; i < focusEls.length; i++) { + orders.push([ + focusEls[i], + i + ]); + } + var sort = function (order1, order2) { + var el1 = order1[0], el2 = order2[0], tab1 = syn.tabIndex(el1) || 0, tab2 = syn.tabIndex(el2) || 0; + if (tab1 === tab2) { + return order1[1] - order2[1]; + } else { + if (tab1 === 0) { + return 1; + } else if (tab2 === 0) { + return -1; + } else { + return tab1 - tab2; + } + } + }; + orders.sort(sort); + for (i = 0; i < orders.length; i++) { + el = orders[i][0]; + if (this === el) { + if (!syn.key.shiftKey) { + current = orders[i + 1][0]; + if (!current) { + current = orders[0][0]; + } + } else { + current = orders[i - 1][0]; + if (!current) { + current = orders[focusEls.length - 1][0]; + } + } + } + } + if (!current) { + current = firstNotIndexed; + } else { + syn.__tryFocus(current); + } + return current; + }, + 'left': function (options, scope, key, force, sel) { + if (syn.typeable.test(this)) { + if (syn.key.shiftKey) { + syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1, sel.end); + } else { + syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1); + } + } + }, + 'right': function (options, scope, key, force, sel) { + if (syn.typeable.test(this)) { + if (syn.key.shiftKey) { + syn.selectText(this, sel.start, sel.end + 1 > getText(this).length ? getText(this).length : sel.end + 1); + } else { + syn.selectText(this, sel.end + 1 > getText(this).length ? getText(this).length : sel.end + 1); + } + } + }, + 'up': function () { + if (/select/i.test(this.nodeName)) { + this.selectedIndex = this.selectedIndex ? this.selectedIndex - 1 : 0; + } + }, + 'down': function () { + if (/select/i.test(this.nodeName)) { + syn.changeOnBlur(this, 'selectedIndex', this.selectedIndex); + this.selectedIndex = this.selectedIndex + 1; + } + }, + 'shift': function () { + return null; + }, + 'ctrl': function () { + return null; + } + } +}); +h.extend(syn.create, { + keydown: { + setup: function (type, options, element) { + if (h.inArray(options, syn.key.kinds.special) !== -1) { + syn.key[options + 'Key'] = element; + } + } + }, + keypress: { + setup: function (type, options, element) { + if (syn.support.keyCharacters && !syn.support.keysOnNotFocused) { + syn.__tryFocus(element); + } + } + }, + keyup: { + setup: function (type, options, element) { + if (h.inArray(options, syn.key.kinds.special) !== -1) { + syn.key[options + 'Key'] = null; + } + } + }, + key: { + options: function (type, options, element) { + options = typeof options !== 'object' ? { character: options } : options; + options = h.extend({}, options); + if (options.character) { + h.extend(options, syn.key.options(options.character, type)); + delete options.character; + } + options = h.extend({ + ctrlKey: !!syn.key.ctrlKey, + altKey: !!syn.key.altKey, + shiftKey: !!syn.key.shiftKey, + metaKey: !!syn.key.metaKey + }, options); + return options; + }, + event: function (type, options, element) { + var doc = h.getWindow(element).document || document, event; + if (doc.createEvent) { + try { + event = doc.createEvent('KeyEvents'); + event.initKeyEvent(type, true, true, window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode); + } catch (e) { + event = h.createBasicStandardEvent(type, options, doc); + } + event.synthetic = true; + return event; + } else { + try { + event = h.createEventObject.apply(this, arguments); + h.extend(event, options); + } catch (e) { + } + return event; + } + } + } +}); +var convert = { + 'enter': '\r', + 'backspace': '\b', + 'tab': '\t', + 'space': ' ' + }; +h.extend(syn.init.prototype, { + _key: function (element, options, callback) { + if (/-up$/.test(options) && h.inArray(options.replace('-up', ''), syn.key.kinds.special) !== -1) { + syn.trigger(element, 'keyup', options.replace('-up', '')); + return callback(true, element); + } + var activeElement = h.getWindow(element).document.activeElement, caret = syn.typeable.test(element) && getSelection(element), key = convert[options] || options, runDefaults = syn.trigger(element, 'keydown', key), getDefault = syn.key.getDefault, prevent = syn.key.browser.prevent, defaultResult, keypressOptions = syn.key.options(key, 'keypress'); + if (runDefaults) { + if (!keypressOptions) { + defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret); + } else { + if (activeElement !== h.getWindow(element).document.activeElement) { + element = h.getWindow(element).document.activeElement; + } + runDefaults = syn.trigger(element, 'keypress', keypressOptions); + if (runDefaults) { + defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret); + } + } + } else { + if (keypressOptions && h.inArray('keypress', prevent.keydown) === -1) { + if (activeElement !== h.getWindow(element).document.activeElement) { + element = h.getWindow(element).document.activeElement; + } + syn.trigger(element, 'keypress', keypressOptions); + } + } + if (defaultResult && defaultResult.nodeName) { + element = defaultResult; + } + if (defaultResult !== null) { + syn.schedule(function () { + if (syn.support.oninput) { + syn.trigger(element, 'input', syn.key.options(key, 'input')); + } + syn.trigger(element, 'keyup', syn.key.options(key, 'keyup')); + callback(runDefaults, element); + }, 1); + } else { + callback(runDefaults, element); + } + return element; + }, + _type: function (element, options, callback) { + var parts = (options + '').match(/(\[[^\]]+\])|([^\[])/g), self = this, runNextPart = function (runDefaults, el) { + var part = parts.shift(); + if (!part) { + callback(runDefaults, el); + return; + } + el = el || element; + if (part.length > 1) { + part = part.substr(1, part.length - 2); + } + self._key(el, part, runNextPart); + }; + runNextPart(); + } +}); +}); +/*syn@0.1.1#key.support*/ +define('syn/key.support', function(require, exports, module) { +var syn = require('syn/synthetic'); +require('syn/key'); +if (!syn.config.support) { + (function checkForSupport() { + if (!document.body) { + return syn.schedule(checkForSupport, 1); + } + var div = document.createElement('div'), checkbox, submit, form, anchor, textarea, inputter, one, doc; + doc = document.documentElement; + div.innerHTML = '
      ' + '' + '' + '' + '' + '' + '' + '' + '' + '
      '; + doc.insertBefore(div, doc.firstElementChild || doc.children[0]); + form = div.firstChild; + checkbox = form.childNodes[0]; + submit = form.childNodes[2]; + anchor = form.getElementsByTagName('a')[0]; + textarea = form.getElementsByTagName('textarea')[0]; + inputter = form.childNodes[3]; + one = form.childNodes[4]; + form.onsubmit = function (ev) { + if (ev.preventDefault) { + ev.preventDefault(); + } + syn.support.keypressSubmits = true; + ev.returnValue = false; + return false; + }; + syn.__tryFocus(inputter); + syn.trigger(inputter, 'keypress', '\r'); + syn.trigger(inputter, 'keypress', 'a'); + syn.support.keyCharacters = inputter.value === 'a'; + inputter.value = 'a'; + syn.trigger(inputter, 'keypress', '\b'); + syn.support.backspaceWorks = inputter.value === ''; + inputter.onchange = function () { + syn.support.focusChanges = true; + }; + syn.__tryFocus(inputter); + syn.trigger(inputter, 'keypress', 'a'); + syn.__tryFocus(form.childNodes[5]); + syn.trigger(inputter, 'keypress', 'b'); + syn.support.keysOnNotFocused = inputter.value === 'ab'; + syn.bind(anchor, 'click', function (ev) { + if (ev.preventDefault) { + ev.preventDefault(); + } + syn.support.keypressOnAnchorClicks = true; + ev.returnValue = false; + return false; + }); + syn.trigger(anchor, 'keypress', '\r'); + syn.support.textareaCarriage = textarea.value.length === 4; + syn.support.oninput = 'oninput' in one; + doc.removeChild(div); + syn.support.ready++; + }()); +} else { + syn.helpers.extend(syn.support, syn.config.support); +} +}); +/*syn@0.1.1#drag*/ +define('syn/drag', function(require, exports, module) { +var syn = require('syn/synthetic'); +(function dragSupport() { + if (!document.body) { + syn.schedule(dragSupport, 1); + return; + } + var div = document.createElement('div'); + document.body.appendChild(div); + syn.helpers.extend(div.style, { + width: '100px', + height: '10000px', + backgroundColor: 'blue', + position: 'absolute', + top: '10px', + left: '0px', + zIndex: 19999 + }); + document.body.scrollTop = 11; + if (!document.elementFromPoint) { + return; + } + var el = document.elementFromPoint(3, 1); + if (el === div) { + syn.support.elementFromClient = true; + } else { + syn.support.elementFromPage = true; + } + document.body.removeChild(div); + document.body.scrollTop = 0; +}()); +var elementFromPoint = function (point, element) { + var clientX = point.clientX, clientY = point.clientY, win = syn.helpers.getWindow(element), el; + if (syn.support.elementFromPage) { + var off = syn.helpers.scrollOffset(win); + clientX = clientX + off.left; + clientY = clientY + off.top; + } + el = win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element; + if (el === win.document.documentElement && (point.clientY < 0 || point.clientX < 0)) { + return element; + } else { + return el; + } + }, createEventAtPoint = function (event, point, element) { + var el = elementFromPoint(point, element); + syn.trigger(el || element, event, point); + return el; + }, mouseMove = function (point, element, last) { + var el = elementFromPoint(point, element); + if (last !== el && el && last) { + var options = syn.helpers.extend({}, point); + options.relatedTarget = el; + syn.trigger(last, 'mouseout', options); + options.relatedTarget = last; + syn.trigger(el, 'mouseover', options); + } + syn.trigger(el || element, 'mousemove', point); + return el; + }, startMove = function (start, end, duration, element, callback) { + var startTime = new Date(), distX = end.clientX - start.clientX, distY = end.clientY - start.clientY, win = syn.helpers.getWindow(element), current = elementFromPoint(start, element), cursor = win.document.createElement('div'), calls = 0, move; + move = function onmove() { + var now = new Date(), scrollOffset = syn.helpers.scrollOffset(win), fraction = (calls === 0 ? 0 : now - startTime) / duration, options = { + clientX: distX * fraction + start.clientX, + clientY: distY * fraction + start.clientY + }; + calls++; + if (fraction < 1) { + syn.helpers.extend(cursor.style, { + left: options.clientX + scrollOffset.left + 2 + 'px', + top: options.clientY + scrollOffset.top + 2 + 'px' + }); + current = mouseMove(options, element, current); + syn.schedule(onmove, 15); + } else { + current = mouseMove(end, element, current); + win.document.body.removeChild(cursor); + callback(); + } + }; + syn.helpers.extend(cursor.style, { + height: '5px', + width: '5px', + backgroundColor: 'red', + position: 'absolute', + zIndex: 19999, + fontSize: '1px' + }); + win.document.body.appendChild(cursor); + move(); + }, startDrag = function (start, end, duration, element, callback) { + createEventAtPoint('mousedown', start, element); + startMove(start, end, duration, element, function () { + createEventAtPoint('mouseup', end, element); + callback(); + }); + }, center = function (el) { + var j = syn.jquery()(el), o = j.offset(); + return { + pageX: o.left + j.outerWidth() / 2, + pageY: o.top + j.outerHeight() / 2 + }; + }, convertOption = function (option, win, from) { + var page = /(\d+)[x ](\d+)/, client = /(\d+)X(\d+)/, relative = /([+-]\d+)[xX ]([+-]\d+)/, parts; + if (typeof option === 'string' && relative.test(option) && from) { + var cent = center(from); + parts = option.match(relative); + option = { + pageX: cent.pageX + parseInt(parts[1]), + pageY: cent.pageY + parseInt(parts[2]) + }; + } + if (typeof option === 'string' && page.test(option)) { + parts = option.match(page); + option = { + pageX: parseInt(parts[1]), + pageY: parseInt(parts[2]) + }; + } + if (typeof option === 'string' && client.test(option)) { + parts = option.match(client); + option = { + clientX: parseInt(parts[1]), + clientY: parseInt(parts[2]) + }; + } + if (typeof option === 'string') { + option = syn.jquery()(option, win.document)[0]; + } + if (option.nodeName) { + option = center(option); + } + if (option.pageX) { + var off = syn.helpers.scrollOffset(win); + option = { + clientX: option.pageX - off.left, + clientY: option.pageY - off.top + }; + } + return option; + }, adjust = function (from, to, win) { + if (from.clientY < 0) { + var off = syn.helpers.scrollOffset(win); + var top = off.top + from.clientY - 100, diff = top - off.top; + if (top > 0) { + } else { + top = 0; + diff = -off.top; + } + from.clientY = from.clientY - diff; + to.clientY = to.clientY - diff; + syn.helpers.scrollOffset(win, { + top: top, + left: off.left + }); + } + }; +syn.helpers.extend(syn.init.prototype, { + _move: function (from, options, callback) { + var win = syn.helpers.getWindow(from), fro = convertOption(options.from || from, win, from), to = convertOption(options.to || options, win, from); + if (options.adjust !== false) { + adjust(fro, to, win); + } + startMove(fro, to, options.duration || 500, from, callback); + }, + _drag: function (from, options, callback) { + var win = syn.helpers.getWindow(from), fro = convertOption(options.from || from, win, from), to = convertOption(options.to || options, win, from); + if (options.adjust !== false) { + adjust(fro, to, win); + } + startDrag(fro, to, options.duration || 500, from, callback); + } +}); +}); +/*syn@0.1.1#syn*/ +define('syn', function(require, exports, module) { +var syn = require('syn/synthetic'); +require('syn/mouse.support'); +require('syn/browsers'); +require('syn/key.support'); +require('syn/drag'); +window.syn = syn; +module.exports = syn; +}); From deca6c0ec3c0c5643e581a7665460ef391f05398 Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Tue, 19 Sep 2017 00:00:39 +0300 Subject: [PATCH 2/3] Remove embedded qunit.js and syn.js, only use them as node modules I will squash this commit with the previous so that qunit.js and syn.js are never pushed to the repo. --- karma.conf.js | 5 +- package.json | 3 +- qunit_test_runner.html | 4 +- test/core_tests_presentation.html | 2 +- test/qunit/qunit.css | 301 --- test/qunit/qunit.js | 4158 ----------------------------- test/syn/syn.js | 2422 ----------------- 7 files changed, 8 insertions(+), 6887 deletions(-) delete mode 100644 test/qunit/qunit.css delete mode 100644 test/qunit/qunit.js delete mode 100644 test/syn/syn.js diff --git a/karma.conf.js b/karma.conf.js index a99351a..4d42e54 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,8 @@ module.exports = function( config ) { proxies : { '/test/' : '/base/test/', - '/js/' : '/base/js/' + '/js/' : '/base/js/', + '/node_modules/syn/dist/' : '/base/node_modules/syn/dist/' }, // List of files / patterns to load in the browser @@ -25,7 +26,7 @@ module.exports = function( config ) { {pattern: "test/plugins/*/*.html", watched: true, served: true, included: false}, // JS files for iframe {pattern: "js/impress.js", watched: true, served: true, included: false}, - {pattern: "test/syn/syn.js", watched: true, served: true, included: false} + {pattern: "node_modules/syn/dist/global/syn.js", watched: false, served: true, included: false} ], // List of files to exclude diff --git a/package.json b/package.json index 982a5c8..89860be 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "karma-firefox-launcher": "~0.1", "karma-qunit": "1.0.0", "karma-sauce-launcher": "1.0.0", - "qunitjs": "2.0.0-rc1" + "qunitjs": "2.0.0-rc1", + "syn": "*" } } diff --git a/qunit_test_runner.html b/qunit_test_runner.html index cc316bf..92f24d6 100644 --- a/qunit_test_runner.html +++ b/qunit_test_runner.html @@ -7,14 +7,14 @@ QUnit tests for impress.js - +
      - + diff --git a/test/core_tests_presentation.html b/test/core_tests_presentation.html index 52152ef..34ebb9f 100644 --- a/test/core_tests_presentation.html +++ b/test/core_tests_presentation.html @@ -19,6 +19,6 @@ - + diff --git a/test/qunit/qunit.css b/test/qunit/qunit.css deleted file mode 100644 index a59e282..0000000 --- a/test/qunit/qunit.css +++ /dev/null @@ -1,301 +0,0 @@ -/*! - * QUnit 1.20.0 - * http://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-10-27T17:53Z - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #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-filteredTest { - padding: 0.5em 1em 0.5em 1em; - background-color: #F4FF77; - color: #366097; -} - -#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 { - visibility: hidden; - position: absolute; - width: 0; - height: 0; - padding: 0; - border: 0; - margin: 0; -} - -#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 p a { - padding: 0.25em; - color: #6B6464; -} -#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-source { - margin: 0.6em 0 0.3em; -} - -.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; -} diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js deleted file mode 100644 index 904943f..0000000 --- a/test/qunit/qunit.js +++ /dev/null @@ -1,4158 +0,0 @@ -/*! - * QUnit 1.20.0 - * http://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-10-27T17:53Z - */ - -(function( global ) { - -var QUnit = {}; - -var Date = global.Date; -var now = Date.now || function() { - return new Date().getTime(); -}; - -var setTimeout = global.setTimeout; -var clearTimeout = global.clearTimeout; - -// Store a local window from the global to allow direct references. -var window = global.window; - -var defined = { - document: window && window.document !== undefined, - setTimeout: setTimeout !== undefined, - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }() ) -}; - -var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); -var globalStartCalled = false; -var runStarted = false; - -var toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty; - -// 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; -} - -// 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; -} - -/** - * 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). - */ -function objectValues ( 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; -} - -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 - // This block runs on every environment, so `global` is being used instead of `window` - // to avoid errors on node. - if ( prop !== "constructor" || a !== global ) { - if ( b[ prop ] === undefined ) { - delete a[ prop ]; - } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { - a[ prop ] = b[ prop ]; - } - } - } - } - - return a; -} - -function objectType( 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 "Set": - case "Map": - case "Date": - case "RegExp": - case "Function": - case "Symbol": - return type.toLowerCase(); - } - if ( typeof obj === "object" ) { - return "object"; - } -} - -// Safe object type checking -function is( type, obj ) { - return QUnit.objectType( obj ) === type; -} - -var getUrlParams = function() { - var i, current; - var urlParams = {}; - var location = window.location; - var params = location.search.slice( 1 ).split( "&" ); - var length = params.length; - - 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 ]; - } - } - } - - return urlParams; -}; - -// Doesn't support IE6 to IE9, it will return undefined on these browsers -// 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.stack ) { - 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 ]; - - // Support: Safari <=6 only - } else if ( e.sourceURL ) { - - // 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 error = new Error(); - - // Support: Safari <=7 only, IE <=10 - 11 only - // Not all browsers generate the `stack` property for `new Error()`, see also #636 - if ( !error.stack ) { - try { - throw error; - } catch ( err ) { - error = err; - } - } - - return extractStacktrace( error, offset ); -} - -/** - * Config object: Maintain internal state - * Later exposed as QUnit.config - * `config` initialized at top of scope - */ -var 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, - - // HTML Reporter: collapse every test except the first failing test - // If false, all failing tests will be expanded - collapse: true, - - // by default, scroll to top of the page when suite is done - scrolltop: true, - - // depth up-to which object will be dumped - maxDepth: 5, - - // 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 " + - "global object (`window` in Browsers). 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: [], - - // Stack of nested modules - moduleStack: [], - - // The first unnamed module - currentModule: { - name: "", - tests: [] - }, - - callbacks: {} -}; - -var urlParams = defined.document ? getUrlParams() : {}; - -// Push a loose unnamed module to the modules collection -config.modules.push( config.currentModule ); - -if ( urlParams.filter === true ) { - delete urlParams.filter; -} - -// String search anywhere in moduleName+testName -config.filter = urlParams.filter; - -config.testId = []; -if ( urlParams.testId ) { - // Ensure that urlParams.testId is an array - urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); - for (var i = 0; i < urlParams.testId.length; i++ ) { - config.testId.push( urlParams.testId[ i ] ); - } -} - -var loggingCallbacks = {}; - -// Register logging callbacks -function registerLoggingCallbacks( obj ) { - var i, l, key, - callbackNames = [ "begin", "done", "log", "testStart", "testDone", - "moduleStart", "moduleDone" ]; - - function registerLoggingCallback( key ) { - var loggingCallback = function( callback ) { - if ( 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 = callbackNames.length; i < l; i++ ) { - key = callbackNames[ i ]; - - // Initialize key collection of logging callback - if ( objectType( config.callbacks[ key ] ) === "undefined" ) { - config.callbacks[ key ] = []; - } - - obj[ key ] = registerLoggingCallback( key ); - } -} - -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 ( global.console && global.console.warn ) { - global.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/" - ); - } - } - } -} - -( function() { - if ( !defined.document ) { - return; - } - - // `onErrorFnPrev` initialized at top of scope - // Preserve other handlers - var 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; - }; -} )(); - -QUnit.urlParams = urlParams; - -// Figure out if we're running the tests from a server or not -QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); - -// Expose the current QUnit version -QUnit.version = "1.20.0"; - -extend( QUnit, { - - // call on start of module test to prepend name to all tests - module: function( name, testEnvironment, executeNow ) { - var module, moduleFns; - var currentModule = config.currentModule; - - if ( arguments.length === 2 ) { - if ( testEnvironment instanceof Function ) { - executeNow = testEnvironment; - testEnvironment = undefined; - } - } - - // 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; - } - - module = createModule(); - - moduleFns = { - beforeEach: setHook( module, "beforeEach" ), - afterEach: setHook( module, "afterEach" ) - }; - - if ( executeNow instanceof Function ) { - config.moduleStack.push( module ); - setCurrentModule( module ); - executeNow.call( module.testEnvironment, moduleFns ); - config.moduleStack.pop(); - module = module.parentModule || currentModule; - } - - setCurrentModule( module ); - - function createModule() { - var parentModule = config.moduleStack.length ? - config.moduleStack.slice( -1 )[ 0 ] : null; - var moduleName = parentModule !== null ? - [ parentModule.name, name ].join( " > " ) : name; - var module = { - name: moduleName, - parentModule: parentModule, - tests: [] - }; - - var env = {}; - if ( parentModule ) { - extend( env, parentModule.testEnvironment ); - delete env.beforeEach; - delete env.afterEach; - } - extend( env, testEnvironment ); - module.testEnvironment = env; - - config.modules.push( module ); - return module; - } - - function setCurrentModule( module ) { - config.currentModule = module; - } - - }, - - // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. - asyncTest: asyncTest, - - test: test, - - skip: skip, - - only: only, - - // 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; - - // If semaphore is non-numeric, throw error - if ( isNaN( config.current.semaphore ) ) { - config.current.semaphore = 0; - - QUnit.pushFailure( - "Called start() with a non-numeric decrement.", - sourceFromStacktrace( 2 ) - ); - return; - } - - // 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, - - is: is, - - objectType: objectType, - - 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(); - } - }, - - stack: function( offset ) { - offset = ( offset || 0 ) + 2; - return sourceFromStacktrace( offset ); - } -}); - -registerLoggingCallbacks( QUnit ); - -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 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 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 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 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 - }); -} - -function setHook( module, hookName ) { - if ( module.testEnvironment === undefined ) { - module.testEnvironment = {}; - } - - return function( callback ) { - module.testEnvironment[ hookName ] = callback; - }; -} - -var focused = false; - -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; - - if ( this.module.testEnvironment ) { - delete this.module.testEnvironment.beforeEach; - delete this.module.testEnvironment.afterEach; - } - this.testEnvironment = extend( {}, this.module.testEnvironment ); - - 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 ) { - runTest( this ); - return; - } - - try { - runTest( this ); - } 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(); - } - } - - function runTest( test ) { - promise = test.callback.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise ); - } - }, - - after: function() { - checkPollution(); - }, - - queueHook: function( hook, hookName ) { - var promise, - test = this; - return function runHook() { - config.current = test; - if ( config.notrycatch ) { - callHook(); - return; - } - try { - callHook(); - } catch ( error ) { - test.pushFailure( hookName + " failed on " + test.testName + ": " + - ( error.message || error ), extractStacktrace( error, 0 ) ); - } - - function callHook() { - promise = hook.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise, hookName ); - } - }; - }, - - // Currently only used for module level hooks, can be used to add global level ones - hooks: function( handler ) { - var hooks = []; - - function processHooks( test, module ) { - if ( module.parentModule ) { - processHooks( test, module.parentModule ); - } - if ( module.testEnvironment && - QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { - hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); - } - } - - // Hooks are ignored on skipped tests - if ( !this.skip ) { - processHooks( this, this.module ); - } - 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, - - // Source of Test - source: this.stack, - - // 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 priority, - 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(); - } - ]); - } - - // Prioritize previously failed tests, detected from sessionStorage - priority = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); - - return synchronize( run, priority ); - }, - - push: function( result, actual, expected, message, negative ) { - var source, - details = { - module: this.module.name, - name: this.testName, - result: result, - message: message, - actual: actual, - expected: expected, - testId: this.testId, - negative: negative || false, - 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, - function() { 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 && config.filter.toLowerCase(), - module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), - fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); - - function testInModuleChain( testModule ) { - var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; - if ( testModuleName === module ) { - return true; - } else if ( testModule.parentModule ) { - return testInModuleChain( testModule.parentModule ); - } else { - return false; - } - } - - // 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 ) { - return false; - } - - if ( module && !testInModuleChain( this.module ) ) { - return false; - } - - if ( !filter ) { - return true; - } - - include = filter.charAt( 0 ) !== "!"; - if ( !include ) { - filter = filter.slice( 1 ); - } - - // If the filter matches, we need to honour include - if ( fullName.indexOf( filter ) !== -1 ) { - return include; - } - - // Otherwise, do the opposite - return !include; - } -}; - -// Resets the test setup. Useful for tests that modify the DOM. -/* -DEPRECATED: Use multiple tests instead of resetting inside a test. -Use testStart or testDone for custom cleanup. -This method will throw an error in 2.0, and will be removed in 2.1 -*/ -QUnit.reset = function() { - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( !defined.document ) { - return; - } - - var fixture = defined.document && document.getElementById && - document.getElementById( "qunit-fixture" ); - - if ( fixture ) { - fixture.innerHTML = config.fixture; - } -}; - -QUnit.pushFailure = function() { - if ( !QUnit.config.current ) { - throw new Error( "pushFailure() assertion outside test context, in " + - sourceFromStacktrace( 2 ) ); - } - - // Gets current test obj - var currentTest = QUnit.config.current; - - return currentTest.pushFailure.apply( currentTest, arguments ); -}; - -// Based on Java's String.hashCode, a simple but not -// rigorously collision resistant hashing function -function generateHash( module, testName ) { - var hex, - i = 0, - hash = 0, - str = module + "\x1C" + testName, - len = str.length; - - for ( ; i < len; i++ ) { - hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); - hash |= 0; - } - - // Convert the possibly negative integer hash code into an 8 character hex string, which isn't - // strictly necessary but increases user understanding that the id is a SHA-like hash - hex = ( 0x100000000 + hash ).toString( 16 ); - if ( hex.length < 8 ) { - hex = "0000000" + hex; - } - - return hex.slice( -8 ); -} - -function synchronize( callback, priority ) { - var last = !priority; - - if ( QUnit.objectType( callback ) === "array" ) { - while ( callback.length ) { - synchronize( callback.shift() ); - } - return; - } - - if ( priority ) { - priorityFill( callback ); - } else { - config.queue.push( callback ); - } - - if ( config.autorun && !config.blocking ) { - process( last ); - } -} - -// Place previously failed tests on a queue priority line, respecting the order they get assigned. -function priorityFill( callback ) { - var queue, prioritizedQueue; - - queue = config.queue.slice( priorityFill.pos ); - prioritizedQueue = config.queue.slice( 0, -config.queue.length + priorityFill.pos ); - - queue.unshift( callback ); - queue.unshift.apply( queue, prioritizedQueue ); - - config.queue = queue; - - priorityFill.pos += 1; -} -priorityFill.pos = 0; - -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in global ) { - if ( hasOwn.call( global, 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( ", " ) ); - } -} - -// Will be exposed as QUnit.asyncTest -function asyncTest( testName, expected, callback ) { - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - QUnit.test( testName, expected, callback, true ); -} - -// Will be exposed as QUnit.test -function test( testName, expected, callback, async ) { - if ( focused ) { return; } - - var newTest; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - newTest = new Test({ - testName: testName, - expected: expected, - async: async, - callback: callback - }); - - newTest.queue(); -} - -// Will be exposed as QUnit.skip -function skip( testName ) { - if ( focused ) { return; } - - var test = new Test({ - testName: testName, - skip: true - }); - - test.queue(); -} - -// Will be exposed as QUnit.only -function only( testName, expected, callback, async ) { - var newTest; - - if ( focused ) { return; } - - QUnit.config.queue.length = 0; - focused = true; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - - newTest = new Test({ - testName: testName, - expected: expected, - async: async, - callback: callback - }); - - newTest.queue(); -} - -function Assert( testContext ) { - this.test = testContext; -} - -// Assert helpers -QUnit.assert = Assert.prototype = { - - // Specify the number of expected assertions to guarantee that failed test - // (no assertions are run at all) don't slip through. - expect: function( asserts ) { - if ( arguments.length === 1 ) { - this.test.expected = asserts; - } else { - return this.test.expected; - } - }, - - // Increment this Test's semaphore counter, then return a function that - // decrements that counter a maximum of once. - async: function( count ) { - var test = this.test, - popped = false, - acceptCallCount = count; - - if ( typeof acceptCallCount === "undefined" ) { - acceptCallCount = 1; - } - - test.semaphore += 1; - test.usedAsync = true; - pauseProcessing(); - - return function done() { - - if ( popped ) { - test.pushFailure( "Too many calls to the `assert.async` callback", - sourceFromStacktrace( 2 ) ); - return; - } - acceptCallCount -= 1; - if ( acceptCallCount > 0 ) { - return; - } - - test.semaphore -= 1; - popped = true; - resumeProcessing(); - }; - }, - - // Exports test.push() to the user API - push: function( /* result, actual, expected, message, negative */ ) { - var assert = this, - currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; - - // Backwards compatibility fix. - // Allows the direct use of global exported assertions and QUnit.assert.* - // Although, it's use is not recommended as it can leak assertions - // to other tests from async tests, because we only get a reference to the current test, - // not exactly the test where assertion were intended to be called. - if ( !currentTest ) { - throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); - } - - if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { - currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", - sourceFromStacktrace( 2 ) ); - - // Allow this assertion to continue running anyway... - } - - if ( !( assert instanceof Assert ) ) { - assert = currentTest.assert; - } - return assert.test.push.apply( assert.test, arguments ); - }, - - ok: function( result, message ) { - message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + - QUnit.dump.parse( result ) ); - this.push( !!result, result, true, message ); - }, - - notOk: function( result, message ) { - message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + - QUnit.dump.parse( result ) ); - this.push( !result, result, false, message, true ); - }, - - equal: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected == actual, actual, expected, message ); - }, - - notEqual: function( actual, expected, message ) { - /*jshint eqeqeq:false */ - this.push( expected != actual, actual, expected, message, true ); - }, - - propEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - notPropEqual: function( actual, expected, message ) { - actual = objectValues( actual ); - expected = objectValues( expected ); - this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); - }, - - deepEqual: function( actual, expected, message ) { - this.push( QUnit.equiv( actual, expected ), actual, expected, message ); - }, - - notDeepEqual: function( actual, expected, message ) { - this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); - }, - - strictEqual: function( actual, expected, message ) { - this.push( expected === actual, actual, expected, message ); - }, - - notStrictEqual: function( actual, expected, message ) { - this.push( expected !== actual, actual, expected, message, true ); - }, - - "throws": function( block, expected, message ) { - var actual, expectedType, - expectedOutput = expected, - ok = false, - currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; - - // 'expected' is optional unless doing string comparison - if ( message == null && typeof expected === "string" ) { - message = expected; - expected = null; - } - - currentTest.ignoreGlobalErrors = true; - try { - block.call( currentTest.testEnvironment ); - } catch (e) { - actual = e; - } - currentTest.ignoreGlobalErrors = false; - - if ( actual ) { - expectedType = QUnit.objectType( expected ); - - // we don't want to validate thrown error - if ( !expected ) { - ok = true; - expectedOutput = null; - - // expected is a regexp - } else if ( expectedType === "regexp" ) { - ok = expected.test( errorString( actual ) ); - - // expected is a string - } else if ( expectedType === "string" ) { - ok = expected === errorString( actual ); - - // expected is a constructor, maybe an Error constructor - } else if ( expectedType === "function" && actual instanceof expected ) { - ok = true; - - // expected is an Error object - } else if ( expectedType === "object" ) { - ok = actual instanceof expected.constructor && - actual.name === expected.name && - actual.message === expected.message; - - // expected is a validation function which returns true if validation passed - } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { - expectedOutput = null; - ok = true; - } - } - - currentTest.assert.push( ok, actual, expectedOutput, message ); - } -}; - -// Provide an alternative to assert.throws(), for environments that consider throws a reserved word -// Known to us are: Closure Compiler, Narwhal -(function() { - /*jshint sub:true */ - Assert.prototype.raises = Assert.prototype[ "throws" ]; -}()); - -function errorString( error ) { - var name, message, - resultErrorString = error.toString(); - if ( resultErrorString.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 resultErrorString; - } -} - -// Test for equality any JavaScript type. -// Author: Philippe Rathé -QUnit.equiv = (function() { - - // Stack to decide between skip/abort functions - var callers = []; - - // Stack to avoiding loops from circular referencing - var parents = []; - var parentsB = []; - - function useStrictEquality( b, a ) { - - /*jshint eqeqeq:false */ - if ( b instanceof a.constructor || a instanceof b.constructor ) { - - // To catch short annotation VS 'new' annotation of a declaration. e.g.: - // `var i = 1;` - // `var j = new Number(1);` - return a == b; - } else { - return a === b; - } - } - - function compareConstructors( a, b ) { - var getProto = Object.getPrototypeOf || function( obj ) { - - /*jshint proto: true */ - return obj.__proto__; - }; - var protoA = getProto( a ); - var protoB = getProto( b ); - - // Comparing constructors is more strict than using `instanceof` - if ( a.constructor === b.constructor ) { - return true; - } - - // Ref #851 - // If the obj prototype descends from a null constructor, treat it - // as a null prototype. - if ( protoA && protoA.constructor === null ) { - protoA = null; - } - if ( protoB && protoB.constructor === null ) { - protoB = null; - } - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( ( protoA === null && protoB === Object.prototype ) || - ( protoB === null && protoA === Object.prototype ) ) { - return true; - } - - return false; - } - - var callbacks = { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - "symbol": useStrictEquality, - - "nan": function( b ) { - return isNaN( b ); - }, - - "date": function( b, a ) { - return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function( b, a ) { - return QUnit.objectType( b ) === "regexp" && - - // The regex itself - a.source === b.source && - - // And its modifiers - a.global === b.global && - - // (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function() { - var caller = callers[ callers.length - 1 ]; - return caller !== Object && typeof caller !== "undefined"; - }, - - "array": function( b, a ) { - var i, j, len, loop, aCircular, bCircular; - - // b could be an object literal here - if ( QUnit.objectType( b ) !== "array" ) { - return false; - } - - len = a.length; - if ( len !== b.length ) { - // safe and faster - return false; - } - - // Track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - for ( i = 0; i < len; i++ ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - parents.pop(); - parentsB.pop(); - return false; - } - } - parents.pop(); - parentsB.pop(); - return true; - }, - - "set": function( b, a ) { - var aArray, bArray; - - // `b` could be any object here - if ( QUnit.objectType( b ) !== "set" ) { - return false; - } - - aArray = []; - a.forEach( function( v ) { - aArray.push( v ); - }); - bArray = []; - b.forEach( function( v ) { - bArray.push( v ); - }); - - return innerEquiv( bArray, aArray ); - }, - - "map": function( b, a ) { - var aArray, bArray; - - // `b` could be any object here - if ( QUnit.objectType( b ) !== "map" ) { - return false; - } - - aArray = []; - a.forEach( function( v, k ) { - aArray.push( [ k, v ] ); - }); - bArray = []; - b.forEach( function( v, k ) { - bArray.push( [ k, v ] ); - }); - - return innerEquiv( bArray, aArray ); - }, - - "object": function( b, a ) { - var i, j, loop, aCircular, bCircular; - - // Default to true - var eq = true; - var aProperties = []; - var bProperties = []; - - if ( compareConstructors( a, b ) === false ) { - return false; - } - - // Stack constructor before traversing properties - callers.push( a.constructor ); - - // Track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - - // Be strict: don't ensure hasOwnProperty and go deep - for ( i in a ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - eq = false; - break; - } - } - } - aProperties.push( i ); - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { - eq = false; - break; - } - } - - parents.pop(); - parentsB.pop(); - - // Unstack, we are done - callers.pop(); - - for ( i in b ) { - - // Collect b's properties - bProperties.push( i ); - } - - // Ensures identical properties name - return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); - } - }; - - function typeEquiv( a, b ) { - var prop = QUnit.objectType( a ); - return callbacks[ prop ]( b, a ); - } - - // The real equiv function - function innerEquiv() { - var args = [].slice.apply( arguments ); - if ( args.length < 2 ) { - - // End transition - return true; - } - - return ( (function( a, b ) { - if ( a === b ) { - - // Catch the most you can - return true; - } else if ( a === null || b === null || typeof a === "undefined" || - typeof b === "undefined" || - QUnit.objectType( a ) !== QUnit.objectType( b ) ) { - - // Don't lose time with error prone cases - return false; - } else { - return typeEquiv( a, b ); - } - - // Apply transition with (1..n) arguments - }( args[ 0 ], args[ 1 ] ) ) && - innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); - } - - return innerEquiv; -}()); - -// Based on jsDump by Ariel Flesler -// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html -QUnit.dump = (function() { - function quote( str ) { - return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; - } - function literal( o ) { - return o + ""; - } - function join( pre, arr, post ) { - var s = dump.separator(), - base = dump.indent(), - inner = dump.indent( 1 ); - if ( arr.join ) { - arr = arr.join( "," + s + inner ); - } - if ( !arr ) { - return pre + post; - } - return [ pre, inner + arr, base + post ].join( s ); - } - function array( arr, stack ) { - var i = arr.length, - ret = new Array( i ); - - if ( dump.maxDepth && dump.depth > dump.maxDepth ) { - return "[object Array]"; - } - - this.up(); - while ( i-- ) { - ret[ i ] = this.parse( arr[ i ], undefined, stack ); - } - this.down(); - return join( "[", ret, "]" ); - } - - var reName = /^function (\w+)/, - dump = { - - // objType is used mostly internally, you can fix a (custom) type in advance - parse: function( obj, objType, stack ) { - stack = stack || []; - var res, parser, parserType, - inStack = inArray( obj, stack ); - - if ( inStack !== -1 ) { - return "recursion(" + ( inStack - stack.length ) + ")"; - } - - objType = objType || this.typeOf( obj ); - parser = this.parsers[ objType ]; - parserType = typeof parser; - - if ( parserType === "function" ) { - stack.push( obj ); - res = parser.call( this, obj, stack ); - stack.pop(); - return res; - } - return ( parserType === "string" ) ? parser : this.parsers.error; - }, - typeOf: function( obj ) { - var type; - if ( obj === null ) { - type = "null"; - } else if ( typeof obj === "undefined" ) { - type = "undefined"; - } else if ( QUnit.is( "regexp", obj ) ) { - type = "regexp"; - } else if ( QUnit.is( "date", obj ) ) { - type = "date"; - } else if ( QUnit.is( "function", obj ) ) { - type = "function"; - } else if ( obj.setInterval !== undefined && - obj.document !== undefined && - obj.nodeType === undefined ) { - type = "window"; - } else if ( obj.nodeType === 9 ) { - type = "document"; - } else if ( obj.nodeType ) { - type = "node"; - } else if ( - - // native arrays - toString.call( obj ) === "[object Array]" || - - // NodeList objects - ( typeof obj.length === "number" && obj.item !== undefined && - ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && - obj[ 0 ] === undefined ) ) ) - ) { - type = "array"; - } else if ( obj.constructor === Error.prototype.constructor ) { - type = "error"; - } else { - type = typeof obj; - } - return type; - }, - separator: function() { - return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; - }, - // extra can be a number, shortcut for increasing-calling-decreasing - indent: function( extra ) { - if ( !this.multiline ) { - return ""; - } - var chr = this.indentChar; - if ( this.HTML ) { - chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); - } - return new Array( this.depth + ( extra || 0 ) ).join( chr ); - }, - up: function( a ) { - this.depth += a || 1; - }, - down: function( a ) { - this.depth -= a || 1; - }, - setParser: function( name, parser ) { - this.parsers[ name ] = parser; - }, - // The next 3 are exposed so you can use them - quote: quote, - literal: literal, - join: join, - // - depth: 1, - maxDepth: QUnit.config.maxDepth, - - // This is the list of parsers, to modify them, use dump.setParser - parsers: { - window: "[Window]", - document: "[Document]", - error: function( error ) { - return "Error(\"" + error.message + "\")"; - }, - unknown: "[Unknown]", - "null": "null", - "undefined": "undefined", - "function": function( fn ) { - var ret = "function", - - // functions never have name in IE - name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; - - if ( name ) { - ret += " " + name; - } - ret += "( "; - - ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); - return join( ret, dump.parse( fn, "functionCode" ), "}" ); - }, - array: array, - nodelist: array, - "arguments": array, - object: function( map, stack ) { - var keys, key, val, i, nonEnumerableProperties, - ret = []; - - if ( dump.maxDepth && dump.depth > dump.maxDepth ) { - return "[object Object]"; - } - - dump.up(); - keys = []; - for ( key in map ) { - keys.push( key ); - } - - // Some properties are not always enumerable on Error objects. - nonEnumerableProperties = [ "message", "name" ]; - for ( i in nonEnumerableProperties ) { - key = nonEnumerableProperties[ i ]; - if ( key in map && inArray( key, keys ) < 0 ) { - keys.push( key ); - } - } - keys.sort(); - for ( i = 0; i < keys.length; i++ ) { - key = keys[ i ]; - val = map[ key ]; - ret.push( dump.parse( key, "key" ) + ": " + - dump.parse( val, undefined, stack ) ); - } - dump.down(); - return join( "{", ret, "}" ); - }, - node: function( node ) { - var len, i, val, - open = dump.HTML ? "<" : "<", - close = dump.HTML ? ">" : ">", - tag = node.nodeName.toLowerCase(), - ret = open + tag, - attrs = node.attributes; - - if ( attrs ) { - for ( i = 0, len = attrs.length; i < len; i++ ) { - val = attrs[ i ].nodeValue; - - // IE6 includes all attributes in .attributes, even ones not explicitly - // set. Those have values like undefined, null, 0, false, "" or - // "inherit". - if ( val && val !== "inherit" ) { - ret += " " + attrs[ i ].nodeName + "=" + - dump.parse( val, "attribute" ); - } - } - } - ret += close; - - // Show content of TextNode or CDATASection - if ( node.nodeType === 3 || node.nodeType === 4 ) { - ret += node.nodeValue; - } - - return ret + open + "/" + tag + close; - }, - - // function calls it internally, it's the arguments part of the function - functionArgs: function( fn ) { - var args, - l = fn.length; - - if ( !l ) { - return ""; - } - - args = new Array( l ); - while ( l-- ) { - - // 97 is 'a' - args[ l ] = String.fromCharCode( 97 + l ); - } - return " " + args.join( ", " ) + " "; - }, - // object calls it internally, the key part of an item in a map - key: quote, - // function calls it internally, it's the content of the function - functionCode: "[code]", - // node calls it internally, it's an html attribute value - attribute: quote, - string: quote, - date: quote, - regexp: literal, - number: literal, - "boolean": literal - }, - // if true, entities are escaped ( <, >, \t, space and \n ) - HTML: false, - // indentation unit - indentChar: " ", - // if true, items in a collection, are separated by a \n, else just a space. - multiline: true - }; - - return dump; -}()); - -// back compat -QUnit.jsDump = QUnit.dump; - -// For browser, export only select globals -if ( defined.document ) { - - // Deprecated - // Extend assert methods to QUnit and Global scope through Backwards compatibility - (function() { - var i, - assertions = Assert.prototype; - - function applyCurrent( current ) { - return function() { - var assert = new Assert( QUnit.config.current ); - current.apply( assert, arguments ); - }; - } - - for ( i in assertions ) { - QUnit[ i ] = applyCurrent( assertions[ i ] ); - } - })(); - - (function() { - var i, l, - keys = [ - "test", - "module", - "expect", - "asyncTest", - "start", - "stop", - "ok", - "notOk", - "equal", - "notEqual", - "propEqual", - "notPropEqual", - "deepEqual", - "notDeepEqual", - "strictEqual", - "notStrictEqual", - "throws", - "raises" - ]; - - for ( i = 0, l = keys.length; i < l; i++ ) { - window[ keys[ i ] ] = QUnit[ keys[ i ] ]; - } - })(); - - window.QUnit = QUnit; -} - -// For nodejs -if ( typeof module !== "undefined" && module && module.exports ) { - module.exports = QUnit; - - // For consistency with CommonJS environments' exports - module.exports.QUnit = QUnit; -} - -// For CommonJS with exports, but without module.exports, like Rhino -if ( typeof exports !== "undefined" && exports ) { - exports.QUnit = QUnit; -} - -if ( typeof define === "function" && define.amd ) { - define( function() { - return QUnit; - } ); - QUnit.config.autostart = false; -} - -/* - * This file is a modified version of google-diff-match-patch's JavaScript implementation - * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), - * modifications are licensed as more fully set forth in LICENSE.txt. - * - * The original source of google-diff-match-patch is attributable and licensed as follows: - * - * Copyright 2006 Google Inc. - * http://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * More Info: - * https://code.google.com/p/google-diff-match-patch/ - * - * Usage: QUnit.diff(expected, actual) - * - */ -QUnit.diff = ( function() { - function DiffMatchPatch() { - } - - // DIFF FUNCTIONS - - /** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ - var DIFF_DELETE = -1, - DIFF_INSERT = 1, - DIFF_EQUAL = 0; - - /** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} optChecklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @return {!Array.} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { - var deadline, checklines, commonlength, - commonprefix, commonsuffix, diffs; - - // The diff must be complete in up to 1 second. - deadline = ( new Date() ).getTime() + 1000; - - // Check for null inputs. - if ( text1 === null || text2 === null ) { - throw new Error( "Null input. (DiffMain)" ); - } - - // Check for equality (speedup). - if ( text1 === text2 ) { - if ( text1 ) { - return [ - [ DIFF_EQUAL, text1 ] - ]; - } - return []; - } - - if ( typeof optChecklines === "undefined" ) { - optChecklines = true; - } - - checklines = optChecklines; - - // Trim off common prefix (speedup). - commonlength = this.diffCommonPrefix( text1, text2 ); - commonprefix = text1.substring( 0, commonlength ); - text1 = text1.substring( commonlength ); - text2 = text2.substring( commonlength ); - - // Trim off common suffix (speedup). - commonlength = this.diffCommonSuffix( text1, text2 ); - commonsuffix = text1.substring( text1.length - commonlength ); - text1 = text1.substring( 0, text1.length - commonlength ); - text2 = text2.substring( 0, text2.length - commonlength ); - - // Compute the diff on the middle block. - diffs = this.diffCompute( text1, text2, checklines, deadline ); - - // Restore the prefix and suffix. - if ( commonprefix ) { - diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); - } - if ( commonsuffix ) { - diffs.push( [ DIFF_EQUAL, commonsuffix ] ); - } - this.diffCleanupMerge( diffs ); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { - var changes, equalities, equalitiesLength, lastequality, - pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Is there an insertion operation before the last equality. - preIns = false; - // Is there a deletion operation before the last equality. - preDel = false; - // Is there an insertion operation after the last equality. - postIns = false; - // Is there a deletion operation after the last equality. - postDel = false; - while ( pointer < diffs.length ) { - - // Equality found. - if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { - if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { - - // Candidate found. - equalities[ equalitiesLength++ ] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[ pointer ][ 1 ]; - } else { - - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; - - // An insertion or deletion. - } else { - - if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { - postDel = true; - } else { - postIns = true; - } - - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || - ( ( lastequality.length < 2 ) && - ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { - - // Duplicate record. - diffs.splice( - equalities[ equalitiesLength - 1 ], - 0, - [ DIFF_DELETE, lastequality ] - ); - - // Change second copy to insert. - diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if ( preIns && preDel ) { - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; - postIns = postDel = false; - } - changes = true; - } - } - pointer++; - } - - if ( changes ) { - this.diffCleanupMerge( diffs ); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { - var op, data, x, - html = []; - for ( x = 0; x < diffs.length; x++ ) { - op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) - data = diffs[ x ][ 1 ]; // Text of change. - switch ( op ) { - case DIFF_INSERT: - html[ x ] = "" + data + ""; - break; - case DIFF_DELETE: - html[ x ] = "" + data + ""; - break; - case DIFF_EQUAL: - html[ x ] = "" + data + ""; - break; - } - } - return html.join( "" ); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerstart; - // Quick check for common null cases. - if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min( text1.length, text2.length ); - pointermid = pointermax; - pointerstart = 0; - while ( pointermin < pointermid ) { - if ( text1.substring( pointerstart, pointermid ) === - text2.substring( pointerstart, pointermid ) ) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerend; - // Quick check for common null cases. - if ( !text1 || - !text2 || - text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min( text1.length, text2.length ); - pointermid = pointermax; - pointerend = 0; - while ( pointermin < pointermid ) { - if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === - text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { - var diffs, longtext, shorttext, i, hm, - text1A, text2A, text1B, text2B, - midCommon, diffsA, diffsB; - - if ( !text1 ) { - // Just add some text (speedup). - return [ - [ DIFF_INSERT, text2 ] - ]; - } - - if ( !text2 ) { - // Just delete some text (speedup). - return [ - [ DIFF_DELETE, text1 ] - ]; - } - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf( shorttext ); - if ( i !== -1 ) { - // Shorter text is inside the longer text (speedup). - diffs = [ - [ DIFF_INSERT, longtext.substring( 0, i ) ], - [ DIFF_EQUAL, shorttext ], - [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] - ]; - // Swap insertions for deletions if diff is reversed. - if ( text1.length > text2.length ) { - diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; - } - return diffs; - } - - if ( shorttext.length === 1 ) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch( text1, text2 ); - if ( hm ) { - // A half-match was found, sort out the return data. - text1A = hm[ 0 ]; - text1B = hm[ 1 ]; - text2A = hm[ 2 ]; - text2B = hm[ 3 ]; - midCommon = hm[ 4 ]; - // Send both pairs off for separate processing. - diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); - diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); - // Merge the results. - return diffsA.concat( [ - [ DIFF_EQUAL, midCommon ] - ], diffsB ); - } - - if ( checklines && text1.length > 100 && text2.length > 100 ) { - return this.diffLineMode( text1, text2, deadline ); - } - - return this.diffBisect( text1, text2, deadline ); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { - var longtext, shorttext, dmp, - text1A, text2B, text2A, text1B, midCommon, - hm1, hm2, hm; - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { - return null; // Pointless. - } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI( longtext, shorttext, i ) { - var seed, j, bestCommon, prefixLength, suffixLength, - bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); - j = -1; - bestCommon = ""; - while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { - prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), - shorttext.substring( j ) ); - suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), - shorttext.substring( 0, j ) ); - if ( bestCommon.length < suffixLength + prefixLength ) { - bestCommon = shorttext.substring( j - suffixLength, j ) + - shorttext.substring( j, j + prefixLength ); - bestLongtextA = longtext.substring( 0, i - suffixLength ); - bestLongtextB = longtext.substring( i + prefixLength ); - bestShorttextA = shorttext.substring( 0, j - suffixLength ); - bestShorttextB = shorttext.substring( j + prefixLength ); - } - } - if ( bestCommon.length * 2 >= longtext.length ) { - return [ bestLongtextA, bestLongtextB, - bestShorttextA, bestShorttextB, bestCommon - ]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI( longtext, shorttext, - Math.ceil( longtext.length / 4 ) ); - // Check again based on the third quarter. - hm2 = diffHalfMatchI( longtext, shorttext, - Math.ceil( longtext.length / 2 ) ); - if ( !hm1 && !hm2 ) { - return null; - } else if ( !hm2 ) { - hm = hm1; - } else if ( !hm1 ) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - text1A, text1B, text2A, text2B; - if ( text1.length > text2.length ) { - text1A = hm[ 0 ]; - text1B = hm[ 1 ]; - text2A = hm[ 2 ]; - text2B = hm[ 3 ]; - } else { - text2A = hm[ 0 ]; - text2B = hm[ 1 ]; - text1A = hm[ 2 ]; - text1B = hm[ 3 ]; - } - midCommon = hm[ 4 ]; - return [ text1A, text1B, text2A, text2B, midCommon ]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { - var a, diffs, linearray, pointer, countInsert, - countDelete, textInsert, textDelete, j; - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars( text1, text2 ); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - - diffs = this.DiffMain( text1, text2, false, deadline ); - - // Convert the diff back to original text. - this.diffCharsToLines( diffs, linearray ); - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic( diffs ); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push( [ DIFF_EQUAL, "" ] ); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - while ( pointer < diffs.length ) { - switch ( diffs[ pointer ][ 0 ] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[ pointer ][ 1 ]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[ pointer ][ 1 ]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if ( countDelete >= 1 && countInsert >= 1 ) { - // Delete the offending records and add the merged ones. - diffs.splice( pointer - countDelete - countInsert, - countDelete + countInsert ); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain( textDelete, textInsert, false, deadline ); - for ( j = a.length - 1; j >= 0; j-- ) { - diffs.splice( pointer, 0, a[ j ] ); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { - var text1Length, text2Length, maxD, vOffset, vLength, - v1, v2, x, delta, front, k1start, k1end, k2start, - k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array( vLength ); - v2 = new Array( vLength ); - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for ( x = 0; x < vLength; x++ ) { - v1[ x ] = -1; - v2[ x ] = -1; - } - v1[ vOffset + 1 ] = 0; - v2[ vOffset + 1 ] = 0; - delta = text1Length - text2Length; - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = ( delta % 2 !== 0 ); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for ( d = 0; d < maxD; d++ ) { - // Bail out if deadline is reached. - if ( ( new Date() ).getTime() > deadline ) { - break; - } - - // Walk the front path one step. - for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { - k1Offset = vOffset + k1; - if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { - x1 = v1[ k1Offset + 1 ]; - } else { - x1 = v1[ k1Offset - 1 ] + 1; - } - y1 = x1 - k1; - while ( x1 < text1Length && y1 < text2Length && - text1.charAt( x1 ) === text2.charAt( y1 ) ) { - x1++; - y1++; - } - v1[ k1Offset ] = x1; - if ( x1 > text1Length ) { - // Ran off the right of the graph. - k1end += 2; - } else if ( y1 > text2Length ) { - // Ran off the bottom of the graph. - k1start += 2; - } else if ( front ) { - k2Offset = vOffset + delta - k1; - if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[ k2Offset ]; - if ( x1 >= x2 ) { - // Overlap detected. - return this.diffBisectSplit( text1, text2, x1, y1, deadline ); - } - } - } - } - - // Walk the reverse path one step. - for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { - k2Offset = vOffset + k2; - if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { - x2 = v2[ k2Offset + 1 ]; - } else { - x2 = v2[ k2Offset - 1 ] + 1; - } - y2 = x2 - k2; - while ( x2 < text1Length && y2 < text2Length && - text1.charAt( text1Length - x2 - 1 ) === - text2.charAt( text2Length - y2 - 1 ) ) { - x2++; - y2++; - } - v2[ k2Offset ] = x2; - if ( x2 > text1Length ) { - // Ran off the left of the graph. - k2end += 2; - } else if ( y2 > text2Length ) { - // Ran off the top of the graph. - k2start += 2; - } else if ( !front ) { - k1Offset = vOffset + delta - k2; - if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { - x1 = v1[ k1Offset ]; - y1 = vOffset + x1 - k1Offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if ( x1 >= x2 ) { - // Overlap detected. - return this.diffBisectSplit( text1, text2, x1, y1, deadline ); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - }; - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring( 0, x ); - text2a = text2.substring( 0, y ); - text1b = text1.substring( x ); - text2b = text2.substring( y ); - - // Compute both diffs serially. - diffs = this.DiffMain( text1a, text2a, false, deadline ); - diffsb = this.DiffMain( text1b, text2b, false, deadline ); - - return diffs.concat( diffsb ); - }; - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { - var changes, equalities, equalitiesLength, lastequality, - pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, - lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Number of characters that changed prior to the equality. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - // Number of characters that changed after the equality. - lengthInsertions2 = 0; - lengthDeletions2 = 0; - while ( pointer < diffs.length ) { - if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. - equalities[ equalitiesLength++ ] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[ pointer ][ 1 ]; - } else { // An insertion or deletion. - if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { - lengthInsertions2 += diffs[ pointer ][ 1 ].length; - } else { - lengthDeletions2 += diffs[ pointer ][ 1 ].length; - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if ( lastequality && ( lastequality.length <= - Math.max( lengthInsertions1, lengthDeletions1 ) ) && - ( lastequality.length <= Math.max( lengthInsertions2, - lengthDeletions2 ) ) ) { - - // Duplicate record. - diffs.splice( - equalities[ equalitiesLength - 1 ], - 0, - [ DIFF_DELETE, lastequality ] - ); - - // Change second copy to insert. - diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; - - // Throw away the equality we just deleted. - equalitiesLength--; - - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; - - // Reset the counters. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if ( changes ) { - this.diffCleanupMerge( diffs ); - } - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while ( pointer < diffs.length ) { - if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && - diffs[ pointer ][ 0 ] === DIFF_INSERT ) { - deletion = diffs[ pointer - 1 ][ 1 ]; - insertion = diffs[ pointer ][ 1 ]; - overlapLength1 = this.diffCommonOverlap( deletion, insertion ); - overlapLength2 = this.diffCommonOverlap( insertion, deletion ); - if ( overlapLength1 >= overlapLength2 ) { - if ( overlapLength1 >= deletion.length / 2 || - overlapLength1 >= insertion.length / 2 ) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice( - pointer, - 0, - [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] - ); - diffs[ pointer - 1 ][ 1 ] = - deletion.substring( 0, deletion.length - overlapLength1 ); - diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); - pointer++; - } - } else { - if ( overlapLength2 >= deletion.length / 2 || - overlapLength2 >= insertion.length / 2 ) { - - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice( - pointer, - 0, - [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] - ); - - diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; - diffs[ pointer - 1 ][ 1 ] = - insertion.substring( 0, insertion.length - overlapLength2 ); - diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; - diffs[ pointer + 1 ][ 1 ] = - deletion.substring( overlapLength2 ); - pointer++; - } - } - pointer++; - } - pointer++; - } - }; - - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { - var text1Length, text2Length, textLength, - best, length, pattern, found; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - // Eliminate the null case. - if ( text1Length === 0 || text2Length === 0 ) { - return 0; - } - // Truncate the longer string. - if ( text1Length > text2Length ) { - text1 = text1.substring( text1Length - text2Length ); - } else if ( text1Length < text2Length ) { - text2 = text2.substring( 0, text1Length ); - } - textLength = Math.min( text1Length, text2Length ); - // Quick check for the worst case. - if ( text1 === text2 ) { - return textLength; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: http://neil.fraser.name/news/2010/11/04/ - best = 0; - length = 1; - while ( true ) { - pattern = text1.substring( textLength - length ); - found = text2.indexOf( pattern ); - if ( found === -1 ) { - return best; - } - length += found; - if ( found === 0 || text1.substring( textLength - length ) === - text2.substring( 0, length ) ) { - best = length; - length++; - } - } - }; - - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { - var lineArray, lineHash, chars1, chars2; - lineArray = []; // e.g. lineArray[4] === 'Hello\n' - lineHash = {}; // e.g. lineHash['Hello\n'] === 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[ 0 ] = ""; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge( text ) { - var chars, lineStart, lineEnd, lineArrayLength, line; - chars = ""; - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - lineStart = 0; - lineEnd = -1; - // Keeping our own length variable is faster than looking it up. - lineArrayLength = lineArray.length; - while ( lineEnd < text.length - 1 ) { - lineEnd = text.indexOf( "\n", lineStart ); - if ( lineEnd === -1 ) { - lineEnd = text.length - 1; - } - line = text.substring( lineStart, lineEnd + 1 ); - lineStart = lineEnd + 1; - - if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : - ( lineHash[ line ] !== undefined ) ) { - chars += String.fromCharCode( lineHash[ line ] ); - } else { - chars += String.fromCharCode( lineArrayLength ); - lineHash[ line ] = lineArrayLength; - lineArray[ lineArrayLength++ ] = line; - } - } - return chars; - } - - chars1 = diffLinesToCharsMunge( text1 ); - chars2 = diffLinesToCharsMunge( text2 ); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { - var x, chars, text, y; - for ( x = 0; x < diffs.length; x++ ) { - chars = diffs[ x ][ 1 ]; - text = []; - for ( y = 0; y < chars.length; y++ ) { - text[ y ] = lineArray[ chars.charCodeAt( y ) ]; - } - diffs[ x ][ 1 ] = text.join( "" ); - } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { - var pointer, countDelete, countInsert, textInsert, textDelete, - commonlength, changes, diffPointer, position; - diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - commonlength; - while ( pointer < diffs.length ) { - switch ( diffs[ pointer ][ 0 ] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[ pointer ][ 1 ]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[ pointer ][ 1 ]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if ( countDelete + countInsert > 1 ) { - if ( countDelete !== 0 && countInsert !== 0 ) { - // Factor out any common prefixies. - commonlength = this.diffCommonPrefix( textInsert, textDelete ); - if ( commonlength !== 0 ) { - if ( ( pointer - countDelete - countInsert ) > 0 && - diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === - DIFF_EQUAL ) { - diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += - textInsert.substring( 0, commonlength ); - } else { - diffs.splice( 0, 0, [ DIFF_EQUAL, - textInsert.substring( 0, commonlength ) - ] ); - pointer++; - } - textInsert = textInsert.substring( commonlength ); - textDelete = textDelete.substring( commonlength ); - } - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix( textInsert, textDelete ); - if ( commonlength !== 0 ) { - diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - - commonlength ) + diffs[ pointer ][ 1 ]; - textInsert = textInsert.substring( 0, textInsert.length - - commonlength ); - textDelete = textDelete.substring( 0, textDelete.length - - commonlength ); - } - } - // Delete the offending records and add the merged ones. - if ( countDelete === 0 ) { - diffs.splice( pointer - countInsert, - countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); - } else if ( countInsert === 0 ) { - diffs.splice( pointer - countDelete, - countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); - } else { - diffs.splice( - pointer - countDelete - countInsert, - countDelete + countInsert, - [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] - ); - } - pointer = pointer - countDelete - countInsert + - ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; - } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { - - // Merge this equality with the previous one. - diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; - diffs.splice( pointer, 1 ); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - } - if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - changes = false; - pointer = 1; - - // Intentionally ignore the first and last element (don't need checking). - while ( pointer < diffs.length - 1 ) { - if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && - diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { - - diffPointer = diffs[ pointer ][ 1 ]; - position = diffPointer.substring( - diffPointer.length - diffs[ pointer - 1 ][ 1 ].length - ); - - // This is a single edit surrounded by equalities. - if ( position === diffs[ pointer - 1 ][ 1 ] ) { - - // Shift the edit over the previous equality. - diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + - diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - - diffs[ pointer - 1 ][ 1 ].length ); - diffs[ pointer + 1 ][ 1 ] = - diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; - diffs.splice( pointer - 1, 1 ); - changes = true; - } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === - diffs[ pointer + 1 ][ 1 ] ) { - - // Shift the edit over the next equality. - diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; - diffs[ pointer ][ 1 ] = - diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + - diffs[ pointer + 1 ][ 1 ]; - diffs.splice( pointer + 1, 1 ); - changes = true; - } - } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if ( changes ) { - this.diffCleanupMerge( diffs ); - } - }; - - return function( o, n ) { - var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain( o, n ); - diff.diffCleanupEfficiency( output ); - text = diff.diffPrettyHtml( output ); - - return text; - }; -}() ); - -// Get a reference to the global object, like window in browsers -}( (function() { - return this; -})() )); - -(function() { - -// Don't load the HTML Reporter on non-Browser environments -if ( typeof window === "undefined" || !window.document ) { - return; -} - -// Deprecated QUnit.init - Ref #530 -// Re-initialize the configuration options -QUnit.init = function() { - var tests, banner, result, qunit, - config = QUnit.config; - - config.stats = { all: 0, bad: 0 }; - config.moduleStats = { all: 0, bad: 0 }; - config.started = 0; - config.updateRate = 1000; - config.blocking = false; - config.autostart = true; - config.autorun = false; - config.filter = ""; - config.queue = []; - - // Return on non-browser environments - // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { - return; - } - - qunit = id( "qunit" ); - if ( qunit ) { - qunit.innerHTML = - "

      " + escapeText( document.title ) + "

      " + - "

      " + - "
      " + - "

      " + - "
        "; - } - - tests = id( "qunit-tests" ); - banner = id( "qunit-banner" ); - result = id( "qunit-testresult" ); - - if ( tests ) { - tests.innerHTML = ""; - } - - if ( banner ) { - banner.className = ""; - } - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
         "; - } -}; - -var config = QUnit.config, - collapseNext = false, - hasOwn = Object.prototype.hasOwnProperty, - defined = { - document: window.document !== undefined, - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; - } - }()) - }, - modulesList = []; - -/** -* Escape text for attribute or text content. -*/ -function escapeText( s ) { - if ( !s ) { - return ""; - } - s = s + ""; - - // Both single quotes and double quotes (for attributes) - return s.replace( /['"<>&]/g, function( s ) { - switch ( s ) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - } - }); -} - -/** - * @param {HTMLElement} elem - * @param {string} type - * @param {Function} fn - */ -function addEvent( elem, type, fn ) { - if ( elem.addEventListener ) { - - // Standards-based browsers - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - - // support: IE <9 - elem.attachEvent( "on" + type, function() { - var event = window.event; - if ( !event.target ) { - event.target = event.srcElement || document; - } - - fn.call( elem, event ); - }); - } -} - -/** - * @param {Array|NodeList} elems - * @param {string} type - * @param {Function} fn - */ -function addEvents( elems, type, fn ) { - var i = elems.length; - while ( i-- ) { - addEvent( elems[ i ], type, fn ); - } -} - -function hasClass( elem, name ) { - return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; -} - -function addClass( elem, name ) { - if ( !hasClass( elem, name ) ) { - elem.className += ( elem.className ? " " : "" ) + name; - } -} - -function toggleClass( elem, name ) { - if ( hasClass( elem, name ) ) { - removeClass( elem, name ); - } else { - addClass( elem, name ); - } -} - -function removeClass( elem, name ) { - var set = " " + elem.className + " "; - - // Class name may appear multiple times - while ( set.indexOf( " " + name + " " ) >= 0 ) { - set = set.replace( " " + name + " ", " " ); - } - - // trim for prettiness - elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); -} - -function id( name ) { - return defined.document && document.getElementById && document.getElementById( name ); -} - -function getUrlConfigHtml() { - var i, j, val, - escaped, escapedTooltip, - selection = false, - len = config.urlConfig.length, - urlConfigHtml = ""; - - for ( i = 0; i < len; i++ ) { - val = config.urlConfig[ i ]; - if ( typeof val === "string" ) { - val = { - id: val, - label: val - }; - } - - escaped = escapeText( val.id ); - escapedTooltip = escapeText( val.tooltip ); - - if ( config[ val.id ] === undefined ) { - config[ val.id ] = QUnit.urlParams[ val.id ]; - } - - if ( !val.value || typeof val.value === "string" ) { - urlConfigHtml += ""; - } else { - urlConfigHtml += ""; - } - } - - return urlConfigHtml; -} - -// Handle "click" events on toolbar checkboxes and "change" for select menus. -// Updates the URL with the new state of `config.urlConfig` values. -function toolbarChanged() { - var updatedUrl, value, - field = this, - params = {}; - - // Detect if field is a select menu or a checkbox - if ( "selectedIndex" in field ) { - value = field.options[ field.selectedIndex ].value || undefined; - } else { - value = field.checked ? ( field.defaultValue || true ) : undefined; - } - - params[ field.name ] = value; - updatedUrl = setUrl( params ); - - if ( "hidepassed" === field.name && "replaceState" in window.history ) { - config[ field.name ] = value || false; - if ( value ) { - addClass( id( "qunit-tests" ), "hidepass" ); - } else { - removeClass( id( "qunit-tests" ), "hidepass" ); - } - - // It is not necessary to refresh the whole page - window.history.replaceState( null, "", updatedUrl ); - } else { - window.location = updatedUrl; - } -} - -function setUrl( params ) { - var key, - querystring = "?"; - - params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); - - for ( key in params ) { - if ( hasOwn.call( params, key ) ) { - if ( params[ key ] === undefined ) { - continue; - } - querystring += encodeURIComponent( key ); - if ( params[ key ] !== true ) { - querystring += "=" + encodeURIComponent( params[ key ] ); - } - querystring += "&"; - } - } - return location.protocol + "//" + location.host + - location.pathname + querystring.slice( 0, -1 ); -} - -function applyUrlParams() { - var selectedModule, - modulesList = id( "qunit-modulefilter" ), - filter = id( "qunit-filter-input" ).value; - - selectedModule = modulesList ? - decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : - undefined; - - window.location = setUrl({ - module: ( selectedModule === "" ) ? undefined : selectedModule, - filter: ( filter === "" ) ? undefined : filter, - - // Remove testId filter - testId: undefined - }); -} - -function toolbarUrlConfigContainer() { - var urlConfigContainer = document.createElement( "span" ); - - urlConfigContainer.innerHTML = getUrlConfigHtml(); - addClass( urlConfigContainer, "qunit-url-config" ); - - // For oldIE support: - // * Add handlers to the individual elements instead of the container - // * Use "click" instead of "change" for checkboxes - addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); - addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); - - return urlConfigContainer; -} - -function toolbarLooseFilter() { - var filter = document.createElement( "form" ), - label = document.createElement( "label" ), - input = document.createElement( "input" ), - button = document.createElement( "button" ); - - addClass( filter, "qunit-filter" ); - - label.innerHTML = "Filter: "; - - input.type = "text"; - input.value = config.filter || ""; - input.name = "filter"; - input.id = "qunit-filter-input"; - - button.innerHTML = "Go"; - - label.appendChild( input ); - - filter.appendChild( label ); - filter.appendChild( button ); - addEvent( filter, "submit", function( ev ) { - applyUrlParams(); - - if ( ev && ev.preventDefault ) { - ev.preventDefault(); - } - - return false; - }); - - return filter; -} - -function toolbarModuleFilterHtml() { - var i, - moduleFilterHtml = ""; - - if ( !modulesList.length ) { - return false; - } - - modulesList.sort(function( a, b ) { - return a.localeCompare( b ); - }); - - moduleFilterHtml += "" + - ""; - - return moduleFilterHtml; -} - -function toolbarModuleFilter() { - var toolbar = id( "qunit-testrunner-toolbar" ), - moduleFilter = document.createElement( "span" ), - moduleFilterHtml = toolbarModuleFilterHtml(); - - if ( !toolbar || !moduleFilterHtml ) { - return false; - } - - moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); - moduleFilter.innerHTML = moduleFilterHtml; - - addEvent( moduleFilter.lastChild, "change", applyUrlParams ); - - toolbar.appendChild( moduleFilter ); -} - -function appendToolbar() { - var toolbar = id( "qunit-testrunner-toolbar" ); - - if ( toolbar ) { - toolbar.appendChild( toolbarUrlConfigContainer() ); - toolbar.appendChild( toolbarLooseFilter() ); - } -} - -function appendHeader() { - var header = id( "qunit-header" ); - - if ( header ) { - header.innerHTML = "" + header.innerHTML + " "; - } -} - -function appendBanner() { - var banner = id( "qunit-banner" ); - - if ( banner ) { - banner.className = ""; - } -} - -function appendTestResults() { - var tests = id( "qunit-tests" ), - result = id( "qunit-testresult" ); - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - tests.innerHTML = ""; - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = "Running...
         "; - } -} - -function storeFixture() { - var fixture = id( "qunit-fixture" ); - if ( fixture ) { - config.fixture = fixture.innerHTML; - } -} - -function appendFilteredTest() { - var testId = QUnit.config.testId; - if ( !testId || testId.length <= 0 ) { - return ""; - } - return "
        Rerunning selected tests: " + testId.join(", ") + - " " + "Run all tests" + "
        "; -} - -function appendUserAgent() { - var userAgent = id( "qunit-userAgent" ); - - if ( userAgent ) { - userAgent.innerHTML = ""; - userAgent.appendChild( - document.createTextNode( - "QUnit " + QUnit.version + "; " + navigator.userAgent - ) - ); - } -} - -function appendTestsList( modules ) { - var i, l, x, z, test, moduleObj; - - for ( i = 0, l = modules.length; i < l; i++ ) { - moduleObj = modules[ i ]; - - if ( moduleObj.name ) { - modulesList.push( moduleObj.name ); - } - - for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { - test = moduleObj.tests[ x ]; - - appendTest( test.name, test.testId, moduleObj.name ); - } - } -} - -function appendTest( name, testId, moduleName ) { - var title, rerunTrigger, testBlock, assertList, - tests = id( "qunit-tests" ); - - if ( !tests ) { - return; - } - - title = document.createElement( "strong" ); - title.innerHTML = getNameHtml( name, moduleName ); - - rerunTrigger = document.createElement( "a" ); - rerunTrigger.innerHTML = "Rerun"; - rerunTrigger.href = setUrl({ testId: testId }); - - testBlock = document.createElement( "li" ); - testBlock.appendChild( title ); - testBlock.appendChild( rerunTrigger ); - testBlock.id = "qunit-test-output-" + testId; - - assertList = document.createElement( "ol" ); - assertList.className = "qunit-assert-list"; - - testBlock.appendChild( assertList ); - - tests.appendChild( testBlock ); -} - -// HTML Reporter initialization and load -QUnit.begin(function( details ) { - var qunit = id( "qunit" ); - - // Fixture is the only one necessary to run without the #qunit element - storeFixture(); - - if ( qunit ) { - qunit.innerHTML = - "

        " + escapeText( document.title ) + "

        " + - "

        " + - "
        " + - appendFilteredTest() + - "

        " + - "
          "; - } - - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(); - appendTestsList( details.modules ); - toolbarModuleFilter(); - - if ( qunit && config.hidepassed ) { - addClass( qunit.lastChild, "hidepass" ); - } -}); - -QUnit.done(function( details ) { - var i, key, - banner = id( "qunit-banner" ), - tests = id( "qunit-tests" ), - html = [ - "Tests completed in ", - details.runtime, - " milliseconds.
          ", - "", - details.passed, - " assertions of ", - details.total, - " passed, ", - details.failed, - " failed." - ].join( "" ); - - if ( banner ) { - banner.className = details.failed ? "qunit-fail" : "qunit-pass"; - } - - if ( tests ) { - id( "qunit-testresult" ).innerHTML = html; - } - - if ( config.altertitle && defined.document && document.title ) { - - // show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document.title = [ - ( details.failed ? "\u2716" : "\u2714" ), - document.title.replace( /^[\u2714\u2716] /i, "" ) - ].join( " " ); - } - - // clear own sessionStorage items if all tests passed - if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { - for ( i = 0; i < sessionStorage.length; i++ ) { - key = sessionStorage.key( i++ ); - if ( key.indexOf( "qunit-test-" ) === 0 ) { - sessionStorage.removeItem( key ); - } - } - } - - // scroll back to top to show results - if ( config.scrolltop && window.scrollTo ) { - window.scrollTo( 0, 0 ); - } -}); - -function getNameHtml( name, module ) { - var nameHtml = ""; - - if ( module ) { - nameHtml = "" + escapeText( module ) + ": "; - } - - nameHtml += "" + escapeText( name ) + ""; - - return nameHtml; -} - -QUnit.testStart(function( details ) { - var running, testBlock, bad; - - testBlock = id( "qunit-test-output-" + details.testId ); - if ( testBlock ) { - testBlock.className = "running"; - } else { - - // Report later registered tests - appendTest( details.name, details.testId, details.module ); - } - - running = id( "qunit-testresult" ); - if ( running ) { - bad = QUnit.config.reorder && defined.sessionStorage && - +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); - - running.innerHTML = ( bad ? - "Rerunning previously failed test:
          " : - "Running:
          " ) + - getNameHtml( details.name, details.module ); - } - -}); - -function stripHtml( string ) { - // strip tags, html entity and whitespaces - return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); -} - -QUnit.log(function( details ) { - var assertList, assertLi, - message, expected, actual, diff, - showDiff = false, - testItem = id( "qunit-test-output-" + details.testId ); - - if ( !testItem ) { - return; - } - - message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); - message = "" + message + ""; - message += "@ " + details.runtime + " ms"; - - // pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if ( !details.result && hasOwn.call( details, "expected" ) ) { - if ( details.negative ) { - expected = escapeText( "NOT " + QUnit.dump.parse( details.expected ) ); - } else { - expected = escapeText( QUnit.dump.parse( details.expected ) ); - } - - actual = escapeText( QUnit.dump.parse( details.actual ) ); - message += ""; - - if ( actual !== expected ) { - - message += ""; - - // Don't show diff if actual or expected are booleans - if ( !( /^(true|false)$/.test( actual ) ) && - !( /^(true|false)$/.test( expected ) ) ) { - diff = QUnit.diff( expected, actual ); - showDiff = stripHtml( diff ).length !== - stripHtml( expected ).length + - stripHtml( actual ).length; - } - - // Don't show diff if expected and actual are totally different - if ( showDiff ) { - message += ""; - } - } else if ( expected.indexOf( "[object Array]" ) !== -1 || - expected.indexOf( "[object Object]" ) !== -1 ) { - message += ""; - } - - if ( details.source ) { - message += ""; - } - - message += "
          Expected:
          " +
          -			expected +
          -			"
          Result:
          " +
          -				actual + "
          Diff:
          " +
          -					diff + "
          Message: " + - "Diff suppressed as the depth of object is more than current max depth (" + - QUnit.config.maxDepth + ").

          Hint: Use QUnit.dump.maxDepth to " + - " run with a higher max depth or " + - "Rerun without max depth.

          Source:
          " +
          -				escapeText( details.source ) + "
          "; - - // this occours when pushFailure is set and we have an extracted stack trace - } else if ( !details.result && details.source ) { - message += "" + - "" + - "
          Source:
          " +
          -			escapeText( details.source ) + "
          "; - } - - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - assertLi = document.createElement( "li" ); - assertLi.className = details.result ? "pass" : "fail"; - assertLi.innerHTML = message; - assertList.appendChild( assertLi ); -}); - -QUnit.testDone(function( details ) { - var testTitle, time, testItem, assertList, - good, bad, testCounts, skipped, sourceName, - tests = id( "qunit-tests" ); - - if ( !tests ) { - return; - } - - testItem = id( "qunit-test-output-" + details.testId ); - - assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; - - good = details.passed; - bad = details.failed; - - // store result when possible - if ( config.reorder && defined.sessionStorage ) { - if ( bad ) { - sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); - } else { - sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); - } - } - - if ( bad === 0 ) { - - // Collapse the passing tests - addClass( assertList, "qunit-collapsed" ); - } else if ( bad && config.collapse && !collapseNext ) { - - // Skip collapsing the first failing test - collapseNext = true; - } else { - - // Collapse remaining tests - addClass( assertList, "qunit-collapsed" ); - } - - // testItem.firstChild is the test name - testTitle = testItem.firstChild; - - testCounts = bad ? - "" + bad + ", " + "" + good + ", " : - ""; - - testTitle.innerHTML += " (" + testCounts + - details.assertions.length + ")"; - - if ( details.skipped ) { - testItem.className = "skipped"; - skipped = document.createElement( "em" ); - skipped.className = "qunit-skipped-label"; - skipped.innerHTML = "skipped"; - testItem.insertBefore( skipped, testTitle ); - } else { - addEvent( testTitle, "click", function() { - toggleClass( assertList, "qunit-collapsed" ); - }); - - testItem.className = bad ? "fail" : "pass"; - - time = document.createElement( "span" ); - time.className = "runtime"; - time.innerHTML = details.runtime + " ms"; - testItem.insertBefore( time, assertList ); - } - - // Show the source of the test when showing assertions - if ( details.source ) { - sourceName = document.createElement( "p" ); - sourceName.innerHTML = "Source: " + details.source; - addClass( sourceName, "qunit-source" ); - if ( bad === 0 ) { - addClass( sourceName, "qunit-collapsed" ); - } - addEvent( testTitle, "click", function() { - toggleClass( sourceName, "qunit-collapsed" ); - }); - testItem.appendChild( sourceName ); - } -}); - -if ( defined.document ) { - - // Avoid readyState issue with phantomjs - // Ref: #818 - var notPhantom = ( function( p ) { - return !( p && p.version && p.version.major > 0 ); - } )( window.phantom ); - - if ( notPhantom && document.readyState === "complete" ) { - QUnit.load(); - } else { - addEvent( window, "load", QUnit.load ); - } -} else { - config.pageLoaded = true; - config.autorun = true; -} - -})(); diff --git a/test/syn/syn.js b/test/syn/syn.js deleted file mode 100644 index 188e04a..0000000 --- a/test/syn/syn.js +++ /dev/null @@ -1,2422 +0,0 @@ -/** - * syn - 0.0.3 - * - * @copyright 2015 Bitovi - * Fri, 06 Mar 2015 01:47:33 GMT - * @license MIT - */ - -/*[global-shim]*/ -(function (exports, global){ - var origDefine = global.define; - - var get = function(name){ - var parts = name.split("."), - cur = global, - i; - for(i = 0 ; i < parts.length; i++){ - if(!cur) { - break; - } - cur = cur[parts[i]]; - } - return cur; - }; - var modules = global.define && global.define.modules || {}; - var ourDefine = global.define = function(moduleName, deps, callback){ - var module; - if(typeof deps === "function") { - callback = deps; - deps = []; - } - var args = [], - i; - for(i =0; i < deps.length; i++) { - args.push( exports[deps[i]] ? get(exports[deps[i]]) : ( modules[deps[i]] || get(deps[i]) ) ); - } - // CJS has no dependencies but 3 callback arguments - if(!deps.length && callback.length) { - module = { exports: {} }; - var require = function(name) { - return exports[name] ? get(exports[name]) : modules[name]; - }; - args.push(require, module.exports, module); - } - - global.define = origDefine; - var result = callback ? callback.apply(null, args) : undefined; - global.define = ourDefine; - - // Favor CJS module.exports over the return value - modules[moduleName] = module && module.exports ? module.exports : result; - }; - global.define.modules = modules; - global.define.amd = true; - global.System = { - define: function(__name, __code){ - global.define = origDefine; - eval("(function() { " + __code + " \n }).call(global);"); - global.define = ourDefine; - } - }; -})({},window) -/*syn@0.1.1#synthetic*/ -define('syn/synthetic', function(require, exports, module) { -var opts = window.syn ? window.syn : {}; -var extend = function (d, s) { - var p; - for (p in s) { - d[p] = s[p]; - } - return d; - }, browser = { - msie: !!(window.attachEvent && !window.opera), - opera: !!window.opera, - webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, - safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') === -1, - gecko: navigator.userAgent.indexOf('Gecko') > -1, - mobilesafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/), - rhino: navigator.userAgent.match(/Rhino/) && true - }, createEventObject = function (type, options, element) { - var event = element.ownerDocument.createEventObject(); - return extend(event, options); - }, data = {}, id = 1, expando = '_synthetic' + new Date().getTime(), bind, unbind, schedule, key = /keypress|keyup|keydown/, page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/, activeElement, syn = function (type, element, options, callback) { - return new syn.init(type, element, options, callback); - }; -syn.config = opts; -syn.__tryFocus = function tryFocus(element) { - try { - element.focus(); - } catch (e) { - } -}; -bind = function (el, ev, f) { - return el.addEventListener ? el.addEventListener(ev, f, false) : el.attachEvent('on' + ev, f); -}; -unbind = function (el, ev, f) { - return el.addEventListener ? el.removeEventListener(ev, f, false) : el.detachEvent('on' + ev, f); -}; -schedule = syn.config.schedule || function (fn, ms) { - setTimeout(fn, ms); -}; -extend(syn, { - init: function (type, element, options, callback) { - var args = syn.args(options, element, callback), self = this; - this.queue = []; - this.element = args.element; - if (typeof this[type] === 'function') { - this[type](args.element, args.options, function (defaults, el) { - if (args.callback) { - args.callback.apply(self, arguments); - } - self.done.apply(self, arguments); - }); - } else { - this.result = syn.trigger(args.element, type, args.options); - if (args.callback) { - args.callback.call(this, args.element, this.result); - } - } - }, - jquery: function (el, fast) { - if (window.FuncUnit && window.FuncUnit.jQuery) { - return window.FuncUnit.jQuery; - } - if (el) { - return syn.helpers.getWindow(el).jQuery || window.jQuery; - } else { - return window.jQuery; - } - }, - args: function () { - var res = {}, i = 0; - for (; i < arguments.length; i++) { - if (typeof arguments[i] === 'function') { - res.callback = arguments[i]; - } else if (arguments[i] && arguments[i].jquery) { - res.element = arguments[i][0]; - } else if (arguments[i] && arguments[i].nodeName) { - res.element = arguments[i]; - } else if (res.options && typeof arguments[i] === 'string') { - res.element = document.getElementById(arguments[i]); - } else if (arguments[i]) { - res.options = arguments[i]; - } - } - return res; - }, - click: function (element, options, callback) { - syn('click!', element, options, callback); - }, - defaults: { - focus: function focus() { - if (!syn.support.focusChanges) { - var element = this, nodeName = element.nodeName.toLowerCase(); - syn.data(element, 'syntheticvalue', element.value); - if (nodeName === 'input' || nodeName === 'textarea') { - bind(element, 'blur', function blur() { - if (syn.data(element, 'syntheticvalue') !== element.value) { - syn.trigger(element, 'change', {}); - } - unbind(element, 'blur', blur); - }); - } - } - }, - submit: function () { - syn.onParents(this, function (el) { - if (el.nodeName.toLowerCase() === 'form') { - el.submit(); - return false; - } - }); - } - }, - changeOnBlur: function (element, prop, value) { - bind(element, 'blur', function onblur() { - if (value !== element[prop]) { - syn.trigger(element, 'change', {}); - } - unbind(element, 'blur', onblur); - }); - }, - closest: function (el, type) { - while (el && el.nodeName.toLowerCase() !== type.toLowerCase()) { - el = el.parentNode; - } - return el; - }, - data: function (el, key, value) { - var d; - if (!el[expando]) { - el[expando] = id++; - } - if (!data[el[expando]]) { - data[el[expando]] = {}; - } - d = data[el[expando]]; - if (value) { - data[el[expando]][key] = value; - } else { - return data[el[expando]][key]; - } - }, - onParents: function (el, func) { - var res; - while (el && res !== false) { - res = func(el); - el = el.parentNode; - } - return el; - }, - focusable: /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i, - isFocusable: function (elem) { - var attributeNode; - if (elem.getAttributeNode) { - attributeNode = elem.getAttributeNode('tabIndex'); - } - return this.focusable.test(elem.nodeName) || attributeNode && attributeNode.specified && syn.isVisible(elem); - }, - isVisible: function (elem) { - return elem.offsetWidth && elem.offsetHeight || elem.clientWidth && elem.clientHeight; - }, - tabIndex: function (elem) { - var attributeNode = elem.getAttributeNode('tabIndex'); - return attributeNode && attributeNode.specified && (parseInt(elem.getAttribute('tabIndex')) || 0); - }, - bind: bind, - unbind: unbind, - schedule: schedule, - browser: browser, - helpers: { - createEventObject: createEventObject, - createBasicStandardEvent: function (type, defaults, doc) { - var event; - try { - event = doc.createEvent('Events'); - } catch (e2) { - event = doc.createEvent('UIEvents'); - } finally { - event.initEvent(type, true, true); - extend(event, defaults); - } - return event; - }, - inArray: function (item, array) { - var i = 0; - for (; i < array.length; i++) { - if (array[i] === item) { - return i; - } - } - return -1; - }, - getWindow: function (element) { - if (element.ownerDocument) { - return element.ownerDocument.defaultView || element.ownerDocument.parentWindow; - } - }, - extend: extend, - scrollOffset: function (win, set) { - var doc = win.document.documentElement, body = win.document.body; - if (set) { - window.scrollTo(set.left, set.top); - } else { - return { - left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0), - top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0) - }; - } - }, - scrollDimensions: function (win) { - var doc = win.document.documentElement, body = win.document.body, docWidth = doc.clientWidth, docHeight = doc.clientHeight, compat = win.document.compatMode === 'CSS1Compat'; - return { - height: compat && docHeight || body.clientHeight || docHeight, - width: compat && docWidth || body.clientWidth || docWidth - }; - }, - addOffset: function (options, el) { - var jq = syn.jquery(el), off; - if (typeof options === 'object' && options.clientX === undefined && options.clientY === undefined && options.pageX === undefined && options.pageY === undefined && jq) { - el = jq(el); - off = el.offset(); - options.pageX = off.left + el.width() / 2; - options.pageY = off.top + el.height() / 2; - } - } - }, - key: { - ctrlKey: null, - altKey: null, - shiftKey: null, - metaKey: null - }, - dispatch: function (event, element, type, autoPrevent) { - if (element.dispatchEvent && event) { - var preventDefault = event.preventDefault, prevents = autoPrevent ? -1 : 0; - if (autoPrevent) { - bind(element, type, function ontype(ev) { - ev.preventDefault(); - unbind(this, type, ontype); - }); - } - event.preventDefault = function () { - prevents++; - if (++prevents > 0) { - preventDefault.apply(this, []); - } - }; - element.dispatchEvent(event); - return prevents <= 0; - } else { - try { - window.event = event; - } catch (e) { - } - return element.sourceIndex <= 0 || element.fireEvent && element.fireEvent('on' + type, event); - } - }, - create: { - page: { - event: function (type, options, element) { - var doc = syn.helpers.getWindow(element).document || document, event; - if (doc.createEvent) { - event = doc.createEvent('Events'); - event.initEvent(type, true, true); - return event; - } else { - try { - event = createEventObject(type, options, element); - } catch (e) { - } - return event; - } - } - }, - focus: { - event: function (type, options, element) { - syn.onParents(element, function (el) { - if (syn.isFocusable(el)) { - if (el.nodeName.toLowerCase() !== 'html') { - syn.__tryFocus(el); - activeElement = el; - } else if (activeElement) { - var doc = syn.helpers.getWindow(element).document; - if (doc !== window.document) { - return false; - } else if (doc.activeElement) { - doc.activeElement.blur(); - activeElement = null; - } else { - activeElement.blur(); - activeElement = null; - } - } - return false; - } - }); - return true; - } - } - }, - support: { - clickChanges: false, - clickSubmits: false, - keypressSubmits: false, - mouseupSubmits: false, - radioClickChanges: false, - focusChanges: false, - linkHrefJS: false, - keyCharacters: false, - backspaceWorks: false, - mouseDownUpClicks: false, - tabKeyTabs: false, - keypressOnAnchorClicks: false, - optionClickBubbles: false, - ready: 0 - }, - trigger: function (element, type, options) { - if (!options) { - options = {}; - } - var create = syn.create, setup = create[type] && create[type].setup, kind = key.test(type) ? 'key' : page.test(type) ? 'page' : 'mouse', createType = create[type] || {}, createKind = create[kind], event, ret, autoPrevent, dispatchEl = element; - if (syn.support.ready === 2 && setup) { - setup(type, options, element); - } - autoPrevent = options._autoPrevent; - delete options._autoPrevent; - if (createType.event) { - ret = createType.event(type, options, element); - } else { - options = createKind.options ? createKind.options(type, options, element) : options; - if (!syn.support.changeBubbles && /option/i.test(element.nodeName)) { - dispatchEl = element.parentNode; - } - event = createKind.event(type, options, dispatchEl); - ret = syn.dispatch(event, dispatchEl, type, autoPrevent); - } - if (ret && syn.support.ready === 2 && syn.defaults[type]) { - syn.defaults[type].call(element, options, autoPrevent); - } - return ret; - }, - eventSupported: function (eventName) { - var el = document.createElement('div'); - eventName = 'on' + eventName; - var isSupported = eventName in el; - if (!isSupported) { - el.setAttribute(eventName, 'return;'); - isSupported = typeof el[eventName] === 'function'; - } - el = null; - return isSupported; - } -}); -extend(syn.init.prototype, { - then: function (type, element, options, callback) { - if (syn.autoDelay) { - this.delay(); - } - var args = syn.args(options, element, callback), self = this; - this.queue.unshift(function (el, prevented) { - if (typeof this[type] === 'function') { - this.element = args.element || el; - this[type](this.element, args.options, function (defaults, el) { - if (args.callback) { - args.callback.apply(self, arguments); - } - self.done.apply(self, arguments); - }); - } else { - this.result = syn.trigger(args.element, type, args.options); - if (args.callback) { - args.callback.call(this, args.element, this.result); - } - return this; - } - }); - return this; - }, - delay: function (timeout, callback) { - if (typeof timeout === 'function') { - callback = timeout; - timeout = null; - } - timeout = timeout || 600; - var self = this; - this.queue.unshift(function () { - schedule(function () { - if (callback) { - callback.apply(self, []); - } - self.done.apply(self, arguments); - }, timeout); - }); - return this; - }, - done: function (defaults, el) { - if (el) { - this.element = el; - } - if (this.queue.length) { - this.queue.pop().call(this, this.element, defaults); - } - }, - '_click': function (element, options, callback, force) { - syn.helpers.addOffset(options, element); - syn.trigger(element, 'mousedown', options); - schedule(function () { - syn.trigger(element, 'mouseup', options); - if (!syn.support.mouseDownUpClicks || force) { - syn.trigger(element, 'click', options); - callback(true); - } else { - syn.create.click.setup('click', options, element); - syn.defaults.click.call(element); - schedule(function () { - callback(true); - }, 1); - } - }, 1); - }, - '_rightClick': function (element, options, callback) { - syn.helpers.addOffset(options, element); - var mouseopts = extend(extend({}, syn.mouse.browser.right.mouseup), options); - syn.trigger(element, 'mousedown', mouseopts); - schedule(function () { - syn.trigger(element, 'mouseup', mouseopts); - if (syn.mouse.browser.right.contextmenu) { - syn.trigger(element, 'contextmenu', extend(extend({}, syn.mouse.browser.right.contextmenu), options)); - } - callback(true); - }, 1); - }, - '_dblclick': function (element, options, callback) { - syn.helpers.addOffset(options, element); - var self = this; - this._click(element, options, function () { - schedule(function () { - self._click(element, options, function () { - syn.trigger(element, 'dblclick', options); - callback(true); - }, true); - }, 2); - }); - } -}); -var actions = [ - 'click', - 'dblclick', - 'move', - 'drag', - 'key', - 'type', - 'rightClick' - ], makeAction = function (name) { - syn[name] = function (element, options, callback) { - return syn('_' + name, element, options, callback); - }; - syn.init.prototype[name] = function (element, options, callback) { - return this.then('_' + name, element, options, callback); - }; - }, i = 0; -for (; i < actions.length; i++) { - makeAction(actions[i]); -} -module.exports = syn; -}); -/*syn@0.1.1#mouse*/ -define('syn/mouse', function(require, exports, module) { -var syn = require('syn/synthetic'); -var h = syn.helpers, getWin = h.getWindow; -syn.mouse = {}; -h.extend(syn.defaults, { - mousedown: function (options) { - syn.trigger(this, 'focus', {}); - }, - click: function () { - var element = this, href, type, createChange, radioChanged, nodeName, scope; - try { - href = element.href; - type = element.type; - createChange = syn.data(element, 'createChange'); - radioChanged = syn.data(element, 'radioChanged'); - scope = getWin(element); - nodeName = element.nodeName.toLowerCase(); - } catch (e) { - return; - } - if (!syn.support.linkHrefJS && /^\s*javascript:/.test(href)) { - var code = href.replace(/^\s*javascript:/, ''); - if (code !== '//' && code.indexOf('void(0)') === -1) { - if (window.selenium) { - eval('with(selenium.browserbot.getCurrentWindow()){' + code + '}'); - } else { - eval('with(scope){' + code + '}'); - } - } - } - if (!syn.support.clickSubmits && (nodeName === 'input' && type === 'submit') || nodeName === 'button') { - var form = syn.closest(element, 'form'); - if (form) { - syn.trigger(form, 'submit', {}); - } - } - if (nodeName === 'a' && element.href && !/^\s*javascript:/.test(href)) { - scope.location.href = href; - } - if (nodeName === 'input' && type === 'checkbox') { - if (!syn.support.clickChanges) { - syn.trigger(element, 'change', {}); - } - } - if (nodeName === 'input' && type === 'radio') { - if (radioChanged && !syn.support.radioClickChanges) { - syn.trigger(element, 'change', {}); - } - } - if (nodeName === 'option' && createChange) { - syn.trigger(element.parentNode, 'change', {}); - syn.data(element, 'createChange', false); - } - } -}); -h.extend(syn.create, { - mouse: { - options: function (type, options, element) { - var doc = document.documentElement, body = document.body, center = [ - options.pageX || 0, - options.pageY || 0 - ], left = syn.mouse.browser && syn.mouse.browser.left[type], right = syn.mouse.browser && syn.mouse.browser.right[type]; - return h.extend({ - bubbles: true, - cancelable: true, - view: window, - detail: 1, - screenX: 1, - screenY: 1, - clientX: options.clientX || center[0] - (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0), - clientY: options.clientY || center[1] - (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0), - ctrlKey: !!syn.key.ctrlKey, - altKey: !!syn.key.altKey, - shiftKey: !!syn.key.shiftKey, - metaKey: !!syn.key.metaKey, - button: left && left.button !== null ? left.button : right && right.button || (type === 'contextmenu' ? 2 : 0), - relatedTarget: document.documentElement - }, options); - }, - event: function (type, defaults, element) { - var doc = getWin(element).document || document, event; - if (doc.createEvent) { - try { - event = doc.createEvent('MouseEvents'); - event.initMouseEvent(type, defaults.bubbles, defaults.cancelable, defaults.view, defaults.detail, defaults.screenX, defaults.screenY, defaults.clientX, defaults.clientY, defaults.ctrlKey, defaults.altKey, defaults.shiftKey, defaults.metaKey, defaults.button, defaults.relatedTarget); - } catch (e) { - event = h.createBasicStandardEvent(type, defaults, doc); - } - event.synthetic = true; - return event; - } else { - try { - event = h.createEventObject(type, defaults, element); - } catch (e) { - } - return event; - } - } - }, - click: { - setup: function (type, options, element) { - var nodeName = element.nodeName.toLowerCase(); - if (!syn.support.clickChecks && !syn.support.changeChecks && nodeName === 'input') { - type = element.type.toLowerCase(); - if (type === 'checkbox') { - element.checked = !element.checked; - } - if (type === 'radio') { - if (!element.checked) { - try { - syn.data(element, 'radioChanged', true); - } catch (e) { - } - element.checked = true; - } - } - } - if (nodeName === 'a' && element.href && !/^\s*javascript:/.test(element.href)) { - syn.data(element, 'href', element.href); - } - if (/option/i.test(element.nodeName)) { - var child = element.parentNode.firstChild, i = -1; - while (child) { - if (child.nodeType === 1) { - i++; - if (child === element) { - break; - } - } - child = child.nextSibling; - } - if (i !== element.parentNode.selectedIndex) { - element.parentNode.selectedIndex = i; - syn.data(element, 'createChange', true); - } - } - } - }, - mousedown: { - setup: function (type, options, element) { - var nn = element.nodeName.toLowerCase(); - if (syn.browser.safari && (nn === 'select' || nn === 'option')) { - options._autoPrevent = true; - } - } - } -}); -}); -/*syn@0.1.1#mouse.support*/ -define('syn/mouse.support', function(require, exports, module) { -var syn = require('syn/synthetic'); -require('syn/mouse'); -if (!document.body) { - syn.schedule(function () { - checkSupport(syn); - }, 1); -} else { - window.__synthTest = function () { - syn.support.linkHrefJS = true; - }; - var div = document.createElement('div'), checkbox, submit, form, select; - div.innerHTML = '
          ' + '' + '' + '' + '' + '' + '' + '' + '' + '
          '; - document.documentElement.appendChild(div); - form = div.firstChild; - checkbox = form.childNodes[0]; - submit = form.childNodes[2]; - select = form.getElementsByTagName('select')[0]; - syn.trigger(form.childNodes[6], 'click', {}); - checkbox.checked = false; - checkbox.onchange = function () { - syn.support.clickChanges = true; - }; - syn.trigger(checkbox, 'click', {}); - syn.support.clickChecks = checkbox.checked; - checkbox.checked = false; - syn.trigger(checkbox, 'change', {}); - syn.support.changeChecks = checkbox.checked; - form.onsubmit = function (ev) { - if (ev.preventDefault) { - ev.preventDefault(); - } - syn.support.clickSubmits = true; - return false; - }; - syn.trigger(submit, 'click', {}); - form.childNodes[1].onchange = function () { - syn.support.radioClickChanges = true; - }; - syn.trigger(form.childNodes[1], 'click', {}); - syn.bind(div, 'click', function onclick() { - syn.support.optionClickBubbles = true; - syn.unbind(div, 'click', onclick); - }); - syn.trigger(select.firstChild, 'click', {}); - syn.support.changeBubbles = syn.eventSupported('change'); - div.onclick = function () { - syn.support.mouseDownUpClicks = true; - }; - syn.trigger(div, 'mousedown', {}); - syn.trigger(div, 'mouseup', {}); - document.documentElement.removeChild(div); - syn.support.ready++; -} -}); -/*syn@0.1.1#browsers*/ -define('syn/browsers', function(require, exports, module) { -var syn = require('syn/synthetic'); -require('syn/mouse'); -syn.key.browsers = { - webkit: { - 'prevent': { - 'keyup': [], - 'keydown': [ - 'char', - 'keypress' - ], - 'keypress': ['char'] - }, - 'character': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 'char', - 'char' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'specialChars': { - 'keydown': [ - 0, - 'char' - ], - 'keyup': [ - 0, - 'char' - ] - }, - 'navigation': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'special': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'tab': { - 'keydown': [ - 0, - 'char' - ], - 'keyup': [ - 0, - 'char' - ] - }, - 'pause-break': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'caps': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'escape': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'num-lock': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'scroll-lock': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'print': { - 'keyup': [ - 0, - 'key' - ] - }, - 'function': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - '\r': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 'char', - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - } - }, - gecko: { - 'prevent': { - 'keyup': [], - 'keydown': ['char'], - 'keypress': ['char'] - }, - 'character': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 'char', - 0 - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'specialChars': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'navigation': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'special': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - '\t': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'pause-break': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'caps': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'escape': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'num-lock': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'scroll-lock': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - 'print': { - 'keyup': [ - 0, - 'key' - ] - }, - 'function': { - 'keydown': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - }, - '\r': { - 'keydown': [ - 0, - 'key' - ], - 'keypress': [ - 0, - 'key' - ], - 'keyup': [ - 0, - 'key' - ] - } - }, - msie: { - 'prevent': { - 'keyup': [], - 'keydown': [ - 'char', - 'keypress' - ], - 'keypress': ['char'] - }, - 'character': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'char' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'specialChars': { - 'keydown': [ - null, - 'char' - ], - 'keyup': [ - null, - 'char' - ] - }, - 'navigation': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'special': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'tab': { - 'keydown': [ - null, - 'char' - ], - 'keyup': [ - null, - 'char' - ] - }, - 'pause-break': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'caps': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'escape': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'num-lock': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'scroll-lock': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'print': { - 'keyup': [ - null, - 'key' - ] - }, - 'function': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - '\r': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - } - }, - opera: { - 'prevent': { - 'keyup': [], - 'keydown': [], - 'keypress': ['char'] - }, - 'character': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'char' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'specialChars': { - 'keydown': [ - null, - 'char' - ], - 'keypress': [ - null, - 'char' - ], - 'keyup': [ - null, - 'char' - ] - }, - 'navigation': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ] - }, - 'special': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'tab': { - 'keydown': [ - null, - 'char' - ], - 'keypress': [ - null, - 'char' - ], - 'keyup': [ - null, - 'char' - ] - }, - 'pause-break': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'caps': { - 'keydown': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'escape': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ] - }, - 'num-lock': { - 'keyup': [ - null, - 'key' - ], - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ] - }, - 'scroll-lock': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - 'print': {}, - 'function': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - }, - '\r': { - 'keydown': [ - null, - 'key' - ], - 'keypress': [ - null, - 'key' - ], - 'keyup': [ - null, - 'key' - ] - } - } -}; -syn.mouse.browsers = { - webkit: { - 'right': { - 'mousedown': { - 'button': 2, - 'which': 3 - }, - 'mouseup': { - 'button': 2, - 'which': 3 - }, - 'contextmenu': { - 'button': 2, - 'which': 3 - } - }, - 'left': { - 'mousedown': { - 'button': 0, - 'which': 1 - }, - 'mouseup': { - 'button': 0, - 'which': 1 - }, - 'click': { - 'button': 0, - 'which': 1 - } - } - }, - opera: { - 'right': { - 'mousedown': { - 'button': 2, - 'which': 3 - }, - 'mouseup': { - 'button': 2, - 'which': 3 - } - }, - 'left': { - 'mousedown': { - 'button': 0, - 'which': 1 - }, - 'mouseup': { - 'button': 0, - 'which': 1 - }, - 'click': { - 'button': 0, - 'which': 1 - } - } - }, - msie: { - 'right': { - 'mousedown': { 'button': 2 }, - 'mouseup': { 'button': 2 }, - 'contextmenu': { 'button': 0 } - }, - 'left': { - 'mousedown': { 'button': 1 }, - 'mouseup': { 'button': 1 }, - 'click': { 'button': 0 } - } - }, - chrome: { - 'right': { - 'mousedown': { - 'button': 2, - 'which': 3 - }, - 'mouseup': { - 'button': 2, - 'which': 3 - }, - 'contextmenu': { - 'button': 2, - 'which': 3 - } - }, - 'left': { - 'mousedown': { - 'button': 0, - 'which': 1 - }, - 'mouseup': { - 'button': 0, - 'which': 1 - }, - 'click': { - 'button': 0, - 'which': 1 - } - } - }, - gecko: { - 'left': { - 'mousedown': { - 'button': 0, - 'which': 1 - }, - 'mouseup': { - 'button': 0, - 'which': 1 - }, - 'click': { - 'button': 0, - 'which': 1 - } - }, - 'right': { - 'mousedown': { - 'button': 2, - 'which': 3 - }, - 'mouseup': { - 'button': 2, - 'which': 3 - }, - 'contextmenu': { - 'button': 2, - 'which': 3 - } - } - } -}; -syn.key.browser = function () { - if (syn.key.browsers[window.navigator.userAgent]) { - return syn.key.browsers[window.navigator.userAgent]; - } - for (var browser in syn.browser) { - if (syn.browser[browser] && syn.key.browsers[browser]) { - return syn.key.browsers[browser]; - } - } - return syn.key.browsers.gecko; -}(); -syn.mouse.browser = function () { - if (syn.mouse.browsers[window.navigator.userAgent]) { - return syn.mouse.browsers[window.navigator.userAgent]; - } - for (var browser in syn.browser) { - if (syn.browser[browser] && syn.mouse.browsers[browser]) { - return syn.mouse.browsers[browser]; - } - } - return syn.mouse.browsers.gecko; -}(); -}); -/*syn@0.1.1#typeable*/ -define('syn/typeable', function(require, exports, module) { -var syn = require('syn/synthetic'); -var typeables = []; -var __indexOf = [].indexOf || function (item) { - for (var i = 0, l = this.length; i < l; i++) { - if (i in this && this[i] === item) { - return i; - } - } - return -1; - }; -syn.typeable = function (fn) { - if (__indexOf.call(typeables, fn) === -1) { - typeables.push(fn); - } -}; -syn.typeable.test = function (el) { - for (var i = 0, len = typeables.length; i < len; i++) { - if (typeables[i](el)) { - return true; - } - } - return false; -}; -var type = syn.typeable; -var typeableExp = /input|textarea/i; -type(function (el) { - return typeableExp.test(el.nodeName); -}); -type(function (el) { - return __indexOf.call([ - '', - 'true' - ], el.getAttribute('contenteditable')) !== -1; -}); -}); -/*syn@0.1.1#key*/ -define('syn/key', function(require, exports, module) { -var syn = require('syn/synthetic'); -require('syn/typeable'); -require('syn/browsers'); -var h = syn.helpers, getSelection = function (el) { - var real, r, start; - if (el.selectionStart !== undefined) { - if (document.activeElement && document.activeElement !== el && el.selectionStart === el.selectionEnd && el.selectionStart === 0) { - return { - start: el.value.length, - end: el.value.length - }; - } - return { - start: el.selectionStart, - end: el.selectionEnd - }; - } else { - try { - if (el.nodeName.toLowerCase() === 'input') { - real = h.getWindow(el).document.selection.createRange(); - r = el.createTextRange(); - r.setEndPoint('EndToStart', real); - start = r.text.length; - return { - start: start, - end: start + real.text.length - }; - } else { - real = h.getWindow(el).document.selection.createRange(); - r = real.duplicate(); - var r2 = real.duplicate(), r3 = real.duplicate(); - r2.collapse(); - r3.collapse(false); - r2.moveStart('character', -1); - r3.moveStart('character', -1); - r.moveToElementText(el); - r.setEndPoint('EndToEnd', real); - start = r.text.length - real.text.length; - var end = r.text.length; - if (start !== 0 && r2.text === '') { - start += 2; - } - if (end !== 0 && r3.text === '') { - end += 2; - } - return { - start: start, - end: end - }; - } - } catch (e) { - var prop = formElExp.test(el.nodeName) ? 'value' : 'textContent'; - return { - start: el[prop].length, - end: el[prop].length - }; - } - } - }, getFocusable = function (el) { - var document = h.getWindow(el).document, res = []; - var els = document.getElementsByTagName('*'), len = els.length; - for (var i = 0; i < len; i++) { - if (syn.isFocusable(els[i]) && els[i] !== document.documentElement) { - res.push(els[i]); - } - } - return res; - }, formElExp = /input|textarea/i, textProperty = function () { - var el = document.createElement('span'); - return el.textContent != null ? 'textContent' : 'innerText'; - }(), getText = function (el) { - if (formElExp.test(el.nodeName)) { - return el.value; - } - return el[textProperty]; - }, setText = function (el, value) { - if (formElExp.test(el.nodeName)) { - el.value = value; - } else { - el[textProperty] = value; - } - }; -h.extend(syn, { - keycodes: { - '\b': 8, - '\t': 9, - '\r': 13, - 'shift': 16, - 'ctrl': 17, - 'alt': 18, - 'pause-break': 19, - 'caps': 20, - 'escape': 27, - 'num-lock': 144, - 'scroll-lock': 145, - 'print': 44, - 'page-up': 33, - 'page-down': 34, - 'end': 35, - 'home': 36, - 'left': 37, - 'up': 38, - 'right': 39, - 'down': 40, - 'insert': 45, - 'delete': 46, - ' ': 32, - '0': 48, - '1': 49, - '2': 50, - '3': 51, - '4': 52, - '5': 53, - '6': 54, - '7': 55, - '8': 56, - '9': 57, - 'a': 65, - 'b': 66, - 'c': 67, - 'd': 68, - 'e': 69, - 'f': 70, - 'g': 71, - 'h': 72, - 'i': 73, - 'j': 74, - 'k': 75, - 'l': 76, - 'm': 77, - 'n': 78, - 'o': 79, - 'p': 80, - 'q': 81, - 'r': 82, - 's': 83, - 't': 84, - 'u': 85, - 'v': 86, - 'w': 87, - 'x': 88, - 'y': 89, - 'z': 90, - 'num0': 96, - 'num1': 97, - 'num2': 98, - 'num3': 99, - 'num4': 100, - 'num5': 101, - 'num6': 102, - 'num7': 103, - 'num8': 104, - 'num9': 105, - '*': 106, - '+': 107, - 'subtract': 109, - 'decimal': 110, - 'divide': 111, - ';': 186, - '=': 187, - ',': 188, - 'dash': 189, - '-': 189, - 'period': 190, - '.': 190, - 'forward-slash': 191, - '/': 191, - '`': 192, - '[': 219, - '\\': 220, - ']': 221, - '\'': 222, - 'left window key': 91, - 'right window key': 92, - 'select key': 93, - 'f1': 112, - 'f2': 113, - 'f3': 114, - 'f4': 115, - 'f5': 116, - 'f6': 117, - 'f7': 118, - 'f8': 119, - 'f9': 120, - 'f10': 121, - 'f11': 122, - 'f12': 123 - }, - selectText: function (el, start, end) { - if (el.setSelectionRange) { - if (!end) { - syn.__tryFocus(el); - el.setSelectionRange(start, start); - } else { - el.selectionStart = start; - el.selectionEnd = end; - } - } else if (el.createTextRange) { - var r = el.createTextRange(); - r.moveStart('character', start); - end = end || start; - r.moveEnd('character', end - el.value.length); - r.select(); - } - }, - getText: function (el) { - if (syn.typeable.test(el)) { - var sel = getSelection(el); - return el.value.substring(sel.start, sel.end); - } - var win = syn.helpers.getWindow(el); - if (win.getSelection) { - return win.getSelection().toString(); - } else if (win.document.getSelection) { - return win.document.getSelection().toString(); - } else { - return win.document.selection.createRange().text; - } - }, - getSelection: getSelection -}); -h.extend(syn.key, { - data: function (key) { - if (syn.key.browser[key]) { - return syn.key.browser[key]; - } - for (var kind in syn.key.kinds) { - if (h.inArray(key, syn.key.kinds[kind]) > -1) { - return syn.key.browser[kind]; - } - } - return syn.key.browser.character; - }, - isSpecial: function (keyCode) { - var specials = syn.key.kinds.special; - for (var i = 0; i < specials.length; i++) { - if (syn.keycodes[specials[i]] === keyCode) { - return specials[i]; - } - } - }, - options: function (key, event) { - var keyData = syn.key.data(key); - if (!keyData[event]) { - return null; - } - var charCode = keyData[event][0], keyCode = keyData[event][1], result = {}; - if (keyCode === 'key') { - result.keyCode = syn.keycodes[key]; - } else if (keyCode === 'char') { - result.keyCode = key.charCodeAt(0); - } else { - result.keyCode = keyCode; - } - if (charCode === 'char') { - result.charCode = key.charCodeAt(0); - } else if (charCode !== null) { - result.charCode = charCode; - } - if (result.keyCode) { - result.which = result.keyCode; - } else { - result.which = result.charCode; - } - return result; - }, - kinds: { - special: [ - 'shift', - 'ctrl', - 'alt', - 'caps' - ], - specialChars: ['\b'], - navigation: [ - 'page-up', - 'page-down', - 'end', - 'home', - 'left', - 'up', - 'right', - 'down', - 'insert', - 'delete' - ], - 'function': [ - 'f1', - 'f2', - 'f3', - 'f4', - 'f5', - 'f6', - 'f7', - 'f8', - 'f9', - 'f10', - 'f11', - 'f12' - ] - }, - getDefault: function (key) { - if (syn.key.defaults[key]) { - return syn.key.defaults[key]; - } - for (var kind in syn.key.kinds) { - if (h.inArray(key, syn.key.kinds[kind]) > -1 && syn.key.defaults[kind]) { - return syn.key.defaults[kind]; - } - } - return syn.key.defaults.character; - }, - defaults: { - 'character': function (options, scope, key, force, sel) { - if (/num\d+/.test(key)) { - key = key.match(/\d+/)[0]; - } - if (force || !syn.support.keyCharacters && syn.typeable.test(this)) { - var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end), character = key; - setText(this, before + character + after); - var charLength = character === '\n' && syn.support.textareaCarriage ? 2 : character.length; - syn.selectText(this, before.length + charLength); - } - }, - 'c': function (options, scope, key, force, sel) { - if (syn.key.ctrlKey) { - syn.key.clipboard = syn.getText(this); - } else { - syn.key.defaults.character.apply(this, arguments); - } - }, - 'v': function (options, scope, key, force, sel) { - if (syn.key.ctrlKey) { - syn.key.defaults.character.call(this, options, scope, syn.key.clipboard, true, sel); - } else { - syn.key.defaults.character.apply(this, arguments); - } - }, - 'a': function (options, scope, key, force, sel) { - if (syn.key.ctrlKey) { - syn.selectText(this, 0, getText(this).length); - } else { - syn.key.defaults.character.apply(this, arguments); - } - }, - 'home': function () { - syn.onParents(this, function (el) { - if (el.scrollHeight !== el.clientHeight) { - el.scrollTop = 0; - return false; - } - }); - }, - 'end': function () { - syn.onParents(this, function (el) { - if (el.scrollHeight !== el.clientHeight) { - el.scrollTop = el.scrollHeight; - return false; - } - }); - }, - 'page-down': function () { - syn.onParents(this, function (el) { - if (el.scrollHeight !== el.clientHeight) { - var ch = el.clientHeight; - el.scrollTop += ch; - return false; - } - }); - }, - 'page-up': function () { - syn.onParents(this, function (el) { - if (el.scrollHeight !== el.clientHeight) { - var ch = el.clientHeight; - el.scrollTop -= ch; - return false; - } - }); - }, - '\b': function (options, scope, key, force, sel) { - if (!syn.support.backspaceWorks && syn.typeable.test(this)) { - var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end); - if (sel.start === sel.end && sel.start > 0) { - setText(this, before.substring(0, before.length - 1) + after); - syn.selectText(this, sel.start - 1); - } else { - setText(this, before + after); - syn.selectText(this, sel.start); - } - } - }, - 'delete': function (options, scope, key, force, sel) { - if (!syn.support.backspaceWorks && syn.typeable.test(this)) { - var current = getText(this), before = current.substr(0, sel.start), after = current.substr(sel.end); - if (sel.start === sel.end && sel.start <= getText(this).length - 1) { - setText(this, before + after.substring(1)); - } else { - setText(this, before + after); - } - syn.selectText(this, sel.start); - } - }, - '\r': function (options, scope, key, force, sel) { - var nodeName = this.nodeName.toLowerCase(); - if (nodeName === 'input') { - syn.trigger(this, 'change', {}); - } - if (!syn.support.keypressSubmits && nodeName === 'input') { - var form = syn.closest(this, 'form'); - if (form) { - syn.trigger(form, 'submit', {}); - } - } - if (!syn.support.keyCharacters && nodeName === 'textarea') { - syn.key.defaults.character.call(this, options, scope, '\n', undefined, sel); - } - if (!syn.support.keypressOnAnchorClicks && nodeName === 'a') { - syn.trigger(this, 'click', {}); - } - }, - '\t': function (options, scope) { - var focusEls = getFocusable(this), current = null, i = 0, el, firstNotIndexed, orders = []; - for (; i < focusEls.length; i++) { - orders.push([ - focusEls[i], - i - ]); - } - var sort = function (order1, order2) { - var el1 = order1[0], el2 = order2[0], tab1 = syn.tabIndex(el1) || 0, tab2 = syn.tabIndex(el2) || 0; - if (tab1 === tab2) { - return order1[1] - order2[1]; - } else { - if (tab1 === 0) { - return 1; - } else if (tab2 === 0) { - return -1; - } else { - return tab1 - tab2; - } - } - }; - orders.sort(sort); - for (i = 0; i < orders.length; i++) { - el = orders[i][0]; - if (this === el) { - if (!syn.key.shiftKey) { - current = orders[i + 1][0]; - if (!current) { - current = orders[0][0]; - } - } else { - current = orders[i - 1][0]; - if (!current) { - current = orders[focusEls.length - 1][0]; - } - } - } - } - if (!current) { - current = firstNotIndexed; - } else { - syn.__tryFocus(current); - } - return current; - }, - 'left': function (options, scope, key, force, sel) { - if (syn.typeable.test(this)) { - if (syn.key.shiftKey) { - syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1, sel.end); - } else { - syn.selectText(this, sel.start === 0 ? 0 : sel.start - 1); - } - } - }, - 'right': function (options, scope, key, force, sel) { - if (syn.typeable.test(this)) { - if (syn.key.shiftKey) { - syn.selectText(this, sel.start, sel.end + 1 > getText(this).length ? getText(this).length : sel.end + 1); - } else { - syn.selectText(this, sel.end + 1 > getText(this).length ? getText(this).length : sel.end + 1); - } - } - }, - 'up': function () { - if (/select/i.test(this.nodeName)) { - this.selectedIndex = this.selectedIndex ? this.selectedIndex - 1 : 0; - } - }, - 'down': function () { - if (/select/i.test(this.nodeName)) { - syn.changeOnBlur(this, 'selectedIndex', this.selectedIndex); - this.selectedIndex = this.selectedIndex + 1; - } - }, - 'shift': function () { - return null; - }, - 'ctrl': function () { - return null; - } - } -}); -h.extend(syn.create, { - keydown: { - setup: function (type, options, element) { - if (h.inArray(options, syn.key.kinds.special) !== -1) { - syn.key[options + 'Key'] = element; - } - } - }, - keypress: { - setup: function (type, options, element) { - if (syn.support.keyCharacters && !syn.support.keysOnNotFocused) { - syn.__tryFocus(element); - } - } - }, - keyup: { - setup: function (type, options, element) { - if (h.inArray(options, syn.key.kinds.special) !== -1) { - syn.key[options + 'Key'] = null; - } - } - }, - key: { - options: function (type, options, element) { - options = typeof options !== 'object' ? { character: options } : options; - options = h.extend({}, options); - if (options.character) { - h.extend(options, syn.key.options(options.character, type)); - delete options.character; - } - options = h.extend({ - ctrlKey: !!syn.key.ctrlKey, - altKey: !!syn.key.altKey, - shiftKey: !!syn.key.shiftKey, - metaKey: !!syn.key.metaKey - }, options); - return options; - }, - event: function (type, options, element) { - var doc = h.getWindow(element).document || document, event; - if (doc.createEvent) { - try { - event = doc.createEvent('KeyEvents'); - event.initKeyEvent(type, true, true, window, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode); - } catch (e) { - event = h.createBasicStandardEvent(type, options, doc); - } - event.synthetic = true; - return event; - } else { - try { - event = h.createEventObject.apply(this, arguments); - h.extend(event, options); - } catch (e) { - } - return event; - } - } - } -}); -var convert = { - 'enter': '\r', - 'backspace': '\b', - 'tab': '\t', - 'space': ' ' - }; -h.extend(syn.init.prototype, { - _key: function (element, options, callback) { - if (/-up$/.test(options) && h.inArray(options.replace('-up', ''), syn.key.kinds.special) !== -1) { - syn.trigger(element, 'keyup', options.replace('-up', '')); - return callback(true, element); - } - var activeElement = h.getWindow(element).document.activeElement, caret = syn.typeable.test(element) && getSelection(element), key = convert[options] || options, runDefaults = syn.trigger(element, 'keydown', key), getDefault = syn.key.getDefault, prevent = syn.key.browser.prevent, defaultResult, keypressOptions = syn.key.options(key, 'keypress'); - if (runDefaults) { - if (!keypressOptions) { - defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret); - } else { - if (activeElement !== h.getWindow(element).document.activeElement) { - element = h.getWindow(element).document.activeElement; - } - runDefaults = syn.trigger(element, 'keypress', keypressOptions); - if (runDefaults) { - defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key, undefined, caret); - } - } - } else { - if (keypressOptions && h.inArray('keypress', prevent.keydown) === -1) { - if (activeElement !== h.getWindow(element).document.activeElement) { - element = h.getWindow(element).document.activeElement; - } - syn.trigger(element, 'keypress', keypressOptions); - } - } - if (defaultResult && defaultResult.nodeName) { - element = defaultResult; - } - if (defaultResult !== null) { - syn.schedule(function () { - if (syn.support.oninput) { - syn.trigger(element, 'input', syn.key.options(key, 'input')); - } - syn.trigger(element, 'keyup', syn.key.options(key, 'keyup')); - callback(runDefaults, element); - }, 1); - } else { - callback(runDefaults, element); - } - return element; - }, - _type: function (element, options, callback) { - var parts = (options + '').match(/(\[[^\]]+\])|([^\[])/g), self = this, runNextPart = function (runDefaults, el) { - var part = parts.shift(); - if (!part) { - callback(runDefaults, el); - return; - } - el = el || element; - if (part.length > 1) { - part = part.substr(1, part.length - 2); - } - self._key(el, part, runNextPart); - }; - runNextPart(); - } -}); -}); -/*syn@0.1.1#key.support*/ -define('syn/key.support', function(require, exports, module) { -var syn = require('syn/synthetic'); -require('syn/key'); -if (!syn.config.support) { - (function checkForSupport() { - if (!document.body) { - return syn.schedule(checkForSupport, 1); - } - var div = document.createElement('div'), checkbox, submit, form, anchor, textarea, inputter, one, doc; - doc = document.documentElement; - div.innerHTML = '
          ' + '' + '' + '' + '' + '' + '' + '' + '' + '
          '; - doc.insertBefore(div, doc.firstElementChild || doc.children[0]); - form = div.firstChild; - checkbox = form.childNodes[0]; - submit = form.childNodes[2]; - anchor = form.getElementsByTagName('a')[0]; - textarea = form.getElementsByTagName('textarea')[0]; - inputter = form.childNodes[3]; - one = form.childNodes[4]; - form.onsubmit = function (ev) { - if (ev.preventDefault) { - ev.preventDefault(); - } - syn.support.keypressSubmits = true; - ev.returnValue = false; - return false; - }; - syn.__tryFocus(inputter); - syn.trigger(inputter, 'keypress', '\r'); - syn.trigger(inputter, 'keypress', 'a'); - syn.support.keyCharacters = inputter.value === 'a'; - inputter.value = 'a'; - syn.trigger(inputter, 'keypress', '\b'); - syn.support.backspaceWorks = inputter.value === ''; - inputter.onchange = function () { - syn.support.focusChanges = true; - }; - syn.__tryFocus(inputter); - syn.trigger(inputter, 'keypress', 'a'); - syn.__tryFocus(form.childNodes[5]); - syn.trigger(inputter, 'keypress', 'b'); - syn.support.keysOnNotFocused = inputter.value === 'ab'; - syn.bind(anchor, 'click', function (ev) { - if (ev.preventDefault) { - ev.preventDefault(); - } - syn.support.keypressOnAnchorClicks = true; - ev.returnValue = false; - return false; - }); - syn.trigger(anchor, 'keypress', '\r'); - syn.support.textareaCarriage = textarea.value.length === 4; - syn.support.oninput = 'oninput' in one; - doc.removeChild(div); - syn.support.ready++; - }()); -} else { - syn.helpers.extend(syn.support, syn.config.support); -} -}); -/*syn@0.1.1#drag*/ -define('syn/drag', function(require, exports, module) { -var syn = require('syn/synthetic'); -(function dragSupport() { - if (!document.body) { - syn.schedule(dragSupport, 1); - return; - } - var div = document.createElement('div'); - document.body.appendChild(div); - syn.helpers.extend(div.style, { - width: '100px', - height: '10000px', - backgroundColor: 'blue', - position: 'absolute', - top: '10px', - left: '0px', - zIndex: 19999 - }); - document.body.scrollTop = 11; - if (!document.elementFromPoint) { - return; - } - var el = document.elementFromPoint(3, 1); - if (el === div) { - syn.support.elementFromClient = true; - } else { - syn.support.elementFromPage = true; - } - document.body.removeChild(div); - document.body.scrollTop = 0; -}()); -var elementFromPoint = function (point, element) { - var clientX = point.clientX, clientY = point.clientY, win = syn.helpers.getWindow(element), el; - if (syn.support.elementFromPage) { - var off = syn.helpers.scrollOffset(win); - clientX = clientX + off.left; - clientY = clientY + off.top; - } - el = win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element; - if (el === win.document.documentElement && (point.clientY < 0 || point.clientX < 0)) { - return element; - } else { - return el; - } - }, createEventAtPoint = function (event, point, element) { - var el = elementFromPoint(point, element); - syn.trigger(el || element, event, point); - return el; - }, mouseMove = function (point, element, last) { - var el = elementFromPoint(point, element); - if (last !== el && el && last) { - var options = syn.helpers.extend({}, point); - options.relatedTarget = el; - syn.trigger(last, 'mouseout', options); - options.relatedTarget = last; - syn.trigger(el, 'mouseover', options); - } - syn.trigger(el || element, 'mousemove', point); - return el; - }, startMove = function (start, end, duration, element, callback) { - var startTime = new Date(), distX = end.clientX - start.clientX, distY = end.clientY - start.clientY, win = syn.helpers.getWindow(element), current = elementFromPoint(start, element), cursor = win.document.createElement('div'), calls = 0, move; - move = function onmove() { - var now = new Date(), scrollOffset = syn.helpers.scrollOffset(win), fraction = (calls === 0 ? 0 : now - startTime) / duration, options = { - clientX: distX * fraction + start.clientX, - clientY: distY * fraction + start.clientY - }; - calls++; - if (fraction < 1) { - syn.helpers.extend(cursor.style, { - left: options.clientX + scrollOffset.left + 2 + 'px', - top: options.clientY + scrollOffset.top + 2 + 'px' - }); - current = mouseMove(options, element, current); - syn.schedule(onmove, 15); - } else { - current = mouseMove(end, element, current); - win.document.body.removeChild(cursor); - callback(); - } - }; - syn.helpers.extend(cursor.style, { - height: '5px', - width: '5px', - backgroundColor: 'red', - position: 'absolute', - zIndex: 19999, - fontSize: '1px' - }); - win.document.body.appendChild(cursor); - move(); - }, startDrag = function (start, end, duration, element, callback) { - createEventAtPoint('mousedown', start, element); - startMove(start, end, duration, element, function () { - createEventAtPoint('mouseup', end, element); - callback(); - }); - }, center = function (el) { - var j = syn.jquery()(el), o = j.offset(); - return { - pageX: o.left + j.outerWidth() / 2, - pageY: o.top + j.outerHeight() / 2 - }; - }, convertOption = function (option, win, from) { - var page = /(\d+)[x ](\d+)/, client = /(\d+)X(\d+)/, relative = /([+-]\d+)[xX ]([+-]\d+)/, parts; - if (typeof option === 'string' && relative.test(option) && from) { - var cent = center(from); - parts = option.match(relative); - option = { - pageX: cent.pageX + parseInt(parts[1]), - pageY: cent.pageY + parseInt(parts[2]) - }; - } - if (typeof option === 'string' && page.test(option)) { - parts = option.match(page); - option = { - pageX: parseInt(parts[1]), - pageY: parseInt(parts[2]) - }; - } - if (typeof option === 'string' && client.test(option)) { - parts = option.match(client); - option = { - clientX: parseInt(parts[1]), - clientY: parseInt(parts[2]) - }; - } - if (typeof option === 'string') { - option = syn.jquery()(option, win.document)[0]; - } - if (option.nodeName) { - option = center(option); - } - if (option.pageX) { - var off = syn.helpers.scrollOffset(win); - option = { - clientX: option.pageX - off.left, - clientY: option.pageY - off.top - }; - } - return option; - }, adjust = function (from, to, win) { - if (from.clientY < 0) { - var off = syn.helpers.scrollOffset(win); - var top = off.top + from.clientY - 100, diff = top - off.top; - if (top > 0) { - } else { - top = 0; - diff = -off.top; - } - from.clientY = from.clientY - diff; - to.clientY = to.clientY - diff; - syn.helpers.scrollOffset(win, { - top: top, - left: off.left - }); - } - }; -syn.helpers.extend(syn.init.prototype, { - _move: function (from, options, callback) { - var win = syn.helpers.getWindow(from), fro = convertOption(options.from || from, win, from), to = convertOption(options.to || options, win, from); - if (options.adjust !== false) { - adjust(fro, to, win); - } - startMove(fro, to, options.duration || 500, from, callback); - }, - _drag: function (from, options, callback) { - var win = syn.helpers.getWindow(from), fro = convertOption(options.from || from, win, from), to = convertOption(options.to || options, win, from); - if (options.adjust !== false) { - adjust(fro, to, win); - } - startDrag(fro, to, options.duration || 500, from, callback); - } -}); -}); -/*syn@0.1.1#syn*/ -define('syn', function(require, exports, module) { -var syn = require('syn/synthetic'); -require('syn/mouse.support'); -require('syn/browsers'); -require('syn/key.support'); -require('syn/drag'); -window.syn = syn; -module.exports = syn; -}); From 0b6dbede8b0eb1677c2f5f780c32128ed7aa587c Mon Sep 17 00:00:00 2001 From: Henrik Ingo Date: Sun, 24 Sep 2017 13:32:49 +0300 Subject: [PATCH 3/3] Lock versions for firefox and syn node modules --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 89860be..1b377eb 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "chrome": "0.1.0", - "firefox": "*", + "firefox": "0.0.1", "jscs": "2.11.0", "jshint": "2.9.1", "karma": "0.13.22", @@ -39,6 +39,6 @@ "karma-qunit": "1.0.0", "karma-sauce-launcher": "1.0.0", "qunitjs": "2.0.0-rc1", - "syn": "*" + "syn": "0.10.0" } }