فهرست منبع

Cleanup: Util code

File: util.js
Tests Added: True (partial -- for logging and array push methods)
Changes:
- Fixed JSHint Errors (indentation, semicolons, global "use strict")
- Made browser detection methods more readable
- added some newline characters when appropriate for readability
- throw Errors not strings!
- Removed conf_defaults, and added make_properties and set_defaults
  instead (see below)

The removal of conf_defaults and switch to make_properties and
set_defaults is to facilitate the switch over to normal Javascript
constructors instead of Crockford-style constructors.  Now, methods
are added to the objects prototype (and thus make properties is called
outside the constructor).
Solly Ross 11 سال پیش
والد
کامیت
d21cd6c164
2فایلهای تغییر یافته به همراه471 افزوده شده و 229 حذف شده
  1. 366 229
      include/util.js
  2. 105 0
      tests/test.util.js

+ 366 - 229
include/util.js

@@ -6,9 +6,8 @@
  * See README.md for usage and integration instructions.
  */
 
-"use strict";
-/*jslint bitwise: false, white: false */
-/*global window, console, document, navigator, ActiveXObject */
+/* jshint white: false, nonstandard: true */
+/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */
 
 // Globals defined here
 var Util = {};
@@ -19,129 +18,161 @@ var Util = {};
  */
 
 Array.prototype.push8 = function (num) {
+    "use strict";
     this.push(num & 0xFF);
 };
 
 Array.prototype.push16 = function (num) {
+    "use strict";
     this.push((num >> 8) & 0xFF,
-              (num     ) & 0xFF  );
+              num & 0xFF);
 };
 Array.prototype.push32 = function (num) {
+    "use strict";
     this.push((num >> 24) & 0xFF,
               (num >> 16) & 0xFF,
               (num >>  8) & 0xFF,
-              (num      ) & 0xFF  );
+              num & 0xFF);
 };
 
 // IE does not support map (even in IE9)
 //This prototype is provided by the Mozilla foundation and
 //is distributed under the MIT license.
 //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
-if (!Array.prototype.map)
-{
-  Array.prototype.map = function(fun /*, thisp*/)
-  {
-    var len = this.length;
-    if (typeof fun != "function")
-      throw new TypeError();
-
-    var res = new Array(len);
-    var thisp = arguments[1];
-    for (var i = 0; i < len; i++)
-    {
-      if (i in this)
-        res[i] = fun.call(thisp, this[i], i, this);
-    }
+if (!Array.prototype.map) {
+    Array.prototype.map = function (fun /*, thisp*/) {
+        "use strict";
+        var len = this.length;
+        if (typeof fun != "function") {
+            throw new TypeError();
+        }
 
-    return res;
-  };
+        var res = new Array(len);
+        var thisp = arguments[1];
+        for (var i = 0; i < len; i++) {
+            if (i in this) {
+                res[i] = fun.call(thisp, this[i], i, this);
+            }
+        }
+
+        return res;
+    };
 }
 
 // IE <9 does not support indexOf
 //This prototype is provided by the Mozilla foundation and
 //is distributed under the MIT license.
 //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
-if (!Array.prototype.indexOf)
-{
-  Array.prototype.indexOf = function(elt /*, from*/)
-  {
-    var len = this.length >>> 0;
-
-    var from = Number(arguments[1]) || 0;
-    from = (from < 0)
-         ? Math.ceil(from)
-         : Math.floor(from);
-    if (from < 0)
-      from += len;
-
-    for (; from < len; from++)
-    {
-      if (from in this &&
-          this[from] === elt)
-        return from;
-    }
-    return -1;
-  };
+if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (elt /*, from*/) {
+        "use strict";
+        var len = this.length >>> 0;
+
+        var from = Number(arguments[1]) || 0;
+        from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+        if (from < 0) {
+            from += len;
+        }
+
+        for (; from < len; from++) {
+            if (from in this &&
+                    this[from] === elt) {
+                return from;
+            }
+        }
+        return -1;
+    };
 }
 
 // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
 if (!Object.keys) {
-  Object.keys = (function () {
-    'use strict';
-    var hasOwnProperty = Object.prototype.hasOwnProperty,
-        hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
-        dontEnums = [
-          'toString',
-          'toLocaleString',
-          'valueOf',
-          'hasOwnProperty',
-          'isPrototypeOf',
-          'propertyIsEnumerable',
-          'constructor'
-        ],
-        dontEnumsLength = dontEnums.length;
+    Object.keys = (function () {
+        'use strict';
+        var hasOwnProperty = Object.prototype.hasOwnProperty,
+            hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+            dontEnums = [
+                'toString',
+                'toLocaleString',
+                'valueOf',
+                'hasOwnProperty',
+                'isPrototypeOf',
+                'propertyIsEnumerable',
+                'constructor'
+            ],
+            dontEnumsLength = dontEnums.length;
+
+        return function (obj) {
+            if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
+                throw new TypeError('Object.keys called on non-object');
+            }
 
-    return function (obj) {
-      if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
-        throw new TypeError('Object.keys called on non-object');
-      }
+            var result = [], prop, i;
 
-      var result = [], prop, i;
+            for (prop in obj) {
+                if (hasOwnProperty.call(obj, prop)) {
+                    result.push(prop);
+                }
+            }
 
-      for (prop in obj) {
-        if (hasOwnProperty.call(obj, prop)) {
-          result.push(prop);
-        }
-      }
+            if (hasDontEnumBug) {
+                for (i = 0; i < dontEnumsLength; i++) {
+                    if (hasOwnProperty.call(obj, dontEnums[i])) {
+                        result.push(dontEnums[i]);
+                    }
+                }
+            }
+            return result;
+        };
+    })();
+}
 
-      if (hasDontEnumBug) {
-        for (i = 0; i < dontEnumsLength; i++) {
-          if (hasOwnProperty.call(obj, dontEnums[i])) {
-            result.push(dontEnums[i]);
-          }
+// PhantomJS 1.x doesn't support bind,
+// so leave this in until PhantomJS 2.0 is released
+//This prototype is provided by the Mozilla foundation and
+//is distributed under the MIT license.
+//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (oThis) {
+        if (typeof this !== "function") {
+            // closest thing possible to the ECMAScript 5
+            // internal IsCallable function
+            throw new TypeError("Function.prototype.bind - " +
+                                "what is trying to be bound is not callable");
         }
-      }
-      return result;
+
+        var aArgs = Array.prototype.slice.call(arguments, 1),
+                fToBind = this,
+                fNOP = function () {},
+                fBound = function () {
+                    return fToBind.apply(this instanceof fNOP && oThis ? this
+                                                                       : oThis,
+                                         aArgs.concat(Array.prototype.slice.call(arguments)));
+                };
+
+        fNOP.prototype = this.prototype;
+        fBound.prototype = new fNOP();
+
+        return fBound;
     };
-  }());
 }
 
-// 
+//
 // requestAnimationFrame shim with setTimeout fallback
 //
 
-window.requestAnimFrame = (function(){
-    return  window.requestAnimationFrame       || 
-            window.webkitRequestAnimationFrame || 
-            window.mozRequestAnimationFrame    || 
-            window.oRequestAnimationFrame      || 
-            window.msRequestAnimationFrame     || 
-            function(callback){
+window.requestAnimFrame = (function () {
+    "use strict";
+    return  window.requestAnimationFrame       ||
+            window.webkitRequestAnimationFrame ||
+            window.mozRequestAnimationFrame    ||
+            window.oRequestAnimationFrame      ||
+            window.msRequestAnimationFrame     ||
+            function (callback) {
                 window.setTimeout(callback, 1000 / 60);
             };
 })();
 
-/* 
+/*
  * ------------------------------------------------------
  * Namespaced in Util
  * ------------------------------------------------------
@@ -153,6 +184,7 @@ window.requestAnimFrame = (function(){
 
 Util._log_level = 'warn';
 Util.init_logging = function (level) {
+    "use strict";
     if (typeof level === 'undefined') {
         level = Util._log_level;
     } else {
@@ -163,26 +195,34 @@ Util.init_logging = function (level) {
             window.console = {
                 'log'  : window.opera.postError,
                 'warn' : window.opera.postError,
-                'error': window.opera.postError };
+                'error': window.opera.postError
+            };
         } else {
             window.console = {
-                'log'  : function(m) {},
-                'warn' : function(m) {},
-                'error': function(m) {}};
+                'log'  : function (m) {},
+                'warn' : function (m) {},
+                'error': function (m) {}
+            };
         }
     }
 
     Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {};
+    /* jshint -W086 */
     switch (level) {
-        case 'debug': Util.Debug = function (msg) { console.log(msg); };
-        case 'info':  Util.Info  = function (msg) { console.log(msg); };
-        case 'warn':  Util.Warn  = function (msg) { console.warn(msg); };
-        case 'error': Util.Error = function (msg) { console.error(msg); };
+        case 'debug':
+            Util.Debug = function (msg) { console.log(msg); };
+        case 'info':
+            Util.Info  = function (msg) { console.log(msg); };
+        case 'warn':
+            Util.Warn  = function (msg) { console.warn(msg); };
+        case 'error':
+            Util.Error = function (msg) { console.error(msg); };
         case 'none':
             break;
         default:
-            throw("invalid logging type '" + level + "'");
+            throw new Error("invalid logging type '" + level + "'");
     }
+    /* jshint +W086 */
 };
 Util.get_logging = function () {
     return Util._log_level;
@@ -190,93 +230,133 @@ Util.get_logging = function () {
 // Initialize logging level
 Util.init_logging();
 
+Util.make_property = function (proto, name, mode, type) {
+    "use strict";
 
-// Set configuration default for Crockford style function namespaces
-Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) {
-    var getter, setter;
+    var getter;
+    if (type === 'arr') {
+        getter = function (idx) {
+            if (typeof idx !== 'undefined') {
+                return this['_' + name][idx];
+            } else {
+                return this['_' + name];
+            }
+        };
+    } else {
+        getter = function () {
+            return this['_' + name];
+        };
+    }
 
-    // Default getter function
-    getter = function (idx) {
-        if ((type in {'arr':1, 'array':1}) &&
-            (typeof idx !== 'undefined')) {
-            return cfg[v][idx];
+    var make_setter = function (process_val) {
+        if (process_val) {
+            return function (val, idx) {
+                if (typeof idx !== 'undefined') {
+                    this['_' + name][idx] = process_val(val);
+                } else {
+                    this['_' + name] = process_val(val);
+                }
+            };
         } else {
-            return cfg[v];
+            return function (val, idx) {
+                if (typeof idx !== 'undefined') {
+                    this['_' + name][idx] = val;
+                } else {
+                    this['_' + name] = val;
+                }
+            };
         }
     };
 
-    // Default setter function
-    setter = function (val, idx) {
-        if (type in {'boolean':1, 'bool':1}) {
-            if ((!val) || (val in {'0':1, 'no':1, 'false':1})) {
-                val = false;
+    var setter;
+    if (type === 'bool') {
+        setter = make_setter(function (val) {
+            if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
+                return false;
             } else {
-                val = true;
+                return true;
             }
-        } else if (type in {'integer':1, 'int':1}) {
-            val = parseInt(val, 10);
-        } else if (type === 'str') {
-            val = String(val);
-        } else if (type === 'func') {
+        });
+    } else if (type === 'int') {
+        setter = make_setter(function (val) { return parseInt(val, 10); });
+    } else if (type === 'float') {
+        setter = make_setter(parseFloat);
+    } else if (type === 'str') {
+        setter = make_setter(String);
+    } else if (type === 'func') {
+        setter = make_setter(function (val) {
             if (!val) {
-                val = function () {};
+                return function () {};
+            } else {
+                return val;
             }
-        }
-        if (typeof idx !== 'undefined') {
-            cfg[v][idx] = val;
-        } else {
-            cfg[v] = val;
-        }
-    };
-
-    // Set the description
-    api[v + '_description'] = desc;
+        });
+    } else if (type === 'arr' || type === 'dom' || type == 'raw') {
+        setter = make_setter();
+    } else {
+        throw new Error('Unknown property type ' + type);  // some sanity checking
+    }
 
-    // Set the getter function
-    if (typeof api['get_' + v] === 'undefined') {
-        api['get_' + v] = getter;
+    // set the getter
+    if (typeof proto['get_' + name] === 'undefined') {
+        proto['get_' + name] = getter;
     }
 
-    // Set the setter function with extra sanity checks
-    if (typeof api['set_' + v] === 'undefined') {
-        api['set_' + v] = function (val, idx) {
-            if (mode in {'RO':1, 'ro':1}) {
-                throw(v + " is read-only");
-            } else if ((mode in {'WO':1, 'wo':1}) &&
-                       (typeof cfg[v] !== 'undefined')) {
-                throw(v + " can only be set once");
-            }
-            setter(val, idx);
-        };
+    // set the setter if needed
+    if (typeof proto['set_' + name] === 'undefined') {
+        if (mode === 'rw') {
+            proto['set_' + name] = setter;
+        } else if (mode === 'wo') {
+            proto['set_' + name] = function (val, idx) {
+                if (typeof this['_' + name] !== 'undefined') {
+                    throw new Error(name + " can only be set once");
+                }
+                setter.call(this, val, idx);
+            };
+        }
     }
 
-    // Set the default value
-    if (typeof defaults[v] !== 'undefined') {
-        defval = defaults[v];
-    } else if ((type in {'arr':1, 'array':1}) &&
-            (! (defval instanceof Array))) {
-        defval = [];
+    // make a special setter that we can use in set defaults
+    proto['_raw_set_' + name] = function (val, idx) {
+        setter.call(this, val, idx);
+        //delete this['_init_set_' + name];  // remove it after use
+    };
+};
+
+Util.make_properties = function (constructor, arr) {
+    "use strict";
+    for (var i = 0; i < arr.length; i++) {
+        Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]);
     }
-    // Coerce existing setting to the right type
-    //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]);
-    setter(defval);
 };
 
-// Set group of configuration defaults
-Util.conf_defaults = function(cfg, api, defaults, arr) {
+Util.set_defaults = function (obj, conf, defaults) {
+    var defaults_keys = Object.keys(defaults);
+    var conf_keys = Object.keys(conf);
+    var keys_obj = {};
     var i;
-    for (i = 0; i < arr.length; i++) {
-        Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1],
-                arr[i][2], arr[i][3], arr[i][4]);
+    for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; }
+    for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; }
+    var keys = Object.keys(keys_obj);
+
+    for (i = 0; i < keys.length; i++) {
+        var setter = obj['_raw_set_' + keys[i]];
+
+        if (conf[keys[i]]) {
+            setter.call(obj, conf[keys[i]]);
+        } else {
+            setter.call(obj, defaults[keys[i]]);
+        }
     }
 };
 
 /*
  * Decode from UTF-8
  */
-Util.decodeUTF8 = function(utf8string) {
+Util.decodeUTF8 = function (utf8string) {
+    "use strict";
     return decodeURIComponent(escape(utf8string));
-}
+};
 
 
 
@@ -291,42 +371,46 @@ Util.decodeUTF8 = function(utf8string) {
 // Handles the case where load_scripts is invoked from a script that
 // itself is loaded via load_scripts. Once all scripts are loaded the
 // window.onscriptsloaded handler is called (if set).
-Util.get_include_uri = function() {
+Util.get_include_uri = function () {
     return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/";
-}
+};
 Util._loading_scripts = [];
 Util._pending_scripts = [];
-Util.load_scripts = function(files) {
+Util.load_scripts = function (files) {
+    "use strict";
     var head = document.getElementsByTagName('head')[0], script,
         ls = Util._loading_scripts, ps = Util._pending_scripts;
-    for (var f=0; f<files.length; f++) {
+
+    var loadFunc = function (e) {
+        while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
+                                 ls[0].readyState === 'complete')) {
+            // For IE, append the script to trigger execution
+            var s = ls.shift();
+            //console.log("loaded script: " + s.src);
+            head.appendChild(s);
+        }
+        if (!this.readyState ||
+            (Util.Engine.presto && this.readyState === 'loaded') ||
+            this.readyState === 'complete') {
+            if (ps.indexOf(this) >= 0) {
+                this.onload = this.onreadystatechange = null;
+                //console.log("completed script: " + this.src);
+                ps.splice(ps.indexOf(this), 1);
+
+                // Call window.onscriptsload after last script loads
+                if (ps.length === 0 && window.onscriptsload) {
+                    window.onscriptsload();
+                }
+            }
+        }
+    };
+
+    for (var f = 0; f < files.length; f++) {
         script = document.createElement('script');
         script.type = 'text/javascript';
         script.src = Util.get_include_uri() + files[f];
         //console.log("loading script: " + script.src);
-        script.onload = script.onreadystatechange = function (e) {
-            while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
-                                     ls[0].readyState === 'complete')) {
-                // For IE, append the script to trigger execution
-                var s = ls.shift();
-                //console.log("loaded script: " + s.src);
-                head.appendChild(s);
-            }
-            if (!this.readyState ||
-                (Util.Engine.presto && this.readyState === 'loaded') ||
-                this.readyState === 'complete') {
-                if (ps.indexOf(this) >= 0) {
-                    this.onload = this.onreadystatechange = null;
-                    //console.log("completed script: " + this.src);
-                    ps.splice(ps.indexOf(this), 1);
-
-                    // Call window.onscriptsload after last script loads
-                    if (ps.length === 0 && window.onscriptsload) {
-                        window.onscriptsload();
-                    }
-                }
-            }
-        };
+        script.onload = script.onreadystatechange = loadFunc;
         // In-order script execution tricks
         if (Util.Engine.trident) {
             // For IE wait until readyState is 'loaded' before
@@ -341,20 +425,22 @@ Util.load_scripts = function(files) {
         }
         ps.push(script);
     }
-}
+};
 
 
 // Get DOM element position on page
 //  This solution is based based on http://www.greywyvern.com/?post=331
 //  Thanks to Brian Huisman AKA GreyWyvern!
-Util.getPosition = (function() {
+Util.getPosition = (function () {
+    "use strict";
     function getStyle(obj, styleProp) {
+        var y;
         if (obj.currentStyle) {
-            var y = obj.currentStyle[styleProp];
+            y = obj.currentStyle[styleProp];
         } else if (window.getComputedStyle)
-            var y = window.getComputedStyle(obj, null)[styleProp];
+            y = window.getComputedStyle(obj, null)[styleProp];
         return y;
-    };
+    }
 
     function scrollDist() {
         var myScrollTop = 0, myScrollLeft = 0;
@@ -383,7 +469,7 @@ Util.getPosition = (function() {
         }
 
         return [myScrollLeft, myScrollTop];
-    };
+    }
 
     return function (obj) {
         var curleft = 0, curtop = 0, scr = obj, fixed = false;
@@ -403,7 +489,7 @@ Util.getPosition = (function() {
         do {
             curleft += obj.offsetLeft;
             curtop += obj.offsetTop;
-        } while (obj = obj.offsetParent);
+        } while ((obj = obj.offsetParent));
 
         return {'x': curleft, 'y': curtop};
     };
@@ -412,6 +498,7 @@ Util.getPosition = (function() {
 
 // Get mouse event position in DOM element
 Util.getEventPosition = function (e, obj, scale) {
+    "use strict";
     var evt, docX, docY, pos;
     //if (!e) evt = window.event;
     evt = (e ? e : window.event);
@@ -431,38 +518,41 @@ Util.getEventPosition = function (e, obj, scale) {
     }
     var realx = docX - pos.x;
     var realy = docY - pos.y;
-    var x = Math.max(Math.min(realx, obj.width-1), 0);
-    var y = Math.max(Math.min(realy, obj.height-1), 0);
+    var x = Math.max(Math.min(realx, obj.width - 1), 0);
+    var y = Math.max(Math.min(realy, obj.height - 1), 0);
     return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale};
 };
 
 
 // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events
-Util.addEvent = function (obj, evType, fn){
-    if (obj.attachEvent){
-        var r = obj.attachEvent("on"+evType, fn);
+Util.addEvent = function (obj, evType, fn) {
+    "use strict";
+    if (obj.attachEvent) {
+        var r = obj.attachEvent("on" + evType, fn);
         return r;
-    } else if (obj.addEventListener){
-        obj.addEventListener(evType, fn, false); 
+    } else if (obj.addEventListener) {
+        obj.addEventListener(evType, fn, false);
         return true;
     } else {
-        throw("Handler could not be attached");
+        throw new Error("Handler could not be attached");
     }
 };
 
-Util.removeEvent = function(obj, evType, fn){
-    if (obj.detachEvent){
-        var r = obj.detachEvent("on"+evType, fn);
+Util.removeEvent = function (obj, evType, fn) {
+    "use strict";
+    if (obj.detachEvent) {
+        var r = obj.detachEvent("on" + evType, fn);
         return r;
-    } else if (obj.removeEventListener){
+    } else if (obj.removeEventListener) {
         obj.removeEventListener(evType, fn, false);
         return true;
     } else {
-        throw("Handler could not be removed");
+        throw new Error("Handler could not be removed");
     }
 };
 
-Util.stopEvent = function(e) {
+Util.stopEvent = function (e) {
+    "use strict";
     if (e.stopPropagation) { e.stopPropagation(); }
     else                   { e.cancelBubble = true; }
 
@@ -474,41 +564,88 @@ Util.stopEvent = function(e) {
 // Set browser engine versions. Based on mootools.
 Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)};
 
-Util.Engine = {
-    // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
-    //'presto': (function() {
-    //         return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
-    'presto': (function() { return (!window.opera) ? false : true; }()),
-
-    'trident': (function() {
-            return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()),
-    'webkit': (function() {
-            try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
-    //'webkit': (function() {
-    //        return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()),
-    'gecko': (function() {
-            return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }())
-};
-if (Util.Engine.webkit) {
-    // Extract actual webkit version if available
-    Util.Engine.webkit = (function(v) {
-            var re = new RegExp('WebKit/([0-9\.]*) ');
-            v = (navigator.userAgent.match(re) || ['', v])[1];
-            return parseFloat(v, 10);
-        })(Util.Engine.webkit);
-}
+(function () {
+    "use strict";
+    // 'presto': (function () { return (!window.opera) ? false : true; }()),
+    var detectPresto = function () {
+        return !!window.opera;
+    };
+
+    // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
+    var detectTrident = function () {
+        if (!window.ActiveXObject) {
+            return false;
+        } else {
+            if (window.XMLHttpRequest) {
+                return (document.querySelectorAll) ? 6 : 5;
+            } else {
+                return 4;
+            }
+        }
+    };
+
+    // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()),
+    var detectInitialWebkit = function () {
+        try {
+            if (navigator.taintEnabled) {
+                return false;
+            } else {
+                if (Util.Features.xpath) {
+                    return (Util.Features.query) ? 525 : 420;
+                } else {
+                    return 419;
+                }
+            }
+        } catch (e) {
+            return false;
+        }
+    };
+
+    var detectActualWebkit = function (initial_ver) {
+        var re = /WebKit\/([0-9\.]*) /;
+        var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1];
+        return parseFloat(str_ver, 10);
+    };
+
+    // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }())
+    var detectGecko = function () {
+        /* jshint -W041 */
+        if (!document.getBoxObjectFor && window.mozInnerScreenX == null) {
+            return false;
+        } else {
+            return (document.getElementsByClassName) ? 19 : 18;
+        }
+        /* jshint +W041 */
+    };
+
+    Util.Engine = {
+        // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
+        //'presto': (function() {
+        //         return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
+        'presto': detectPresto(),
+        'trident': detectTrident(),
+        'webkit': detectInitialWebkit(),
+        'gecko': detectGecko(),
+    };
+
+    if (Util.Engine.webkit) {
+        // Extract actual webkit version if available
+        Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit);
+    }
+})();
 
-Util.Flash = (function(){
+Util.Flash = (function () {
+    "use strict";
     var v, version;
     try {
         v = navigator.plugins['Shockwave Flash'].description;
-    } catch(err1) {
+    } catch (err1) {
         try {
             v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
-        } catch(err2) {
+        } catch (err2) {
             v = '0 r0';
         }
     }
     version = v.match(/\d+/g);
     return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0};
-}()); 
+}());

+ 105 - 0
tests/test.util.js

@@ -0,0 +1,105 @@
+// requires local modules: util
+/* jshint expr: true */
+
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Utils', function() {
+    "use strict";
+
+    describe('Array instance methods', function () {
+        describe('push8', function () {
+            it('should push a byte on to the array', function () {
+                var arr = [1];
+                arr.push8(128);
+                expect(arr).to.deep.equal([1, 128]);
+            });
+
+            it('should only use the least significant byte of any number passed in', function () {
+                var arr = [1];
+                arr.push8(0xABCD);
+                expect(arr).to.deep.equal([1, 0xCD]);
+            });
+        });
+
+        describe('push16', function () {
+            it('should push two bytes on to the array', function () {
+                var arr = [1];        
+                arr.push16(0xABCD);
+                expect(arr).to.deep.equal([1, 0xAB, 0xCD]);
+            });
+
+            it('should only use the two least significant bytes of any number passed in', function () {
+                var arr = [1];        
+                arr.push16(0xABCDEF);
+                expect(arr).to.deep.equal([1, 0xCD, 0xEF]);
+            });
+        });
+
+        describe('push32', function () {
+            it('should push four bytes on to the array', function () {
+                var arr = [1];        
+                arr.push32(0xABCDEF12);
+                expect(arr).to.deep.equal([1, 0xAB, 0xCD, 0xEF, 0x12]);
+            });
+
+            it('should only use the four least significant bytes of any number passed in', function () {
+                var arr = [1];        
+                arr.push32(0xABCDEF1234);
+                expect(arr).to.deep.equal([1, 0xCD, 0xEF, 0x12, 0x34]);
+            });
+        });
+    });
+    
+    describe('logging functions', function () {
+        beforeEach(function () {
+            sinon.spy(console, 'log');
+            sinon.spy(console, 'warn');
+            sinon.spy(console, 'error');
+        });
+
+        afterEach(function () {
+           console.log.restore();
+           console.warn.restore();
+           console.error.restore();
+        });
+
+        it('should use noop for levels lower than the min level', function () {
+            Util.init_logging('warn');
+            Util.Debug('hi');
+            Util.Info('hello');
+            expect(console.log).to.not.have.been.called;
+        });
+
+        it('should use console.log for Debug and Info', function () {
+            Util.init_logging('debug');
+            Util.Debug('dbg');
+            Util.Info('inf');
+            expect(console.log).to.have.been.calledWith('dbg');
+            expect(console.log).to.have.been.calledWith('inf');
+        });
+
+        it('should use console.warn for Warn', function () {
+            Util.init_logging('warn');
+            Util.Warn('wrn');
+            expect(console.warn).to.have.been.called;
+            expect(console.warn).to.have.been.calledWith('wrn');
+        });
+
+        it('should use console.error for Error', function () {
+            Util.init_logging('error');
+            Util.Error('err');
+            expect(console.error).to.have.been.called;
+            expect(console.error).to.have.been.calledWith('err');
+        });
+    });
+
+    // TODO(directxman12): test the conf_default and conf_defaults methods
+    // TODO(directxman12): test decodeUTF8
+    // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent)
+    // TODO(directxman12): figure out a good way to test getPosition and getEventPosition
+    // TODO(directxman12): figure out how to test the browser detection functions properly    
+    //                     (we can't really test them against the browsers, except for Gecko
+    //                     via PhantomJS, the default test driver)
+    // TODO(directxman12): figure out how to test Util.Flash
+});