You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2016/03/28 11:46:39 UTC

[3/3] ambari git commit: AMBARI-15596. Add missing unit tests files for ambari-web utils (alexantonenko)

AMBARI-15596. Add missing unit tests files for ambari-web utils (alexantonenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/d5d1afea
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/d5d1afea
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/d5d1afea

Branch: refs/heads/trunk
Commit: d5d1afea8be684fed1e712f7a80b5e06f4d79ba5
Parents: aa033d0
Author: Alex Antonenko <hi...@gmail.com>
Authored: Mon Mar 28 11:53:29 2016 +0300
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Mon Mar 28 11:53:29 2016 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |    9 +
 ambari-web/app/utils/array_utils.js             |    2 +-
 ambari-web/app/utils/configs_collection.js      |    2 +-
 ambari-web/app/utils/heatmap.js                 |    1 -
 ambari-web/app/utils/hosts.js                   |   10 +-
 ambari-web/app/utils/polling.js                 |   20 +-
 ambari-web/test/utils/action_sequence_test.js   |  323 ++
 ambari-web/test/utils/array_utils_test.js       |  125 +
 .../test/utils/configs_collection_test.js       |  334 ++
 ambari-web/test/utils/credentials_test.js       |  717 ++++
 ambari-web/test/utils/file_utils_test.js        |  126 +
 .../test/utils/handlebars_helpers_test.js       |   57 +
 ambari-web/test/utils/heatmap_test.js           |  141 +
 ambari-web/test/utils/hosts_test.js             | 3467 ++++++++++++++++++
 ambari-web/test/utils/polling_test.js           | 1333 +++++++
 15 files changed, 6646 insertions(+), 21 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index d114379..db504f2 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -167,9 +167,13 @@ var files = [
   'test/mixins/unit_convert/base_unit_convert_mixin_test',
   'test/utils/ajax/ajax_test',
   'test/utils/ajax/ajax_queue_test',
+  'test/utils/action_sequence_test',
+  'test/utils/array_utils_test',
   'test/utils/batch_scheduled_requests_test',
   'test/utils/blueprint_test',
   'test/utils/config_test',
+  'test/utils/configs_collection_test',
+  'test/utils/credentials_test',
   'test/utils/date/date_test',
   'test/utils/date/timezone_test',
   'test/utils/data_manipulation_test',
@@ -178,9 +182,14 @@ var files = [
   'test/utils/ember_computed_test',
   'test/utils/ember_reopen_test',
   'test/utils/form_field_test',
+  'test/utils/file_utils_test',
+  'test/utils/handlebars_helpers_test',
+  'test/utils/heatmap_test',
   'test/utils/host_progress_popup_test',
+  'test/utils/hosts_test',
   'test/utils/misc_test',
   'test/utils/number_utils_test',
+  'test/utils/polling_test',
   'test/utils/validator_test',
   'test/utils/config_test',
   'test/utils/string_utils_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/utils/array_utils.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/array_utils.js b/ambari-web/app/utils/array_utils.js
index bee12ee..643ed67 100644
--- a/ambari-web/app/utils/array_utils.js
+++ b/ambari-web/app/utils/array_utils.js
@@ -26,7 +26,7 @@ module.exports = {
   uniqObjectsbyId: function (arr, id) {
     var result = [];
     arr.forEach(function (item) {
-      var isIdPresent = result.someProperty('id', item.id);
+      var isIdPresent = result.someProperty(id, item[id]);
       if (!isIdPresent) {
         result.pushObject(item);
       }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/utils/configs_collection.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/configs_collection.js b/ambari-web/app/utils/configs_collection.js
index fde7a17..a9a570a 100644
--- a/ambari-web/app/utils/configs_collection.js
+++ b/ambari-web/app/utils/configs_collection.js
@@ -40,7 +40,7 @@ var configsCollection = [],
 App.configsCollection = Em.Object.create({
 
   /**
-   * adds config property to configs array anf map
+   * adds config property to configs array and map
    * should assert error if config has no id
    * @param c
    * @method add

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/utils/heatmap.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/heatmap.js b/ambari-web/app/utils/heatmap.js
index 9d8c178..e033005 100644
--- a/ambari-web/app/utils/heatmap.js
+++ b/ambari-web/app/utils/heatmap.js
@@ -16,7 +16,6 @@
  * limitations under the License.
  */
 
-var App = require('app');
 module.exports = {
   mappers: Em.Mixin.create({
     metricMapperWithTransform: function (json, metricName, transformValueFunction) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/utils/hosts.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/hosts.js b/ambari-web/app/utils/hosts.js
index 2467e1a..e4a437c 100644
--- a/ambari-web/app/utils/hosts.js
+++ b/ambari-web/app/utils/hosts.js
@@ -151,15 +151,11 @@ module.exports = {
 
             host.set('filterColumnValue', value);
 
-            if (!skip && filterText) {
-              if ((value == null || !value.toString().match(filterText)) && !host.get('host.publicHostName').match(filterText)) {
-                skip = true;
-              }
+            if (!skip && filterText && (value == null || !value.toString().match(filterText)) && !host.get('host.publicHostName').match(filterText)) {
+              skip = true;
             }
-            if (!skip && filterComponent) {
-              if (hostComponentNames.length > 0) {
+            if (!skip && filterComponent && hostComponentNames.length > 0) {
                 skip = !hostComponentNames.contains(filterComponent.get('componentName'));
-              }
             }
             host.set('filtered', !skip);
           }, this);

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/app/utils/polling.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/polling.js b/ambari-web/app/utils/polling.js
index b83ca23..7337fbb 100644
--- a/ambari-web/app/utils/polling.js
+++ b/ambari-web/app/utils/polling.js
@@ -76,7 +76,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
           self.set('isSuccess', true);
           self.set('isError', false);
         } else {
-          var requestId = jsonData.Requests.id;
+          var requestId = Em.get(jsonData, 'Requests.id');
           self.set('requestId', requestId);
           self.doPolling();
         }
@@ -101,7 +101,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
   },
 
   doPolling: function () {
-    if (this.get('requestId')) {
+    if (!Em.isNone(this.get('requestId'))) {
       this.startPolling();
     }
   },
@@ -110,7 +110,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
    * server call to obtain task logs
    */
   pollTaskLog: function () {
-    if (this.get('currentTaskId')) {
+    if (!Em.isNone(this.get('currentTaskId'))) {
       App.ajax.send({
         name: 'background_operations.get_by_task',
         sender: this,
@@ -119,7 +119,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
           taskId: this.get('currentTaskId')
         },
         success: 'pollTaskLogSuccessCallback'
-      })
+      });
     }
   },
 
@@ -139,7 +139,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
    * @return {Boolean}
    */
   startPolling: function () {
-    if (!this.get('requestId')) return false;
+    if (Em.isNone(this.get('requestId'))) return false;
 
     this.pollTaskLog();
     App.ajax.send({
@@ -169,10 +169,8 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
 
   reloadErrorCallback: function (request, ajaxOptions, error, opt, params) {
     this._super(request, ajaxOptions, error, opt, params);
-    if (request.status) {
-      if (!this.get('isSuccess')) {
-        this.set('isError', true);
-      }
+    if (request.status && !this.get('isSuccess')) {
+      this.set('isError', true);
     }
   },
 
@@ -182,7 +180,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
 
   replacePolledData: function (polledData) {
     var currentTaskId = this.get('currentTaskId');
-    if (currentTaskId) {
+    if (!Em.isNone(currentTaskId)) {
       var task = this.get('polledData').findProperty('Tasks.id', currentTaskId);
       var currentTask = polledData.findProperty('Tasks.id', currentTaskId);
       if (task && currentTask) {
@@ -226,7 +224,7 @@ App.Poll = Em.Object.extend(App.ReloadPopupMixin, {
   parseInfo: function (polledData) {
     var tasksData = polledData.tasks;
     var requestId = this.get('requestId');
-    if (polledData.Requests && polledData.Requests.id && polledData.Requests.id != requestId) {
+    if (polledData.Requests && !Em.isNone(polledData.Requests.id) && polledData.Requests.id != requestId) {
       // We don't want to use non-current requestId's tasks data to
       // determine the current install status.
       // Also, we don't want to keep polling if it is not the

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/action_sequence_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/action_sequence_test.js b/ambari-web/test/utils/action_sequence_test.js
new file mode 100644
index 0000000..4ae9dc8
--- /dev/null
+++ b/ambari-web/test/utils/action_sequence_test.js
@@ -0,0 +1,323 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+require('utils/action_sequence');
+
+describe('App.actionSequence', function () {
+
+  var actionSequence;
+
+  beforeEach(function () {
+    actionSequence = App.actionSequence.create();
+  });
+
+  describe('#setSequence', function () {
+
+    var cases = [
+        {
+          sequenceIn: [{}, {}],
+          sequenceOut: [{}, {}],
+          title: 'array passed'
+        },
+        {
+          sequenceIn: {
+            '0': {},
+            '1': {},
+            'length': 2
+          },
+          sequenceOut: [{}],
+          title: 'array-like object passed'
+        },
+        {
+          sequenceIn: 0,
+          sequenceOut: [{}],
+          title: 'primitive passed'
+        }
+      ],
+      result;
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          actionSequence.set('sequence', [{}]);
+          result = actionSequence.setSequence(item.sequenceIn);
+        });
+
+        it('should return context', function () {
+          expect(result).to.eql(actionSequence);
+        });
+
+        it('sequence property', function () {
+          expect(actionSequence.get('sequence')).to.eql(item.sequenceOut);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#start', function () {
+
+    beforeEach(function () {
+      actionSequence.setProperties({
+        actionCounter: 0,
+        sequence: [{}]
+      });
+      sinon.stub(actionSequence, 'runNextAction', Em.K);
+      actionSequence.start();
+    });
+
+    afterEach(function () {
+      actionSequence.runNextAction.restore();
+    });
+
+    it('should set the counter', function () {
+      expect(actionSequence.get('actionCounter')).to.equal(1);
+    });
+
+    it('should start the sequence', function () {
+      expect(actionSequence.runNextAction.calledOnce).to.be.true;
+    });
+
+    it('should call runNextAction with correct arguments', function () {
+      expect(actionSequence.runNextAction.calledWith(0, null)).to.be.true;
+    });
+
+  });
+
+  describe('#onFinish', function () {
+
+    var cases = [
+        {
+          callbackIn: Em.isNone,
+          callbackOut: Em.isNone,
+          title: 'function passed'
+        },
+        {
+          callbackIn: 'function () {}',
+          callbackOut: Em.clb,
+          title: 'array-like object passed'
+        },
+        {
+          callbackIn: 'function () {}',
+          callbackOut: Em.clb,
+          title: 'primitive passed'
+        }
+      ],
+      result;
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          actionSequence.set('finishedCallback', Em.clb);
+          result = actionSequence.onFinish(item.callbackIn);
+        });
+
+        it('should return context', function () {
+          expect(result).to.eql(actionSequence);
+        });
+
+        it('finishedCallback property', function () {
+          expect(actionSequence.get('finishedCallback')).to.eql(item.callbackOut);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#runNextAction', function () {
+
+    var actions = {
+        callback: Em.K,
+        sync: function (prevResponse) {
+          actions.callback(prevResponse);
+          return prevResponse;
+        },
+        async: function (prevResponse) {
+          actions.callback(prevResponse);
+          return {
+            done: function (callback) {
+              return callback.call(this, prevResponse);
+            }
+          };
+        }
+      },
+      prevResponse = {},
+      cases = [
+        {
+          index: 0,
+          actionCounter: 0,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 0,
+          title: 'no iterations left (case 1)'
+        },
+        {
+          index: 3,
+          actionCounter: 3,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 0,
+          title: 'no iterations left (case 2)'
+        },
+        {
+          index: 1,
+          actionCounter: 3,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 2,
+          title: 'starting from the middle'
+        },
+        {
+          index: 0,
+          actionCounter: 2,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 2,
+          title: 'ending at the middle'
+        },
+        {
+          index: 0,
+          actionCounter: 3,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 3,
+          title: 'all iterations'
+        },
+        {
+          index: 0,
+          actionCounter: 3,
+          sequence: [
+            {
+              callback: actions.sync,
+              type: 'sync'
+            },
+            {
+              callback: actions.async,
+              type: 'async'
+            },
+            {
+              callback: actions.sync,
+              type: 'sync'
+            }
+          ],
+          actionCallCount: 3,
+          title: 'asynchronous action'
+        }
+      ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          sinon.spy(actions, 'callback');
+          sinon.stub(actionSequence, 'finishedCallback', Em.K);
+          actionSequence.setProperties({
+            context: actionSequence,
+            actionCounter: item.actionCounter,
+            sequence: item.sequence
+          });
+          actionSequence.runNextAction(item.index, prevResponse);
+        });
+
+        afterEach(function () {
+          actions.callback.restore();
+          actionSequence.finishedCallback.restore();
+        });
+
+        it('number of calls', function () {
+          expect(actions.callback.callCount).to.equal(item.actionCallCount);
+        });
+
+        if (item.actionCallCount) {
+          it('argument passed to callback', function () {
+            expect(actions.callback.alwaysCalledWith(prevResponse)).to.be.true;
+          });
+        }
+
+        it('finish callback', function () {
+          expect(actionSequence.finishedCallback.calledOnce).to.be.true;
+        });
+
+      });
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/array_utils_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/array_utils_test.js b/ambari-web/test/utils/array_utils_test.js
new file mode 100644
index 0000000..ec7c5d8
--- /dev/null
+++ b/ambari-web/test/utils/array_utils_test.js
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var arrayUtils = require('utils/array_utils');
+
+describe('array_utils', function () {
+
+  describe('#uniqObjectsbyId', function () {
+
+    var arr = [
+        {
+          n: 0,
+          v: 'v0'
+        },
+        {
+          n: 0,
+          v: 'v01'
+        },
+        {
+          n: 1,
+          v: 'v1'
+        },
+        {
+          n: '1',
+          v: 'v11'
+        },
+        {
+          n: 2,
+          v: 'v2'
+        }
+      ],
+      result = [
+        {
+          n: 0,
+          v: 'v0'
+        },
+        {
+          n: 1,
+          v: 'v1'
+        },
+        {
+          n: '1',
+          v: 'v11'
+        },
+        {
+          n: 2,
+          v: 'v2'
+        }
+      ];
+
+    it('should return one element for one id', function () {
+      expect(arrayUtils.uniqObjectsbyId(arr, 'n').toArray()).to.eql(result);
+    });
+
+  });
+
+  describe('#intersect', function () {
+
+    var cases = [
+      {
+        arr1: [Infinity, 0, 1, 2, {a: 1}, {b: 2}, null, undefined],
+        arr2: ['undefined', null, {b: '2'}, {a: 1}, 2.0, '1', 0, Infinity],
+        result: [null, 2, 0, Infinity],
+        title: 'arrays of the same length have common items'
+      },
+      {
+        arr1: [true, false, [0, 1], [2, 3], [4, 5], [6], null, undefined],
+        arr2: [undefined, 'null', 6, [4, 5], ['2', '3'], [String(0), String(1)], '0,1', false, 'true'],
+        result: [false, undefined],
+        title: 'arrays of different length have common items'
+      },
+      {
+        arr1: ['1', function () {}, NaN],
+        arr2: ['function () {}', Number('1'), NaN],
+        result: [],
+        title: 'arrays have no common items'
+      },
+      {
+        arr1: [[0], undefined, null],
+        arr2: [],
+        result: [],
+        title: 'one of arrays is empty'
+      },
+      {
+        arr1: [],
+        arr2: [],
+        result: [],
+        title: 'both arrays are empty'
+      }
+    ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        it('arrays intersection', function () {
+          expect(arrayUtils.intersect(item.arr1, item.arr2)).to.eql(item.result);
+        });
+
+        it('commutativity', function () {
+          expect(arrayUtils.intersect(item.arr1, item.arr2).sort()).to.eql(arrayUtils.intersect(item.arr2, item.arr1).sort());
+        });
+
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/configs_collection_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/configs_collection_test.js b/ambari-web/test/utils/configs_collection_test.js
new file mode 100644
index 0000000..bd97950
--- /dev/null
+++ b/ambari-web/test/utils/configs_collection_test.js
@@ -0,0 +1,334 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+require('utils/configs_collection');
+
+describe('App.configsCollection', function () {
+
+  var configsCollection;
+
+  beforeEach(function () {
+    configsCollection = Em.Object.create(App.configsCollection);
+    sinon.spy(Em, 'assert');
+  });
+
+  afterEach(function () {
+    Em.assert.restore();
+  });
+
+  describe('#add', function () {
+
+    var cases = [
+      {
+        collection: [],
+        isError: false,
+        title: 'initial state'
+      },
+      {
+        obj: undefined,
+        collection: [],
+        isError: true,
+        title: 'no item passed'
+      },
+      {
+        obj: undefined,
+        collection: [],
+        isError: true,
+        title: 'null passed'
+      },
+      {
+        obj: {},
+        collection: [],
+        isError: true,
+        title: 'no id passed'
+      },
+      {
+        obj: {
+          id: 1,
+          name: 'n10'
+        },
+        collection: [
+          {
+            id: 1,
+            name: 'n10'
+          }
+        ],
+        mapItem: {
+          id: 1,
+          name: 'n10'
+        },
+        isError: false,
+        title: 'new item'
+      },
+      {
+        obj: {
+          id: 1,
+          name: 'n11'
+        },
+        collection: [
+          {
+            id: 1,
+            name: 'n10'
+          }
+        ],
+        mapItem: {
+          id: 1,
+          name: 'n11'
+        },
+        isError: false,
+        title: 'duplicate id'
+      },
+      {
+        obj: {
+          id: '1',
+          name: 'n12'
+        },
+        collection: [
+          {
+            id: 1,
+            name: 'n10'
+          }
+        ],
+        mapItem: {
+          id: '1',
+          name: 'n12'
+        },
+        isError: false,
+        title: 'duplicate id, key name conversion'
+      }
+    ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          try {
+            if (item.hasOwnProperty('obj')) {
+              configsCollection.add(item.obj);
+            }
+          } catch (e) {}
+        });
+
+        it('thrown error', function () {
+          expect(Em.assert.threw()).to.equal(item.isError);
+        });
+
+        it('configs array', function () {
+          expect(configsCollection.getAll()).to.eql(item.collection);
+        });
+
+        if (item.obj && item.obj.id) {
+          it('configs map', function () {
+            expect(configsCollection.getConfig(item.obj.id)).to.eql(item.mapItem);
+          });
+        }
+
+      });
+
+    });
+
+  });
+
+  describe('#getConfig', function () {
+
+    var cases = [
+      {
+        result: undefined,
+        isError: true,
+        title: 'no id passed'
+      },
+      {
+        id: null,
+        result: undefined,
+        isError: true,
+        title: 'invalid id passed'
+      },
+      {
+        id: 1,
+        result: {
+          id: 1
+        },
+        isError: false,
+        title: 'existing item'
+      },
+      {
+        id: 1,
+        result: {
+          id: 1
+        },
+        isError: false,
+        title: 'existing item, key name conversion'
+      },
+      {
+        id: 2,
+        result: undefined,
+        isError: false,
+        title: 'item doesn\'t exist'
+      }
+    ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        var result;
+
+        beforeEach(function () {
+          configsCollection.add({
+            id: 1
+          });
+          try {
+            result = configsCollection.getConfig(item.id);
+          } catch (e) {}
+        });
+
+        it('thrown error', function () {
+          expect(Em.assert.threw()).to.equal(item.isError);
+        });
+
+        it('returned value', function () {
+          expect(result).to.eql(item.result);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#getConfigByName', function () {
+
+    var configIds = ['n0_f0', 'n1_f1'],
+      cases = [
+        {
+          fileName: 'f0',
+          result: undefined,
+          isError: true,
+          title: 'no name passed'
+        },
+        {
+          name: 'n0',
+          result: undefined,
+          isError: true,
+          title: 'no filename passed'
+        },
+        {
+          name: 'n0',
+          fileName: 'f0',
+          result: {
+            id: 'n0_f0'
+          },
+          isError: false,
+          title: 'existing item'
+        },
+        {
+          name: 'n0',
+          fileName: 'f1',
+          result: undefined,
+          isError: false,
+          title: 'not existing item'
+        }
+      ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        var result;
+
+        beforeEach(function () {
+          sinon.stub(App.config, 'configId', function (name, fileName) {
+            return name + '_' + fileName;
+          });
+          configIds.forEach(function (id) {
+            configsCollection.add({
+              id: id
+            });
+          });
+          try {
+            result = configsCollection.getConfigByName(item.name, item.fileName);
+          } catch (e) {}
+        });
+
+        afterEach(function () {
+          App.config.configId.restore();
+          configsCollection.clearAll();
+        });
+
+
+        it('thrown error', function () {
+          expect(Em.assert.threw()).to.equal(item.isError);
+        });
+
+        it('returned value', function () {
+          expect(result).to.eql(item.result);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#getAll', function () {
+
+    var configs = [
+      {
+        id: 'c0'
+      },
+      {
+        id: 'c1'
+      }
+    ];
+
+    beforeEach(function () {
+      configsCollection.clearAll();
+    });
+
+    it('should return all configs', function () {
+      configs.forEach(function (item) {
+        configsCollection.add(item);
+      });
+      expect(configsCollection.getAll()).to.eql(configs);
+    });
+
+  });
+
+
+  describe('#clearAll', function () {
+
+    beforeEach(function () {
+      configsCollection.add({
+        id: 'c0'
+      });
+      configsCollection.clearAll();
+    });
+
+    it('should clear configs array', function () {
+      expect(configsCollection.getAll()).to.have.length(0);
+    });
+
+    it('should clear configs map', function () {
+      expect(configsCollection.getConfig('c0')).to.be.undefined;
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/credentials_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/credentials_test.js b/ambari-web/test/utils/credentials_test.js
new file mode 100644
index 0000000..7bf6791
--- /dev/null
+++ b/ambari-web/test/utils/credentials_test.js
@@ -0,0 +1,717 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var credentials = require('utils/credentials');
+var testHelpers = require('test/helpers');
+
+describe('credentials utils', function () {
+
+  var storeTypeStatusMock = function (clusterName, key) {
+    var result = {};
+    result[key] = clusterName;
+    return result;
+  };
+
+  describe('#createCredentials', function () {
+
+    it('should send AJAX request', function () {
+      credentials.createCredentials('c', 'a', {});
+      expect(testHelpers.findAjaxRequest('name', 'credentials.create')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.create',
+          data: {
+            clusterName: 'c',
+            resource: {},
+            alias: 'a'
+          },
+          error: 'createCredentialsErrorCallback'
+        }
+      ]);
+    });
+
+  });
+
+  describe('#credentialsSuccessCallback', function () {
+
+    var params = {
+        callback: Em.K
+      },
+      cases = [
+        {
+          items: [],
+          callbackArgument: [],
+          title: 'no data returned'
+        },
+        {
+          items: [{}, {}],
+          callbackArgument: [undefined, undefined],
+          title: 'empty data returned'
+        },
+        {
+          items: [
+            {
+              Credential: {
+                id: 0
+              }
+            },
+            {
+              Credential: {
+                id: 1
+              }
+            }
+          ],
+          callbackArgument: [
+            {
+              id: 0
+            },
+            {
+              id: 1
+            }
+          ],
+          title: 'valid data returned'
+        }
+      ];
+
+    beforeEach(function () {
+      sinon.spy(params, 'callback');
+    });
+
+    afterEach(function () {
+      params.callback.restore();
+    });
+
+    cases.forEach(function (item) {
+
+      it(item.title, function () {
+        credentials.credentialsSuccessCallback({
+          items: item.items
+        }, null, params);
+        expect(params.callback.firstCall.args).to.eql([item.callbackArgument]);
+      });
+
+    });
+
+  });
+
+  describe('#createOrUpdateCredentials', function () {
+
+    var mock = {
+        dfd: {
+          getCredential: null,
+          updateCredentials: null,
+          createCredentials: null
+        },
+        callback: Em.K,
+        getCredential: function () {
+          return mock.dfd.getCredential.promise();
+        },
+        updateCredentials: function () {
+          return mock.dfd.updateCredentials.promise();
+        },
+        createCredentials: function () {
+          return mock.dfd.createCredentials.promise();
+        }
+      },
+      cases = [
+        {
+          getCredentialResolve: true,
+          credentialsCallback: 'updateCredentials',
+          isCredentialsCallbackResolve: true,
+          status: 'success',
+          result: {
+            status: 200
+          },
+          callbackArgs: [
+            true,
+            {
+              status: 200
+            }
+          ],
+          title: 'successful credentials update'
+        },
+        {
+          getCredentialResolve: true,
+          credentialsCallback: 'updateCredentials',
+          isCredentialsCallbackResolve: false,
+          status: 'error',
+          result: {
+            status: 404
+          },
+          callbackArgs: [
+            false,
+            {
+              status: 404
+            }
+          ],
+          title: 'failed credentials update'
+        },
+        {
+          getCredentialResolve: false,
+          credentialsCallback: 'createCredentials',
+          isCredentialsCallbackResolve: true,
+          status: 'success',
+          result: {
+            status: 201
+          },
+          callbackArgs: [
+            true,
+            {
+              status: 201
+            }
+          ],
+          title: 'successful credentials creation'
+        },
+        {
+          getCredentialResolve: false,
+          credentialsCallback: 'createCredentials',
+          isCredentialsCallbackResolve: false,
+          status: 'error',
+          result: {
+            status: 500
+          },
+          callbackArgs: [
+            false,
+            {
+              status: 500
+            }
+          ],
+          title: 'failed credentials creation'
+        }
+      ];
+
+    beforeEach(function () {
+      sinon.stub(credentials, 'getCredential', mock.getCredential);
+      sinon.stub(credentials, 'updateCredentials', mock.updateCredentials);
+      sinon.stub(credentials, 'createCredentials', mock.createCredentials);
+      sinon.spy(mock, 'callback');
+      mock.dfd.getCredential = $.Deferred();
+      mock.dfd.updateCredentials = $.Deferred();
+      mock.dfd.createCredentials = $.Deferred();
+    });
+
+    afterEach(function () {
+      credentials.getCredential.restore();
+      credentials.updateCredentials.restore();
+      credentials.createCredentials.restore();
+      mock.callback.restore();
+    });
+
+    cases.forEach(function (item) {
+
+      var getCredentialMethod = item.getCredentialResolve ? 'resolve' : 'reject',
+        credentialsCallbackMethod = item.isCredentialsCallbackResolve ? 'resolve' : 'reject';
+
+      it(item.title, function () {
+        mock.dfd.getCredential[getCredentialMethod]();
+        mock.dfd[item.credentialsCallback][credentialsCallbackMethod](null, item.status, item.result);
+        credentials.createOrUpdateCredentials().done(mock.callback);
+        expect(mock.callback.firstCall.args).to.eql(item.callbackArgs);
+      });
+
+    });
+
+  });
+
+  describe('#getCredential', function () {
+
+    it('should send AJAX request', function () {
+      credentials.getCredential('c', 'a', Em.K);
+      expect(testHelpers.findAjaxRequest('name', 'credentials.get')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.get',
+          data: {
+            clusterName: 'c',
+            alias: 'a',
+            callback: Em.K
+          },
+          success: 'getCredentialSuccessCallback',
+          error: 'getCredentialErrorCallback'
+        }
+      ]);
+    });
+
+  });
+
+  describe('#getCredentialSuccessCallback', function () {
+
+    var params = {
+        callback: Em.K
+      },
+      cases = [
+        {
+          data: null,
+          callback: undefined,
+          callbackCallCount: 0,
+          title: 'no callback passed'
+        },
+        {
+          data: null,
+          callback: null,
+          callbackCallCount: 0,
+          title: 'invalid callback passed'
+        },
+        {
+          data: null,
+          callbackCallCount: 1,
+          callbackArgument: null,
+          title: 'no data passed'
+        },
+        {
+          data: {},
+          callbackCallCount: 1,
+          callbackArgument: null,
+          title: 'no credential info passed'
+        },
+        {
+          data: {
+            Credential: 'c'
+          },
+          callbackCallCount: 1,
+          callbackArgument: 'c',
+          title: 'credential info passed'
+        }
+      ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          sinon.spy(params, 'callback');
+          credentials.getCredentialSuccessCallback(item.data, null, item.hasOwnProperty('callback') ? {
+            callback: item.callback
+          } : params);
+        });
+
+        afterEach(function () {
+          params.callback.restore();
+        });
+
+        it('callback call count', function () {
+          expect(params.callback.callCount).to.equal(item.callbackCallCount);
+        });
+
+        if (item.callbackCallCount) {
+          it('callback argument', function () {
+            expect(params.callback.firstCall.args).to.eql([item.callbackArgument]);
+          });
+        }
+
+      });
+
+    });
+
+  });
+
+  describe('#updateCredentials', function () {
+
+    it('should send AJAX request', function () {
+      credentials.updateCredentials('c', 'a', {});
+      expect(testHelpers.findAjaxRequest('name', 'credentials.update')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.update',
+          data: {
+            clusterName: 'c',
+            alias: 'a',
+            resource: {}
+          }
+        }
+      ]);
+    });
+
+  });
+
+  describe('#credentials', function () {
+
+    it('should send AJAX request', function () {
+      credentials.credentials('c', Em.K);
+      expect(testHelpers.findAjaxRequest('name', 'credentials.list')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.list',
+          data: {
+            clusterName: 'c',
+            callback: Em.K
+          },
+          success: 'credentialsSuccessCallback'
+        }
+      ]);
+    });
+
+  });
+
+  describe('#removeCredentials', function () {
+
+    it('should send AJAX request', function () {
+      credentials.removeCredentials('c', 'a');
+      expect(testHelpers.findAjaxRequest('name', 'credentials.delete')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.delete',
+          data: {
+            clusterName: 'c',
+            alias: 'a'
+          }
+        }
+      ]);
+    });
+
+  });
+
+  describe('#storageInfo', function () {
+
+    it('should send AJAX request', function () {
+      credentials.storageInfo('c', Em.K);
+      expect(testHelpers.findAjaxRequest('name', 'credentials.store.info')).to.eql([
+        {
+          sender: credentials,
+          name: 'credentials.store.info',
+          data: {
+            clusterName: 'c',
+            callback: Em.K
+          },
+          success: 'storageInfoSuccessCallback'
+        }
+      ]);
+    });
+
+  });
+
+  describe('#storageInfoSuccessCallback', function () {
+
+    var params = {
+        callback: Em.K
+      },
+      cases = [
+        {
+          callbackArgument: null,
+          title: 'no clusters'
+        },
+        {
+          clusters: null,
+          callbackArgument: null,
+          title: 'invalid clusters info'
+        },
+        {
+          clusters: {},
+          callbackArgument: {
+            persistent: false,
+            temporary: false
+          },
+          title: 'empty clusters info'
+        },
+        {
+          clusters: {
+            credential_store_properties: {
+              'storage.persistent': true,
+              'storage.temporary': true
+            }
+          },
+          callbackArgument: {
+            persistent: false,
+            temporary: false
+          },
+          title: 'invalid storage properties format'
+        },
+        {
+          clusters: {
+            credential_store_properties: {}
+          },
+          callbackArgument: {
+            persistent: false,
+            temporary: false
+          },
+          title: 'no storage properties'
+        },
+        {
+          clusters: {
+            credential_store_properties: {
+              'storage.persistent': 'true',
+              'storage.temporary': 'false'
+            }
+          },
+          callbackArgument: {
+            persistent: true,
+            temporary: false
+          },
+          title: 'valid storage properties format - persistent storage'
+        },
+        {
+          clusters: {
+            credential_store_properties: {
+              'storage.persistent': 'false',
+              'storage.temporary': 'true'
+            }
+          },
+          callbackArgument: {
+            persistent: false,
+            temporary: true
+          },
+          title: 'valid storage properties format - temporary storage'
+        },
+        {
+          clusters: {
+            credential_store_properties: {
+              'storage.persistent': 'true',
+              'storage.temporary': 'true'
+            }
+          },
+          callbackArgument: {
+            persistent: true,
+            temporary: true
+          },
+          title: 'valid storage properties format - both types'
+        }
+      ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          sinon.spy(params, 'callback');
+          credentials.storageInfoSuccessCallback({
+            Clusters: item.clusters
+          }, null, params);
+        });
+
+        afterEach(function () {
+          params.callback.restore();
+        });
+
+        it('callback execution', function () {
+          expect(params.callback.calledOnce).to.be.true;
+        });
+
+        it('callback argument', function () {
+          expect(params.callback.firstCall.args).to.eql([item.callbackArgument]);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#isStorePersisted', function () {
+
+    beforeEach(function () {
+      sinon.stub(credentials, 'storeTypeStatus', storeTypeStatusMock);
+    });
+
+    afterEach(function () {
+      credentials.storeTypeStatus.restore();
+    });
+
+    it('should return storeTypeStatus result', function () {
+      expect(credentials.isStorePersisted('c')).to.eql({
+        persistent: 'c'
+      });
+    });
+
+  });
+
+
+  describe('#isStoreTemporary', function () {
+
+    beforeEach(function () {
+      sinon.stub(credentials, 'storeTypeStatus', storeTypeStatusMock);
+    });
+
+    afterEach(function () {
+      credentials.storeTypeStatus.restore();
+    });
+
+    it('should return storeTypeStatus result', function () {
+      expect(credentials.isStoreTemporary('c')).to.eql({
+        temporary: 'c'
+      });
+    });
+
+  });
+
+  describe('#storeTypeStatus', function () {
+
+    var mock = {
+        successCallback: Em.K,
+        errorCallback: Em.K
+      },
+      data = {
+        clusterName: 'c'
+      },
+      error = {
+        status: 404
+      },
+      cases = [
+        {
+          isSuccess: true,
+          callbackArgument: data,
+          title: 'success'
+        },
+        {
+          isSuccess: false,
+          callbackArgument: error,
+          title: 'fail'
+        }
+      ];
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        var callbackName = item.isSuccess ? 'successCallback' : 'errorCallback';
+
+        beforeEach(function () {
+          sinon.spy(mock, 'successCallback');
+          sinon.spy(mock, 'errorCallback');
+          sinon.stub(credentials, 'storageInfo', function (clusterName, callback) {
+            var dfd = $.Deferred();
+            if (item.isSuccess) {
+              callback({
+                temporary: data
+              });
+            } else {
+              dfd.reject(error);
+            }
+            return dfd.promise();
+          });
+          credentials.storeTypeStatus(null, 'temporary').then(mock.successCallback, mock.errorCallback);
+        });
+
+        afterEach(function () {
+          mock.successCallback.restore();
+          mock.errorCallback.restore();
+          credentials.storageInfo.restore();
+        });
+
+        it('success callback', function () {
+          expect(mock.successCallback.called).to.equal(item.isSuccess);
+        });
+
+        it('error callback', function () {
+          expect(mock.errorCallback.called).to.not.equal(item.isSuccess);
+        });
+
+        it('callback called once', function () {
+          expect(mock[callbackName].calledOnce).to.be.true;
+        });
+
+        it('callback arguments', function () {
+          expect(mock[callbackName].firstCall.args).to.eql([item.callbackArgument]);
+        });
+
+      });
+
+    });
+
+  });
+
+  describe('#createCredentialResource', function () {
+
+    it('should return object with arguments', function () {
+      expect(credentials.createCredentialResource('p', 'c', 't')).to.eql({
+        principal: 'p',
+        key: 'c',
+        type: 't'
+      });
+    });
+
+  });
+
+  describe('#isKDCCredentialsPersisted', function () {
+
+    var cases = [
+      {
+        credentials: [],
+        isKDCCredentialsPersisted: false,
+        title: 'empty array passed'
+      },
+      {
+        credentials: [{}, {}],
+        isKDCCredentialsPersisted: false,
+        title: 'no aliases passed'
+      },
+      {
+        credentials: [
+          {
+            alias: 'a0'
+          },
+          {
+            alias: 'a1'
+          }
+        ],
+        isKDCCredentialsPersisted: false,
+        title: 'no KDC admin credentials passed'
+      },
+      {
+        credentials: [
+          {
+            alias: 'kdc.admin.credential'
+          },
+          {
+            alias: 'a2'
+          }
+        ],
+        isKDCCredentialsPersisted: false,
+        title: 'no KDC admin credentials type passed'
+      },
+      {
+        credentials: [
+          {
+            alias: 'kdc.admin.credential',
+            type: 'temporary'
+          },
+          {
+            alias: 'a3'
+          }
+        ],
+        isKDCCredentialsPersisted: false,
+        title: 'temporary storage'
+      },
+      {
+        credentials: [
+          {
+            alias: 'kdc.admin.credential',
+            type: 'persisted'
+          },
+          {
+            alias: 'kdc.admin.credential'
+          },
+          {
+            alias: 'a4'
+          }
+        ],
+        isKDCCredentialsPersisted: true,
+        title: 'persistent storage'
+      }
+    ];
+
+    cases.forEach(function (item) {
+
+      it(item.title, function () {
+        expect(credentials.isKDCCredentialsPersisted(item.credentials)).to.equal(item.isKDCCredentialsPersisted);
+      });
+
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/file_utils_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/file_utils_test.js b/ambari-web/test/utils/file_utils_test.js
new file mode 100644
index 0000000..8c9eb6d
--- /dev/null
+++ b/ambari-web/test/utils/file_utils_test.js
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fileUtils = require('utils/file_utils');
+
+describe('file_utils', function () {
+
+  describe('#openInfoInNewTab', function () {
+
+    var mock = {
+      document: {
+        write: Em.K
+      },
+      focus: Em.K
+    };
+
+    beforeEach(function () {
+      sinon.stub(window, 'open').returns(mock);
+      sinon.spy(mock.document, 'write');
+      sinon.spy(mock, 'focus');
+      fileUtils.openInfoInNewTab('data');
+    });
+
+    afterEach(function () {
+      window.open.restore();
+      mock.document.write.restore();
+      mock.focus.restore();
+    });
+
+    it('opening new window', function () {
+      expect(window.open.calledOnce).to.be.true;
+    });
+
+    it('no URL for new window', function () {
+      expect(window.open.firstCall.args).to.eql(['']);
+    });
+
+    it('writing document contents', function () {
+      expect(mock.document.write.calledOnce).to.be.true;
+    });
+
+    it('document contents', function () {
+      expect(mock.document.write.firstCall.args).to.eql(['data']);
+    });
+
+    it('focusing on new window', function () {
+      expect(mock.focus.calledOnce).to.be.true;
+    });
+
+  });
+
+  describe('#safariDownload', function () {
+
+    var linkEl = {
+      click: Em.K
+    };
+
+    beforeEach(function () {
+      sinon.stub(document, 'createElement').returns(linkEl);
+      sinon.stub(document.body, 'appendChild', Em.K);
+      sinon.stub(document.body, 'removeChild', Em.K);
+      sinon.spy(linkEl, 'click');
+      fileUtils.safariDownload('file data', 'csv', 'file.csv');
+    });
+
+    afterEach(function () {
+      document.createElement.restore();
+      document.body.appendChild.restore();
+      document.body.removeChild.restore();
+      linkEl.click.restore();
+    });
+
+    it('creating new element', function () {
+      expect(document.createElement.calledOnce).to.be.true;
+    });
+
+    it('new element is a link', function () {
+      expect(document.createElement.firstCall.args).to.eql(['a']);
+    });
+
+    it('link URL', function () {
+      expect(linkEl.href).to.equal('data:attachment/csv;charset=utf-8,file%20data');
+    });
+
+    it('file name', function () {
+      expect(linkEl.download).to.equal('file.csv');
+    });
+
+    it('appending element to document', function () {
+      expect(document.body.appendChild.calledOnce).to.be.true;
+    });
+
+    it('link is appended', function () {
+      expect(document.body.appendChild.firstCall.args).to.eql([linkEl]);
+    });
+
+    it('link is clicked', function () {
+      expect(linkEl.click.calledOnce).to.be.true;
+    });
+
+    it('removing element from document', function () {
+      expect(document.body.removeChild.calledOnce).to.be.true;
+    });
+
+    it('link is removed', function () {
+      expect(document.body.removeChild.firstCall.args).to.eql([linkEl]);
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/handlebars_helpers_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/handlebars_helpers_test.js b/ambari-web/test/utils/handlebars_helpers_test.js
new file mode 100644
index 0000000..4e705ad
--- /dev/null
+++ b/ambari-web/test/utils/handlebars_helpers_test.js
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+require('utils/handlebars_helpers');
+
+describe('handlebars_helpers', function () {
+
+  describe('#registerBoundHelper', function () {
+
+    var options = {
+      hash: {}
+    };
+
+    beforeEach(function () {
+      sinon.stub(Em.Handlebars, 'registerHelper', function (name, callback) {
+        callback('prop', options);
+      });
+      sinon.stub(Em.Handlebars.helpers, 'view', Em.K);
+      App.registerBoundHelper('helper', {});
+    });
+
+    afterEach(function () {
+      Em.Handlebars.registerHelper.restore();
+      Em.Handlebars.helpers.view.restore();
+    });
+
+    it('contentBinding', function () {
+      expect(options.hash.contentBinding).to.equal('prop');
+    });
+
+    it('view', function () {
+      expect(Em.Handlebars.helpers.view.calledOnce).to.be.true;
+    });
+
+    it('view arguments', function () {
+      expect(Em.Handlebars.helpers.view.firstCall.args).to.eql([{}, options]);
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/d5d1afea/ambari-web/test/utils/heatmap_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/heatmap_test.js b/ambari-web/test/utils/heatmap_test.js
new file mode 100644
index 0000000..0a63cbf
--- /dev/null
+++ b/ambari-web/test/utils/heatmap_test.js
@@ -0,0 +1,141 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var heatmap = require('utils/heatmap');
+
+describe('heatmap utils', function () {
+
+  describe('mappers', function () {
+
+    var mappers;
+
+    beforeEach(function () {
+      mappers = Em.Object.create(heatmap.mappers);
+    });
+
+    describe('#metricMapperWithTransform', function () {
+
+      var cases = [
+        {
+          hostComponents: null,
+          hostToValueMap: {},
+          title: 'no host components data'
+        },
+        {
+          hostComponents: [null, null],
+          metricName: 'm0',
+          hostToValueMap: {},
+          title: 'host components data is absent'
+        },
+        {
+          hostComponents: [{}, {}],
+          metricName: 'm1',
+          hostToValueMap: {},
+          title: 'provided metric data is absent'
+        },
+        {
+          hostComponents: [{}, {}],
+          metricName: 'm2.m3',
+          hostToValueMap: {},
+          title: 'provided metrics data is absent'
+        },
+        {
+          hostComponents: [
+            null,
+            {},
+            {
+              m4: 1,
+              HostRoles: {
+                host_name: 'h0'
+              }
+            },
+            {
+              m4: 1.5,
+              HostRoles: {
+                host_name: 'h1'
+              }
+            },
+            {
+              m4: 1.60,
+              HostRoles: {
+                host_name: 'h2'
+              }
+            },
+            {
+              m4: 1.72,
+              HostRoles: {
+                host_name: 'h3'
+              }
+            },
+            {
+              m4: 1.85,
+              HostRoles: {
+                host_name: 'h4'
+              }
+            },
+            {
+              m4: 1.97,
+              HostRoles: {
+                host_name: 'h5'
+              }
+            }
+          ],
+          metricName: 'm4',
+          hostToValueMap: {
+            h0: '1.0',
+            h1: '1.5',
+            h2: '1.6',
+            h3: '1.7',
+            h4: '1.9',
+            h5: '2.0'
+          },
+          title: 'no transform function'
+        },
+        {
+          hostComponents: [
+            {
+              m5: 100,
+              HostRoles: {
+                host_name: 'h6'
+              }
+            }
+          ],
+          metricName: 'm5',
+          transformValueFunction: Math.sqrt,
+          hostToValueMap: {
+            h6: '10.0'
+          },
+          title: 'transform function provided'
+        }
+      ];
+
+      cases.forEach(function (item) {
+
+        it(item.title, function () {
+          expect(mappers.metricMapperWithTransform({
+            host_components: item.hostComponents
+          }, item.metricName, item.transformValueFunction)).to.eql(item.hostToValueMap);
+        });
+
+      });
+
+    });
+
+  });
+
+});