You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2015/03/16 12:33:18 UTC

[1/4] ambari git commit: AMBARI-10062. Ambari Web Client Makes Request For All Alerts On Specific Alert Page (onechiporenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk 39157df9d -> ed616f1d5


AMBARI-10062. Ambari Web Client Makes Request For All Alerts On Specific Alert Page (onechiporenko)


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

Branch: refs/heads/trunk
Commit: 72b40b7b70daf5ad42e32aef4f94f08cddb35403
Parents: 39157df
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Fri Mar 13 19:50:43 2015 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Mon Mar 16 10:58:43 2015 +0200

----------------------------------------------------------------------
 .../app/controllers/global/update_controller.js |  12 +-
 .../main/alert_definitions_controller.js        | 161 ++---------------
 .../main/alerts/alert_instances_controller.js   | 181 +++++++++++++++++++
 .../app/mixins/common/table_server_mixin.js     |   6 +-
 ambari-web/app/models/service.js                |   4 +-
 ambari-web/app/styles/application.less          |   9 -
 ambari-web/app/styles/common.less               |   8 +
 ambari-web/app/templates/application.hbs        |  18 +-
 .../common/modal_popups/alerts_popup.hbs        |   5 +
 .../main/alerts/definition_details.hbs          |   2 +-
 ambari-web/app/templates/main/host.hbs          |   2 +-
 ambari-web/app/utils/ajax/ajax.js               |  14 +-
 ambari-web/app/utils/ember_reopen.js            |  13 ++
 .../main/alerts/definition_details_view.js      |   5 +-
 ambari-web/app/views/main/host.js               |   2 +-
 .../main/alert_definitions_controller_test.js   |  32 ----
 .../alerts/alert_instances_controller_test.js   |  33 ++++
 17 files changed, 300 insertions(+), 207 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/controllers/global/update_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/update_controller.js b/ambari-web/app/controllers/global/update_controller.js
index 41cbd10..d89e687 100644
--- a/ambari-web/app/controllers/global/update_controller.js
+++ b/ambari-web/app/controllers/global/update_controller.js
@@ -114,6 +114,15 @@ App.UpdateController = Em.Controller.extend({
   }),
 
   /**
+   * Pagination query-parameters for unhealthy alerts request
+   * @type {{from: Number, page_size: Number}}
+   */
+  queryParamsForUnhealthyAlertInstances: {
+    from: 0,
+    page_size: 10
+  },
+
+  /**
    * map describes relations between updater function and table
    */
   tableUpdaterMap: {
@@ -469,7 +478,8 @@ App.UpdateController = Em.Controller.extend({
 
   updateUnhealthyAlertInstances: function (callback) {
     var testUrl = '/data/alerts/alert_instances.json';
-    var realUrl = '/alerts?fields=*&Alert/state.in(CRITICAL,WARNING)&Alert/maintenance_state.in(OFF)';
+    var queryParams = this.get('queryParamsForUnhealthyAlertInstances');
+    var realUrl = '/alerts?fields=*&Alert/state.in(CRITICAL,WARNING)&Alert/maintenance_state.in(OFF)&from=' + queryParams.from + '&page_size=' + queryParams.page_size;
     var url = this.getUrl(testUrl, realUrl);
 
     App.HttpClient.get(url, App.alertInstanceMapper, {

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/controllers/main/alert_definitions_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alert_definitions_controller.js b/ambari-web/app/controllers/main/alert_definitions_controller.js
index f5086d1..8f7fcb8 100644
--- a/ambari-web/app/controllers/main/alert_definitions_controller.js
+++ b/ambari-web/app/controllers/main/alert_definitions_controller.js
@@ -37,14 +37,6 @@ App.MainAlertDefinitionsController = Em.ArrayController.extend({
 
   /**
    * List of all <code>App.AlertDefinition</code>
-   * Consists of:
-   * <ul>
-   *   <li>App.PortAlertDefinition</li>
-   *   <li>App.MetricsAlertDefinition</li>
-   *   <li>App.WebAlertDefinition</li>
-   *   <li>App.AggregateAlertDefinition</li>
-   *   <li>App.ScriptAlertDefinition</li>
-   * </ul>
    * @type {App.AlertDefinition[]}
    */
   content: App.AlertDefinition.find(),
@@ -92,19 +84,23 @@ App.MainAlertDefinitionsController = Em.ArrayController.extend({
   /**
    * Calculate critical count for each service, to show up the label on services menu
    * @method getCriticalAlertsCountForService
-   * @return {number}
+   * @return {Number}
    */
   getCriticalAlertsCountForService: function (service) {
-    return this.get('criticalAlertInstances').filterProperty('service', service).get('length');
+    return this.get('content').filterProperty('service.serviceName', service).
+      invoke('getWithDefault', 'summary.CRITICAL.count', 0).
+      reduce(Em.sum, 0);
   },
 
   /**
    * Calculate critical/warning count for each service, to show up the label on services menu
    * @method getCriticalOrWarningAlertsCountForService
-   * @return {number}
+   * @return {Number}
    */
   getCriticalOrWarningAlertsCountForService: function (service) {
-    return this.get('unhealthyAlertInstances').filterProperty('service', service).get('length');
+    return this.get('content').filterProperty('service.serviceName', service).map(function (alertDefinition) {
+      return alertDefinition.getWithDefault('summary.CRITICAL.count', 0) + alertDefinition.getWithDefault('summary.WARNING.count', 0);
+    }).reduce(Em.sum, 0);
   },
 
   /**
@@ -112,138 +108,21 @@ App.MainAlertDefinitionsController = Em.ArrayController.extend({
    */
 
   /**
-   * Alerts number to show up on top-nav bar: number of critical/warning alerts
-   * @type {number}
+   * Number of all critical and warning alert instances
+   * Calculation is based on each <code>alertDefinitions.summary</code>
+   * @type {Number}
    */
-  allAlertsCount: function () {
-    return this.get('unhealthyAlertInstances.length');
-  }.property('unhealthyAlertInstances.length'),
-
-  criticalAlertInstances: function () {
-    return App.AlertInstance.find().toArray().filterProperty('state', 'CRITICAL');
-  }.property('mapperTimestamp'),
-
-  warningAlertInstances: function () {
-    return App.AlertInstance.find().toArray().filterProperty('state', 'WARNING');
-  }.property('mapperTimestamp'),
-
-  unhealthyAlertInstances: function () {
-    return this.get('criticalAlertInstances').concat(this.get('warningAlertInstances'));
-  }.property('criticalAlertInstances', 'warningAlertInstances'),
+  unhealthyAlertInstancesCount: function () {
+    return this.get('content').map(function (alertDefinition) {
+      return alertDefinition.getWithDefault('summary.CRITICAL.count', 0) + alertDefinition.getWithDefault('summary.WARNING.count', 0);
+    }).reduce(Em.sum, 0);
+  }.property('content.@each.summary'),
 
   /**
-   * if critical alerts exist, if true, the alert badge should be red.
+   * if critical alerts exist, the alert badge should be red.
+   * @type {Boolean}
    */
   isCriticalAlerts: function () {
-    return this.get('unhealthyAlertInstances').someProperty('state', 'CRITICAL');
-  }.property('unhealthyAlertInstances.@each.state'),
-
-  /**
-   * Onclick handler for alerts number located right to bg ops number (see application.hbs)
-   * @method showPopup
-   * @return {App.ModalPopup}
-   */
-  showPopup: function () {
-
-    var self = this;
-
-    return App.ModalPopup.show({
-
-      header: function () {
-        return Em.I18n.t('alerts.fastAccess.popup.header').format(this.get('definitionsController.allAlertsCount'));
-      }.property('definitionsController.allAlertsCount'),
-
-      definitionsController: this,
-
-      classNames: ['sixty-percent-width-modal', 'alerts-popup'],
-
-      secondary: Em.I18n.t('alerts.fastAccess.popup.body.showmore'),
-
-      autoHeight: false,
-
-      isHideBodyScroll: true,
-
-      onSecondary: function () {
-        this.hide();
-        App.router.transitionTo('main.alerts.index');
-      },
-
-      bodyClass: App.TableView.extend({
-
-        templateName: require('templates/common/modal_popups/alerts_popup'),
-
-        controller: self,
-
-        isPaginate: true,
-
-        content: function () {
-          return this.get('controller.unhealthyAlertInstances');
-        }.property('controller.unhealthyAlertInstances.length', 'controller.unhealthyAlertInstances.@each.state'),
-
-        isLoaded: function () {
-          return !!this.get('controller.unhealthyAlertInstances');
-        }.property('controller.unhealthyAlertInstances'),
-
-        isAlertEmptyList: function () {
-          return !this.get('content.length');
-        }.property('content.length'),
-
-        /**
-         * No filtering for alert definitions
-         * @method filter
-         */
-        filter: function() {
-          this.set('filteredContent', this.get('content'));
-        }.observes('content.length'),
-
-        refreshTooltips: function () {
-          this.ensureTooltip();
-        }.observes('contents.length'),
-
-        ensureTooltip: function () {
-          Em.run.next(this, function () {
-            App.tooltip($(".timeago"));
-          });
-        },
-        /**
-         * Router transition to alert definition details page
-         * @param event
-         */
-        gotoAlertDetails: function (event) {
-          if (event && event.context) {
-            this.get('parentView').hide();
-            var definition = this.get('controller.content').findProperty('id', event.context.get('definitionId'));
-            App.router.transitionTo('main.alerts.alertDetails', definition);
-          }
-        },
-
-        /**
-         * Router transition to service summary page
-         * @param event
-         */
-        gotoService: function (event) {
-          if (event && event.context) {
-            this.get('parentView').hide();
-            App.router.transitionTo('main.services.service.summary', event.context);
-          }
-        },
-
-        /**
-         * Router transition to host level alerts page
-         * @param event
-         */
-        goToHostAlerts: function (event) {
-          if (event && event.context) {
-            this.get('parentView').hide();
-            App.router.transitionTo('main.hosts.hostDetails.alerts', event.context);
-          }
-        },
-
-        didInsertElement: function () {
-          this.filter();
-          this.ensureTooltip();
-        }
-      })
-    });
-  }
+    return !this.get('content').everyProperty('summary.CRITICAL.count', 0);
+  }.property('content.@each.summary')
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/controllers/main/alerts/alert_instances_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alerts/alert_instances_controller.js b/ambari-web/app/controllers/main/alerts/alert_instances_controller.js
index a3bf7fe..9bf91d5 100644
--- a/ambari-web/app/controllers/main/alerts/alert_instances_controller.js
+++ b/ambari-web/app/controllers/main/alerts/alert_instances_controller.js
@@ -22,6 +22,17 @@ App.MainAlertInstancesController = Em.Controller.extend({
 
   name: 'mainAlertInstancesController',
 
+  content: App.AlertInstance.find(),
+
+  /**
+   * @type {App.AlertInstance[]}
+   */
+  unhealthyAlertInstances: function () {
+    return App.AlertInstance.find().filter(function (item) {
+      return ['CRITICAL', 'WARNING'].contains(item.get('state'));
+    });
+  }.property('content.[]'),
+
   /**
    * Are alertInstances loaded
    * @type {boolean}
@@ -169,6 +180,176 @@ App.MainAlertInstancesController = Em.Controller.extend({
    */
   getAlertInstancesErrorCallback: function () {
     this.set('isLoaded', true);
+  },
+
+  /**
+   * Onclick handler for alerts number located right to bg ops number (see application.hbs)
+   * @method showPopup
+   * @return {App.ModalPopup}
+   */
+  showPopup: function () {
+
+    var self = this;
+
+    return App.ModalPopup.show({
+
+      alertsNumberBinding: 'App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount',
+
+      header: function () {
+        return Em.I18n.t('alerts.fastAccess.popup.header').format(this.get('alertsNumber'));
+      }.property('alertsNumber'),
+
+      definitionsController: this,
+
+      classNames: ['sixty-percent-width-modal', 'alerts-popup'],
+
+      secondary: Em.I18n.t('alerts.fastAccess.popup.body.showmore'),
+
+      autoHeight: false,
+
+      isHideBodyScroll: true,
+
+      onSecondary: function () {
+        this._super();
+        App.router.transitionTo('main.alerts.index');
+      },
+
+      bodyClass: App.TableView.extend(App.TableServerViewMixin, {
+
+        updaterBinding: 'App.router.updateController',
+
+        templateName: require('templates/common/modal_popups/alerts_popup'),
+
+        controller: self,
+
+        isPaginate: true,
+
+        willInsertElement: function () {
+          this._super();
+          this.updateAlertInstances();
+        },
+
+        /**
+         * Number of all critical and warning alert instances
+         * @type {Boolean}
+         */
+        filteredCount: function () {
+          return App.router.get('mainAlertDefinitionsController.unhealthyAlertInstancesCount');
+        }.property('alertsNumber'),
+
+        content: function () {
+          return this.get('controller.unhealthyAlertInstances');
+        }.property('controller.unhealthyAlertInstances.@each.state'),
+
+        isLoaded: function () {
+          return !!this.get('controller.unhealthyAlertInstances');
+        }.property('controller.unhealthyAlertInstances'),
+
+        isAlertEmptyList: function () {
+          return !this.get('content.length');
+        }.property('content.length'),
+
+        /**
+         * Update list of shown alert instances
+         * @method updateAlertInstances
+         */
+        updateAlertInstances: function () {
+          var self = this,
+            displayLength = this.get('displayLength'),
+            startIndex = this.get('startIndex');
+          if (!displayLength) return; // wait while table-info is loaded
+          this.get('updater').set('queryParamsForUnhealthyAlertInstances', {
+            from: startIndex - 1,
+            page_size: displayLength
+          });
+          this.set('filteringComplete', false);
+          this.get('updater').updateUnhealthyAlertInstances(function() {
+            self.set('filteringComplete', true);
+          });
+        }.observes('displayLength', 'startIndex'),
+
+        /**
+         * Show spinner when filter/sorting request is in processing
+         * @method overlayObserver
+         */
+        overlayObserver: function() {
+          var $tbody = this.$('#alert-info'),
+            $overlay = this.$('.table-overlay'),
+            $spinner = $($overlay).find('.spinner');
+          if (!this.get('filteringComplete')) {
+            if (!$tbody) return;
+            var tbodyPos =  $tbody.position();
+            if (!tbodyPos) return;
+            $spinner.css('display', 'block');
+            $overlay.css({
+              top: tbodyPos.top + 1,
+              left: tbodyPos.left + 1,
+              width: $tbody.width() - 1,
+              height: $tbody.height() - 1
+            });
+          }
+        },
+
+        /**
+         * No filtering for alert definitions
+         * @method filter
+         */
+        filter: function() {
+          this.set('filteredContent', this.get('content'));
+        }.observes('content.length'),
+
+        refreshTooltips: function () {
+          this.ensureTooltip();
+        }.observes('contents.[]'),
+
+        ensureTooltip: function () {
+          Em.run.next(this, function () {
+            App.tooltip($(".timeago"));
+          });
+        },
+        /**
+         * Router transition to alert definition details page
+         * @param event
+         */
+        gotoAlertDetails: function (event) {
+          if (event && event.context) {
+            this.get('parentView').hide();
+            var definition = App.AlertDefinition.find().findProperty('id', event.context.get('definitionId'));
+            App.router.transitionTo('main.alerts.alertDetails', definition);
+          }
+        },
+
+        /**
+         * Router transition to service summary page
+         * @param event
+         */
+        gotoService: function (event) {
+          if (event && event.context) {
+            this.get('parentView').hide();
+            App.router.transitionTo('main.services.service.summary', event.context);
+          }
+        },
+
+        /**
+         * Router transition to host level alerts page
+         * @param event
+         */
+        goToHostAlerts: function (event) {
+          if (event && event.context) {
+            this.get('parentView').hide();
+            App.router.transitionTo('main.hosts.hostDetails.alerts', event.context);
+          }
+        },
+
+        didInsertElement: function () {
+          this.filter();
+          this.ensureTooltip();
+          this.addObserver('filteringComplete', this, this.overlayObserver);
+          this.overlayObserver();
+          return this._super();
+        }
+      })
+    });
   }
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/mixins/common/table_server_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/table_server_mixin.js b/ambari-web/app/mixins/common/table_server_mixin.js
index 9c35d98..5162a02 100644
--- a/ambari-web/app/mixins/common/table_server_mixin.js
+++ b/ambari-web/app/mixins/common/table_server_mixin.js
@@ -19,10 +19,10 @@
 var App = require('app');
 var validator = require('utils/validator');
 
-//TODO integrate mixin into mainHostController to avoid code duplication
 App.TableServerMixin = Em.Mixin.create({
   queryParams: [],
   resetStartIndex: false,
+
   /**
    * filterProps support follow types of filter:
    * MATCH - match of RegExp
@@ -33,8 +33,9 @@ App.TableServerMixin = Em.Mixin.create({
    * CUSTOM - substitute values with keys "{#}" in alias
    */
   filterProps: [],
+
   /**
-   * include "from" nad "page_size"
+   * include "from" and "page_size"
    */
   paginationProps: [
     {
@@ -50,6 +51,7 @@ App.TableServerMixin = Em.Mixin.create({
       type: 'EQUAL'
     }
   ],
+
   sortProps: [],
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/models/service.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/service.js b/ambari-web/app/models/service.js
index d633ad2..a4696d1 100644
--- a/ambari-web/app/models/service.js
+++ b/ambari-web/app/models/service.js
@@ -143,12 +143,12 @@ App.Service = DS.Model.extend({
 
   hasCriticalAlerts: function () {
     var controller = App.router.get('mainAlertDefinitionsController');
-    return controller.getCriticalAlertsCountForService(this) > 0;
+    return controller.getCriticalAlertsCountForService(this.get('serviceName')) > 0;
   }.property('alertsCount'),
 
   alertsCount: function () {
     var controller = App.router.get('mainAlertDefinitionsController');
-    return controller.getCriticalOrWarningAlertsCountForService(this);
+    return controller.getCriticalOrWarningAlertsCountForService(this.get('serviceName'));
   }.property('App.router.mainAlertDefinitionsController.content.@each.isCriticalOrWarning')
 });
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index 6b51006..ea132d1 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -2866,15 +2866,6 @@ table.graphs {
     }
   }
 
-  .hosts-overlay {
-    position: absolute;
-    opacity: 0.9;
-    background-color: #fefefe;
-    .spinner {
-      display: none;
-    }
-  }
-
   .status-dot-position {
     background-position: center;
     background-repeat: no-repeat;

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/styles/common.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/common.less b/ambari-web/app/styles/common.less
index e9e2528..fde2bd3 100644
--- a/ambari-web/app/styles/common.less
+++ b/ambari-web/app/styles/common.less
@@ -304,3 +304,11 @@
   opacity: 0;
 }
 
+.table-overlay {
+  position: absolute;
+  opacity: 0.9;
+  background-color: #fefefe;
+  .spinner {
+    display: none;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/templates/application.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/application.hbs b/ambari-web/app/templates/application.hbs
index dd63a11..63da6e7 100644
--- a/ambari-web/app/templates/application.hbs
+++ b/ambari-web/app/templates/application.hbs
@@ -38,16 +38,14 @@
                     {{allOperationsCount}} {{pluralize allOperationsCount singular="t:op" plural="t:ops"}}</span>
                 {{/if}}
               {{/with}}
-              {{#with App.router.mainAlertDefinitionsController}}
-                {{#if allAlertsCount}}
-                  <span {{bindAttr class=":label isCriticalAlerts:alert-crit-count:alert-warn-count"}} {{action "showPopup" target="App.router.mainAlertDefinitionsController"}}>
-                  {{allAlertsCount}} {{pluralize allAlertsCount singular="alert" plural="alerts"}}</span>
-                {{else}}
-                  <span rel="tooltip" {{translateAttr title="titlebar.alerts.noAlerts"}}
-                        class="label alerts-none-count" {{action "showPopup" target="App.router.mainAlertDefinitionsController"}}>
-                  {{allAlertsCount}} {{pluralize allAlertsCount singular="alert" plural="alerts"}}</span>
-                {{/if}}
-              {{/with}}
+              {{#if App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount}}
+                <span {{bindAttr class=":label App.router.mainAlertDefinitionsController.isCriticalAlerts:alert-crit-count:alert-warn-count"}} {{action "showPopup" target="App.router.mainAlertInstancesController"}}>
+                {{App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount}} {{pluralize App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount singular="alert" plural="alerts"}}</span>
+              {{else}}
+                <span rel="tooltip" {{translateAttr title="titlebar.alerts.noAlerts"}}
+                      class="label alerts-none-count" {{action "showPopup" target="App.router.mainAlertInstancesController"}}>
+                {{App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount}} {{pluralize App.router.mainAlertDefinitionsController.unhealthyAlertInstancesCount singular="alert" plural="alerts"}}</span>
+              {{/if}}
             </a>
           {{else}}
             <a class="logo"><img src="/img/logo-white.png" alt="Apache Ambari" title="Apache Ambari"></a>

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/templates/common/modal_popups/alerts_popup.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/modal_popups/alerts_popup.hbs b/ambari-web/app/templates/common/modal_popups/alerts_popup.hbs
index a0f7808..2da35ec 100644
--- a/ambari-web/app/templates/common/modal_popups/alerts_popup.hbs
+++ b/ambari-web/app/templates/common/modal_popups/alerts_popup.hbs
@@ -61,6 +61,11 @@
         {{/if}}
       </div>
     </div>
+
+    <div {{bindAttr class="view.filteringComplete:hidden :table-overlay"}}>
+      <div class="spinner"></div>
+    </div>
+
     {{#if view.isPaginate}}
       <div class="page-bar pull-right no-borders mtm">
         <div class="items-on-page">

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/templates/main/alerts/definition_details.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/definition_details.hbs b/ambari-web/app/templates/main/alerts/definition_details.hbs
index 960cf04..ac0652d 100644
--- a/ambari-web/app/templates/main/alerts/definition_details.hbs
+++ b/ambari-web/app/templates/main/alerts/definition_details.hbs
@@ -203,7 +203,7 @@
                   <time class="timeago"
                         rel="tooltip"  {{bindAttr data-original-title="instance.statusChangedAndLastCheckedFormatted"}}>{{instance.lastTriggeredForFormatted}}</time>
                 </td>
-                <td>{{view view.parentView.lastDayCount hostBinding="instance.host"}}</td>
+                <td>{{view view.parentView.lastDayCount hostNameBinding="instance.hostName"}}</td>
                 <td><span class="alert-text" rel="tooltip" {{bindAttr data-original-title="instance.text"}}
                           class="alert-text">{{instance.text}}</span></td>
                 {{/view}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/templates/main/host.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host.hbs b/ambari-web/app/templates/main/host.hbs
index 3724926..2fa4f37 100644
--- a/ambari-web/app/templates/main/host.hbs
+++ b/ambari-web/app/templates/main/host.hbs
@@ -163,7 +163,7 @@
     </tbody>
   </table>
 
-  <div {{bindAttr class="view.filteringComplete:hidden :hosts-overlay"}}>
+  <div {{bindAttr class="view.filteringComplete:hidden :table-overlay"}}>
     <div class="spinner"></div>
   </div>
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/utils/ajax/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js
index 240d944..c3e7d80 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -373,20 +373,24 @@ var urls = {
     'real': '/clusters/{clusterName}/alert_definitions?fields=*',
     'mock': 'data/alerts/alertDefinitions.json'
   },
+  'alerts.notifications': {
+    'real': '/alert_targets?fields=*',
+    'mock': '/data/alerts/alertNotifications.json'
+  },
   'alerts.instances': {
     'real': '/clusters/{clusterName}/alerts?fields=*',
     'mock': '/data/alerts/alert_instances.json'
   },
-  'alerts.notifications': {
-    'real': '/alert_targets?fields=*',
-    'mock': '/data/alerts/alertNotifications.json'
+  'alerts.instances.unhealthy': {
+    'real': '/clusters/{clusterName}/alerts?fields=*&Alert/state.in(CRITICAL,WARNING)&{paginationInfo}',
+    'mock': '/data/alerts/alert_instances.json'
   },
   'alerts.instances.by_definition': {
-    'real': '/clusters/{clusterName}/alerts?fields=*&(Alert/definition_id={definitionId}|Alert/state.in(CRITICAL,WARNING))',
+    'real': '/clusters/{clusterName}/alerts?fields=*&Alert/definition_id={definitionId}',
     'mock': '/data/alerts/alert_instances.json'
   },
   'alerts.instances.by_host': {
-    'real': '/clusters/{clusterName}/alerts?fields=*&(Alert/host_name={hostName}|Alert/state.in(CRITICAL,WARNING))',
+    'real': '/clusters/{clusterName}/alerts?fields=*&Alert/host_name={hostName}',
     'mock': '/data/alerts/alert_instances.json'
   },
   'alerts.update_alert_definition': {

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/utils/ember_reopen.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ember_reopen.js b/ambari-web/app/utils/ember_reopen.js
index a879cde..4446f1e 100644
--- a/ambari-web/app/utils/ember_reopen.js
+++ b/ambari-web/app/utils/ember_reopen.js
@@ -115,6 +115,19 @@ Ember.isBlank = function(obj) {
 };
 
 /**
+ * Calculates sum of two numbers
+ * Use this function as a callback on <code>invoke</code> etc
+ *
+ * @method sum
+ * @param {Number} a
+ * @param {Number} b
+ * @returns {Number}
+ */
+Ember.sum = function (a, b) {
+  return a + b;
+};
+
+/**
  *
  */
 Ember.RadioButton = Ember.Checkbox.extend({

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/views/main/alerts/definition_details_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/alerts/definition_details_view.js b/ambari-web/app/views/main/alerts/definition_details_view.js
index 28a7c9b..a5e717a 100644
--- a/ambari-web/app/views/main/alerts/definition_details_view.js
+++ b/ambari-web/app/views/main/alerts/definition_details_view.js
@@ -85,11 +85,12 @@ App.MainAlertDefinitionDetailsView = App.TableView.extend({
    * View calculates and represents count of alerts on appropriate host during last day
    */
   lastDayCount: Em.View.extend({
+    hostName: '', // binding from template
     template: Ember.Handlebars.compile('<span>{{view.count}}</span>'),
     count: function () {
       var lastDayAlertsCount = this.get('parentView.controller.lastDayAlertsCount');
-      return lastDayAlertsCount ? lastDayAlertsCount[this.get('host.hostName')] || 0 : Em.I18n.t('app.loadingPlaceholder');
-    }.property('parentView.controller.lastDayAlertsCount', 'host')
+      return lastDayAlertsCount ? lastDayAlertsCount[this.get('hostName')] || 0 : Em.I18n.t('app.loadingPlaceholder');
+    }.property('parentView.controller.lastDayAlertsCount', 'hostName')
   }),
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/app/views/main/host.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host.js b/ambari-web/app/views/main/host.js
index f771f75..53445e0 100644
--- a/ambari-web/app/views/main/host.js
+++ b/ambari-web/app/views/main/host.js
@@ -293,7 +293,7 @@ App.MainHostView = App.TableView.extend(App.TableServerViewMixin, {
    */
   overlayObserver: function() {
     var $tbody = this.$('tbody'),
-      $overlay = this.$('.hosts-overlay'),
+      $overlay = this.$('.table-overlay'),
       $spinner = $($overlay).find('.spinner');
     if (!this.get('filteringComplete')) {
       if (!$tbody) return;

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/test/controllers/main/alert_definitions_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/alert_definitions_controller_test.js b/ambari-web/test/controllers/main/alert_definitions_controller_test.js
index 103ab8e..03a4d80 100644
--- a/ambari-web/test/controllers/main/alert_definitions_controller_test.js
+++ b/ambari-web/test/controllers/main/alert_definitions_controller_test.js
@@ -53,36 +53,4 @@ describe('App.MainAlertDefinitionsController', function() {
 
   });
 
-  describe('#showPopup', function () {
-
-    describe('#bodyClass', function () {
-
-      var bodyView;
-
-      beforeEach(function () {
-        controller.reopen({unhealthyAlertInstances: [
-          App.AlertInstance.createRecord({state: 'CRITICAL'}),
-          App.AlertInstance.createRecord({state: 'WARNING'}),
-          App.AlertInstance.createRecord({state: 'WARNING'}),
-          App.AlertInstance.createRecord({state: 'CRITICAL'})
-        ]});
-        bodyView = controller.showPopup().get('bodyClass').create();
-      });
-
-      it('#content', function () {
-        expect(bodyView.get('content.length')).to.equal(4);
-      });
-
-      it('#isLoaded', function () {
-        expect(bodyView.get('isLoaded')).to.be.true;
-      });
-
-      it('#isAlertEmptyList', function () {
-        expect(bodyView.get('isAlertEmptyList')).to.be.false;
-      });
-
-    });
-
-  });
-
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/72b40b7b/ambari-web/test/controllers/main/alerts/alert_instances_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/alerts/alert_instances_controller_test.js b/ambari-web/test/controllers/main/alerts/alert_instances_controller_test.js
index 8f24e5a..d5cda71 100644
--- a/ambari-web/test/controllers/main/alerts/alert_instances_controller_test.js
+++ b/ambari-web/test/controllers/main/alerts/alert_instances_controller_test.js
@@ -68,4 +68,37 @@ describe('App.MainAlertInstancesController', function () {
 
   });
 
+
+  describe('#showPopup', function () {
+
+    describe('#bodyClass', function () {
+
+      var bodyView;
+
+      beforeEach(function () {
+        controller.reopen({unhealthyAlertInstances: [
+          App.AlertInstance.createRecord({state: 'CRITICAL'}),
+          App.AlertInstance.createRecord({state: 'WARNING'}),
+          App.AlertInstance.createRecord({state: 'WARNING'}),
+          App.AlertInstance.createRecord({state: 'CRITICAL'})
+        ]});
+        bodyView = controller.showPopup().get('bodyClass').create();
+      });
+
+      it('#content', function () {
+        expect(bodyView.get('content.length')).to.equal(4);
+      });
+
+      it('#isLoaded', function () {
+        expect(bodyView.get('isLoaded')).to.be.true;
+      });
+
+      it('#isAlertEmptyList', function () {
+        expect(bodyView.get('isAlertEmptyList')).to.be.false;
+      });
+
+    });
+
+  });
+
 });
\ No newline at end of file


[4/4] ambari git commit: AMBARI-10077. Add UI unit tests for Create App Wizard controllers (onechiporenko)

Posted by on...@apache.org.
AMBARI-10077. Add UI unit tests for Create App Wizard controllers (onechiporenko)


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

Branch: refs/heads/trunk
Commit: ed616f1d544eb1311c734d5199e9d081e794eaf1
Parents: 72b40b7
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Mon Mar 16 11:10:53 2015 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Mon Mar 16 11:10:53 2015 +0200

----------------------------------------------------------------------
 .../ui/app/assets/javascripts/sinon-1.13.0.js   | 5830 ++++++++++++++++++
 .../app/assets/javascripts/sinon-qunit-1.0.0.js |   62 +
 .../src/main/resources/ui/app/assets/tests.html |    2 +
 .../ui/test/integration/pages/index_test.js     |    1 +
 .../integration/pages/slider_errors_test.js     |    1 +
 .../processes/create_new_app_test.js            |    2 +
 .../createAppWizard/step1_controller_test.js    |  243 +-
 .../createAppWizard/step2_controller_test.js    |  366 ++
 .../createAppWizard/step3_controller_test.js    |  399 +-
 .../createAppWizard/step4_controller_test.js    |  341 +-
 .../controllers/slider_app_controller_test.js   |   65 +-
 .../controllers/slider_apps_controller_test.js  |   18 +-
 .../unit/controllers/slider_controller_test.js  |   22 +-
 13 files changed, 7246 insertions(+), 106 deletions(-)
----------------------------------------------------------------------



[3/4] ambari git commit: AMBARI-10077. Add UI unit tests for Create App Wizard controllers (onechiporenko)

Posted by on...@apache.org.
http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-1.13.0.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-1.13.0.js b/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-1.13.0.js
new file mode 100644
index 0000000..31ab75e
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-1.13.0.js
@@ -0,0 +1,5830 @@
+/**
+ * Sinon.JS 1.13.0, 2015/03/05
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function (root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    define('sinon', [], function () {
+      return (root.sinon = factory());
+    });
+  } else if (typeof exports === 'object') {
+    module.exports = factory();
+  } else {
+    root.sinon = factory();
+  }
+}(this, function () {
+  var samsam, formatio;
+  (function () {
+                function define(mod, deps, fn) {
+                  if (mod == "samsam") {
+                    samsam = deps();
+                  } else if (typeof deps === "function" && mod.length === 0) {
+                    lolex = deps();
+                  } else if (typeof fn === "function") {
+                    formatio = fn(samsam);
+                  }
+                }
+    define.amd = {};
+((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) ||
+ (typeof module === "object" &&
+      function (m) { module.exports = m(); }) || // Node
+ function (m) { this.samsam = m(); } // Browser globals
+)(function () {
+    var o = Object.prototype;
+    var div = typeof document !== "undefined" && document.createElement("div");
+
+    function isNaN(value) {
+        // Unlike global isNaN, this avoids type coercion
+        // typeof check avoids IE host object issues, hat tip to
+        // lodash
+        var val = value; // JsLint thinks value !== value is "weird"
+        return typeof value === "number" && value !== val;
+    }
+
+    function getClass(value) {
+        // Returns the internal [[Class]] by calling Object.prototype.toString
+        // with the provided value as this. Return value is a string, naming the
+        // internal class, e.g. "Array"
+        return o.toString.call(value).split(/[ \]]/)[1];
+    }
+
+    /**
+     * @name samsam.isArguments
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is an ``arguments`` object,
+     * ``false`` otherwise.
+     */
+    function isArguments(object) {
+        if (getClass(object) === 'Arguments') { return true; }
+        if (typeof object !== "object" || typeof object.length !== "number" ||
+                getClass(object) === "Array") {
+            return false;
+        }
+        if (typeof object.callee == "function") { return true; }
+        try {
+            object[object.length] = 6;
+            delete object[object.length];
+        } catch (e) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.isElement
+     * @param Object object
+     *
+     * Returns ``true`` if ``object`` is a DOM element node. Unlike
+     * Underscore.js/lodash, this function will return ``false`` if ``object``
+     * is an *element-like* object, i.e. a regular object with a ``nodeType``
+     * property that holds the value ``1``.
+     */
+    function isElement(object) {
+        if (!object || object.nodeType !== 1 || !div) { return false; }
+        try {
+            object.appendChild(div);
+            object.removeChild(div);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @name samsam.keys
+     * @param Object object
+     *
+     * Return an array of own property names.
+     */
+    function keys(object) {
+        var ks = [], prop;
+        for (prop in object) {
+            if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); }
+        }
+        return ks;
+    }
+
+    /**
+     * @name samsam.isDate
+     * @param Object value
+     *
+     * Returns true if the object is a ``Date``, or *date-like*. Duck typing
+     * of date objects work by checking that the object has a ``getTime``
+     * function whose return value equals the return value from the object's
+     * ``valueOf``.
+     */
+    function isDate(value) {
+        return typeof value.getTime == "function" &&
+            value.getTime() == value.valueOf();
+    }
+
+    /**
+     * @name samsam.isNegZero
+     * @param Object value
+     *
+     * Returns ``true`` if ``value`` is ``-0``.
+     */
+    function isNegZero(value) {
+        return value === 0 && 1 / value === -Infinity;
+    }
+
+    /**
+     * @name samsam.equal
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Returns ``true`` if two objects are strictly equal. Compared to
+     * ``===`` there are two exceptions:
+     *
+     *   - NaN is considered equal to NaN
+     *   - -0 and +0 are not considered equal
+     */
+    function identical(obj1, obj2) {
+        if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) {
+            return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2);
+        }
+    }
+
+
+    /**
+     * @name samsam.deepEqual
+     * @param Object obj1
+     * @param Object obj2
+     *
+     * Deep equal comparison. Two values are "deep equal" if:
+     *
+     *   - They are equal, according to samsam.identical
+     *   - They are both date objects representing the same time
+     *   - They are both arrays containing elements that are all deepEqual
+     *   - They are objects with the same set of properties, and each property
+     *     in ``obj1`` is deepEqual to the corresponding property in ``obj2``
+     *
+     * Supports cyclic objects.
+     */
+    function deepEqualCyclic(obj1, obj2) {
+
+        // used for cyclic comparison
+        // contain already visited objects
+        var objects1 = [],
+            objects2 = [],
+        // contain pathes (position in the object structure)
+        // of the already visited objects
+        // indexes same as in objects arrays
+            paths1 = [],
+            paths2 = [],
+        // contains combinations of already compared objects
+        // in the manner: { "$1['ref']$2['ref']": true }
+            compared = {};
+
+        /**
+         * used to check, if the value of a property is an object
+         * (cyclic logic is only needed for objects)
+         * only needed for cyclic logic
+         */
+        function isObject(value) {
+
+            if (typeof value === 'object' && value !== null &&
+                    !(value instanceof Boolean) &&
+                    !(value instanceof Date)    &&
+                    !(value instanceof Number)  &&
+                    !(value instanceof RegExp)  &&
+                    !(value instanceof String)) {
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * returns the index of the given object in the
+         * given objects array, -1 if not contained
+         * only needed for cyclic logic
+         */
+        function getIndex(objects, obj) {
+
+            var i;
+            for (i = 0; i < objects.length; i++) {
+                if (objects[i] === obj) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        // does the recursion for the deep equal check
+        return (function deepEqual(obj1, obj2, path1, path2) {
+            var type1 = typeof obj1;
+            var type2 = typeof obj2;
+
+            // == null also matches undefined
+            if (obj1 === obj2 ||
+                    isNaN(obj1) || isNaN(obj2) ||
+                    obj1 == null || obj2 == null ||
+                    type1 !== "object" || type2 !== "object") {
+
+                return identical(obj1, obj2);
+            }
+
+            // Elements are only equal if identical(expected, actual)
+            if (isElement(obj1) || isElement(obj2)) { return false; }
+
+            var isDate1 = isDate(obj1), isDate2 = isDate(obj2);
+            if (isDate1 || isDate2) {
+                if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) {
+                    return false;
+                }
+            }
+
+            if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
+                if (obj1.toString() !== obj2.toString()) { return false; }
+            }
+
+            var class1 = getClass(obj1);
+            var class2 = getClass(obj2);
+            var keys1 = keys(obj1);
+            var keys2 = keys(obj2);
+
+            if (isArguments(obj1) || isArguments(obj2)) {
+                if (obj1.length !== obj2.length) { return false; }
+            } else {
+                if (type1 !== type2 || class1 !== class2 ||
+                        keys1.length !== keys2.length) {
+                    return false;
+                }
+            }
+
+            var key, i, l,
+                // following vars are used for the cyclic logic
+                value1, value2,
+                isObject1, isObject2,
+                index1, index2,
+                newPath1, newPath2;
+
+            for (i = 0, l = keys1.length; i < l; i++) {
+                key = keys1[i];
+                if (!o.hasOwnProperty.call(obj2, key)) {
+                    return false;
+                }
+
+                // Start of the cyclic logic
+
+                value1 = obj1[key];
+                value2 = obj2[key];
+
+                isObject1 = isObject(value1);
+                isObject2 = isObject(value2);
+
+                // determine, if the objects were already visited
+                // (it's faster to check for isObject first, than to
+                // get -1 from getIndex for non objects)
+                index1 = isObject1 ? getIndex(objects1, value1) : -1;
+                index2 = isObject2 ? getIndex(objects2, value2) : -1;
+
+                // determine the new pathes of the objects
+                // - for non cyclic objects the current path will be extended
+                //   by current property name
+                // - for cyclic objects the stored path is taken
+                newPath1 = index1 !== -1
+                    ? paths1[index1]
+                    : path1 + '[' + JSON.stringify(key) + ']';
+                newPath2 = index2 !== -1
+                    ? paths2[index2]
+                    : path2 + '[' + JSON.stringify(key) + ']';
+
+                // stop recursion if current objects are already compared
+                if (compared[newPath1 + newPath2]) {
+                    return true;
+                }
+
+                // remember the current objects and their pathes
+                if (index1 === -1 && isObject1) {
+                    objects1.push(value1);
+                    paths1.push(newPath1);
+                }
+                if (index2 === -1 && isObject2) {
+                    objects2.push(value2);
+                    paths2.push(newPath2);
+                }
+
+                // remember that the current objects are already compared
+                if (isObject1 && isObject2) {
+                    compared[newPath1 + newPath2] = true;
+                }
+
+                // End of cyclic logic
+
+                // neither value1 nor value2 is a cycle
+                // continue with next level
+                if (!deepEqual(value1, value2, newPath1, newPath2)) {
+                    return false;
+                }
+            }
+
+            return true;
+
+        }(obj1, obj2, '$1', '$2'));
+    }
+
+    var match;
+
+    function arrayContains(array, subset) {
+        if (subset.length === 0) { return true; }
+        var i, l, j, k;
+        for (i = 0, l = array.length; i < l; ++i) {
+            if (match(array[i], subset[0])) {
+                for (j = 0, k = subset.length; j < k; ++j) {
+                    if (!match(array[i + j], subset[j])) { return false; }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @name samsam.match
+     * @param Object object
+     * @param Object matcher
+     *
+     * Compare arbitrary value ``object`` with matcher.
+     */
+    match = function match(object, matcher) {
+        if (matcher && typeof matcher.test === "function") {
+            return matcher.test(object);
+        }
+
+        if (typeof matcher === "function") {
+            return matcher(object) === true;
+        }
+
+        if (typeof matcher === "string") {
+            matcher = matcher.toLowerCase();
+            var notNull = typeof object === "string" || !!object;
+            return notNull &&
+                (String(object)).toLowerCase().indexOf(matcher) >= 0;
+        }
+
+        if (typeof matcher === "number") {
+            return matcher === object;
+        }
+
+        if (typeof matcher === "boolean") {
+            return matcher === object;
+        }
+
+        if (typeof(matcher) === "undefined") {
+            return typeof(object) === "undefined";
+        }
+
+        if (matcher === null) {
+            return object === null;
+        }
+
+        if (getClass(object) === "Array" && getClass(matcher) === "Array") {
+            return arrayContains(object, matcher);
+        }
+
+        if (matcher && typeof matcher === "object") {
+            if (matcher === object) {
+                return true;
+            }
+            var prop;
+            for (prop in matcher) {
+                var value = object[prop];
+                if (typeof value === "undefined" &&
+                        typeof object.getAttribute === "function") {
+                    value = object.getAttribute(prop);
+                }
+                if (matcher[prop] === null || typeof matcher[prop] === 'undefined') {
+                    if (value !== matcher[prop]) {
+                        return false;
+                    }
+                } else if (typeof  value === "undefined" || !match(value, matcher[prop])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        throw new Error("Matcher was not a string, a number, a " +
+                        "function, a boolean or an object");
+    };
+
+    return {
+        isArguments: isArguments,
+        isElement: isElement,
+        isDate: isDate,
+        isNegZero: isNegZero,
+        identical: identical,
+        deepEqual: deepEqualCyclic,
+        match: match,
+        keys: keys
+    };
+});
+((typeof define === "function" && define.amd && function (m) {
+    define("formatio", ["samsam"], m);
+}) || (typeof module === "object" && function (m) {
+    module.exports = m(require("samsam"));
+}) || function (m) { this.formatio = m(this.samsam); }
+)(function (samsam) {
+    
+    var formatio = {
+        excludeConstructors: ["Object", /^.$/],
+        quoteStrings: true,
+        limitChildrenCount: 0
+    };
+
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    var specialObjects = [];
+    if (typeof global !== "undefined") {
+        specialObjects.push({ object: global, value: "[object global]" });
+    }
+    if (typeof document !== "undefined") {
+        specialObjects.push({
+            object: document,
+            value: "[object HTMLDocument]"
+        });
+    }
+    if (typeof window !== "undefined") {
+        specialObjects.push({ object: window, value: "[object Window]" });
+    }
+
+    function functionName(func) {
+        if (!func) { return ""; }
+        if (func.displayName) { return func.displayName; }
+        if (func.name) { return func.name; }
+        var matches = func.toString().match(/function\s+([^\(]+)/m);
+        return (matches && matches[1]) || "";
+    }
+
+    function constructorName(f, object) {
+        var name = functionName(object && object.constructor);
+        var excludes = f.excludeConstructors ||
+                formatio.excludeConstructors || [];
+
+        var i, l;
+        for (i = 0, l = excludes.length; i < l; ++i) {
+            if (typeof excludes[i] === "string" && excludes[i] === name) {
+                return "";
+            } else if (excludes[i].test && excludes[i].test(name)) {
+                return "";
+            }
+        }
+
+        return name;
+    }
+
+    function isCircular(object, objects) {
+        if (typeof object !== "object") { return false; }
+        var i, l;
+        for (i = 0, l = objects.length; i < l; ++i) {
+            if (objects[i] === object) { return true; }
+        }
+        return false;
+    }
+
+    function ascii(f, object, processed, indent) {
+        if (typeof object === "string") {
+            var qs = f.quoteStrings;
+            var quote = typeof qs !== "boolean" || qs;
+            return processed || quote ? '"' + object + '"' : object;
+        }
+
+        if (typeof object === "function" && !(object instanceof RegExp)) {
+            return ascii.func(object);
+        }
+
+        processed = processed || [];
+
+        if (isCircular(object, processed)) { return "[Circular]"; }
+
+        if (Object.prototype.toString.call(object) === "[object Array]") {
+            return ascii.array.call(f, object, processed);
+        }
+
+        if (!object) { return String((1/object) === -Infinity ? "-0" : object); }
+        if (samsam.isElement(object)) { return ascii.element(object); }
+
+        if (typeof object.toString === "function" &&
+                object.toString !== Object.prototype.toString) {
+            return object.toString();
+        }
+
+        var i, l;
+        for (i = 0, l = specialObjects.length; i < l; i++) {
+            if (object === specialObjects[i].object) {
+                return specialObjects[i].value;
+            }
+        }
+
+        return ascii.object.call(f, object, processed, indent);
+    }
+
+    ascii.func = function (func) {
+        return "function " + functionName(func) + "() {}";
+    };
+
+    ascii.array = function (array, processed) {
+        processed = processed || [];
+        processed.push(array);
+        var pieces = [];
+        var i, l;
+        l = (this.limitChildrenCount > 0) ? 
+            Math.min(this.limitChildrenCount, array.length) : array.length;
+
+        for (i = 0; i < l; ++i) {
+            pieces.push(ascii(this, array[i], processed));
+        }
+
+        if(l < array.length)
+            pieces.push("[... " + (array.length - l) + " more elements]");
+
+        return "[" + pieces.join(", ") + "]";
+    };
+
+    ascii.object = function (object, processed, indent) {
+        processed = processed || [];
+        processed.push(object);
+        indent = indent || 0;
+        var pieces = [], properties = samsam.keys(object).sort();
+        var length = 3;
+        var prop, str, obj, i, k, l;
+        l = (this.limitChildrenCount > 0) ? 
+            Math.min(this.limitChildrenCount, properties.length) : properties.length;
+
+        for (i = 0; i < l; ++i) {
+            prop = properties[i];
+            obj = object[prop];
+
+            if (isCircular(obj, processed)) {
+                str = "[Circular]";
+            } else {
+                str = ascii(this, obj, processed, indent + 2);
+            }
+
+            str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str;
+            length += str.length;
+            pieces.push(str);
+        }
+
+        var cons = constructorName(this, object);
+        var prefix = cons ? "[" + cons + "] " : "";
+        var is = "";
+        for (i = 0, k = indent; i < k; ++i) { is += " "; }
+
+        if(l < properties.length)
+            pieces.push("[... " + (properties.length - l) + " more elements]");
+
+        if (length + indent > 80) {
+            return prefix + "{\n  " + is + pieces.join(",\n  " + is) + "\n" +
+                is + "}";
+        }
+        return prefix + "{ " + pieces.join(", ") + " }";
+    };
+
+    ascii.element = function (element) {
+        var tagName = element.tagName.toLowerCase();
+        var attrs = element.attributes, attr, pairs = [], attrName, i, l, val;
+
+        for (i = 0, l = attrs.length; i < l; ++i) {
+            attr = attrs.item(i);
+            attrName = attr.nodeName.toLowerCase().replace("html:", "");
+            val = attr.nodeValue;
+            if (attrName !== "contenteditable" || val !== "inherit") {
+                if (!!val) { pairs.push(attrName + "=\"" + val + "\""); }
+            }
+        }
+
+        var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
+        var content = element.innerHTML;
+
+        if (content.length > 20) {
+            content = content.substr(0, 20) + "[...]";
+        }
+
+        var res = formatted + pairs.join(" ") + ">" + content +
+                "</" + tagName + ">";
+
+        return res.replace(/ contentEditable="inherit"/, "");
+    };
+
+    function Formatio(options) {
+        for (var opt in options) {
+            this[opt] = options[opt];
+        }
+    }
+
+    Formatio.prototype = {
+        functionName: functionName,
+
+        configure: function (options) {
+            return new Formatio(options);
+        },
+
+        constructorName: function (object) {
+            return constructorName(this, object);
+        },
+
+        ascii: function (object, processed, indent) {
+            return ascii(this, object, processed, indent);
+        }
+    };
+
+    return Formatio.prototype;
+});
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.lolex=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+(function (global){
+/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
+/*global global*/
+/**
+ * @author Christian Johansen (christian@cjohansen.no) and contributors
+ * @license BSD
+ *
+ * Copyright (c) 2010-2014 Christian Johansen
+ */
+
+// node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref()
+// browsers, a number.
+// see https://github.com/cjohansen/Sinon.JS/pull/436
+var timeoutResult = setTimeout(function() {}, 0);
+var addTimerReturnsObject = typeof timeoutResult === "object";
+clearTimeout(timeoutResult);
+
+var NativeDate = Date;
+var id = 1;
+
+/**
+ * Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into
+ * number of milliseconds. This is used to support human-readable strings passed
+ * to clock.tick()
+ */
+function parseTime(str) {
+    if (!str) {
+        return 0;
+    }
+
+    var strings = str.split(":");
+    var l = strings.length, i = l;
+    var ms = 0, parsed;
+
+    if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
+        throw new Error("tick only understands numbers and 'h:m:s'");
+    }
+
+    while (i--) {
+        parsed = parseInt(strings[i], 10);
+
+        if (parsed >= 60) {
+            throw new Error("Invalid time " + str);
+        }
+
+        ms += parsed * Math.pow(60, (l - i - 1));
+    }
+
+    return ms * 1000;
+}
+
+/**
+ * Used to grok the `now` parameter to createClock.
+ */
+function getEpoch(epoch) {
+    if (!epoch) { return 0; }
+    if (typeof epoch.getTime === "function") { return epoch.getTime(); }
+    if (typeof epoch === "number") { return epoch; }
+    throw new TypeError("now should be milliseconds since UNIX epoch");
+}
+
+function inRange(from, to, timer) {
+    return timer && timer.callAt >= from && timer.callAt <= to;
+}
+
+function mirrorDateProperties(target, source) {
+    if (source.now) {
+        target.now = function now() {
+            return target.clock.now;
+        };
+    } else {
+        delete target.now;
+    }
+
+    if (source.toSource) {
+        target.toSource = function toSource() {
+            return source.toSource();
+        };
+    } else {
+        delete target.toSource;
+    }
+
+    target.toString = function toString() {
+        return source.toString();
+    };
+
+    target.prototype = source.prototype;
+    target.parse = source.parse;
+    target.UTC = source.UTC;
+    target.prototype.toUTCString = source.prototype.toUTCString;
+
+    for (var prop in source) {
+        if (source.hasOwnProperty(prop)) {
+            target[prop] = source[prop];
+        }
+    }
+
+    return target;
+}
+
+function createDate() {
+    function ClockDate(year, month, date, hour, minute, second, ms) {
+        // Defensive and verbose to avoid potential harm in passing
+        // explicit undefined when user does not pass argument
+        switch (arguments.length) {
+        case 0:
+            return new NativeDate(ClockDate.clock.now);
+        case 1:
+            return new NativeDate(year);
+        case 2:
+            return new NativeDate(year, month);
+        case 3:
+            return new NativeDate(year, month, date);
+        case 4:
+            return new NativeDate(year, month, date, hour);
+        case 5:
+            return new NativeDate(year, month, date, hour, minute);
+        case 6:
+            return new NativeDate(year, month, date, hour, minute, second);
+        default:
+            return new NativeDate(year, month, date, hour, minute, second, ms);
+        }
+    }
+
+    return mirrorDateProperties(ClockDate, NativeDate);
+}
+
+function addTimer(clock, timer) {
+    if (typeof timer.func === "undefined") {
+        throw new Error("Callback must be provided to timer calls");
+    }
+
+    if (!clock.timers) {
+        clock.timers = {};
+    }
+
+    timer.id = id++;
+    timer.createdAt = clock.now;
+    timer.callAt = clock.now + (timer.delay || 0);
+
+    clock.timers[timer.id] = timer;
+
+    if (addTimerReturnsObject) {
+        return {
+            id: timer.id,
+            ref: function() {},
+            unref: function() {}
+        };
+    }
+    else {
+        return timer.id;
+    }
+}
+
+function firstTimerInRange(clock, from, to) {
+    var timers = clock.timers, timer = null;
+
+    for (var id in timers) {
+        if (!inRange(from, to, timers[id])) {
+            continue;
+        }
+
+        if (!timer || ~compareTimers(timer, timers[id])) {
+            timer = timers[id];
+        }
+    }
+
+    return timer;
+}
+
+function compareTimers(a, b) {
+    // Sort first by absolute timing
+    if (a.callAt < b.callAt) {
+        return -1;
+    }
+    if (a.callAt > b.callAt) {
+        return 1;
+    }
+
+    // Sort next by immediate, immediate timers take precedence
+    if (a.immediate && !b.immediate) {
+        return -1;
+    }
+    if (!a.immediate && b.immediate) {
+        return 1;
+    }
+
+    // Sort next by creation time, earlier-created timers take precedence
+    if (a.createdAt < b.createdAt) {
+        return -1;
+    }
+    if (a.createdAt > b.createdAt) {
+        return 1;
+    }
+
+    // Sort next by id, lower-id timers take precedence
+    if (a.id < b.id) {
+        return -1;
+    }
+    if (a.id > b.id) {
+        return 1;
+    }
+
+    // As timer ids are unique, no fallback `0` is necessary
+}
+
+function callTimer(clock, timer) {
+    if (typeof timer.interval == "number") {
+        clock.timers[timer.id].callAt += timer.interval;
+    } else {
+        delete clock.timers[timer.id];
+    }
+
+    try {
+        if (typeof timer.func == "function") {
+            timer.func.apply(null, timer.args);
+        } else {
+            eval(timer.func);
+        }
+    } catch (e) {
+        var exception = e;
+    }
+
+    if (!clock.timers[timer.id]) {
+        if (exception) {
+            throw exception;
+        }
+        return;
+    }
+
+    if (exception) {
+        throw exception;
+    }
+}
+
+function uninstall(clock, target) {
+    var method;
+
+    for (var i = 0, l = clock.methods.length; i < l; i++) {
+        method = clock.methods[i];
+
+        if (target[method].hadOwnProperty) {
+            target[method] = clock["_" + method];
+        } else {
+            try {
+                delete target[method];
+            } catch (e) {}
+        }
+    }
+
+    // Prevent multiple executions which will completely remove these props
+    clock.methods = [];
+}
+
+function hijackMethod(target, method, clock) {
+    clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(target, method);
+    clock["_" + method] = target[method];
+
+    if (method == "Date") {
+        var date = mirrorDateProperties(clock[method], target[method]);
+        target[method] = date;
+    } else {
+        target[method] = function () {
+            return clock[method].apply(clock, arguments);
+        };
+
+        for (var prop in clock[method]) {
+            if (clock[method].hasOwnProperty(prop)) {
+                target[method][prop] = clock[method][prop];
+            }
+        }
+    }
+
+    target[method].clock = clock;
+}
+
+var timers = {
+    setTimeout: setTimeout,
+    clearTimeout: clearTimeout,
+    setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
+    clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
+    setInterval: setInterval,
+    clearInterval: clearInterval,
+    Date: Date
+};
+
+var keys = Object.keys || function (obj) {
+    var ks = [];
+    for (var key in obj) {
+        ks.push(key);
+    }
+    return ks;
+};
+
+exports.timers = timers;
+
+var createClock = exports.createClock = function (now) {
+    var clock = {
+        now: getEpoch(now),
+        timeouts: {},
+        Date: createDate()
+    };
+
+    clock.Date.clock = clock;
+
+    clock.setTimeout = function setTimeout(func, timeout) {
+        return addTimer(clock, {
+            func: func,
+            args: Array.prototype.slice.call(arguments, 2),
+            delay: timeout
+        });
+    };
+
+    clock.clearTimeout = function clearTimeout(timerId) {
+        if (!timerId) {
+            // null appears to be allowed in most browsers, and appears to be
+            // relied upon by some libraries, like Bootstrap carousel
+            return;
+        }
+        if (!clock.timers) {
+            clock.timers = [];
+        }
+        // in Node, timerId is an object with .ref()/.unref(), and
+        // its .id field is the actual timer id.
+        if (typeof timerId === "object") {
+            timerId = timerId.id
+        }
+        if (timerId in clock.timers) {
+            delete clock.timers[timerId];
+        }
+    };
+
+    clock.setInterval = function setInterval(func, timeout) {
+        return addTimer(clock, {
+            func: func,
+            args: Array.prototype.slice.call(arguments, 2),
+            delay: timeout,
+            interval: timeout
+        });
+    };
+
+    clock.clearInterval = function clearInterval(timerId) {
+        clock.clearTimeout(timerId);
+    };
+
+    clock.setImmediate = function setImmediate(func) {
+        return addTimer(clock, {
+            func: func,
+            args: Array.prototype.slice.call(arguments, 1),
+            immediate: true
+        });
+    };
+
+    clock.clearImmediate = function clearImmediate(timerId) {
+        clock.clearTimeout(timerId);
+    };
+
+    clock.tick = function tick(ms) {
+        ms = typeof ms == "number" ? ms : parseTime(ms);
+        var tickFrom = clock.now, tickTo = clock.now + ms, previous = clock.now;
+        var timer = firstTimerInRange(clock, tickFrom, tickTo);
+
+        var firstException;
+        while (timer && tickFrom <= tickTo) {
+            if (clock.timers[timer.id]) {
+                tickFrom = clock.now = timer.callAt;
+                try {
+                    callTimer(clock, timer);
+                } catch (e) {
+                    firstException = firstException || e;
+                }
+            }
+
+            timer = firstTimerInRange(clock, previous, tickTo);
+            previous = tickFrom;
+        }
+
+        clock.now = tickTo;
+
+        if (firstException) {
+            throw firstException;
+        }
+
+        return clock.now;
+    };
+
+    clock.reset = function reset() {
+        clock.timers = {};
+    };
+
+    return clock;
+};
+
+exports.install = function install(target, now, toFake) {
+    if (typeof target === "number") {
+        toFake = now;
+        now = target;
+        target = null;
+    }
+
+    if (!target) {
+        target = global;
+    }
+
+    var clock = createClock(now);
+
+    clock.uninstall = function () {
+        uninstall(clock, target);
+    };
+
+    clock.methods = toFake || [];
+
+    if (clock.methods.length === 0) {
+        clock.methods = keys(timers);
+    }
+
+    for (var i = 0, l = clock.methods.length; i < l; i++) {
+        hijackMethod(target, clock.methods[i], clock);
+    }
+
+    return clock;
+};
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}]},{},[1])(1)
+});
+  })();
+  var define;
+/**
+ * Sinon core utilities. For internal use only.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+var sinon = (function () {
+"use strict";
+
+    var sinon;
+    var isNode = typeof module !== "undefined" && module.exports && typeof require === "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    function loadDependencies(require, exports, module) {
+        sinon = module.exports = require("./sinon/util/core");
+        require("./sinon/extend");
+        require("./sinon/typeOf");
+        require("./sinon/times_in_words");
+        require("./sinon/spy");
+        require("./sinon/call");
+        require("./sinon/behavior");
+        require("./sinon/stub");
+        require("./sinon/mock");
+        require("./sinon/collection");
+        require("./sinon/assert");
+        require("./sinon/sandbox");
+        require("./sinon/test");
+        require("./sinon/test_case");
+        require("./sinon/match");
+        require("./sinon/format");
+        require("./sinon/log_error");
+    }
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+        sinon = module.exports;
+    } else {
+        sinon = {};
+    }
+
+    return sinon;
+}());
+
+/**
+ * @depend ../../sinon.js
+ */
+/**
+ * Sinon core utilities. For internal use only.
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var div = typeof document != "undefined" && document.createElement("div");
+    var hasOwn = Object.prototype.hasOwnProperty;
+
+    function isDOMNode(obj) {
+        var success = false;
+
+        try {
+            obj.appendChild(div);
+            success = div.parentNode == obj;
+        } catch (e) {
+            return false;
+        } finally {
+            try {
+                obj.removeChild(div);
+            } catch (e) {
+                // Remove failed, not much we can do about that
+            }
+        }
+
+        return success;
+    }
+
+    function isElement(obj) {
+        return div && obj && obj.nodeType === 1 && isDOMNode(obj);
+    }
+
+    function isFunction(obj) {
+        return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
+    }
+
+    function isReallyNaN(val) {
+        return typeof val === "number" && isNaN(val);
+    }
+
+    function mirrorProperties(target, source) {
+        for (var prop in source) {
+            if (!hasOwn.call(target, prop)) {
+                target[prop] = source[prop];
+            }
+        }
+    }
+
+    function isRestorable(obj) {
+        return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
+    }
+
+    function makeApi(sinon) {
+        sinon.wrapMethod = function wrapMethod(object, property, method) {
+            if (!object) {
+                throw new TypeError("Should wrap property of object");
+            }
+
+            if (typeof method != "function") {
+                throw new TypeError("Method wrapper should be function");
+            }
+
+            var wrappedMethod = object[property],
+                error;
+
+            if (!isFunction(wrappedMethod)) {
+                error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
+                                    property + " as function");
+            } else if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
+                error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
+            } else if (wrappedMethod.calledBefore) {
+                var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
+                error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
+            }
+
+            if (error) {
+                if (wrappedMethod && wrappedMethod.stackTrace) {
+                    error.stack += "\n--------------\n" + wrappedMethod.stackTrace;
+                }
+                throw error;
+            }
+
+            // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
+            // when using hasOwn.call on objects from other frames.
+            var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
+            object[property] = method;
+            method.displayName = property;
+            // Set up a stack trace which can be used later to find what line of
+            // code the original method was created on.
+            method.stackTrace = (new Error("Stack Trace for original")).stack;
+
+            method.restore = function () {
+                // For prototype properties try to reset by delete first.
+                // If this fails (ex: localStorage on mobile safari) then force a reset
+                // via direct assignment.
+                if (!owned) {
+                    delete object[property];
+                }
+                if (object[property] === method) {
+                    object[property] = wrappedMethod;
+                }
+            };
+
+            method.restore.sinon = true;
+            mirrorProperties(method, wrappedMethod);
+
+            return method;
+        };
+
+        sinon.create = function create(proto) {
+            var F = function () {};
+            F.prototype = proto;
+            return new F();
+        };
+
+        sinon.deepEqual = function deepEqual(a, b) {
+            if (sinon.match && sinon.match.isMatcher(a)) {
+                return a.test(b);
+            }
+
+            if (typeof a != "object" || typeof b != "object") {
+                if (isReallyNaN(a) && isReallyNaN(b)) {
+                    return true;
+                } else {
+                    return a === b;
+                }
+            }
+
+            if (isElement(a) || isElement(b)) {
+                return a === b;
+            }
+
+            if (a === b) {
+                return true;
+            }
+
+            if ((a === null && b !== null) || (a !== null && b === null)) {
+                return false;
+            }
+
+            if (a instanceof RegExp && b instanceof RegExp) {
+                return (a.source === b.source) && (a.global === b.global) &&
+                    (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
+            }
+
+            var aString = Object.prototype.toString.call(a);
+            if (aString != Object.prototype.toString.call(b)) {
+                return false;
+            }
+
+            if (aString == "[object Date]") {
+                return a.valueOf() === b.valueOf();
+            }
+
+            var prop, aLength = 0, bLength = 0;
+
+            if (aString == "[object Array]" && a.length !== b.length) {
+                return false;
+            }
+
+            for (prop in a) {
+                aLength += 1;
+
+                if (!(prop in b)) {
+                    return false;
+                }
+
+                if (!deepEqual(a[prop], b[prop])) {
+                    return false;
+                }
+            }
+
+            for (prop in b) {
+                bLength += 1;
+            }
+
+            return aLength == bLength;
+        };
+
+        sinon.functionName = function functionName(func) {
+            var name = func.displayName || func.name;
+
+            // Use function decomposition as a last resort to get function
+            // name. Does not rely on function decomposition to work - if it
+            // doesn't debugging will be slightly less informative
+            // (i.e. toString will say 'spy' rather than 'myFunc').
+            if (!name) {
+                var matches = func.toString().match(/function ([^\s\(]+)/);
+                name = matches && matches[1];
+            }
+
+            return name;
+        };
+
+        sinon.functionToString = function toString() {
+            if (this.getCall && this.callCount) {
+                var thisValue, prop, i = this.callCount;
+
+                while (i--) {
+                    thisValue = this.getCall(i).thisValue;
+
+                    for (prop in thisValue) {
+                        if (thisValue[prop] === this) {
+                            return prop;
+                        }
+                    }
+                }
+            }
+
+            return this.displayName || "sinon fake";
+        };
+
+        sinon.getConfig = function (custom) {
+            var config = {};
+            custom = custom || {};
+            var defaults = sinon.defaultConfig;
+
+            for (var prop in defaults) {
+                if (defaults.hasOwnProperty(prop)) {
+                    config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
+                }
+            }
+
+            return config;
+        };
+
+        sinon.defaultConfig = {
+            injectIntoThis: true,
+            injectInto: null,
+            properties: ["spy", "stub", "mock", "clock", "server", "requests"],
+            useFakeTimers: true,
+            useFakeServer: true
+        };
+
+        sinon.timesInWords = function timesInWords(count) {
+            return count == 1 && "once" ||
+                count == 2 && "twice" ||
+                count == 3 && "thrice" ||
+                (count || 0) + " times";
+        };
+
+        sinon.calledInOrder = function (spies) {
+            for (var i = 1, l = spies.length; i < l; i++) {
+                if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
+                    return false;
+                }
+            }
+
+            return true;
+        };
+
+        sinon.orderByFirstCall = function (spies) {
+            return spies.sort(function (a, b) {
+                // uuid, won't ever be equal
+                var aCall = a.getCall(0);
+                var bCall = b.getCall(0);
+                var aId = aCall && aCall.callId || -1;
+                var bId = bCall && bCall.callId || -1;
+
+                return aId < bId ? -1 : 1;
+            });
+        };
+
+        sinon.createStubInstance = function (constructor) {
+            if (typeof constructor !== "function") {
+                throw new TypeError("The constructor should be a function.");
+            }
+            return sinon.stub(sinon.create(constructor.prototype));
+        };
+
+        sinon.restore = function (object) {
+            if (object !== null && typeof object === "object") {
+                for (var prop in object) {
+                    if (isRestorable(object[prop])) {
+                        object[prop].restore();
+                    }
+                }
+            } else if (isRestorable(object)) {
+                object.restore();
+            }
+        };
+
+        return sinon;
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    function loadDependencies(require, exports) {
+        makeApi(exports);
+    }
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend util/core.js
+ */
+
+(function (sinon) {
+    function makeApi(sinon) {
+
+        // Adapted from https://developer.mozilla.org/en/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
+        var hasDontEnumBug = (function () {
+            var obj = {
+                constructor: function () {
+                    return "0";
+                },
+                toString: function () {
+                    return "1";
+                },
+                valueOf: function () {
+                    return "2";
+                },
+                toLocaleString: function () {
+                    return "3";
+                },
+                prototype: function () {
+                    return "4";
+                },
+                isPrototypeOf: function () {
+                    return "5";
+                },
+                propertyIsEnumerable: function () {
+                    return "6";
+                },
+                hasOwnProperty: function () {
+                    return "7";
+                },
+                length: function () {
+                    return "8";
+                },
+                unique: function () {
+                    return "9"
+                }
+            };
+
+            var result = [];
+            for (var prop in obj) {
+                result.push(obj[prop]());
+            }
+            return result.join("") !== "0123456789";
+        })();
+
+        /* Public: Extend target in place with all (own) properties from sources in-order. Thus, last source will
+         *         override properties in previous sources.
+         *
+         * target - The Object to extend
+         * sources - Objects to copy properties from.
+         *
+         * Returns the extended target
+         */
+        function extend(target /*, sources */) {
+            var sources = Array.prototype.slice.call(arguments, 1),
+                source, i, prop;
+
+            for (i = 0; i < sources.length; i++) {
+                source = sources[i];
+
+                for (prop in source) {
+                    if (source.hasOwnProperty(prop)) {
+                        target[prop] = source[prop];
+                    }
+                }
+
+                // Make sure we copy (own) toString method even when in JScript with DontEnum bug
+                // See https://developer.mozilla.org/en/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug
+                if (hasDontEnumBug && source.hasOwnProperty("toString") && source.toString !== target.toString) {
+                    target.toString = source.toString;
+                }
+            }
+
+            return target;
+        };
+
+        sinon.extend = extend;
+        return sinon.extend;
+    }
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        module.exports = makeApi(sinon);
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend util/core.js
+ */
+
+(function (sinon) {
+    function makeApi(sinon) {
+
+        function timesInWords(count) {
+            switch (count) {
+                case 1:
+                    return "once";
+                case 2:
+                    return "twice";
+                case 3:
+                    return "thrice";
+                default:
+                    return (count || 0) + " times";
+            }
+        }
+
+        sinon.timesInWords = timesInWords;
+        return sinon.timesInWords;
+    }
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        module.exports = makeApi(sinon);
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend util/core.js
+ */
+/**
+ * Format functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2014 Christian Johansen
+ */
+
+(function (sinon, formatio) {
+    function makeApi(sinon) {
+        function typeOf(value) {
+            if (value === null) {
+                return "null";
+            } else if (value === undefined) {
+                return "undefined";
+            }
+            var string = Object.prototype.toString.call(value);
+            return string.substring(8, string.length - 1).toLowerCase();
+        };
+
+        sinon.typeOf = typeOf;
+        return sinon.typeOf;
+    }
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        module.exports = makeApi(sinon);
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(
+    (typeof sinon == "object" && sinon || null),
+    (typeof formatio == "object" && formatio)
+));
+
+/**
+ * @depend util/core.js
+ * @depend typeOf.js
+ */
+/*jslint eqeqeq: false, onevar: false, plusplus: false*/
+/*global module, require, sinon*/
+/**
+ * Match functions
+ *
+ * @author Maximilian Antoni (mail@maxantoni.de)
+ * @license BSD
+ *
+ * Copyright (c) 2012 Maximilian Antoni
+ */
+
+(function (sinon) {
+    function makeApi(sinon) {
+        function assertType(value, type, name) {
+            var actual = sinon.typeOf(value);
+            if (actual !== type) {
+                throw new TypeError("Expected type of " + name + " to be " +
+                    type + ", but was " + actual);
+            }
+        }
+
+        var matcher = {
+            toString: function () {
+                return this.message;
+            }
+        };
+
+        function isMatcher(object) {
+            return matcher.isPrototypeOf(object);
+        }
+
+        function matchObject(expectation, actual) {
+            if (actual === null || actual === undefined) {
+                return false;
+            }
+            for (var key in expectation) {
+                if (expectation.hasOwnProperty(key)) {
+                    var exp = expectation[key];
+                    var act = actual[key];
+                    if (match.isMatcher(exp)) {
+                        if (!exp.test(act)) {
+                            return false;
+                        }
+                    } else if (sinon.typeOf(exp) === "object") {
+                        if (!matchObject(exp, act)) {
+                            return false;
+                        }
+                    } else if (!sinon.deepEqual(exp, act)) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        matcher.or = function (m2) {
+            if (!arguments.length) {
+                throw new TypeError("Matcher expected");
+            } else if (!isMatcher(m2)) {
+                m2 = match(m2);
+            }
+            var m1 = this;
+            var or = sinon.create(matcher);
+            or.test = function (actual) {
+                return m1.test(actual) || m2.test(actual);
+            };
+            or.message = m1.message + ".or(" + m2.message + ")";
+            return or;
+        };
+
+        matcher.and = function (m2) {
+            if (!arguments.length) {
+                throw new TypeError("Matcher expected");
+            } else if (!isMatcher(m2)) {
+                m2 = match(m2);
+            }
+            var m1 = this;
+            var and = sinon.create(matcher);
+            and.test = function (actual) {
+                return m1.test(actual) && m2.test(actual);
+            };
+            and.message = m1.message + ".and(" + m2.message + ")";
+            return and;
+        };
+
+        var match = function (expectation, message) {
+            var m = sinon.create(matcher);
+            var type = sinon.typeOf(expectation);
+            switch (type) {
+            case "object":
+                if (typeof expectation.test === "function") {
+                    m.test = function (actual) {
+                        return expectation.test(actual) === true;
+                    };
+                    m.message = "match(" + sinon.functionName(expectation.test) + ")";
+                    return m;
+                }
+                var str = [];
+                for (var key in expectation) {
+                    if (expectation.hasOwnProperty(key)) {
+                        str.push(key + ": " + expectation[key]);
+                    }
+                }
+                m.test = function (actual) {
+                    return matchObject(expectation, actual);
+                };
+                m.message = "match(" + str.join(", ") + ")";
+                break;
+            case "number":
+                m.test = function (actual) {
+                    return expectation == actual;
+                };
+                break;
+            case "string":
+                m.test = function (actual) {
+                    if (typeof actual !== "string") {
+                        return false;
+                    }
+                    return actual.indexOf(expectation) !== -1;
+                };
+                m.message = "match(\"" + expectation + "\")";
+                break;
+            case "regexp":
+                m.test = function (actual) {
+                    if (typeof actual !== "string") {
+                        return false;
+                    }
+                    return expectation.test(actual);
+                };
+                break;
+            case "function":
+                m.test = expectation;
+                if (message) {
+                    m.message = message;
+                } else {
+                    m.message = "match(" + sinon.functionName(expectation) + ")";
+                }
+                break;
+            default:
+                m.test = function (actual) {
+                    return sinon.deepEqual(expectation, actual);
+                };
+            }
+            if (!m.message) {
+                m.message = "match(" + expectation + ")";
+            }
+            return m;
+        };
+
+        match.isMatcher = isMatcher;
+
+        match.any = match(function () {
+            return true;
+        }, "any");
+
+        match.defined = match(function (actual) {
+            return actual !== null && actual !== undefined;
+        }, "defined");
+
+        match.truthy = match(function (actual) {
+            return !!actual;
+        }, "truthy");
+
+        match.falsy = match(function (actual) {
+            return !actual;
+        }, "falsy");
+
+        match.same = function (expectation) {
+            return match(function (actual) {
+                return expectation === actual;
+            }, "same(" + expectation + ")");
+        };
+
+        match.typeOf = function (type) {
+            assertType(type, "string", "type");
+            return match(function (actual) {
+                return sinon.typeOf(actual) === type;
+            }, "typeOf(\"" + type + "\")");
+        };
+
+        match.instanceOf = function (type) {
+            assertType(type, "function", "type");
+            return match(function (actual) {
+                return actual instanceof type;
+            }, "instanceOf(" + sinon.functionName(type) + ")");
+        };
+
+        function createPropertyMatcher(propertyTest, messagePrefix) {
+            return function (property, value) {
+                assertType(property, "string", "property");
+                var onlyProperty = arguments.length === 1;
+                var message = messagePrefix + "(\"" + property + "\"";
+                if (!onlyProperty) {
+                    message += ", " + value;
+                }
+                message += ")";
+                return match(function (actual) {
+                    if (actual === undefined || actual === null ||
+                            !propertyTest(actual, property)) {
+                        return false;
+                    }
+                    return onlyProperty || sinon.deepEqual(value, actual[property]);
+                }, message);
+            };
+        }
+
+        match.has = createPropertyMatcher(function (actual, property) {
+            if (typeof actual === "object") {
+                return property in actual;
+            }
+            return actual[property] !== undefined;
+        }, "has");
+
+        match.hasOwn = createPropertyMatcher(function (actual, property) {
+            return actual.hasOwnProperty(property);
+        }, "hasOwn");
+
+        match.bool = match.typeOf("boolean");
+        match.number = match.typeOf("number");
+        match.string = match.typeOf("string");
+        match.object = match.typeOf("object");
+        match.func = match.typeOf("function");
+        match.array = match.typeOf("array");
+        match.regexp = match.typeOf("regexp");
+        match.date = match.typeOf("date");
+
+        sinon.match = match;
+        return match;
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        require("./typeOf");
+        module.exports = makeApi(sinon);
+    }
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend util/core.js
+ */
+/**
+ * Format functions
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2014 Christian Johansen
+ */
+
+(function (sinon, formatio) {
+    function makeApi(sinon) {
+        function valueFormatter(value) {
+            return "" + value;
+        }
+
+        function getFormatioFormatter() {
+            var formatter = formatio.configure({
+                    quoteStrings: false,
+                    limitChildrenCount: 250
+                });
+
+            function format() {
+                return formatter.ascii.apply(formatter, arguments);
+            };
+
+            return format;
+        }
+
+        function getNodeFormatter(value) {
+            function format(value) {
+                return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
+            };
+
+            try {
+                var util = require("util");
+            } catch (e) {
+                /* Node, but no util module - would be very old, but better safe than sorry */
+            }
+
+            return util ? format : valueFormatter;
+        }
+
+        var isNode = typeof module !== "undefined" && module.exports && typeof require == "function",
+            formatter;
+
+        if (isNode) {
+            try {
+                formatio = require("formatio");
+            } catch (e) {}
+        }
+
+        if (formatio) {
+            formatter = getFormatioFormatter()
+        } else if (isNode) {
+            formatter = getNodeFormatter();
+        } else {
+            formatter = valueFormatter;
+        }
+
+        sinon.format = formatter;
+        return sinon.format;
+    }
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        module.exports = makeApi(sinon);
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(
+    (typeof sinon == "object" && sinon || null),
+    (typeof formatio == "object" && formatio)
+));
+
+/**
+  * @depend util/core.js
+  * @depend match.js
+  * @depend format.js
+  */
+/**
+  * Spy calls
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @author Maximilian Antoni (mail@maxantoni.de)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  * Copyright (c) 2013 Maximilian Antoni
+  */
+
+(function (sinon) {
+    function makeApi(sinon) {
+        function throwYieldError(proxy, text, args) {
+            var msg = sinon.functionName(proxy) + text;
+            if (args.length) {
+                msg += " Received [" + slice.call(args).join(", ") + "]";
+            }
+            throw new Error(msg);
+        }
+
+        var slice = Array.prototype.slice;
+
+        var callProto = {
+            calledOn: function calledOn(thisValue) {
+                if (sinon.match && sinon.match.isMatcher(thisValue)) {
+                    return thisValue.test(this.thisValue);
+                }
+                return this.thisValue === thisValue;
+            },
+
+            calledWith: function calledWith() {
+                var l = arguments.length;
+                if (l > this.args.length) {
+                    return false;
+                }
+                for (var i = 0; i < l; i += 1) {
+                    if (!sinon.deepEqual(arguments[i], this.args[i])) {
+                        return false;
+                    }
+                }
+
+                return true;
+            },
+
+            calledWithMatch: function calledWithMatch() {
+                var l = arguments.length;
+                if (l > this.args.length) {
+                    return false;
+                }
+                for (var i = 0; i < l; i += 1) {
+                    var actual = this.args[i];
+                    var expectation = arguments[i];
+                    if (!sinon.match || !sinon.match(expectation).test(actual)) {
+                        return false;
+                    }
+                }
+                return true;
+            },
+
+            calledWithExactly: function calledWithExactly() {
+                return arguments.length == this.args.length &&
+                    this.calledWith.apply(this, arguments);
+            },
+
+            notCalledWith: function notCalledWith() {
+                return !this.calledWith.apply(this, arguments);
+            },
+
+            notCalledWithMatch: function notCalledWithMatch() {
+                return !this.calledWithMatch.apply(this, arguments);
+            },
+
+            returned: function returned(value) {
+                return sinon.deepEqual(value, this.returnValue);
+            },
+
+            threw: function threw(error) {
+                if (typeof error === "undefined" || !this.exception) {
+                    return !!this.exception;
+                }
+
+                return this.exception === error || this.exception.name === error;
+            },
+
+            calledWithNew: function calledWithNew() {
+                return this.proxy.prototype && this.thisValue instanceof this.proxy;
+            },
+
+            calledBefore: function (other) {
+                return this.callId < other.callId;
+            },
+
+            calledAfter: function (other) {
+                return this.callId > other.callId;
+            },
+
+            callArg: function (pos) {
+                this.args[pos]();
+            },
+
+            callArgOn: function (pos, thisValue) {
+                this.args[pos].apply(thisValue);
+            },
+
+            callArgWith: function (pos) {
+                this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1)));
+            },
+
+            callArgOnWith: function (pos, thisValue) {
+                var args = slice.call(arguments, 2);
+                this.args[pos].apply(thisValue, args);
+            },
+
+            yield: function () {
+                this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0)));
+            },
+
+            yieldOn: function (thisValue) {
+                var args = this.args;
+                for (var i = 0, l = args.length; i < l; ++i) {
+                    if (typeof args[i] === "function") {
+                        args[i].apply(thisValue, slice.call(arguments, 1));
+                        return;
+                    }
+                }
+                throwYieldError(this.proxy, " cannot yield since no callback was passed.", args);
+            },
+
+            yieldTo: function (prop) {
+                this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1)));
+            },
+
+            yieldToOn: function (prop, thisValue) {
+                var args = this.args;
+                for (var i = 0, l = args.length; i < l; ++i) {
+                    if (args[i] && typeof args[i][prop] === "function") {
+                        args[i][prop].apply(thisValue, slice.call(arguments, 2));
+                        return;
+                    }
+                }
+                throwYieldError(this.proxy, " cannot yield to '" + prop +
+                    "' since no callback was passed.", args);
+            },
+
+            toString: function () {
+                var callStr = this.proxy.toString() + "(";
+                var args = [];
+
+                for (var i = 0, l = this.args.length; i < l; ++i) {
+                    args.push(sinon.format(this.args[i]));
+                }
+
+                callStr = callStr + args.join(", ") + ")";
+
+                if (typeof this.returnValue != "undefined") {
+                    callStr += " => " + sinon.format(this.returnValue);
+                }
+
+                if (this.exception) {
+                    callStr += " !" + this.exception.name;
+
+                    if (this.exception.message) {
+                        callStr += "(" + this.exception.message + ")";
+                    }
+                }
+
+                return callStr;
+            }
+        };
+
+        callProto.invokeCallback = callProto.yield;
+
+        function createSpyCall(spy, thisValue, args, returnValue, exception, id) {
+            if (typeof id !== "number") {
+                throw new TypeError("Call id is not a number");
+            }
+            var proxyCall = sinon.create(callProto);
+            proxyCall.proxy = spy;
+            proxyCall.thisValue = thisValue;
+            proxyCall.args = args;
+            proxyCall.returnValue = returnValue;
+            proxyCall.exception = exception;
+            proxyCall.callId = id;
+
+            return proxyCall;
+        }
+        createSpyCall.toString = callProto.toString; // used by mocks
+
+        sinon.spyCall = createSpyCall;
+        return createSpyCall;
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        require("./match");
+        require("./format");
+        module.exports = makeApi(sinon);
+    }
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+  * @depend times_in_words.js
+  * @depend util/core.js
+  * @depend extend.js
+  * @depend call.js
+  * @depend format.js
+  */
+/**
+  * Spy functions
+  *
+  * @author Christian Johansen (christian@cjohansen.no)
+  * @license BSD
+  *
+  * Copyright (c) 2010-2013 Christian Johansen
+  */
+
+(function (sinon) {
+    function makeApi(sinon) {
+        var push = Array.prototype.push;
+        var slice = Array.prototype.slice;
+        var callId = 0;
+
+        function spy(object, property) {
+            if (!property && typeof object == "function") {
+                return spy.create(object);
+            }
+
+            if (!object && !property) {
+                return spy.create(function () { });
+            }
+
+            var method = object[property];
+            return sinon.wrapMethod(object, property, spy.create(method));
+        }
+
+        function matchingFake(fakes, args, strict) {
+            if (!fakes) {
+                return;
+            }
+
+            for (var i = 0, l = fakes.length; i < l; i++) {
+                if (fakes[i].matches(args, strict)) {
+                    return fakes[i];
+                }
+            }
+        }
+
+        function incrementCallCount() {
+            this.called = true;
+            this.callCount += 1;
+            this.notCalled = false;
+            this.calledOnce = this.callCount == 1;
+            this.calledTwice = this.callCount == 2;
+            this.calledThrice = this.callCount == 3;
+        }
+
+        function createCallProperties() {
+            this.firstCall = this.getCall(0);
+            this.secondCall = this.getCall(1);
+            this.thirdCall = this.getCall(2);
+            this.lastCall = this.getCall(this.callCount - 1);
+        }
+
+        var vars = "a,b,c,d,e,f,g,h,i,j,k,l";
+        function createProxy(func, proxyLength) {
+            // Retain the function length:
+            var p;
+            if (proxyLength) {
+                eval("p = (function proxy(" + vars.substring(0, proxyLength * 2 - 1) +
+                    ") { return p.invoke(func, this, slice.call(arguments)); });");
+            } else {
+                p = function proxy() {
+                    return p.invoke(func, this, slice.call(arguments));
+                };
+            }
+            return p;
+        }
+
+        var uuid = 0;
+
+        // Public API
+        var spyApi = {
+            reset: function () {
+                if (this.invoking) {
+                    var err = new Error("Cannot reset Sinon function while invoking it. " +
+                                        "Move the call to .reset outside of the callback.");
+                    err.name = "InvalidResetException";
+                    throw err;
+                }
+
+                this.called = false;
+                this.notCalled = true;
+                this.calledOnce = false;
+                this.calledTwice = false;
+                this.calledThrice = false;
+                this.callCount = 0;
+                this.firstCall = null;
+                this.secondCall = null;
+                this.thirdCall = null;
+                this.lastCall = null;
+                this.args = [];
+                this.returnValues = [];
+                this.thisValues = [];
+                this.exceptions = [];
+                this.callIds = [];
+                if (this.fakes) {
+                    for (var i = 0; i < this.fakes.length; i++) {
+                        this.fakes[i].reset();
+                    }
+                }
+
+                return this;
+            },
+
+            create: function create(func, spyLength) {
+                var name;
+
+                if (typeof func != "function") {
+                    func = function () { };
+                } else {
+                    name = sinon.functionName(func);
+                }
+
+                if (!spyLength) {
+                    spyLength = func.length;
+                }
+
+                var proxy = createProxy(func, spyLength);
+
+                sinon.extend(proxy, spy);
+                delete proxy.create;
+                sinon.extend(proxy, func);
+
+                proxy.reset();
+                proxy.prototype = func.prototype;
+                proxy.displayName = name || "spy";
+                proxy.toString = sinon.functionToString;
+                proxy.instantiateFake = sinon.spy.create;
+                proxy.id = "spy#" + uuid++;
+
+                return proxy;
+            },
+
+            invoke: function invoke(func, thisValue, args) {
+                var matching = matchingFake(this.fakes, args);
+                var exception, returnValue;
+
+                incrementCallCount.call(this);
+                push.call(this.thisValues, thisValue);
+                push.call(this.args, args);
+                push.call(this.callIds, callId++);
+
+                // Make call properties available from within the spied function:
+                createCallProperties.call(this);
+
+                try {
+                    this.invoking = true;
+
+                    if (matching) {
+                        returnValue = matching.invoke(func, thisValue, args);
+                    } else {
+                        returnValue = (this.func || func).apply(thisValue, args);
+                    }
+
+                    var thisCall = this.getCall(this.callCount - 1);
+                    if (thisCall.calledWithNew() && typeof returnValue !== "object") {
+                        returnValue = thisValue;
+                    }
+                } catch (e) {
+                    exception = e;
+                } finally {
+                    delete this.invoking;
+                }
+
+                push.call(this.exceptions, exception);
+                push.call(this.returnValues, returnValue);
+
+                // Make return value and exception available in the calls:
+                createCallProperties.call(this);
+
+                if (exception !== undefined) {
+                    throw exception;
+                }
+
+                return returnValue;
+            },
+
+            named: function named(name) {
+                this.displayName = name;
+                return this;
+            },
+
+            getCall: function getCall(i) {
+                if (i < 0 || i >= this.callCount) {
+                    return null;
+                }
+
+                return sinon.spyCall(this, this.thisValues[i], this.args[i],
+                                        this.returnValues[i], this.exceptions[i],
+                                        this.callIds[i]);
+            },
+
+            getCalls: function () {
+                var calls = [];
+                var i;
+
+                for (i = 0; i < this.callCount; i++) {
+                    calls.push(this.getCall(i));
+                }
+
+                return calls;
+            },
+
+            calledBefore: function calledBefore(spyFn) {
+                if (!this.called) {
+                    return false;
+                }
+
+                if (!spyFn.called) {
+                    return true;
+                }
+
+                return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1];
+            },
+
+            calledAfter: function calledAfter(spyFn) {
+                if (!this.called || !spyFn.called) {
+                    return false;
+                }
+
+                return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1];
+            },
+
+            withArgs: function () {
+                var args = slice.call(arguments);
+
+                if (this.fakes) {
+                    var match = matchingFake(this.fakes, args, true);
+
+                    if (match) {
+                        return match;
+                    }
+                } else {
+                    this.fakes = [];
+                }
+
+                var original = this;
+                var fake = this.instantiateFake();
+                fake.matchingAguments = args;
+                fake.parent = this;
+                push.call(this.fakes, fake);
+
+                fake.withArgs = function () {
+                    return original.withArgs.apply(original, arguments);
+                };
+
+                for (var i = 0; i < this.args.length; i++) {
+                    if (fake.matches(this.args[i])) {
+                        incrementCallCount.call(fake);
+                        push.call(fake.thisValues, this.thisValues[i]);
+                        push.call(fake.args, this.args[i]);
+                        push.call(fake.returnValues, this.returnValues[i]);
+                        push.call(fake.exceptions, this.exceptions[i]);
+                        push.call(fake.callIds, this.callIds[i]);
+                    }
+                }
+                createCallProperties.call(fake);
+
+                return fake;
+            },
+
+            matches: function (args, strict) {
+                var margs = this.matchingAguments;
+
+                if (margs.length <= args.length &&
+                    sinon.deepEqual(margs, args.slice(0, margs.length))) {
+                    return !strict || margs.length == args.length;
+                }
+            },
+
+            printf: function (format) {
+                var spy = this;
+                var args = slice.call(arguments, 1);
+                var formatter;
+
+                return (format || "").replace(/%(.)/g, function (match, specifyer) {
+                    formatter = spyApi.formatters[specifyer];
+
+                    if (typeof formatter == "function") {
+                        return formatter.call(null, spy, args);
+                    } else if (!isNaN(parseInt(specifyer, 10))) {
+                        return sinon.format(args[specifyer - 1]);
+                    }
+
+                    return "%" + specifyer;
+                });
+            }
+        };
+
+        function delegateToCalls(method, matchAny, actual, notCalled) {
+            spyApi[method] = function () {
+                if (!this.called) {
+                    if (notCalled) {
+                        return notCalled.apply(this, arguments);
+                    }
+                    return false;
+                }
+
+                var currentCall;
+                var matches = 0;
+
+                for (var i = 0, l = this.callCount; i < l; i += 1) {
+                    currentCall = this.getCall(i);
+
+                    if (currentCall[actual || method].apply(currentCall, arguments)) {
+                        matches += 1;
+
+                        if (matchAny) {
+                            return true;
+                        }
+                    }
+                }
+
+                return matches === this.callCount;
+            };
+        }
+
+        delegateToCalls("calledOn", true);
+        delegateToCalls("alwaysCalledOn", false, "calledOn");
+        delegateToCalls("calledWith", true);
+        delegateToCalls("calledWithMatch", true);
+        delegateToCalls("alwaysCalledWith", false, "calledWith");
+        delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch");
+        delegateToCalls("calledWithExactly", true);
+        delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly");
+        delegateToCalls("neverCalledWith", false, "notCalledWith",
+            function () { return true; });
+        delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch",
+            function () { return true; });
+        delegateToCalls("threw", true);
+        delegateToCalls("alwaysThrew", false, "threw");
+        delegateToCalls("returned", true);
+        delegateToCalls("alwaysReturned", false, "returned");
+        delegateToCalls("calledWithNew", true);
+        delegateToCalls("alwaysCalledWithNew", false, "calledWithNew");
+        delegateToCalls("callArg", false, "callArgWith", function () {
+            throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+        });
+        spyApi.callArgWith = spyApi.callArg;
+        delegateToCalls("callArgOn", false, "callArgOnWith", function () {
+            throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
+        });
+        spyApi.callArgOnWith = spyApi.callArgOn;
+        delegateToCalls("yield", false, "yield", function () {
+            throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+        });
+        // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode.
+        spyApi.invokeCallback = spyApi.yield;
+        delegateToCalls("yieldOn", false, "yieldOn", function () {
+            throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
+        });
+        delegateToCalls("yieldTo", false, "yieldTo", function (property) {
+            throw new Error(this.toString() + " cannot yield to '" + property +
+                "' since it was not yet invoked.");
+        });
+        delegateToCalls("yieldToOn", false, "yieldToOn", function (property) {
+            throw new Error(this.toString() + " cannot yield to '" + property +
+                "' since it was not yet invoked.");
+        });
+
+        spyApi.formatters = {
+            c: function (spy) {
+                return sinon.timesInWords(spy.callCount);
+            },
+
+            n: function (spy) {
+                return spy.toString();
+            },
+
+            C: function (spy) {
+                var calls = [];
+
+                for (var i = 0, l = spy.callCount; i < l; ++i) {
+                    var stringifiedCall = "    " + spy.getCall(i).toString();
+                    if (/\n/.test(calls[i - 1])) {
+                        stringifiedCall = "\n" + stringifiedCall;
+                    }
+                    push.call(calls, stringifiedCall);
+                }
+
+                return calls.length > 0 ? "\n" + calls.join("\n") : "";
+            },
+
+            t: function (spy) {
+                var objects = [];
+
+                for (var i = 0, l = spy.callCount; i < l; ++i) {
+                    push.call(objects, sinon.format(spy.thisValues[i]));
+                }
+
+                return objects.join(", ");
+            },
+
+            "*": function (spy, args) {
+                var formatted = [];
+
+                for (var i = 0, l = args.length; i < l; ++i) {
+                    push.call(formatted, sinon.format(args[i]));
+                }
+
+                return formatted.join(", ");
+            }
+        };
+
+        sinon.extend(spy, spyApi);
+
+        spy.spyCall = sinon.spyCall;
+        sinon.spy = spy;
+
+        return spy;
+    }
+
+    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
+    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;
+
+    function loadDependencies(require, exports, module) {
+        var sinon = require("./util/core");
+        require("./call");
+        require("./extend");
+        require("./times_in_words");
+        require("./format");
+        module.exports = makeApi(sinon);
+    }
+
+    if (isAMD) {
+        define(loadDependencies);
+    } else if (isNode) {
+        loadDependencies(require, module.exports, module);
+    } else if (!sinon) {
+        return;
+    } else {
+        makeApi(sinon);
+    }
+}(typeof sinon == "object" && sinon || null));
+
+/**
+ * @depend util/core.js
+ * @depend extend.js
+ */
+/**
+ * Stub behavior
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @author Tim Fischbach (mail@timfischbach.de)
+ * @license BSD
+ *
+ * Copyright (c) 2010-2013 Christian Johansen
+ */
+
+(function (sinon) {
+    var slice = Array.prototype.slice;
+    var join = Array.prototype.join;
+    var useLeftMostCallback = -1;
+    var useRightMostCallback = -2;
+
+    var nextTick = (function () {
+        if (typeof process === "object" && typeof process.nextTick === "function") {
+            return process.nextTick;
+        } else if (typeof setImmediate === "function") {
+            return setImmediate;
+        } else {
+            return function (callback) {
+                setTimeout(callback, 0);
+            };
+        }
+    })();
+
+    function throwsException(error, message) {
+        if (typeof error == "string") {
+            this.exception = new Error(message || "");
+            this.exception.name = error;
+        } else if (!error) {
+            this.exception = new Error("Error");
+        } else {
+            this.exception = error;
+        }
+
+        return this;
+    }
+
+    function getCallback(behavior, args) {
+        var callArgAt = behavior.callArgAt;
+
+        if (callArgAt >= 0) {
+            return args[callArgAt];
+        }
+
+        var argumentList;
+
+        if (callArgAt === useLeftMostCallback) {
+            argumentList = args;
+        }
+
+        if (callArgAt === useRightMostCallback) {
+            argumentList = slice.call(args).reverse();
+        }
+
+        var callArgProp = behavior.callArgProp;
+
+        for (var i = 0, l = argumentList.length; i < l; ++i) {
+            if (!callArgProp && typeof argumentList[i] == "function") {
+                return argumentList[i];
+            }
+
+            if (callArgProp && argumentList[i] &&
+                typeof argumentList[i][callArgProp] == "function") {
+                return argumentList[i][callArgProp];
+            }
+        }
+
+        return null;
+    }
+
+

<TRUNCATED>

[2/4] ambari git commit: AMBARI-10077. Add UI unit tests for Create App Wizard controllers (onechiporenko)

Posted by on...@apache.org.
http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-qunit-1.0.0.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-qunit-1.0.0.js b/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-qunit-1.0.0.js
new file mode 100644
index 0000000..c26232f
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/assets/javascripts/sinon-qunit-1.0.0.js
@@ -0,0 +1,62 @@
+/**
+ * sinon-qunit 1.0.0, 2010/12/09
+ *
+ * @author Christian Johansen (christian@cjohansen.no)
+ *
+ * (The BSD License)
+ * 
+ * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * 
+ *     * Redistributions of source code must retain the above copyright notice,
+ *       this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright notice,
+ *       this list of conditions and the following disclaimer in the documentation
+ *       and/or other materials provided with the distribution.
+ *     * Neither the name of Christian Johansen nor the names of his contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+/*global sinon, QUnit, test*/
+sinon.assert.fail = function (msg) {
+    QUnit.ok(false, msg);
+};
+
+sinon.assert.pass = function (assertion) {
+    QUnit.ok(true, assertion);
+};
+
+sinon.config = {
+    injectIntoThis: true,
+    injectInto: null,
+    properties: ["spy", "stub", "mock", "clock", "sandbox"],
+    useFakeTimers: true,
+    useFakeServer: false
+};
+
+(function (global) {
+    var qTest = QUnit.test;
+    
+    QUnit.test = global.test = function (testName, expected, callback, async) {
+        if (arguments.length === 2) {
+            callback = expected;
+            expected = null;
+        }
+
+        return qTest(testName, expected, sinon.test(callback), async);
+    };
+}(this));

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/app/assets/tests.html
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/assets/tests.html b/contrib/views/slider/src/main/resources/ui/app/assets/tests.html
index 6c08bff..b6591da 100644
--- a/contrib/views/slider/src/main/resources/ui/app/assets/tests.html
+++ b/contrib/views/slider/src/main/resources/ui/app/assets/tests.html
@@ -30,6 +30,8 @@
 <script src="javascripts/vendor.js"></script>
 <script src="javascripts/jquery.mockjax.js"></script>
 <script src="javascripts/ember-qunit.js"></script>
+<script src="javascripts/sinon-1.13.0.js"></script>
+<script src="javascripts/sinon-qunit-1.0.0.js"></script>
 <script src="javascripts/app.js"></script>
 <script>
   emq.globalize();

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/integration/pages/index_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/integration/pages/index_test.js b/contrib/views/slider/src/main/resources/ui/test/integration/pages/index_test.js
index 2b63506..641a856 100644
--- a/contrib/views/slider/src/main/resources/ui/test/integration/pages/index_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/integration/pages/index_test.js
@@ -19,6 +19,7 @@
 QUnit.module('integration/pages - index', {
 
   setup: function () {
+    sinon.config.useFakeTimers = false;
     App.set('viewEnabled', true);
     App.__container__.lookup('controller:Slider').getViewDisplayParametersSuccessCallback({
       "ViewInstanceInfo": {

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/integration/pages/slider_errors_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/integration/pages/slider_errors_test.js b/contrib/views/slider/src/main/resources/ui/test/integration/pages/slider_errors_test.js
index 7f81706..6ccc89c 100644
--- a/contrib/views/slider/src/main/resources/ui/test/integration/pages/slider_errors_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/integration/pages/slider_errors_test.js
@@ -19,6 +19,7 @@
 QUnit.module('integration/pages - index', {
 
   setup: function () {
+    sinon.config.useFakeTimers = false;
     Ember.run(App, App.advanceReadiness);
     Em.run(function () {
       var p = {

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/integration/processes/create_new_app_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/integration/processes/create_new_app_test.js b/contrib/views/slider/src/main/resources/ui/test/integration/processes/create_new_app_test.js
index 5c658ef..5be4d5b 100644
--- a/contrib/views/slider/src/main/resources/ui/test/integration/processes/create_new_app_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/integration/processes/create_new_app_test.js
@@ -108,6 +108,8 @@ QUnit.module('integration/processes - Create New App', {
 
   setup: function () {
 
+    sinon.config.useFakeTimers = false;
+
     $.mockjax({
       type: 'GET',
       url: '*',

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step1_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step1_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step1_controller_test.js
index b167f1d..51bbe23 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step1_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step1_controller_test.js
@@ -20,12 +20,49 @@ moduleFor('controller:createAppWizardStep1', 'App.CreateAppWizardStep1Controller
 
   needs: [
     'controller:createAppWizard'
-  ]
+  ],
+
+  setup: function () {
+    sinon.stub(App.ajax, 'send', Em.K);
+  },
+
+  teardown: function () {
+    App.ajax.send.restore();
+  }
+
+});
+
+var selectedType = Em.Object.create({
+  id: 'HBASE',
+  configs: {
+    n0: 'v0'
+  }
+});
+
+test('appWizardController', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    controllers: {
+      createAppWizard: {
+        key: 'value0'
+      }
+    }
+  });
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.key', 'value1');
+  });
+
+  equal(controller.get('appWizardController.key'), 'value1', 'should link to App.CreateAppWizardController');
 
 });
 
 test('isAppTypesError', function () {
 
+  expect(2);
+
   var controller = this.subject({availableTypes: {content: []}});
   equal(controller.get('isAppTypesError'), true, 'should be true if no app types provided');
 
@@ -38,6 +75,92 @@ test('isAppTypesError', function () {
 
 });
 
+test('typeDescription', function () {
+
+  expect(2);
+
+  var controller = this.subject();
+
+  equal(controller.get('typeDescription'), '', 'default typeDescription');
+
+  Em.run(function () {
+    controller.set('selectedType', Em.Object.create({
+      displayName: 'HBASE'
+    }));
+  });
+
+  equal(controller.get('typeDescription'), Em.I18n.t('wizard.step1.typeDescription').format('HBASE'), 'typeDescription is set from selectedType.displayName');
+
+});
+
+test('initializeNewApp', function () {
+
+  expect(9);
+
+  var controller = this.subject({
+      store: Em.Object.create({
+        all: function () {
+          return [];
+        }
+      })
+    }),
+    app = Em.Object.create({
+      name: 'n',
+      includeFilePatterns: 'i',
+      excludeFilePatterns: 'e',
+      frequency: 'f',
+      queueName: 'q',
+      specialLabel: 's',
+      selectedYarnLabel: 'y'
+    }),
+    title = '{0} should be taken from appWizardController.newApp';
+
+  Em.run(function () {
+    controller.initializeNewApp();
+  });
+
+  equal(controller.get('newApp.selectedYarnLabel'), 0, 'selectedYarnLabel should be 0 as default');
+
+  var values = Em.keys(controller.get('newApp')).without('appType').without('configs').without('selectedYarnLabel').map(function (item) {
+    return controller.get('newApp.' + item);
+  });
+
+  propEqual(values.uniq(), [''], 'should set properties values to empty strings as default');
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.newApp', app);
+    controller.initializeNewApp();
+  });
+
+  Em.keys(app).forEach(function (key) {
+    equal(controller.get('newApp.' + key), app.get(key), title.format(key));
+  });
+
+});
+
+test('loadAvailableTypes', function () {
+
+  expect(1);
+
+  var testObject = {
+      key: 'value'
+    },
+    controller = this.subject({
+    store: Em.Object.create({
+      all: function () {
+        return testObject;
+      }
+    })
+  });
+
+  Em.run(function () {
+    controller.loadAvailableTypes();
+  });
+
+  propEqual(controller.get('availableTypes'), testObject, 'availableTypes should be loaded from store');
+
+});
+
 test('nameValidator', function () {
   expect(7);
 
@@ -79,13 +202,9 @@ test('nameValidator', function () {
 
 test('validateAppNameSuccessCallback', function () {
 
-  var selectedType = Em.Object.create({
-      id: 'HBASE',
-      configs: {
-        n0: 'v0'
-      }
-    }),
-    title = 'newApp should have {0} set',
+  expect(5);
+
+  var title = 'newApp should have {0} set',
     controller = this.subject({
       newApp: Em.Object.create(),
       selectedType: selectedType
@@ -104,6 +223,67 @@ test('validateAppNameSuccessCallback', function () {
 
 });
 
+test('validateAppNameErrorCallback', function () {
+
+  expect(7);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    sinon.stub(Bootstrap.ModalManager, 'open', Em.K);
+    sinon.stub(controller, 'defaultErrorHandler', Em.K);
+    controller.validateAppNameErrorCallback({
+      status: 409
+    }, null, null, null, {
+      name: 'name'
+    });
+  });
+
+  ok(Bootstrap.ModalManager.open.calledOnce, 'app name conflict popup should be displayed');
+  ok(!controller.defaultErrorHandler.called, 'defaultErrorHandler shouldn\'t be executed');
+
+  Em.run(function () {
+    Bootstrap.ModalManager.open.restore();
+    controller.defaultErrorHandler.restore();
+    sinon.stub(Bootstrap.ModalManager, 'open', Em.K);
+    sinon.stub(controller, 'defaultErrorHandler', Em.K);
+    controller.validateAppNameErrorCallback({
+      status: 400
+    }, null, null, {
+      url: 'url',
+      type: 'type'
+    }, null);
+  });
+
+  ok(!Bootstrap.ModalManager.open.called, 'app name conflict popup shouldn\'t be displayed');
+  ok(controller.defaultErrorHandler.calledOnce, 'defaultErrorHandler should be executed');
+  propEqual(controller.defaultErrorHandler.firstCall.args[0], {
+    status: 400
+  }, 'should pass request info to defaultErrorHandler');
+  equal(controller.defaultErrorHandler.firstCall.args[1], 'url', 'should pass url to defaultErrorHandler');
+  equal(controller.defaultErrorHandler.firstCall.args[2], 'type', 'should pass type to defaultErrorHandler');
+
+  Bootstrap.ModalManager.open.restore();
+  controller.defaultErrorHandler.restore();
+
+});
+
+test('validateAppNameCompleteCallback', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    validateAppNameRequestExecuting: true
+  });
+
+  Em.run(function () {
+    controller.validateAppNameCompleteCallback();
+  });
+
+  ok(!controller.get('validateAppNameRequestExecuting'), 'validateAppNameRequestExecuting should be set to false');
+
+});
+
 test('isSubmitDisabled', function () {
 
   expect(6);
@@ -129,8 +309,8 @@ test('isSubmitDisabled', function () {
         title: 'app name is invalid'
       },
       {
-        key: 'no app types are available',
-        title: 'request is executing'
+        key: 'isAppTypesError',
+        title: 'no app types are available'
       },
       {
         key: 'isFrequencyError',
@@ -205,4 +385,47 @@ test('frequencyValidator', function () {
     equal(controller.get('frequencyErrorMessage'), item.frequencyErrorMessage, messageTitle.format(item.title));
   });
 
+});
+
+test('saveApp', function () {
+
+  expect(4);
+
+  var controller = this.subject({
+      newApp: Em.Object.create(),
+      selectedType: selectedType
+    }),
+    saveAppTitle = 'newApp should have {0} set';
+
+  Em.run(function () {
+    controller.saveApp();
+  });
+
+  propEqual(controller.get('newApp.appType'), selectedType, saveAppTitle.format('appType'));
+  propEqual(controller.get('newApp.configs'), selectedType.configs, saveAppTitle.format('configs'));
+  deepEqual(controller.get('newApp.predefinedConfigNames'), Em.keys(selectedType.configs), saveAppTitle.format('predefinedConfigNames'));
+  propEqual(controller.get('appWizardController.newApp'), controller.get('newApp'), 'newApp should be set in CreateAppWizardController');
+
+});
+
+test('actions.submit', function () {
+
+  expect(3);
+
+  var controller = this.subject({
+    validateAppNameRequestExecuting: false,
+    validateAppNameSuccessCallback: Em.K,
+    newApp: {
+      name: 'name'
+    }
+  });
+
+  Em.run(function () {
+    controller.send('submit');
+  });
+
+  ok(controller.get('validateAppNameRequestExecuting'), 'validateAppNameRequestExecuting should be set to true');
+  ok(App.ajax.send.calledOnce, 'request to server should be sent');
+  equal(App.ajax.send.firstCall.args[0].data.name, 'name', 'name should be passed');
+
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step2_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step2_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step2_controller_test.js
index 641f51a..5115a64 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step2_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step2_controller_test.js
@@ -24,8 +24,325 @@ moduleFor('controller:createAppWizardStep2', 'App.CreateAppWizardStep2Controller
 
 });
 
+var title = 'should be {0}';
+
+test('appWizardController', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    controllers: {
+      createAppWizard: {
+        key: 'value0'
+      }
+    }
+  });
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.key', 'value1');
+  });
+
+  equal(controller.get('appWizardController.key'), 'value1', 'should link to App.CreateAppWizardController');
+
+});
+
+test('isError', function () {
+
+  expect(18);
+
+  var cases = [
+      {
+        content: [],
+        isError: false
+      },
+      {
+        content: [
+          Em.Object.create()
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            yarnMemory: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: ' \r\n',
+            yarnMemory: ' \r\n',
+            yarnCPU: ' \r\n'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: 'n',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: 'n',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: 'n'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0.5',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0.5',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: '0.5'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '-1',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '-1',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: '-1'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          }),
+          Em.Object.create({
+            numInstances: '-1',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          })
+        ],
+        isError: true
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          })
+        ],
+        isError: false
+      },
+      {
+        content: [
+          Em.Object.create({
+            numInstances: '0',
+            yarnMemory: '0',
+            yarnCPU: '0'
+          }),
+          Em.Object.create({
+            numInstances: '1',
+            yarnMemory: '1',
+            yarnCPU: '1'
+          })
+        ],
+        isError: false
+      }
+    ],
+    controller = this.subject();
+
+  cases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.set('content', item.content);
+    });
+
+    equal(controller.get('isError'), item.isError, title.format(item.isError));
+
+  });
+
+});
+
+test('isSubmitDisabled', function () {
+
+  expect(2);
+
+  var cases = [true, false],
+  controller = this.subject();
+
+  cases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.set('isError', item);
+    });
+
+    equal(controller.get('isSubmitDisabled'), item, title.format(item));
+
+  });
+
+});
+
+test('initializeNewApp', function () {
+
+  expect(4);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.newApp', {
+      components: [
+        Em.Object.create({
+          name: 'n'
+        })
+      ]
+    });
+    controller.initializeNewApp();
+  });
+
+  equal(controller.get('newApp.components.length'), 1, 'newApp should be taken from appWizardController');
+  equal(controller.get('newApp.components')[0].get('name'), 'n', 'newApp has correct names of components');
+  equal(controller.get('content.length'), 1, 'content should be taken from appWizardController');
+  equal(controller.get('content')[0].get('name'), 'n', 'content has correct names of components');
+
+});
+
+test('loadTypeComponents', function () {
+
+  expect(8);
+
+  var toStringTitle = 'should convert {0} to string',
+    controller = this.subject();
+
+  Em.run(function () {
+    controller.set('newApp', {
+      appType: {
+        components: [
+          Em.Object.create({
+            name: 'n0',
+            defaultNumInstances: 0,
+            defaultYARNMemory: 128,
+            defaultYARNCPU: 1
+          })
+        ]
+      }
+    });
+    controller.set('controllers.createAppWizard.newApp', {
+      components: [
+        Em.Object.create({
+          name: 'n1'
+        })
+      ]
+    });
+    controller.loadTypeComponents();
+  });
+
+  equal(controller.get('content.length'), 1, 'content should contain one item');
+  equal(controller.get('content')[0].get('name'), 'n1', 'should take components from wizard controller');
+
+  Em.run(function () {
+    controller.get('content').clear();
+    controller.get('controllers.createAppWizard.newApp.components').clear();
+    controller.loadTypeComponents();
+  });
+
+  equal(controller.get('content.length'), 1, 'content contains one item');
+  equal(controller.get('content')[0].get('name'), 'n0', 'should take components from step controller');
+  deepEqual(controller.get('content')[0].get('numInstances'), '0', toStringTitle.format('numInstances'));
+  deepEqual(controller.get('content')[0].get('yarnMemory'), '128', toStringTitle.format('yarnMemory'));
+  deepEqual(controller.get('content')[0].get('yarnCPU'), '1', toStringTitle.format('yarnCPU'));
+
+  Em.run(function () {
+    controller.get('content').clear();
+    controller.get('controllers.createAppWizard.newApp.components').clear();
+    controller.get('newApp.appType.components').clear();
+    controller.loadTypeComponents();
+  });
+
+  equal(controller.get('content.length'), 0, 'content should remain empty');
+
+});
+
 test('isNotInteger', function () {
 
+  expect(6);
+
   var controller = this.subject({});
   equal(controller.isNotInteger('1'), false, 'Valid value');
   equal(controller.isNotInteger('-1'), true, 'Invalid value (1)');
@@ -35,3 +352,52 @@ test('isNotInteger', function () {
   equal(controller.isNotInteger(null), true, 'Invalid value (5)');
 
 });
+
+test('saveComponents', function () {
+
+  expect(2);
+
+  var controller = this.subject({
+    content: [
+      Em.Object.create({
+        name: 'n'
+      })
+    ]
+  });
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.newApp', {});
+    controller.saveComponents();
+  });
+
+  equal(controller.get('appWizardController.newApp.components.length'), 1, 'components in wizard controller should be set from content');
+  equal(controller.get('appWizardController.newApp.components')[0].get('name'), 'n', 'components in wizard controller have correct names');
+
+});
+
+test('actions.submit', function () {
+
+  expect(3);
+
+  var controller = this.subject({
+    content: [
+      Em.Object.create({
+        name: 'n'
+      })
+    ]
+  });
+
+  Em.run(function () {
+    controller.get('controllers.createAppWizard').setProperties({
+      newApp: {},
+      currentStep: 2,
+      transitionToRoute: Em.K
+    });
+    controller.send('submit');
+  });
+
+  equal(controller.get('appWizardController.newApp.components.length'), 1, 'components in wizard controller should be set from content');
+  equal(controller.get('appWizardController.newApp.components')[0].get('name'), 'n', 'components in wizard controller have correct names');
+  equal(controller.get('appWizardController.currentStep'), '3', 'should go to step3');
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step3_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step3_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step3_controller_test.js
index 6a10e61..17d66e4 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step3_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step3_controller_test.js
@@ -19,44 +19,403 @@
 moduleFor('controller:createAppWizardStep3', 'App.CreateAppWizardStep3Controller', {
 
   needs: [
-    'controller:createAppWizard',
-    'controller:slider'
-  ]
+    'controller:createAppWizard'
+  ],
+
+  teardown: function () {
+    App.reset();
+  }
+
+});
+
+test('appWizardController', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    controllers: {
+      createAppWizard: {
+        key: 'value0'
+      }
+    }
+  });
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.key', 'value1');
+  });
+
+  equal(controller.get('appWizardController.key'), 'value1', 'should link to App.CreateAppWizardController');
+
+});
+
+test('newAppConfigs', function () {
+
+  expect(1);
+
+  var configs = {
+      java_home: '/usr/jdk64/jdk1.7.0_40'
+    },
+    controller = this.subject();
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.newApp', {
+      configs: configs
+    });
+  });
+
+  propEqual(controller.get('newAppConfigs'), configs, 'configs should be taken from wizard controller');
+
+});
+
+test('sectionKeys', function () {
+
+  expect(2);
+
+  var cases = [
+      {
+        sectionKeys: ['general', 'custom'],
+        title: 'no newAppConfigs set'
+      },
+      {
+        newAppConfigs: {
+          'p0': 'v0',
+          'site.p1': 'v1',
+          'site.p2.0': 'v2',
+          'site.p2.1': 'v3'
+        },
+        sectionKeys: ['general', 'p1', 'p2', 'custom'],
+        title: 'newAppConfigs are set'
+      }
+    ],
+    controller = this.subject();
+
+  cases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.set('controllers.createAppWizard.newApp', {
+        configs: item.newAppConfigs
+      });
+    });
+
+    propEqual(controller.get('sectionKeys'), item.sectionKeys, item.title);
+
+  });
+
+});
+
+test('loadStep', function () {
+
+  expect(3);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    sinon.stub(controller, 'clearStep', Em.K);
+    sinon.stub(controller, 'initConfigs', Em.K);
+    controller.loadStep();
+  });
+
+  ok(controller.clearStep.calledOnce, 'clearStep should be executed');
+  ok(controller.initConfigs.calledOnce, 'initConfigs should be executed');
+  ok(controller.initConfigs.firstCall.args[0], 'true should be passed as argument to initConfigs');
+
+  controller.clearStep.restore();
+  controller.initConfigs.restore();
 
 });
 
 test('initConfigs', function () {
 
+  expect(15);
+
   var controller = this.subject(),
-    wizardController = controller.get('controllers.createAppWizard');
+    titleDefault = 'should set default {0} property value',
+    titleCustom = 'should set custom {0} property value',
+    titleGlobal = 'should set global {0} property value',
+    titleLabel = 'label shouldn\'t contain \'site.\'';
 
   Em.run(function () {
-    wizardController.setProperties({
-      content: {},
-      newApp: {
-        appType: {
-          configs: []
+    App.setProperties({
+      javaHome: '/usr/jdk64/jdk1.7.0_45',
+      metricsHost: 'host0',
+      metricsPort: '3333',
+      metricsLibPath: '/metrics/lib'
+    });
+    controller.get('controllers.createAppWizard').setProperties({
+      'content': {},
+      'newApp': {
+        'appType': {
+          'configs': {}
         },
-        configs: {
-          java_home: '/usr/jdk64/jdk1.7.0_40'
+        'configs': {
+          'java_home': '/usr/jdk64/jdk1.7.0_40',
+          'site.global.metric_collector_host': 'host1',
+          'site.global.metric_collector_port': '8080',
+          'site.global.metric_collector_lib': '/ams/lib'
+        }
+      }
+    });
+    controller.initConfigs(true);
+  });
+
+  equal(controller.get('configs').findBy('name', 'java_home').get('value'), '/usr/jdk64/jdk1.7.0_45', titleGlobal.format('java_home'));
+  equal(controller.get('configs').findBy('name', 'java_home').get('label'), 'java_home', 'label should be equal to name');
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_host')
+    .get('value'), 'host0', titleGlobal.format('site.global.metric_collector_host'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_port')
+    .get('value'), '3333', titleGlobal.format('site.global.metric_collector_port'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_lib')
+    .get('value'), '/metrics/lib', titleGlobal.format('site.global.metric_collector_lib'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_host')
+    .get('label'), 'global.metric_collector_host', titleLabel);
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_port')
+    .get('label'), 'global.metric_collector_port', titleLabel);
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_lib')
+    .get('label'), 'global.metric_collector_lib', titleLabel);
+
+  Em.run(function () {
+    App.setProperties({
+      javaHome: null,
+      metricsHost: null,
+      metricsPort: null,
+      metricsLibPath: null
+    });
+    controller.initConfigs();
+  });
+
+  equal(controller.get('configs').findBy('name', 'java_home').get('value'), '/usr/jdk64/jdk1.7.0_40', titleCustom.format('java_home'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_host')
+    .get('value'), 'host1', titleGlobal.format('site.global.metric_collector_host'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_port')
+    .get('value'), '8080', titleGlobal.format('site.global.metric_collector_port'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_lib')
+    .get('value'), '/ams/lib', titleGlobal.format('site.global.metric_collector_lib'));
+
+  Em.run(function () {
+    controller.get('controllers.createAppWizard').setProperties({
+      'newApp': {
+        'appType': {
+          'configs':  {
+            'site.global.metric_collector_host': 'host3',
+            'site.global.metric_collector_port': '8888',
+            'site.global.metric_collector_lib': '/var/ams/lib'
+          }
+        },
+        'configs': {}
+      }
+    });
+    controller.initConfigs();
+  });
+
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_host')
+    .get('value'), 'host3', titleDefault.format('site.global.metric_collector_host'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_port')
+    .get('value'), '8888', titleDefault.format('site.global.metric_collector_port'));
+  equal(controller.get('configs').findBy('name', 'ams_metrics').configs.findBy('name', 'site.global.metric_collector_lib')
+    .get('value'), '/var/ams/lib', titleDefault.format('site.global.metric_collector_lib'));
+
+});
+
+test('initConfigSetDependencies', function () {
+
+  expect(1);
+
+  var configSet = {
+      dependencies: [
+        {
+          name: 'App.javaHome'
         }
+      ]
+    },
+    javaHome = '/usr/jdk64/jdk1.7.0_40',
+    controller = this.subject();
+
+  Em.run(function () {
+    App.set('javaHome', javaHome);
+    controller.initConfigSetDependencies(configSet);
+  });
+
+  equal(configSet.dependencies[0].map, javaHome, 'should set map property');
+
+});
+
+test('clearStep', function () {
+
+  expect(1);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    controller.clearStep();
+  });
+
+  ok(!controller.get('isError'), 'isError should be false');
+
+});
+
+test('validateConfigs', function () {
+
+  expect(4);
+
+  var controller = this.subject({
+    configs: [
+      {
+        isSet: false,
+        name: 'p0',
+        value: 'v0'
       }
+    ]
+  });
+
+  ok(controller.validateConfigs(), 'configs are valid');
+  propEqual(controller.get('configsObject'), {
+    p0: 'v0'
+  }, 'configsObject is set');
+
+  Em.run(function () {
+    controller.set('addConfigSetProperties', function () {
+      return null;
     });
-    App.__container__.lookup('controller:slider').getViewDisplayParametersSuccessCallback({
-      "ViewInstanceInfo" : {
-        "instance_data": {
-          "java.home": "/usr/jdk64/jdk1.7.0_45"
+  });
+
+  ok(!controller.validateConfigs(), 'configs are invalid');
+  ok(controller.get('isError'), 'isError is set to true');
+
+});
+
+test('addConfigSetProperties', function () {
+
+  expect(1);
+
+  var configs = [
+      {
+        isSet: false,
+        configs: [
+          {
+            name: 'p0',
+            value: 'v0'
+          }
+        ]
+      },
+      {
+        isSet: true,
+        trigger: {},
+        configs: [
+          {
+            name: 'p1',
+            value: 'v1'
+          }
+        ]
+      },
+      {
+        isSet: true,
+        trigger: {
+          name: 'p2',
+          value: 'v2'
         },
-        "properties": {
-          "slider.user": "admin"
+        configs: [
+          {
+            name: 'p3',
+            value: 'v3'
+          }
+        ]
+      }
+    ],
+    controller = this.subject();
+
+    deepEqual(controller.addConfigSetProperties(configs), [configs[0], configs[2].configs[0]], 'should add config from config sets to general configs array');
+
+});
+
+test('saveConfigs', function () {
+
+  expect(5);
+
+  var configsObject = {
+      p0: 'v0'
+    },
+    controller = this.subject({
+      configsObject: configsObject
+    }),
+    metricsCases = [
+      {
+        configsObject: {
+          'site.global.metrics_enabled': null
         }
+      },
+      {
+        configsObject: {
+          'site.global.metrics_enabled': 'true',
+          'site.global.metric_collector_host': 'h0',
+          'site.global.metric_collector_port': '3333'
+        },
+        metricsEnabledExpected: 'true'
+      },
+      {
+        configsObject: {
+          'site.global.metrics_enabled': 'true',
+          'site.global.metric_collector_host': null,
+          'site.global.metric_collector_port': '8080'
+        },
+        metricsEnabledExpected: 'false'
+      },
+      {
+        configsObject: {
+          'site.global.metrics_enabled': 'true',
+          'site.global.metric_collector_host': 'h1',
+          'site.global.metric_collector_port': null
+        },
+        metricsEnabledExpected: 'false'
       }
+    ],
+    metricsTitle = 'site.global.metrics_enabled should be {0}';
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.newApp', {});
+    controller.saveConfigs();
+  });
+
+  propEqual(controller.get('controllers.createAppWizard.newApp.configs'), configsObject, 'configs are saved to wizard controller');
+
+  metricsCases.forEach(function (item) {
+    controller.reopen({
+      configsObject: item.configsObject
     });
-    Em.run(function () {
-      controller.initConfigs(true);
+    controller.set('controllers.createAppWizard.newApp', {});
+    controller.saveConfigs();
+    equal(controller.get('controllers.createAppWizard.newApp.configs')['site.global.metrics_enabled'],
+      item.metricsEnabledExpected, metricsTitle.format(item.metricsEnabledExpected || 'undefined'));
+  }, this);
+
+});
+
+test('actions.submit', function () {
+
+  expect(2);
+
+  var configsObject = {
+      p0: 'v0'
+    },
+    controller = this.subject({
+      configs: [
+        {
+          isSet: false,
+          name: 'p0',
+          value: 'v0'
+        }
+      ]
+    });
+
+  Em.run(function () {
+    controller.get('controllers.createAppWizard').setProperties({
+      newApp: {},
+      currentStep: 3,
+      transitionToRoute: Em.K
     });
+    controller.send('submit');
   });
 
-  equal(controller.get('configs').findBy('label', 'java_home').get('value'), '/usr/jdk64/jdk1.7.0_45', 'should set default java_home property value');
+  propEqual(controller.get('controllers.createAppWizard.newApp.configs'), configsObject, 'configs are passed to wizard controller');
+  equal(controller.get('controllers.createAppWizard.currentStep'), 4, 'should go to step4');
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step4_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step4_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step4_controller_test.js
index 627e726..fe44f47 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step4_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/createAppWizard/step4_controller_test.js
@@ -23,17 +23,40 @@ moduleFor('controller:createAppWizardStep4', 'App.CreateAppWizardStep4Controller
   ],
 
   setup: function () {
-    App.ajax.send = Em.K;
+    sinon.stub(App.ajax, 'send', Em.K);
   },
 
   teardown: function () {
+    App.ajax.send.restore();
     App.reset();
   }
 
 });
 
+test('appWizardController', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    controllers: {
+      createAppWizard: {
+        key: 'value0'
+      }
+    }
+  });
+
+  Em.run(function () {
+    controller.set('controllers.createAppWizard.key', 'value1');
+  });
+
+  equal(controller.get('appWizardController.key'), 'value1', 'should link to App.CreateAppWizardController');
+
+});
+
 test('isSubmitDisabled', function () {
 
+  expect(3);
+
   var controller = this.subject({
     newApp: Em.Object.create({
       appType: {
@@ -58,9 +81,31 @@ test('isSubmitDisabled', function () {
 
 });
 
+test('configsFormatted', function () {
+
+  expect(1);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    controller.set('newApp', {
+      configs: {
+        p0: 'v0',
+        p1: 'v1',
+        p2: 'v2'
+      }
+    });
+  });
+
+  equal(controller.get('configsFormatted'), '"p0":"v0",\n"p1":"v1",\n"p2":"v2"', 'configs formatted correctly');
+
+});
+
 test('resourcesFormatted', function () {
 
-  var cases = [
+  expect(18);
+
+  var propertiesCases = [
       {
         propertyName: 'numInstances',
         expectedPropertyName: 'instanceCount',
@@ -82,24 +127,73 @@ test('resourcesFormatted', function () {
         value: 2
       }
     ],
+    globalCases = [
+      {
+        includeFilePatterns: null,
+        includeFilePatternsExpected: null,
+        excludeFilePatterns: null,
+        excludeFilePatternsExpected: null,
+        frequency: '1000',
+        frequencyExpected: '1000',
+        title: 'all parameters except one are null'
+      },
+      {
+        includeFilePatterns: '*.log',
+        includeFilePatternsExpected: '*.log',
+        excludeFilePatterns: '*.zip',
+        excludeFilePatternsExpected: '*.zip',
+        frequency: '1000',
+        frequencyExpected: '1000',
+        title: 'all parameters are valid'
+      }
+    ],
+    globalCasesUndefined = [
+      {
+        includeFilePatterns: null,
+        excludeFilePatterns: null,
+        frequency: null,
+        title: 'no patterns and frequency specified'
+      },
+      {
+        includeFilePatterns: ' \r\n',
+        excludeFilePatterns: null,
+        frequency: null,
+        title: 'one parameter is empty string after trimming'
+      }
+    ],
+    selectedYarnLabelCases = [
+      {
+        selectedYarnLabel: 0,
+        components: []
+      },
+      {
+        selectedYarnLabel: 1,
+        yarnLabelExpression: ''
+      },
+      {
+        selectedYarnLabel: 2,
+        specialLabel: 'specialLabel',
+        yarnLabelExpression: 'specialLabel'
+      }
+    ],
     title = '{0} should be {1}',
-    label = 'label';
+    selectedYarnLabelTitle = 'selected YARN label is {0}',
+    label = 'label',
+    controller = this.subject({
+      newApp: Em.Object.create({
+        components: [
+          Em.Object.create({
+            name: 'c',
+            numInstances: '0',
+            yarnMemory: '512',
+            yarnCPU: '1',
+            priority: 1
+          })
+        ]
+      })
+    });
 
-  var controller = this.subject({
-    newApp: Em.Object.create({
-      components: [
-        Em.Object.create({
-          name: 'c',
-          numInstances: '0',
-          yarnMemory: '512',
-          yarnCPU: '1',
-          priority: 1
-        })
-      ]
-    })
-  });
-
-  cases.forEach(function (item) {
+  propertiesCases.forEach(function (item) {
 
     Em.run(function () {
       controller.get('newApp.components')[0].set(item.propertyName, item.value);
@@ -130,4 +224,215 @@ test('resourcesFormatted', function () {
 
   equal(controller.get('resourcesFormatted.components')[0].yarnLabel, label, 'yarnLabel should be trimmed');
 
+  globalCases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.get('newApp').setProperties({
+        includeFilePatterns: item.includeFilePatterns,
+        excludeFilePatterns: item.excludeFilePatterns,
+        frequency: item.frequency
+      });
+      controller.notifyPropertyChange('newApp.components.@each.numInstances');
+    });
+
+    var global = controller.get('resourcesFormatted.global');
+
+    deepEqual(global['yarn.log.include.patterns'], item.includeFilePatternsExpected, item.title);
+    deepEqual(global['yarn.log.exclude.patterns'], item.excludeFilePatternsExpected, item.title);
+    deepEqual(global['yarn.log.interval'], item.frequencyExpected, item.title);
+
+  });
+
+  globalCasesUndefined.forEach(function (item) {
+
+    Em.run(function () {
+      controller.get('newApp').setProperties({
+        includeFilePatterns: item.includeFilePatterns,
+        excludeFilePatterns: item.excludeFilePatterns,
+        frequency: item.frequency
+      });
+      controller.notifyPropertyChange('newApp.components.@each.numInstances');
+    });
+
+    equal(typeof controller.get('resourcesFormatted.global'), 'undefined', item.title);
+
+  });
+
+  selectedYarnLabelCases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.get('newApp').setProperties({
+        selectedYarnLabel: item.selectedYarnLabel,
+        specialLabel: item.specialLabel
+      });
+      controller.notifyPropertyChange('newApp.components.@each.numInstances');
+    });
+
+    var message = selectedYarnLabelTitle.format(item.selectedYarnLabel);
+
+    if (Em.isNone(item.yarnLabelExpression)) {
+      ok(!controller.get('resourcesFormatted.components').isAny('id', 'slider-appmaster'), message);
+    } else {
+      equal(controller.get('resourcesFormatted.components').findBy('id', 'slider-appmaster')['yarn.label.expression'],
+        item.yarnLabelExpression, message);
+    }
+
+  });
+
+});
+
+test('loadStep', function () {
+
+  expect(1);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    sinon.stub(controller, 'initializeNewApp', Em.K);
+    controller.loadStep();
+  });
+
+  ok(controller.initializeNewApp.calledOnce, 'initializeNewApp should be executed');
+
+  controller.initializeNewApp.restore();
+
+});
+
+test('initializeNewApp', function () {
+
+  expect(1);
+
+  var newApp = {
+      key: 'value'
+    },
+    controller = this.subject({
+      controllers: {
+        createAppWizard: {
+          newApp: newApp
+        }
+      }
+    });
+
+  Em.run(function () {
+    controller.loadStep();
+  });
+
+  propEqual(controller.get('newApp'), newApp, 'should initialize new app');
+
+});
+
+test('sendAppDataToServerSuccessCallback', function () {
+
+  expect(1);
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    sinon.stub(controller.get('appWizardController'), 'hidePopup', Em.K);
+    controller.sendAppDataToServerSuccessCallback();
+  });
+
+  ok(controller.get('appWizardController').hidePopup.calledOnce, 'popup should be closed');
+
+  controller.get('appWizardController').hidePopup.restore();
+
+});
+
+test('sendAppDataToServerCompleteCallback', function () {
+
+  expect(1);
+
+  var controller = this.subject({
+    isSubmitDisabled: true
+  });
+
+  Em.run(function () {
+    controller.sendAppDataToServerCompleteCallback();
+  });
+
+  ok(!controller.get('isSubmitDisabled'), 'Finish button should be enabled');
+
+});
+
+test('sendAppDataToServer', function () {
+
+  var controller = this.subject({
+      newApp: Em.Object.create({
+        appType: {
+          index: 'ACCUMULO',
+          version: '1'
+        },
+        name: 'name',
+        configs: {
+          key: 'value'
+        }
+      }),
+      resourcesFormatted: {
+        components: []
+      }
+    }),
+    cases = [
+      {
+        queueName: null,
+        title: 'queueName not set'
+      },
+      {
+        queueName: ' \n',
+        title: 'empty queueName value'
+      },
+      {
+        queueName: ' queue\n',
+        queue: 'queue',
+        title: 'queueName set correctly'
+      }
+    ];
+
+  Em.run(function () {
+    controller.sendAppDataToServer();
+  });
+
+  ok(controller.get('isSubmitDisabled'), 'Finish button should be disabled');
+  ok(App.ajax.send.calledOnce, 'request to server should be sent');
+
+  cases.forEach(function (item) {
+
+    Em.run(function () {
+      controller.set('newApp.queueName', item.queueName);
+      controller.sendAppDataToServer();
+    });
+
+    var data = {
+      typeName: 'ACCUMULO',
+      typeVersion: '1',
+      name: 'name',
+      resources: {
+        components: []
+      },
+      typeConfigs: {
+        key: 'value'
+      }
+    };
+    if (item.queue) {
+      data.queue = item.queue;
+    }
+
+    propEqual(App.ajax.send.lastCall.args[0].data.data, data, item.title);
+
+  });
+
+});
+
+test('actions.finish', function () {
+
+  var controller = this.subject();
+
+  Em.run(function () {
+    sinon.stub(controller, 'sendAppDataToServer', Em.K);
+    controller.send('finish');
+  });
+
+  ok(controller.sendAppDataToServer.calledOnce, 'data should be sent to server');
+
+  controller.sendAppDataToServer.restore();
+
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_app_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_app_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_app_controller_test.js
index 1dd8b70..b9d632e 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_app_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_app_controller_test.js
@@ -20,7 +20,21 @@ moduleFor('controller:sliderApp', 'App.SliderAppController', {
 
   needs: [
     'component:bs-modal'
-  ]
+  ],
+
+  setup: function () {
+    sinon.stub(Bootstrap.ModalManager, 'register', Em.K);
+    sinon.stub(Bootstrap.ModalManager, 'open', Em.K);
+    sinon.stub(App.ajax, 'send', Em.K);
+    sinon.stub(Bootstrap.ModalManager, 'close', Em.K);
+  },
+
+  teardown: function () {
+    Bootstrap.ModalManager.register.restore();
+    Bootstrap.ModalManager.open.restore();
+    App.ajax.send.restore();
+    Bootstrap.ModalManager.close.restore();
+  }
 
 });
 
@@ -168,8 +182,7 @@ test('destroyButtonEnabled', function () {
 
 test('confirmDestroy', function () {
 
-  var bsRegister = Bootstrap.ModalManager.register,
-    controller = this.subject(),
+  var controller = this.subject(),
     assertionsEqual = [
       {
         propertyName: 'name',
@@ -187,11 +200,13 @@ test('confirmDestroy', function () {
     assertionsDeepEqual = [
       {
         propertyName: 'targetObject',
-        value: controller
+        value: controller,
+        valueFormatted: 'App.SliderAppController'
       },
       {
         propertyName: 'controller',
-        value: controller
+        value: controller,
+        valueFormatted: 'App.SliderAppController'
       },
       {
         propertyName: 'body',
@@ -205,21 +220,17 @@ test('confirmDestroy', function () {
     title = 'modalComponent.{0} should be {1}';
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.register', function (name, component) {
-      this.registeredComponent = component;
-    });
     controller.confirmDestroy();
   });
 
+  ok(Bootstrap.ModalManager.register.calledOnce, 'Bootstrap.ModalManager.register should be executed');
   assertionsEqual.forEach(function (item) {
-    equal(Bootstrap.ModalManager.registeredComponent[item.propertyName], item.value, title.format(item.propertyName, item.value));
+    equal(Bootstrap.ModalManager.register.firstCall.args[1][item.propertyName], item.value, title.format(item.propertyName, item.value));
   });
   assertionsDeepEqual.forEach(function (item) {
-    deepEqual(Bootstrap.ModalManager.registeredComponent[item.propertyName], item.value, title.format(item.propertyName, item.value));
+    deepEqual(Bootstrap.ModalManager.register.firstCall.args[1][item.propertyName], item.value, title.format(item.propertyName, item.valueFormatted || item.value));
   });
 
-  Bootstrap.set('ModalManager.register', bsRegister);
-
 });
 
 test('tryDoAction', function () {
@@ -306,8 +317,7 @@ test('validateGroupedComponents', function () {
 
 test('flex', function () {
 
-  var bsOpen = Bootstrap.ModalManager.open,
-    controller = this.subject({
+  var controller = this.subject({
       appType: {
         components: [
           Em.Object.create({
@@ -326,15 +336,12 @@ test('flex', function () {
     });
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.open', Em.K);
     controller.flex();
   });
 
   equal(controller.get('groupedComponents')[0].count, 1, 'count should be incremented');
   equal(controller.get('groupedComponents')[1].count, 0, 'count shouldn\'t be incremented');
 
-  Bootstrap.set('ModalManager.open', bsOpen);
-
 });
 
 test('mapComponentsForFlexRequest', function () {
@@ -401,8 +408,7 @@ test('actionErrorCallback', function () {
 
 test('actions.submitFlex', function () {
 
-  var bsClose = Bootstrap.ModalManager.close,
-    controller = this.subject({
+  var controller = this.subject({
       model: Em.Object.create({
         id: 0,
         name: 'n'
@@ -414,15 +420,12 @@ test('actions.submitFlex', function () {
     });
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.close', Em.K);
     controller.send('submitFlex');
   });
 
   equal(controller.get('groupedComponents.length'), 0, 'should clear grouped components');
   ok(!controller.get('groupedComponentsHaveErrors'), 'should clear components errors');
 
-  Bootstrap.set('ModalManager.close', bsClose);
-
 });
 
 test('actions.closeFlex', function () {
@@ -443,8 +446,7 @@ test('actions.closeFlex', function () {
 
 test('modalConfirmed', function () {
 
-  var bsClose = Bootstrap.ModalManager.close,
-    controller = this.subject({
+  var controller = this.subject({
       confirmChecked: true,
       currentAction: 'customMethod',
       customMethod: function () {
@@ -453,39 +455,31 @@ test('modalConfirmed', function () {
     });
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.close', Em.K);
     controller.send('modalConfirmed');
   });
 
   deepEqual(controller.get('groupedComponents'), [{}], 'currentAction should be executed');
   ok(!controller.get('confirmChecked'), 'confirmChecked should be false');
 
-  Bootstrap.set('ModalManager.close', bsClose);
-
 });
 
 test('modalCanceled', function () {
 
-  var bsClose = Bootstrap.ModalManager.close,
-    controller = this.subject({
+  var controller = this.subject({
       confirmChecked: true
     });
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.close', Em.K);
     controller.send('modalCanceled');
   });
 
   ok(!controller.get('confirmChecked'), 'confirmChecked should be false');
 
-  Bootstrap.set('ModalManager.close', bsClose);
-
 });
 
 test('openModal', function () {
 
-  var bsOpen = Bootstrap.ModalManager.open,
-    cases = [
+  var cases = [
       {
         options: {
           action: 'customMethod'
@@ -512,7 +506,6 @@ test('openModal', function () {
     });
 
   Em.run(function () {
-    Bootstrap.set('ModalManager.open', Em.K);
     controller.send('openModal', {
       action: 'customMethod'
     });
@@ -530,8 +523,6 @@ test('openModal', function () {
 
   });
 
-  Bootstrap.set('ModalManager.open', bsOpen);
-
 });
 
 test('quickLinksOrdered', function() {

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_apps_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_apps_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_apps_controller_test.js
index b79ff1a..3462d22 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_apps_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_apps_controller_test.js
@@ -16,12 +16,21 @@
  * limitations under the License.
  */
 
-moduleFor('controller:sliderApps', 'App.SliderAppsController');
+moduleFor('controller:sliderApps', 'App.SliderAppsController', {
+
+  setup: function () {
+    sinon.stub(Bootstrap.ModalManager, 'open', Em.K);
+  },
+
+  teardown: function () {
+    Bootstrap.ModalManager.open.restore();
+  }
+
+});
 
 test('showUnavailableAppsPopup', function () {
 
-  var bsOpen = Bootstrap.ModalManager.open,
-    cases = [
+  var cases = [
       {
         message: 'message',
         result: 'message',
@@ -39,14 +48,11 @@ test('showUnavailableAppsPopup', function () {
   cases.forEach(function (item) {
 
     Em.run(function () {
-      Bootstrap.set('ModalManager.open', Em.K);
       controller.showUnavailableAppsPopup(item.message);
     });
 
     equal(controller.get('errorMessage'), item.result, item.title);
 
-    Bootstrap.set('ModalManager.open', bsOpen);
-
   });
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/ed616f1d/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_controller_test.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_controller_test.js b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_controller_test.js
index 308f455..e31d266 100644
--- a/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_controller_test.js
+++ b/contrib/views/slider/src/main/resources/ui/test/unit/controllers/slider_controller_test.js
@@ -72,49 +72,41 @@ test('getViewDisplayParametersSuccessCallback', function () {
 
 test('getParametersFromViewPropertiesSuccessCallback', function () {
 
-  var storeAll = App.SliderApp.store.all,
-    controller = this.subject();
+  var controller = this.subject();
 
   Em.run(function () {
-    App.SliderApp.store.all = function () {
-      return properties;
-    };
+    sinon.stub(App.SliderApp.store, 'all').returns(properties);
     controller.getParametersFromViewPropertiesSuccessCallback({
       parameters: {
         'site.global.metric_collector_lib': 'file:///usr/lib/ambari-metrics-hadoop-sink/ambari-metrics-hadoop-sink.jar',
         'site.global.metric_collector_host': 'h2',
-        'site.global.metric_collector_port': '6188',
+        'site.global.metric_collector_port': '6188'
       },
       validations: [{}, {}]
     });
+    App.SliderApp.store.all.restore();
   });
 
   equal(App.get('metricsHost'), 'h2', 'should set metrics server host');
   equal(App.get('metricsPort'), '6188', 'should set metrics server port');
   equal(App.get('metricsLibPath'), 'file:///usr/lib/ambari-metrics-hadoop-sink/ambari-metrics-hadoop-sink.jar', 'should set metrics lib path');
 
-  App.SliderApp.store.set('all', storeAll);
-
 });
 
  test('initMetricsServerProperties', function () {
 
-   var storeAll = App.SliderApp.store.all,
-     controller = this.subject();
+   var controller = this.subject();
 
    Em.run(function () {
-     App.SliderApp.store.all = function () {
-       return properties;
-     };
+     sinon.stub(App.SliderApp.store, 'all').returns(properties);
      controller.initMetricsServerProperties();
+     App.SliderApp.store.all.restore();
    });
 
    equal(App.get('metricsHost'), 'h2', 'should set metrics server host');
    equal(App.get('metricsPort'), '6188', 'should set metrics server port');
    equal(App.get('metricsLibPath'), 'file:///usr/lib/ambari-metrics-hadoop-sink/ambari-metrics-hadoop-sink.jar', 'should set metrics lib path');
 
-   App.SliderApp.store.set('all', storeAll);
-
  });
 
 test('finishSliderConfiguration', function () {