Started xos-util tests and emproved readme
diff --git a/xos/configurations/frontend/README.md b/xos/configurations/frontend/README.md
index a964d91..7e8a771 100644
--- a/xos/configurations/frontend/README.md
+++ b/xos/configurations/frontend/README.md
@@ -13,3 +13,13 @@
 - Run `make` command
 
 You'll be able to visit XOS at `0.0.0.0:9000` and the `xos/core/xoslib` folder is shared with the container. This means that any update to that folder is automatically reported in the container.
+
+## Test
+
+To run the FE tests, navigate to: `xos/core/xoslib`, and run 'npm test'.
+
+This will install the required `npm` dependencies and run the test.
+
+Tests are runned in a headless browser (_PhantomJs_) by _Karma_ and the assertions are made with _Jasmine_. This is a pretty common standard for FE testing so you should feel at home.
+
+You can find the tests in the `spec/` folder, each source file has a corresponding `.test` file in it.
diff --git a/xos/core/xoslib/package.json b/xos/core/xoslib/package.json
index dd6714b..831f182 100644
--- a/xos/core/xoslib/package.json
+++ b/xos/core/xoslib/package.json
@@ -4,6 +4,7 @@
   "description": "Add to the following in settings.py",
   "main": "index.js",
   "scripts": {
+    "pretest": "npm install",
     "test": "karma start",
     "lint": "eslint ."
   },
diff --git a/xos/core/xoslib/spec/xoslib/utils.test.js b/xos/core/xoslib/spec/xoslib/utils.test.js
new file mode 100644
index 0000000..ad65067
--- /dev/null
+++ b/xos/core/xoslib/spec/xoslib/utils.test.js
@@ -0,0 +1,69 @@
+'use strict';
+
+describe('The XOS Lib Utilities', function() {
+  describe('The idInArray method', function() {
+    it('should match a string ID', () => {
+      let res = idInArray('1', [1, 2, 3]);
+      expect(res).toBeTruthy();
+    });
+
+    it('should march a number ID', () => {
+      let res = idInArray(1, [1, 2, 3]);
+      expect(res).toBeTruthy();
+    });
+
+    it('should not match this ID', () => {
+      let res = idInArray(4, [1, 2, 3]);
+      expect(res).toBeFalsy();
+    });
+  });
+
+  describe('The firstCharUpper', () => {
+    it('should return the first char UPPERCASE', () => {
+      let res = firstCharUpper('test');
+      expect(res).toEqual('Test');
+    });
+  });
+
+  describe('The toTitleCase', () => {
+    it('should convert all word\'s first letter to uppercase and the other to lowercase', () => {
+      let res = toTitleCase('tesT tEst');
+      expect(res).toEqual('Test Test');
+    });
+  });
+
+  describe('The fieldNameToHumanReadable method', () => {
+    it('should convert lodash to spaces and apply toTitleCase', () => {
+      let res = fieldNameToHumanReadable('tEst_fIelD');
+      expect(res).toEqual('Test Field')
+    });
+  });
+
+  describe('The limitTableRows', () => {
+    it('should be tested', () => {
+      
+    });
+  });
+
+  describe('The validateField', () => {
+    it('should should validate notBlank', () => {
+      let res = validateField('notBlank', null);
+      expect(res).toEqual('can not be blank');
+    });
+
+    it('should validate a url', () => {
+      let res = validateField('url', 'test a fake url');
+      expect(res).toEqual('must be a valid url');
+    });
+
+    it('should validate a port', () => {
+      let res = validateField('portspec', 'i a not a port');
+      expect(res).toEqual('must be a valid portspec (example: \'tcp 123, udp 456-789\')');
+    });
+
+    it('should return true for a valid url', () => {
+      let res = validateField('url', 'www.onlab.us');
+      expect(res).toBeTruthy();
+    });
+  });
+});
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index dfc6c38..abce329 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -40,6 +40,8 @@
     CORDSUBSCRIBER_API = XOSLIB_BASE + "/cordsubscriber/";
     CORDUSER_API = XOSLIB_BASE + "/corduser/";
 
+    console.log(Backbone);
+
     XOSModel = Backbone.Model.extend({
         relatedCollections: [],
         foreignCollections: [],
@@ -81,13 +83,13 @@
             },
 
             listMethods: function() {
-                var res = [];

-                for(var m in this) {

-                    if(typeof this[m] == "function") {

-                        res.push(m)

-                    }

-                }

-                return res;

+                var res = [];
+                for(var m in this) {
+                    if(typeof this[m] == "function") {
+                        res.push(m)
+                    }
+                }
+                return res;
             },
 
             save: function(attributes, options) {
@@ -157,93 +159,93 @@
           this.isLoaded = false;
           this.failedLoad = false;
           this.startedLoad = false;
-          this.sortVar = 'name';

-          this.sortOrder = 'asc';

-          this.on( "sort", this.sorted );

-        },

-

-        relatedCollections: [],

-        foreignCollections: [],

-        foreignFields: {},

+          this.sortVar = 'name';
+          this.sortOrder = 'asc';
+          this.on( "sort", this.sorted );
+        },
+
+        relatedCollections: [],
+        foreignCollections: [],
+        foreignFields: {},
         m2mFields: {},
         readonlyFields: [],
-        detailLinkFields: [],

-

-        sorted: function() {

-            //console.log("sorted " + this.modelName);

-        },

-

-        simpleComparator: function( model ){

-          parts=this.sortVar.split(".");

-          result = model.get(parts[0]);

-          for (index=1; index<parts.length; ++index) {

-              result=result[parts[index]];

-          }

-          return result;

-        },

-

-        comparator: function (left, right) {

-            var l = this.simpleComparator(left);

-            var r = this.simpleComparator(right);

-

-            if (l === void 0) return -1;

-            if (r === void 0) return 1;

-

-            if (this.sortOrder=="desc") {

-                return l < r ? 1 : l > r ? -1 : 0;

-            } else {

-                return l < r ? -1 : l > r ? 1 : 0;

-            }

-        },

-

-        fetchSuccess: function(collection, response, options) {

-            //console.log("fetch succeeded " + collection.modelName);

-            this.failedLoad = false;

-            this.fetching = false;

-            if (!this.isLoaded) {

-                this.isLoaded = true;

-                Backbone.trigger("xoslib:collectionLoadChange", this);

-            }

-            this.trigger("fetchStateChange");

-            if (options["orig_success"]) {

-                options["orig_success"](collection, response, options);

-            }

-        },

-

-        fetchFailure: function(collection, response, options) {

-            //console.log("fetch failed " + collection.modelName);

-            this.fetching = false;

-            if ((!this.isLoaded) && (!this.failedLoad)) {

-                this.failedLoad=true;

-                Backbone.trigger("xoslib:collectionLoadChange", this);

-            }

-            this.trigger("fetchStateChange");

-            if (options["orig_failure"]) {

-                options["orig_failure"](collection, response, options);

-            }

-        },

-

-        fetch: function(options) {

-            var self=this;

-            this.fetching=true;

-            //console.log("fetch " + this.modelName);

-            if (!this.startedLoad) {

-                this.startedLoad=true;

-                Backbone.trigger("xoslib:collectionLoadChange", this);

-            }

-            this.trigger("fetchStateChange");

-            if (options == undefined) {

-                options = {};

-            }

-            options["orig_success"] = options["success"];

-            options["orig_failure"] = options["failure"];

-            options["success"] = function(collection, response, options) { self.fetchSuccess.call(self, collection, response, options); };

-            options["failure"] = this.fetchFailure;

-            Backbone.Collection.prototype.fetch.call(this, options);

-        },

-

-        startPolling: function() {

-            if (!this._polling) {

+        detailLinkFields: [],
+
+        sorted: function() {
+            //console.log("sorted " + this.modelName);
+        },
+
+        simpleComparator: function( model ){
+          parts=this.sortVar.split(".");
+          result = model.get(parts[0]);
+          for (index=1; index<parts.length; ++index) {
+              result=result[parts[index]];
+          }
+          return result;
+        },
+
+        comparator: function (left, right) {
+            var l = this.simpleComparator(left);
+            var r = this.simpleComparator(right);
+
+            if (l === void 0) return -1;
+            if (r === void 0) return 1;
+
+            if (this.sortOrder=="desc") {
+                return l < r ? 1 : l > r ? -1 : 0;
+            } else {
+                return l < r ? -1 : l > r ? 1 : 0;
+            }
+        },
+
+        fetchSuccess: function(collection, response, options) {
+            //console.log("fetch succeeded " + collection.modelName);
+            this.failedLoad = false;
+            this.fetching = false;
+            if (!this.isLoaded) {
+                this.isLoaded = true;
+                Backbone.trigger("xoslib:collectionLoadChange", this);
+            }
+            this.trigger("fetchStateChange");
+            if (options["orig_success"]) {
+                options["orig_success"](collection, response, options);
+            }
+        },
+
+        fetchFailure: function(collection, response, options) {
+            //console.log("fetch failed " + collection.modelName);
+            this.fetching = false;
+            if ((!this.isLoaded) && (!this.failedLoad)) {
+                this.failedLoad=true;
+                Backbone.trigger("xoslib:collectionLoadChange", this);
+            }
+            this.trigger("fetchStateChange");
+            if (options["orig_failure"]) {
+                options["orig_failure"](collection, response, options);
+            }
+        },
+
+        fetch: function(options) {
+            var self=this;
+            this.fetching=true;
+            //console.log("fetch " + this.modelName);
+            if (!this.startedLoad) {
+                this.startedLoad=true;
+                Backbone.trigger("xoslib:collectionLoadChange", this);
+            }
+            this.trigger("fetchStateChange");
+            if (options == undefined) {
+                options = {};
+            }
+            options["orig_success"] = options["success"];
+            options["orig_failure"] = options["failure"];
+            options["success"] = function(collection, response, options) { self.fetchSuccess.call(self, collection, response, options); };
+            options["failure"] = this.fetchFailure;
+            Backbone.Collection.prototype.fetch.call(this, options);
+        },
+
+        startPolling: function() {
+            if (!this._polling) {
                 var collection=this;
                 setInterval(function() { collection.fetch(); }, 10000);
                 this._polling=true;
@@ -333,13 +335,13 @@
             },
 
         listMethods: function() {
-                var res = [];

-                for(var m in this) {

-                    if(typeof this[m] == "function") {

-                        res.push(m)

-                    }

-                }

-                return res;

+                var res = [];
+                for(var m in this) {
+                    if(typeof this[m] == "function") {
+                        res.push(m)
+                    }
+                }
+                return res;
             },
     });
 
@@ -787,29 +789,29 @@
     xos = new xoslib();
 
     function getCookie(name) {
-        var cookieValue = null;

-        if (document.cookie && document.cookie != '') {

-            var cookies = document.cookie.split(';');

-            for (var i = 0; i < cookies.length; i++) {

-                var cookie = jQuery.trim(cookies[i]);

-                // Does this cookie string begin with the name we want?

-                if (cookie.substring(0, name.length + 1) == (name + '=')) {

-                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));

-                    break;

-                }

-            }

-        }

-        return cookieValue;

+        var cookieValue = null;
+        if (document.cookie && document.cookie != '') {
+            var cookies = document.cookie.split(';');
+            for (var i = 0; i < cookies.length; i++) {
+                var cookie = jQuery.trim(cookies[i]);
+                // Does this cookie string begin with the name we want?
+                if (cookie.substring(0, name.length + 1) == (name + '=')) {
+                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+                    break;
+                }
+            }
+        }
+        return cookieValue;
     }
 
     (function() {
-      var _sync = Backbone.sync;

-      Backbone.sync = function(method, model, options){

-        options.beforeSend = function(xhr){

-          var token = getCookie("csrftoken");

-          xhr.setRequestHeader('X-CSRFToken', token);

-        };

-        return _sync(method, model, options);

-      };

+      var _sync = Backbone.sync;
+      Backbone.sync = function(method, model, options){
+        options.beforeSend = function(xhr){
+          var token = getCookie("csrftoken");
+          xhr.setRequestHeader('X-CSRFToken', token);
+        };
+        return _sync(method, model, options);
+      };
     })();
 }