├── .gitmodules ├── LICENSE.txt ├── Namespace.js ├── example ├── com │ └── sandbox │ │ ├── MyNameIsClass.js │ │ ├── SayHiClass.js │ │ └── classes.js ├── index.html └── sandbox.js └── test ├── assets ├── jsunittest.js └── unittest.css └── unit ├── fixtures └── a.js ├── index.html └── namespace_test.js /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/jsunittest"] 2 | path = vendor/jsunittest 3 | url = git://github.com/drnic/jsunittest.git 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | NAMESPACE.JS 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Maxime Bouroumeau-Fuseau 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /Namespace.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: Namespace.js 3 | Namespace utility 4 | 5 | Copyright: 6 | Copyright (c) 2009 Maxime Bouroumeau-Fuseau 7 | 8 | License: 9 | MIT-style license. 10 | 11 | Version: 12 | 1.1 13 | */ 14 | 15 | /*jslint evil : true */ 16 | /*global Namespace, XMLHttpRequest, ActiveXObject, window, document */ 17 | 18 | var Namespace = (function() { 19 | 20 | var _listeners = {}; 21 | var _includedIdentifiers = []; 22 | 23 | /** 24 | * Returns an object in an array unless the object is an array 25 | * 26 | * @param mixed obj 27 | * @return Array 28 | */ 29 | var _toArray = function(obj) { 30 | // checks if it's an array 31 | if (typeof(obj) == 'object' && obj.sort) { 32 | return obj; 33 | } 34 | return Array(obj); 35 | }; 36 | 37 | /** 38 | * Creates an XMLHttpRequest object 39 | * 40 | * @return XMLHttpRequest 41 | */ 42 | var _createXmlHttpRequest = function() { 43 | var xhr; 44 | try { xhr = new XMLHttpRequest(); } catch(e) { 45 | try { xhr = new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch(e2) { 46 | try { xhr = new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch(e3) { 47 | try { xhr = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e4) { 48 | try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e5) { 49 | throw new Error( "This browser does not support XMLHttpRequest." ); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | return xhr; 56 | }; 57 | 58 | /** 59 | * Checks if an http request is successful based on its status code. 60 | * Borrowed from dojo (http://www.dojotoolkit.org). 61 | * 62 | * @param Integer status Http status code 63 | * @return Boolean 64 | */ 65 | var _isHttpRequestSuccessful = function(status) { 66 | return (status >= 200 && status < 300) || // Boolean 67 | status == 304 || // allow any 2XX response code 68 | status == 1223 || // get it out of the cache 69 | (!status && (window.location.protocol == "file:" || window.location.protocol == "chrome:") ); // Internet Explorer mangled the status code 70 | }; 71 | 72 | /** 73 | * Creates a script tag with the specified data as content 74 | * 75 | * @param String data The content of the script 76 | */ 77 | var _createScript = function(data) { 78 | var script = document.createElement('script'); 79 | script.type = 'text/javascript'; 80 | script.text = data; 81 | 82 | if (typeof window.execScript === "object") { // According to IE 83 | window.execScript(data); 84 | } else { 85 | try { // Attempt body insertion 86 | document.body.appendChild(script); 87 | } catch (e) { // Fall back on eval 88 | window['eval'](data); 89 | } 90 | } 91 | }; 92 | 93 | /** 94 | * Dispatches an event 95 | * 96 | * @param String eventName 97 | * @param Object properties 98 | */ 99 | var _dispatchEvent = function(eventName, properties) { 100 | if (!_listeners[eventName]) { return; } 101 | properties.event = eventName; 102 | for (var i = 0; i < _listeners[eventName].length; i++) { 103 | _listeners[eventName][i](properties); 104 | } 105 | }; 106 | 107 | /** 108 | * Creates an Object following the specified namespace identifier. 109 | * 110 | * @public 111 | * @param String identifier The namespace string 112 | * @param Object klasses (OPTIONAL) An object which properties will be added to the namespace 113 | * @return Object The most inner object 114 | */ 115 | var _namespace = function(identifier) { 116 | var klasses = arguments[1] || false; 117 | var ns = window; 118 | 119 | if (identifier !== '') { 120 | var parts = identifier.split(Namespace.separator); 121 | for (var i = 0; i < parts.length; i++) { 122 | if (!ns[parts[i]]) { 123 | ns[parts[i]] = {}; 124 | } 125 | ns = ns[parts[i]]; 126 | } 127 | } 128 | 129 | if (klasses) { 130 | for (var klass in klasses) { 131 | if (klasses.hasOwnProperty(klass)) { 132 | ns[klass] = klasses[klass]; 133 | } 134 | } 135 | } 136 | 137 | _dispatchEvent('create', { 'identifier': identifier }); 138 | return ns; 139 | }; 140 | 141 | /** 142 | * Checks if the specified identifier is defined 143 | * 144 | * @public 145 | * @param String identifier The namespace string 146 | * @return Boolean 147 | */ 148 | _namespace.exist = function(identifier) { 149 | if (identifier === '') { return true; } 150 | 151 | var parts = identifier.split(Namespace.separator); 152 | var ns = window; 153 | for (var i = 0; i < parts.length; i++) { 154 | if (!ns[parts[i]]) { 155 | return false; 156 | } 157 | ns = ns[parts[i]]; 158 | } 159 | 160 | return true; 161 | }; 162 | 163 | /** 164 | * Maps an identifier to a uri. Is public so it can be overriden by custom scripts. 165 | * 166 | * @public 167 | * @param String identifier The namespace identifier 168 | * @return String The uri 169 | */ 170 | _namespace.mapIdentifierToUri = function(identifier) { 171 | var regexp = new RegExp('\\' + Namespace.separator, 'g'); 172 | return Namespace.baseUri + identifier.replace(regexp, '/') + '.js'; 173 | }; 174 | 175 | /** 176 | * Loads a remote script atfer mapping the identifier to an uri 177 | * 178 | * @param String identifier The namespace identifier 179 | * @param Function successCallback When set, the file will be loaded asynchronously. Will be called when the file is loaded 180 | * @param Function errorCallback Callback to be called when an error occurs 181 | * @return Boolean Success of failure when loading synchronously 182 | */ 183 | var _loadScript = function(identifier) { 184 | var successCallback = arguments[1]; 185 | var errorCallback = arguments[2]; 186 | var async = typeof successCallback === "function"; 187 | var uri = _namespace.mapIdentifierToUri(identifier); 188 | var event = { 'identifier': identifier, 'uri': uri, 'async': async, 'callback': successCallback }; 189 | 190 | var xhr = _createXmlHttpRequest(); 191 | xhr.open("GET", uri, async); 192 | 193 | if (async) { 194 | xhr.onreadystatechange = function() { 195 | if (xhr.readyState == 4) { 196 | if (_isHttpRequestSuccessful(xhr.status || 0)) { 197 | _createScript(xhr.responseText); 198 | _dispatchEvent('include', event); 199 | successCallback(); 200 | return; 201 | } 202 | event.status = xhr.status; 203 | _dispatchEvent('includeError', event); 204 | if (typeof errorCallback === "function") { 205 | errorCallback(); 206 | } 207 | } 208 | }; 209 | } 210 | 211 | xhr.send(null); 212 | 213 | if (!async) { 214 | if (_isHttpRequestSuccessful(xhr.status || 0)) { 215 | _createScript(xhr.responseText); 216 | _dispatchEvent('include', event); 217 | return true; 218 | } 219 | event.status = xhr.status; 220 | _dispatchEvent('includeError', event); 221 | return false; 222 | } 223 | }; 224 | 225 | /** 226 | * Includes a remote javascript file identified by the specified namespace string. The identifier 227 | * must point to a class. Separators in the string will be converted to slashes and the .js extension will be appended. 228 | * 229 | * @public 230 | * @param String identifier The namespace string 231 | * @param Function callback (OPTIONAL) A function to call when the remote script has been included 232 | */ 233 | _namespace.include = function(identifier) { 234 | var successCallback = arguments[1] || false; 235 | var errorCallback = arguments[2] || false; 236 | 237 | // checks if the identifier is not already included 238 | if (_includedIdentifiers[identifier]) { 239 | if (typeof successCallback === "function") { successCallback(); } 240 | return true; 241 | } 242 | 243 | if (successCallback) { 244 | _loadScript(identifier, function() { 245 | _includedIdentifiers[identifier] = true; 246 | successCallback(); 247 | }, errorCallback); 248 | } else { 249 | if (_loadScript(identifier)) { 250 | _includedIdentifiers[identifier] = true; 251 | return true; 252 | } 253 | return false; 254 | } 255 | }; 256 | 257 | /** 258 | * Imports properties from the specified namespace to the global space (ie. under window) 259 | * 260 | * The identifier string can contain the * wildcard character as its last segment (eg: com.test.*) 261 | * which will import all properties from the namespace. 262 | * 263 | * If not, the targeted namespace will be imported (ie. if com.test is imported, the test object 264 | * will now be global). If the targeted object is not found, it will be included using include(). 265 | * 266 | * @public 267 | * @param String identifier The namespace string 268 | * @param Function callback (OPTIONAL) A function to call when the process is completed (including the include() if used) 269 | * @param Boolean autoInclude (OPTIONAL) Whether to automatically auto include the targeted object is not found. Default is Namespace.autoInclude 270 | */ 271 | _namespace.use = function(identifier) { 272 | var identifiers = _toArray(identifier); 273 | var callback = arguments[1] || false; 274 | var autoInclude = arguments.length > 2 ? arguments[2] : Namespace.autoInclude; 275 | var event = { 'identifier': identifier }; 276 | var parts, target, ns; 277 | 278 | for (var i = 0; i < identifiers.length; i++) { 279 | identifier = identifiers[i]; 280 | 281 | parts = identifier.split(Namespace.separator); 282 | target = parts.pop(); 283 | ns = _namespace(parts.join(Namespace.separator)); 284 | 285 | if (target == '*') { 286 | // imports all objects from the identifier, can't use include() in that case 287 | for (var objectName in ns) { 288 | if (ns.hasOwnProperty(objectName)) { 289 | window[objectName] = ns[objectName]; 290 | } 291 | } 292 | } else { 293 | // imports only one object 294 | if (ns[target]) { 295 | // the object exists, import it 296 | window[target] = ns[target]; 297 | } else { 298 | // the object does not exist 299 | if (autoInclude) { 300 | // try to auto include it 301 | if (callback) { 302 | _namespace.include(identifier, function() { 303 | window[target] = ns[target]; 304 | 305 | if (i + 1 < identifiers.length) { 306 | // we continue to unpack the rest from here 307 | _namespace.unpack(identifiers.slice(i + 1), callback, autoInclude); 308 | } else { 309 | // no more identifiers to unpack 310 | _dispatchEvent('use', event); 311 | if (typeof callback === "function") { 312 | callback(); 313 | } 314 | } 315 | }); 316 | return; 317 | } else { 318 | _namespace.include(identifier); 319 | window[target] = ns[target]; 320 | } 321 | } 322 | } 323 | } 324 | 325 | } 326 | 327 | // all identifiers have been unpacked 328 | _dispatchEvent('use', event); 329 | if (typeof callback === "function") { callback(); } 330 | }; 331 | 332 | /** 333 | * Binds the include() and unpack() method to a specified identifier 334 | * 335 | * @public 336 | * @param String identifier The namespace identifier 337 | * @return Object 338 | */ 339 | _namespace.from = function(identifier) { 340 | return { 341 | /** 342 | * Includes the identifier specified in from() 343 | * 344 | * @see Namespace.include() 345 | */ 346 | include: function() { 347 | var callback = arguments[0] || false; 348 | _namespace.include(identifier, callback); 349 | }, 350 | /** 351 | * Includes the identifier specified in from() and unpack 352 | * the specified indentifier in _identifier 353 | * 354 | * @see Namespace.use() 355 | */ 356 | use: function(_identifier) { 357 | var callback = arguments[1] || false; 358 | if (_identifier.charAt(0) == '.') { 359 | _identifier = identifier + _identifier; 360 | } 361 | 362 | if (callback) { 363 | _namespace.include(identifier, function() { 364 | _namespace.use(_identifier, callback, false); 365 | }); 366 | } else { 367 | _namespace.include(identifier); 368 | _namespace.use(_identifier, callback, false); 369 | } 370 | } 371 | }; 372 | }; 373 | 374 | /** 375 | * Registers a namespace so it won't be included 376 | * 377 | * Idea and code submitted by Nathan Smith (http://github.com/smith) 378 | * 379 | * @param String|Array identifier 380 | */ 381 | _namespace.provide = function(identifier) { 382 | var identifiers = _toArray(identifier); 383 | 384 | for (var i = 0; i < identifiers.length; i++) { 385 | if (!(identifier in _includedIdentifiers)) { 386 | _dispatchEvent('provide', { 'identifier': identifier }); 387 | _includedIdentifiers[identifier] = true; 388 | } 389 | } 390 | }; 391 | 392 | /** 393 | * Registers a function to be called when the specified event is dispatched 394 | * 395 | * @param String eventName 396 | * @param Function callback 397 | */ 398 | _namespace.addEventListener = function(eventName, callback) { 399 | if (!_listeners[eventName]) { _listeners[eventName] = []; } 400 | _listeners[eventName].push(callback); 401 | }; 402 | 403 | /** 404 | * Unregisters an event listener 405 | * 406 | * @param String eventName 407 | * @param Function callback 408 | */ 409 | _namespace.removeEventListener = function(eventName, callback) { 410 | if (!_listeners[eventName]) { return; } 411 | for (var i = 0; i < _listeners[eventName].length; i++) { 412 | if (_listeners[eventName][i] == callback) { 413 | delete _listeners[eventName][i]; 414 | return; 415 | } 416 | } 417 | }; 418 | 419 | /** 420 | * Adds methods to javascript native's object 421 | * Inspired by http://thinkweb2.com/projects/prototype/namespacing-made-easy/ 422 | * 423 | * @public 424 | */ 425 | _namespace.registerNativeExtensions = function() { 426 | /** 427 | * @see Namespace() 428 | */ 429 | String.prototype.namespace = function() { 430 | var klasses = arguments[0] || {}; 431 | return _namespace(this.valueOf(), klasses); 432 | }; 433 | /** 434 | * @see Namespace.include() 435 | */ 436 | String.prototype.include = function() { 437 | var callback = arguments[0] || false; 438 | return _namespace.include(this.valueOf(), callback); 439 | }; 440 | /** 441 | * @see Namespace.use() 442 | */ 443 | String.prototype.use = function() { 444 | var callback = arguments[0] || false; 445 | return _namespace.use(this.valueOf(), callback); 446 | }; 447 | /** 448 | * @see Namespace.from() 449 | */ 450 | String.prototype.from = function() { 451 | return _namespace.from(this.valueOf()); 452 | }; 453 | /** 454 | * @see Namespace.provide() 455 | * Idea and code submitted by Nathan Smith (http://github.com/smith) 456 | */ 457 | String.prototype.provide = function() { 458 | return _namespace.provide(this.valueOf()); 459 | }; 460 | /** 461 | * @see Namespace.use() 462 | */ 463 | Array.prototype.use = function() { 464 | var callback = arguments[0] || false; 465 | return _namespace.use(this, callback); 466 | }; 467 | /** 468 | * @see Namespace.provide() 469 | */ 470 | Array.prototype.provide = function() { 471 | return _namespace.provide(this); 472 | }; 473 | }; 474 | 475 | return _namespace; 476 | })(); 477 | 478 | /** 479 | * Namespace segment separator 480 | * 481 | * @var String 482 | */ 483 | Namespace.separator = '.'; 484 | 485 | /** 486 | * Base uri when using Namespace.include() 487 | * Must end with a slash 488 | * 489 | * @var String 490 | */ 491 | Namespace.baseUri = './'; 492 | 493 | /** 494 | * Whether to automatically call Namespace.include() when Namespace.import() 495 | * does not find the targeted object. 496 | * 497 | * @var Boolean 498 | */ 499 | Namespace.autoInclude = true; 500 | 501 | -------------------------------------------------------------------------------- /example/com/sandbox/MyNameIsClass.js: -------------------------------------------------------------------------------- 1 | 2 | Namespace('com.sandbox', { 3 | 4 | MyNameIsClass: function(name) { 5 | com.sandbox.Console.log('my name is ' + name); 6 | return {}; 7 | } 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /example/com/sandbox/SayHiClass.js: -------------------------------------------------------------------------------- 1 | 2 | Namespace('com.sandbox', { 3 | 4 | SayHiClass: function() { 5 | com.sandbox.Console.log('hi'); 6 | return {}; 7 | } 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /example/com/sandbox/classes.js: -------------------------------------------------------------------------------- 1 | 2 | Namespace('com.sandbox.classes', { 3 | 4 | MyClass1: function() { 5 | com.sandbox.Console.log('MyClass1'); 6 | return {}; 7 | }, 8 | 9 | MyClass2: function() { 10 | com.sandbox.Console.log('MyClass2'); 11 | return {}; 12 | } 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sandbox 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/sandbox.js: -------------------------------------------------------------------------------- 1 | 2 | // creates an empty namespace 3 | Namespace('com.sandbox'); 4 | 5 | // creates or use a namespace and fill it with the specified properties 6 | Namespace('com.sandbox', { 7 | 8 | Console: { 9 | log: function(message) { 10 | var element = document.getElementById('console'); 11 | element.innerHTML = element.innerHTML + message + '
'; 12 | } 13 | } 14 | 15 | }); 16 | 17 | // calls Console using the fully qualified name (fqn) 18 | com.sandbox.Console.log('hello world'); 19 | 20 | // imports all properties from com.sandbox into the global space 21 | Namespace.use('com.sandbox.*'); 22 | Console.log('unpacked hello world'); 23 | 24 | // includes com/sandbox/SayHiClass.js 25 | // as we use a callback, file is loaded asynchronously 26 | Namespace.include('com.sandbox.SayHiClass', function() { 27 | new com.sandbox.SayHiClass(); 28 | }); 29 | 30 | // auto includes com/sandbox/MyNameIsClass.js file and imports MyNameIsClass into the global space 31 | // unpack() will use include(). will be async as we use a callback 32 | Namespace.use('com.sandbox.MyNameIsClass', function() { 33 | new MyNameIsClass('peter'); 34 | }); 35 | 36 | // imports all properties from com.sandbox.classes after including the file com/sandbox/classes.js 37 | // the use() identifier can also be relative to the identifier used in from() by starting 38 | // with a dot (would be .* in this case) 39 | Namespace.from('com.sandbox.classes').use('com.sandbox.classes.*', function() { 40 | new MyClass1(); 41 | new MyClass2(); 42 | }); 43 | 44 | // register a listener for the includeError event 45 | Namespace.addEventListener('includeError', function(event) { 46 | Console.log('an error occured loading ' + event.identifier); 47 | }); 48 | // try to include an inexistant file 49 | Namespace.include('com.sandbox.toto'); 50 | 51 | -------------------------------------------------------------------------------- /test/assets/jsunittest.js: -------------------------------------------------------------------------------- 1 | /* Jsunittest, version 0.7.2 2 | * (c) 2008 Dr Nic Williams 3 | * 4 | * Jsunittest is freely distributable under 5 | * the terms of an MIT-style license. 6 | * For details, see the web site: http://jsunittest.rubyforge.org 7 | * 8 | *--------------------------------------------------------------------------*/ 9 | 10 | var JsUnitTest = { 11 | Unit: {}, 12 | inspect: function(object) { 13 | try { 14 | if (typeof object == "undefined") return 'undefined'; 15 | if (object === null) return 'null'; 16 | if (typeof object == "string") { 17 | var useDoubleQuotes = arguments[1]; 18 | var escapedString = this.gsub(object, /[\x00-\x1f\\]/, function(match) { 19 | var character = String.specialChar[match[0]]; 20 | return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); 21 | }); 22 | if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; 23 | return "'" + escapedString.replace(/'/g, '\\\'') + "'"; 24 | }; 25 | return String(object); 26 | } catch (e) { 27 | if (e instanceof RangeError) return '...'; 28 | throw e; 29 | } 30 | }, 31 | $: function(element) { 32 | if (arguments.length > 1) { 33 | for (var i = 0, elements = [], length = arguments.length; i < length; i++) 34 | elements.push(this.$(arguments[i])); 35 | return elements; 36 | } 37 | if (typeof element == "string") 38 | element = document.getElementById(element); 39 | return element; 40 | }, 41 | gsub: function(source, pattern, replacement) { 42 | var result = '', match; 43 | replacement = arguments.callee.prepareReplacement(replacement); 44 | 45 | while (source.length > 0) { 46 | if (match = source.match(pattern)) { 47 | result += source.slice(0, match.index); 48 | result += JsUnitTest.String.interpret(replacement(match)); 49 | source = source.slice(match.index + match[0].length); 50 | } else { 51 | result += source, source = ''; 52 | } 53 | } 54 | return result; 55 | }, 56 | scan: function(source, pattern, iterator) { 57 | this.gsub(source, pattern, iterator); 58 | return String(source); 59 | }, 60 | escapeHTML: function(data) { 61 | return data.replace(/&/g,'&').replace(//g,'>'); 62 | }, 63 | arrayfromargs: function(args) { 64 | var myarray = new Array(); 65 | var i; 66 | 67 | for (i=0;i\s*/, 99 | adjacent: /^\s*\+\s*/, 100 | descendant: /^\s/, 101 | 102 | // selectors follow 103 | tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, 104 | id: /^#([\w\-\*]+)(\b|$)/, 105 | className: /^\.([\w\-\*]+)(\b|$)/, 106 | pseudo: 107 | /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, 108 | attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, 109 | attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ 110 | }; 111 | 112 | var assertions = { 113 | tagName: function(element, matches) { 114 | return matches[1].toUpperCase() == element.tagName.toUpperCase(); 115 | }, 116 | 117 | className: function(element, matches) { 118 | return Element.hasClassName(element, matches[1]); 119 | }, 120 | 121 | id: function(element, matches) { 122 | return element.id === matches[1]; 123 | }, 124 | 125 | attrPresence: function(element, matches) { 126 | return Element.hasAttribute(element, matches[1]); 127 | }, 128 | 129 | attr: function(element, matches) { 130 | var nodeValue = Element.readAttribute(element, matches[1]); 131 | return nodeValue && operators[matches[2]](nodeValue, matches[5] || matches[6]); 132 | } 133 | }; 134 | var e = this.expression, ps = patterns, as = assertions; 135 | var le, p, m; 136 | 137 | while (e && le !== e && (/\S/).test(e)) { 138 | le = e; 139 | for (var i in ps) { 140 | p = ps[i]; 141 | if (m = e.match(p)) { 142 | // use the Selector.assertions methods unless the selector 143 | // is too complex. 144 | if (as[i]) { 145 | tokens.push([i, Object.clone(m)]); 146 | e = e.replace(m[0], ''); 147 | } 148 | } 149 | } 150 | } 151 | 152 | var match = true, name, matches; 153 | for (var i = 0, token; token = tokens[i]; i++) { 154 | name = token[0], matches = token[1]; 155 | if (!assertions[name](element, matches)) { 156 | match = false; break; 157 | } 158 | } 159 | 160 | return match; 161 | }, 162 | toQueryParams: function(query, separator) { 163 | var query = query || window.location.search; 164 | var match = query.replace(/^\s+/, '').replace(/\s+$/, '').match(/([^?#]*)(#.*)?$/); 165 | if (!match) return { }; 166 | 167 | var hash = {}; 168 | var parts = match[1].split(separator || '&'); 169 | for (var i=0; i < parts.length; i++) { 170 | var pair = parts[i].split('='); 171 | if (pair[0]) { 172 | var key = decodeURIComponent(pair.shift()); 173 | var value = pair.length > 1 ? pair.join('=') : pair[0]; 174 | if (value != undefined) value = decodeURIComponent(value); 175 | 176 | if (key in hash) { 177 | var object = hash[key]; 178 | var isArray = object != null && typeof object == "object" && 179 | 'splice' in object && 'join' in object 180 | if (!isArray) hash[key] = [hash[key]]; 181 | hash[key].push(value); 182 | } 183 | else hash[key] = value; 184 | } 185 | }; 186 | return hash; 187 | }, 188 | 189 | String: { 190 | interpret: function(value) { 191 | return value == null ? '' : String(value); 192 | } 193 | } 194 | }; 195 | 196 | JsUnitTest.gsub.prepareReplacement = function(replacement) { 197 | if (typeof replacement == "function") return replacement; 198 | var template = new Template(replacement); 199 | return function(match) { return template.evaluate(match) }; 200 | }; 201 | 202 | JsUnitTest.Version = '0.7.2'; 203 | 204 | JsUnitTest.Template = function(template, pattern) { 205 | this.template = template; //template.toString(); 206 | this.pattern = pattern || JsUnitTest.Template.Pattern; 207 | }; 208 | 209 | JsUnitTest.Template.prototype.evaluate = function(object) { 210 | if (typeof object.toTemplateReplacements == "function") 211 | object = object.toTemplateReplacements(); 212 | 213 | return JsUnitTest.gsub(this.template, this.pattern, function(match) { 214 | if (object == null) return ''; 215 | 216 | var before = match[1] || ''; 217 | if (before == '\\') return match[2]; 218 | 219 | var ctx = object, expr = match[3]; 220 | var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; 221 | match = pattern.exec(expr); 222 | if (match == null) return before; 223 | 224 | while (match != null) { 225 | var comp = (match[1].indexOf('[]') === 0) ? match[2].gsub('\\\\]', ']') : match[1]; 226 | ctx = ctx[comp]; 227 | if (null == ctx || '' == match[3]) break; 228 | expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); 229 | match = pattern.exec(expr); 230 | } 231 | 232 | return before + JsUnitTest.String.interpret(ctx); 233 | }); 234 | } 235 | 236 | JsUnitTest.Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; 237 | JsUnitTest.Event = {}; 238 | // written by Dean Edwards, 2005 239 | // with input from Tino Zijdel, Matthias Miller, Diego Perini 240 | // namespaced by Dr Nic Williams 2008 241 | 242 | // http://dean.edwards.name/weblog/2005/10/add-event/ 243 | // http://dean.edwards.name/weblog/2005/10/add-event2/ 244 | JsUnitTest.Event.addEvent = function(element, type, handler) { 245 | if (element.addEventListener) { 246 | element.addEventListener(type, handler, false); 247 | } else { 248 | // assign each event handler a unique ID 249 | if (!handler.$$guid) handler.$$guid = JsUnitTest.Event.addEvent.guid++; 250 | // create a hash table of event types for the element 251 | if (!element.events) element.events = {}; 252 | // create a hash table of event handlers for each element/event pair 253 | var handlers = element.events[type]; 254 | if (!handlers) { 255 | handlers = element.events[type] = {}; 256 | // store the existing event handler (if there is one) 257 | if (element["on" + type]) { 258 | handlers[0] = element["on" + type]; 259 | } 260 | } 261 | // store the event handler in the hash table 262 | handlers[handler.$$guid] = handler; 263 | // assign a global event handler to do all the work 264 | element["on" + type] = this.handleEvent; 265 | } 266 | }; 267 | // a counter used to create unique IDs 268 | JsUnitTest.Event.addEvent.guid = 1; 269 | 270 | JsUnitTest.Event.removeEvent = function(element, type, handler) { 271 | if (element.removeEventListener) { 272 | element.removeEventListener(type, handler, false); 273 | } else { 274 | // delete the event handler from the hash table 275 | if (element.events && element.events[type]) { 276 | delete element.events[type][handler.$$guid]; 277 | } 278 | } 279 | }; 280 | 281 | JsUnitTest.Event.handleEvent = function(event) { 282 | var returnValue = true; 283 | // grab the event object (IE uses a global event object) 284 | event = event || JsUnitTest.Event.fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); 285 | // get a reference to the hash table of event handlers 286 | var handlers = this.events[event.type]; 287 | // execute each event handler 288 | for (var i in handlers) { 289 | this.$$handleEvent = handlers[i]; 290 | if (this.$$handleEvent(event) === false) { 291 | returnValue = false; 292 | } 293 | } 294 | return returnValue; 295 | }; 296 | 297 | JsUnitTest.Event.fixEvent = function(event) { 298 | // add W3C standard event methods 299 | event.preventDefault = this.fixEvent.preventDefault; 300 | event.stopPropagation = this.fixEvent.stopPropagation; 301 | return event; 302 | }; 303 | JsUnitTest.Event.fixEvent.preventDefault = function() { 304 | this.returnValue = false; 305 | }; 306 | JsUnitTest.Event.fixEvent.stopPropagation = function() { 307 | this.cancelBubble = true; 308 | }; 309 | 310 | JsUnitTest.Unit.Logger = function(element) { 311 | this.element = JsUnitTest.$(element); 312 | if (this.element) this._createLogTable(); 313 | }; 314 | 315 | JsUnitTest.Unit.Logger.prototype.start = function(testName) { 316 | if (!this.element) return; 317 | var tbody = this.element.getElementsByTagName('tbody')[0]; 318 | 319 | var tr = document.createElement('tr'); 320 | var td; 321 | 322 | //testname 323 | td = document.createElement('td'); 324 | td.appendChild(document.createTextNode(testName)); 325 | tr.appendChild(td) 326 | 327 | tr.appendChild(document.createElement('td'));//status 328 | tr.appendChild(document.createElement('td'));//message 329 | 330 | tbody.appendChild(tr); 331 | }; 332 | 333 | JsUnitTest.Unit.Logger.prototype.setStatus = function(status) { 334 | var logline = this.getLastLogLine(); 335 | logline.className = status; 336 | var statusCell = logline.getElementsByTagName('td')[1]; 337 | statusCell.appendChild(document.createTextNode(status)); 338 | }; 339 | 340 | JsUnitTest.Unit.Logger.prototype.finish = function(status, summary) { 341 | if (!this.element) return; 342 | this.setStatus(status); 343 | this.message(summary); 344 | }; 345 | 346 | JsUnitTest.Unit.Logger.prototype.message = function(message) { 347 | if (!this.element) return; 348 | var cell = this.getMessageCell(); 349 | 350 | // cell.appendChild(document.createTextNode(this._toHTML(message))); 351 | cell.innerHTML = this._toHTML(message); 352 | }; 353 | 354 | JsUnitTest.Unit.Logger.prototype.summary = function(summary) { 355 | if (!this.element) return; 356 | var div = this.element.getElementsByTagName('div')[0]; 357 | div.innerHTML = this._toHTML(summary); 358 | }; 359 | 360 | JsUnitTest.Unit.Logger.prototype.getLastLogLine = function() { 361 | var tbody = this.element.getElementsByTagName('tbody')[0]; 362 | var loglines = tbody.getElementsByTagName('tr'); 363 | return loglines[loglines.length - 1]; 364 | }; 365 | 366 | JsUnitTest.Unit.Logger.prototype.getMessageCell = function() { 367 | var logline = this.getLastLogLine(); 368 | return logline.getElementsByTagName('td')[2]; 369 | }; 370 | 371 | JsUnitTest.Unit.Logger.prototype._createLogTable = function() { 372 | var html = '
running...
' + 373 | '' + 374 | '' + 375 | '' + 376 | '
StatusTestMessage
'; 377 | this.element.innerHTML = html; 378 | }; 379 | 380 | JsUnitTest.Unit.Logger.prototype.appendActionButtons = function(actions) { 381 | // actions = $H(actions); 382 | // if (!actions.any()) return; 383 | // var div = new Element("div", {className: 'action_buttons'}); 384 | // actions.inject(div, function(container, action) { 385 | // var button = new Element("input").setValue(action.key).observe("click", action.value); 386 | // button.type = "button"; 387 | // return container.insert(button); 388 | // }); 389 | // this.getMessageCell().insert(div); 390 | }; 391 | 392 | JsUnitTest.Unit.Logger.prototype._toHTML = function(txt) { 393 | return JsUnitTest.escapeHTML(txt).replace(/\n/g,"
"); 394 | }; 395 | JsUnitTest.Unit.MessageTemplate = function(string) { 396 | var parts = []; 397 | var str = JsUnitTest.scan((string || ''), /(?=[^\\])\?|(?:\\\?|[^\?])+/, function(part) { 398 | parts.push(part[0]); 399 | }); 400 | this.parts = parts; 401 | }; 402 | 403 | JsUnitTest.Unit.MessageTemplate.prototype.evaluate = function(params) { 404 | var results = []; 405 | for (var i=0; i < this.parts.length; i++) { 406 | var part = this.parts[i]; 407 | var result = (part == '?') ? JsUnitTest.inspect(params.shift()) : part.replace(/\\\?/, '?'); 408 | results.push(result); 409 | }; 410 | return results.join(''); 411 | }; 412 | // A generic function for performming AJAX requests 413 | // It takes one argument, which is an object that contains a set of options 414 | // All of which are outline in the comments, below 415 | // From John Resig's book Pro JavaScript Techniques 416 | // published by Apress, 2006-8 417 | JsUnitTest.ajax = function( options ) { 418 | 419 | // Load the options object with defaults, if no 420 | // values were provided by the user 421 | options = { 422 | // The type of HTTP Request 423 | type: options.type || "POST", 424 | 425 | // The URL the request will be made to 426 | url: options.url || "", 427 | 428 | // How long to wait before considering the request to be a timeout 429 | timeout: options.timeout || 5000, 430 | 431 | // Functions to call when the request fails, succeeds, 432 | // or completes (either fail or succeed) 433 | onComplete: options.onComplete || function(){}, 434 | onError: options.onError || function(){}, 435 | onSuccess: options.onSuccess || function(){}, 436 | 437 | // The data type that'll be returned from the server 438 | // the default is simply to determine what data was returned from the 439 | // and act accordingly. 440 | data: options.data || "" 441 | }; 442 | 443 | // Create the request object 444 | var xml = window.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest(); 445 | 446 | // Open the asynchronous POST request 447 | xml.open(options.type, options.url, true); 448 | 449 | // We're going to wait for a request for 5 seconds, before giving up 450 | var timeoutLength = 5000; 451 | 452 | // Keep track of when the request has been succesfully completed 453 | var requestDone = false; 454 | 455 | // Initalize a callback which will fire 5 seconds from now, cancelling 456 | // the request (if it has not already occurred). 457 | setTimeout(function(){ 458 | requestDone = true; 459 | }, timeoutLength); 460 | 461 | // Watch for when the state of the document gets updated 462 | xml.onreadystatechange = function(){ 463 | // Wait until the data is fully loaded, 464 | // and make sure that the request hasn't already timed out 465 | if ( xml.readyState == 4 && !requestDone ) { 466 | 467 | // Check to see if the request was successful 468 | if ( httpSuccess( xml ) ) { 469 | 470 | // Execute the success callback with the data returned from the server 471 | options.onSuccess( httpData( xml, options.type ) ); 472 | 473 | // Otherwise, an error occurred, so execute the error callback 474 | } else { 475 | options.onError(); 476 | } 477 | 478 | // Call the completion callback 479 | options.onComplete(); 480 | 481 | // Clean up after ourselves, to avoid memory leaks 482 | xml = null; 483 | } 484 | }; 485 | 486 | // Establish the connection to the server 487 | xml.send(null); 488 | 489 | // Determine the success of the HTTP response 490 | function httpSuccess(r) { 491 | try { 492 | // If no server status is provided, and we're actually 493 | // requesting a local file, then it was successful 494 | return !r.status && location.protocol == "file:" || 495 | 496 | // Any status in the 200 range is good 497 | ( r.status >= 200 && r.status < 300 ) || 498 | 499 | // Successful if the document has not been modified 500 | r.status == 304 || 501 | 502 | // Safari returns an empty status if the file has not been modified 503 | navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined"; 504 | } catch(e){} 505 | 506 | // If checking the status failed, then assume that the request failed too 507 | return false; 508 | } 509 | 510 | // Extract the correct data from the HTTP response 511 | function httpData(r,type) { 512 | // Get the content-type header 513 | var ct = r.getResponseHeader("content-type"); 514 | 515 | // If no default type was provided, determine if some 516 | // form of XML was returned from the server 517 | var data = !type && ct && ct.indexOf("xml") >= 0; 518 | 519 | // Get the XML Document object if XML was returned from 520 | // the server, otherwise return the text contents returned by the server 521 | data = type == "xml" || data ? r.responseXML : r.responseText; 522 | 523 | // If the specified type is "script", execute the returned text 524 | // response as if it was JavaScript 525 | if ( type == "script" ) 526 | eval.call( window, data ); 527 | 528 | // Return the response data (either an XML Document or a text string) 529 | return data; 530 | } 531 | 532 | }; 533 | JsUnitTest.Unit.Assertions = { 534 | buildMessage: function(message, template) { 535 | var args = JsUnitTest.arrayfromargs(arguments).slice(2); 536 | return (message ? message + '\n' : '') + 537 | new JsUnitTest.Unit.MessageTemplate(template).evaluate(args); 538 | }, 539 | 540 | flunk: function(message) { 541 | this.assertBlock(message || 'Flunked', function() { return false }); 542 | }, 543 | 544 | assertBlock: function(message, block) { 545 | try { 546 | block.call(this) ? this.pass() : this.fail(message); 547 | } catch(e) { this.error(e) } 548 | }, 549 | 550 | assert: function(expression, message) { 551 | message = this.buildMessage(message || 'assert', 'got ', expression); 552 | this.assertBlock(message, function() { return expression }); 553 | }, 554 | 555 | assertEqual: function(expected, actual, message) { 556 | message = this.buildMessage(message || 'assertEqual', 'expected , actual: ', expected, actual); 557 | this.assertBlock(message, function() { return expected == actual }); 558 | }, 559 | 560 | assertNotEqual: function(expected, actual, message) { 561 | message = this.buildMessage(message || 'assertNotEqual', 'expected , actual: ', expected, actual); 562 | this.assertBlock(message, function() { return expected != actual }); 563 | }, 564 | 565 | assertEnumEqual: function(expected, actual, message) { 566 | message = this.buildMessage(message || 'assertEnumEqual', 'expected , actual: ', expected, actual); 567 | var expected_array = JsUnitTest.flattenArray(expected); 568 | var actual_array = JsUnitTest.flattenArray(actual); 569 | this.assertBlock(message, function() { 570 | if (expected_array.length == actual_array.length) { 571 | for (var i=0; i < expected_array.length; i++) { 572 | if (expected_array[i] != actual_array[i]) return false; 573 | }; 574 | return true; 575 | } 576 | return false; 577 | }); 578 | }, 579 | 580 | assertEnumNotEqual: function(expected, actual, message) { 581 | message = this.buildMessage(message || 'assertEnumNotEqual', ' was the same as ', expected, actual); 582 | var expected_array = JsUnitTest.flattenArray(expected); 583 | var actual_array = JsUnitTest.flattenArray(actual); 584 | this.assertBlock(message, function() { 585 | if (expected_array.length == actual_array.length) { 586 | for (var i=0; i < expected_array.length; i++) { 587 | if (expected_array[i] != actual_array[i]) return true; 588 | }; 589 | return false; 590 | } 591 | return true; 592 | }); 593 | }, 594 | 595 | assertHashEqual: function(expected, actual, message) { 596 | message = this.buildMessage(message || 'assertHashEqual', 'expected , actual: ', expected, actual); 597 | var expected_array = JsUnitTest.flattenArray(JsUnitTest.hashToSortedArray(expected)); 598 | var actual_array = JsUnitTest.flattenArray(JsUnitTest.hashToSortedArray(actual)); 599 | var block = function() { 600 | if (expected_array.length == actual_array.length) { 601 | for (var i=0; i < expected_array.length; i++) { 602 | if (expected_array[i] != actual_array[i]) return false; 603 | }; 604 | return true; 605 | } 606 | return false; 607 | }; 608 | this.assertBlock(message, block); 609 | }, 610 | 611 | assertHashNotEqual: function(expected, actual, message) { 612 | message = this.buildMessage(message || 'assertHashNotEqual', ' was the same as ', expected, actual); 613 | var expected_array = JsUnitTest.flattenArray(JsUnitTest.hashToSortedArray(expected)); 614 | var actual_array = JsUnitTest.flattenArray(JsUnitTest.hashToSortedArray(actual)); 615 | // from now we recursively zip & compare nested arrays 616 | var block = function() { 617 | if (expected_array.length == actual_array.length) { 618 | for (var i=0; i < expected_array.length; i++) { 619 | if (expected_array[i] != actual_array[i]) return true; 620 | }; 621 | return false; 622 | } 623 | return true; 624 | }; 625 | this.assertBlock(message, block); 626 | }, 627 | 628 | assertIdentical: function(expected, actual, message) { 629 | message = this.buildMessage(message || 'assertIdentical', 'expected , actual: ', expected, actual); 630 | this.assertBlock(message, function() { return expected === actual }); 631 | }, 632 | 633 | assertNotIdentical: function(expected, actual, message) { 634 | message = this.buildMessage(message || 'assertNotIdentical', 'expected , actual: ', expected, actual); 635 | this.assertBlock(message, function() { return expected !== actual }); 636 | }, 637 | 638 | assertNull: function(obj, message) { 639 | message = this.buildMessage(message || 'assertNull', 'got ', obj); 640 | this.assertBlock(message, function() { return obj === null }); 641 | }, 642 | 643 | assertNotNull: function(obj, message) { 644 | message = this.buildMessage(message || 'assertNotNull', 'got ', obj); 645 | this.assertBlock(message, function() { return obj !== null }); 646 | }, 647 | 648 | assertUndefined: function(obj, message) { 649 | message = this.buildMessage(message || 'assertUndefined', 'got ', obj); 650 | this.assertBlock(message, function() { return typeof obj == "undefined" }); 651 | }, 652 | 653 | assertNotUndefined: function(obj, message) { 654 | message = this.buildMessage(message || 'assertNotUndefined', 'got ', obj); 655 | this.assertBlock(message, function() { return typeof obj != "undefined" }); 656 | }, 657 | 658 | assertNullOrUndefined: function(obj, message) { 659 | message = this.buildMessage(message || 'assertNullOrUndefined', 'got ', obj); 660 | this.assertBlock(message, function() { return obj == null }); 661 | }, 662 | 663 | assertNotNullOrUndefined: function(obj, message) { 664 | message = this.buildMessage(message || 'assertNotNullOrUndefined', 'got ', obj); 665 | this.assertBlock(message, function() { return obj != null }); 666 | }, 667 | 668 | assertMatch: function(expected, actual, message) { 669 | message = this.buildMessage(message || 'assertMatch', 'regex did not match ', expected, actual); 670 | this.assertBlock(message, function() { return new RegExp(expected).exec(actual) }); 671 | }, 672 | 673 | assertNoMatch: function(expected, actual, message) { 674 | message = this.buildMessage(message || 'assertNoMatch', 'regex matched ', expected, actual); 675 | this.assertBlock(message, function() { return !(new RegExp(expected).exec(actual)) }); 676 | }, 677 | 678 | assertHasClass: function(element, klass, message) { 679 | element = JsUnitTest.$(element); 680 | message = this.buildMessage(message || 'assertHasClass', '? doesn\'t have class .', element, klass); 681 | this.assertBlock(message, function() { 682 | return !!element.className.match(new RegExp(klass)) 683 | }); 684 | }, 685 | 686 | assertHidden: function(element, message) { 687 | element = JsUnitTest.$(element); 688 | message = this.buildMessage(message || 'assertHidden', '? isn\'t hidden.', element); 689 | this.assertBlock(message, function() { return !element.style.display || element.style.display == 'none' }); 690 | }, 691 | 692 | assertInstanceOf: function(expected, actual, message) { 693 | message = this.buildMessage(message || 'assertInstanceOf', ' was not an instance of the expected type', actual); 694 | this.assertBlock(message, function() { return actual instanceof expected }); 695 | }, 696 | 697 | assertNotInstanceOf: function(expected, actual, message) { 698 | message = this.buildMessage(message || 'assertNotInstanceOf', ' was an instance of the expected type', actual); 699 | this.assertBlock(message, function() { return !(actual instanceof expected) }); 700 | }, 701 | 702 | assertRespondsTo: function(method, obj, message) { 703 | message = this.buildMessage(message || 'assertRespondsTo', 'object doesn\'t respond to ', method); 704 | this.assertBlock(message, function() { return (method in obj && typeof obj[method] == 'function') }); 705 | }, 706 | 707 | assertRaise: function(exceptionName, method, message) { 708 | message = this.buildMessage(message || 'assertRaise', ' exception expected but none was raised', exceptionName); 709 | var block = function() { 710 | try { 711 | method(); 712 | return false; 713 | } catch(e) { 714 | if (e.name == exceptionName) return true; 715 | else throw e; 716 | } 717 | }; 718 | this.assertBlock(message, block); 719 | }, 720 | 721 | assertNothingRaised: function(method, message) { 722 | try { 723 | method(); 724 | this.assert(true, "Expected nothing to be thrown"); 725 | } catch(e) { 726 | message = this.buildMessage(message || 'assertNothingRaised', ' was thrown when nothing was expected.', e); 727 | this.flunk(message); 728 | } 729 | }, 730 | 731 | _isVisible: function(element) { 732 | element = JsUnitTest.$(element); 733 | if(!element.parentNode) return true; 734 | this.assertNotNull(element); 735 | if(element.style && (element.style.display == 'none')) 736 | return false; 737 | 738 | return arguments.callee.call(this, element.parentNode); 739 | }, 740 | 741 | assertVisible: function(element, message) { 742 | message = this.buildMessage(message, '? was not visible.', element); 743 | this.assertBlock(message, function() { return this._isVisible(element) }); 744 | }, 745 | 746 | assertNotVisible: function(element, message) { 747 | message = this.buildMessage(message, '? was not hidden and didn\'t have a hidden parent either.', element); 748 | this.assertBlock(message, function() { return !this._isVisible(element) }); 749 | }, 750 | 751 | assertElementsMatch: function() { 752 | var pass = true, expressions = JsUnitTest.arrayfromargs(arguments); 753 | var elements = expressions.shift(); 754 | if (elements.length != expressions.length) { 755 | message = this.buildMessage('assertElementsMatch', 'size mismatch: ? elements, ? expressions (?).', elements.length, expressions.length, expressions); 756 | this.flunk(message); 757 | pass = false; 758 | } 759 | for (var i=0; i < expressions.length; i++) { 760 | var expression = expressions[i]; 761 | var element = JsUnitTest.$(elements[i]); 762 | if (JsUnitTest.selectorMatch(expression, element)) { 763 | pass = true; 764 | break; 765 | } 766 | message = this.buildMessage('assertElementsMatch', 'In index : expected but got ?', index, expression, element); 767 | this.flunk(message); 768 | pass = false; 769 | }; 770 | this.assert(pass, "Expected all elements to match."); 771 | }, 772 | 773 | assertElementMatches: function(element, expression, message) { 774 | this.assertElementsMatch([element], expression); 775 | } 776 | }; 777 | JsUnitTest.Unit.Runner = function(testcases) { 778 | var argumentOptions = arguments[1] || {}; 779 | var options = this.options = {}; 780 | options.testLog = ('testLog' in argumentOptions) ? argumentOptions.testLog : 'testlog'; 781 | options.resultsURL = this.queryParams.resultsURL; 782 | options.testLog = JsUnitTest.$(options.testLog); 783 | 784 | this.tests = this.getTests(testcases); 785 | this.currentTest = 0; 786 | this.logger = new JsUnitTest.Unit.Logger(options.testLog); 787 | 788 | var self = this; 789 | JsUnitTest.Event.addEvent(window, "load", function() { 790 | setTimeout(function() { 791 | self.runTests(); 792 | }, 0.1); 793 | }); 794 | }; 795 | 796 | JsUnitTest.Unit.Runner.prototype.queryParams = JsUnitTest.toQueryParams(); 797 | 798 | JsUnitTest.Unit.Runner.prototype.portNumber = function() { 799 | if (window.location.search.length > 0) { 800 | var matches = window.location.search.match(/\:(\d{3,5})\//); 801 | if (matches) { 802 | return parseInt(matches[1]); 803 | } 804 | } 805 | return null; 806 | }; 807 | 808 | JsUnitTest.Unit.Runner.prototype.getTests = function(testcases) { 809 | var tests = [], options = this.options; 810 | if (this.queryParams.tests) tests = this.queryParams.tests.split(','); 811 | else if (options.tests) tests = options.tests; 812 | else if (options.test) tests = [option.test]; 813 | else { 814 | for (testname in testcases) { 815 | if (testname.match(/^test/)) tests.push(testname); 816 | } 817 | } 818 | var results = []; 819 | for (var i=0; i < tests.length; i++) { 820 | var test = tests[i]; 821 | if (testcases[test]) 822 | results.push( 823 | new JsUnitTest.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown) 824 | ); 825 | }; 826 | return results; 827 | }; 828 | 829 | JsUnitTest.Unit.Runner.prototype.getResult = function() { 830 | var results = { 831 | tests: this.tests.length, 832 | assertions: 0, 833 | failures: 0, 834 | errors: 0, 835 | warnings: 0 836 | }; 837 | 838 | for (var i=0; i < this.tests.length; i++) { 839 | var test = this.tests[i]; 840 | results.assertions += test.assertions; 841 | results.failures += test.failures; 842 | results.errors += test.errors; 843 | results.warnings += test.warnings; 844 | }; 845 | return results; 846 | }; 847 | 848 | JsUnitTest.Unit.Runner.prototype.postResults = function() { 849 | if (this.options.resultsURL) { 850 | // new Ajax.Request(this.options.resultsURL, 851 | // { method: 'get', parameters: this.getResult(), asynchronous: false }); 852 | var results = this.getResult(); 853 | var url = this.options.resultsURL + "?"; 854 | url += "tests="+ this.tests.length + "&"; 855 | url += "assertions="+ results.assertions + "&"; 856 | url += "warnings=" + results.warnings + "&"; 857 | url += "failures=" + results.failures + "&"; 858 | url += "errors=" + results.errors; 859 | JsUnitTest.ajax({ 860 | url: url, 861 | type: 'GET' 862 | }) 863 | } 864 | }; 865 | 866 | JsUnitTest.Unit.Runner.prototype.runTests = function() { 867 | var test = this.tests[this.currentTest], actions; 868 | 869 | if (!test) return this.finish(); 870 | if (!test.isWaiting) this.logger.start(test.name); 871 | test.run(); 872 | var self = this; 873 | if(test.isWaiting) { 874 | this.logger.message("Waiting for " + test.timeToWait + "ms"); 875 | // setTimeout(this.runTests.bind(this), test.timeToWait || 1000); 876 | setTimeout(function() { 877 | self.runTests(); 878 | }, test.timeToWait || 1000); 879 | return; 880 | } 881 | 882 | this.logger.finish(test.status(), test.summary()); 883 | if (actions = test.actions) this.logger.appendActionButtons(actions); 884 | this.currentTest++; 885 | // tail recursive, hopefully the browser will skip the stackframe 886 | this.runTests(); 887 | }; 888 | 889 | JsUnitTest.Unit.Runner.prototype.finish = function() { 890 | this.postResults(); 891 | this.logger.summary(this.summary()); 892 | }; 893 | 894 | JsUnitTest.Unit.Runner.prototype.summary = function() { 895 | return new JsUnitTest.Template('#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors, #{warnings} warnings').evaluate(this.getResult()); 896 | }; 897 | JsUnitTest.Unit.Testcase = function(name, test, setup, teardown) { 898 | this.name = name; 899 | this.test = test || function() {}; 900 | this.setup = setup || function() {}; 901 | this.teardown = teardown || function() {}; 902 | this.messages = []; 903 | this.actions = {}; 904 | }; 905 | // import JsUnitTest.Unit.Assertions 906 | 907 | for (method in JsUnitTest.Unit.Assertions) { 908 | JsUnitTest.Unit.Testcase.prototype[method] = JsUnitTest.Unit.Assertions[method]; 909 | } 910 | 911 | JsUnitTest.Unit.Testcase.prototype.isWaiting = false; 912 | JsUnitTest.Unit.Testcase.prototype.timeToWait = 1000; 913 | JsUnitTest.Unit.Testcase.prototype.assertions = 0; 914 | JsUnitTest.Unit.Testcase.prototype.failures = 0; 915 | JsUnitTest.Unit.Testcase.prototype.errors = 0; 916 | JsUnitTest.Unit.Testcase.prototype.warnings = 0; 917 | JsUnitTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port; 918 | 919 | // JsUnitTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port == 4711; 920 | 921 | JsUnitTest.Unit.Testcase.prototype.wait = function(time, nextPart) { 922 | this.isWaiting = true; 923 | this.test = nextPart; 924 | this.timeToWait = time; 925 | }; 926 | 927 | JsUnitTest.Unit.Testcase.prototype.run = function(rethrow) { 928 | try { 929 | try { 930 | if (!this.isWaiting) this.setup(); 931 | this.isWaiting = false; 932 | this.test(); 933 | } finally { 934 | if(!this.isWaiting) { 935 | this.teardown(); 936 | } 937 | } 938 | } 939 | catch(e) { 940 | if (rethrow) throw e; 941 | this.error(e, this); 942 | } 943 | }; 944 | 945 | JsUnitTest.Unit.Testcase.prototype.summary = function() { 946 | var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors, #{warnings} warnings\n'; 947 | return new JsUnitTest.Template(msg).evaluate(this) + 948 | this.messages.join("\n"); 949 | }; 950 | 951 | JsUnitTest.Unit.Testcase.prototype.pass = function() { 952 | this.assertions++; 953 | }; 954 | 955 | JsUnitTest.Unit.Testcase.prototype.fail = function(message) { 956 | this.failures++; 957 | var line = ""; 958 | try { 959 | throw new Error("stack"); 960 | } catch(e){ 961 | line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1]; 962 | } 963 | this.messages.push("Failure: " + message + (line ? " Line #" + line : "")); 964 | }; 965 | 966 | JsUnitTest.Unit.Testcase.prototype.warning = function(message) { 967 | this.warnings++; 968 | var line = ""; 969 | try { 970 | throw new Error("stack"); 971 | } catch(e){ 972 | line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1]; 973 | } 974 | this.messages.push("Warning: " + message + (line ? " Line #" + line : "")); 975 | }; 976 | JsUnitTest.Unit.Testcase.prototype.warn = JsUnitTest.Unit.Testcase.prototype.warning; 977 | 978 | JsUnitTest.Unit.Testcase.prototype.info = function(message) { 979 | this.messages.push("Info: " + message); 980 | }; 981 | 982 | JsUnitTest.Unit.Testcase.prototype.error = function(error, test) { 983 | this.errors++; 984 | this.actions['retry with throw'] = function() { test.run(true) }; 985 | this.messages.push(error.name + ": "+ error.message + "(" + JsUnitTest.inspect(error) + ")"); 986 | }; 987 | 988 | JsUnitTest.Unit.Testcase.prototype.status = function() { 989 | if (this.failures > 0) return 'failed'; 990 | if (this.errors > 0) return 'error'; 991 | if (this.warnings > 0) return 'warning'; 992 | return 'passed'; 993 | }; 994 | 995 | JsUnitTest.Unit.Testcase.prototype.benchmark = function(operation, iterations) { 996 | var startAt = new Date(); 997 | (iterations || 1).times(operation); 998 | var timeTaken = ((new Date())-startAt); 999 | this.info((arguments[2] || 'Operation') + ' finished ' + 1000 | iterations + ' iterations in ' + (timeTaken/1000)+'s' ); 1001 | return timeTaken; 1002 | }; 1003 | 1004 | Test = JsUnitTest -------------------------------------------------------------------------------- /test/assets/unittest.css: -------------------------------------------------------------------------------- 1 | body, div, p, h1, h2, h3, ul, ol, span, a, table, td, form, img, li { 2 | font-family: sans-serif; 3 | } 4 | 5 | body { 6 | font-size:0.8em; 7 | } 8 | 9 | #log { 10 | padding-bottom: 1em; 11 | border-bottom: 2px solid #000; 12 | margin-bottom: 2em; 13 | } 14 | 15 | .logsummary { 16 | margin-top: 1em; 17 | margin-bottom: 1em; 18 | padding: 1ex; 19 | border: 1px solid #000; 20 | font-weight: bold; 21 | } 22 | 23 | .logtable { 24 | width:100%; 25 | border-collapse: collapse; 26 | border: 1px dotted #666; 27 | } 28 | 29 | .logtable td, .logtable th { 30 | text-align: left; 31 | padding: 3px 8px; 32 | border: 1px dotted #666; 33 | } 34 | 35 | .logtable .passed { 36 | background-color: #cfc; 37 | } 38 | 39 | .logtable .failed, .logtable .error { 40 | background-color: #fcc; 41 | } 42 | 43 | .logtable .warning { 44 | background-color: #FC6; 45 | } 46 | 47 | .logtable td div.action_buttons { 48 | display: inline; 49 | } 50 | 51 | .logtable td div.action_buttons input { 52 | margin: 0 5px; 53 | font-size: 10px; 54 | } 55 | -------------------------------------------------------------------------------- /test/unit/fixtures/a.js: -------------------------------------------------------------------------------- 1 | a = {}; 2 | -------------------------------------------------------------------------------- /test/unit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Namespace.js unit test file 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/unit/namespace_test.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Test, Namespace, a */ 2 | 3 | var t = new Test.Unit.Runner({ 4 | setup : function () { 5 | Namespace.separator = "."; 6 | Namespace.baseUri = "./"; 7 | }, 8 | 9 | teardown : function () { 10 | // Delete any global objects that were created 11 | // IE has problems here 12 | try { 13 | delete window.a; 14 | } catch (e) { window.a = undefined; } 15 | 16 | // Remove script tags other than Namespace.js, namespace_test.js 17 | // and unittest.js 18 | var tags = document.getElementsByTagName("SCRIPT"), tag; 19 | for (var i = 0; i < tags.length; i += 1) { 20 | tag = tags[i]; 21 | if (tag.src.match("amespace") === null && 22 | tag.src.match("unittest") === null) { 23 | tag.parentNode.removeChild(tag); 24 | } 25 | } 26 | }, 27 | 28 | testNamespace : function () { 29 | this.assert(typeof Namespace === "function"); 30 | // Create some objects using namespace 31 | Namespace("a.b.c"); 32 | 33 | this.assert(typeof a === "object"); 34 | this.assert(typeof a.b === "object"); 35 | this.assert(typeof a.b.c === "object"); 36 | 37 | // Create some properties and make sure they are preserved 38 | a.b.foo = true; 39 | a.b.bar = "hello"; 40 | Namespace("a.b.baz"); 41 | 42 | this.assert(a.b.foo); 43 | this.assertIdentical(a.b.bar, "hello"); 44 | this.assert(typeof a.b.baz === "object"); 45 | }, 46 | 47 | testExist : function () { 48 | this.assert(typeof Namespace.exist === "function"); 49 | 50 | this.assert(!Namespace.exist("a")); 51 | 52 | a = {}; 53 | a.b = function () {}; 54 | a.b.c = "something"; 55 | 56 | this.assert(Namespace.exist("a")); 57 | this.assert(Namespace.exist("a.b")); 58 | this.assert(Namespace.exist("a.b.c")); 59 | 60 | try { 61 | delete window.a; 62 | } catch (e) { window.a = undefined; } 63 | 64 | this.assert(!Namespace.exist("a")); 65 | }, 66 | 67 | testMapIdentifierToUri : function () { 68 | this.assert(typeof Namespace.mapIdentifierToUri === "function"); 69 | 70 | this.assertIdentical(Namespace.mapIdentifierToUri("a.b.c"), "./a/b/c.js"); 71 | 72 | Namespace.separator = ":"; 73 | 74 | this.assertIdentical(Namespace.mapIdentifierToUri("a:b:c"), "./a/b/c.js"); 75 | 76 | Namespace.separator = "."; 77 | Namespace.baseUri = "/foo/bar/"; 78 | 79 | this.assertIdentical(Namespace.mapIdentifierToUri("a.b.c"), "/foo/bar/a/b/c.js"); 80 | }, 81 | 82 | testInclude : function () { 83 | this.assert(typeof Namespace.include === "function"); 84 | 85 | Namespace.include("fixtures/a"); 86 | 87 | this.assert(typeof a === "object"); 88 | }, 89 | 90 | testUse : function () { 91 | 92 | }, 93 | 94 | testFrom : function () { 95 | 96 | }, 97 | 98 | testProvide : function () { 99 | 100 | }, 101 | 102 | testAddEventListener : function () { 103 | 104 | }, 105 | 106 | testRemoveEventListener : function () { 107 | 108 | }, 109 | 110 | testRegisterNativeExtensions : function () { 111 | 112 | } 113 | }); 114 | --------------------------------------------------------------------------------