├── .gitignore ├── app ├── locales │ ├── Logger.json │ ├── DataGenerator.json │ └── MessageView.json ├── css │ ├── blocks │ │ └── b-message-view │ │ │ └── b-message-view.css │ └── pages │ │ └── index.css ├── descriptors │ ├── Logger.json │ ├── MessageView.json │ └── DataGenerator.json ├── templates │ └── MessageView.html ├── modules │ ├── Logger.js │ ├── DataGenerator.js │ └── MessageView.js └── views │ └── MessageView.html ├── test ├── Makefile ├── units │ ├── CoreTest.js │ ├── SandboxTest.js │ ├── main.js │ ├── TemplateTest.js │ └── EventManagerTest.js ├── lmd.index.json ├── index.html ├── vendors │ ├── qunit.css │ └── qunit.js └── lmd.index.js ├── index.json ├── index.html ├── index.js ├── lib ├── Template.js ├── Sandbox.js ├── Core.js └── EventManager.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /app/locales/Logger.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/locales/DataGenerator.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | lmd ./lmd.index.json ./lmd.index.js 3 | 4 | help: 5 | @echo "USAGE:\n\tmake - makes lmd.index.js" -------------------------------------------------------------------------------- /app/css/blocks/b-message-view/b-message-view.css: -------------------------------------------------------------------------------- 1 | .b-message-view 2 | { 3 | color: green; 4 | font-family: monospace; 5 | } -------------------------------------------------------------------------------- /app/locales/MessageView.json: -------------------------------------------------------------------------------- 1 | { 2 | "text_label": { 3 | "ru": "Он сказал: ", 4 | "en": "He said: " 5 | } 6 | } -------------------------------------------------------------------------------- /app/descriptors/Logger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Logger", 3 | "acl": { 4 | "listen:newData": true, 5 | "listen:ready": true 6 | }, 7 | "resources": {} 8 | } -------------------------------------------------------------------------------- /app/css/pages/index.css: -------------------------------------------------------------------------------- 1 | /*$buildFrom: ../../index.json */ 2 | 3 | /* app/css/blocks/b-message-view/b-message-view.css */ 4 | .b-message-view { 5 | color: green; 6 | font-family: monospace; 7 | } -------------------------------------------------------------------------------- /app/templates/MessageView.html: -------------------------------------------------------------------------------- 1 |
2 | {%=label%}{%=value%} 3 |
-------------------------------------------------------------------------------- /app/descriptors/MessageView.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MessageView", 3 | "acl": { 4 | "trigger:newData:display": true, 5 | "listen:newData": true 6 | }, 7 | "resources": {} 8 | } -------------------------------------------------------------------------------- /app/descriptors/DataGenerator.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataGenerator", 3 | "acl": { 4 | "trigger:newData": true, 5 | "listen:destroy": true 6 | }, 7 | "resources": { 8 | "interval": 1000 9 | } 10 | } -------------------------------------------------------------------------------- /test/units/CoreTest.js: -------------------------------------------------------------------------------- 1 | function CoreTest (require) { 2 | var ok = require('ok'), 3 | equal = require('equal'), 4 | test = require('test'), 5 | expect = require('expect'), 6 | module = require('module'); 7 | 8 | module('Core'); 9 | 10 | test("Core test", function() { 11 | ok(false, "implement"); 12 | }); 13 | } -------------------------------------------------------------------------------- /test/units/SandboxTest.js: -------------------------------------------------------------------------------- 1 | function SandboxTest (require) { 2 | var ok = require('ok'), 3 | equal = require('equal'), 4 | test = require('test'), 5 | expect = require('expect'), 6 | module = require('module'); 7 | 8 | module('Sandbox'); 9 | 10 | test("Sandbox test", function() { 11 | ok(false, "implement"); 12 | }); 13 | } -------------------------------------------------------------------------------- /index.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": ["MessageView", "DataGenerator", "Logger"], 3 | "safe_modules": ["Logger"], 4 | "layout": { 5 | "MessageView": "b-message-view" 6 | }, 7 | "locale": "ru", 8 | "path": { 9 | "descriptor": "./app/descriptors/", 10 | "module": "./app/modules/", 11 | "locale": "./app/locales/", 12 | "template": "./app/templates/" 13 | } 14 | } -------------------------------------------------------------------------------- /test/lmd.index.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./units/", 3 | "modules": { 4 | "main": "main.js", 5 | "*Test": "*Test.js", 6 | "Core": "../../lib/Core.js", 7 | "Sandbox": "../../lib/Sandbox.js", 8 | "Template": "../../lib/Template.js", 9 | "EventManager": "../../lib/EventManager.js" 10 | }, 11 | "main": "main", 12 | "lazy": false, 13 | "pack": false, 14 | "global": "window" 15 | } -------------------------------------------------------------------------------- /app/modules/Logger.js: -------------------------------------------------------------------------------- 1 | function Logger(require, exports, module) { 2 | // this modules is safe - can include orther modules 3 | var log = require('console').log; 4 | 5 | "use strict"; 6 | var printLog = function (event) { 7 | log(event.type, event.data); 8 | }; 9 | 10 | return function (sandbox) { 11 | sandbox.bind('newData', printLog); 12 | sandbox.bind('ready', printLog); 13 | }; 14 | } -------------------------------------------------------------------------------- /app/views/MessageView.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageView 6 | 7 | 8 | 9 |
10 | He said: 0.5139364818759941 11 |
12 | 13 | -------------------------------------------------------------------------------- /app/modules/DataGenerator.js: -------------------------------------------------------------------------------- 1 | function DataGenerator(sandboxed, exports, module) { 2 | "use strict"; 3 | var intervalId; 4 | 5 | return function (sandbox) { 6 | if (intervalId) { 7 | return; // 1 instance only 8 | } 9 | intervalId = setInterval(function () { 10 | sandbox.trigger('newData', Math.random()); 11 | }, sandbox.getResource('interval')); 12 | 13 | sandbox.bind('destroy', function () { 14 | clearInterval(intervalId); 15 | }); 16 | }; 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Application 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @see articles: 4 | * 5 | * Andrew Dupont (Gowalla, Prototype.js, S2) 6 | * 1. Maintainable JavaScript 7 | * http://channel9.msdn.com/Events/MIX/MIX11/EXT23 8 | * 9 | * Nicholas Zakas (Yahoo!, YUI, YUI Test) 10 | * 2. Writing Maintainable JavaScript 11 | * http://www.yuiblog.com/blog/2007/05/25/video-zakas/ 12 | * Slides: 13 | * New: http://www.slideshare.net/nzakas/maintainable-javascript-2011 14 | * Old: http://www.slideshare.net/nzakas/maintainable-javascript-1071179 15 | * 16 | * 3. Scalable JavaScript Application Architecture 17 | * http://developer.yahoo.com/yui/theater/video.php?v=zakas-architecture 18 | */ 19 | 20 | function main(require, exports, module) { 21 | "use strict"; 22 | require('Core').init(); 23 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Scalable JavaScript Application

11 |

12 |
13 |

14 |
    15 |
    16 | 17 | -------------------------------------------------------------------------------- /app/modules/MessageView.js: -------------------------------------------------------------------------------- 1 | function MessageView(sandboxed, exports, module) { 2 | "use strict"; 3 | 4 | var MessageView = function (sandbox) { 5 | var self = this; 6 | this.sandbox = sandbox; 7 | this.template = sandbox.getTemplate("b-message-view"); 8 | this.label = sandbox.getText("text_label"); 9 | this.parentElement = sandbox.getBox(); 10 | 11 | sandbox.bind('newData', function (event) { 12 | self.update(event.data); 13 | }); 14 | }; 15 | 16 | MessageView.prototype.update = function (text) { 17 | this.parentElement.innerHTML = this.template({ 18 | label: this.label, 19 | value: text 20 | }); 21 | this.sandbox.trigger('newData:display'); 22 | }; 23 | 24 | return MessageView; 25 | } -------------------------------------------------------------------------------- /test/units/main.js: -------------------------------------------------------------------------------- 1 | function main (require) { 2 | var $ = require('$'); 3 | 4 | $(function () { 5 | require('TemplateTest'); 6 | require('EventManagerTest'); 7 | require('SandboxTest'); 8 | require('CoreTest'); 9 | 10 | /*test("a basic test example", function() { 11 | ok( true, "this test is fine" ); 12 | var value = "hello"; 13 | equal( value, "hello", "We expect value to be hello" ); 14 | }); 15 | 16 | module("Module A"); 17 | 18 | test("first test within module", function() { 19 | ok( true, "all pass" ); 20 | }); 21 | 22 | test("second test within module", function() { 23 | ok( true, "all pass" ); 24 | }); 25 | 26 | module("Module B"); 27 | 28 | test("some other test", function() { 29 | expect(2); 30 | equal( true, false, "failing test" ); 31 | equal( true, true, "passing test" ); 32 | });*/ 33 | }); 34 | } -------------------------------------------------------------------------------- /lib/Template.js: -------------------------------------------------------------------------------- 1 | function Template() { 2 | /** 3 | * Simple JavaScript Templating 4 | * John Resig - http://ejohn.org/ - MIT Licensed 5 | * 6 | * @param {String} str template string 7 | * @param {Object} [data] template data 8 | * 9 | * @returns {Function|String} template or string 10 | */ 11 | return function (str, data) { 12 | var fn = new Function("obj", 13 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 14 | 15 | // Introduce the data as local variables using with(){} 16 | "with(obj||{}){p.push('" + 17 | 18 | // Convert the template into pure JavaScript 19 | String(str) 20 | .replace(/[\r\t\n]/g, " ") 21 | .split("{%").join("\t") 22 | .replace(/((^|%})[^\t]*)'/g, "$1\r") 23 | .replace(/\t=(.*?)%}/g, "',$1,'") 24 | .split("\t").join("');") 25 | .split("%}").join("p.push('") 26 | .split("\r").join("\\'") 27 | + "');}return p.join('');"); 28 | 29 | // Provide some basic currying to the user 30 | return data ? fn( data ) : fn; 31 | }; 32 | } -------------------------------------------------------------------------------- /test/units/TemplateTest.js: -------------------------------------------------------------------------------- 1 | function TemplateTest (require) { 2 | var ok = require('ok'), 3 | equal = require('equal'), 4 | test = require('test'), 5 | expect = require('expect'), 6 | module = require('module'), 7 | raises = require('raises'), 8 | 9 | templateFactory = require('Template'); 10 | 11 | var templateResult, 12 | template = '
    {%=a%}
    ', 13 | emptyTemplate = '
    '; 14 | 15 | module('Template'); 16 | 17 | test("basic test", function() { 18 | templateResult = templateFactory(template); 19 | ok(typeof templateResult === "function", "template factory should return a function if only template passed"); 20 | equal("
    123
    ", templateResult({a: 123}), "should work multiply times"); 21 | equal("
    123
    ", templateResult({a: 123, b: 123}), "should ignore extraparams"); 22 | 23 | equal("
    ", templateFactory(emptyTemplate)(), "template should work and return a string if no data passed"); 24 | 25 | ok(typeof templateFactory(template, {a: 1}) === "string", "template factory should return a string if template and data passed"); 26 | 27 | equal(templateFactory(emptyTemplate)(), templateFactory(emptyTemplate, 1), "sould be the same"); 28 | equal(templateFactory(template)({a: 123}), templateFactory(template, {a: 123}), "sould be the same"); 29 | 30 | raises(function () { 31 | templateFactory(template)(); 32 | }, "Should throw an error if in-template variable is not passed to data object") 33 | }); 34 | } -------------------------------------------------------------------------------- /lib/Sandbox.js: -------------------------------------------------------------------------------- 1 | function Sandbox(require) { 2 | var Core = require('Core'), 3 | EventManager = require('EventManager'); 4 | 5 | var uuid = 0; 6 | 7 | /** 8 | * @constructor 9 | * @param {Object} descriptor 10 | */ 11 | var Sandbox = function (descriptor) { 12 | this.descriptor = descriptor || {}; 13 | this.namespace = this.descriptor.name + ++uuid; 14 | }; 15 | 16 | /** 17 | * Gets module box 18 | * 19 | * @returns {HTMLElement|undefined} 20 | */ 21 | Sandbox.prototype.getBox = function () { 22 | return Core.getBox(this.descriptor.name); 23 | }; 24 | 25 | /** 26 | * Checks if module allowed to... 27 | * 28 | * @param {String} role... 29 | * 30 | * @returns {Boolean} 31 | */ 32 | Sandbox.prototype.is = function () { 33 | var acl = this.descriptor.acl; 34 | 35 | if (acl['*']) { 36 | return true; 37 | } 38 | 39 | for (var i = 0, c = arguments.length, role; i < c; i++) { 40 | role = arguments[i]; 41 | if (!(acl[role] || acl[role.split(':')[0] + ':*'])) { 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | }; 48 | 49 | /** 50 | * Binds to specific event 51 | * 52 | * @param {String} event 53 | * @param {Function} callback 54 | * 55 | * @returns {Sandbox} 56 | */ 57 | Sandbox.prototype.bind = function (event, callback) { 58 | if (this.is('listen:' + event)) { 59 | // Adds module name as namespace 60 | EventManager.bind(event, callback, this.namespace); 61 | } 62 | 63 | return this; 64 | }; 65 | 66 | /** 67 | * Unbinds specific event 68 | * 69 | * @param {String} event 70 | * @param {Function} [callback] 71 | * 72 | * @returns {Sandbox} 73 | */ 74 | Sandbox.prototype.unbind = function (event, callback) { 75 | if (this.is('listen:' + event)) { 76 | // Adds module name as namespace 77 | EventManager.unbind(event, callback, this.namespace); 78 | } 79 | 80 | return this; 81 | }; 82 | 83 | /** 84 | * Triggers specific event 85 | * 86 | * @param {String} event 87 | * @param data 88 | * 89 | * @returns {Sandbox} 90 | */ 91 | Sandbox.prototype.trigger = function (event, data) { 92 | if (this.is('trigger:' + event)) { 93 | EventManager.trigger(event, data); 94 | } 95 | 96 | return this; 97 | }; 98 | 99 | /** 100 | * gets locale string 101 | * 102 | * @param {String} message 103 | * 104 | * @returns {String} 105 | */ 106 | Sandbox.prototype.getText = function (message) { 107 | return Core.getText(this.descriptor.name, message); 108 | }; 109 | 110 | /** 111 | * gets module resource 112 | * 113 | * @param {String} resource 114 | * 115 | * @returns {Mixed} 116 | */ 117 | Sandbox.prototype.getResource = function (resource) { 118 | return this.descriptor.resources[resource]; 119 | }; 120 | 121 | /** 122 | * gets module template 123 | * 124 | * @param {String} templateSelector 125 | * 126 | * @returns {Function|undefined} 127 | */ 128 | Sandbox.prototype.getTemplate = function (templateSelector) { 129 | return Core.getTemplate(this.descriptor.name, templateSelector); 130 | }; 131 | 132 | // exports 133 | return Sandbox; 134 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scalable JavaScript Application Example 2 | ======================================= 3 | 4 | This application architecture is base on this: 5 | 6 | 1. Andrew Dupont (Gowalla, Prototype.js, S2) **Maintainable JavaScript** http://channel9.msdn.com/Events/MIX/MIX11/EXT23 7 | 2. Nicholas Zakas (Yahoo!, YUI, YUI Test) **Writing Maintainable JavaScript** http://www.yuiblog.com/blog/2007/05/25/video-zakas/ Slides: http://www.slideshare.net/nzakas/maintainable-javascript-2011 http://www.slideshare.net/nzakas/maintainable-javascript-1071179 8 | 3. **Scalable JavaScript Application Architecture** http://developer.yahoo.com/yui/theater/video.php?v=zakas-architecture 9 | 10 | The architecture of this application is described at Yandex.Subbotnik in Yekaterinburg 11 | 12 | Video(ru): http://video.yandex.ru/users/ya-events/view/291/#hq 13 | 14 | Slides(ru): http://www.slideshare.net/azproduction/making-scalable-javascript-application 15 | 16 | Depends 17 | ------- 18 | 19 | - LMD v1.3.0+ (https://github.com/azproduction/lmd) `npm install lmd -g` - required for build 20 | - jQuery or Zepto.js - required for core modules (see index.html) 21 | 22 | Build and run the example 23 | ------------------------- 24 | 25 | 1. `cd build && make` 26 | 2. open `index.html` 27 | 28 | **Build makes only js files** 29 | 30 | Running tests 31 | ------------- 32 | 33 | 1. `cd test && make` 34 | 2. open `test/index.html` 35 | 36 | Changelog 37 | --------- 38 | 39 | **v2.0** 40 | 41 | - **Modules as all application architecture are greatly simplified** 42 | - This app is now depend on LMD so all modules are synchronous 43 | - Added build system based on LMD (https://github.com/azproduction/lmd) 44 | - Core is finally splitted on 4 logical parts: Core, EventManager, Sandbox and Template engine 45 | - ModuleManager, loader and $script.js are wiped 46 | - Part of ModuleManager logic is moved to Core 47 | - All modules are loaded on startup (using lmd) so we no longer need a heavy loaders and $script.js 48 | - All application modules (not lib modules) are totally sandboxed (cant require at all) 49 | - Simple acl wildcards support `listen:*` and `*` but not `listen:ololo:*` 50 | 51 | **v2.1** (not compatable with v2.0) 52 | 53 | - SJSA no longer depend on jQuery nor Zepto 54 | - Sandbox#bind and Sandbox#trigger now accepts multiply event names 55 | - Sandbox#getBox now returns HTMLElement instead of jQuery/Zepto instance 56 | - Core.getBox uses getElementById instead of jQuery/Zepto selector 57 | - Sandbox#getTemplate now accept elementId instead of selector (all templates should be marked with id) 58 | - app descriptor can content list of safe modules (can require other modules) see `app/modules/Logger.js` for example 59 | - EventManager totally refactored see `test/units/EventManagerTest.js` for details 60 | 61 | Licence 62 | ------- 63 | 64 | (The MIT License) 65 | 66 | Copyright (c) 2011 Mikhail Davydov 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining 69 | a copy of this software and associated documentation files (the 70 | 'Software'), to deal in the Software without restriction, including 71 | without limitation the rights to use, copy, modify, merge, publish, 72 | distribute, sublicense, and/or sell copies of the Software, and to 73 | permit persons to whom the Software is furnished to do so, subject to 74 | the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be 77 | included in all copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 80 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 82 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 83 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 84 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 85 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/units/EventManagerTest.js: -------------------------------------------------------------------------------- 1 | function EventManagerTest (require) { 2 | var ok = require('ok'), 3 | equal = require('equal'), 4 | test = require('test'), 5 | expect = require('expect'), 6 | module = require('module'), 7 | raises = require('raises'); 8 | 9 | var EventManager = require('EventManager'); 10 | 11 | module('EventManager'); 12 | 13 | test("basic test", function() { 14 | expect(3); 15 | 16 | var data = {data: 'data'}; 17 | var namespace = 'mytestnamespace'; 18 | var listenterThatShouldFire = function (event) { 19 | ok(true, "event fired"); 20 | equal(data, event.data, 'should pass data'); 21 | equal('event1', event.type, 'should pass event name'); 22 | }; 23 | 24 | var listenterThatShouldndFire = function () { 25 | ok(false, "event shouldnt fire"); 26 | }; 27 | 28 | // EventManager.bind 29 | // EventManager.trigger 30 | EventManager.bind('event1', listenterThatShouldFire); 31 | EventManager.trigger('event1', data); // +2 32 | EventManager.unbind('event1', listenterThatShouldFire); 33 | 34 | // 35 | EventManager.bind('event2', listenterThatShouldndFire); 36 | EventManager.bind('event2', listenterThatShouldndFire, namespace); 37 | 38 | 39 | // EventManager.unbind 40 | EventManager.unbind('event2', namespace); 41 | EventManager.unbind('event2', listenterThatShouldndFire); 42 | EventManager.trigger('event2'); // 0 43 | 44 | // EventManager.unbind 45 | EventManager.unbind('event1', listenterThatShouldFire); 46 | EventManager.unbind('event1', namespace); 47 | EventManager.trigger('event1', {}); // 0 48 | EventManager.trigger('event2', {}); // 0 49 | 50 | 51 | }); 52 | 53 | test("bind test", function() { 54 | expect(7); 55 | 56 | var listenter = function (event) { 57 | ok(event.data.ok, "event fired"); 58 | }; 59 | 60 | var listenterThatShouldndFire = function (event) { 61 | ok(false, "event fired"); 62 | }; 63 | 64 | EventManager.bind(' event1 event2 event2 ', listenter); 65 | EventManager.bind('event1', listenter); 66 | EventManager.bind('event1', listenter, "namespace"); 67 | EventManager.trigger('event1 event2', {ok: true}); // 5 68 | 69 | EventManager.bind('event1', listenterThatShouldndFire, "namespace"); 70 | EventManager.unbind('event1', "namespace"); // should unbind listenterThatShouldndFire and listenter 71 | EventManager.trigger('event1', {ok: true}); // 2 72 | 73 | EventManager.unbind('event1 event2'); 74 | 75 | EventManager.trigger('event1 event2', {ok: false}); // 0 76 | }); 77 | 78 | test("bind namespace test", function() { 79 | expect(20); 80 | 81 | var listenter = function (event) { 82 | ok(event.data.ok, "event fired"); 83 | }; 84 | 85 | var listenterThatShouldndFire = function (event) { 86 | ok(false, "event fired"); 87 | }; 88 | 89 | EventManager.bind('event1', listenterThatShouldndFire, "namespace"); 90 | EventManager.bind('event2', listenterThatShouldndFire, "namespace"); 91 | EventManager.bind('event3', listenterThatShouldndFire, "namespace"); 92 | 93 | EventManager.unbindAllNs("namespace"); 94 | 95 | EventManager.trigger('event1 event2 event3', {ok: false}); // 0 96 | 97 | EventManager.bind('event1', listenter); 98 | EventManager.bind('event1', listenter); 99 | EventManager.bind('event1', listenter); 100 | EventManager.bind('event1', listenter, "namespace"); 101 | EventManager.bind('event1', listenter, "namespace"); 102 | EventManager.bind('event1', listenter, "namespace"); 103 | EventManager.bind('event1', listenter, "namespace2"); 104 | 105 | EventManager.trigger('event1', {ok: true}); // 7 106 | 107 | EventManager.unbind('event1', listenter); 108 | EventManager.trigger('event1', {ok: true}); // 6 109 | 110 | EventManager.unbind('event1'); 111 | EventManager.trigger('event1', {ok: true}); // 4 112 | 113 | EventManager.unbind('event1', listenter, "namespace"); 114 | EventManager.trigger('event1', {ok: true}); // 3 115 | 116 | EventManager.unbind('event1', "namespace"); 117 | EventManager.unbind('event1', "namespace2"); 118 | 119 | EventManager.trigger('event1', {ok: false}); // 0 120 | }); 121 | 122 | test("trigger test", function() { 123 | expect(2); 124 | EventManager.bind('event1', function () { 125 | throw new Error("Some error text"); 126 | }); 127 | 128 | EventManager.bind('event1', function () { 129 | ok(true, "event should fire after error"); 130 | }); 131 | 132 | raises(function () { 133 | EventManager.trigger('event1', {}); 134 | }, "shouldnt catch error"); 135 | 136 | EventManager.trigger('event1', {}, true); // 1 safe trigger 137 | 138 | EventManager.unbind('event1'); // cleanup 139 | }); 140 | 141 | test("one namespace trigger test", function() { 142 | expect(1); 143 | 144 | EventManager.bind('event1', function () { 145 | ok(true, "event should fire"); 146 | }, 'namespace'); 147 | 148 | EventManager.bind('event1', function () { 149 | ok(false, "event shouldnt fire"); 150 | }, 'namespace2'); 151 | 152 | EventManager.trigger('event1', {}, false, 'namespace'); // 1 153 | 154 | EventManager.unbind('event1'); // cleanup 155 | }); 156 | } -------------------------------------------------------------------------------- /test/vendors/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Fri Feb 17 10:40:01 UTC 2012 10 | * Last Commit: 21df3d147120e85cccae648e358b2f31fd915d4d 11 | */ 12 | 13 | /** Font Family and Sizes */ 14 | 15 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 16 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 20 | #qunit-tests { font-size: smaller; } 21 | 22 | 23 | /** Resets */ 24 | 25 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | 31 | /** Header */ 32 | 33 | #qunit-header { 34 | padding: 0.5em 0 0.5em 1em; 35 | 36 | color: #8699a4; 37 | background-color: #0d3349; 38 | 39 | font-size: 1.5em; 40 | line-height: 1em; 41 | font-weight: normal; 42 | 43 | border-radius: 15px 15px 0 0; 44 | -moz-border-radius: 15px 15px 0 0; 45 | -webkit-border-top-right-radius: 15px; 46 | -webkit-border-top-left-radius: 15px; 47 | } 48 | 49 | #qunit-header a { 50 | text-decoration: none; 51 | color: #c2ccd1; 52 | } 53 | 54 | #qunit-header a:hover, 55 | #qunit-header a:focus { 56 | color: #fff; 57 | } 58 | 59 | #qunit-header label { 60 | display: inline-block; 61 | } 62 | 63 | #qunit-banner { 64 | height: 5px; 65 | } 66 | 67 | #qunit-testrunner-toolbar { 68 | padding: 0.5em 0 0.5em 2em; 69 | color: #5E740B; 70 | background-color: #eee; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | 81 | /** Tests: Pass/Fail */ 82 | 83 | #qunit-tests { 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests li { 88 | padding: 0.4em 0.5em 0.4em 2.5em; 89 | border-bottom: 1px solid #fff; 90 | list-style-position: inside; 91 | } 92 | 93 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 94 | display: none; 95 | } 96 | 97 | #qunit-tests li strong { 98 | cursor: pointer; 99 | } 100 | 101 | #qunit-tests li a { 102 | padding: 0.5em; 103 | color: #c2ccd1; 104 | text-decoration: none; 105 | } 106 | #qunit-tests li a:hover, 107 | #qunit-tests li a:focus { 108 | color: #000; 109 | } 110 | 111 | #qunit-tests ol { 112 | margin-top: 0.5em; 113 | padding: 0.5em; 114 | 115 | background-color: #fff; 116 | 117 | border-radius: 15px; 118 | -moz-border-radius: 15px; 119 | -webkit-border-radius: 15px; 120 | 121 | box-shadow: inset 0px 2px 13px #999; 122 | -moz-box-shadow: inset 0px 2px 13px #999; 123 | -webkit-box-shadow: inset 0px 2px 13px #999; 124 | } 125 | 126 | #qunit-tests table { 127 | border-collapse: collapse; 128 | margin-top: .2em; 129 | } 130 | 131 | #qunit-tests th { 132 | text-align: right; 133 | vertical-align: top; 134 | padding: 0 .5em 0 0; 135 | } 136 | 137 | #qunit-tests td { 138 | vertical-align: top; 139 | } 140 | 141 | #qunit-tests pre { 142 | margin: 0; 143 | white-space: pre-wrap; 144 | word-wrap: break-word; 145 | } 146 | 147 | #qunit-tests del { 148 | background-color: #e0f2be; 149 | color: #374e0c; 150 | text-decoration: none; 151 | } 152 | 153 | #qunit-tests ins { 154 | background-color: #ffcaca; 155 | color: #500; 156 | text-decoration: none; 157 | } 158 | 159 | /*** Test Counts */ 160 | 161 | #qunit-tests b.counts { color: black; } 162 | #qunit-tests b.passed { color: #5E740B; } 163 | #qunit-tests b.failed { color: #710909; } 164 | 165 | #qunit-tests li li { 166 | margin: 0.5em; 167 | padding: 0.4em 0.5em 0.4em 0.5em; 168 | background-color: #fff; 169 | border-bottom: none; 170 | list-style-position: inside; 171 | } 172 | 173 | /*** Passing Styles */ 174 | 175 | #qunit-tests li li.pass { 176 | color: #5E740B; 177 | background-color: #fff; 178 | border-left: 26px solid #C6E746; 179 | } 180 | 181 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 182 | #qunit-tests .pass .test-name { color: #366097; } 183 | 184 | #qunit-tests .pass .test-actual, 185 | #qunit-tests .pass .test-expected { color: #999999; } 186 | 187 | #qunit-banner.qunit-pass { background-color: #C6E746; } 188 | 189 | /*** Failing Styles */ 190 | 191 | #qunit-tests li li.fail { 192 | color: #710909; 193 | background-color: #fff; 194 | border-left: 26px solid #EE5757; 195 | white-space: pre; 196 | } 197 | 198 | #qunit-tests > li:last-child { 199 | border-radius: 0 0 15px 15px; 200 | -moz-border-radius: 0 0 15px 15px; 201 | -webkit-border-bottom-right-radius: 15px; 202 | -webkit-border-bottom-left-radius: 15px; 203 | } 204 | 205 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 206 | #qunit-tests .fail .test-name, 207 | #qunit-tests .fail .module-name { color: #000000; } 208 | 209 | #qunit-tests .fail .test-actual { color: #EE5757; } 210 | #qunit-tests .fail .test-expected { color: green; } 211 | 212 | #qunit-banner.qunit-fail { background-color: #EE5757; } 213 | 214 | 215 | /** Result */ 216 | 217 | #qunit-testresult { 218 | padding: 0.5em 0.5em 0.5em 2.5em; 219 | 220 | color: #2b81af; 221 | background-color: #D2E0E6; 222 | 223 | border-bottom: 1px solid white; 224 | } 225 | 226 | /** Fixture */ 227 | 228 | #qunit-fixture { 229 | position: absolute; 230 | top: -10000px; 231 | left: -10000px; 232 | width: 1000px; 233 | height: 1000px; 234 | } 235 | -------------------------------------------------------------------------------- /lib/Core.js: -------------------------------------------------------------------------------- 1 | function Core(require, exports) { 2 | "use strict"; 3 | 4 | var templateFactory = require('Template'), 5 | Sandbox = require('Sandbox'), 6 | EventManager = require('EventManager'); 7 | 8 | var bind = function (func, context) { 9 | return function () { 10 | return func.apply(context, arguments); 11 | }; 12 | }; 13 | 14 | /** 15 | * @namespace 16 | */ 17 | var Core = { 18 | /** 19 | * Application descriptor 20 | * 21 | * @type Object 22 | */ 23 | descriptor: {}, 24 | 25 | /** 26 | * Modules descriptors 27 | * 28 | * @type Object 29 | */ 30 | descriptors: {}, 31 | 32 | /** 33 | * Modules locales 34 | * 35 | * @type Object 36 | */ 37 | locales: {}, 38 | 39 | /** 40 | * Modules templates 41 | * 42 | * @type Object 43 | */ 44 | templates: {}, 45 | 46 | /** 47 | * @type Object 48 | */ 49 | sandboxes: {}, 50 | 51 | /** 52 | * Starts app 53 | * 54 | * @params {Object} [data] 55 | * 56 | * @returns Core 57 | */ 58 | init: function (data) { 59 | data = data || {}; 60 | this.descriptor = data.descriptor || require('descriptor'); 61 | this.descriptor.modules = this.descriptor.modules || []; 62 | this.descriptor.layout = this.descriptor.layout || {}; 63 | this.descriptors = data.descriptors || require('descriptors'); 64 | this.templates = data.templates || require('templates'); 65 | this.locales = data.locales || require('locales'); 66 | 67 | this._initModules(); 68 | 69 | return this; 70 | }, 71 | 72 | /** 73 | * @private 74 | */ 75 | _initModules: function () { 76 | // Load all 77 | for (var i = 0, c = this.descriptor.modules.length; i < c; i++) { 78 | this.initModule(this.descriptor.modules[i]); 79 | } 80 | }, 81 | 82 | /** 83 | * Public version of _initModules 84 | * 85 | * @param {String} name 86 | * 87 | * @returns Core 88 | */ 89 | initModule: function (name) { 90 | if (this.sandboxes[name]) { 91 | return this; 92 | } 93 | var sandbox = new Sandbox(this.descriptors[name]); 94 | var module = require(name); 95 | this.sandboxes[name] = sandbox; 96 | 97 | new module(sandbox); 98 | 99 | return this; 100 | }, 101 | 102 | /** 103 | * Destroys module 104 | * 105 | * @param {String} name 106 | * 107 | * @returns Core 108 | */ 109 | destroyModule: function (name) { 110 | var sandbox = this.sandboxes[name]; 111 | if (sandbox) { 112 | EventManager.trigger('destroy', null, true, sandbox.namespace); 113 | 114 | // Cleanup 115 | EventManager.unbindAllNs(sandbox.namespace); 116 | var box = this.getBox(); 117 | if (box) { 118 | box.innerHTML = ''; 119 | } 120 | delete this.sandboxes[name]; 121 | } 122 | return this; 123 | }, 124 | 125 | /** 126 | * Get modules box if exists 127 | * 128 | * @param {String} name 129 | * 130 | * @returns {HTMLElement|null} 131 | */ 132 | getBox: function (name) { 133 | var elementId = this.descriptor.layout[name]; 134 | if (elementId) { 135 | return document.getElementById(elementId); 136 | } 137 | return null; 138 | }, 139 | 140 | /** 141 | * Gets module template 142 | * 143 | * @param {String} moduleName 144 | * @param {String} templateId 145 | * 146 | * @returns {Function|undefined} 147 | */ 148 | getTemplate: function (moduleName, templateId) { 149 | if (typeof this.templates[moduleName] === "string") { 150 | // wrap all templates 151 | var div = document.createElement('div'); 152 | div.innerHTML = this.templates[moduleName]; 153 | this.templates[moduleName] = div; 154 | } 155 | var templateElement = this._getElementById(this.templates[moduleName], templateId); 156 | return templateFactory(templateElement ? templateElement.innerHTML : ''); 157 | }, 158 | 159 | /** 160 | * polyfill 161 | */ 162 | _getElementById: function (element, elementId) { 163 | if (element.querySelector) { // modern browser 164 | return element.querySelector('#' + elementId); 165 | } 166 | 167 | var nodes = element.childNodes; 168 | for (var i = 0, c = nodes.length; i < c; i++) { 169 | if (nodes[i].getAttribute) { // its element 170 | if (nodes[i].getAttribute('id') === elementId) { 171 | return nodes[i]; 172 | } else { 173 | return this._getElementById(nodes[i], elementId); 174 | } 175 | } 176 | } 177 | return null; 178 | }, 179 | 180 | /** 181 | * gets locale string 182 | * 183 | * @param {String} moduleName 184 | * @param {String} message 185 | * 186 | * @returns {String} 187 | */ 188 | getText: function (moduleName, message) { 189 | var locale = this.locales[moduleName][message]; 190 | return (typeof locale === "object" ? locale[this.descriptor.locale] : locale) || message; 191 | } 192 | }; 193 | 194 | // --------------------------------------------------------------------------------------------------------------------- 195 | 196 | /** 197 | * Global Core object 198 | */ 199 | var coreExports = { 200 | trigger: bind(EventManager.trigger, EventManager), 201 | bind: bind(EventManager.bind, EventManager), 202 | unbind: bind(EventManager.unbind, EventManager), 203 | unbindAllNs: bind(EventManager.unbindAllNs, EventManager), 204 | 205 | init: bind(Core.init, Core), 206 | destroyModule: bind(Core.destroyModule, Core), 207 | initModule: bind(Core.initModule, Core), 208 | getTemplate: bind(Core.getTemplate, Core), 209 | getText: bind(Core.getText, Core), 210 | getBox: bind(Core.getBox, Core) 211 | }; 212 | 213 | // aliases 214 | coreExports.on = coreExports.bind; 215 | coreExports.off = coreExports.unbind; 216 | 217 | // exports 218 | for (var i in coreExports) { 219 | exports[i] = coreExports[i]; 220 | } 221 | } -------------------------------------------------------------------------------- /lib/EventManager.js: -------------------------------------------------------------------------------- 1 | function EventManager(require) { 2 | 3 | /** 4 | * @namespace 5 | */ 6 | return { 7 | /** 8 | * @type {Object} 9 | * 10 | * @format 11 | * 12 | * { 13 | * "eventName": { 14 | * "namespace": [function, ...], 15 | * ... 16 | * }, 17 | * ... 18 | * } 19 | */ 20 | eventsNs: {}, 21 | 22 | /** 23 | * 24 | * @param {String} eventName 25 | * @param {String} [eventNamespace] 26 | * 27 | * @returns {Array|Object} 28 | */ 29 | _getEventListNs: function (eventName, eventNamespace) { 30 | var self = this; 31 | if (!self.eventsNs[eventName]) { 32 | self.eventsNs[eventName] = {}; 33 | } 34 | 35 | if (eventNamespace) { 36 | if (!self.eventsNs[eventName][eventNamespace]) { 37 | self.eventsNs[eventName][eventNamespace] = []; 38 | } 39 | return self.eventsNs[eventName][eventNamespace]; 40 | } else { 41 | return self.eventsNs[eventName]; 42 | } 43 | }, 44 | 45 | /** 46 | * 47 | * @param {String} events 48 | * 49 | * @returnts {String[]} 50 | */ 51 | _parseEventsString: function (events) { 52 | var eventList = events.split(' '), 53 | result = []; 54 | 55 | // filter empty strings 56 | for (var i = 0, c = eventList.length; i < c; i++) { 57 | if (eventList[i]) { 58 | result.push(eventList[i]); 59 | } 60 | } 61 | 62 | return result; 63 | }, 64 | 65 | /** 66 | * @param {String} events event string list 67 | * @param {Array} data event data 68 | * @param {Boolean} [is_safe=false] do catch callback errors 69 | * @param {String} ns trigger for that namespace only 70 | * 71 | * @returns {EventManager} 72 | */ 73 | trigger: function (events, data, is_safe, ns) { 74 | if (typeof events === "string") { 75 | events = this._parseEventsString(events); 76 | 77 | // loop events 78 | for (var i = 0, c = events.length, eventListNs, eventList, eventName; i < c; i++) { 79 | eventName = events[i]; 80 | eventListNs = this._getEventListNs(eventName); // {"namespace": [listOfCallbacks, ...], ...} 81 | // loop namespaces 82 | for (var namespace in eventListNs) { 83 | if (ns && ns !== namespace) { 84 | continue; 85 | } 86 | if (eventListNs.hasOwnProperty(namespace)) { 87 | eventList = eventListNs[namespace]; // [listOfCallbacks, ...] 88 | // loop callbacks 89 | for (var j = 0, c1 = eventList.length, event; j < c1; j++) { 90 | event = {type: events[i], data: data}; 91 | // dont catch error 92 | if (is_safe) { 93 | try { 94 | eventList[j](event); 95 | } catch (e) {} 96 | } else { 97 | eventList[j](event); 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | return this; 105 | }, 106 | 107 | /** 108 | * 109 | * @param {String} events 110 | * @param {Function} callback 111 | * @param {String} [namespace="*"] 112 | * 113 | * bind(events, callback) bind callback to event of namespace '*' 114 | * bind(events, callback, namespace) bind callback to event of namespace 115 | * 116 | * @returns {EventManager} 117 | */ 118 | bind: function (events, callback, namespace) { 119 | if (typeof events === "string" && typeof callback === "function") { 120 | namespace = namespace || '*'; 121 | events = this._parseEventsString(events); 122 | 123 | for (var i = 0, c = events.length; i < c; i++) { 124 | this._getEventListNs(events[i], namespace).push(callback); 125 | } 126 | } 127 | return this; 128 | }, 129 | 130 | /** 131 | * 132 | * @param {String} events events 133 | * @param {Function|String} [callback] callback or namespace 134 | * @param {String} [namespace="*"] namespace or undefined 135 | * 136 | * @example 137 | * unbind(event) wipe all callbcaks of namespace '*' 138 | * unbind(event, function) remove one callback of event of namespace '*' 139 | * unbind(event, namespace) wipe all callbacks of event of namespace 140 | * unbind(event, function, namespace) remove one callback of event of namespace 141 | * 142 | * @returns {EventManager} 143 | */ 144 | unbind: function (events, callback, namespace) { 145 | if (typeof events === "string") { 146 | if (typeof callback === "string" && typeof namespace === "undefined") { 147 | namespace = callback; 148 | callback = void 0; 149 | } 150 | namespace = namespace || '*'; 151 | events = this._parseEventsString(events); 152 | for (var i = 0, c = events.length, eventList, callbackIndex; i < c; i++) { 153 | eventList = this._getEventListNs(events[i], namespace); 154 | if (callback) { 155 | callbackIndex = eventList.indexOf(callback); 156 | if (callbackIndex !== -1) { 157 | eventList.splice(callbackIndex, 1); 158 | } 159 | } else { 160 | eventList.splice(0); // wipe 161 | } 162 | } 163 | } 164 | return this; 165 | }, 166 | 167 | /** 168 | * Unbinds all events of selected namespace 169 | * 170 | * @param {String} namespace 171 | * 172 | * @returns {EventManager} 173 | */ 174 | unbindAllNs: function (namespace) { 175 | var eventListNs; 176 | for (var eventName in this.eventsNs) { 177 | if (this.eventsNs.hasOwnProperty(eventName)) { 178 | eventListNs = this.eventsNs[eventName]; 179 | if (eventListNs[namespace]) { 180 | eventListNs[namespace] = []; 181 | } 182 | } 183 | } 184 | return this; 185 | } 186 | }; 187 | } -------------------------------------------------------------------------------- /test/lmd.index.js: -------------------------------------------------------------------------------- 1 | (function (window, sandboxed_modules) { 2 | var modules = {}, 3 | initialized_modules = {}, 4 | require = function (moduleName) { 5 | var module = modules[moduleName], 6 | output; 7 | 8 | // Already inited - return as is 9 | if (initialized_modules[moduleName] && module) { 10 | return module; 11 | } 12 | 13 | // Lazy LMD module 14 | if (typeof module === "string") { 15 | module = (0, window.eval)(module); 16 | } 17 | 18 | // Predefine in case of recursive require 19 | output = {exports: {}}; 20 | initialized_modules[moduleName] = 1; 21 | modules[moduleName] = output.exports; 22 | 23 | if (!module) { 24 | // if undefined - try to pick up module from globals (like jQuery) 25 | module = window[moduleName]; 26 | } else if (typeof module === "function") { 27 | // Ex-Lazy LMD module or unpacked module ("pack": false) 28 | module = module(sandboxed_modules[moduleName] ? null : require, output.exports, output) || output.exports; 29 | } 30 | 31 | return modules[moduleName] = module; 32 | }, 33 | lmd = function (misc) { 34 | var output = {exports: {}}; 35 | switch (typeof misc) { 36 | case "function": 37 | misc(require, output.exports, output); 38 | break; 39 | case "object": 40 | for (var moduleName in misc) { 41 | // reset module init flag in case of overwriting 42 | initialized_modules[moduleName] = 0; 43 | modules[moduleName] = misc[moduleName]; 44 | } 45 | break; 46 | } 47 | return lmd; 48 | }; 49 | return lmd; 50 | })(window,{})({ 51 | "CoreTest": function CoreTest (require) { 52 | var ok = require('ok'), 53 | equal = require('equal'), 54 | test = require('test'), 55 | expect = require('expect'), 56 | module = require('module'); 57 | 58 | module('Core'); 59 | 60 | test("Core test", function() { 61 | ok(false, "implement"); 62 | }); 63 | }, 64 | "EventManagerTest": function EventManagerTest (require) { 65 | var ok = require('ok'), 66 | equal = require('equal'), 67 | test = require('test'), 68 | expect = require('expect'), 69 | module = require('module'), 70 | raises = require('raises'); 71 | 72 | var EventManager = require('EventManager'); 73 | 74 | module('EventManager'); 75 | 76 | test("basic test", function() { 77 | expect(3); 78 | 79 | var data = {data: 'data'}; 80 | var namespace = 'mytestnamespace'; 81 | var listenterThatShouldFire = function (event) { 82 | ok(true, "event fired"); 83 | equal(data, event.data, 'should pass data'); 84 | equal('event1', event.type, 'should pass event name'); 85 | }; 86 | 87 | var listenterThatShouldndFire = function () { 88 | ok(false, "event shouldnt fire"); 89 | }; 90 | 91 | // EventManager.bind 92 | // EventManager.trigger 93 | EventManager.bind('event1', listenterThatShouldFire); 94 | EventManager.trigger('event1', data); // +2 95 | EventManager.unbind('event1', listenterThatShouldFire); 96 | 97 | // 98 | EventManager.bind('event2', listenterThatShouldndFire); 99 | EventManager.bind('event2', listenterThatShouldndFire, namespace); 100 | 101 | 102 | // EventManager.unbind 103 | EventManager.unbind('event2', namespace); 104 | EventManager.unbind('event2', listenterThatShouldndFire); 105 | EventManager.trigger('event2'); // 0 106 | 107 | // EventManager.unbind 108 | EventManager.unbind('event1', listenterThatShouldFire); 109 | EventManager.unbind('event1', namespace); 110 | EventManager.trigger('event1', {}); // 0 111 | EventManager.trigger('event2', {}); // 0 112 | 113 | 114 | }); 115 | 116 | test("bind test", function() { 117 | expect(7); 118 | 119 | var listenter = function (event) { 120 | ok(event.data.ok, "event fired"); 121 | }; 122 | 123 | var listenterThatShouldndFire = function (event) { 124 | ok(false, "event fired"); 125 | }; 126 | 127 | EventManager.bind(' event1 event2 event2 ', listenter); 128 | EventManager.bind('event1', listenter); 129 | EventManager.bind('event1', listenter, "namespace"); 130 | EventManager.trigger('event1 event2', {ok: true}); // 5 131 | 132 | EventManager.bind('event1', listenterThatShouldndFire, "namespace"); 133 | EventManager.unbind('event1', "namespace"); // should unbind listenterThatShouldndFire and listenter 134 | EventManager.trigger('event1', {ok: true}); // 2 135 | 136 | EventManager.unbind('event1 event2'); 137 | 138 | EventManager.trigger('event1 event2', {ok: false}); // 0 139 | }); 140 | 141 | test("bind namespace test", function() { 142 | expect(20); 143 | 144 | var listenter = function (event) { 145 | ok(event.data.ok, "event fired"); 146 | }; 147 | 148 | var listenterThatShouldndFire = function (event) { 149 | ok(false, "event fired"); 150 | }; 151 | 152 | EventManager.bind('event1', listenterThatShouldndFire, "namespace"); 153 | EventManager.bind('event2', listenterThatShouldndFire, "namespace"); 154 | EventManager.bind('event3', listenterThatShouldndFire, "namespace"); 155 | 156 | EventManager.unbindAllNs("namespace"); 157 | 158 | EventManager.trigger('event1 event2 event3', {ok: false}); // 0 159 | 160 | EventManager.bind('event1', listenter); 161 | EventManager.bind('event1', listenter); 162 | EventManager.bind('event1', listenter); 163 | EventManager.bind('event1', listenter, "namespace"); 164 | EventManager.bind('event1', listenter, "namespace"); 165 | EventManager.bind('event1', listenter, "namespace"); 166 | EventManager.bind('event1', listenter, "namespace2"); 167 | 168 | EventManager.trigger('event1', {ok: true}); // 7 169 | 170 | EventManager.unbind('event1', listenter); 171 | EventManager.trigger('event1', {ok: true}); // 6 172 | 173 | EventManager.unbind('event1'); 174 | EventManager.trigger('event1', {ok: true}); // 4 175 | 176 | EventManager.unbind('event1', listenter, "namespace"); 177 | EventManager.trigger('event1', {ok: true}); // 3 178 | 179 | EventManager.unbind('event1', "namespace"); 180 | EventManager.unbind('event1', "namespace2"); 181 | 182 | EventManager.trigger('event1', {ok: false}); // 0 183 | }); 184 | 185 | test("trigger test", function() { 186 | expect(2); 187 | EventManager.bind('event1', function () { 188 | throw new Error("Some error text"); 189 | }); 190 | 191 | EventManager.bind('event1', function () { 192 | ok(true, "event should fire after error"); 193 | }); 194 | 195 | raises(function () { 196 | EventManager.trigger('event1', {}); 197 | }, "shouldnt catch error"); 198 | 199 | EventManager.trigger('event1', {}, true); // 1 safe trigger 200 | 201 | EventManager.unbind('event1'); // cleanup 202 | }); 203 | 204 | test("one namespace trigger test", function() { 205 | expect(1); 206 | 207 | EventManager.bind('event1', function () { 208 | ok(true, "event should fire"); 209 | }, 'namespace'); 210 | 211 | EventManager.bind('event1', function () { 212 | ok(false, "event shouldnt fire"); 213 | }, 'namespace2'); 214 | 215 | EventManager.trigger('event1', {}, false, 'namespace'); // 1 216 | 217 | EventManager.unbind('event1'); // cleanup 218 | }); 219 | }, 220 | "SandboxTest": function SandboxTest (require) { 221 | var ok = require('ok'), 222 | equal = require('equal'), 223 | test = require('test'), 224 | expect = require('expect'), 225 | module = require('module'); 226 | 227 | module('Sandbox'); 228 | 229 | test("Sandbox test", function() { 230 | ok(false, "implement"); 231 | }); 232 | }, 233 | "TemplateTest": function TemplateTest (require) { 234 | var ok = require('ok'), 235 | equal = require('equal'), 236 | test = require('test'), 237 | expect = require('expect'), 238 | module = require('module'), 239 | raises = require('raises'), 240 | 241 | templateFactory = require('Template'); 242 | 243 | var templateResult, 244 | template = '
    {%=a%}
    ', 245 | emptyTemplate = '
    '; 246 | 247 | module('Template'); 248 | 249 | test("basic test", function() { 250 | templateResult = templateFactory(template); 251 | ok(typeof templateResult === "function", "template factory should return a function if only template passed"); 252 | equal("
    123
    ", templateResult({a: 123}), "should work multiply times"); 253 | equal("
    123
    ", templateResult({a: 123, b: 123}), "should ignore extraparams"); 254 | 255 | equal("
    ", templateFactory(emptyTemplate)(), "template should work and return a string if no data passed"); 256 | 257 | ok(typeof templateFactory(template, {a: 1}) === "string", "template factory should return a string if template and data passed"); 258 | 259 | equal(templateFactory(emptyTemplate)(), templateFactory(emptyTemplate, 1), "sould be the same"); 260 | equal(templateFactory(template)({a: 123}), templateFactory(template, {a: 123}), "sould be the same"); 261 | 262 | raises(function () { 263 | templateFactory(template)(); 264 | }, "Should throw an error if in-template variable is not passed to data object") 265 | }); 266 | }, 267 | "Core": function Core(require, exports) { 268 | "use strict"; 269 | 270 | var templateFactory = require('Template'), 271 | Sandbox = require('Sandbox'), 272 | EventManager = require('EventManager'); 273 | 274 | var bind = function (func, context) { 275 | return function () { 276 | return func.apply(context, arguments); 277 | }; 278 | }; 279 | 280 | /** 281 | * @namespace 282 | */ 283 | var Core = { 284 | /** 285 | * Application descriptor 286 | * 287 | * @type Object 288 | */ 289 | descriptor: {}, 290 | 291 | /** 292 | * Modules descriptors 293 | * 294 | * @type Object 295 | */ 296 | descriptors: {}, 297 | 298 | /** 299 | * Modules locales 300 | * 301 | * @type Object 302 | */ 303 | locales: {}, 304 | 305 | /** 306 | * Modules templates 307 | * 308 | * @type Object 309 | */ 310 | templates: {}, 311 | 312 | /** 313 | * @type Object 314 | */ 315 | sandboxes: {}, 316 | 317 | /** 318 | * Starts app 319 | * 320 | * @params {Object} [data] 321 | * 322 | * @returns Core 323 | */ 324 | init: function (data) { 325 | data = data || {}; 326 | this.descriptor = data.descriptor || require('descriptor'); 327 | this.descriptor.modules = this.descriptor.modules || []; 328 | this.descriptor.layout = this.descriptor.layout || {}; 329 | this.descriptors = data.descriptors || require('descriptors'); 330 | this.templates = data.templates || require('templates'); 331 | this.locales = data.locales || require('locales'); 332 | 333 | this._initModules(); 334 | 335 | return this; 336 | }, 337 | 338 | /** 339 | * @private 340 | */ 341 | _initModules: function () { 342 | // Load all 343 | for (var i = 0, c = this.descriptor.modules.length; i < c; i++) { 344 | this.initModule(this.descriptor.modules[i]); 345 | } 346 | }, 347 | 348 | /** 349 | * Public version of _initModules 350 | * 351 | * @param {String} name 352 | * 353 | * @returns Core 354 | */ 355 | initModule: function (name) { 356 | if (this.sandboxes[name]) { 357 | return this; 358 | } 359 | var sandbox = new Sandbox(this.descriptors[name]); 360 | this.sandboxes[name] = sandbox; 361 | 362 | new require(name)(sandbox); 363 | 364 | return this; 365 | }, 366 | 367 | /** 368 | * Destroys module 369 | * 370 | * @param {String} name 371 | * 372 | * @returns Core 373 | */ 374 | destroyModule: function (name) { 375 | var sandbox = this.sandboxes[name]; 376 | if (sandbox) { 377 | EventManager.trigger('destroy', null, true, sandbox.namespace); 378 | 379 | // Cleanup 380 | EventManager.unbindAllNs(sandbox.namespace); 381 | var box = this.getBox(); 382 | if (box) { 383 | box.innerHTML = ''; 384 | } 385 | delete this.sandboxes[name]; 386 | } 387 | return this; 388 | }, 389 | 390 | /** 391 | * Get modules box if exists 392 | * 393 | * @param {String} name 394 | * 395 | * @returns {HTMLElement|null} 396 | */ 397 | getBox: function (name) { 398 | var elementId = this.descriptor.layout[name]; 399 | if (elementId) { 400 | return document.getElementById(elementId); 401 | } 402 | return null; 403 | }, 404 | 405 | /** 406 | * Gets module template 407 | * 408 | * @param {String} moduleName 409 | * @param {String} templateId 410 | * 411 | * @returns {Function|undefined} 412 | */ 413 | getTemplate: function (moduleName, templateId) { 414 | if (typeof this.templates[moduleName] === "string") { 415 | // wrap all templates 416 | var div = document.createElement('div'); 417 | div.innerHTML = this.templates[moduleName]; 418 | this.templates[moduleName] = div; 419 | } 420 | var templateElement = this._getElementById(this.templates[moduleName], templateId); 421 | return templateFactory(templateElement ? templateElement.innerHTML : ''); 422 | }, 423 | 424 | /** 425 | * polyfill 426 | */ 427 | _getElementById: function (element, elementId) { 428 | if (element.querySelector) { // modern browser 429 | return element.querySelector('#' + elementId); 430 | } 431 | 432 | var nodes = element.childNodes; 433 | for (var i = 0, c = nodes.length; i < c; i++) { 434 | if (nodes[i].getAttribute) { // its element 435 | if (nodes[i].getAttribute('id') === elementId) { 436 | return nodes[i]; 437 | } else { 438 | return this._getElementById(nodes[i], elementId); 439 | } 440 | } 441 | } 442 | return null; 443 | }, 444 | 445 | /** 446 | * gets locale string 447 | * 448 | * @param {String} moduleName 449 | * @param {String} message 450 | * 451 | * @returns {String} 452 | */ 453 | getText: function (moduleName, message) { 454 | var locale = this.locales[moduleName][message]; 455 | return (typeof locale === "object" ? locale[this.descriptor.locale] : locale) || message; 456 | } 457 | }; 458 | 459 | // --------------------------------------------------------------------------------------------------------------------- 460 | 461 | /** 462 | * Global Core object 463 | */ 464 | var coreExports = { 465 | trigger: bind(EventManager.trigger, EventManager), 466 | bind: bind(EventManager.bind, EventManager), 467 | unbind: bind(EventManager.unbind, EventManager), 468 | unbindAllNs: bind(EventManager.unbindAllNs, EventManager), 469 | 470 | init: bind(Core.init, Core), 471 | destroyModule: bind(Core.destroyModule, Core), 472 | initModule: bind(Core.initModule, Core), 473 | getTemplate: bind(Core.getTemplate, Core), 474 | getText: bind(Core.getText, Core), 475 | getBox: bind(Core.getBox, Core) 476 | }; 477 | 478 | // aliases 479 | coreExports.on = coreExports.bind; 480 | coreExports.off = coreExports.unbind; 481 | 482 | // exports 483 | for (var i in coreExports) { 484 | exports[i] = coreExports[i]; 485 | } 486 | }, 487 | "Sandbox": function Sandbox(require) { 488 | var Core = require('Core'), 489 | EventManager = require('EventManager'); 490 | 491 | var uuid = 0; 492 | 493 | /** 494 | * @constructor 495 | * @param {Object} descriptor 496 | */ 497 | var Sandbox = function (descriptor) { 498 | this.descriptor = descriptor || {}; 499 | this.namespace = this.descriptor.name + ++uuid; 500 | }; 501 | 502 | /** 503 | * Gets module box 504 | * 505 | * @returns {HTMLElement|undefined} 506 | */ 507 | Sandbox.prototype.getBox = function () { 508 | return Core.getBox(this.descriptor.name); 509 | }; 510 | 511 | /** 512 | * Checks if module allowed to... 513 | * 514 | * @param {String} role... 515 | * 516 | * @returns {Boolean} 517 | */ 518 | Sandbox.prototype.is = function () { 519 | var acl = this.descriptor.acl; 520 | 521 | if (acl['*']) { 522 | return true; 523 | } 524 | 525 | for (var i = 0, c = arguments.length, role; i < c; i++) { 526 | role = arguments[i]; 527 | if (!(acl[role] || acl[role.split(':')[0] + ':*'])) { 528 | return false; 529 | } 530 | } 531 | 532 | return true; 533 | }; 534 | 535 | /** 536 | * Binds to specific event 537 | * 538 | * @param {String} event 539 | * @param {Function} callback 540 | * 541 | * @returns {Sandbox} 542 | */ 543 | Sandbox.prototype.bind = function (event, callback) { 544 | if (this.is('listen:' + event)) { 545 | // Adds module name as namespace 546 | EventManager.bind(event, callback, this.namespace); 547 | } 548 | 549 | return this; 550 | }; 551 | 552 | /** 553 | * Unbinds specific event 554 | * 555 | * @param {String} event 556 | * @param {Function} [callback] 557 | * 558 | * @returns {Sandbox} 559 | */ 560 | Sandbox.prototype.unbind = function (event, callback) { 561 | if (this.is('listen:' + event)) { 562 | // Adds module name as namespace 563 | EventManager.unbind(event, callback, this.namespace); 564 | } 565 | 566 | return this; 567 | }; 568 | 569 | /** 570 | * Triggers specific event 571 | * 572 | * @param {String} event 573 | * @param data 574 | * 575 | * @returns {Sandbox} 576 | */ 577 | Sandbox.prototype.trigger = function (event, data) { 578 | if (this.is('trigger:' + event)) { 579 | EventManager.trigger(event, data); 580 | } 581 | 582 | return this; 583 | }; 584 | 585 | /** 586 | * gets locale string 587 | * 588 | * @param {String} message 589 | * 590 | * @returns {String} 591 | */ 592 | Sandbox.prototype.getText = function (message) { 593 | return Core.getText(this.descriptor.name, message); 594 | }; 595 | 596 | /** 597 | * gets module resource 598 | * 599 | * @param {String} resource 600 | * 601 | * @returns {Mixed} 602 | */ 603 | Sandbox.prototype.getResource = function (resource) { 604 | return this.descriptor.resources[resource]; 605 | }; 606 | 607 | /** 608 | * gets module template 609 | * 610 | * @param {String} templateSelector 611 | * 612 | * @returns {Function|undefined} 613 | */ 614 | Sandbox.prototype.getTemplate = function (templateSelector) { 615 | return Core.getTemplate(this.descriptor.name, templateSelector); 616 | }; 617 | 618 | // exports 619 | return Sandbox; 620 | }, 621 | "Template": function Template() { 622 | /** 623 | * Simple JavaScript Templating 624 | * John Resig - http://ejohn.org/ - MIT Licensed 625 | * 626 | * @param {String} str template string 627 | * @param {Object} [data] template data 628 | * 629 | * @returns {Function|String} template or string 630 | */ 631 | return function (str, data) { 632 | var fn = new Function("obj", 633 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 634 | 635 | // Introduce the data as local variables using with(){} 636 | "with(obj||{}){p.push('" + 637 | 638 | // Convert the template into pure JavaScript 639 | String(str) 640 | .replace(/[\r\t\n]/g, " ") 641 | .split("{%").join("\t") 642 | .replace(/((^|%})[^\t]*)'/g, "$1\r") 643 | .replace(/\t=(.*?)%}/g, "',$1,'") 644 | .split("\t").join("');") 645 | .split("%}").join("p.push('") 646 | .split("\r").join("\\'") 647 | + "');}return p.join('');"); 648 | 649 | // Provide some basic currying to the user 650 | return data ? fn( data ) : fn; 651 | }; 652 | }, 653 | "EventManager": function EventManager(require) { 654 | 655 | /** 656 | * @namespace 657 | */ 658 | return { 659 | /** 660 | * @type {Object} 661 | * 662 | * @format 663 | * 664 | * { 665 | * "eventName": { 666 | * "namespace": [function, ...], 667 | * ... 668 | * }, 669 | * ... 670 | * } 671 | */ 672 | eventsNs: {}, 673 | 674 | /** 675 | * 676 | * @param {String} eventName 677 | * @param {String} [eventNamespace] 678 | * 679 | * @returns {Array|Object} 680 | */ 681 | _getEventListNs: function (eventName, eventNamespace) { 682 | var self = this; 683 | if (!self.eventsNs[eventName]) { 684 | self.eventsNs[eventName] = {}; 685 | } 686 | 687 | if (eventNamespace) { 688 | if (!self.eventsNs[eventName][eventNamespace]) { 689 | self.eventsNs[eventName][eventNamespace] = []; 690 | } 691 | return self.eventsNs[eventName][eventNamespace]; 692 | } else { 693 | return self.eventsNs[eventName]; 694 | } 695 | }, 696 | 697 | /** 698 | * 699 | * @param {String} events 700 | * 701 | * @returnts {String[]} 702 | */ 703 | _parseEventsString: function (events) { 704 | var eventList = events.split(' '), 705 | result = []; 706 | 707 | // filter empty strings 708 | for (var i = 0, c = eventList.length; i < c; i++) { 709 | if (eventList[i]) { 710 | result.push(eventList[i]); 711 | } 712 | } 713 | 714 | return result; 715 | }, 716 | 717 | /** 718 | * @param {String} events event string list 719 | * @param {Array} data event data 720 | * @param {Boolean} [is_safe=false] do catch callback errors 721 | * @param {String} ns trigger for that namespace only 722 | * 723 | * @returns {EventManager} 724 | */ 725 | trigger: function (events, data, is_safe, ns) { 726 | if (typeof events === "string") { 727 | events = this._parseEventsString(events); 728 | 729 | // loop events 730 | for (var i = 0, c = events.length, eventListNs, eventList, eventName; i < c; i++) { 731 | eventName = events[i]; 732 | eventListNs = this._getEventListNs(eventName); // {"namespace": [listOfCallbacks, ...], ...} 733 | // loop namespaces 734 | for (var namespace in eventListNs) { 735 | if (ns && ns !== namespace) { 736 | continue; 737 | } 738 | if (eventListNs.hasOwnProperty(namespace)) { 739 | eventList = eventListNs[namespace]; // [listOfCallbacks, ...] 740 | // loop callbacks 741 | for (var j = 0, c1 = eventList.length, event; j < c1; j++) { 742 | event = {type: events[i], data: data}; 743 | // dont catch error 744 | if (is_safe) { 745 | try { 746 | eventList[j](event); 747 | } catch (e) {} 748 | } else { 749 | eventList[j](event); 750 | } 751 | } 752 | } 753 | } 754 | } 755 | } 756 | return this; 757 | }, 758 | 759 | /** 760 | * 761 | * @param {String} events 762 | * @param {Function} callback 763 | * @param {String} [namespace="*"] 764 | * 765 | * bind(events, callback) bind callback to event of namespace '*' 766 | * bind(events, callback, namespace) bind callback to event of namespace 767 | * 768 | * @returns {EventManager} 769 | */ 770 | bind: function (events, callback, namespace) { 771 | if (typeof events === "string" && typeof callback === "function") { 772 | namespace = namespace || '*'; 773 | events = this._parseEventsString(events); 774 | 775 | for (var i = 0, c = events.length; i < c; i++) { 776 | this._getEventListNs(events[i], namespace).push(callback); 777 | } 778 | } 779 | return this; 780 | }, 781 | 782 | /** 783 | * 784 | * @param {String} events events 785 | * @param {Function|String} [callback] callback or namespace 786 | * @param {String} [namespace="*"] namespace or undefined 787 | * 788 | * @example 789 | * unbind(event) wipe all callbcaks of namespace '*' 790 | * unbind(event, function) remove one callback of event of namespace '*' 791 | * unbind(event, namespace) wipe all callbacks of event of namespace 792 | * unbind(event, function, namespace) remove one callback of event of namespace 793 | * 794 | * @returns {EventManager} 795 | */ 796 | unbind: function (events, callback, namespace) { 797 | if (typeof events === "string") { 798 | if (typeof callback === "string" && typeof namespace === "undefined") { 799 | namespace = callback; 800 | callback = void 0; 801 | } 802 | namespace = namespace || '*'; 803 | events = this._parseEventsString(events); 804 | for (var i = 0, c = events.length, eventList, callbackIndex; i < c; i++) { 805 | eventList = this._getEventListNs(events[i], namespace); 806 | if (callback) { 807 | callbackIndex = eventList.indexOf(callback); 808 | if (callbackIndex !== -1) { 809 | eventList.splice(callbackIndex, 1); 810 | } 811 | } else { 812 | eventList.splice(0); // wipe 813 | } 814 | } 815 | } 816 | return this; 817 | }, 818 | 819 | /** 820 | * Unbinds all events of selected namespace 821 | * 822 | * @param {String} namespace 823 | * 824 | * @returns {EventManager} 825 | */ 826 | unbindAllNs: function (namespace) { 827 | var eventListNs; 828 | for (var eventName in this.eventsNs) { 829 | if (this.eventsNs.hasOwnProperty(eventName)) { 830 | eventListNs = this.eventsNs[eventName]; 831 | if (eventListNs[namespace]) { 832 | eventListNs[namespace] = []; 833 | } 834 | } 835 | } 836 | return this; 837 | } 838 | }; 839 | } 840 | })(function main (require) { 841 | var $ = require('$'); 842 | 843 | $(function () { 844 | require('TemplateTest'); 845 | require('EventManagerTest'); 846 | require('SandboxTest'); 847 | require('CoreTest'); 848 | 849 | /*test("a basic test example", function() { 850 | ok( true, "this test is fine" ); 851 | var value = "hello"; 852 | equal( value, "hello", "We expect value to be hello" ); 853 | }); 854 | 855 | module("Module A"); 856 | 857 | test("first test within module", function() { 858 | ok( true, "all pass" ); 859 | }); 860 | 861 | test("second test within module", function() { 862 | ok( true, "all pass" ); 863 | }); 864 | 865 | module("Module B"); 866 | 867 | test("some other test", function() { 868 | expect(2); 869 | equal( true, false, "failing test" ); 870 | equal( true, true, "passing test" ); 871 | });*/ 872 | }); 873 | }) -------------------------------------------------------------------------------- /test/vendors/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Fri Feb 17 10:40:01 UTC 2012 10 | * Last Commit: 21df3d147120e85cccae648e358b2f31fd915d4d 11 | */ 12 | 13 | (function(window) { 14 | 15 | var defined = { 16 | setTimeout: typeof window.setTimeout !== "undefined", 17 | sessionStorage: (function() { 18 | var x = "qunit-test-string"; 19 | try { 20 | sessionStorage.setItem(x, x); 21 | sessionStorage.removeItem(x); 22 | return true; 23 | } catch(e) { 24 | return false; 25 | } 26 | })() 27 | }; 28 | 29 | var testId = 0, 30 | toString = Object.prototype.toString, 31 | hasOwn = Object.prototype.hasOwnProperty; 32 | 33 | var Test = function(name, testName, expected, async, callback) { 34 | this.name = name; 35 | this.testName = testName; 36 | this.expected = expected; 37 | this.async = async; 38 | this.callback = callback; 39 | this.assertions = []; 40 | }; 41 | Test.prototype = { 42 | init: function() { 43 | var tests = id("qunit-tests"); 44 | if (tests) { 45 | var b = document.createElement("strong"); 46 | b.innerHTML = "Running " + this.name; 47 | var li = document.createElement("li"); 48 | li.appendChild( b ); 49 | li.className = "running"; 50 | li.id = this.id = "test-output" + testId++; 51 | tests.appendChild( li ); 52 | } 53 | }, 54 | setup: function() { 55 | if (this.module != config.previousModule) { 56 | if ( config.previousModule ) { 57 | runLoggingCallbacks('moduleDone', QUnit, { 58 | name: config.previousModule, 59 | failed: config.moduleStats.bad, 60 | passed: config.moduleStats.all - config.moduleStats.bad, 61 | total: config.moduleStats.all 62 | } ); 63 | } 64 | config.previousModule = this.module; 65 | config.moduleStats = { all: 0, bad: 0 }; 66 | runLoggingCallbacks( 'moduleStart', QUnit, { 67 | name: this.module 68 | } ); 69 | } else if (config.autorun) { 70 | runLoggingCallbacks( 'moduleStart', QUnit, { 71 | name: this.module 72 | } ); 73 | } 74 | 75 | config.current = this; 76 | this.testEnvironment = extend({ 77 | setup: function() {}, 78 | teardown: function() {} 79 | }, this.moduleTestEnvironment); 80 | 81 | runLoggingCallbacks( 'testStart', QUnit, { 82 | name: this.testName, 83 | module: this.module 84 | }); 85 | 86 | // allow utility functions to access the current test environment 87 | // TODO why?? 88 | QUnit.current_testEnvironment = this.testEnvironment; 89 | 90 | try { 91 | if ( !config.pollution ) { 92 | saveGlobal(); 93 | } 94 | 95 | this.testEnvironment.setup.call(this.testEnvironment); 96 | } catch(e) { 97 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 98 | } 99 | }, 100 | run: function() { 101 | config.current = this; 102 | if ( this.async ) { 103 | QUnit.stop(); 104 | } 105 | 106 | if ( config.notrycatch ) { 107 | this.callback.call(this.testEnvironment); 108 | return; 109 | } 110 | try { 111 | this.callback.call(this.testEnvironment); 112 | } catch(e) { 113 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 114 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 115 | // else next test will carry the responsibility 116 | saveGlobal(); 117 | 118 | // Restart the tests if they're blocking 119 | if ( config.blocking ) { 120 | QUnit.start(); 121 | } 122 | } 123 | }, 124 | teardown: function() { 125 | config.current = this; 126 | try { 127 | this.testEnvironment.teardown.call(this.testEnvironment); 128 | checkPollution(); 129 | } catch(e) { 130 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 131 | } 132 | }, 133 | finish: function() { 134 | config.current = this; 135 | if ( this.expected != null && this.expected != this.assertions.length ) { 136 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 137 | } 138 | 139 | var good = 0, bad = 0, 140 | tests = id("qunit-tests"); 141 | 142 | config.stats.all += this.assertions.length; 143 | config.moduleStats.all += this.assertions.length; 144 | 145 | if ( tests ) { 146 | var ol = document.createElement("ol"); 147 | 148 | for ( var i = 0; i < this.assertions.length; i++ ) { 149 | var assertion = this.assertions[i]; 150 | 151 | var li = document.createElement("li"); 152 | li.className = assertion.result ? "pass" : "fail"; 153 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 154 | ol.appendChild( li ); 155 | 156 | if ( assertion.result ) { 157 | good++; 158 | } else { 159 | bad++; 160 | config.stats.bad++; 161 | config.moduleStats.bad++; 162 | } 163 | } 164 | 165 | // store result when possible 166 | if ( QUnit.config.reorder && defined.sessionStorage ) { 167 | if (bad) { 168 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 169 | } else { 170 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 171 | } 172 | } 173 | 174 | if (bad == 0) { 175 | ol.style.display = "none"; 176 | } 177 | 178 | var b = document.createElement("strong"); 179 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 180 | 181 | var a = document.createElement("a"); 182 | a.innerHTML = "Rerun"; 183 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 184 | 185 | addEvent(b, "click", function() { 186 | var next = b.nextSibling.nextSibling, 187 | display = next.style.display; 188 | next.style.display = display === "none" ? "block" : "none"; 189 | }); 190 | 191 | addEvent(b, "dblclick", function(e) { 192 | var target = e && e.target ? e.target : window.event.srcElement; 193 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 194 | target = target.parentNode; 195 | } 196 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 197 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 198 | } 199 | }); 200 | 201 | var li = id(this.id); 202 | li.className = bad ? "fail" : "pass"; 203 | li.removeChild( li.firstChild ); 204 | li.appendChild( b ); 205 | li.appendChild( a ); 206 | li.appendChild( ol ); 207 | 208 | } else { 209 | for ( var i = 0; i < this.assertions.length; i++ ) { 210 | if ( !this.assertions[i].result ) { 211 | bad++; 212 | config.stats.bad++; 213 | config.moduleStats.bad++; 214 | } 215 | } 216 | } 217 | 218 | try { 219 | QUnit.reset(); 220 | } catch(e) { 221 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 222 | } 223 | 224 | runLoggingCallbacks( 'testDone', QUnit, { 225 | name: this.testName, 226 | module: this.module, 227 | failed: bad, 228 | passed: this.assertions.length - bad, 229 | total: this.assertions.length 230 | } ); 231 | }, 232 | 233 | queue: function() { 234 | var test = this; 235 | synchronize(function() { 236 | test.init(); 237 | }); 238 | function run() { 239 | // each of these can by async 240 | synchronize(function() { 241 | test.setup(); 242 | }); 243 | synchronize(function() { 244 | test.run(); 245 | }); 246 | synchronize(function() { 247 | test.teardown(); 248 | }); 249 | synchronize(function() { 250 | test.finish(); 251 | }); 252 | } 253 | // defer when previous test run passed, if storage is available 254 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 255 | if (bad) { 256 | run(); 257 | } else { 258 | synchronize(run, true); 259 | }; 260 | } 261 | 262 | }; 263 | 264 | var QUnit = { 265 | 266 | // call on start of module test to prepend name to all tests 267 | module: function(name, testEnvironment) { 268 | config.currentModule = name; 269 | config.currentModuleTestEnviroment = testEnvironment; 270 | }, 271 | 272 | asyncTest: function(testName, expected, callback) { 273 | if ( arguments.length === 2 ) { 274 | callback = expected; 275 | expected = null; 276 | } 277 | 278 | QUnit.test(testName, expected, callback, true); 279 | }, 280 | 281 | test: function(testName, expected, callback, async) { 282 | var name = '' + escapeInnerText(testName) + ''; 283 | 284 | if ( arguments.length === 2 ) { 285 | callback = expected; 286 | expected = null; 287 | } 288 | 289 | if ( config.currentModule ) { 290 | name = '' + config.currentModule + ": " + name; 291 | } 292 | 293 | if ( !validTest(config.currentModule + ": " + testName) ) { 294 | return; 295 | } 296 | 297 | var test = new Test(name, testName, expected, async, callback); 298 | test.module = config.currentModule; 299 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 300 | test.queue(); 301 | }, 302 | 303 | /** 304 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 305 | */ 306 | expect: function(asserts) { 307 | config.current.expected = asserts; 308 | }, 309 | 310 | /** 311 | * Asserts true. 312 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 313 | */ 314 | ok: function(a, msg) { 315 | if (!config.current) { 316 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 317 | } 318 | a = !!a; 319 | var details = { 320 | result: a, 321 | message: msg 322 | }; 323 | msg = escapeInnerText(msg); 324 | runLoggingCallbacks( 'log', QUnit, details ); 325 | config.current.assertions.push({ 326 | result: a, 327 | message: msg 328 | }); 329 | }, 330 | 331 | /** 332 | * Checks that the first two arguments are equal, with an optional message. 333 | * Prints out both actual and expected values. 334 | * 335 | * Prefered to ok( actual == expected, message ) 336 | * 337 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 338 | * 339 | * @param Object actual 340 | * @param Object expected 341 | * @param String message (optional) 342 | */ 343 | equal: function(actual, expected, message) { 344 | QUnit.push(expected == actual, actual, expected, message); 345 | }, 346 | 347 | notEqual: function(actual, expected, message) { 348 | QUnit.push(expected != actual, actual, expected, message); 349 | }, 350 | 351 | deepEqual: function(actual, expected, message) { 352 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 353 | }, 354 | 355 | notDeepEqual: function(actual, expected, message) { 356 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 357 | }, 358 | 359 | strictEqual: function(actual, expected, message) { 360 | QUnit.push(expected === actual, actual, expected, message); 361 | }, 362 | 363 | notStrictEqual: function(actual, expected, message) { 364 | QUnit.push(expected !== actual, actual, expected, message); 365 | }, 366 | 367 | raises: function(block, expected, message) { 368 | var actual, ok = false; 369 | 370 | if (typeof expected === 'string') { 371 | message = expected; 372 | expected = null; 373 | } 374 | 375 | try { 376 | block(); 377 | } catch (e) { 378 | actual = e; 379 | } 380 | 381 | if (actual) { 382 | // we don't want to validate thrown error 383 | if (!expected) { 384 | ok = true; 385 | // expected is a regexp 386 | } else if (QUnit.objectType(expected) === "regexp") { 387 | ok = expected.test(actual); 388 | // expected is a constructor 389 | } else if (actual instanceof expected) { 390 | ok = true; 391 | // expected is a validation function which returns true is validation passed 392 | } else if (expected.call({}, actual) === true) { 393 | ok = true; 394 | } 395 | } 396 | 397 | QUnit.ok(ok, message); 398 | }, 399 | 400 | start: function(count) { 401 | config.semaphore -= count || 1; 402 | if (config.semaphore > 0) { 403 | // don't start until equal number of stop-calls 404 | return; 405 | } 406 | if (config.semaphore < 0) { 407 | // ignore if start is called more often then stop 408 | config.semaphore = 0; 409 | } 410 | // A slight delay, to avoid any current callbacks 411 | if ( defined.setTimeout ) { 412 | window.setTimeout(function() { 413 | if (config.semaphore > 0) { 414 | return; 415 | } 416 | if ( config.timeout ) { 417 | clearTimeout(config.timeout); 418 | } 419 | 420 | config.blocking = false; 421 | process(true); 422 | }, 13); 423 | } else { 424 | config.blocking = false; 425 | process(true); 426 | } 427 | }, 428 | 429 | stop: function(count) { 430 | config.semaphore += count || 1; 431 | config.blocking = true; 432 | 433 | if ( config.testTimeout && defined.setTimeout ) { 434 | clearTimeout(config.timeout); 435 | config.timeout = window.setTimeout(function() { 436 | QUnit.ok( false, "Test timed out" ); 437 | config.semaphore = 1; 438 | QUnit.start(); 439 | }, config.testTimeout); 440 | } 441 | } 442 | }; 443 | 444 | //We want access to the constructor's prototype 445 | (function() { 446 | function F(){}; 447 | F.prototype = QUnit; 448 | QUnit = new F(); 449 | //Make F QUnit's constructor so that we can add to the prototype later 450 | QUnit.constructor = F; 451 | })(); 452 | 453 | // deprecated; still export them to window to provide clear error messages 454 | // next step: remove entirely 455 | QUnit.equals = function() { 456 | throw new Error("QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 457 | }; 458 | QUnit.same = function() { 459 | throw new Error("QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 460 | }; 461 | 462 | // Maintain internal state 463 | var config = { 464 | // The queue of tests to run 465 | queue: [], 466 | 467 | // block until document ready 468 | blocking: true, 469 | 470 | // when enabled, show only failing tests 471 | // gets persisted through sessionStorage and can be changed in UI via checkbox 472 | hidepassed: false, 473 | 474 | // by default, run previously failed tests first 475 | // very useful in combination with "Hide passed tests" checked 476 | reorder: true, 477 | 478 | // by default, modify document.title when suite is done 479 | altertitle: true, 480 | 481 | urlConfig: ['noglobals', 'notrycatch'], 482 | 483 | //logging callback queues 484 | begin: [], 485 | done: [], 486 | log: [], 487 | testStart: [], 488 | testDone: [], 489 | moduleStart: [], 490 | moduleDone: [] 491 | }; 492 | 493 | // Load paramaters 494 | (function() { 495 | var location = window.location || { search: "", protocol: "file:" }, 496 | params = location.search.slice( 1 ).split( "&" ), 497 | length = params.length, 498 | urlParams = {}, 499 | current; 500 | 501 | if ( params[ 0 ] ) { 502 | for ( var i = 0; i < length; i++ ) { 503 | current = params[ i ].split( "=" ); 504 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 505 | // allow just a key to turn on a flag, e.g., test.html?noglobals 506 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 507 | urlParams[ current[ 0 ] ] = current[ 1 ]; 508 | } 509 | } 510 | 511 | QUnit.urlParams = urlParams; 512 | config.filter = urlParams.filter; 513 | 514 | // Figure out if we're running the tests from a server or not 515 | QUnit.isLocal = !!(location.protocol === 'file:'); 516 | })(); 517 | 518 | // Expose the API as global variables, unless an 'exports' 519 | // object exists, in that case we assume we're in CommonJS 520 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 521 | extend(window, QUnit); 522 | window.QUnit = QUnit; 523 | } else { 524 | module.exports = QUnit; 525 | } 526 | 527 | // define these after exposing globals to keep them in these QUnit namespace only 528 | extend(QUnit, { 529 | config: config, 530 | 531 | // Initialize the configuration options 532 | init: function() { 533 | extend(config, { 534 | stats: { all: 0, bad: 0 }, 535 | moduleStats: { all: 0, bad: 0 }, 536 | started: +new Date, 537 | updateRate: 1000, 538 | blocking: false, 539 | autostart: true, 540 | autorun: false, 541 | filter: "", 542 | queue: [], 543 | semaphore: 0 544 | }); 545 | 546 | var qunit = id( "qunit" ); 547 | if ( qunit ) { 548 | qunit.innerHTML = 549 | '

    ' + document.title + '

    ' + 550 | '

    ' + 551 | '
    ' + 552 | '

    ' + 553 | '
      '; 554 | } 555 | 556 | var tests = id( "qunit-tests" ), 557 | banner = id( "qunit-banner" ), 558 | result = id( "qunit-testresult" ); 559 | 560 | if ( tests ) { 561 | tests.innerHTML = ""; 562 | } 563 | 564 | if ( banner ) { 565 | banner.className = ""; 566 | } 567 | 568 | if ( result ) { 569 | result.parentNode.removeChild( result ); 570 | } 571 | 572 | if ( tests ) { 573 | result = document.createElement( "p" ); 574 | result.id = "qunit-testresult"; 575 | result.className = "result"; 576 | tests.parentNode.insertBefore( result, tests ); 577 | result.innerHTML = 'Running...
       '; 578 | } 579 | }, 580 | 581 | /** 582 | * Resets the test setup. Useful for tests that modify the DOM. 583 | * 584 | * If jQuery is available, uses jQuery's replaceWith(), otherwise use replaceChild 585 | */ 586 | reset: function() { 587 | if ( window.jQuery ) { 588 | jQuery( "#qunit-fixture" ).replaceWith( config.fixture.cloneNode(true) ); 589 | } else { 590 | var main = id( 'qunit-fixture' ); 591 | if ( main ) { 592 | main.parentNode.replaceChild(config.fixture.cloneNode(true), main); 593 | } 594 | } 595 | }, 596 | 597 | /** 598 | * Trigger an event on an element. 599 | * 600 | * @example triggerEvent( document.body, "click" ); 601 | * 602 | * @param DOMElement elem 603 | * @param String type 604 | */ 605 | triggerEvent: function( elem, type, event ) { 606 | if ( document.createEvent ) { 607 | event = document.createEvent("MouseEvents"); 608 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 609 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 610 | elem.dispatchEvent( event ); 611 | 612 | } else if ( elem.fireEvent ) { 613 | elem.fireEvent("on"+type); 614 | } 615 | }, 616 | 617 | // Safe object type checking 618 | is: function( type, obj ) { 619 | return QUnit.objectType( obj ) == type; 620 | }, 621 | 622 | objectType: function( obj ) { 623 | if (typeof obj === "undefined") { 624 | return "undefined"; 625 | 626 | // consider: typeof null === object 627 | } 628 | if (obj === null) { 629 | return "null"; 630 | } 631 | 632 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 633 | 634 | switch (type) { 635 | case 'Number': 636 | if (isNaN(obj)) { 637 | return "nan"; 638 | } else { 639 | return "number"; 640 | } 641 | case 'String': 642 | case 'Boolean': 643 | case 'Array': 644 | case 'Date': 645 | case 'RegExp': 646 | case 'Function': 647 | return type.toLowerCase(); 648 | } 649 | if (typeof obj === "object") { 650 | return "object"; 651 | } 652 | return undefined; 653 | }, 654 | 655 | push: function(result, actual, expected, message) { 656 | if (!config.current) { 657 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 658 | } 659 | var details = { 660 | result: result, 661 | message: message, 662 | actual: actual, 663 | expected: expected 664 | }; 665 | 666 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 667 | message = '' + message + ""; 668 | var output = message; 669 | if (!result) { 670 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 671 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 672 | output += ''; 673 | if (actual != expected) { 674 | output += ''; 675 | output += ''; 676 | } 677 | var source = sourceFromStacktrace(); 678 | if (source) { 679 | details.source = source; 680 | output += ''; 681 | } 682 | output += "
      Expected:
      ' + expected + '
      Result:
      ' + actual + '
      Diff:
      ' + QUnit.diff(expected, actual) +'
      Source:
      ' + escapeInnerText(source) + '
      "; 683 | } 684 | 685 | runLoggingCallbacks( 'log', QUnit, details ); 686 | 687 | config.current.assertions.push({ 688 | result: !!result, 689 | message: output 690 | }); 691 | }, 692 | 693 | url: function( params ) { 694 | params = extend( extend( {}, QUnit.urlParams ), params ); 695 | var querystring = "?", 696 | key; 697 | for ( key in params ) { 698 | if ( !hasOwn.call( params, key ) ) { 699 | continue; 700 | } 701 | querystring += encodeURIComponent( key ) + "=" + 702 | encodeURIComponent( params[ key ] ) + "&"; 703 | } 704 | return window.location.pathname + querystring.slice( 0, -1 ); 705 | }, 706 | 707 | extend: extend, 708 | id: id, 709 | addEvent: addEvent 710 | }); 711 | 712 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 713 | //Doing this allows us to tell if the following methods have been overwritten on the actual 714 | //QUnit object, which is a deprecated way of using the callbacks. 715 | extend(QUnit.constructor.prototype, { 716 | // Logging callbacks; all receive a single argument with the listed properties 717 | // run test/logs.html for any related changes 718 | begin: registerLoggingCallback('begin'), 719 | // done: { failed, passed, total, runtime } 720 | done: registerLoggingCallback('done'), 721 | // log: { result, actual, expected, message } 722 | log: registerLoggingCallback('log'), 723 | // testStart: { name } 724 | testStart: registerLoggingCallback('testStart'), 725 | // testDone: { name, failed, passed, total } 726 | testDone: registerLoggingCallback('testDone'), 727 | // moduleStart: { name } 728 | moduleStart: registerLoggingCallback('moduleStart'), 729 | // moduleDone: { name, failed, passed, total } 730 | moduleDone: registerLoggingCallback('moduleDone') 731 | }); 732 | 733 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 734 | config.autorun = true; 735 | } 736 | 737 | QUnit.load = function() { 738 | runLoggingCallbacks( 'begin', QUnit, {} ); 739 | 740 | // Initialize the config, saving the execution queue 741 | var oldconfig = extend({}, config); 742 | QUnit.init(); 743 | extend(config, oldconfig); 744 | 745 | config.blocking = false; 746 | 747 | var urlConfigHtml = '', len = config.urlConfig.length; 748 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 749 | config[val] = QUnit.urlParams[val]; 750 | urlConfigHtml += ''; 751 | } 752 | 753 | var userAgent = id("qunit-userAgent"); 754 | if ( userAgent ) { 755 | userAgent.innerHTML = navigator.userAgent; 756 | } 757 | var banner = id("qunit-header"); 758 | if ( banner ) { 759 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 760 | addEvent( banner, "change", function( event ) { 761 | var params = {}; 762 | params[ event.target.name ] = event.target.checked ? true : undefined; 763 | window.location = QUnit.url( params ); 764 | }); 765 | } 766 | 767 | var toolbar = id("qunit-testrunner-toolbar"); 768 | if ( toolbar ) { 769 | var filter = document.createElement("input"); 770 | filter.type = "checkbox"; 771 | filter.id = "qunit-filter-pass"; 772 | addEvent( filter, "click", function() { 773 | var ol = document.getElementById("qunit-tests"); 774 | if ( filter.checked ) { 775 | ol.className = ol.className + " hidepass"; 776 | } else { 777 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 778 | ol.className = tmp.replace(/ hidepass /, " "); 779 | } 780 | if ( defined.sessionStorage ) { 781 | if (filter.checked) { 782 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 783 | } else { 784 | sessionStorage.removeItem("qunit-filter-passed-tests"); 785 | } 786 | } 787 | }); 788 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 789 | filter.checked = true; 790 | var ol = document.getElementById("qunit-tests"); 791 | ol.className = ol.className + " hidepass"; 792 | } 793 | toolbar.appendChild( filter ); 794 | 795 | var label = document.createElement("label"); 796 | label.setAttribute("for", "qunit-filter-pass"); 797 | label.innerHTML = "Hide passed tests"; 798 | toolbar.appendChild( label ); 799 | } 800 | 801 | var main = id('qunit-fixture'); 802 | if ( main ) { 803 | config.fixture = main.cloneNode(true); 804 | } 805 | 806 | if (config.autostart) { 807 | QUnit.start(); 808 | } 809 | }; 810 | 811 | addEvent(window, "load", QUnit.load); 812 | 813 | // addEvent(window, "error") gives us a useless event object 814 | window.onerror = function( message, file, line ) { 815 | if ( QUnit.config.current ) { 816 | ok( false, message + ", " + file + ":" + line ); 817 | } else { 818 | test( "global failure", function() { 819 | ok( false, message + ", " + file + ":" + line ); 820 | }); 821 | } 822 | }; 823 | 824 | function done() { 825 | config.autorun = true; 826 | 827 | // Log the last module results 828 | if ( config.currentModule ) { 829 | runLoggingCallbacks( 'moduleDone', QUnit, { 830 | name: config.currentModule, 831 | failed: config.moduleStats.bad, 832 | passed: config.moduleStats.all - config.moduleStats.bad, 833 | total: config.moduleStats.all 834 | } ); 835 | } 836 | 837 | var banner = id("qunit-banner"), 838 | tests = id("qunit-tests"), 839 | runtime = +new Date - config.started, 840 | passed = config.stats.all - config.stats.bad, 841 | html = [ 842 | 'Tests completed in ', 843 | runtime, 844 | ' milliseconds.
      ', 845 | '', 846 | passed, 847 | ' tests of ', 848 | config.stats.all, 849 | ' passed, ', 850 | config.stats.bad, 851 | ' failed.' 852 | ].join(''); 853 | 854 | if ( banner ) { 855 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 856 | } 857 | 858 | if ( tests ) { 859 | id( "qunit-testresult" ).innerHTML = html; 860 | } 861 | 862 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 863 | // show ✖ for good, ✔ for bad suite result in title 864 | // use escape sequences in case file gets loaded with non-utf-8-charset 865 | document.title = [ 866 | (config.stats.bad ? "\u2716" : "\u2714"), 867 | document.title.replace(/^[\u2714\u2716] /i, "") 868 | ].join(" "); 869 | } 870 | 871 | // clear own sessionStorage items if all tests passed 872 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 873 | for (var key in sessionStorage) { 874 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-") === 0 ) { 875 | sessionStorage.removeItem(key); 876 | } 877 | } 878 | } 879 | 880 | runLoggingCallbacks( 'done', QUnit, { 881 | failed: config.stats.bad, 882 | passed: passed, 883 | total: config.stats.all, 884 | runtime: runtime 885 | } ); 886 | } 887 | 888 | function validTest( name ) { 889 | var filter = config.filter, 890 | run = false; 891 | 892 | if ( !filter ) { 893 | return true; 894 | } 895 | 896 | var not = filter.charAt( 0 ) === "!"; 897 | if ( not ) { 898 | filter = filter.slice( 1 ); 899 | } 900 | 901 | if ( name.indexOf( filter ) !== -1 ) { 902 | return !not; 903 | } 904 | 905 | if ( not ) { 906 | run = true; 907 | } 908 | 909 | return run; 910 | } 911 | 912 | // so far supports only Firefox, Chrome and Opera (buggy) 913 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 914 | function sourceFromStacktrace(offset) { 915 | offset = offset || 3; 916 | try { 917 | throw new Error(); 918 | } catch ( e ) { 919 | if (e.stacktrace) { 920 | // Opera 921 | return e.stacktrace.split("\n")[offset + 3]; 922 | } else if (e.stack) { 923 | // Firefox, Chrome 924 | var stack = e.stack.split("\n"); 925 | if (/^error$/i.test(stack[0])) { 926 | stack.shift(); 927 | } 928 | return stack[offset]; 929 | } else if (e.sourceURL) { 930 | // Safari, PhantomJS 931 | // TODO sourceURL points at the 'throw new Error' line above, useless 932 | //return e.sourceURL + ":" + e.line; 933 | } 934 | } 935 | } 936 | 937 | function escapeInnerText(s) { 938 | if (!s) { 939 | return ""; 940 | } 941 | s = s + ""; 942 | return s.replace(/[\&<>]/g, function(s) { 943 | switch(s) { 944 | case "&": return "&"; 945 | case "<": return "<"; 946 | case ">": return ">"; 947 | default: return s; 948 | } 949 | }); 950 | } 951 | 952 | function synchronize( callback, last ) { 953 | config.queue.push( callback ); 954 | 955 | if ( config.autorun && !config.blocking ) { 956 | process(last); 957 | } 958 | } 959 | 960 | function process( last ) { 961 | var start = new Date().getTime(); 962 | config.depth = config.depth ? config.depth + 1 : 1; 963 | 964 | while ( config.queue.length && !config.blocking ) { 965 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 966 | config.queue.shift()(); 967 | } else { 968 | window.setTimeout( function(){ 969 | process( last ); 970 | }, 13 ); 971 | break; 972 | } 973 | } 974 | config.depth--; 975 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 976 | done(); 977 | } 978 | } 979 | 980 | function saveGlobal() { 981 | config.pollution = []; 982 | 983 | if ( config.noglobals ) { 984 | for ( var key in window ) { 985 | if ( !hasOwn.call( window, key ) ) { 986 | continue; 987 | } 988 | config.pollution.push( key ); 989 | } 990 | } 991 | } 992 | 993 | function checkPollution( name ) { 994 | var old = config.pollution; 995 | saveGlobal(); 996 | 997 | var newGlobals = diff( config.pollution, old ); 998 | if ( newGlobals.length > 0 ) { 999 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 1000 | } 1001 | 1002 | var deletedGlobals = diff( old, config.pollution ); 1003 | if ( deletedGlobals.length > 0 ) { 1004 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1005 | } 1006 | } 1007 | 1008 | // returns a new Array with the elements that are in a but not in b 1009 | function diff( a, b ) { 1010 | var result = a.slice(); 1011 | for ( var i = 0; i < result.length; i++ ) { 1012 | for ( var j = 0; j < b.length; j++ ) { 1013 | if ( result[i] === b[j] ) { 1014 | result.splice(i, 1); 1015 | i--; 1016 | break; 1017 | } 1018 | } 1019 | } 1020 | return result; 1021 | } 1022 | 1023 | function fail(message, exception, callback) { 1024 | if ( typeof console !== "undefined" && console.error && console.warn ) { 1025 | console.error(message); 1026 | console.error(exception); 1027 | console.error(exception.stack); 1028 | console.warn(callback.toString()); 1029 | 1030 | } else if ( window.opera && opera.postError ) { 1031 | opera.postError(message, exception, callback.toString); 1032 | } 1033 | } 1034 | 1035 | function extend(a, b) { 1036 | for ( var prop in b ) { 1037 | if ( b[prop] === undefined ) { 1038 | delete a[prop]; 1039 | 1040 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1041 | } else if ( prop !== "constructor" || a !== window ) { 1042 | a[prop] = b[prop]; 1043 | } 1044 | } 1045 | 1046 | return a; 1047 | } 1048 | 1049 | function addEvent(elem, type, fn) { 1050 | if ( elem.addEventListener ) { 1051 | elem.addEventListener( type, fn, false ); 1052 | } else if ( elem.attachEvent ) { 1053 | elem.attachEvent( "on" + type, fn ); 1054 | } else { 1055 | fn(); 1056 | } 1057 | } 1058 | 1059 | function id(name) { 1060 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1061 | document.getElementById( name ); 1062 | } 1063 | 1064 | function registerLoggingCallback(key){ 1065 | return function(callback){ 1066 | config[key].push( callback ); 1067 | }; 1068 | } 1069 | 1070 | // Supports deprecated method of completely overwriting logging callbacks 1071 | function runLoggingCallbacks(key, scope, args) { 1072 | //debugger; 1073 | var callbacks; 1074 | if ( QUnit.hasOwnProperty(key) ) { 1075 | QUnit[key].call(scope, args); 1076 | } else { 1077 | callbacks = config[key]; 1078 | for( var i = 0; i < callbacks.length; i++ ) { 1079 | callbacks[i].call( scope, args ); 1080 | } 1081 | } 1082 | } 1083 | 1084 | // Test for equality any JavaScript type. 1085 | // Author: Philippe Rathé 1086 | QUnit.equiv = function () { 1087 | 1088 | var innerEquiv; // the real equiv function 1089 | var callers = []; // stack to decide between skip/abort functions 1090 | var parents = []; // stack to avoiding loops from circular referencing 1091 | 1092 | // Call the o related callback with the given arguments. 1093 | function bindCallbacks(o, callbacks, args) { 1094 | var prop = QUnit.objectType(o); 1095 | if (prop) { 1096 | if (QUnit.objectType(callbacks[prop]) === "function") { 1097 | return callbacks[prop].apply(callbacks, args); 1098 | } else { 1099 | return callbacks[prop]; // or undefined 1100 | } 1101 | } 1102 | } 1103 | 1104 | var getProto = Object.getPrototypeOf || function (obj) { 1105 | return obj.__proto__; 1106 | }; 1107 | 1108 | var callbacks = function () { 1109 | 1110 | // for string, boolean, number and null 1111 | function useStrictEquality(b, a) { 1112 | if (b instanceof a.constructor || a instanceof b.constructor) { 1113 | // to catch short annotaion VS 'new' annotation of a 1114 | // declaration 1115 | // e.g. var i = 1; 1116 | // var j = new Number(1); 1117 | return a == b; 1118 | } else { 1119 | return a === b; 1120 | } 1121 | } 1122 | 1123 | return { 1124 | "string" : useStrictEquality, 1125 | "boolean" : useStrictEquality, 1126 | "number" : useStrictEquality, 1127 | "null" : useStrictEquality, 1128 | "undefined" : useStrictEquality, 1129 | 1130 | "nan" : function(b) { 1131 | return isNaN(b); 1132 | }, 1133 | 1134 | "date" : function(b, a) { 1135 | return QUnit.objectType(b) === "date" 1136 | && a.valueOf() === b.valueOf(); 1137 | }, 1138 | 1139 | "regexp" : function(b, a) { 1140 | return QUnit.objectType(b) === "regexp" 1141 | && a.source === b.source && // the regex itself 1142 | a.global === b.global && // and its modifers 1143 | // (gmi) ... 1144 | a.ignoreCase === b.ignoreCase 1145 | && a.multiline === b.multiline; 1146 | }, 1147 | 1148 | // - skip when the property is a method of an instance (OOP) 1149 | // - abort otherwise, 1150 | // initial === would have catch identical references anyway 1151 | "function" : function() { 1152 | var caller = callers[callers.length - 1]; 1153 | return caller !== Object && typeof caller !== "undefined"; 1154 | }, 1155 | 1156 | "array" : function(b, a) { 1157 | var i, j, loop; 1158 | var len; 1159 | 1160 | // b could be an object literal here 1161 | if (!(QUnit.objectType(b) === "array")) { 1162 | return false; 1163 | } 1164 | 1165 | len = a.length; 1166 | if (len !== b.length) { // safe and faster 1167 | return false; 1168 | } 1169 | 1170 | // track reference to avoid circular references 1171 | parents.push(a); 1172 | for (i = 0; i < len; i++) { 1173 | loop = false; 1174 | for (j = 0; j < parents.length; j++) { 1175 | if (parents[j] === a[i]) { 1176 | loop = true;// dont rewalk array 1177 | } 1178 | } 1179 | if (!loop && !innerEquiv(a[i], b[i])) { 1180 | parents.pop(); 1181 | return false; 1182 | } 1183 | } 1184 | parents.pop(); 1185 | return true; 1186 | }, 1187 | 1188 | "object" : function(b, a) { 1189 | var i, j, loop; 1190 | var eq = true; // unless we can proove it 1191 | var aProperties = [], bProperties = []; // collection of 1192 | // strings 1193 | 1194 | // comparing constructors is more strict than using 1195 | // instanceof 1196 | if (a.constructor !== b.constructor) { 1197 | // Allow objects with no prototype to be equivalent to 1198 | // objects with Object as their constructor. 1199 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1200 | (getProto(b) === null && getProto(a) === Object.prototype))) 1201 | { 1202 | return false; 1203 | } 1204 | } 1205 | 1206 | // stack constructor before traversing properties 1207 | callers.push(a.constructor); 1208 | // track reference to avoid circular references 1209 | parents.push(a); 1210 | 1211 | for (i in a) { // be strict: don't ensures hasOwnProperty 1212 | // and go deep 1213 | loop = false; 1214 | for (j = 0; j < parents.length; j++) { 1215 | if (parents[j] === a[i]) 1216 | loop = true; // don't go down the same path 1217 | // twice 1218 | } 1219 | aProperties.push(i); // collect a's properties 1220 | 1221 | if (!loop && !innerEquiv(a[i], b[i])) { 1222 | eq = false; 1223 | break; 1224 | } 1225 | } 1226 | 1227 | callers.pop(); // unstack, we are done 1228 | parents.pop(); 1229 | 1230 | for (i in b) { 1231 | bProperties.push(i); // collect b's properties 1232 | } 1233 | 1234 | // Ensures identical properties name 1235 | return eq 1236 | && innerEquiv(aProperties.sort(), bProperties 1237 | .sort()); 1238 | } 1239 | }; 1240 | }(); 1241 | 1242 | innerEquiv = function() { // can take multiple arguments 1243 | var args = Array.prototype.slice.apply(arguments); 1244 | if (args.length < 2) { 1245 | return true; // end transition 1246 | } 1247 | 1248 | return (function(a, b) { 1249 | if (a === b) { 1250 | return true; // catch the most you can 1251 | } else if (a === null || b === null || typeof a === "undefined" 1252 | || typeof b === "undefined" 1253 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1254 | return false; // don't lose time with error prone cases 1255 | } else { 1256 | return bindCallbacks(a, callbacks, [ b, a ]); 1257 | } 1258 | 1259 | // apply transition with (1..n) arguments 1260 | })(args[0], args[1]) 1261 | && arguments.callee.apply(this, args.splice(1, 1262 | args.length - 1)); 1263 | }; 1264 | 1265 | return innerEquiv; 1266 | 1267 | }(); 1268 | 1269 | /** 1270 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1271 | * http://flesler.blogspot.com Licensed under BSD 1272 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1273 | * 1274 | * @projectDescription Advanced and extensible data dumping for Javascript. 1275 | * @version 1.0.0 1276 | * @author Ariel Flesler 1277 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1278 | */ 1279 | QUnit.jsDump = (function() { 1280 | function quote( str ) { 1281 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1282 | }; 1283 | function literal( o ) { 1284 | return o + ''; 1285 | }; 1286 | function join( pre, arr, post ) { 1287 | var s = jsDump.separator(), 1288 | base = jsDump.indent(), 1289 | inner = jsDump.indent(1); 1290 | if ( arr.join ) 1291 | arr = arr.join( ',' + s + inner ); 1292 | if ( !arr ) 1293 | return pre + post; 1294 | return [ pre, inner + arr, base + post ].join(s); 1295 | }; 1296 | function array( arr, stack ) { 1297 | var i = arr.length, ret = Array(i); 1298 | this.up(); 1299 | while ( i-- ) 1300 | ret[i] = this.parse( arr[i] , undefined , stack); 1301 | this.down(); 1302 | return join( '[', ret, ']' ); 1303 | }; 1304 | 1305 | var reName = /^function (\w+)/; 1306 | 1307 | var jsDump = { 1308 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1309 | stack = stack || [ ]; 1310 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1311 | type = typeof parser; 1312 | var inStack = inArray(obj, stack); 1313 | if (inStack != -1) { 1314 | return 'recursion('+(inStack - stack.length)+')'; 1315 | } 1316 | //else 1317 | if (type == 'function') { 1318 | stack.push(obj); 1319 | var res = parser.call( this, obj, stack ); 1320 | stack.pop(); 1321 | return res; 1322 | } 1323 | // else 1324 | return (type == 'string') ? parser : this.parsers.error; 1325 | }, 1326 | typeOf:function( obj ) { 1327 | var type; 1328 | if ( obj === null ) { 1329 | type = "null"; 1330 | } else if (typeof obj === "undefined") { 1331 | type = "undefined"; 1332 | } else if (QUnit.is("RegExp", obj)) { 1333 | type = "regexp"; 1334 | } else if (QUnit.is("Date", obj)) { 1335 | type = "date"; 1336 | } else if (QUnit.is("Function", obj)) { 1337 | type = "function"; 1338 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1339 | type = "window"; 1340 | } else if (obj.nodeType === 9) { 1341 | type = "document"; 1342 | } else if (obj.nodeType) { 1343 | type = "node"; 1344 | } else if ( 1345 | // native arrays 1346 | toString.call( obj ) === "[object Array]" || 1347 | // NodeList objects 1348 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1349 | ) { 1350 | type = "array"; 1351 | } else { 1352 | type = typeof obj; 1353 | } 1354 | return type; 1355 | }, 1356 | separator:function() { 1357 | return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; 1358 | }, 1359 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1360 | if ( !this.multiline ) 1361 | return ''; 1362 | var chr = this.indentChar; 1363 | if ( this.HTML ) 1364 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1365 | return Array( this._depth_ + (extra||0) ).join(chr); 1366 | }, 1367 | up:function( a ) { 1368 | this._depth_ += a || 1; 1369 | }, 1370 | down:function( a ) { 1371 | this._depth_ -= a || 1; 1372 | }, 1373 | setParser:function( name, parser ) { 1374 | this.parsers[name] = parser; 1375 | }, 1376 | // The next 3 are exposed so you can use them 1377 | quote:quote, 1378 | literal:literal, 1379 | join:join, 1380 | // 1381 | _depth_: 1, 1382 | // This is the list of parsers, to modify them, use jsDump.setParser 1383 | parsers:{ 1384 | window: '[Window]', 1385 | document: '[Document]', 1386 | error:'[ERROR]', //when no parser is found, shouldn't happen 1387 | unknown: '[Unknown]', 1388 | 'null':'null', 1389 | 'undefined':'undefined', 1390 | 'function':function( fn ) { 1391 | var ret = 'function', 1392 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1393 | if ( name ) 1394 | ret += ' ' + name; 1395 | ret += '('; 1396 | 1397 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1398 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1399 | }, 1400 | array: array, 1401 | nodelist: array, 1402 | arguments: array, 1403 | object:function( map, stack ) { 1404 | var ret = [ ]; 1405 | QUnit.jsDump.up(); 1406 | for ( var key in map ) { 1407 | var val = map[key]; 1408 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); 1409 | } 1410 | QUnit.jsDump.down(); 1411 | return join( '{', ret, '}' ); 1412 | }, 1413 | node:function( node ) { 1414 | var open = QUnit.jsDump.HTML ? '<' : '<', 1415 | close = QUnit.jsDump.HTML ? '>' : '>'; 1416 | 1417 | var tag = node.nodeName.toLowerCase(), 1418 | ret = open + tag; 1419 | 1420 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1421 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1422 | if ( val ) 1423 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1424 | } 1425 | return ret + close + open + '/' + tag + close; 1426 | }, 1427 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1428 | var l = fn.length; 1429 | if ( !l ) return ''; 1430 | 1431 | var args = Array(l); 1432 | while ( l-- ) 1433 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1434 | return ' ' + args.join(', ') + ' '; 1435 | }, 1436 | key:quote, //object calls it internally, the key part of an item in a map 1437 | functionCode:'[code]', //function calls it internally, it's the content of the function 1438 | attribute:quote, //node calls it internally, it's an html attribute value 1439 | string:quote, 1440 | date:quote, 1441 | regexp:literal, //regex 1442 | number:literal, 1443 | 'boolean':literal 1444 | }, 1445 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1446 | id:'id', 1447 | name:'name', 1448 | 'class':'className' 1449 | }, 1450 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1451 | indentChar:' ',//indentation unit 1452 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1453 | }; 1454 | 1455 | return jsDump; 1456 | })(); 1457 | 1458 | // from Sizzle.js 1459 | function getText( elems ) { 1460 | var ret = "", elem; 1461 | 1462 | for ( var i = 0; elems[i]; i++ ) { 1463 | elem = elems[i]; 1464 | 1465 | // Get the text from text nodes and CDATA nodes 1466 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1467 | ret += elem.nodeValue; 1468 | 1469 | // Traverse everything else, except comment nodes 1470 | } else if ( elem.nodeType !== 8 ) { 1471 | ret += getText( elem.childNodes ); 1472 | } 1473 | } 1474 | 1475 | return ret; 1476 | }; 1477 | 1478 | //from jquery.js 1479 | function inArray( elem, array ) { 1480 | if ( array.indexOf ) { 1481 | return array.indexOf( elem ); 1482 | } 1483 | 1484 | for ( var i = 0, length = array.length; i < length; i++ ) { 1485 | if ( array[ i ] === elem ) { 1486 | return i; 1487 | } 1488 | } 1489 | 1490 | return -1; 1491 | } 1492 | 1493 | /* 1494 | * Javascript Diff Algorithm 1495 | * By John Resig (http://ejohn.org/) 1496 | * Modified by Chu Alan "sprite" 1497 | * 1498 | * Released under the MIT license. 1499 | * 1500 | * More Info: 1501 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1502 | * 1503 | * Usage: QUnit.diff(expected, actual) 1504 | * 1505 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1506 | */ 1507 | QUnit.diff = (function() { 1508 | function diff(o, n) { 1509 | var ns = {}; 1510 | var os = {}; 1511 | 1512 | for (var i = 0; i < n.length; i++) { 1513 | if (ns[n[i]] == null) 1514 | ns[n[i]] = { 1515 | rows: [], 1516 | o: null 1517 | }; 1518 | ns[n[i]].rows.push(i); 1519 | } 1520 | 1521 | for (var i = 0; i < o.length; i++) { 1522 | if (os[o[i]] == null) 1523 | os[o[i]] = { 1524 | rows: [], 1525 | n: null 1526 | }; 1527 | os[o[i]].rows.push(i); 1528 | } 1529 | 1530 | for (var i in ns) { 1531 | if ( !hasOwn.call( ns, i ) ) { 1532 | continue; 1533 | } 1534 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1535 | n[ns[i].rows[0]] = { 1536 | text: n[ns[i].rows[0]], 1537 | row: os[i].rows[0] 1538 | }; 1539 | o[os[i].rows[0]] = { 1540 | text: o[os[i].rows[0]], 1541 | row: ns[i].rows[0] 1542 | }; 1543 | } 1544 | } 1545 | 1546 | for (var i = 0; i < n.length - 1; i++) { 1547 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1548 | n[i + 1] == o[n[i].row + 1]) { 1549 | n[i + 1] = { 1550 | text: n[i + 1], 1551 | row: n[i].row + 1 1552 | }; 1553 | o[n[i].row + 1] = { 1554 | text: o[n[i].row + 1], 1555 | row: i + 1 1556 | }; 1557 | } 1558 | } 1559 | 1560 | for (var i = n.length - 1; i > 0; i--) { 1561 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1562 | n[i - 1] == o[n[i].row - 1]) { 1563 | n[i - 1] = { 1564 | text: n[i - 1], 1565 | row: n[i].row - 1 1566 | }; 1567 | o[n[i].row - 1] = { 1568 | text: o[n[i].row - 1], 1569 | row: i - 1 1570 | }; 1571 | } 1572 | } 1573 | 1574 | return { 1575 | o: o, 1576 | n: n 1577 | }; 1578 | } 1579 | 1580 | return function(o, n) { 1581 | o = o.replace(/\s+$/, ''); 1582 | n = n.replace(/\s+$/, ''); 1583 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1584 | 1585 | var str = ""; 1586 | 1587 | var oSpace = o.match(/\s+/g); 1588 | if (oSpace == null) { 1589 | oSpace = [" "]; 1590 | } 1591 | else { 1592 | oSpace.push(" "); 1593 | } 1594 | var nSpace = n.match(/\s+/g); 1595 | if (nSpace == null) { 1596 | nSpace = [" "]; 1597 | } 1598 | else { 1599 | nSpace.push(" "); 1600 | } 1601 | 1602 | if (out.n.length == 0) { 1603 | for (var i = 0; i < out.o.length; i++) { 1604 | str += '' + out.o[i] + oSpace[i] + ""; 1605 | } 1606 | } 1607 | else { 1608 | if (out.n[0].text == null) { 1609 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1610 | str += '' + out.o[n] + oSpace[n] + ""; 1611 | } 1612 | } 1613 | 1614 | for (var i = 0; i < out.n.length; i++) { 1615 | if (out.n[i].text == null) { 1616 | str += '' + out.n[i] + nSpace[i] + ""; 1617 | } 1618 | else { 1619 | var pre = ""; 1620 | 1621 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1622 | pre += '' + out.o[n] + oSpace[n] + ""; 1623 | } 1624 | str += " " + out.n[i].text + nSpace[i] + pre; 1625 | } 1626 | } 1627 | } 1628 | 1629 | return str; 1630 | }; 1631 | })(); 1632 | 1633 | // get at whatever the global object is, like window in browsers 1634 | })( (function() {return this}).call() ); 1635 | --------------------------------------------------------------------------------