diff --git a/Jakefile b/Jakefile index 4841639f..e488d81a 100644 --- a/Jakefile +++ b/Jakefile @@ -255,6 +255,13 @@ desc(_dfit_nl(['Run tests for the distribution.', 'Requires: `karma`, `karma-mocha-reporter`.'])); task('test', ['dist-min', 'test-dist']); +// test-debug ================================================================== + +desc(_dfit_nl(['Run tests with debug for the unminified distribution.', + 'Usage: Just call {jake test}.', + 'Requires: `karma`, `karma-mocha-reporter`.'])); +task('test-debug', ['dist', 'test-dist-debug']); + // test-dist =================================================================== desc(_dfit_nl(['Test the distribution which already exists.', @@ -272,6 +279,24 @@ task('test-dist', { async: true }, function() { complete(); }); }); +// test-dist-debug ============================================================= + +desc(_dfit_nl(['Test the unminified distribution which already exists.', + 'Usage: Just call {jake test-dist-debug}.', + 'Requires: `karma`, `karma-mocha-reporter`.'])); +task('test-dist-debug', { async: true }, function() { + _print('Running tests'); + + jake.exec([ Binaries.KARMA, 'start', + _loc(Tests.Config), + '--single-run=false', + '--debug' + ].join(' '), EXEC_OPTS, + function() { _print('Tests finished successfully'); + _print(DONE_MARKER); + complete(); }); +}); + // docs ======================================================================== desc(_dfit_nl(['Generate Docco docs and compile API documentation into '+ diff --git a/doc/embedding.md b/doc/embedding.md index 98d3ca21..6bef2fb5 100644 --- a/doc/embedding.md +++ b/doc/embedding.md @@ -246,9 +246,11 @@ URL | `IFRAME`/`div` | JS Object | Default | Description `z`/`zoom` | `anm-zoom` | `zoom` | `1` | animation zoom `t`/`from` | `anm-start-from` | - | `0` | a time to start playing from (multiplier is 10ms, so `310` means _3s 100ms_) `p`/`at` | `anm-stop-at` | - | - | a time of animation where to stop at, when Player was initialized (multiplier is 10ms, so `310` means _3s 100ms_) -- | _`div`-only:_ `anm-src` | - | - | JSON for the animation to load from +- | _`div`-only:_ `anm-src` | - | - | JSON for the animation to load from +- | _`div`-only:_ `anm-importer` | - | `animatron` | Importer to use with this JSON `m`/`mode` | `anm-mode` | `mode` | - | (_deprecated_) a mode of a Player, one of: ... -`lm`/`lmode` | `anm-loading-mode` | `loadingMode` | `onplay` | `onplay` means to start loading an animation when user clicks _Play_ button (and show _thumbnail_ before), `onrequest` means to start loading animation only when the script asked for it and expect it to be completely loaded when user clicks _Play_ button +`lm`/`lmode` | `anm-loading-mode` | `loadingMode` | `rightaway` | see [section below][lmodes-pmodes] +`pm`/`pmode` | `anm-playing-mode` | `playingMode` | `onrequest` | see [section below][lmodes-pmodes] - | `anm-events` | `handleEvents` | `false` | allows animation to catch and process user mouse/keyboard events by itself (has a meaning for games or infographics) - | `anm-debug` | `debug` | `false` | show debug information like FPS and paths/bounds of objects `bg`/`bgcolor` | `anm-bg-color` | `bgColor` | `transparent` | set background color of an animation (if it is set, it can't be overriden), format is `#00ff00` @@ -263,6 +265,58 @@ URL | `IFRAME`/`div` | JS Object | Default | Description `me`/`errors` | `anm-mute-errors` | `muteErrors` | `false` | do not stop playing if some errors happened during the playing process, just log them `ver`/`version` | - | - | `latest` | override player version used to play this project +## Loading Modes and Playing Modes + +Most times, you'll only need `autoPlay` option. But in some cases you may wish to configure loading and playing precisely. + +First, some quick tips: + +* if you want scene to load and play immediately when you specified source, just set `autoPlay` to `true`, no loading / playing mode needed; +* if you want to postpone loading even when you specified animation source with HTML attribute, set `loadingMode` to `onrequest`; +* if you want loading to always automatically happen before playing (i.e. to load scene just when user pressed Play button or you called `play` method, and play it then), set `loadingMode` to `onplay`; +* if you want loading to happen in background i.e. when you specified source with HTML attribute, but player to start only when user hovers over it, set `playingMode` to `onhover`, leave `loadingMode` being default; ...only when user scrolled down to it — set `playingMode` to `wheninview`; + +Loading modes are: + +* `rightaway` _(default)_ — searches for an animation source where possible (i.e. HTML tag attribute) and, if finds it, tries to load it on player creation; if source wasn't found, waits for user to call `.load` manually as for 'onrequest'; +* `onrequest` — waits for user to manually call `.load` method; if animation source was passed i.e. through HTML tag attribute, waits for user to call `.load` method without parameters and uses this URL as a source; this allows user to completely control a moment of loading; if `.load` method was called with some values, this call cancels postponed load and overrides it; +* `onplay` — when play button was pressed or `.play` method was called, automatically starts loading a scene and plays it just after; even if scene was passed with HTML attributes, waits for `.play` call; +* `onidle` — not yet implemented; + +Playing modes are: + +* `onrequest` _(default)_ — waits for user to manually call `.play` method or press play button; +* `onhover` — starts playing animation (if loaded before) when user hovered with mouse over the player canvas; +* `wheninview` — starts playing animation (if loaded before) when at least some part of canvas appears in user's browser viewport; + + +Loading Mode | Playing Mode | `autoPlay` | HTML attr. | `forSnaphot`/manual load | Result +-------------|--------------|------------|------------|-----------|--- +`rightaway` | `onrequest` | `false` | none | yes | loads a scene from `.load` call and waits for a call to `.play` method (or play button to be pressed) +`rightaway` | `onrequest` | `true` | none | yes | loads a scene from `.load` call and immediately starts playing it +`rightaway` | `onrequest` | `false` | has | - | immediately loads a scene specified in HTML attributes and waits for a call to `.play` method (or play button to be pressed) +`rightaway` | `onrequest` | `true` | has | - | immediately loads a scene specified in HTML attribute and then starts playing it +`onrequest` | `onrequest` | `false` | none | yes | loads a scene from `.load` call and waits for a call to `.play` method (or play button to be pressed) +`onrequest` | `onrequest` | `true` | none | yes | loads a scene from `.load` call and immediately starts playing it +`onrequest` | `onrequest` | `false` | has | - | waits for user to call `.load` method w/o attributes, then loads scene (specified in HTML attributes) and waits for a call to `.play` method (or play button to be pressed) +`onrequest` | `onrequest` | `true` | has | - | waits for user to call `.load` method w/o attributes, then loads scene (specified in HTML attributes) and immediately plays it +`onplay` | `onrequest` | `false` | none | yes | do not loads the scene passed with a `.load` call, but postpones loading to a next call to `.play` method (or play button to be pressed), then loads and plays it just after that +`onplay` | `onrequest` | `true` | none | yes | do not loads the scene passed with a `.load` call, but postpones it to a call to `.play` method, but since it is called immediately, loads and plays the scene as soon as Player ready to do so +`onplay` | `onrequest` | `false` | has | - | do not loads the scene specified with HTML attributes, but postpones loading to a next call to `.play` method (or play button to be pressed), then loads and plays it just after that +`onplay` | `onrequest` | `true` | has | - | do not loads the scene specified with HTML attributes, but postpones it to a call to `.play` method, but since it is called immediately, loads and plays the scene as soon as Player ready to do so +`rightaway` | `onhover` | any | has | - | immediately loads a scene specified in HTML attributes and waits for user to move mouse over a Player to start playing +`rightaway` | `onhover` | any | none | yes | loads a scene from a `.load` call and waits for user to move mouse over a Player to start playing +`rightaway` | `wheninview` | any | has | - | immediately loads a scene specified in HTML attributes and waits for user to scroll down to a Player to start playing +`rightaway` | `wheninview` | any | none | yes | loads a scene from a `.load` call and waits for user to scroll down to a Player to start playing +`onplay` | `onhover` | any | has | - | do not loads the scene specified with HTML attributes, but postpones loading to a moment when user will move mouse over the Player, then loads and plays it just after that +`onplay` | `onhover` | any | none | yes | do not loads the scene passed with a `.load` call, but postpones loading to a moment when user will move mouse over the Player, then loads and plays it just after that +`onplay` | `wheninview` | any | has | - | do not loads the scene specified with HTML attributes, but postpones loading to a moment when user will scroll down to the Player position, then loads and plays it just after that +`onplay` | `wheninview` | any | none | yes | do not loads the scene passed with a `.load` call, but postpones loading to a moment when user will scroll down to the Player position, then loads and plays it just after that +`onrequest` | `onhover` | any | has | - | same as `onplay`/`onhover`, since `.play` method is called on mouse hover +`onrequest` | `onhover` | any | none | yes | same as `onplay`/`onhover`, since `.play` method is called on mouse hover +`onrequest` | `wheninview` | any | has | - | same as `onplay`/`wheninview`, since `.play` method is called on scroll down +`onrequest` | `wheninview` | any | none | yes | same as `onplay`/`wheninview`, since `.play` method is called on scroll down + [permanent]: https://github.com/Animatron/player/blob/docs/doc/embedding.md [iframe]: #iframe @@ -274,3 +328,4 @@ URL | `IFRAME`/`div` | JS Object | Default | Description [adding-events]: #adding-events [create-player]: #custom-scene-with-createplayer [for-snapshot]: #snapshot-with-forsnapshot +[lmodes-pmodes]: #loading-modes-and-playing-modes diff --git a/examples/configuration.js b/examples/configuration.js index b1b059a1..6c81a161 100644 --- a/examples/configuration.js +++ b/examples/configuration.js @@ -8,6 +8,20 @@ var currentMode; // embed, config, publish, html var shortVersion = true; +var loadingModes = [ + { value: 'rightaway', name: 'right away', description: 'loads animation just immediately when finds it\'s source (i.e. from HTML attribute)' }, + { value: 'onrequest', name: 'on request', description: 'waits for user to manually call .load() method' }, + { value: 'onplay', name: 'on play', description: 'when play button was pressed, starts loading a scene and plays it just after' }/*, + { value: 'onidle', name: 'on idle', description: ' waits for pause in user actions (mouse move, clicks, keyboard) to load the animation' },*/ +]; + +var playingModes = [ + { value: 'onrequest', name: 'on request', description: 'same to autoPlay: false, waits for user to manually call .play() method' }, + { value: 'rightaway', name: 'right away', description: 'same to autoPlay: true' }, + { value: 'onhover', name: 'on hover', description: 'starts playing animation when user hovered with mouse over the player canvas' }, + { value: 'wheninview', name: 'when in view', description: 'starts playing animation when Player appeares in browser viewport' } +]; + function getElm(id) { return document.getElementById(id); } function collectOptions() { @@ -25,6 +39,7 @@ function collectOptions() { if (!getElm('opts-bg-color').disabled) options.bgColor = getElm('opts-bg-color').value; if (!getElm('opts-ribbons').disabled) options.ribbonsColor = getElm('opts-ribbons').value; if (!getElm('opts-loading').disabled) options.loadingMode = getElm('opts-loading').selectedIndex; + if (!getElm('opts-playing').disabled) options.playingMode = getElm('opts-playing').selectedIndex; if (!getElm('opts-thumbnail').disabled) options.thumbnail = getElm('opts-thumbnail').value; //if (!getElm('opts-images').disabled) options.imagesEnabled = getElm('opts-images').checked; if (!getElm('opts-audio').disabled) options.audioEnabled = getElm('opts-audio').checked; @@ -134,12 +149,30 @@ function init() { 'loading': { label: 'Loading', type: 'select', create: function() { var select = document.createElement('select'); - var onPlay = document.createElement('option'); - onPlay.innerText = onPlay.textContent = 'on play'; - var onRequest = document.createElement('option'); - onRequest.innerText = onRequest.textContent = 'on request'; - select.appendChild(onPlay); - select.appendChild(onRequest); + for (var i = 0, il = loadingModes.length, mode; i < il; i++) { + var option = document.createElement('option'); + option.innerText = option.textContent = loadingModes[i].name; + select.appendChild(option); + } + select.setAttribute('title', loadingModes[0].description); + select.addEventListener('change', function() { + select.setAttribute('title', loadingModes[select.selectedIndex].description); + }); + return select; + }, + modify: function(elm, form) { elm.selectedIndex = 0; } }, + 'playing': { label: 'Playing', type: 'select', + create: function() { + var select = document.createElement('select'); + for (var i = 0, il = playingModes.length, mode; i < il; i++) { + var option = document.createElement('option'); + option.innerText = option.textContent = playingModes[i].name; + select.appendChild(option); + } + select.setAttribute('title', playingModes[0].description); + select.addEventListener('change', function() { + select.setAttribute('title', playingModes[select.selectedIndex].description); + }); return select; }, modify: function(elm, form) { elm.selectedIndex = 0; } }, @@ -195,7 +228,8 @@ var optionsMapper = function(mode, options) { function numberOption(v) { return v; }; function colorOption(v) { return (v.indexOf('#') >= 0) ? v.slice(1) : v; }; function booleanOption(v) { return v ? '1' : '0'; }; - function loadingModeOption(v) { return (v === 1) ? 'onrequest' : 'onplay' }; + function loadingModeOption(v) { return loadingModes[v].value }; + function playingModeOption(v) { return playingModes[v].value }; return { width: extractOption('width', 'w', 'width', numberOption), @@ -209,6 +243,7 @@ var optionsMapper = function(mode, options) { startFrom: extractOption('startFrom', 't', 'from', parseTime), stopAt: extractOption('stopAt', 'p', 'at', parseTime), loadingMode: extractOption('loadingMode', 'lm', 'lmode', loadingModeOption), + playingMode: extractOption('playingMode', 'pm', 'pmode', playingModeOption), bgColor: extractOption('bgColor', 'bg', 'bgcolor', colorOption), ribbonsColor: extractOption('ribbonsColor', 'rc', 'ribcolor', colorOption), audioEnabled: extractOption('audioEnabled', 's', 'audio', booleanOption), @@ -227,7 +262,8 @@ var optionsMapper = function(mode, options) { function textOption(v) { return '\'' + v + '\''; }; function colorOption(v) { return '\'' + v + '\''; }; function booleanOption(v) { return v ? 'true' : 'false'; }; - function loadingModeOption(v) { return (v === 1) ? '\'onrequest\'' : '\'onplay\'' }; + function loadingModeOption(v) { return '\'' + loadingModes[v].value + '\''; }; + function playingModeOption(v) { return '\'' + playingModes[v].value + '\''; }; function thumbnailOption(v) { return v; }; return { @@ -240,6 +276,7 @@ var optionsMapper = function(mode, options) { speed: extractOption('speed', numberOption), zoom: extractOption('zoom', numberOption), loadingMode: extractOption('loadingMode', loadingModeOption), + playingMode: extractOption('playingMode', playingModeOption), bgColor: extractOption('bgColor', colorOption), ribbonsColor: extractOption('ribbonsColor', colorOption), thumbnail: extractOption('thumbnail', textOption), @@ -262,7 +299,8 @@ var optionsMapper = function(mode, options) { function textOption(v) { return v; }; function numberOption(v) { return v; }; function booleanOption(v) { return v ? 'true' : 'false'; }; - function loadingModeOption(v) { return (v === 1) ? 'onrequest' : 'onplay' }; + function loadingModeOption(v) { return loadingModes[v].value }; + function playingModeOption(v) { return playingModes[v].value }; return { width: extractOption('width', 'anm-width', numberOption), @@ -276,6 +314,7 @@ var optionsMapper = function(mode, options) { startFrom: extractOption('startFrom', 'anm-start-from', parseTime), stopAt: extractOption('stopAt', 'anm-stop-at', parseTime), loadingMode: extractOption('loadingMode', 'anm-loading-mode', loadingModeOption), + playingMode: extractOption('playingMode', 'anm-playing-mode', playingModeOption), bgColor: extractOption('bgColor', 'anm-bg-color', colorOption), ribbonsColor: extractOption('ribbonsColor', 'anm-rib-color', colorOption), thumbnail: extractOption('thumbnail', 'anm-thumbnail', textOption), diff --git a/package.json b/package.json index 228f70bd..78e14d32 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "jasmine-core": "^2.3.4", "karma": "^0.12.37", "karma-chrome-launcher": "^0.2.0", - "karma-mocha-reporter": "^1.0.2", - "karma-jasmine": "^0.3.6" + "karma-jasmine": "^0.3.6", + "karma-jasmine-ajax": "^0.1.13", + "karma-mocha-reporter": "^1.0.2" } } diff --git a/spec/Runner.html b/spec/Runner.html index 5e0d2231..ef66ddac 100644 --- a/spec/Runner.html +++ b/spec/Runner.html @@ -2,14 +2,16 @@ - Jasmine Spec Runner v2.2.0 + Jasmine Spec Runner v2.3.4 - - + + - - - + + + + + @@ -19,6 +21,7 @@ + diff --git a/spec/configuration.spec.js b/spec/configuration.spec.js new file mode 100644 index 00000000..e69de29b diff --git a/spec/karma.conf.js b/spec/karma.conf.js index 4f318fc4..d5684ffd 100644 --- a/spec/karma.conf.js +++ b/spec/karma.conf.js @@ -3,6 +3,12 @@ module.exports = function(config) { + function isDebug() { + return process.argv.some(function(argument) { + argument === '--debug'; + }); + } + var options = { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -11,16 +17,17 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], + frameworks: ['jasmine-ajax', 'jasmine'], // list of files / patterns to load in the browser files: [ - './dist/bundle/animatron.min.js', + isDebug ? './dist/bundle/animatron.js' : './dist/bundle/animatron.min.js', './spec/search.spec.js', './spec/mouse.spec.js', - './spec/time.spec.js' + './spec/time.spec.js', + './spec/loading-modes.spec.js' ], diff --git a/spec/lib/jasmine-2.2.0/MIT.LICENSE b/spec/lib/jasmine-2.3.4/MIT.LICENSE similarity index 100% rename from spec/lib/jasmine-2.2.0/MIT.LICENSE rename to spec/lib/jasmine-2.3.4/MIT.LICENSE diff --git a/spec/lib/jasmine-2.2.0/boot.js b/spec/lib/jasmine-2.3.4/boot.js old mode 100644 new mode 100755 similarity index 89% rename from spec/lib/jasmine-2.2.0/boot.js rename to spec/lib/jasmine-2.3.4/boot.js index e8ddd551..04ed64c1 --- a/spec/lib/jasmine-2.2.0/boot.js +++ b/spec/lib/jasmine-2.3.4/boot.js @@ -35,13 +35,9 @@ var jasmineInterface = jasmineRequire.interface(jasmine, env); /** - * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. + * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. */ - if (typeof window == "undefined" && typeof exports == "object") { - extend(exports, jasmineInterface); - } else { - extend(window, jasmineInterface); - } + extend(window, jasmineInterface); /** * ## Runner Parameters @@ -56,6 +52,9 @@ var catchingExceptions = queryString.getParam("catch"); env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); + var throwingExpectationFailures = queryString.getParam("throwFailures"); + env.throwOnExpectationFailure(throwingExpectationFailures); + /** * ## Reporters * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). @@ -63,6 +62,7 @@ var htmlReporter = new jasmine.HtmlReporter({ env: env, onRaiseExceptionsClick: function() { queryString.navigateWithNewParam("catch", !env.catchingExceptions()); }, + onThrowExpectationsClick: function() { queryString.navigateWithNewParam("throwFailures", !env.throwingExpectationFailures()); }, addToExistingQueryString: function(key, value) { return queryString.fullStringWithNewParam(key, value); }, getContainer: function() { return document.body; }, createElement: function() { return document.createElement.apply(document, arguments); }, diff --git a/spec/lib/jasmine-2.2.0/console.js b/spec/lib/jasmine-2.3.4/console.js old mode 100644 new mode 100755 similarity index 100% rename from spec/lib/jasmine-2.2.0/console.js rename to spec/lib/jasmine-2.3.4/console.js diff --git a/spec/lib/jasmine-2.2.0/jasmine-html.js b/spec/lib/jasmine-2.3.4/jasmine-html.js old mode 100644 new mode 100755 similarity index 87% rename from spec/lib/jasmine-2.2.0/jasmine-html.js rename to spec/lib/jasmine-2.3.4/jasmine-html.js index bee5a04f..259f45ce --- a/spec/lib/jasmine-2.2.0/jasmine-html.js +++ b/spec/lib/jasmine-2.3.4/jasmine-html.js @@ -40,6 +40,7 @@ jasmineRequire.HtmlReporter = function(j$) { createElement = options.createElement, createTextNode = options.createTextNode, onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, + onThrowExpectationsClick = options.onThrowExpectationsClick || function() {}, addToExistingQueryString = options.addToExistingQueryString || defaultQueryString, timer = options.timer || noopTimer, results = [], @@ -145,22 +146,51 @@ jasmineRequire.HtmlReporter = function(j$) { this.jasmineDone = function() { var banner = find('.banner'); - banner.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); - var alert = find('.alert'); + alert.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); + + banner.appendChild( + createDom('div', { className: 'run-options' }, + createDom('span', { className: 'trigger' }, 'Options'), + createDom('div', { className: 'payload' }, + createDom('div', { className: 'exceptions' }, + createDom('input', { + className: 'raise', + id: 'raise-exceptions', + type: 'checkbox' + }), + createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions')), + createDom('div', { className: 'throw-failures' }, + createDom('input', { + className: 'throw', + id: 'throw-failures', + type: 'checkbox' + }), + createDom('label', { className: 'label', 'for': 'throw-failures' }, 'stop spec on expectation failure')) + ) + )); - alert.appendChild(createDom('span', { className: 'exceptions' }, - createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions'), - createDom('input', { - className: 'raise', - id: 'raise-exceptions', - type: 'checkbox' - }) - )); - var checkbox = find('#raise-exceptions'); + var raiseCheckbox = find('#raise-exceptions'); + + raiseCheckbox.checked = !env.catchingExceptions(); + raiseCheckbox.onclick = onRaiseExceptionsClick; - checkbox.checked = !env.catchingExceptions(); - checkbox.onclick = onRaiseExceptionsClick; + var throwCheckbox = find('#throw-failures'); + throwCheckbox.checked = env.throwingExpectationFailures(); + throwCheckbox.onclick = onThrowExpectationsClick; + + var optionsMenu = find('.run-options'), + optionsTrigger = optionsMenu.querySelector('.trigger'), + optionsPayload = optionsMenu.querySelector('.payload'), + isOpen = /\bopen\b/; + + optionsTrigger.onclick = function() { + if (isOpen.test(optionsPayload.className)) { + optionsPayload.className = optionsPayload.className.replace(isOpen, ''); + } else { + optionsPayload.className += ' open'; + } + }; if (specsExecuted < totalSpecsDefined) { var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; diff --git a/spec/lib/jasmine-2.2.0/jasmine.css b/spec/lib/jasmine-2.3.4/jasmine.css old mode 100644 new mode 100755 similarity index 95% rename from spec/lib/jasmine-2.2.0/jasmine.css rename to spec/lib/jasmine-2.3.4/jasmine.css index ecc5f5e7..f9f4ae90 --- a/spec/lib/jasmine-2.2.0/jasmine.css +++ b/spec/lib/jasmine-2.3.4/jasmine.css @@ -8,11 +8,10 @@ body { overflow-y: scroll; } .jasmine_html-reporter .banner { position: relative; } .jasmine_html-reporter .banner .title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } .jasmine_html-reporter .banner .version { margin-left: 14px; position: relative; top: 6px; } -.jasmine_html-reporter .banner .duration { position: absolute; right: 14px; top: 6px; } .jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } .jasmine_html-reporter .version { color: #aaa; } .jasmine_html-reporter .banner { margin-top: 14px; } -.jasmine_html-reporter .duration { color: #aaa; float: right; } +.jasmine_html-reporter .duration { color: #fff; float: right; line-height: 28px; padding-right: 9px; } .jasmine_html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } .jasmine_html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } .jasmine_html-reporter .symbol-summary li.passed { font-size: 14px; } @@ -25,7 +24,10 @@ body { overflow-y: scroll; } .jasmine_html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } .jasmine_html-reporter .symbol-summary li.empty { font-size: 14px; } .jasmine_html-reporter .symbol-summary li.empty:before { color: #ba9d37; content: "\02022"; } -.jasmine_html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } +.jasmine_html-reporter .run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; } +.jasmine_html-reporter .run-options .trigger { cursor: pointer; padding: 8px 16px; } +.jasmine_html-reporter .run-options .payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; } +.jasmine_html-reporter .run-options .payload.open { display: block; } .jasmine_html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } .jasmine_html-reporter .bar.failed { background-color: #ca3a11; } .jasmine_html-reporter .bar.passed { background-color: #007069; } @@ -36,14 +38,7 @@ body { overflow-y: scroll; } .jasmine_html-reporter .bar a { color: white; } .jasmine_html-reporter.spec-list .bar.menu.failure-list, .jasmine_html-reporter.spec-list .results .failures { display: none; } .jasmine_html-reporter.failure-list .bar.menu.spec-list, .jasmine_html-reporter.failure-list .summary { display: none; } -.jasmine_html-reporter .running-alert { background-color: #666; } .jasmine_html-reporter .results { margin-top: 14px; } -.jasmine_html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } -.jasmine_html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } -.jasmine_html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } -.jasmine_html-reporter.showDetails .summary { display: none; } -.jasmine_html-reporter.showDetails #details { display: block; } -.jasmine_html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } .jasmine_html-reporter .summary { margin-top: 14px; } .jasmine_html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } .jasmine_html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } @@ -51,6 +46,7 @@ body { overflow-y: scroll; } .jasmine_html-reporter .summary li.failed a { color: #ca3a11; } .jasmine_html-reporter .summary li.empty a { color: #ba9d37; } .jasmine_html-reporter .summary li.pending a { color: #ba9d37; } +.jasmine_html-reporter .summary li.disabled a { color: #bababa; } .jasmine_html-reporter .description + .suite { margin-top: 0; } .jasmine_html-reporter .suite { margin-top: 14px; } .jasmine_html-reporter .suite a { color: #333; } diff --git a/spec/lib/jasmine-2.2.0/jasmine.js b/spec/lib/jasmine-2.3.4/jasmine.js old mode 100644 new mode 100755 similarity index 86% rename from spec/lib/jasmine-2.2.0/jasmine.js rename to spec/lib/jasmine-2.3.4/jasmine.js index 6bf3f02a..312d591e --- a/spec/lib/jasmine-2.2.0/jasmine.js +++ b/spec/lib/jasmine-2.3.4/jasmine.js @@ -42,7 +42,8 @@ var getJasmineRequireObj = (function (jasmineGlobal) { jRequire.base(j$, jasmineGlobal); j$.util = jRequire.util(); - j$.Any = jRequire.Any(); + j$.errors = jRequire.errors(); + j$.Any = jRequire.Any(j$); j$.Anything = jRequire.Anything(j$); j$.CallTracker = jRequire.CallTracker(); j$.MockDate = jRequire.MockDate(); @@ -63,8 +64,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.SpyRegistry = jRequire.SpyRegistry(j$); j$.SpyStrategy = jRequire.SpyStrategy(); j$.StringMatching = jRequire.StringMatching(j$); - j$.Suite = jRequire.Suite(); + j$.Suite = jRequire.Suite(j$); j$.Timer = jRequire.Timer(); + j$.TreeProcessor = jRequire.TreeProcessor(); j$.version = jRequire.version(); j$.matchers = jRequire.requireMatchers(jRequire, j$); @@ -302,6 +304,7 @@ getJasmineRequireObj().Spec = function(j$) { this.expectationResultFactory = attrs.expectationResultFactory || function() { }; this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; this.catchingExceptions = attrs.catchingExceptions || function() { return true; }; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; if (!this.queueableFn.fn) { this.pend(); @@ -317,12 +320,16 @@ getJasmineRequireObj().Spec = function(j$) { }; } - Spec.prototype.addExpectationResult = function(passed, data) { + Spec.prototype.addExpectationResult = function(passed, data, isError) { var expectationResult = this.expectationResultFactory(data); if (passed) { this.result.passedExpectations.push(expectationResult); } else { this.result.failedExpectations.push(expectationResult); + + if (this.throwOnExpectationFailure && !isError) { + throw new j$.errors.ExpectationFailed(); + } } }; @@ -330,13 +337,13 @@ getJasmineRequireObj().Spec = function(j$) { return this.expectationFactory(actual, this); }; - Spec.prototype.execute = function(onComplete) { + Spec.prototype.execute = function(onComplete, enabled) { var self = this; this.onStart(this); - if (this.markedPending || this.disabled) { - complete(); + if (!this.isExecutable() || this.markedPending || enabled === false) { + complete(enabled); return; } @@ -350,8 +357,8 @@ getJasmineRequireObj().Spec = function(j$) { userContext: this.userContext() }); - function complete() { - self.result.status = self.status(); + function complete(enabledAgain) { + self.result.status = self.status(enabledAgain); self.resultCallback(self.result); if (onComplete) { @@ -366,13 +373,17 @@ getJasmineRequireObj().Spec = function(j$) { return; } + if (e instanceof j$.errors.ExpectationFailed) { + return; + } + this.addExpectationResult(false, { matcherName: '', passed: false, expected: '', actual: '', error: e - }); + }, true); }; Spec.prototype.disable = function() { @@ -386,8 +397,13 @@ getJasmineRequireObj().Spec = function(j$) { } }; - Spec.prototype.status = function() { - if (this.disabled) { + Spec.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; + }; + + Spec.prototype.status = function(enabled) { + if (this.disabled || enabled === false) { return 'disabled'; } @@ -403,7 +419,7 @@ getJasmineRequireObj().Spec = function(j$) { }; Spec.prototype.isExecutable = function() { - return !this.disabled && !this.markedPending; + return !this.disabled; }; Spec.prototype.getFullName = function() { @@ -444,7 +460,7 @@ getJasmineRequireObj().Env = function(j$) { var realSetTimeout = j$.getGlobal().setTimeout; var realClearTimeout = j$.getGlobal().clearTimeout; - this.clock = new j$.Clock(global, new j$.DelayedFunctionScheduler(), new j$.MockDate(global)); + this.clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); var runnableLookupTable = {}; var runnableResources = {}; @@ -452,6 +468,7 @@ getJasmineRequireObj().Env = function(j$) { var currentSpec = null; var currentlyExecutingSuites = []; var currentDeclarationSuite = null; + var throwOnExpectationFailure = false; var currentSuite = function() { return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; @@ -533,27 +550,21 @@ getJasmineRequireObj().Env = function(j$) { delete runnableResources[id]; }; - var beforeAndAfterFns = function(suite, runnablesExplictlySet) { + var beforeAndAfterFns = function(suite) { return function() { var befores = [], - afters = [], - beforeAlls = [], - afterAlls = []; + afters = []; while(suite) { befores = befores.concat(suite.beforeFns); afters = afters.concat(suite.afterFns); - if (runnablesExplictlySet()) { - beforeAlls = beforeAlls.concat(suite.beforeAllFns); - afterAlls = afterAlls.concat(suite.afterAllFns); - } - suite = suite.parentSuite; } + return { - befores: beforeAlls.reverse().concat(befores.reverse()), - afters: afters.concat(afterAlls) + befores: befores.reverse(), + afters: afters }; }; }; @@ -599,10 +610,18 @@ getJasmineRequireObj().Env = function(j$) { return j$.Spec.isPendingSpecException(e) || catchExceptions; }; + this.throwOnExpectationFailure = function(value) { + throwOnExpectationFailure = !!value; + }; + + this.throwingExpectationFailures = function() { + return throwOnExpectationFailure; + }; + var queueRunnerFactory = function(options) { options.catchException = catchException; options.clearStack = options.clearStack || clearStack; - options.timer = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; + options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; options.fail = self.fail; new j$.QueueRunner(options).execute(); @@ -623,26 +642,40 @@ getJasmineRequireObj().Env = function(j$) { }; this.execute = function(runnablesToRun) { - if(runnablesToRun) { - runnablesExplictlySet = true; - } else if (focusedRunnables.length) { - runnablesExplictlySet = true; - runnablesToRun = focusedRunnables; - } else { - runnablesToRun = [topSuite.id]; + if(!runnablesToRun) { + if (focusedRunnables.length) { + runnablesToRun = focusedRunnables; + } else { + runnablesToRun = [topSuite.id]; + } } + var processor = new j$.TreeProcessor({ + tree: topSuite, + runnableIds: runnablesToRun, + queueRunnerFactory: queueRunnerFactory, + nodeStart: function(suite) { + currentlyExecutingSuites.push(suite); + defaultResourcesForRunnable(suite.id, suite.parentSuite.id); + reporter.suiteStarted(suite.result); + }, + nodeComplete: function(suite, result) { + if (!suite.disabled) { + clearResourcesForRunnable(suite.id); + } + currentlyExecutingSuites.pop(); + reporter.suiteDone(result); + } + }); - var allFns = []; - for(var i = 0; i < runnablesToRun.length; i++) { - var runnable = runnableLookupTable[runnablesToRun[i]]; - allFns.push((function(runnable) { return { fn: function(done) { runnable.execute(done); } }; })(runnable)); + if(!processor.processTree().valid) { + throw new Error('Invalid order: would cause a beforeAll or afterAll to be run multiple times'); } reporter.jasmineStarted({ totalSpecsDefined: totalSpecsDefined }); - queueRunnerFactory({queueableFns: allFns, onComplete: reporter.jasmineDone}); + processor.execute(reporter.jasmineDone); }; this.addReporter = function(reporterToAdd) { @@ -666,28 +699,13 @@ getJasmineRequireObj().Env = function(j$) { id: getNextSuiteId(), description: description, parentSuite: currentDeclarationSuite, - queueRunner: queueRunnerFactory, - onStart: suiteStarted, expectationFactory: expectationFactory, expectationResultFactory: expectationResultFactory, - runnablesExplictlySetGetter: runnablesExplictlySetGetter, - resultCallback: function(attrs) { - if (!suite.disabled) { - clearResourcesForRunnable(suite.id); - } - currentlyExecutingSuites.pop(); - reporter.suiteDone(attrs); - } + throwOnExpectationFailure: throwOnExpectationFailure }); runnableLookupTable[suite.id] = suite; return suite; - - function suiteStarted(suite) { - currentlyExecutingSuites.push(suite); - defaultResourcesForRunnable(suite.id, suite.parentSuite.id); - reporter.suiteStarted(suite.result); - } }; this.describe = function(description, specDefinitions) { @@ -759,17 +777,11 @@ getJasmineRequireObj().Env = function(j$) { } } - var runnablesExplictlySet = false; - - var runnablesExplictlySetGetter = function(){ - return runnablesExplictlySet; - }; - var specFactory = function(description, fn, suite, timeout) { totalSpecsDefined++; var spec = new j$.Spec({ id: getNextSpecId(), - beforeAndAfterFns: beforeAndAfterFns(suite, runnablesExplictlySetGetter), + beforeAndAfterFns: beforeAndAfterFns(suite), expectationFactory: expectationFactory, resultCallback: specResultCallback, getSpecName: function(spec) { @@ -783,7 +795,8 @@ getJasmineRequireObj().Env = function(j$) { queueableFn: { fn: fn, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } - } + }, + throwOnExpectationFailure: throwOnExpectationFailure }); runnableLookupTable[spec.id] = spec; @@ -1023,7 +1036,7 @@ getJasmineRequireObj().CallTracker = function() { }; getJasmineRequireObj().Clock = function() { - function Clock(global, delayedFunctionScheduler, mockDate) { + function Clock(global, delayedFunctionSchedulerFactory, mockDate) { var self = this, realTimingFunctions = { setTimeout: global.setTimeout, @@ -1038,19 +1051,24 @@ getJasmineRequireObj().Clock = function() { clearInterval: clearInterval }, installed = false, + delayedFunctionScheduler, timer; self.install = function() { + if(!originalTimingFunctionsIntact()) { + throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); + } replace(global, fakeTimingFunctions); timer = fakeTimingFunctions; + delayedFunctionScheduler = delayedFunctionSchedulerFactory(); installed = true; return self; }; self.uninstall = function() { - delayedFunctionScheduler.reset(); + delayedFunctionScheduler = null; mockDate.uninstall(); replace(global, realTimingFunctions); @@ -1058,6 +1076,15 @@ getJasmineRequireObj().Clock = function() { installed = false; }; + self.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; + self.mockDate = function(initialDate) { mockDate.install(initialDate); }; @@ -1101,6 +1128,13 @@ getJasmineRequireObj().Clock = function() { return self; + function originalTimingFunctionsIntact() { + return global.setTimeout === realTimingFunctions.setTimeout && + global.clearTimeout === realTimingFunctions.clearTimeout && + global.setInterval === realTimingFunctions.setInterval && + global.clearInterval === realTimingFunctions.clearInterval; + } + function legacyIE() { //if these methods are polyfilled, apply will be present return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; @@ -1210,13 +1244,6 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { } }; - self.reset = function() { - currentTime = 0; - scheduledLookup = []; - scheduledFunctions = {}; - delayedFnCount = 0; - }; - return self; function indexOfFirstToPass(array, testFn) { @@ -1642,6 +1669,23 @@ getJasmineRequireObj().pp = function(j$) { if(array.length > length){ this.append(', ...'); } + + var self = this; + var first = array.length === 0; + this.iterateObject(array, function(property, isGetter) { + if (property.match(/^\d+$/)) { + return; + } + + if (first) { + first = false; + } else { + self.append(', '); + } + + self.formatProperty(array, property, isGetter); + }); + this.append(' ]'); }; @@ -1664,18 +1708,22 @@ getJasmineRequireObj().pp = function(j$) { self.append(', '); } - self.append(property); - self.append(': '); - if (isGetter) { - self.append(''); - } else { - self.format(obj[property]); - } + self.formatProperty(obj, property, isGetter); }); this.append(' })'); }; + StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) { + this.append(property); + this.append(': '); + if (isGetter) { + this.append(''); + } else { + this.format(obj[property]); + } + }; + StringPrettyPrinter.prototype.append = function(value) { this.string += value; }; @@ -1706,7 +1754,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { this.onException = attrs.onException || function() {}; this.catchException = attrs.catchException || function() { return true; }; this.userContext = attrs.userContext || {}; - this.timer = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; + this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; this.fail = attrs.fail || function() {}; } @@ -1746,7 +1794,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { function attemptAsync(queueableFn) { var clearTimeout = function () { - Function.prototype.apply.apply(self.timer.clearTimeout, [j$.getGlobal(), [timeoutId]]); + Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); }, next = once(function () { clearTimeout(timeoutId); @@ -1760,7 +1808,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { }; if (queueableFn.timeout) { - timeoutId = Function.prototype.apply.apply(self.timer.setTimeout, [j$.getGlobal(), [function() { + timeoutId = Function.prototype.apply.apply(self.timeout.setTimeout, [j$.getGlobal(), [function() { var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); onException(error, queueableFn); next(); @@ -1938,24 +1986,20 @@ getJasmineRequireObj().SpyStrategy = function() { return SpyStrategy; }; -getJasmineRequireObj().Suite = function() { +getJasmineRequireObj().Suite = function(j$) { function Suite(attrs) { this.env = attrs.env; this.id = attrs.id; this.parentSuite = attrs.parentSuite; this.description = attrs.description; - this.onStart = attrs.onStart || function() {}; - this.resultCallback = attrs.resultCallback || function() {}; - this.clearStack = attrs.clearStack || function(fn) {fn();}; this.expectationFactory = attrs.expectationFactory; this.expectationResultFactory = attrs.expectationResultFactory; - this.runnablesExplictlySetGetter = attrs.runnablesExplictlySetGetter || function() {}; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; this.beforeFns = []; this.afterFns = []; this.beforeAllFns = []; this.afterAllFns = []; - this.queueRunner = attrs.queueRunner || function() {}; this.disabled = false; this.children = []; @@ -2018,51 +2062,17 @@ getJasmineRequireObj().Suite = function() { } }; - Suite.prototype.execute = function(onComplete) { - var self = this; - - this.onStart(this); - - if (this.disabled) { - complete(); - return; - } - - var allFns = []; - - for (var i = 0; i < this.children.length; i++) { - allFns.push(wrapChildAsAsync(this.children[i])); - } - - if (this.isExecutable()) { - allFns = this.beforeAllFns.concat(allFns); - allFns = allFns.concat(this.afterAllFns); - } - - this.queueRunner({ - queueableFns: allFns, - onComplete: complete, - userContext: this.sharedUserContext(), - onException: function() { self.onException.apply(self, arguments); } - }); - - function complete() { - self.result.status = self.status(); - self.resultCallback(self.result); - - if (onComplete) { - onComplete(); - } - } + Suite.prototype.isExecutable = function() { + return !this.disabled; + }; - function wrapChildAsAsync(child) { - return { fn: function(done) { child.execute(done); } }; - } + Suite.prototype.canBeReentered = function() { + return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0; }; - Suite.prototype.isExecutable = function() { - var runnablesExplicitlySet = this.runnablesExplictlySetGetter(); - return !runnablesExplicitlySet && hasExecutableChild(this.children); + Suite.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; }; Suite.prototype.sharedUserContext = function() { @@ -2078,6 +2088,10 @@ getJasmineRequireObj().Suite = function() { }; Suite.prototype.onException = function() { + if (arguments[0] instanceof j$.errors.ExpectationFailed) { + return; + } + if(isAfterAll(this.children)) { var data = { matcherName: '', @@ -2099,10 +2113,17 @@ getJasmineRequireObj().Suite = function() { if(isAfterAll(this.children) && isFailure(arguments)){ var data = arguments[1]; this.result.failedExpectations.push(this.expectationResultFactory(data)); + if(this.throwOnExpectationFailure) { + throw new j$.errors.ExpectationFailed(); + } } else { for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; - child.addExpectationResult.apply(child, arguments); + try { + child.addExpectationResult.apply(child, arguments); + } catch(e) { + // keep going + } } } }; @@ -2115,17 +2136,6 @@ getJasmineRequireObj().Suite = function() { return !args[0]; } - function hasExecutableChild(children) { - var foundActive = false; - for (var i = 0; i < children.length; i++) { - if (children[i].isExecutable()) { - foundActive = true; - break; - } - } - return foundActive; - } - function clone(obj) { var clonedObj = {}; for (var prop in obj) { @@ -2167,7 +2177,211 @@ getJasmineRequireObj().Timer = function() { return Timer; }; -getJasmineRequireObj().Any = function() { +getJasmineRequireObj().TreeProcessor = function() { + function TreeProcessor(attrs) { + var tree = attrs.tree, + runnableIds = attrs.runnableIds, + queueRunnerFactory = attrs.queueRunnerFactory, + nodeStart = attrs.nodeStart || function() {}, + nodeComplete = attrs.nodeComplete || function() {}, + stats = { valid: true }, + processed = false, + defaultMin = Infinity, + defaultMax = 1 - Infinity; + + this.processTree = function() { + processNode(tree, false); + processed = true; + return stats; + }; + + this.execute = function(done) { + if (!processed) { + this.processTree(); + } + + if (!stats.valid) { + throw 'invalid order'; + } + + var childFns = wrapChildren(tree, 0); + + queueRunnerFactory({ + queueableFns: childFns, + userContext: tree.sharedUserContext(), + onException: function() { + tree.onException.apply(tree, arguments); + }, + onComplete: done + }); + }; + + function runnableIndex(id) { + for (var i = 0; i < runnableIds.length; i++) { + if (runnableIds[i] === id) { + return i; + } + } + } + + function processNode(node, parentEnabled) { + var executableIndex = runnableIndex(node.id); + + if (executableIndex !== undefined) { + parentEnabled = true; + } + + parentEnabled = parentEnabled && node.isExecutable(); + + if (!node.children) { + stats[node.id] = { + executable: parentEnabled && node.isExecutable(), + segments: [{ + index: 0, + owner: node, + nodes: [node], + min: startingMin(executableIndex), + max: startingMax(executableIndex) + }] + }; + } else { + var hasExecutableChild = false; + + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i]; + + processNode(child, parentEnabled); + + if (!stats.valid) { + return; + } + + var childStats = stats[child.id]; + + hasExecutableChild = hasExecutableChild || childStats.executable; + } + + stats[node.id] = { + executable: hasExecutableChild + }; + + segmentChildren(node, stats[node.id], executableIndex); + + if (!node.canBeReentered() && stats[node.id].segments.length > 1) { + stats = { valid: false }; + } + } + } + + function startingMin(executableIndex) { + return executableIndex === undefined ? defaultMin : executableIndex; + } + + function startingMax(executableIndex) { + return executableIndex === undefined ? defaultMax : executableIndex; + } + + function segmentChildren(node, nodeStats, executableIndex) { + var currentSegment = { index: 0, owner: node, nodes: [], min: startingMin(executableIndex), max: startingMax(executableIndex) }, + result = [currentSegment], + lastMax = defaultMax, + orderedChildSegments = orderChildSegments(node.children); + + function isSegmentBoundary(minIndex) { + return lastMax !== defaultMax && minIndex !== defaultMin && lastMax < minIndex - 1; + } + + for (var i = 0; i < orderedChildSegments.length; i++) { + var childSegment = orderedChildSegments[i], + maxIndex = childSegment.max, + minIndex = childSegment.min; + + if (isSegmentBoundary(minIndex)) { + currentSegment = {index: result.length, owner: node, nodes: [], min: defaultMin, max: defaultMax}; + result.push(currentSegment); + } + + currentSegment.nodes.push(childSegment); + currentSegment.min = Math.min(currentSegment.min, minIndex); + currentSegment.max = Math.max(currentSegment.max, maxIndex); + lastMax = maxIndex; + } + + nodeStats.segments = result; + } + + function orderChildSegments(children) { + var specifiedOrder = [], + unspecifiedOrder = []; + + for (var i = 0; i < children.length; i++) { + var child = children[i], + segments = stats[child.id].segments; + + for (var j = 0; j < segments.length; j++) { + var seg = segments[j]; + + if (seg.min === defaultMin) { + unspecifiedOrder.push(seg); + } else { + specifiedOrder.push(seg); + } + } + } + + specifiedOrder.sort(function(a, b) { + return a.min - b.min; + }); + + return specifiedOrder.concat(unspecifiedOrder); + } + + function executeNode(node, segmentNumber) { + if (node.children) { + return { + fn: function(done) { + nodeStart(node); + + queueRunnerFactory({ + onComplete: function() { + nodeComplete(node, node.getResult()); + done(); + }, + queueableFns: wrapChildren(node, segmentNumber), + userContext: node.sharedUserContext(), + onException: function() { + node.onException.apply(node, arguments); + } + }); + } + }; + } else { + return { + fn: function(done) { node.execute(done, stats[node.id].executable); } + }; + } + } + + function wrapChildren(node, segmentNumber) { + var result = [], + segmentChildren = stats[node.id].segments[segmentNumber].nodes; + + for (var i = 0; i < segmentChildren.length; i++) { + result.push(executeNode(segmentChildren[i].owner, segmentChildren[i].index)); + } + + if (!stats[node.id].executable) { + return result; + } + + return node.beforeAllFns.concat(result).concat(node.afterAllFns); + } + } + + return TreeProcessor; +}; + +getJasmineRequireObj().Any = function(j$) { function Any(expectedObject) { this.expectedObject = expectedObject; @@ -2198,7 +2412,7 @@ getJasmineRequireObj().Any = function() { }; Any.prototype.jasmineToString = function() { - return ''; + return ''; }; return Any; @@ -2251,11 +2465,35 @@ getJasmineRequireObj().ObjectContaining = function(j$) { this.sample = sample; } + function getPrototype(obj) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(obj); + } + + if (obj.constructor.prototype == obj) { + return null; + } + + return obj.constructor.prototype; + } + + function hasProperty(obj, property) { + if (!obj) { + return false; + } + + if (Object.prototype.hasOwnProperty.call(obj, property)) { + return true; + } + + return hasProperty(getPrototype(obj), property); + } + ObjectContaining.prototype.asymmetricMatch = function(other) { if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } for (var property in this.sample) { - if (!Object.prototype.hasOwnProperty.call(other, property) || + if (!hasProperty(other, property) || !j$.matchersUtil.equals(this.sample[property], other[property])) { return false; } @@ -2292,6 +2530,16 @@ getJasmineRequireObj().StringMatching = function(j$) { return StringMatching; }; +getJasmineRequireObj().errors = function() { + function ExpectationFailed() {} + + ExpectationFailed.prototype = new Error(); + ExpectationFailed.prototype.constructor = ExpectationFailed; + + return { + ExpectationFailed: ExpectationFailed + }; +}; getJasmineRequireObj().matchersUtil = function(j$) { // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? @@ -2461,11 +2709,13 @@ getJasmineRequireObj().matchersUtil = function(j$) { if (result) { // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) && - isFunction(bCtor) && (bCtor instanceof bCtor))) { - return false; + // or `Array`s from different frames are. + if (className !== '[object Array]') { + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && + isFunction(bCtor) && bCtor instanceof bCtor)) { + return false; + } } // Deep compare objects. for (var key in a) { @@ -2939,7 +3189,7 @@ getJasmineRequireObj().toThrowError = function(j$) { return expected === null && errorType === null; }, matches: function(error) { - return (errorType === null || error.constructor === errorType) && + return (errorType === null || error instanceof errorType) && (expected === null || messageMatch(error.message)); } }; @@ -3044,5 +3294,5 @@ getJasmineRequireObj().interface = function(jasmine, env) { }; getJasmineRequireObj().version = function() { - return '2.2.0'; + return '2.3.4'; }; diff --git a/spec/lib/jasmine-2.2.0/jasmine_favicon.png b/spec/lib/jasmine-2.3.4/jasmine_favicon.png old mode 100644 new mode 100755 similarity index 100% rename from spec/lib/jasmine-2.2.0/jasmine_favicon.png rename to spec/lib/jasmine-2.3.4/jasmine_favicon.png diff --git a/spec/lib/jasmine-2.3.4/mock-ajax.js b/spec/lib/jasmine-2.3.4/mock-ajax.js new file mode 100644 index 00000000..64398005 --- /dev/null +++ b/spec/lib/jasmine-2.3.4/mock-ajax.js @@ -0,0 +1,733 @@ +/* + +Jasmine-Ajax - v3.2.0: a set of helpers for testing AJAX requests under the Jasmine +BDD framework for JavaScript. + +http://github.com/jasmine/jasmine-ajax + +Jasmine Home page: http://jasmine.github.io/ + +Copyright (c) 2008-2015 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +getJasmineRequireObj().ajax = function(jRequire) { + var $ajax = {}; + + $ajax.RequestStub = jRequire.AjaxRequestStub(); + $ajax.RequestTracker = jRequire.AjaxRequestTracker(); + $ajax.StubTracker = jRequire.AjaxStubTracker(); + $ajax.ParamParser = jRequire.AjaxParamParser(); + $ajax.event = jRequire.AjaxEvent(); + $ajax.eventBus = jRequire.AjaxEventBus($ajax.event); + $ajax.fakeRequest = jRequire.AjaxFakeRequest($ajax.eventBus); + $ajax.MockAjax = jRequire.MockAjax($ajax); + + return $ajax.MockAjax; +}; + +getJasmineRequireObj().AjaxEvent = function() { + function now() { + return new Date().getTime(); + } + + function noop() { + } + + // Event object + // https://dom.spec.whatwg.org/#concept-event + function XMLHttpRequestEvent(xhr, type) { + this.type = type; + this.bubbles = false; + this.cancelable = false; + this.timeStamp = now(); + + this.isTrusted = false; + this.defaultPrevented = false; + + // Event phase should be "AT_TARGET" + // https://dom.spec.whatwg.org/#dom-event-at_target + this.eventPhase = 2; + + this.target = xhr; + this.currentTarget = xhr; + } + + XMLHttpRequestEvent.prototype.preventDefault = noop; + XMLHttpRequestEvent.prototype.stopPropagation = noop; + XMLHttpRequestEvent.prototype.stopImmediatePropagation = noop; + + function XMLHttpRequestProgressEvent() { + XMLHttpRequestEvent.apply(this, arguments); + + this.lengthComputable = false; + this.loaded = 0; + this.total = 0; + } + + // Extend prototype + XMLHttpRequestProgressEvent.prototype = XMLHttpRequestEvent.prototype; + + return { + event: function(xhr, type) { + return new XMLHttpRequestEvent(xhr, type); + }, + + progressEvent: function(xhr, type) { + return new XMLHttpRequestProgressEvent(xhr, type); + } + }; +}; +getJasmineRequireObj().AjaxEventBus = function(eventFactory) { + function EventBus(source) { + this.eventList = {}; + this.source = source; + } + + function ensureEvent(eventList, name) { + eventList[name] = eventList[name] || []; + return eventList[name]; + } + + function findIndex(list, thing) { + if (list.indexOf) { + return list.indexOf(thing); + } + + for(var i = 0; i < list.length; i++) { + if (thing === list[i]) { + return i; + } + } + + return -1; + } + + EventBus.prototype.addEventListener = function(event, callback) { + ensureEvent(this.eventList, event).push(callback); + }; + + EventBus.prototype.removeEventListener = function(event, callback) { + var index = findIndex(this.eventList[event], callback); + + if (index >= 0) { + this.eventList[event].splice(index, 1); + } + }; + + EventBus.prototype.trigger = function(event) { + var evt; + + // Event 'readystatechange' is should be a simple event. + // Others are progress event. + // https://xhr.spec.whatwg.org/#events + if (event === 'readystatechange') { + evt = eventFactory.event(this.source, event); + } else { + evt = eventFactory.progressEvent(this.source, event); + } + + var eventListeners = this.eventList[event]; + + if (eventListeners) { + for (var i = 0; i < eventListeners.length; i++) { + eventListeners[i].call(this.source, evt); + } + } + }; + + return function(source) { + return new EventBus(source); + }; +}; + +getJasmineRequireObj().AjaxFakeRequest = function(eventBusFactory) { + function extend(destination, source, propertiesToSkip) { + propertiesToSkip = propertiesToSkip || []; + for (var property in source) { + if (!arrayContains(propertiesToSkip, property)) { + destination[property] = source[property]; + } + } + return destination; + } + + function arrayContains(arr, item) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] === item) { + return true; + } + } + return false; + } + + function wrapProgressEvent(xhr, eventName) { + return function() { + if (xhr[eventName]) { + xhr[eventName].apply(xhr, arguments); + } + }; + } + + function initializeEvents(xhr) { + xhr.eventBus.addEventListener('readystatechange', wrapProgressEvent(xhr, 'onreadystatechange')); + xhr.eventBus.addEventListener('loadstart', wrapProgressEvent(xhr, 'onloadstart')); + xhr.eventBus.addEventListener('load', wrapProgressEvent(xhr, 'onload')); + xhr.eventBus.addEventListener('loadend', wrapProgressEvent(xhr, 'onloadend')); + xhr.eventBus.addEventListener('progress', wrapProgressEvent(xhr, 'onprogress')); + xhr.eventBus.addEventListener('error', wrapProgressEvent(xhr, 'onerror')); + xhr.eventBus.addEventListener('abort', wrapProgressEvent(xhr, 'onabort')); + xhr.eventBus.addEventListener('timeout', wrapProgressEvent(xhr, 'ontimeout')); + } + + function unconvertibleResponseTypeMessage(type) { + var msg = [ + "Can't build XHR.response for XHR.responseType of '", + type, + "'.", + "XHR.response must be explicitly stubbed" + ]; + return msg.join(' '); + } + + function fakeRequest(global, requestTracker, stubTracker, paramParser) { + function FakeXMLHttpRequest() { + requestTracker.track(this); + this.eventBus = eventBusFactory(this); + initializeEvents(this); + this.requestHeaders = {}; + this.overriddenMimeType = null; + } + + function findHeader(name, headers) { + name = name.toLowerCase(); + for (var header in headers) { + if (header.toLowerCase() === name) { + return headers[header]; + } + } + } + + function normalizeHeaders(rawHeaders, contentType) { + var headers = []; + + if (rawHeaders) { + if (rawHeaders instanceof Array) { + headers = rawHeaders; + } else { + for (var headerName in rawHeaders) { + if (rawHeaders.hasOwnProperty(headerName)) { + headers.push({ name: headerName, value: rawHeaders[headerName] }); + } + } + } + } else { + headers.push({ name: "Content-Type", value: contentType || "application/json" }); + } + + return headers; + } + + function parseXml(xmlText, contentType) { + if (global.DOMParser) { + return (new global.DOMParser()).parseFromString(xmlText, 'text/xml'); + } else { + var xml = new global.ActiveXObject("Microsoft.XMLDOM"); + xml.async = "false"; + xml.loadXML(xmlText); + return xml; + } + } + + var xmlParsables = ['text/xml', 'application/xml']; + + function getResponseXml(responseText, contentType) { + if (arrayContains(xmlParsables, contentType.toLowerCase())) { + return parseXml(responseText, contentType); + } else if (contentType.match(/\+xml$/)) { + return parseXml(responseText, 'text/xml'); + } + return null; + } + + var iePropertiesThatCannotBeCopied = ['responseBody', 'responseText', 'responseXML', 'status', 'statusText', 'responseTimeout']; + extend(FakeXMLHttpRequest.prototype, new global.XMLHttpRequest(), iePropertiesThatCannotBeCopied); + extend(FakeXMLHttpRequest.prototype, { + open: function() { + this.method = arguments[0]; + this.url = arguments[1]; + this.username = arguments[3]; + this.password = arguments[4]; + this.readyState = 1; + this.eventBus.trigger('readystatechange'); + }, + + setRequestHeader: function(header, value) { + if(this.requestHeaders.hasOwnProperty(header)) { + this.requestHeaders[header] = [this.requestHeaders[header], value].join(', '); + } else { + this.requestHeaders[header] = value; + } + }, + + overrideMimeType: function(mime) { + this.overriddenMimeType = mime; + }, + + abort: function() { + this.readyState = 0; + this.status = 0; + this.statusText = "abort"; + this.eventBus.trigger('readystatechange'); + this.eventBus.trigger('progress'); + this.eventBus.trigger('abort'); + this.eventBus.trigger('loadend'); + }, + + readyState: 0, + + onloadstart: null, + onprogress: null, + onabort: null, + onerror: null, + onload: null, + ontimeout: null, + onloadend: null, + onreadystatechange: null, + + addEventListener: function() { + this.eventBus.addEventListener.apply(this.eventBus, arguments); + }, + + removeEventListener: function(event, callback) { + this.eventBus.removeEventListener.apply(this.eventBus, arguments); + }, + + status: null, + + send: function(data) { + this.params = data; + this.eventBus.trigger('loadstart'); + + var stub = stubTracker.findStub(this.url, data, this.method); + if (stub) { + if (stub.isReturn()) { + this.respondWith(stub); + } else if (stub.isError()) { + this.responseError(); + } else if (stub.isTimeout()) { + this.responseTimeout(); + } + } + }, + + contentType: function() { + return findHeader('content-type', this.requestHeaders); + }, + + data: function() { + if (!this.params) { + return {}; + } + + return paramParser.findParser(this).parse(this.params); + }, + + getResponseHeader: function(name) { + name = name.toLowerCase(); + var resultHeader; + for(var i = 0; i < this.responseHeaders.length; i++) { + var header = this.responseHeaders[i]; + if (name === header.name.toLowerCase()) { + if (resultHeader) { + resultHeader = [resultHeader, header.value].join(', '); + } else { + resultHeader = header.value; + } + } + } + return resultHeader; + }, + + getAllResponseHeaders: function() { + var responseHeaders = []; + for (var i = 0; i < this.responseHeaders.length; i++) { + responseHeaders.push(this.responseHeaders[i].name + ': ' + + this.responseHeaders[i].value); + } + return responseHeaders.join('\r\n') + '\r\n'; + }, + + responseText: null, + response: null, + responseType: null, + + responseValue: function() { + switch(this.responseType) { + case null: + case "": + case "text": + return this.readyState >= 3 ? this.responseText : ""; + case "json": + return JSON.parse(this.responseText); + case "arraybuffer": + throw unconvertibleResponseTypeMessage('arraybuffer'); + case "blob": + throw unconvertibleResponseTypeMessage('blob'); + case "document": + return this.responseXML; + } + }, + + + respondWith: function(response) { + if (this.readyState === 4) { + throw new Error("FakeXMLHttpRequest already completed"); + } + + this.status = response.status; + this.statusText = response.statusText || ""; + this.responseHeaders = normalizeHeaders(response.responseHeaders, response.contentType); + this.readyState = 2; + this.eventBus.trigger('readystatechange'); + + this.responseText = response.responseText || ""; + this.responseType = response.responseType || ""; + this.readyState = 4; + this.responseXML = getResponseXml(response.responseText, this.getResponseHeader('content-type') || ''); + if (this.responseXML) { + this.responseType = 'document'; + } + + if ('response' in response) { + this.response = response.response; + } else { + this.response = this.responseValue(); + } + + this.eventBus.trigger('readystatechange'); + this.eventBus.trigger('progress'); + this.eventBus.trigger('load'); + this.eventBus.trigger('loadend'); + }, + + responseTimeout: function() { + if (this.readyState === 4) { + throw new Error("FakeXMLHttpRequest already completed"); + } + this.readyState = 4; + jasmine.clock().tick(30000); + this.eventBus.trigger('readystatechange'); + this.eventBus.trigger('progress'); + this.eventBus.trigger('timeout'); + this.eventBus.trigger('loadend'); + }, + + responseError: function() { + if (this.readyState === 4) { + throw new Error("FakeXMLHttpRequest already completed"); + } + this.readyState = 4; + this.eventBus.trigger('readystatechange'); + this.eventBus.trigger('progress'); + this.eventBus.trigger('error'); + this.eventBus.trigger('loadend'); + } + }); + + return FakeXMLHttpRequest; + } + + return fakeRequest; +}; + +getJasmineRequireObj().MockAjax = function($ajax) { + function MockAjax(global) { + var requestTracker = new $ajax.RequestTracker(), + stubTracker = new $ajax.StubTracker(), + paramParser = new $ajax.ParamParser(), + realAjaxFunction = global.XMLHttpRequest, + mockAjaxFunction = $ajax.fakeRequest(global, requestTracker, stubTracker, paramParser); + + this.install = function() { + if (global.XMLHttpRequest === mockAjaxFunction) { + throw "MockAjax is already installed."; + } + + global.XMLHttpRequest = mockAjaxFunction; + }; + + this.uninstall = function() { + global.XMLHttpRequest = realAjaxFunction; + + this.stubs.reset(); + this.requests.reset(); + paramParser.reset(); + }; + + this.stubRequest = function(url, data, method) { + var stub = new $ajax.RequestStub(url, data, method); + stubTracker.addStub(stub); + return stub; + }; + + this.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; + + this.addCustomParamParser = function(parser) { + paramParser.add(parser); + }; + + this.requests = requestTracker; + this.stubs = stubTracker; + } + + return MockAjax; +}; + +getJasmineRequireObj().AjaxParamParser = function() { + function ParamParser() { + var defaults = [ + { + test: function(xhr) { + return (/^application\/json/).test(xhr.contentType()); + }, + parse: function jsonParser(paramString) { + return JSON.parse(paramString); + } + }, + { + test: function(xhr) { + return true; + }, + parse: function naiveParser(paramString) { + var data = {}; + var params = paramString.split('&'); + + for (var i = 0; i < params.length; ++i) { + var kv = params[i].replace(/\+/g, ' ').split('='); + var key = decodeURIComponent(kv[0]); + data[key] = data[key] || []; + data[key].push(decodeURIComponent(kv[1])); + } + return data; + } + } + ]; + var paramParsers = []; + + this.add = function(parser) { + paramParsers.unshift(parser); + }; + + this.findParser = function(xhr) { + for(var i in paramParsers) { + var parser = paramParsers[i]; + if (parser.test(xhr)) { + return parser; + } + } + }; + + this.reset = function() { + paramParsers = []; + for(var i in defaults) { + paramParsers.push(defaults[i]); + } + }; + + this.reset(); + } + + return ParamParser; +}; + +getJasmineRequireObj().AjaxRequestStub = function() { + var RETURN = 0, + ERROR = 1, + TIMEOUT = 2; + + function RequestStub(url, stubData, method) { + var normalizeQuery = function(query) { + return query ? query.split('&').sort().join('&') : undefined; + }; + + if (url instanceof RegExp) { + this.url = url; + this.query = undefined; + } else { + var split = url.split('?'); + this.url = split[0]; + this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined; + } + + this.data = (stubData instanceof RegExp) ? stubData : normalizeQuery(stubData); + this.method = method; + + this.andReturn = function(options) { + this.action = RETURN; + this.status = options.status || 200; + + this.contentType = options.contentType; + this.response = options.response; + this.responseText = options.responseText; + this.responseHeaders = options.responseHeaders; + }; + + this.isReturn = function() { + return this.action === RETURN; + }; + + this.andError = function() { + this.action = ERROR; + }; + + this.isError = function() { + return this.action === ERROR; + }; + + this.andTimeout = function() { + this.action = TIMEOUT; + }; + + this.isTimeout = function() { + return this.action === TIMEOUT; + }; + + this.matches = function(fullUrl, data, method) { + var urlMatches = false; + fullUrl = fullUrl.toString(); + if (this.url instanceof RegExp) { + urlMatches = this.url.test(fullUrl); + } else { + var urlSplit = fullUrl.split('?'), + url = urlSplit[0], + query = urlSplit[1]; + urlMatches = this.url === url && this.query === normalizeQuery(query); + } + var dataMatches = false; + if (this.data instanceof RegExp) { + dataMatches = this.data.test(data); + } else { + dataMatches = !this.data || this.data === normalizeQuery(data); + } + return urlMatches && dataMatches && (!this.method || this.method === method); + }; + } + + return RequestStub; +}; + +getJasmineRequireObj().AjaxRequestTracker = function() { + function RequestTracker() { + var requests = []; + + this.track = function(request) { + requests.push(request); + }; + + this.first = function() { + return requests[0]; + }; + + this.count = function() { + return requests.length; + }; + + this.reset = function() { + requests = []; + }; + + this.mostRecent = function() { + return requests[requests.length - 1]; + }; + + this.at = function(index) { + return requests[index]; + }; + + this.filter = function(url_to_match) { + var matching_requests = []; + + for (var i = 0; i < requests.length; i++) { + if (url_to_match instanceof RegExp && + url_to_match.test(requests[i].url)) { + matching_requests.push(requests[i]); + } else if (url_to_match instanceof Function && + url_to_match(requests[i])) { + matching_requests.push(requests[i]); + } else { + if (requests[i].url === url_to_match) { + matching_requests.push(requests[i]); + } + } + } + + return matching_requests; + }; + } + + return RequestTracker; +}; + +getJasmineRequireObj().AjaxStubTracker = function() { + function StubTracker() { + var stubs = []; + + this.addStub = function(stub) { + stubs.push(stub); + }; + + this.reset = function() { + stubs = []; + }; + + this.findStub = function(url, data, method) { + for (var i = stubs.length - 1; i >= 0; i--) { + var stub = stubs[i]; + if (stub.matches(url, data, method)) { + return stub; + } + } + }; + } + + return StubTracker; +}; + +(function() { + var jRequire = getJasmineRequireObj(), + MockAjax = jRequire.ajax(jRequire); + if (typeof window === "undefined" && typeof exports === "object") { + exports.MockAjax = MockAjax; + jasmine.Ajax = new MockAjax(exports); + } else { + window.MockAjax = MockAjax; + jasmine.Ajax = new MockAjax(window); + } +}()); diff --git a/spec/loading-modes.spec.js b/spec/loading-modes.spec.js new file mode 100644 index 00000000..c2c38a6d --- /dev/null +++ b/spec/loading-modes.spec.js @@ -0,0 +1,530 @@ +describe('loading modes', function() { + + var ELEMENT_ID = 'player-target'; + + var JSON_SRC = './spec/empty.json'; + + function FakeImporter() {}; + FakeImporter.prototype.load = function() { return new anm.Animation(); }; + anm.importers.register('fake', FakeImporter); + + function prepareDivElement(id) { + var element = document.createElement('div'); + element.id = id; + document.body.appendChild(element); + return element; + } + + function whenDocumentReady(f) { + anm.engine.onDocReady(f); + } + + function prepareJsonRequestStub() { + jasmine.Ajax.stubRequest(JSON_SRC).andReturn({ + "responseText": '{}' + }); + } + + function lastAjaxCall() { + return jasmine.Ajax.requests.mostRecent(); + } + + beforeEach(function() { + jasmine.Ajax.install(); + }); + + afterEach(function() { + anm.detachAllPlayers(); // this will also detach element if players were created + anm.forgetAllPlayers(); + //if (element && element.parentNode) document.body.removeChild(element); + jasmine.Ajax.uninstall(); + }); + + it('`autoPlay` is off by default', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + expect(anm.createPlayer(ELEMENT_ID).autoPlay).toBeFalsy(); + }); + }); + + it('should have `rightaway` as default option', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + expect(anm.createPlayer(ELEMENT_ID).loadingMode).toBe(anm.C.LM_RIGHTAWAY); + }); + }); + + xit('should fallback to `rightaway` if loadingMode is unknown', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { loadingMode: 'foobarbuz' }); + expect(player.loadingMode).toBe(anm.C.LM_RIGHTAWAY); + }); + }); + + describe('right away', function() { + + it('should not load anything when player created and source wasn\'t specified', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + anm.createPlayer(ELEMENT_ID); + expect(lastAjaxCall()).not.toBeDefined(); + }); + }); + + it('should indeed load animation passed to `load` call', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('should automatically load a scene when source specified with HTML attribute', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + anm.findAndInitPotentialPlayers(); + + var lastCall = lastAjaxCall(); + expect(lastCall).toBeDefined(); + if (lastCall) { expect(lastCall.url).toBe(JSON_SRC) }; + }); + }); + + it('should automatically load a scene when source passed with forSnapshot', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter); + + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('since autoPlay is off, should not play animation after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('since autoPlay is off, should not play animation even when source was specified with HTML attributes', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + var playSpy = jasmine.createSpy('play'); + anm.findAndInitPotentialPlayers({ + handle: { 'play': playSpy } + }); + + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('since autoPlay is off, should not play animation even when source was passed with forSnapshot call', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var playSpy = jasmine.createSpy('play'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + handle: { 'play': playSpy } + }); + expect(playSpy).not.toHaveBeenLoaded(); + }); + }); + + it('when autoPlay is on, yet should not play if no source was specified', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + + var player = anm.createPlayer(ELEMENT_ID, { autoPlay: true }); + playSpy = spyOn(player, 'play'); + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('when autoPlay is on, should automatically play animation passed to a `load` call', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { autoPlay: true }); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + it('when autoPlay is on, should automatically load and play animation specified with HTML attribute', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + var playSpy = jasmine.createSpy('play'); + anm.findAndInitPotentialPlayers({ + autoPlay: true, + handle: { 'play': playSpy } + }); + + expect(playSpy).toHaveBeenCalled(); + }); + }); + + it('when autoPlay is on, should automatically load and play animation passed with forSnapshot method', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var playSpy = jasmine.createSpy('play'); + anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + autoPlay: true, + handle: { 'play': playSpy } + }); + + expect(playSpy).toHaveBeenCalled(); + }); + }); + + }); + + describe('on request', function() { + + it('should not load anything when player created and source wasn\'t specified', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + var loadSpy = jasmine.createSpy('load'); + anm.createPlayer(ELEMENT_ID, { loadingMode: anm.C.LM_ONREQUEST, + handle: { load: loadSpy } }); + expect(loadSpy).not.toHaveBeenCalled(); + }); + }); + + it('still should not load anything even when source was specified with HTML attribute', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + anm.findAndInitPotentialPlayers({ loadingMode: anm.C.LM_ONREQUEST }); + expect(lastAjaxCall()).not.toBeDefined(); + }); + }); + + it('still should not load anything even when source was passed with forSnapshot', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var importLoadSpy = spyOn(fakeImporter, 'load').and.callThrough(); + anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONREQUEST + }); + + expect(importLoadSpy).not.toHaveBeenCalled(); + expect(lastAjaxCall()).not.toBeDefined(); + }); + }); + + it('should load animation when load called manually', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { loadingMode: anm.C.LM_ONREQUEST }); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('should load animation when load called manually w/o arguments and source was specified via HTML attribute', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + anm.findAndInitPotentialPlayers({ loadingMode: anm.C.LM_ONREQUEST }); + + expect(lastAjaxCall()).not.toBeDefined(); + anm.player_manager.instances[0].load(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('should load animation when load called manually w/o arguments and source was provided with forSnapshot', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONREQUEST + }); + + expect(lastAjaxCall()).not.toBeDefined(); + player.load(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('since autoPlay is off, should not play animation after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { + loadingMode: anm.C.LM_ONREQUEST + }); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('since autoPlay is off, should not play animation after a call to `load` even when source was specified with HTML attributes', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + var playSpy = jasmine.createSpy('play'); + anm.findAndInitPotentialPlayers({ + loadingMode: anm.C.LM_ONREQUEST, + handle: { 'play': playSpy } + }); + + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('since autoPlay is off, should not play animation after a call to `load` even when source was passed with forSnapshot call', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONREQUEST + }); + playSpy = spyOn(player, 'play'); + player.load(); + expect(playSpy).not.toHaveBeenCalled(); + }); + }); + + it('if autoPlay is on, should automatically play animation just after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { + loadingMode: anm.C.LM_ONREQUEST, + autoPlay: true + }); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + it('if autoPlay is on and source was specified with HTML attributes, should automatically play animation just after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + var playSpy = jasmine.createSpy('play'); + anm.findAndInitPotentialPlayers({ + loadingMode: anm.C.LM_ONREQUEST, + autoPlay: true, handle: { 'play': playSpy } + }); + anm.player_manager.instances[0].load(); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + it('if autoPlay is on and source was passed with forSnapshot call, should automatically play animation just after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONREQUEST + }); + playSpy = spyOn(player, 'play'); + player.load(); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + }); + + describe('on play', function() { + + it('should not load anything when player was created and source wasn\'t specified', function() { + whenDocumentReady(function() { + prepareDivElement(ELEMENT_ID); + var loadSpy = jasmine.createSpy('load'); + anm.createPlayer(ELEMENT_ID, { loadingMode: anm.C.LM_ONPLAY, + handle: { load: loadSpy } }); + expect(loadSpy).not.toHaveBeenCalled(); + }); + }); + + it('if `load` was called before `play`, should postpone it to `play` call', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { + loadingMode: anm.C.LM_ONPLAY + }); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + player.load(JSON_SRC, fakeImporter); + expect(lastAjaxCall()).not.toBeDefined(); + expect(playSpy).not.toHaveBeenCalled(); + player.play(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('should automatically load and play animation on `play` call when source was specified with HTML attribute', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + anm.findAndInitPotentialPlayers({ + loadingMode: anm.C.LM_ONPLAY, + }); + expect(lastAjaxCall()).not.toBeDefined(); + anm.player_manager.instances[0].play(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('should automatically load and play animation on `play` call when source was passed with forSnapshot', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONPLAY + }); + expect(lastAjaxCall()).not.toBeDefined(); + player.play(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + xit('should fail if `load` wasn\'t called before `play` and no source was specified', function() { + + }); + + it('if autoPlay is on, should automatically load and play animation just after a call to `load`', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + var player = anm.createPlayer(ELEMENT_ID, { + loadingMode: anm.C.LM_ONPLAY + }); + playSpy = spyOn(player, 'play'); + var fakeImporter = anm.importers.create('fake'); + expect(lastAjaxCall()).not.toBeDefined(); + expect(playSpy).not.toHaveBeenCalled(); + player.load(JSON_SRC, fakeImporter); + expect(lastAjaxCall()).toBeDefined(); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + it('if autoPlay is on and source was specified with HTML attributes, should automatically play animation right away', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + var element = prepareDivElement(ELEMENT_ID); + + element.setAttribute('anm-player-target', true); + element.setAttribute('anm-src', JSON_SRC); + element.setAttribute('anm-importer', 'fake'); + + anm.findAndInitPotentialPlayers({ + loadingMode: anm.C.LM_ONPLAY, + autoPlay: true + }); + expect(lastAjaxCall()).not.toBeDefined(); + anm.player_manager.instances[0].play(); + expect(lastAjaxCall()).toBeDefined(); + }); + }); + + it('if autoPlay is on and source was passed with forSnapshot call, should automatically play animation right away', function() { + whenDocumentReady(function() { + prepareJsonRequestStub(); + prepareDivElement(ELEMENT_ID); + + var fakeImporter = anm.importers.create('fake'); + var playSpy = jasmine.createSpy('play'); + var player = anm.Player.forSnapshot(ELEMENT_ID, JSON_SRC, fakeImporter, { + loadingMode: anm.C.LM_ONPLAY, + autoPlay: true, + handle: { play: playSpy } + }); + expect(lastAjaxCall()).toBeDefined(); + expect(playSpy).toHaveBeenCalled(); + }); + }); + + }); + + xdescribe('onidle', function() {}); + + xdescribe('wheninview', function() {}); + +}); diff --git a/spec/playing-modes.spec.js b/spec/playing-modes.spec.js new file mode 100644 index 00000000..1c54c464 --- /dev/null +++ b/spec/playing-modes.spec.js @@ -0,0 +1,7 @@ +xdescribe('playing modes', function() { + + xdescribe('onhover', function() {}); + + xdescribe('wheninview', function() {}); + +}); diff --git a/src/anm/constants.js b/src/anm/constants.js index f0fc909f..1b5bcb4f 100644 --- a/src/anm/constants.js +++ b/src/anm/constants.js @@ -65,13 +65,38 @@ C.LT_URL = 4; // ### Loading modes /* ---------------- */ -C.LM_ONREQUEST = 'onrequest'; -C.LM_ONPLAY = 'onplay'; -// C.LM_ONSCROLL -// C.LM_ONSCROLLIN +// loading modes below are closely tied to `autoPlay` option: if it's set to `true`, playing starts +// immediately after loading (default is `false`) -C.LM_DEFAULT = C.LM_ONREQUEST; +// there are also playing modes below; +C.LM_RIGHTAWAY = 'rightaway'; // searches for an animation source where possible (i.e. HTML tag attribute) + // and, if finds it, tries to load it on player creation; if source wasn't found, + // waits for user to call `.load` manually as for 'onrequest' +C.LM_ONREQUEST = 'onrequest'; // waits for user to manually call `.load` method; if animation source was + // passed i.e. through HTML tag attribute, waits for user to call `.load` + // method without parameters and uses this URL as a source +C.LM_ONPLAY = 'onplay'; // when play button was pressed or `.play` method was called, starts loading a scene and plays it just after +C.LM_ONIDLE = 'onidle'; // waits for pause in user actions (mouse move, clicks, keyboard) to load the animation; planned to use + // requestIdleCallback in future + +C.LM_DEFAULT = C.LM_RIGHTAWAY; + +C.LOADING_MODES = [ C.LM_RIGHTAWAY, C.LM_ONREQUEST, C.LM_ONPLAY, C.LM_ONIDLE ]; + +// ### Playing modes +/* ---------------- */ + +// playing modes are overriden if `autoPlay` == `true` + +C.PM_ONREQUEST = 'onrequest'; // waits for user to manually call `play` method or press play button +C.PM_ONHOVER = 'onhover'; // starts playing animation when user hovered with mouse over the player canvas +C.PM_WHENINVIEW = 'wheninview'; // starts loading animation when at least some part of canvas appears in + // user's browser viewport + +C.PM_DEFAULT = C.PM_ONREQUEST; + +C.PLAYING_MODES = [ C.PM_ONREQUEST, C.PM_ONHOVER, C.PM_WHENINVIEW ]; // Element // ----------------------------------------------------------------------------- diff --git a/src/anm/loader.js b/src/anm/loader.js index d8bc836a..dd2b80f7 100644 --- a/src/anm/loader.js +++ b/src/anm/loader.js @@ -73,6 +73,7 @@ Loader.loadFromUrl = function(player, url, importer, callback) { Loader.loadFromObj = function(player, object, importer, callback) { if (!importer) throw errors.player(ErrLoc.P.NO_IMPORTER_TO_LOAD_WITH, player); var anim = importer.load(object); + //if (!anim) throw errors.player(ErrLoc.P.IMPORTER_RETURNED_EMPTY_ANIMATION, player); player.fire(C.S_IMPORT, { importer: importer, animation: anim, source: object }); Loader.loadAnimation(player, anim, callback); }; diff --git a/src/anm/loc.js b/src/anm/loc.js index b6913093..90262593 100644 --- a/src/anm/loc.js +++ b/src/anm/loc.js @@ -52,6 +52,7 @@ Errors.P.INIT_AFTER_LOAD = 'Initialization was called after loading a animation' Errors.P.SNAPSHOT_LOADING_FAILED = 'Snapshot failed to load ({0})'; Errors.P.IMPORTER_CONSTRUCTOR_PASSED = 'You\'ve passed importer constructor to snapshot loader, but not an instance! ' + 'Probably you used anm.importers.get instead of anm.importers.create.'; +Errors.P.IMPORTER_RETURNED_EMPTY_ANIMATION = 'The importer has not returned any Animation object from .load method'; Errors.P.DOM_NOT_READY = 'Document in not yet ready, please consider moving your initialization script to the bottom of your web page'; Errors.A.OBJECT_IS_NOT_ELEMENT = 'It appears that you\'ve passed not an instance of anm.Element'; Errors.A.ELEMENT_IS_REGISTERED = 'This element is already registered in animation'; diff --git a/src/anm/player.js b/src/anm/player.js index 4f94e41c..e3dd5541 100644 --- a/src/anm/player.js +++ b/src/anm/player.js @@ -127,7 +127,8 @@ Player.DEFAULT_CONFIGURATION = { 'debug': false, 'controlsEnabled': undefined, // undefined means 'auto' 'controlsInvisible': undefined, 'infoEnabled': undefined, // undefined means 'auto' - 'loadingMode': C.LM_DEFAULT, // undefined means 'auto' + 'loadingMode': C.LM_DEFAULT, + 'playingMode': C.PM_DEFAULT, 'thumbnail': undefined, 'bgColor': undefined, 'ribbonsColor': undefined, @@ -174,7 +175,8 @@ Player.EMPTY_BG = 'rgba(0,0,0,.05)'; * controlsEnabled: undefined, // undefined means 'auto' * infoEnabled: undefined, // undefined means 'auto' * handleEvents: undefined, // undefined means 'auto' - * loadingMode: undefined, // undefined means 'auto' + * loadingMode: C.LM_DEFAULT, // see loading modes description in constants.js + * playingMode: C.PM_DEFAULT, // see playing modes description in constants.js * thumbnail: undefined, * forceAnimationSize: false, * stretchToCanvas: false, @@ -234,10 +236,17 @@ Player.prototype.init = function(elm, opts) { } catch(e) {} this._addOpts(opts || {}); this._postInit(); + if (opts && opts.handle) { + for (var event in opts.handle) { + this.on(event, opts.handle[event]); + } + } this._checkOpts(); /* TODO: if (this.canvas.hasAttribute('data-url')) */ playerManager.fire(C.S_NEW_PLAYER, this); + this._checkPreparedSource(); // if scene was specified with HTML attributes, + // and loading mode matches, will load it immediately return this; }; @@ -272,6 +281,10 @@ Player.prototype.load = function(arg1, arg2, arg3, arg4) { throw errors.player(ErrLoc.P.COULD_NOT_LOAD_WHILE_PLAYING, player); } + if (this.loadingMode === C.LM_ONPLAY) { + this._postponedLoad = arguments; return; + } + /* object */ /* object, callback */ /* object, importer */ @@ -315,21 +328,6 @@ Player.prototype.load = function(arg1, arg2, arg3, arg4) { callback = arg3; } - if ((player.loadingMode == C.LM_ONPLAY) && - !player._playLock) { // if play lock is set, we should just load an animation normally, since - // it was requested after the call to 'play', or else it was called by user - // FIXME: may be playLock was set by player and user calls this method - // while some animation is already loading - if (player._postponedLoad) throw errors.player(ErrLoc.P.LOAD_WAS_ALREADY_POSTPONED, player); - player._lastReceivedAnimationId = null; - // this kind of postponed call is different from the ones below (_clearPostpones and _postpone), - // since this one is related to loading mode, rather than calling later some methods which - // were called during the process of loading (and were required to be called when it was finished). - player._postponedLoad = [ object, duration, importer, callback ]; - player.stop(); - return; - } - // if player was loading resources already when .load() was called, inside the ._reset() method // postpones will be cleared and loaders cancelled @@ -454,34 +452,6 @@ Player.prototype.play = function(from, speed, stopAfter) { if (player.infiniteDuration) return; // it's ok to skip this call if it's some dynamic animation (FIXME?) } - if ((player.loadingMode === C.LM_ONPLAY) && !player._lastReceivedAnimationId) { - if (player._playLock) return; // we already loading something - // use _postponedLoad with _playLock flag set - // call play when loading was finished - player._playLock = true; - var loadArgs = player._postponedLoad, - playArgs = arguments; - if (!loadArgs) throw errors.player(ErrLoc.P.NO_LOAD_CALL_BEFORE_PLAY, player); - var loadCallback = loadArgs[3]; - var afterLoad = function() { - if (loadCallback) loadCallback.apply(player, arguments); - player._postponedLoad = null; - player._playLock = false; - player._lastReceivedAnimationId = player.anim.id; - Player.prototype.play.apply(player, playArgs); - }; - loadArgs[3] = afterLoad; // substitute callback with our variant which calls the previous one - Player.prototype.load.apply(player, loadArgs); - return; - } - - if ((player.loadingMode === C.LM_ONREQUEST) && - (player.happens === C.RES_LOADING)) { player._postpone('play', arguments); - return; } // if player loads remote resources just now, - // postpone this task and exit. postponed tasks - // will be called when all remote resources were - // finished loading - // reassigns var to ensure proper function is used //__nextFrame = engine.getRequestFrameFunc(); //__stopAnim = engine.getCancelFrameFunc(); @@ -560,8 +530,7 @@ Player.prototype.stop = function() { // postpone this task and exit. postponed tasks // will be called when all remote resources were // finished loading - if ((player.happens === C.RES_LOADING) && - (player.loadingMode === C.LM_ONREQUEST)) { + if (player.happens === C.RES_LOADING) { player._postpone('stop', arguments); return; } @@ -573,8 +542,7 @@ Player.prototype.stop = function() { var anim = player.anim; - if (anim || ((player.loadingMode == C.LM_ONPLAY) && - player._postponedLoad)) { + if (anim) { player.happens = C.STOPPED; player._drawStill(); player.fire(C.S_CHANGE_STATE, C.STOPPED); @@ -607,8 +575,7 @@ Player.prototype.pause = function() { // postpone this task and exit. postponed tasks // will be called when all remote resources were // finished loading - if ((player.happens === C.RES_LOADING) && - (player.loadingMode === C.LM_ONREQUEST)) { + if (player.happens === C.RES_LOADING) { player._postpone('pause', arguments); return player; } @@ -739,12 +706,17 @@ Player.prototype._addOpts = function(opts) { this.width = opts.width || this.width; this.height = opts.height || this.height; + this.loadingMode = is.defined(opts.loadingMode) + ? ((C.LOADING_MODES.indexOf(opts.loadingMode) >= 0) ? opts.loadingMode : C.LM_DEFAULT) + : this.loadingMode; + this.playingMode = is.defined(opts.playingMode) + ? ((C.PLAYING_MODES.indexOf(opts.playingMode) >= 0) ? opts.playingMode : C.PM_DEFAULT) + : this.playingMode; + this.ribbonsColor = opts.ribbonsColor || this.ribbonsColor; this.thumbnailSrc = opts.thumbnail || this.thumbnailSrc; - this.loadingMode = is.defined(opts.loadingMode) ? - opts.loadingMode : this.loadingMode; this.audioEnabled = is.defined(opts.audioEnabled) ? opts.audioEnabled : this.audioEnabled; this.globalVolume = is.defined(opts.volume) ? @@ -818,16 +790,9 @@ Player.prototype._checkOpts = function() { // initial state of the player, called from constuctor Player.prototype._postInit = function() { this.stop(); - /* TODO: load some default information into player */ - var to_load = engine.hasUrlToLoad(this.wrapper); - if (!to_load.url) to_load = engine.hasUrlToLoad(this.canvas); - if (to_load.url) { - var importer = null; - if (to_load.importer_id && anm.importers.isAccessible(to_load.importer_id)) { - importer = anm.importers.create(to_load.importer_id); - } - this.load(to_load.url, importer); - } + // this._prepared_src will be null if there is no url to load + this._prepared_src = engine.hasUrlToLoad(this.wrapper); + if (!this._prepared_src) this._prepared_src = engine.hasUrlToLoad(this.canvas); }; /** @@ -897,12 +862,11 @@ Player.prototype.forceRedraw = function() { */ Player.prototype.drawAt = function(time) { if (time === Timeline.NO_TIME) throw errors.player(ErrLoc.P.PASSED_TIME_VALUE_IS_NO_TIME, this); - if ((this.happens === C.RES_LOADING) && - (this.loadingMode === C.LM_ONREQUEST)) { this._postpone('drawAt', arguments); - return; } // if player loads remote resources just now, - // postpone this task and exit. postponed tasks - // will be called when all remote resources were - // finished loading + if (this.happens === C.RES_LOADING) { this._postpone('drawAt', arguments); + return; } // if player loads remote resources just now, + // postpone this task and exit. postponed tasks + // will be called when all remote resources were + // finished loading if ((time < 0) || (!this.infiniteDuration && (time > this.anim.getDuration()))) { throw errors.player(utils.strf(ErrLoc.P.PASSED_TIME_NOT_IN_RANGE, [time]), this); } @@ -1393,8 +1357,7 @@ Player.prototype._reset = function() { // clear postponed tasks if player started to load remote resources, // they are not required since new animation is loading in the player now // or it is being detached - if ((this.loadingMode === C.LM_ONREQUEST) && - (this.happens === C.RES_LOADING)) { + if (this.happens === C.RES_LOADING) { this._clearPostpones(); resourceManager.cancel(this.id); } @@ -1640,7 +1603,7 @@ Player.prototype._callPostpones = function() { Player.forSnapshot = function(elm_id, snapshot_url, importer, callback, alt_opts) { var player = new Player(); player.init(elm_id, alt_opts); - player.load(snapshot_url, importer, callback); + if (player.loadingMode === C.LM_RIGHTAWAY) player.load(snapshot_url, importer, callback); return player; }; @@ -1686,4 +1649,16 @@ Player.prototype._applyTimeOptionsIfSet = function() { } } +Player.prototype._checkPreparedSource = function() { + if (this._prepared_src && (this.loadingMode === C.LM_RIGHTAWAY)) { + var url = this._prepared_src.url, + importer_id = this._prepared_src.importer_id; + var importer = null; + if (importer_id && anm.importers.isAccessible(importer_id)) { + importer = anm.importers.create(importer_id); + } + this.load(url, importer); + } +} + module.exports = Player; diff --git a/src/anm/player_manager.js b/src/anm/player_manager.js index 3cfb3169..4b405245 100644 --- a/src/anm/player_manager.js +++ b/src/anm/player_manager.js @@ -40,6 +40,27 @@ PlayerManager.prototype.getPlayer = function(cvs_id) { return this.hash[cvs_id]; }; +/** + * @method detachAll + * + * Detach all players created before + */ +PlayerManager.prototype.detachAll = function() { + for (var i = 0, il = this.instances.length; i < il; i++) { + this.instances[i].detach(); + } +} + +/** + * @method forgetAll + * + * Clear the data collected about all players instances created before + */ +PlayerManager.prototype.forgetAll = function() { + this.hash = {}; + this.instances = []; +} + /** * @method handleDocumentHiddenChange * @private diff --git a/src/engine/dom-engine.js b/src/engine/dom-engine.js index b8f27187..b2a88502 100644 --- a/src/engine/dom-engine.js +++ b/src/engine/dom-engine.js @@ -597,10 +597,11 @@ $DE.checkPlayerCanvas = function(cvs) { }; $DE.hasUrlToLoad = function(elm) { - return { - url: elm.getAttribute(URL_ATTR) || elm.getAttribute(SNAPSHOT_URL_ATTR), - importer_id: elm.getAttribute(IMPORTER_ATTR) - }; + var url = elm.getAttribute(URL_ATTR) || elm.getAttribute(SNAPSHOT_URL_ATTR); + var importer_id = elm.getAttribute(IMPORTER_ATTR); + return (url || importer_id) ? { + url: url, importer_id: importer_id + } : null; }; $DE.setTabIndex = function(cvs, idx) { diff --git a/src/main.js b/src/main.js index 10dc1b5b..21485708 100644 --- a/src/main.js +++ b/src/main.js @@ -13,15 +13,19 @@ var PUBLIC_NAMESPACE = 'anm'; var constants = require('./anm/constants.js'), engine = require('engine'), + manager = require('./anm/player_manager.js'), Player = require('./anm/player.js'); -function findAndInitPotentialPlayers() { +function findAndInitPotentialPlayers(options) { var matches = engine.findPotentialPlayers(); for (var i = 0, il = matches.length; i < il; i++) { - anm.createPlayer(matches[i]); + anm.createPlayer(matches[i], options); } } +var detachAllPlayers = manager.detachAll.bind(manager); +var forgetAllPlayers = manager.forgetAll.bind(manager); + engine.onDocReady(findAndInitPotentialPlayers); var Element = require('./anm/animation/element.js'), @@ -79,6 +83,10 @@ var anm = { Audio: require('./anm/media/audio.js'), Video: require('./anm/media/video.js'), + findAndInitPotentialPlayers: findAndInitPotentialPlayers, + detachAllPlayers: detachAllPlayers, + forgetAllPlayers: forgetAllPlayers, + interop: { playerjs: require('./anm/interop/playerjs-io.js') },