Preparation to bower release
diff --git a/spec/.eslintrc b/spec/.eslintrc
new file mode 100644
index 0000000..493c9e2
--- /dev/null
+++ b/spec/.eslintrc
@@ -0,0 +1,24 @@
+{
+  "rules": {
+    "angular/ng_angularelement": 0,
+    "angular/ng_no_digest": 0,
+    "angular/ng_json_functions": 0
+  },
+  "globals" :{
+    "module": true,
+    "describe": true,
+    "xdescribe": true,
+    "it": true,
+    "xit": true,
+    "before": true,
+    "beforeEach": true,
+    "after": true,
+    "afterEach": true,
+    "expect": true,
+    "jasmine": true,
+    "inject": true,
+    "spyOn": true,
+    "$": true,
+    "clickElement": true
+  } 
+}
\ No newline at end of file
diff --git a/spec/csrftoken.test.js b/spec/csrftoken.test.js
new file mode 100644
index 0000000..fd00181
--- /dev/null
+++ b/spec/csrftoken.test.js
@@ -0,0 +1,53 @@
+(function () {
+  'use strict';
+  describe('The xos.helper module', function(){
+    let SetCSRFToken, httpProviderObj, httpBackend, http, cookies;
+
+    const fakeToken = 'aiuhsnds98234ndASd';
+
+    beforeEach(function() {
+      module(
+        'xos.helpers',
+        function ($httpProvider) {
+          //save our interceptor
+          httpProviderObj = $httpProvider;
+        }
+      );
+
+      inject(function (_SetCSRFToken_, _$httpBackend_, _$http_, _$cookies_) {
+        SetCSRFToken = _SetCSRFToken_;
+        httpBackend = _$httpBackend_;
+        http = _$http_;
+        cookies = _$cookies_
+
+        // mocking $cookie service
+        spyOn(cookies, 'get').and.returnValue(fakeToken);
+      });
+
+    });
+
+    describe('the SetCSRFToken', () => {
+      it('should exist', () => {
+        expect(SetCSRFToken).toBeDefined();
+      });
+
+      it('should attach token the request', (done) => {
+        httpBackend.when('POST', 'http://example.com', null, function(headers) {
+          expect(headers['X-CSRFToken']).toBe(fakeToken);
+          done();
+          return headers;
+        }).respond(200, {name: 'example' });
+
+        http.post('http://example.com');
+
+        httpBackend.flush();
+      });
+    });
+    
+    it('should set SetCSRFToken interceptor', () => {
+      expect(httpProviderObj).toBeDefined();
+      expect(httpProviderObj.interceptors).toContain('SetCSRFToken');
+    });
+
+  });
+})();
diff --git a/spec/label_formatter.test.js b/spec/label_formatter.test.js
new file mode 100644
index 0000000..119f4ce
--- /dev/null
+++ b/spec/label_formatter.test.js
@@ -0,0 +1,42 @@
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The label formatter service', () => {
+
+      let service;
+
+      // load the application module
+      beforeEach(module('xos.helpers'));
+
+      // inject the cartService
+      beforeEach(inject(function (_LabelFormatter_) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _LabelFormatter_;
+      }));
+
+      it('should replace underscores in a string', () => {
+        expect(service._formatByUnderscore('my_test')).toEqual('my test');
+        expect(service._formatByUnderscore('_test')).toEqual('test');
+      });
+
+      it('should split a camel case string', () => {
+        expect(service._formatByUppercase('myTest')).toEqual('my test');
+      });
+
+      it('should capitalize a string', () => {
+        expect(service._capitalize('my test')).toEqual('My test');
+      });
+
+      it('should format an object property to a label', () => {
+        expect(service.format('myWeird_String')).toEqual('My weird string:');
+      });
+
+      it('should not add column if already present', () => {
+        expect(service.format('myWeird_String:')).toEqual('My weird string:');
+      });
+
+    });
+  });
+
+})();
\ No newline at end of file
diff --git a/spec/log.test.js b/spec/log.test.js
new file mode 100644
index 0000000..21085c6
--- /dev/null
+++ b/spec/log.test.js
@@ -0,0 +1,57 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/18/16.
+ */
+/* eslint-disable angular/ng_window_service*/
+
+// TODO write tests for log
+// NODE Actually the code is working, the tests are not.
+
+(function () {
+  'use strict';
+
+  xdescribe('The xos.helper module', function(){
+
+    let log, window;
+
+    let mockLog;
+
+    beforeEach(function() {
+      mockLog = jasmine.createSpyObj('logMock', ['info']);
+    });
+
+    beforeEach(function() {
+      angular.mock.module('xos.helpers', function($injector, $provide) {
+        // console.log('$injector',$injector.get('logDecorator'));
+        $provide.value('$log', mockLog);
+        // $provide.decorator('$log', $injector.get('logDecorator'));
+      });
+    });
+
+    beforeEach(inject(($log, $window) => {
+      log = $log;
+      window = $window;
+      // log.reset();
+    }));
+
+    describe('The log decorator', () => {
+      it('should not print anything', inject(($log) => {
+        // spyOn(log, 'info');
+        $log.info('test');
+        expect(mockLog.info).not.toHaveBeenCalled();
+      }));
+
+    });
+    describe('if logging is enabled', () => {
+      beforeEach(() => {
+        window.location.href += '?debug=true'
+      });
+
+      it('should should log', () => {
+        log.info('test');
+        console.log(log.info.logs);
+      });
+    });
+  });
+})();
diff --git a/spec/noHyperlinks.test.js b/spec/noHyperlinks.test.js
new file mode 100644
index 0000000..7b6e9d0
--- /dev/null
+++ b/spec/noHyperlinks.test.js
@@ -0,0 +1,51 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The NoHyperlinks factory', () => {
+
+      let httpProviderObj, noHyperlinks;
+
+      beforeEach(() => {
+        module(
+          'xos.helpers',
+          ($httpProvider) => {
+            //save our interceptor
+            httpProviderObj = $httpProvider;
+          }
+        );
+
+        inject(function (_NoHyperlinks_) {
+          noHyperlinks = _NoHyperlinks_
+        });
+
+        httpProviderObj.interceptors.push('NoHyperlinks');
+
+      });
+
+      it('should set NoHyperlinks interceptor', () => {
+        expect(httpProviderObj.interceptors).toContain('NoHyperlinks');
+      });
+
+      it('should attach ?no_hyperlinks=1 to the request url', () => {
+        let result = noHyperlinks.request({url: 'sample.url'});
+        expect(result.url).toEqual('sample.url?no_hyperlinks=1');
+      });
+
+      it('should NOT attach ?no_hyperlinks=1 to the request url if is HTML', () => {
+        let result = noHyperlinks.request({url: 'sample.html'});
+        expect(result.url).toEqual('sample.html');
+      });
+
+    });
+  });
+})();
+
diff --git a/spec/notification.test.js b/spec/notification.test.js
new file mode 100644
index 0000000..cbc1e56
--- /dev/null
+++ b/spec/notification.test.js
@@ -0,0 +1,63 @@
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The xosNotification service', () => {
+
+      let service, scope;
+
+      const options = {icon: 'icon', body: 'message'};
+
+      let notificationMock = {
+        requestPermission: () => {
+          return {
+            then: cb => cb('granted')
+          }
+        },
+        permission: 'granted'
+      }
+
+
+      // load the application module
+      beforeEach(module('xos.helpers', ($provide) => {
+        $provide.value('Notification', notificationMock);
+      }));
+
+      // inject the cartService
+      beforeEach(inject(function (_xosNotification_, $rootScope) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _xosNotification_;
+        scope = $rootScope;
+        spyOn(service, 'sendNotification');
+        spyOn(service, 'checkPermission').and.callThrough();
+        spyOn(notificationMock, 'requestPermission').and.callThrough();
+      }));
+
+      it('should exist', () => {
+        expect(service).toBeDefined();
+      });
+
+      describe('when permission are granted', () => {
+        it('should send the notification', () => {
+          service.notify('Test', options);
+          expect(service.sendNotification).toHaveBeenCalledWith('Test', options);
+        });
+      });
+
+      describe('when permission are not granted', () => {
+        beforeEach(() => {
+          notificationMock.permission = false;
+        });
+
+        it('should request permission', () => {
+          service.notify('Test', options);
+          expect(service.checkPermission).toHaveBeenCalled();
+          scope.$apply(); // this resolve the promise
+          expect(service.sendNotification).toHaveBeenCalledWith('Test', options);
+        });
+      });
+
+    });
+  });
+
+})();
\ No newline at end of file
diff --git a/spec/services/helpers/comparator.test.js b/spec/services/helpers/comparator.test.js
new file mode 100644
index 0000000..66e26d3
--- /dev/null
+++ b/spec/services/helpers/comparator.test.js
@@ -0,0 +1,60 @@
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The Comparator service', () => {
+
+      let service;
+
+      // load the application module
+      beforeEach(module('xos.helpers'));
+
+      // inject the cartService
+      beforeEach(inject(function (_Comparator_) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _Comparator_;
+      }));
+
+      describe('given a string', () => {
+        it('should return true if expected is substring of actual', () => {
+          const res = service('test', 'te');
+          expect(res).toBeTruthy();
+        });
+
+        it('should return false if expected is not substring of actual', () => {
+          const res = service('test', 'ab');
+          expect(res).toBeFalsy();
+        });
+      });
+
+      describe('given a boolean', () => {
+        it('should return true if values match', () => {
+          expect(service(false, false)).toBeTruthy();
+          expect(service(true, true)).toBeTruthy();
+          expect(service(0, false)).toBeTruthy();
+          expect(service(1, true)).toBeTruthy();
+        });
+
+        it('should return false if values doesn\'t match', () => {
+          expect(service(false, true)).toBeFalsy();
+          expect(service(true, false)).toBeFalsy();
+          expect(service(1, false)).toBeFalsy();
+          expect(service(0, true)).toBeFalsy();
+        });
+      });
+
+      describe('given a number', () => {
+        // NOTE if numbers should we compare with === ??
+        it('should return true if expected is substring of actual', () => {
+          expect(service(12, 1)).toBeTruthy();
+        });
+
+        it('should return false if expected is not substring of actual', () => {
+          expect(service(12, 3)).toBeFalsy();
+        });
+      });
+
+    });
+  });
+
+})();
\ No newline at end of file
diff --git a/spec/services/helpers/form.helpers.test.js b/spec/services/helpers/form.helpers.test.js
new file mode 100644
index 0000000..a126db5
--- /dev/null
+++ b/spec/services/helpers/form.helpers.test.js
@@ -0,0 +1,335 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+
+    describe('The XosFormHelper service', () => {
+      let service;
+
+      let fields = [
+        'id',
+        'name',
+        'mail',
+        'active',
+        'created'
+      ];
+
+      let modelField = {
+        id: {},
+        name: {},
+        mail: {},
+        active: {},
+        created: {}
+      };
+
+      let model = {
+        id: 1,
+        name: 'test',
+        mail: 'test@onlab.us',
+        active: true,
+        created: '2016-04-18T23:44:16.883181Z',
+        custom: 'MyCustomValue'
+      };
+
+      let customField = {
+        id: {
+          label: 'Id',
+          type: 'number',
+          validators: {
+            required: true
+          },
+          hint: ''
+        },
+        custom: {
+          label: 'Custom Label',
+          type: 'number',
+          validators: {},
+          hint: 'Test Hint'
+        }
+      };
+
+      let formObject = {
+        id: {
+          label: 'Id:',
+          type: 'number',
+          validators: {
+            required: true
+          },
+          hint: ''
+        },
+        name: {
+          label: 'Name:',
+          type: 'text',
+          validators: {},
+          hint: ''
+        },
+        mail: {
+          label: 'Mail:',
+          type: 'email',
+          validators: {},
+          hint: ''
+        },
+        active: {
+          label: 'Active:',
+          type: 'boolean',
+          validators: {},
+          hint: ''
+        },
+        created: {
+          label: 'Created:',
+          type: 'date',
+          validators: {},
+          hint: ''
+        },
+        custom: {
+          label: 'Custom Label:',
+          type: 'number',
+          validators: {},
+          hint: 'Test Hint'
+        }
+      };
+
+      // load the application module
+      beforeEach(module('xos.helpers'));
+
+      // inject the cartService
+      beforeEach(inject(function (_XosFormHelpers_) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _XosFormHelpers_;
+      }));
+
+      describe('the _isEmail method', () => {
+        it('should return true', () => {
+          expect(service._isEmail('test@onlab.us')).toEqual(true);
+        });
+        it('should return false', () => {
+          expect(service._isEmail('testonlab.us')).toEqual(false);
+          expect(service._isEmail('test@onlab')).toEqual(false);
+        });
+      });
+
+      describe('the _getFieldFormat method', () => {
+        it('should return text', () => {
+          expect(service._getFieldFormat('a random text')).toEqual('text');
+          expect(service._getFieldFormat(null)).toEqual('text');
+          expect(service._getFieldFormat('1')).toEqual('text');
+        });
+        it('should return mail', () => {
+          expect(service._getFieldFormat('test@onlab.us')).toEqual('email');
+        });
+        it('should return number', () => {
+          expect(service._getFieldFormat(1)).toEqual('number');
+        });
+        it('should return boolean', () => {
+          expect(service._getFieldFormat(false)).toEqual('boolean');
+          expect(service._getFieldFormat(true)).toEqual('boolean');
+        });
+
+        it('should return date', () => {
+          expect(service._getFieldFormat('2016-04-19T23:09:1092Z')).toEqual('text');
+          expect(service._getFieldFormat(new Date())).toEqual('date');
+          expect(service._getFieldFormat('2016-04-19T23:09:10.208092Z')).toEqual('date');
+        });
+
+        it('should return array', () => {
+          expect(service._getFieldFormat([])).toEqual('array');
+          expect(service._getFieldFormat(['a', 'b'])).toEqual('array');
+        });
+
+        it('should return object', () => {
+          expect(service._getFieldFormat({})).toEqual('object');
+          expect(service._getFieldFormat({foo: 'bar'})).toEqual('object');
+        });
+      });
+
+      describe('the parseModelField mehtod', () => {
+        it('should convert the fields array in an empty form object', () => {
+          expect(service.parseModelField(fields)).toEqual(modelField);
+        });
+
+        xit('should handle nested config', () => {
+          
+        });
+      });
+
+      describe('when modelField are provided', () => {
+        it('should combine modelField and customField in a form object', () => {
+          const form = service.buildFormStructure(modelField, customField, model);
+          expect(form).toEqual(formObject);
+        });
+
+        it('should override modelField properties whith customField properties', () => {
+          const customFieldOverride = {
+            id: {
+              hint: 'something',
+              type: 'select',
+              options: [
+                {id: 1, label: 'one'},
+                {id: 2, label: 'two'}
+              ],
+              validators: {
+                required: true
+              }
+            }
+          };
+          const form = service.buildFormStructure({id: {}}, customFieldOverride, model);
+          
+          expect(form).toEqual({
+            id: {
+              label: 'Id:',
+              validators: {required: true},
+              hint: customFieldOverride.id.hint,
+              type: customFieldOverride.id.type,
+              options: customFieldOverride.id.options
+            }
+          });
+        });
+      });
+
+      describe('when model field is an empty array', () => {
+        let empty_modelField = {
+          // 5: {}
+        };
+        let empty_customFields = {
+          id: {
+            label: 'Id',
+            type: 'number'
+          },
+          name: {
+            label: 'Name',
+            type: 'text'
+          },
+          mail: {
+            label: 'Mail',
+            type: 'email'
+          },
+          active: {
+            label: 'Active',
+            type: 'boolean'
+          },
+          created: {
+            label: 'Created',
+            type: 'date'
+          },
+          custom: {
+            label: 'Custom Label',
+            type: 'number',
+            hint: 'Test Hint'
+          },
+          select: {
+            label: 'Select Label',
+            type: 'select',
+            hint: 'Select Hint',
+            options: [
+              {id: 1, label: 'something'}
+            ]
+          },
+          object: {
+            label: 'Object Label',
+            type: 'object',
+            hint: 'Object Hint',
+            properties: {
+              foo: {
+                type: 'string',
+                label: 'FooLabel',
+                validators: {
+                  required: true
+                }
+              },
+              bar: {
+                type: 'number'
+              }
+            }
+          }
+        };
+
+        let empty_formObject = {
+          id: {
+            label: 'Id:',
+            type: 'number',
+            validators: {},
+            hint: ''
+          },
+          name: {
+            label: 'Name:',
+            type: 'text',
+            validators: {},
+            hint: ''
+          },
+          mail: {
+            label: 'Mail:',
+            type: 'email',
+            validators: {},
+            hint: ''
+          },
+          active: {
+            label: 'Active:',
+            type: 'boolean',
+            validators: {},
+            hint: ''
+          },
+          created: {
+            label: 'Created:',
+            type: 'date',
+            validators: {},
+            hint: ''
+          },
+          custom: {
+            label: 'Custom Label:',
+            type: 'number',
+            validators: {},
+            hint: 'Test Hint'
+          },
+          select: {
+            label: 'Select Label:',
+            type: 'select',
+            hint: 'Select Hint',
+            validators: {},
+            options: [
+              {id: 1, label: 'something'}
+            ]
+          },
+          object: {
+            label: 'Object Label:',
+            type: 'object',
+            hint: 'Object Hint',
+            validators: {},
+            properties: {
+              foo: {
+                type: 'string',
+                label: 'FooLabel',
+                validators: {
+                  required: true
+                }
+              },
+              bar: {
+                type: 'number'
+              }
+            }
+          }
+        };
+
+        let empty_model = {5: 'Nan'}
+
+        it('should create a form object', () => {
+          let res = service.buildFormStructure(empty_modelField, empty_customFields, empty_model);
+          expect(res.id).toEqual(empty_formObject.id);
+          expect(res.name).toEqual(empty_formObject.name);
+          expect(res.mail).toEqual(empty_formObject.mail);
+          expect(res.active).toEqual(empty_formObject.active);
+          expect(res.created).toEqual(empty_formObject.created);
+          expect(res.custom).toEqual(empty_formObject.custom);
+          expect(res.select).toEqual(empty_formObject.select);
+          expect(res.object).toEqual(empty_formObject.object);
+          expect(res).toEqual(empty_formObject);
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/services/helpers/user-prefs.test.js b/spec/services/helpers/user-prefs.test.js
new file mode 100644
index 0000000..63b55c9
--- /dev/null
+++ b/spec/services/helpers/user-prefs.test.js
@@ -0,0 +1,183 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+  'use strict';
+
+  let cookies = {
+    xosUserPrefs: JSON.stringify({test: true})
+  };
+
+  const cookieMock = {
+    get: (name) => {
+      return cookies[name]
+    },
+    put: (name, value) => {
+      cookies[name] = value
+    }
+  };
+
+  describe('The xos.helper module', function(){
+
+    describe('The XosUserPrefs service', () => {
+      let service, deffered, rootScope;
+
+      // load the application module
+      beforeEach(module('xos.helpers', ($provide) => {
+        $provide.value('$cookies', cookieMock);
+      }));
+
+      // inject the cartService
+      beforeEach(inject(function (_XosUserPrefs_) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _XosUserPrefs_;
+        spyOn(cookieMock, 'put').and.callThrough();
+      }));
+
+      beforeEach(inject(($q, $rootScope) => {
+        rootScope = $rootScope;
+      }));
+      it('should exists and have methods', () => {
+        expect(service).toBeDefined();
+        expect(service.getAll).toBeDefined();
+        expect(service.setAll).toBeDefined();
+        expect(service.getSynchronizerNotificationStatus).toBeDefined();
+        expect(service.setSynchronizerNotificationStatus).toBeDefined();
+        expect(service.setUserDetailsCookie).toBeDefined();
+        expect(service.getUserDetailsCookie).toBeDefined();
+      });
+
+      describe('the getAll method', () => {
+        it('should return all the stored prefs', () => {
+          let prefs = service.getAll();
+          expect(prefs).toEqual(JSON.parse(cookies.xosUserPrefs));
+        });
+      });
+
+      describe('the setAll method', () => {
+        it('should override all preferences', () => {
+          service.setAll({test: true, updated: true});
+          expect(JSON.parse(cookies.xosUserPrefs)).toEqual({test: true, updated: true});
+        });
+      });
+
+      describe('the synchronizers status', () => {
+        let syncNotification;
+        beforeEach(() => {
+          syncNotification = {
+            synchronizers: {
+              notification: {
+                first: true,
+                second: false
+              }
+            },
+            userData: {
+              userId: 1
+            }
+          }
+          cookies.xosUserPrefs = JSON.stringify(syncNotification);
+        });
+
+        describe('the getSynchronizerNotificationStatus method', () => {
+          it('should return notification status for all synchronizers', () => {
+            expect(service.getSynchronizerNotificationStatus()).toEqual(syncNotification.synchronizers.notification);
+          });
+
+          it('should return notification status for a single synchronizers', () => {
+            expect(service.getSynchronizerNotificationStatus('first')).toEqual(syncNotification.synchronizers.notification.first);
+            expect(service.getSynchronizerNotificationStatus('second')).toEqual(syncNotification.synchronizers.notification.second);
+          });
+        });
+
+        describe('the setSynchronizerNotificationStatus', () => {
+          
+          it('should throw an error if called without synchronizer name', () => {
+            function wrapper (){
+              service.setSynchronizerNotificationStatus();
+            }
+            expect(wrapper).toThrowError('[XosUserPrefs] When updating a synchronizer is mandatory to provide a name.');
+          });
+
+          it('should update a synchronizer notification status', () => {
+            service.setSynchronizerNotificationStatus('second', true);
+            expect(service.getSynchronizerNotificationStatus('second')).toEqual(true);
+            expect(service.getSynchronizerNotificationStatus('first')).toEqual(true);
+
+            // should persist the change
+            expect(cookieMock.put).toHaveBeenCalledWith('xosUserPrefs', '{"synchronizers":{"notification":{"first":true,"second":true}},"userData":{"userId":1}}');
+          });
+
+          it('should handle empty cookies', () => {
+            cookies.xosUserPrefs = '';
+            service.setSynchronizerNotificationStatus('second', true);
+            expect(service.getSynchronizerNotificationStatus('second')).toEqual(true);
+          });
+        });
+      });
+
+      describe('the userdetails status', () => {
+        let userdata, meMock;
+        const userData = {
+          current_user_id: 2
+        };
+        beforeEach(inject(($q, Me) => {
+          userdata = {
+            userData: {
+              current_user_id: 1
+            }
+          }
+          cookies.xosUserPrefs = JSON.stringify(userdata);
+          deffered= $q.defer();
+          meMock = Me;
+          spyOn(meMock, 'get').and.callFake(function(){
+            return deffered.promise;
+          });
+        }));
+        describe('Get user data method', ()=>{
+          it('it should return the stored data', (done) => {
+            service.getUserDetailsCookie().$promise.then((data) => {
+              expect(data).toEqual(userdata.userData);
+              done();
+            });
+            rootScope.$apply();
+          });
+
+          it('Should call the Api to return the data', (done) => {
+            cookies.xosUserPrefs = JSON.stringify();
+            service.getUserDetailsCookie().$promise.then((res) => {
+              expect(res.userId).toEqual(userData.userId);
+              expect(meMock.get).toHaveBeenCalled();
+              done();
+            });
+            deffered.resolve(userData);
+            rootScope.$apply();
+          });
+
+        });
+        describe('Set user data method', ()=>{
+          it('it should save the provided data', (done) => {
+            service.setUserDetailsCookie(userData).$promise.then(data => {
+              expect(data).toEqual(userData);
+              expect(cookieMock.put).toHaveBeenCalledWith('xosUserPrefs',  '{"userData":{"current_user_id":2}}');
+              done();
+            });
+            rootScope.$apply();
+          });
+          it('Should call the Api to save the data', (done)=>{
+            service.setUserDetailsCookie().$promise.then(data => {
+              expect(meMock.get).toHaveBeenCalled();
+              expect(data).toEqual(userData);
+              expect(cookieMock.put).toHaveBeenCalledWith('xosUserPrefs',  '{"userData":{"current_user_id":2}}');
+              done();
+            });
+            deffered.resolve(userData);
+            rootScope.$apply();
+          });
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/test_helpers.js b/spec/test_helpers.js
new file mode 100644
index 0000000..c373c5e
--- /dev/null
+++ b/spec/test_helpers.js
@@ -0,0 +1,50 @@
+/**
+ * Collection of helpers for xos tests
+ */
+
+/* exported clickElement */
+/* eslint-disable angular/ng_document_service */
+
+const clickElement = function (el){
+  const ev = document.createEvent('MouseEvent');
+  ev.initMouseEvent(
+    'click',
+    true /* bubble */, true /* cancelable */,
+    window, null,
+    0, 0, 0, 0, /* coordinates */
+    false, false, false, false, /* modifier keys */
+    0 /*left*/, null
+  );
+  el.dispatchEvent(ev);
+};
+
+describe('Matchers inclusion', () => {
+  beforeEach(function(){
+    jasmine.addMatchers({
+      toBeInstanceOf: function() {
+
+        return {
+          compare: (actual, expected) => {
+            // const actual = actual;
+            const result = {};
+            result.pass = actual instanceof expected.constructor;
+
+            result.message = 'Expected ' + actual + ' to be instance of ' + expected;
+
+            return result;
+          },
+          negativeCompare: (actual, expected) => {
+            // const actual = actual;
+            const result = {};
+            result.pass = actual instanceof expected.constructor === false;
+
+            result.message = 'Expected ' + actual + ' to be instance of ' + expected;
+
+            return result;
+          }
+        }
+      }
+    });
+  });
+});
+console.log('---------------------- Test Helpers Loaded!! -----------------------');
diff --git a/spec/ui/alert.test.js b/spec/ui/alert.test.js
new file mode 100644
index 0000000..c67d03a
--- /dev/null
+++ b/spec/ui/alert.test.js
@@ -0,0 +1,131 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The xos-alert component', () => {
+
+      let element, scope, isolatedScope;
+
+      let message = 'Test Error Message';
+
+      beforeEach(module('xos.helpers'));
+
+      it('should throw an error if no config is specified', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          $compile(angular.element('<xos-alert></xos-alert>'))($rootScope);
+          $rootScope.$digest();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosAlert] Please provide a configuration via the "config" attribute'));
+      }));
+
+      describe('when correctly configured', () => {
+        beforeEach(inject(($compile, $rootScope) => {
+
+          scope = $rootScope.$new();
+
+          scope.config = {
+            type: 'danger',
+            closeBtn: true
+          };
+
+          element = angular.element(`<xos-alert config="config">${message}</xos-alert>`);
+          $compile(element)(scope);
+          scope.$digest();
+          isolatedScope = element.isolateScope().vm;
+        }));
+
+        it('should transclude the message', () => {
+          let textContainer = element[0].getElementsByTagName('p')[0];
+          let text = angular.element(textContainer).text();
+          expect(text).toEqual(message)
+        });
+
+        it('should have a close button', () => {
+          let btn = element[0].getElementsByTagName('button');
+          expect(btn.length).toEqual(1);
+        });
+
+        describe('when the close button is clicked', () => {
+          it('should hide the alert', () => {
+            let btn = element[0].getElementsByTagName('button')[0];
+            btn.click();
+            let alert = angular.element(element[0].querySelectorAll('.alert')[0]);
+            expect(alert.hasClass('ng-hide')).toBeTruthy();
+          });
+        });
+
+        describe('when autoHide is set', () => {
+
+          let to;
+
+          beforeEach(inject(($compile, $rootScope, $timeout) => {
+            scope = $rootScope.$new();
+
+            scope.config = {
+              type: 'danger',
+              closeBtn: true,
+              autoHide: 500
+            };
+
+            to = $timeout;
+
+            element = angular.element(`<xos-alert config="config">${message}</xos-alert>`);
+            $compile(element)(scope);
+            scope.$digest();
+            isolatedScope = element.isolateScope().vm;
+          }));
+
+          it('should hide the alert', () => {
+            to.flush();
+            expect(isolatedScope.show).toBeFalsy();
+            let alert = angular.element(element[0].querySelectorAll('.alert')[0]);
+            expect(alert.hasClass('ng-hide')).toBeTruthy();
+          });
+        });
+
+        describe('when show is set to false', () => {
+
+          beforeEach(inject(($compile, $rootScope) => {
+            scope = $rootScope.$new();
+
+            scope.config = {
+              type: 'danger',
+              closeBtn: true
+            };
+
+            scope.show = false;
+
+            element = angular.element(`<xos-alert config="config" show="show">${message}</xos-alert>`);
+            $compile(element)(scope);
+            scope.$digest();
+            isolatedScope = element.isolateScope().vm;
+          }));
+
+          it('should hide the alert', () => {
+            let alert = angular.element(element[0].querySelectorAll('.alert')[0]);
+            expect(alert.hasClass('ng-hide')).toBeTruthy();
+          });
+
+          describe('when show is changed to true', () => {
+            beforeEach(() => {
+              scope.show = true;
+              scope.$digest();
+            });
+
+            it('should show the alert', () => {
+              let alert = angular.element(element[0].querySelectorAll('.alert')[0]);
+              expect(alert.hasClass('ng-hide')).toBeFalsy();
+            });
+          });
+        });
+
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/custom-validator.test.js b/spec/ui/custom-validator.test.js
new file mode 100644
index 0000000..c85c934
--- /dev/null
+++ b/spec/ui/custom-validator.test.js
@@ -0,0 +1,107 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function () {
+    describe('The xosCustomValidator directive', () => {
+      let element, scope, isolatedScope, rootScope, compile, form, input;
+      const compileElement = (el) => {
+        element = el;
+
+        if(!scope){
+          scope = rootScope.$new();
+        }
+        if(angular.isUndefined(element)){
+          element = angular.element(`
+            <form name="form">
+              <input name="testInput" type="text" ng-model="value" xos-custom-validator custom-validator="validator"/>
+            </form>
+          `);
+        }
+        compile(element)(scope);
+        scope.$digest();
+        input = $(element).find('input');
+        isolatedScope = angular.element(input).isolateScope();
+        form = scope.form;
+      };
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(inject(function ($compile, $rootScope) {
+        compile = $compile;
+        rootScope = $rootScope;
+      }));
+
+      beforeEach(() => {
+        scope = rootScope.$new();
+        scope.validator = 'validator';
+        scope.value = '';
+        compileElement();
+      });
+
+      it('should bind the validator', () => {
+        expect(isolatedScope.fn).toEqual('validator');
+      });
+
+      describe('given a validator function', () => {
+
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.value = '';
+          scope.validator = (model) => angular.equals(model, 'test');
+          spyOn(scope, 'validator').and.callThrough();
+          compileElement();
+        });
+
+        it('should call the validator function on value change', () => {
+          form.testInput.$setViewValue('something');
+          scope.$digest();
+          expect(scope.validator).toHaveBeenCalledWith('something');
+          expect(scope.value).toEqual('something');
+        });
+
+        it('should set the field invalid', () => {
+          form.testInput.$setViewValue('something');
+          scope.$digest();
+          expect(scope.validator).toHaveBeenCalledWith('something');
+          expect(input).toHaveClass('ng-invalid');
+          expect(input).toHaveClass('ng-invalid-custom');
+        });
+
+        it('should set the field valid', () => {
+          form.testInput.$setViewValue('test');
+          scope.$digest();
+          expect(scope.validator).toHaveBeenCalledWith('test');
+          expect(input).not.toHaveClass('ng-invalid');
+          expect(input).not.toHaveClass('ng-invalid-custom');
+        });
+
+        describe('if the validation function return an array', () => {
+
+          beforeEach(() => {
+            scope = rootScope.$new();
+            scope.value = '';
+            scope.validator = (model) => {
+              return ['randomTest', angular.equals(model, 'test')];
+            };
+            spyOn(scope, 'validator').and.callThrough();
+            compileElement();
+          });
+
+          it('should set the field invalid', () => {
+            form.testInput.$setViewValue('something');
+            scope.$digest();
+            expect(scope.validator).toHaveBeenCalledWith('something');
+            expect(input).toHaveClass('ng-invalid');
+            expect(input).toHaveClass('ng-invalid-random-test');
+          });
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/field.test.js b/spec/ui/field.test.js
new file mode 100644
index 0000000..bfb36a3
--- /dev/null
+++ b/spec/ui/field.test.js
@@ -0,0 +1,330 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+
+    describe('The xosField component', () => {
+      let element, scope, isolatedScope, rootScope, compile;
+      const compileElement = (el) => {
+        element = el;
+
+        if(!scope){
+          scope = rootScope.$new();
+        }
+        if(angular.isUndefined(element)){
+          element = angular.element('<xos-field name="name" field="field" ng-model="ngModel"></xos-field>');
+        }
+        compile(element)(scope);
+        scope.$digest();
+        isolatedScope = element.isolateScope().vm;
+      };
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(inject(function ($compile, $rootScope) {
+        compile = $compile;
+        rootScope = $rootScope;
+      }));
+
+      it('should throw an error if no name is passed', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = $rootScope.$new();
+          scope.field = {
+            label: 'Label',
+            type: 'number',
+            validators: {}
+          };
+          scope.ngModel = 1;
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide a field name'));
+      }));
+
+      it('should throw an error if no field definition is passed', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = $rootScope.$new();
+          scope.name = 'label';
+          scope.ngModel = 1;
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide a field definition'));
+      }));
+
+      it('should throw an error if no field type is passed', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = $rootScope.$new();
+          scope.name = 'label';
+          scope.ngModel = 1;
+          scope.field = {label: 'Label:'}
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide a type in the field definition'));
+      }));
+
+      it('should throw an error if no field model is passed', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = $rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'number',
+            validators: {}
+          };
+          compileElement(angular.element('<xos-field name="name" field="field"></xos-field>'));
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide an ng-model'));
+      }));
+
+      describe('when a text input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'text',
+            validators: {
+              custom: 'fake'
+            }
+          };
+          scope.ngModel = 'label';
+          compileElement();
+        });
+
+        it('should print a text field', () => {
+          expect($(element).find('[name="label"]')).toHaveAttr('type', 'text');
+        });
+
+        it('should attach the custom validator directive', () => {
+          let input = $(element).find('[name="label"]');
+          expect(input).toHaveAttr('xos-custom-validator');
+          expect(input).toHaveAttr('custom-validator', 'vm.field.validators.custom || null');
+        });
+      });
+
+      describe('when a option is selected in dropdown', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'select',
+            validators: {},
+            options: [
+              {
+                id: 0,
+                label: '---Site---'
+              },
+              {
+                id: 1,
+                label: '---Site1---'
+              }
+            ]
+          };
+          scope.ngModel = 0;
+          compileElement();
+        });
+
+        it('No of select elements', () => {
+          expect($(element).find('select').children('option').length).toEqual(2);
+        });
+
+        it('should show the selected value', () => {
+          const elem =  angular.element($(element).find('select').children('option')[0]);
+          expect(elem.text()).toEqual('---Site---');
+          expect(elem).toHaveAttr('selected');
+        });
+      });
+
+      describe('when a number input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'number',
+            validators: {}
+          };
+          scope.ngModel = 10;
+          compileElement();
+        });
+
+        it('should print a number field', () => {
+          expect($(element).find('[name="label"]')).toHaveAttr('type', 'number');
+        });
+      });
+
+      describe('when a boolean input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'boolean',
+            validators: {}
+          };
+          scope.ngModel = true;
+          compileElement();
+        });
+
+        let setFalse, setTrue;
+
+        beforeEach(() => {
+          setFalse= $(element).find('.boolean-field > a:first-child');
+          setTrue = $(element).find('.boolean-field > a:last-child');
+        });
+
+        it('should print two buttons', () => {
+          expect($(element).find('.boolean-field > a').length).toEqual(2)
+        });
+
+        it('should change value to false', () => {
+          expect(isolatedScope.ngModel).toEqual(true);
+          clickElement(setFalse[0]);
+          expect(isolatedScope.ngModel).toEqual(false);
+        });
+
+        it('should change value to true', () => {
+          isolatedScope.ngModel = false;
+          scope.$apply();
+          expect(isolatedScope.ngModel).toEqual(false);
+          clickElement(setTrue[0]);
+          expect(isolatedScope.ngModel).toEqual(true);
+        });
+      });
+
+      describe('when an object input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'object',
+            validators: {}
+          };
+          scope.ngModel = {
+            baz: true,
+            foo: 'bar',
+            foz: 1,
+          };
+          compileElement();
+        });
+
+        it('should print a panel to contain object property field', () => {
+          expect($(element).find('.panel.object-field')).toExist()
+        });
+
+        it('should print the right input type for each property', () => {
+          expect($(element).find('input').length).toBe(2);
+          expect($(element).find('.boolean-field > a').length).toEqual(2);
+        });
+
+        it('should format labels', () => {
+          expect($(element).find('input[name="foo"]').parent().find('label').text()).toBe('Foo:');
+        });
+
+        describe('and the model is empty', () => {
+          beforeEach(() => {
+            scope.ngModel = {
+            };
+            compileElement();
+          });
+
+          it('should not print the panel', () => {
+            expect($(element).find('.panel.object-field')).not.toExist()
+          });
+
+          describe('but field is configured', () => {
+            beforeEach(() => {
+              scope.field.properties = {
+                foo: {
+                  label: 'FooLabel:',
+                  type: 'string',
+                  validators: {
+                    required: true
+                  }
+                },
+                bar: {
+                  type: 'number'
+                }
+              };
+              compileElement();
+            });
+            it('should render panel and configured fields', () => {
+              expect($(element).find('.panel.object-field')).toExist();
+              expect($(element).find('input[name="foo"]').parent().find('label').text()).toBe('FooLabel:');
+              expect($(element).find('input[name="foo"]')).toHaveAttr('type', 'string');
+              expect($(element).find('input[name="foo"]')).toHaveAttr('required');
+
+              expect($(element).find('input[name="bar"]').parent().find('label').text()).toBe('Bar:');
+              expect($(element).find('input[name="bar"]')).toHaveAttr('type', 'number');
+
+            });
+          });
+        });
+      });
+
+      describe('when validation options are passed', () => {
+        let input;
+        describe('given a a text field', () => {
+          beforeEach(() => {
+            scope.field = {
+              label: 'Label',
+              type: 'text',
+              validators: {
+                minlength: 10,
+                maxlength: 15,
+                required: true
+              }
+            };
+
+            scope.$digest();
+            input = $(element).find('input');
+          });
+
+          it('should validate required', () => {
+            scope.ngModel= null;
+            scope.$digest();
+            expect(input).toHaveClass('ng-invalid-required');
+
+            scope.ngModel= 'not too short';
+            scope.$digest();
+            expect(input).not.toHaveClass('ng-invalid-required');
+            expect(input).not.toHaveClass('ng-invalid');
+          });
+
+          it('should validate minlength', () => {
+            scope.ngModel= 'short';
+            scope.$digest();
+            expect(input).toHaveClass('ng-invalid-minlength');
+
+            scope.ngModel= 'not too short';
+            scope.$digest();
+            expect(input).not.toHaveClass('ng-invalid-minlength');
+            expect(input).not.toHaveClass('ng-invalid');
+          });
+
+          it('should validate maxlength', () => {
+            scope.ngModel= 'this is definitely too long!!';
+            scope.$digest();
+            expect(input).toHaveClass('ng-invalid-maxlength');
+
+            scope.ngModel= 'not too short';
+            scope.$digest();
+            expect(input).not.toHaveClass('ng-invalid-maxlength');
+            expect(input).not.toHaveClass('ng-invalid');
+          });
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/form.test.js b/spec/ui/form.test.js
new file mode 100644
index 0000000..e92d31a
--- /dev/null
+++ b/spec/ui/form.test.js
@@ -0,0 +1,269 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/18/16.
+ */
+
+(function () {
+  'use strict';
+
+  let element, scope, isolatedScope, rootScope, compile;
+
+  const compileElement = () => {
+
+    if(!scope){
+      scope = rootScope.$new();
+    }
+
+    element = angular.element(`<xos-form config="config" ng-model="model"></xos-form>`);
+    compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  }
+
+  describe('The xos.helper module', function(){
+
+    describe('The xos-form component', () => {
+
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(inject(($compile, $rootScope) => {
+        rootScope = $rootScope;
+        compile = $compile;
+      }));
+
+      it('should throw an error if no config is specified', () => {
+        function errorFunctionWrapper(){
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosForm] Please provide a configuration via the "config" attribute'));
+      });
+
+      it('should throw an error if no actions is specified', () => {
+        function errorFunctionWrapper(){
+          scope = rootScope.$new();
+          scope.config = 'green';
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosForm] Please provide an action list in the configuration'));
+      });
+
+      describe('when correctly configured', () => {
+        
+        let cb = jasmine.createSpy('callback');
+
+        beforeEach(inject(($rootScope) => {
+
+          scope = $rootScope.$new();
+
+          scope.config = {
+            exclude: ['excludedField'],
+            formName: 'testForm',
+            actions: [
+              {
+                label: 'Save',
+                icon: 'ok', // refers to bootstraps glyphicon
+                cb: cb,
+                class: 'success'
+              }
+            ],
+            fields: {
+              first_name: {
+                label: 'Custom Label'
+              }
+            }
+          };
+
+          scope.model = {
+            id: 1,
+            first_name: 'Jhon',
+            last_name: 'Snow',
+            age: 25,
+            email: 'test@onlab.us',
+            birthDate: '2016-04-18T23:44:16.883181Z',
+            enabled: true,
+            role: 'user', //select
+            a_permissions: [
+            ],
+            object_field: {
+              string: 'bar',
+              number: 1,
+              email: 'teo@onlab.us'
+            }
+          };
+
+          compileElement();
+        }));
+
+        it('should add excluded properties to the list', () => {
+          let expected = ['id', 'validators', 'created', 'updated', 'deleted', 'backend_status', 'excludedField'];
+          expect(isolatedScope.excludedField).toEqual(expected);
+        });
+
+        it('should render 10 input field', () => {
+          // boolean are in the form model, but are not input
+          expect(Object.keys(isolatedScope.formField).length).toEqual(9);
+          const field = element[0].getElementsByTagName('input');
+          expect(field.length).toEqual(10);
+        });
+
+        it('should render 1 boolean field', () => {
+          expect($(element).find('.boolean-field > a').length).toEqual(2)
+        });
+
+        it('when clicking on action should invoke callback', () => {
+          const link = $(element).find('[role="button"]');
+          //console.log(link);
+          link.click();
+          // TODO : Check correct parameters
+          expect(cb).toHaveBeenCalled();
+
+        });
+
+        it('should set a custom label', () => {
+          let nameField = element[0].getElementsByClassName('form-group')[0];
+          let label = angular.element(nameField.getElementsByTagName('label')[0]).text()
+          expect(label).toEqual('Custom Label:');
+        });
+
+        it('should use the correct input type', () => {
+          expect($(element).find('[name="age"]')).toHaveAttr('type', 'number');
+          expect($(element).find('[name="birthDate"]')).toHaveAttr('type', 'date');
+          expect($(element).find('[name="email"]')).toHaveAttr('type', 'email');
+        });
+
+        xdescribe('the boolean field test', () => {
+
+          let setFalse, setTrue;
+
+          beforeEach(() => {
+            setFalse= $(element).find('.boolean-field > button:first-child');
+            setTrue = $(element).find('.boolean-field > button:last-child');
+          });
+
+          it('should change value to false', () => {
+            expect(isolatedScope.ngModel.enabled).toEqual(true);
+            setFalse.click();
+            expect(isolatedScope.ngModel.enabled).toEqual(false);
+          });
+
+          it('should change value to true', () => {
+            isolatedScope.ngModel.enabled = false;
+            scope.$apply();
+            expect(isolatedScope.ngModel.enabled).toEqual(false);
+            setTrue.click()
+            expect(isolatedScope.ngModel.enabled).toEqual(true);
+          });
+        });
+
+        describe('when a deep model is passed', () => {
+
+          beforeEach(inject(($rootScope) => {
+
+            scope = $rootScope.$new();
+
+            scope.config = {
+              exclude: ['excludedField'],
+              formName: 'testForm',
+              actions: [
+                {
+                  label: 'Save',
+                  icon: 'ok', // refers to bootstraps glyphicon
+                  cb: cb,
+                  class: 'success'
+                }
+              ],
+              fields: {
+                object_field: {
+                  field_one: {
+                    label: 'Custom Label'
+                  }
+                }
+              }
+            };
+
+            scope.model = {
+              object_field: {
+                field_one: 'bar',
+                number: 1,
+                email: 'teo@onlab.us'
+              }
+            };
+
+            compileElement();
+          }));
+
+          it('should print nested field', () => {
+            expect($(element).find('input').length).toBe(3);
+          });
+
+          xit('should configure nested fields', () => {
+            let custom_label = $(element).find('input[name=field_one]').parent().find('label');
+            expect(custom_label.text()).toBe('Custom Label');
+          });
+        });
+      });
+      describe('when correctly configured for feedback', () => {
+
+        let fb = jasmine.createSpy('feedback').and.callFake(function(statusFlag) {
+          if(statusFlag){
+            scope.config.feedback.show = true;
+            scope.config.feedback.message = 'Form Submitted';
+            scope.config.feedback.type = 'success';
+          }
+          else {
+            scope.config.feedback.show = true;
+            scope.config.feedback.message = 'Error';
+            scope.config.feedback.type = 'danger';
+
+          }
+        });
+
+        beforeEach(()=> {
+          scope = rootScope.$new();
+          scope.config =
+          {
+
+            feedback: {
+              show: false,
+              message: 'Form submitted successfully !!!',
+              type: 'success'
+            },
+            actions: [
+              {
+                label: 'Save',
+                icon: 'ok', // refers to bootstraps glyphicon
+                cb: () => {},
+                class: 'success'
+              }
+            ]
+          };
+          scope.model={};
+          compileElement();
+        });
+
+        it('should not show feedback when loaded', () => {
+          expect($(element).find('xos-alert > div')).toHaveClass('alert alert-success ng-hide');
+        });
+
+        it('should show a success feedback', () => {
+          fb(true);
+          scope.$digest();
+          expect(isolatedScope.config.feedback.type).toEqual('success');
+          expect(fb).toHaveBeenCalledWith(true);
+          expect($(element).find('xos-alert > div')).toHaveClass('alert alert-success');
+        });
+
+        it('should show an error feedback', function() {
+          fb(false);
+          scope.$digest();
+          expect(isolatedScope.config.feedback.type).toEqual('danger');
+          expect(fb).toHaveBeenCalledWith(false);
+          expect($(element).find('xos-alert > div')).toHaveClass('alert alert-danger');
+        });
+      });
+
+    });
+  });
+})();
diff --git a/spec/ui/pagination.test.js b/spec/ui/pagination.test.js
new file mode 100644
index 0000000..03f1045
--- /dev/null
+++ b/spec/ui/pagination.test.js
@@ -0,0 +1,53 @@
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+    describe('The xos-pagination component', () => {
+
+      let scope, element, isolatedScope;
+      let cb = jasmine.createSpy('callback')
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(inject(function ($compile, $rootScope) {
+        scope = $rootScope.$new();
+
+        scope.pageSize = 2;
+
+        scope.totalElements = 5;
+
+        scope.change = cb;
+
+        element = angular.element('<xos-pagination page-size="pageSize" total-elements="totalElements" change="change"></xos-table>');
+        $compile(element)(scope);
+        scope.$digest();
+        isolatedScope = element.isolateScope().vm;
+      }));
+
+      it('should contain 3 pages', function() {
+        const li = element[0].getElementsByTagName('li');
+        expect(li.length).toEqual(5);
+      });
+
+      it('should call the change function', () => {
+        const li = element[0].getElementsByTagName('li')[3];
+        let link = li.getElementsByTagName('a')[0];
+        link.click();
+        expect(cb).toHaveBeenCalledWith(2);
+      });
+
+      describe('when elements number is less than page size', () => {
+        beforeEach(() => {
+          isolatedScope.pageSize = 10;
+          isolatedScope.totalElements = 9;
+          scope.$digest();
+        });
+
+        it('should not be rendered', () => {
+          const pagination = element[0].getElementsByClassName('pagination');
+          expect(pagination.length).toEqual(0);
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/smart-pie.test.js b/spec/ui/smart-pie.test.js
new file mode 100644
index 0000000..d5a9cfe
--- /dev/null
+++ b/spec/ui/smart-pie.test.js
@@ -0,0 +1,210 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+  'use strict';
+
+  let mockData, compile, rootScope, spy, scope, isolatedScope, element, interval;
+
+  const compileElement = () => {
+
+    if(!scope){
+      scope = rootScope.$new();
+    }
+
+    element = angular.element('<xos-smart-pie config="config"></xos-smart-pie>');
+    compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  }
+
+  describe('The xos.helper module', function(){
+    describe('The xos-smart-pie component', () => {
+      
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(function(){
+        module(function($provide){
+          $provide.service('MockResource', function(){
+            return {
+              query: ''
+            }
+          });
+        });
+      });
+
+      beforeEach(inject(function ($compile, $rootScope) {
+
+        // set mockData
+        mockData = [
+          {
+            id: 1,
+            first_name: 'Jon',
+            last_name: 'Snow',
+            category: 1
+          },
+          {
+            id: 2,
+            first_name: 'Danaerys',
+            last_name: 'Targaryen',
+            category: 2
+          },
+          {
+            id: 3,
+            first_name: 'Aria',
+            last_name: 'Stark',
+            category: 1
+          }
+        ]
+
+        compile = $compile;
+        rootScope = $rootScope;
+      }));
+
+      it('should throw an error if no resource and no data are passed in the config', inject(($compile, $rootScope) => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = $rootScope.$new();
+          scope.config = {};
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosSmartPie] Please provide a resource or an array of data in the configuration'));
+      }));
+
+      describe('when data are passed in the configuration', () => {
+        beforeEach(inject(function ($compile, $rootScope) {
+          scope = $rootScope.$new();
+
+          scope.config = {
+            data: mockData,
+            groupBy: 'category',
+            classes: 'my-test-class'
+          };
+
+          compileElement();
+        }));
+        
+
+        it('should attach provided classes', () => {
+          expect($(element).find('canvas')).toHaveClass('my-test-class');
+        });
+
+        it('should group elements', () => {
+          let groupedData = [2, 1];
+          expect(isolatedScope.data).toEqual(groupedData);
+        });
+
+        describe('when a labelFormatter function is provided', () => {
+          beforeEach(() => {
+            scope.config.labelFormatter = (labels) => {
+              return labels.map(l => l === '1' ? 'First' : 'Second');
+            };
+            compileElement();
+          });
+          it('should format labels', () => {
+            expect(isolatedScope.labels).toEqual(['First', 'Second'])
+          });
+        });
+
+        describe('when provided data changes', () => {
+          beforeEach(() => {
+            scope.config.data.push({
+              id: 2,
+              first_name: 'Danaerys',
+              last_name: 'Targaryen',
+              category: 1
+            });
+            scope.$digest();
+          });
+          it('should calculate again the data', () => {
+            expect(isolatedScope.data).toEqual([3, 1]);
+          });
+        });
+      });
+
+      
+      describe('when a resource is specified in the configuration', () => {
+
+        beforeEach(inject(function ($compile, $rootScope, $q, MockResource) {
+          scope = $rootScope.$new();
+
+          scope.config = {
+            resource: 'MockResource',
+            groupBy: 'category',
+            classes: 'my-test-class'
+          };
+
+          spy = MockResource;
+
+          spyOn(MockResource, 'query').and.callFake(function() {
+            const deferred = $q.defer();
+            deferred.resolve(mockData);
+            return {$promise: deferred.promise};
+          });
+
+          compileElement();
+        }));
+
+
+        it('should call the server and group elements', () => {
+          let groupedData = [2, 1];
+          expect(spy.query).toHaveBeenCalled();
+          expect(isolatedScope.data).toEqual(groupedData);
+        });
+
+        describe('when a labelFormatter function is provided', () => {
+          beforeEach(inject(function ($compile, $rootScope){
+            scope = $rootScope.$new();
+            scope.config = {
+              resource: 'MockResource',
+              groupBy: 'category',
+              classes: 'label-formatter-test',
+              labelFormatter: (labels) => {
+                return labels.map(l => l === '1' ? 'First' : 'Second');
+              }
+            };
+            compileElement();
+          }));
+
+          it('should format labels', () => {
+            expect(isolatedScope.labels).toEqual(['First', 'Second'])
+          });
+        });
+
+        describe('when polling is enabled', () => {
+          beforeEach(inject(function ($compile, $rootScope, $interval){
+
+            //mocked $interval (by ngMock)
+            interval = $interval;
+
+            // cleaning the spy
+            spy.query.calls.reset()
+
+            scope = $rootScope.$new();
+            scope.config = {
+              resource: 'MockResource',
+              groupBy: 'category',
+              classes: 'label-formatter-test',
+              poll: 2
+            };
+            compileElement();
+          }));
+
+          it('should call the backend every 2 second', () => {
+            expect(spy.query).toHaveBeenCalled();
+            expect(spy.query.calls.count()).toEqual(1);
+            interval.flush(2000);
+            expect(spy.query.calls.count()).toEqual(2);
+            interval.flush(2000);
+            expect(spy.query.calls.count()).toEqual(3)
+          });
+        });
+      });
+
+
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/smart-table.test.js b/spec/ui/smart-table.test.js
new file mode 100644
index 0000000..e87e807
--- /dev/null
+++ b/spec/ui/smart-table.test.js
@@ -0,0 +1,198 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+  'use strict';
+
+  let mockData;
+
+  describe('The xos.helper module', function(){
+    describe('The xos-smart-table component', () => {
+
+      let spy, emptySpy, scope, isolatedScope, element;
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(function() {
+
+        // set mockData
+        mockData = [
+          {
+            id: 1,
+            first_name: 'Jon',
+            last_name: 'Snow',
+            hidden_field: 'hidden'
+          }
+        ];
+      });
+
+      // mock the service
+      beforeEach(function(){
+        module(function($provide){
+          $provide.service('MockResource', function(){
+            return {
+              query: '',
+              delete: ''
+            }
+          });
+
+          $provide.service('EmptyResource', function(){
+            return {
+              query: ''
+            }
+          });
+        });
+      })
+
+      beforeEach(inject(function ($compile, $rootScope, $q, MockResource) {
+        scope = $rootScope.$new();
+
+        scope.config = {
+          resource: 'MockResource',
+          hiddenFields: ['hidden_field']
+        };
+
+        spy = MockResource;
+
+        spyOn(MockResource, 'query').and.callFake(function() {
+          const deferred = $q.defer();
+          deferred.resolve(mockData);
+          return {$promise: deferred.promise};
+        });
+
+        spyOn(MockResource, 'delete').and.callFake(function() {
+          const deferred = $q.defer();
+          deferred.resolve();
+          return {$promise: deferred.promise};
+        });
+
+        element = angular.element('<xos-smart-table config="config"></xos-smart-table>');
+        $compile(element)(scope);
+        scope.$digest();
+        isolatedScope = element.isolateScope().vm;
+      }));
+
+      it('should query elements', () => {
+        expect(spy.query).toHaveBeenCalled();
+        expect($(element).find('.alert').parent().parent()).toHaveClass('ng-hide');
+      });
+
+      it('should hide hidden fields', () => {
+        // the 4th field is the mocked save method
+        expect($(element).find('thead th').length).toEqual(3);
+        expect($(element).find('thead th')[0]).toContainText('First name:');
+        expect($(element).find('thead th')[1]).toContainText('Last name:');
+        expect($(element).find('thead th')[2]).toContainText('Actions:');
+      });
+
+      it('should delete a model', () => {
+        // saving mockData (they are going to be deleted)
+        let mock = angular.copy(mockData);
+        $(element).find('a[title="delete"]')[0].click();
+        expect(spy.delete).toHaveBeenCalledWith({id: mock[0].id});
+        expect($(element).find('.alert')).toContainText(`MockResource with id ${mock[0].id} successfully deleted`);
+      });
+
+      it('should show the form', () => {
+        expect($(element).find('.panel')[0]).toHaveClass('ng-hide');
+        $(element).find('a[title="details"]')[0].click();
+        expect($(element).find('.panel')).not.toHaveClass('ng-hide');
+      });
+
+      it('should hide the form', () => {
+        isolatedScope.detailedItem = {
+          some: 'model'
+        };
+        scope.$apply();
+        expect($(element).find('.panel')).not.toHaveClass('ng-hide');
+        $(element).find('.panel .col-xs-1 a')[0].click();
+        expect($(element).find('.panel')[0]).toHaveClass('ng-hide');
+      });
+
+      it('should save an item', inject(($q) => {
+
+        let model = {
+          id: 1,
+          first_name: 'Jon',
+          last_name: 'Snow',
+          hidden_field: 'hidden',
+          $save: '',
+          $update: ''
+        };
+
+        spyOn(model, '$save').and.callFake(function() {
+          const deferred = $q.defer();
+          deferred.resolve();
+          return deferred.promise;
+        });
+
+        spyOn(model, '$update').and.callFake(function() {
+          const deferred = $q.defer();
+          deferred.resolve();
+          return deferred.promise;
+        });
+
+        isolatedScope.detailedItem = model;
+        scope.$apply();
+        $(element).find('xos-form .btn.btn-success').click();
+        expect(model.$update).toHaveBeenCalled();
+      }));
+
+      it('should have an add button', () => {
+        let addBtn = $(element).find('.row .btn.btn-success');
+        expect(addBtn.parent().parent()).not.toHaveClass('ng-hide');
+      });
+
+      describe('when the add button is clicked', () => {
+        beforeEach(() => {
+          let btn = $(element).find('.row .btn.btn-success')
+          btn[0].click();
+        });
+
+        xit('should create a new model', () => {
+          expect(isolatedScope.detailedItem).toBeDefined();
+          expect(isolatedScope.detailedItem).toBeInstanceOf('Resource');
+        });
+      });
+
+      describe('when fetching an empty collection', () => {
+        beforeEach(inject(function ($compile, $rootScope, $q, EmptyResource) {
+          scope = $rootScope.$new();
+
+          scope.config = {
+            resource: 'EmptyResource'
+          };
+
+          emptySpy = EmptyResource;
+
+          spyOn(EmptyResource, 'query').and.callFake(function() {
+            const deferred = $q.defer();
+            deferred.resolve([]);
+            return {$promise: deferred.promise};
+          });
+
+          element = angular.element('<xos-smart-table config="config"></xos-smart-table>');
+          $compile(element)(scope);
+          scope.$digest();
+          isolatedScope = element.isolateScope().vm;
+        }));
+
+        it('should display an alert', () => {
+          expect(emptySpy.query).toHaveBeenCalled();
+          expect($(element).find('.alert').parent().parent()).not.toHaveClass('ng-hide');
+          expect($(element).find('.alert')).toContainText('No data to show');
+        });
+
+        it('should not have an add button', () => {
+          let addBtn = $(element).find('.row .btn.btn-success');
+          expect(addBtn.parent().parent()).toHaveClass('ng-hide');
+        });
+      });
+
+
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/table.test.js b/spec/ui/table.test.js
new file mode 100644
index 0000000..0b6ea87
--- /dev/null
+++ b/spec/ui/table.test.js
@@ -0,0 +1,576 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+  'use strict';
+
+  let scope, element, isolatedScope, rootScope, compile, filter;
+  const compileElement = () => {
+
+    if(!scope){
+      scope = rootScope.$new();
+    }
+
+    element = angular.element('<xos-table config="config" data="data"></xos-table>');
+    compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  };
+
+
+  describe('The xos.helper module', function(){
+    describe('The xos-table component', () => {
+
+      beforeEach(module('xos.helpers'));
+
+      beforeEach(inject(function ($compile, $rootScope, $filter) {
+        compile = $compile;
+        rootScope = $rootScope;
+        filter = $filter;
+      }));
+
+      it('should throw an error if no config is specified', () => {
+        function errorFunctionWrapper(){
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a configuration via the "config" attribute'));
+      });
+
+      it('should throw an error if no config columns are specified', () => {
+        function errorFunctionWrapper(){
+          // setup the parent scope
+          scope = rootScope.$new();
+          scope.config = 'green';
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a columns list in the configuration'));
+      });
+
+      describe('when basically configured', function() {
+
+        beforeEach(inject(function ($compile, $rootScope) {
+
+          scope = $rootScope.$new();
+
+          scope.config = {
+            columns: [
+              {
+                label: 'Label 1',
+                prop: 'label-1'
+              },
+              {
+                label: 'Label 2',
+                prop: 'label-2'
+              }
+            ]
+          };
+
+          scope.data = [
+            {
+              'label-1': 'Sample 1.1',
+              'label-2': 'Sample 1.2'
+            },
+            {
+              'label-1': 'Sample 2.1',
+              'label-2': 'Sample 2.2'
+            }
+          ]
+
+          element = angular.element('<xos-table config="config" data="data"></xos-table>');
+          $compile(element)(scope);
+          scope.$digest();
+          isolatedScope = element.isolateScope().vm;
+        }));
+
+        it('should contain 2 columns', function() {
+          const th = element[0].getElementsByTagName('th');
+          expect(th.length).toEqual(2);
+          expect(isolatedScope.columns.length).toEqual(2);
+        });
+
+        it('should contain 3 rows', function() {
+          const tr = element[0].getElementsByTagName('tr');
+          expect(tr.length).toEqual(3);
+        });
+
+        it('should render labels', () => {
+          let label1 = $(element).find('thead tr th')[0]
+          let label2 = $(element).find('thead tr th')[1]
+          expect($(label1).text().trim()).toEqual('Label 1');
+          expect($(label2).text().trim()).toEqual('Label 2');
+        });
+
+        describe('when no data are provided', () => {
+          beforeEach(() => {
+            isolatedScope.data = [];
+            scope.$digest();
+          });
+          it('should render an alert', () => {
+            let alert = element[0].getElementsByClassName('alert');
+            let table = element[0].getElementsByTagName('table');
+            expect(alert.length).toEqual(1);
+            expect(table.length).toEqual(1);
+          });
+        });
+
+        describe('when a field type is provided', () => {
+          describe('and is boolean', () => {
+            beforeEach(() => {
+              scope.config = {
+                columns: [
+                  {
+                    label: 'Label 1',
+                    prop: 'label-1',
+                    type: 'boolean'
+                  },
+                  {
+                    label: 'Label 2',
+                    prop: 'label-2',
+                    type: 'boolean'
+                  }
+                ]
+              };
+              scope.data = [
+                {
+                  'label-1': true,
+                  'label-2': 1
+                },
+                {
+                  'label-1': false,
+                  'label-2': 0
+                }
+              ];
+              compileElement();
+            });
+
+            it('should render an incon', () => {
+              let td1 = $(element).find('tbody tr:first-child td')[0];
+              let td2 = $(element).find('tbody tr:last-child td')[0];
+              expect($(td1).find('i')).toHaveClass('glyphicon-ok');
+              expect($(td2).find('i')).toHaveClass('glyphicon-remove');
+            });
+
+            describe('with field filters', () => {
+              beforeEach(() => {
+                scope.config.filter = 'field';
+                compileElement();
+              });
+
+              it('should render a dropdown for filtering', () => {
+                let td1 = $(element).find('table tbody tr td')[0];
+                expect(td1).toContainElement('select');
+                expect(td1).not.toContainElement('input');
+              });
+
+              it('should correctly filter results', () => {
+                isolatedScope.query = {
+                  'label-1': false
+                };
+                scope.$digest();
+                expect(isolatedScope.query['label-1']).toBeFalsy();
+                const tr = $(element).find('tbody:last-child > tr');
+                const icon = $(tr[0]).find('td i');
+                expect(tr.length).toEqual(1);
+                expect(icon).toHaveClass('glyphicon-remove');
+              });
+
+              it('should correctly filter results if the field is in the form of 0|1', () => {
+                isolatedScope.query = {
+                  'label-2': false
+                };
+                scope.$digest();
+                expect(isolatedScope.query['label-1']).toBeFalsy();
+                const tr = $(element).find('tbody:last-child > tr');
+                const icon = $(tr[0]).find('td i');
+                expect(tr.length).toEqual(1);
+                expect(icon).toHaveClass('glyphicon-remove');
+              });
+            });
+          });
+
+          describe('and is date', () => {
+            beforeEach(() => {
+              scope.config = {
+                columns: [
+                  {
+                    label: 'Label 1',
+                    prop: 'label-1',
+                    type: 'date'
+                  }
+                ]
+              };
+              scope.data = [
+                {
+                  'label-1': '2015-02-17T22:06:38.059000Z'
+                }
+              ];
+              compileElement();
+            });
+
+            it('should render an formatted date', () => {
+              let td1 = $(element).find('tbody tr:first-child td')[0];
+              const expectedDate = filter('date')(scope.data[0]['label-1'], 'H:mm MMM d, yyyy');
+              expect($(td1).text().trim()).toEqual(expectedDate);
+            });
+          });
+
+          describe('and is array', () => {
+            beforeEach(() => {
+              scope.data = [
+                {categories: ['Film', 'Music']}
+              ];
+              scope.config = {
+                filter: 'field',
+                columns: [
+                  {
+                    label: 'Categories',
+                    prop: 'categories',
+                    type: 'array'
+                  }
+                ]
+              }
+              compileElement();
+            });
+            it('should render a comma separated list', () => {
+              let td1 = $(element).find('tbody:last-child tr:first-child')[0];
+              expect($(td1).text().trim()).toEqual('Film, Music');
+            });
+
+            it('should not render the filter field', () => {
+              let filter = $(element).find('tbody tr td')[0];
+              expect($(filter)).not.toContainElement('input');
+            });
+          });
+
+          describe('and is object', () => {
+            beforeEach(() => {
+              scope.data = [
+                {
+                  attributes: {
+                    age: 20,
+                    height: 50
+                  }
+                }
+              ];
+              scope.config = {
+                filter: 'field',
+                columns: [
+                  {
+                    label: 'Categories',
+                    prop: 'attributes',
+                    type: 'object'
+                  }
+                ]
+              }
+              compileElement();
+            });
+            it('should render a list of key-values', () => {
+              let td = $(element).find('tbody:last-child tr:first-child')[0];
+              let ageLabel = $(td).find('dl dt')[0];
+              let ageValue = $(td).find('dl dd')[0];
+              let heightLabel = $(td).find('dl dt')[1];
+              let heightValue = $(td).find('dl dd')[1];
+              expect($(ageLabel).text().trim()).toEqual('age');
+              expect($(ageValue).text().trim()).toEqual('20');
+              expect($(heightLabel).text().trim()).toEqual('height');
+              expect($(heightValue).text().trim()).toEqual('50');
+            });
+
+            it('should not render the filter field', () => {
+              let filter = $(element).find('tbody tr td')[0];
+              expect($(filter)).not.toContainElement('input');
+            });
+          });
+
+          describe('and is custom', () => {
+
+            let formatterFn = jasmine.createSpy('formatter').and.returnValue('Formatted Content');
+
+            beforeEach(() => {
+              scope.data = [
+                {categories: ['Film', 'Music']}
+              ];
+              scope.config = {
+                filter: 'field',
+                columns: [
+                  {
+                    label: 'Categories',
+                    prop: 'categories',
+                    type: 'custom',
+                    formatter: formatterFn
+                  }
+                ]
+              }
+              compileElement();
+            });
+
+            it('should check for a formatter property', () => {
+              function errorFunctionWrapper(){
+                // setup the parent scope
+                scope = rootScope.$new();
+                scope.config = {
+                  columns: [
+                    {
+                      label: 'Categories',
+                      prop: 'categories',
+                      type: 'custom'
+                    }
+                  ]
+                };
+                compileElement();
+              }
+              expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
+            });
+
+            it('should check that the formatter property is a function', () => {
+              function errorFunctionWrapper(){
+                // setup the parent scope
+                scope = rootScope.$new();
+                scope.config = {
+                  columns: [
+                    {
+                      label: 'Categories',
+                      prop: 'categories',
+                      type: 'custom',
+                      formatter: 'formatter'
+                    }
+                  ]
+                };
+                compileElement();
+              }
+              expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
+            });
+
+            it('should format data using the formatter property', () => {
+              let td1 = $(element).find('tbody:last-child tr:first-child')[0];
+              expect($(td1).text().trim()).toEqual('Formatted Content');
+              // the custom formatted should receive the entire object, otherwise is not so custom
+              expect(formatterFn).toHaveBeenCalledWith({categories: ['Film', 'Music']});
+            });
+
+            it('should not render the filter field', () => {
+              // displayed value is different from model val, filter would not work
+              let filter = $(element).find('tbody tr td')[0];
+              expect($(filter)).not.toContainElement('input');
+            });
+          });
+
+          describe('and is icon', () => {
+
+            beforeEach(() => {
+              scope.config = {
+                columns: [
+                  {
+                    label: 'Label 1',
+                    prop: 'label-1',
+                    type: 'icon',
+                    formatter: item => {
+                      switch (item['label-1']){
+                      case 1:
+                        return 'ok';
+                      case 2:
+                        return 'remove';
+                      case 3:
+                        return 'plus'
+                      }
+                    }
+                  }
+                ]
+              };
+              scope.data = [
+                {
+                  'label-1': 1
+                },
+                {
+                  'label-1': 2
+                },
+                {
+                  'label-1': 3
+                }
+              ];
+              compileElement();
+            });
+
+            it('should render a custom icon', () => {
+              let td1 = $(element).find('tbody tr:first-child td')[0];
+              let td2 = $(element).find('tbody tr:nth-child(2) td')[0];
+              let td3 = $(element).find('tbody tr:last-child td')[0];
+              expect($(td1).find('i')).toHaveClass('glyphicon-ok');
+              expect($(td2).find('i')).toHaveClass('glyphicon-remove');
+              expect($(td3).find('i')).toHaveClass('glyphicon-plus');
+            });
+          });
+        });
+
+        describe('when a link property is provided', () => {
+          beforeEach(() => {
+            scope.data = [
+              {
+                id: 1}
+            ];
+            scope.config = {
+              columns: [
+                {
+                  label: 'Id',
+                  prop: 'id',
+                  link: (item) => {
+                    return `/link/${item.id}`;
+                  }
+                }
+              ]
+            }
+            compileElement();
+          });
+
+          it('should check that the link property is a function', () => {
+            function errorFunctionWrapper(){
+              // setup the parent scope
+              scope = rootScope.$new();
+              scope.config = {
+                columns: [
+                  {
+                    label: 'Categories',
+                    prop: 'categories',
+                    link: 'custom'
+                  }
+                ]
+              };
+              compileElement();
+            }
+            expect(errorFunctionWrapper).toThrow(new Error('[xosTable] The link property should be a function.'));
+          });
+
+          it('should render a link with the correct url', () => {
+            let link = $(element).find('tbody tr:first-child td a')[0];
+            expect($(link).attr('href')).toEqual('/link/1');
+          });
+        });
+
+        describe('when actions are passed', () => {
+
+          let cb = jasmine.createSpy('callback')
+
+          beforeEach(() => {
+            isolatedScope.config.actions = [
+              {
+                label: 'delete',
+                icon: 'remove',
+                cb: cb,
+                color: 'red'
+              }
+            ];
+            scope.$digest();
+          });
+
+          it('should have 3 columns', () => {
+            const th = element[0].getElementsByTagName('th');
+            expect(th.length).toEqual(3);
+            expect(isolatedScope.columns.length).toEqual(2);
+          });
+
+          it('when clicking on action should invoke callback', () => {
+            const link = element[0].getElementsByTagName('a')[0];
+            link.click();
+            expect(cb).toHaveBeenCalledWith(scope.data[0]);
+          });
+        });
+
+        describe('when filter is fulltext', () => {
+          beforeEach(() => {
+            isolatedScope.config.filter = 'fulltext';
+            scope.$digest();
+          });
+
+          it('should render a text field', () => {
+            const textField = element[0].getElementsByTagName('input');
+            expect(textField.length).toEqual(1);
+          });
+
+          describe('and a value is enterd', () => {
+            beforeEach(() => {
+              isolatedScope.query = '2.2';
+              scope.$digest();
+            });
+
+            it('should contain 2 rows', function() {
+              const tr = element[0].getElementsByTagName('tr');
+              expect(tr.length).toEqual(2);
+            });
+          });
+        });
+
+        describe('when filter is field', () => {
+          beforeEach(() => {
+            isolatedScope.config.filter = 'field';
+            scope.$digest();
+          });
+
+          it('should render a text field for each column', () => {
+            const textField = element[0].getElementsByTagName('input');
+            expect(textField.length).toEqual(2);
+          });
+
+          describe('and a value is enterd', () => {
+            beforeEach(() => {
+              isolatedScope.query = {'label-1': '2.1'};
+              scope.$digest();
+            });
+
+            it('should contain 3 rows', function() {
+              const tr = element[0].getElementsByTagName('tr');
+              expect(tr.length).toEqual(3);
+            });
+          });
+        });
+
+        describe('when order is true', () => {
+          beforeEach(() => {
+            isolatedScope.config.order = true;
+            scope.$digest();
+          });
+
+          it('should render a arrows beside', () => {
+            const arrows = element[0].getElementsByTagName('i');
+            expect(arrows.length).toEqual(4);
+          });
+
+          describe('and a default ordering is passed', () => {
+
+            beforeEach(() => {
+              scope.config.order = {
+                field: 'label-1',
+                reverse: true
+              };
+              compileElement();
+            });
+
+            it('should orderBy the default order', () => {
+              const tr = $(element).find('tr');
+              expect($(tr[1]).text()).toContain('Sample 2.2');
+              expect($(tr[2]).text()).toContain('Sample 1.1');
+            });
+          });
+
+          describe('and an order is set', () => {
+            beforeEach(() => {
+              isolatedScope.orderBy = 'label-1';
+              isolatedScope.reverse = true;
+              scope.$digest();
+            });
+
+            it('should orderBy', function() {
+              // console.log($(element).find('table'));
+              const tr = $(element).find('tr');
+              expect($(tr[1]).text()).toContain('Sample 2.2');
+              expect($(tr[2]).text()).toContain('Sample 1.1');
+            });
+          });
+        });
+      });
+    });
+  });
+})();
+
diff --git a/spec/ui/validation.test.js b/spec/ui/validation.test.js
new file mode 100644
index 0000000..44d9f96
--- /dev/null
+++ b/spec/ui/validation.test.js
@@ -0,0 +1,86 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+  'use strict';
+
+  let compile, element, scope, rootScope;
+
+  const compileElement = (el) => {
+    element = el;
+
+    if(!scope){
+      scope = rootScope.$new();
+    }
+    if(angular.isUndefined(element)){
+      element = angular.element('<xos-validation field="field" form="form"></xos-validation>');
+    }
+    compile(element)(scope);
+    scope.$digest();
+  }
+
+  describe('The xos.helper module', function(){
+    describe('The xos-validation component', () => {
+
+      beforeEach(module('xos.helpers'));
+
+      describe('when the form has no errors', () => {
+        beforeEach(inject(($compile, $rootScope) => {
+          compile = $compile;
+          scope = $rootScope.$new();
+
+          scope.field = {
+            $error: {}
+          };
+
+          scope.form = {
+            $submitted: true
+          }
+
+          compileElement();
+        }));
+
+        it('should not show an alert by default', () => {
+          expect($(element).find('xos-alert > .alert')[0]).toHaveClass('ng-hide');
+        });
+      });
+
+      let availableErrors = [
+        {
+          type: 'required',
+          message: 'Field required'
+        },
+        {
+          type: 'email',
+          message: 'This is not a valid email'
+        },
+        {
+          type: 'minlength',
+          message: 'Too short'
+        },
+        {
+          type: 'maxlength',
+          message: 'Too long'
+        },
+        {
+          type: 'custom',
+          message: 'Field invalid'
+        },
+      ];
+
+      // use a loop to generate similar test
+      availableErrors.forEach((e, i) => {
+        it(`should show an alert for ${e.type} errors`, () => {
+          scope.field.$error[e.type] = true;
+          compileElement();
+          let alert = $(element).find('xos-alert > .alert')[i];
+          expect(alert).not.toHaveClass('ng-hide');
+          expect(alert).toHaveText(e.message);
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/spec/ui/xos.helpers.mock.js b/spec/ui/xos.helpers.mock.js
new file mode 100644
index 0000000..bf0f0f5
--- /dev/null
+++ b/spec/ui/xos.helpers.mock.js
@@ -0,0 +1,5 @@
+(function () {
+  'use strict';
+  angular.module('xos.helpers', ['xos.uiComponents'])
+  .factory('_', $window => $window._ );
+})();
\ No newline at end of file