← Back to team overview

yellow team mailing list archive

[Merge] lp:~hazmat/juju-gui/reliable-test into lp:juju-gui

 

Kapil Thangavelu has proposed merging lp:~hazmat/juju-gui/reliable-test into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)

For more details, see:
https://code.launchpad.net/~hazmat/juju-gui/reliable-test/+merge/141197

Make tests more reliable.

- Disable async loading for yui to ensure app code is loaded before tests.
- Yank yui loader closures around tests, this is an anti-pattern.
- Any test file can be run in isolation now.
- Re-order test loading into alphabetical order to detect/correct collisions.
- Enable using firefox for tests via test loader fix.
- WIP enable using phantomjs for running tests on cli.
- Make topology-mega.js more foregiving on its (there's a event subscription leak here.)
- Fix event subscription leak in charm-panel code.

https://codereview.appspot.com/7003054/

-- 
https://code.launchpad.net/~hazmat/juju-gui/reliable-test/+merge/141197
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~hazmat/juju-gui/reliable-test into lp:juju-gui.
=== modified file 'app/app.js'
--- app/app.js	2012-12-19 13:45:10 +0000
+++ app/app.js	2012-12-24 03:49:21 +0000
@@ -745,18 +745,23 @@
 
 }, '0.5.2', {
   requires: [
+    'juju-charm-models',
+    'juju-charm-panel',
+    'juju-charm-store',
     'juju-models',
-    'juju-charm-models',
+    'juju-notifications',
+
+    // This alias doesn't seem to work, including refs by hand.
+    'juju-controllers',
+    'juju-notification-controller',
+    'juju-env',
+
     'juju-views',
-    'juju-controllers',
-    'juju-view-charm-search',
     'io',
     'json-parse',
     'app-base',
     'app-transitions',
     'base',
     'node',
-    'model',
-    'juju-charm-panel',
-    'juju-charm-store']
+    'model']
 });

=== modified file 'app/modules-debug.js'
--- app/modules-debug.js	2012-12-21 12:52:30 +0000
+++ app/modules-debug.js	2012-12-24 03:49:21 +0000
@@ -12,9 +12,10 @@
   filter: 'debug',
   // Set "true" for verbose logging of YUI
   debug: false,
+
   base: '/juju-ui/assets/javascripts/yui/',
   // Use Rollups
-  combine: false,
+  combine: true,
 
   groups: {
     gallery: {
@@ -80,6 +81,7 @@
         'juju-topology': {
           fullpath: '/juju-ui/views/topology/topology.js'
         },
+
         'juju-view-utils': {
           fullpath: '/juju-ui/views/utils.js'
         },
@@ -158,7 +160,9 @@
         },
 
         'juju-controllers': {
-          use: ['juju-env', 'juju-charm-store',
+          use: [
+            'juju-env',
+            'juju-charm-store',
             'juju-notification-controller']
         },
 

=== modified file 'app/templates/overview.handlebars'
--- app/templates/overview.handlebars	2012-12-17 15:39:40 +0000
+++ app/templates/overview.handlebars	2012-12-24 03:49:21 +0000
@@ -1,5 +1,5 @@
 <div class="topology">
-    <div class="topology-canvas crosshatch-background">
+    <div class="crosshatch-background topology-canvas">
         <div class="environment-menu" id="service-menu">
             <div class="triangle">&nbsp;</div>
             <ul>

=== modified file 'app/views/charm-panel.js'
--- app/views/charm-panel.js	2012-12-14 20:25:16 +0000
+++ app/views/charm-panel.js	2012-12-24 03:49:21 +0000
@@ -1056,11 +1056,11 @@
     setPanel({name: 'charms'});
 
     // Update position if we resize the window.
-    Y.on('windowresize', function(e) {
+    subscriptions.push(Y.on('windowresize', function(e) {
       if (isPanelVisible) {
         updatePanelPosition();
       }
-    });
+    }));
 
     /**
      * Hide the charm panel.
@@ -1229,6 +1229,7 @@
   requires: [
     'view',
     'juju-view-utils',
+    'juju-templates',
     'node',
     'handlebars',
     'event-hover',

=== modified file 'app/views/environment.js'
--- app/views/environment.js	2012-12-19 13:45:10 +0000
+++ app/views/environment.js	2012-12-24 03:49:21 +0000
@@ -34,10 +34,10 @@
 
           //If we need the initial HTML template
           // take care of that.
-          if (!this.svg) {
+          if (!this.rendered) {
             EnvironmentView.superclass.render.apply(this, arguments);
             container.setHTML(Templates.overview());
-            this.svg = container.one('.topology');
+            this.rendered = true;
           }
 
           if (!topo) {
@@ -78,13 +78,10 @@
   requires: ['juju-templates',
              'juju-view-utils',
              'juju-models',
-             'd3',
-             'd3-components',
+             'juju-topology',
+             'svg-layouts',
              'base-build',
              'handlebars-base',
              'node',
-             'svg-layouts',
-             'event-resize',
-             'slider',
              'view']
 });

=== modified file 'app/views/topology/mega.js'
--- app/views/topology/mega.js	2012-12-20 13:34:43 +0000
+++ app/views/topology/mega.js	2012-12-24 03:49:21 +0000
@@ -1126,6 +1126,12 @@
           svg = container.one('svg'),
           canvas = container.one('.topology-canvas');
 
+
+      // Test band-aid due to lack of cleanup.
+      if (!canvas) {
+        return;
+      }
+
       topo.fire('beforePageSizeRecalculation');
       // Get the canvas out of the way so we can calculate the size
       // correctly (the canvas contains the svg).  We want it to be the
@@ -1566,8 +1572,6 @@
     'd3',
     'd3-components',
     'juju-templates',
-    'node',
-    'event',
     'juju-models',
     'juju-env'
   ]

=== modified file 'app/views/topology/panzoom.js'
--- app/views/topology/panzoom.js	2012-12-20 16:23:32 +0000
+++ app/views/topology/panzoom.js	2012-12-24 03:49:21 +0000
@@ -181,10 +181,11 @@
   views.PanZoomModule = PanZoomModule;
 }, '0.1.0', {
   requires: [
+    'node',
+    'event',
+    'slider',
     'd3',
     'd3-components',
-    'node',
-    'event',
     'juju-models',
     'juju-env'
   ]

=== modified file 'app/views/utils.js'
--- app/views/utils.js	2012-12-20 21:59:21 +0000
+++ app/views/utils.js	2012-12-24 03:49:21 +0000
@@ -118,6 +118,8 @@
           time: noop,
           timeEnd: noop,
           log: noop,
+          info: noop,
+          error: noop,
           debug: noop
         };
 

=== modified file 'package.json'
--- package.json	2012-12-11 04:11:39 +0000
+++ package.json	2012-12-24 03:49:21 +0000
@@ -15,8 +15,9 @@
     "cryptojs": ">= 2.5.3"
   },
   "devDependencies": {
-    "d3": ">=2.10.1",
+    "d3": "<3.0.0",
     "yui": ">=3.7.0",
+    "yeti": ">=0.2.0",
     "mocha": "1.5.x",
     "express": "3.x",
     "expect.js": "0.1.2",

=== modified file 'test/index.html'
--- test/index.html	2012-12-17 14:53:46 +0000
+++ test/index.html	2012-12-24 03:49:21 +0000
@@ -3,49 +3,64 @@
 <head>
   <meta charset="utf-8">
   <link rel="stylesheet" href="assets/mocha.css">
+
+
+  <!-- Load test runner/environment -->
+  <script src="assets/chai.js"></script>
+  <script src="assets/mocha.js"></script>
+  <script>
+    var assert = chai.assert,
+        expect = chai.expect;
+
+    var should = chai.should();
+    console.log('mocha setup');
+    mocha.setup({'ui': 'bdd', 'ignoreLeaks': false, 'timeout': 20000})
+    console.log('mocha setup done');
+  </script>
+
+  <!-- Load up YUI base, app modules, and test utils -->
+  <!-- Since only the tests depend on these files and the prod tests disable
+       the YUI loader, we have to include them manually here. -->
   <script src="/juju-ui/assets/modules.js"></script>
   <script src="/juju-ui/assets/all-yui.js"></script>
-  <!-- Since only the tests depend on these files and the prod tests disable
-    the YUI loader, we have to include them manually here. -->
   <script src="/juju-ui/assets/event-simulate.js"></script>
   <script src="/juju-ui/assets/node-event-simulate.js"></script>
-  <script src="assets/chai.js"></script>
-  <script src="assets/mocha.js"></script>
   <script src="utils.js"></script>
-  <script>
-    var assert = chai.assert,
-        expect = chai.expect
-        should = chai.should();
-    mocha.setup({'ui': 'bdd', 'ignoreLeaks': false})
-  </script>
-
+
+
+  <!-- Tests (Alphabetical)-->
+  <script src="test_app.js"></script>
+  <script src="test_app_hotkeys.js"></script>
+  <script src="test_application_notifications.js"></script>
+  <script src="test_charm_collection_view.js"></script>
+  <script src="test_charm_configuration.js"></script>
+  <script src="test_charm_panel.js"></script>
+  <script src="test_charm_store.js"></script>
+  <script src="test_charm_view.js"></script>
+  <script src="test_console.js"></script> 
   <script src="test_d3_components.js"></script>
-  <script src="test_topology.js"></script>
+  <script src="test_environment_view.js"></script>
   <script src="test_env.js"></script>
+  <script src="test_endpoints.js"></script>
   <script src="test_model.js"></script>
   <script src="test_notifications.js"></script>
-  <script src="test_app.js"></script>
-  <script src="test_unit_view.js"></script>
-  <script src="test_charm_collection_view.js"></script>
-  <script src="test_charm_view.js"></script>
-  <script src="test_environment_view.js"></script>
+  <script src="test_notifier_widget.js"></script>
+  <script src="test_topology.js"></script>
   <script src="test_service_config_view.js"></script>
   <script src="test_service_view.js"></script>
+  <script src="test_unit_view.js"></script>
   <script src="test_utils.js"></script>
-  <script src="test_charm_panel.js"></script>
-  <script src="test_charm_configuration.js"></script>
-  <script src="test_console.js"></script>
-  <script src="test_endpoints.js"></script>
-  <script src="test_application_notifications.js"></script>
-  <script src="test_charm_store.js"></script>
-  <script src="test_app_hotkeys.js"></script>
-  <script src="test_notifier_widget.js"></script>
+
 
   <script>
-  YUI().use('node', 'event', function(Y) {
+  YUI({'async': false}).use('node', 'event', function(Y) {
      Y.on('domready', function() {
 
      var config = GlobalConfig;
+
+     config.async = false;
+     config.consoleEnabled = true;
+
      for (group in config.groups) {
           var group = config.groups[group];
          for (m in group.modules) {
@@ -54,11 +69,12 @@
               continue
             }
             resource.fullpath = resource.fullpath.replace(
-              '/juju-ui/', '../juju-ui/', 1);
+              '/juju-ui/', '../juju-ui/');
          }
      }
-     // Load before test runner
-     mocha.run();
+     // Run the tests.
+     if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
+     else { mocha.run(); }
      });
   });
   </script>

=== modified file 'test/test_app.js'
--- test/test_app.js	2012-12-03 20:24:44 +0000
+++ test/test_app.js	2012-12-24 03:49:21 +0000
@@ -31,9 +31,18 @@
   return app;
 }
 
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
+(function() {
+
   describe('Application basics', function() {
-    var app, container;
+    var Y, app, container;
+
+    before(function(done) {
+      Y = YUI(GlobalConfig).use(
+          ['juju-gui', 'juju-tests-utils'],
+          function(Y) {
+            done();
+          });
+    });
 
     beforeEach(function() {
       container = Y.one('#main')
@@ -116,14 +125,22 @@
     });
 
   });
-});
-
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
+})();
+
+
+
+(function() {
+
   describe('Application Connection State', function() {
-    var container;
+    var container, Y;
 
-    before(function() {
-      container = Y.Node.create('<div id="test" class="container"></div>');
+    before(function(done) {
+      Y = YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'],
+          function(Y) {
+            container = Y.Node.create(
+                '<div id="test" class="container"></div>');
+            done();
+          });
     });
 
     it('should be able to handle env connection status changes', function() {
@@ -134,7 +151,6 @@
           reset_called = false,
           noop = function() {return this;};
 
-
       // mock the db
       app.db = {
         // mock out notifications
@@ -162,15 +178,23 @@
     });
 
   });
-});
-
-YUI(GlobalConfig).use(['juju-models', 'juju-gui', 'datasource-local',
-  'juju-tests-utils', 'json-stringify'], function(Y) {
+})();
+
+
+(function() {
+
   describe('Application prefetching', function() {
-    var models, conn, env, app, container, charm_store, data, juju;
+    var Y, models, conn, env, app, container, charm_store, data, juju;
 
-    before(function() {
-      models = Y.namespace('juju.models');
+    before(function(done) {
+      console.log('Loading App prefetch test code');
+      Y = YUI(GlobalConfig).use(
+          ['juju-gui', 'datasource-local',
+           'juju-views', 'juju-templates',
+           'juju-tests-utils', 'json-stringify'], function(Y) {
+            models = Y.namespace('juju.models');
+            done();
+          });
     });
 
     beforeEach(function() {
@@ -237,4 +261,4 @@
       get_endpoints_count.should.equal(2);
     });
   });
-});
+})();

=== modified file 'test/test_app_hotkeys.js'
--- test/test_app_hotkeys.js	2012-11-27 14:25:32 +0000
+++ test/test_app_hotkeys.js	2012-12-24 03:49:21 +0000
@@ -1,11 +1,11 @@
 'use strict';
 
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils', 'node-event-simulate'],
-    function(Y) {
-      describe('application hotkeys', function() {
-        var app, container, windowNode;
+describe('application hotkeys', function() {
+  var app, container, windowNode, Y;
 
-        before(function(done) {
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-gui', 'juju-tests-utils', 'node-event-simulate'], function(Y) {
           var env = {
             after: function() {},
             get: function() {},
@@ -21,43 +21,45 @@
           done();
         });
 
-        beforeEach(function() {
-          container = Y.Node.create('<div/>');
-          Y.one('#main').append(container);
-          app.render();
-        });
-
-        afterEach(function() {
-          container.remove(true);
-        });
-
-        it('should listen for alt-S events', function() {
-          var searchInput = Y.Node.create('<input/>');
-          searchInput.set('id', 'charm-search-field');
-          container.append(searchInput);
-          windowNode.simulate('keydown', {
-            keyCode: 83, // "S" key.
-            altKey: true
-          });
-          // Did charm-search-field get the focus?
-          assert.equal(searchInput, Y.one(document.activeElement));
-        });
-
-        it('should listen for alt-E events', function() {
-          var altEtriggered = false;
-          app.on('navigateTo', function(ev) {
-            if (ev && ev.url === '/') {
-              altEtriggered = true;
-            }
-            // Avoid URL change performed by additional listeners.
-            ev.stopImmediatePropagation();
-          });
-          windowNode.simulate('keydown', {
-            keyCode: 69, // "E" key.
-            altKey: true
-          });
-          assert.isTrue(altEtriggered);
-        });
-
-      });
-    });
+  });
+
+  beforeEach(function() {
+    container = Y.Node.create('<div/>');
+    Y.one('#main').append(container);
+    app.render();
+  });
+
+  afterEach(function() {
+    container.remove(true);
+  });
+
+  it('should listen for alt-S events', function() {
+    var searchInput = Y.Node.create('<input/>');
+    searchInput.set('id', 'charm-search-field');
+    container.append(searchInput);
+    windowNode.simulate('keydown', {
+      keyCode: 83, // "S" key.
+      altKey: true
+    });
+    // Did charm-search-field get the focus?
+    assert.equal(searchInput, Y.one(document.activeElement));
+  });
+
+  it('should listen for alt-E events', function() {
+    var altEtriggered = false;
+    app.on('navigateTo', function(ev) {
+      if (ev && ev.url === '/') {
+        altEtriggered = true;
+      }
+      // Avoid URL change performed by additional listeners.
+      ev.stopImmediatePropagation();
+    });
+    windowNode.simulate('keydown', {
+      keyCode: 69, // "E" key.
+      altKey: true
+    });
+    assert.isTrue(altEtriggered);
+  });
+
+});
+

=== modified file 'test/test_d3_components.js'
--- test/test_d3_components.js	2012-12-19 13:45:10 +0000
+++ test/test_d3_components.js	2012-12-24 03:49:21 +0000
@@ -32,7 +32,6 @@
           state.cancelled = true;
         }
       });
-
       done();
     });
   });

=== modified file 'test/test_notifications.js'
--- test/test_notifications.js	2012-11-23 16:21:32 +0000
+++ test/test_notifications.js	2012-12-24 03:49:21 +0000
@@ -1,462 +1,477 @@
 'use strict';
 
-YUI(GlobalConfig).use(['juju-gui', 'node-event-simulate', 'juju-tests-utils'],
+describe('notifications', function() {
+  var Y, juju, models, views;
+
+  var default_env = {
+    'result': [
+      ['service', 'add', {
+        'charm': 'cs:precise/wordpress-6',
+        'id': 'wordpress',
+        'exposed': false
+      }],
+      ['service', 'add', {
+        'charm': 'cs:precise/mediawiki-3',
+        'id': 'mediawiki',
+        'exposed': false
+      }],
+      ['service', 'add', {
+        'charm': 'cs:precise/mysql-6',
+        'id': 'mysql'
+      }],
+      ['relation', 'add', {
+        'interface': 'reversenginx',
+        'scope': 'global',
+        'endpoints':
+         [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
+        'id': 'relation-0000000000'
+      }],
+      ['relation', 'add', {
+        'interface': 'mysql',
+        'scope': 'global',
+        'endpoints':
+         [['mysql', {'role': 'server', 'name': 'db'}],
+          ['wordpress', {'role': 'client', 'name': 'db'}]],
+        'id': 'relation-0000000001'
+      }],
+      ['machine', 'add', {
+        'agent-state': 'running',
+        'instance-state': 'running',
+        'id': 0,
+        'instance-id': 'local',
+        'dns-name': 'localhost'
+      }],
+      ['unit', 'add', {
+        'machine': 0,
+        'agent-state': 'started',
+        'public-address': '192.168.122.113',
+        'id': 'wordpress/0'
+      }],
+      ['unit', 'add', {
+        'machine': 0,
+        'agent-state': 'error',
+        'public-address': '192.168.122.222',
+        'id': 'mysql/0'
+      }]
+    ],
+    'op': 'delta'
+  };
+
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use([
+      'juju-models',
+      'juju-views',
+      'juju-gui',
+      'juju-env',
+      'node-event-simulate',
+      'juju-tests-utils'],
+
     function(Y) {
-      describe('notifications', function() {
-        var juju, models, views;
-
-        var default_env = {
-          'result': [
-            ['service', 'add', {
-              'charm': 'cs:precise/wordpress-6',
-              'id': 'wordpress',
-              'exposed': false
-            }],
-            ['service', 'add', {
-              'charm': 'cs:precise/mediawiki-3',
-              'id': 'mediawiki',
-              'exposed': false
-            }],
-            ['service', 'add', {
-              'charm': 'cs:precise/mysql-6',
-              'id': 'mysql'
-            }],
-            ['relation', 'add', {
-              'interface': 'reversenginx',
-              'scope': 'global',
-              'endpoints':
-               [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
-              'id': 'relation-0000000000'
-            }],
-            ['relation', 'add', {
-              'interface': 'mysql',
-              'scope': 'global',
-              'endpoints':
-               [['mysql', {'role': 'server', 'name': 'db'}],
-                ['wordpress', {'role': 'client', 'name': 'db'}]],
-              'id': 'relation-0000000001'
-            }],
-            ['machine', 'add', {
-              'agent-state': 'running',
-              'instance-state': 'running',
-              'id': 0,
-              'instance-id': 'local',
-              'dns-name': 'localhost'
-            }],
-            ['unit', 'add', {
-              'machine': 0,
-              'agent-state': 'started',
-              'public-address': '192.168.122.113',
-              'id': 'wordpress/0'
-            }],
-            ['unit', 'add', {
-              'machine': 0,
-              'agent-state': 'error',
-              'public-address': '192.168.122.222',
-              'id': 'mysql/0'
-            }]
-          ],
-          'op': 'delta'
-        };
-
-
-        before(function() {
-          juju = Y.namespace('juju');
-          models = Y.namespace('juju.models');
-          views = Y.namespace('juju.views');
-        });
-
-        it('must be able to make notification and lists of notifications',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello'
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             notifications = new models.NotificationList();
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-
-             // timestamp should be generated once
-             var ts = note1.get('timestamp');
-             note1.get('timestamp').should.equal(ts);
-             // force an update so we can test ordering
-             // fast execution can result in same timestamp
-             note2.set('timestamp', ts + 1);
-             note2.get('timestamp').should.be.above(ts);
-
-             // defaults as expected
-             note1.get('level').should.equal('info');
-             note2.get('level').should.equal('info');
-             // the sort order on the list should be by
-             // timestamp
-             notifications.get('title').should.eql(['test2', 'test1']);
-           });
-
-        it('must be able to render its view with sample data',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1', message: 'Hello'}),
-             note2 = new models.Notification({
-               title: 'test2', message: 'I said goodnight!'}),
-             notifications = new models.NotificationList(),
-             container = Y.Node.create('<div id="test">'),
-             env = new juju.Environment(),
-             view = new views.NotificationsView({
-                     container: container,
-                     notifications: notifications,
-                     env: env});
-             view.render();
-             // Verify the expected elements appear in the view
-             container.one('#notify-list').should.not.equal(undefined);
-             container.destroy();
-           });
-
-        it('must be able to limit the size of notification events',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello'
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             note3 = new models.Notification({
-               title: 'test3',
-               message: 'Never remember'
-             }),
-             notifications = new models.NotificationList({
-               max_size: 2
-             });
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-
-             // Adding a new notification should pop the oldest from the list
-             // (we exceed max_size)
-             notifications.add(note3);
-             notifications.size().should.equal(2);
-             notifications.get('title').should.eql(['test3', 'test2']);
-           });
-
-        it('must be able to get notifications for a given model',
-           function() {
-             var m = new models.Service({id: 'mediawiki'}),
-             note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello',
-               modelId: m
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             notifications = new models.NotificationList();
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-             notifications.getNotificationsForModel(m).should.eql(
-             [note1]);
-
-           });
-
-        it('must be able to include and show object links', function() {
-          var container = Y.Node.create('<div id="test">'),
-              conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
-              env = new juju.Environment({conn: conn}),
-              app = new Y.juju.App({env: env, container: container}),
-              db = app.db,
-              mw = db.services.create({id: 'mediawiki',
-                                      name: 'mediawiki'}),
-              notifications = db.notifications,
-              view = new views.NotificationsOverview({
-                        container: container,
-                        notifications: notifications,
-                        app: app,
-                        env: env}).render();
-          // we use overview here for testing as it defaults
-          // to showing all notices
-
-          // we can use app's routing table to derive a link
-          notifications.create({title: 'Service Down',
-            message: 'Your service has an error',
-            link: app.getModelURL(mw)
-          });
-          view.render();
-          var link = container.one('.notice').one('a');
-          link.getAttribute('href').should.equal(
-              '/service/mediawiki/');
-          link.getHTML().should.contain('View Details');
-
-
-          // create a new notice passing the link_title
-          notifications.create({title: 'Service Down',
-            message: 'Your service has an error',
-            link: app.getModelURL(mw),
-            link_title: 'Resolve this'
-          });
-          view.render();
-          link = container.one('.notice').one('a');
-          link.getAttribute('href').should.equal(
-              '/service/mediawiki/');
-          link.getHTML().should.contain('Resolve this');
-        });
-
-        it('must be able to evict irrelevant notices', function() {
-          var container = Y.Node.create(
-              '<div id="test" class="container"></div>'),
-              conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
-              env = new juju.Environment({conn: conn}),
-              app = new Y.juju.App({
-                env: env,
-                container: container,
-                viewContainer: container
-              });
-          var environment_delta = default_env;
-
-          var notifications = app.db.notifications,
-              view = new views.NotificationsView({
-                container: container,
-                notifications: notifications,
-                env: app.env}).render();
-
-
-          app.env.dispatch_result(environment_delta);
-
-
-          notifications.size().should.equal(7);
-          // we have one unit in error
-          view.getShowable().length.should.equal(1);
-
-          // now fire another delta event marking that node as
-          // started
-          app.env.dispatch_result({result: [['unit', 'change', {
-            'machine': 0,
-            'agent-state': 'started',
-            'public-address': '192.168.122.222',
-            'id': 'mysql/0'
-          }]], op: 'delta'});
-          notifications.size().should.equal(8);
-          // This should have evicted the prior notice from seen
-          view.getShowable().length.should.equal(0);
-        });
-
-        it('must properly construct title and message based on level from ' +
-           'event data',
-           function() {
-             var container = Y.Node.create(
-             '<div id="test" class="container"></div>'),
-             app = new Y.juju.App({
-               container: container,
-               viewContainer: container
-             });
-             var environment_delta = {
-               'result': [
-                 ['service', 'add', {
-                   'charm': 'cs:precise/wordpress-6',
-                   'id': 'wordpress'
-                 }],
-                 ['service', 'add', {
-                   'charm': 'cs:precise/mediawiki-3',
-                   'id': 'mediawiki'
-                 }],
-                 ['service', 'add', {
-                   'charm': 'cs:precise/mysql-6',
-                   'id': 'mysql'
-                 }],
-                 ['unit', 'add', {
-                   'agent-state': 'install-error',
-                   'id': 'wordpress/0'
-                 }],
-                 ['unit', 'add', {
-                   'agent-state': 'error',
-                   'public-address': '192.168.122.222',
-                   'id': 'mysql/0'
-                 }],
-                 ['unit', 'add', {
-                   'public-address': '192.168.122.222',
-                   'id': 'mysql/2'
-                 }]
-               ],
-               'op': 'delta'
-             };
-
-             var notifications = app.db.notifications,
-             view = new views.NotificationsView({
-               container: container,
-               notifications: notifications,
-               app: app,
-               env: app.env}).render();
-
-             app.env.dispatch_result(environment_delta);
-
-             notifications.size().should.equal(6);
-             // we have one unit in error
-             var showable = view.getShowable();
-             showable.length.should.equal(2);
-             // The first showable notification should indicate an error.
-             showable[0].level.should.equal('error');
-             showable[0].title.should.equal('Error with mysql/0');
-             showable[0].message.should.equal('Agent-state = error.');
-             // The second showable notification should also indicate an error.
-             showable[1].level.should.equal('error');
-             showable[1].title.should.equal('Error with wordpress/0');
-             showable[1].message.should.equal('Agent-state = install-error.');
-             // The first non-error notice should have an 'info' level and less
-             // severe messaging.
-             var notice = notifications.item(0);
-             notice.get('level').should.equal('info');
-             notice.get('title').should.equal('Problem with mysql/2');
-             notice.get('message').should.equal('');
-           });
-
-
-        it('should open on click and close on clickoutside', function(done) {
-          var container = Y.Node.create('<div id="test-container" ' +
-              'style="display: none" class="container"/>'),
-              notifications = new models.NotificationList(),
-              env = new juju.Environment(),
-              view = new views.NotificationsView({
-                container: container,
-                notifications: notifications,
-                env: env}).render(),
-              indicator;
-
-          Y.one('body').append(container);
-          notifications.add({title: 'testing', 'level': 'error'});
-          indicator = container.one('#notify-indicator');
-
-          indicator.simulate('click');
-          indicator.ancestor().hasClass('open').should.equal(true);
-
-          Y.one('body').simulate('click');
-          indicator.ancestor().hasClass('open').should.equal(false);
-
-          container.remove();
-          done();
-        });
-      });
-
-      describe('changing notifications to words', function() {
-        var juju;
-
-        before(function() {
-          juju = Y.namespace('juju');
-        });
-
-        it('should correctly translate notification operations into English',
-           function() {
-             assert.equal(juju._changeNotificationOpToWords('add'), 'created');
-             assert.equal(juju._changeNotificationOpToWords('remove'),
-                 'removed');
-             assert.equal(juju._changeNotificationOpToWords('not-an-op'),
-                 'changed');
-           });
-      });
-
-      describe('relation notifications', function() {
-        var juju;
-
-        before(function() {
-          juju = Y.namespace('juju');
-        });
-
-        it('should produce reasonable titles', function() {
-          assert.equal(
-              juju._relationNotifications.title(undefined, 'add'),
-              'Relation created');
-          assert.equal(
-              juju._relationNotifications.title(undefined, 'remove'),
-              'Relation removed');
-        });
-
-        it('should generate messages about two-party relations', function() {
-          var changeData =
-              { endpoints:
-                    [['endpoint0', {name: 'relation0'}],
-                     ['endpoint1', {name: 'relation1'}]]};
-          assert.equal(
-              juju._relationNotifications.message(undefined, 'add',
-                  changeData), 'Relation between endpoint0 (relation type ' +
-                  '"relation0") and endpoint1 (relation type "relation1") ' +
-                  'was created');
-        });
-
-        it('should generate messages about one-party relations', function() {
-          var changeData =
-              { endpoints:
-                    [['endpoint1', {name: 'relation1'}]]};
-          assert.equal(
-              juju._relationNotifications.message(undefined, 'add',
-                  changeData), 'Relation with endpoint1 (relation type ' +
-                  '"relation1") was created');
-        });
-      });
-
-      describe('notification visual feedback', function() {
-        var env, models, notifications, notificationsView, notifierBox, views;
-
-        before(function() {
+      juju = Y.namespace('juju');
+      models = Y.namespace('juju.models');
+      views = Y.namespace('juju.views');
+      done();
+    });
+  });
+
+  it('must be able to make notification and lists of notifications',
+     function() {
+        var note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello'
+        }),
+            note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+            }),
+            notifications = new models.NotificationList();
+
+        notifications.add([note1, note2]);
+        notifications.size().should.equal(2);
+
+        // timestamp should be generated once
+        var ts = note1.get('timestamp');
+        note1.get('timestamp').should.equal(ts);
+        // force an update so we can test ordering
+        // fast execution can result in same timestamp
+        note2.set('timestamp', ts + 1);
+        note2.get('timestamp').should.be.above(ts);
+
+        // defaults as expected
+        note1.get('level').should.equal('info');
+        note2.get('level').should.equal('info');
+        // the sort order on the list should be by
+        // timestamp
+        notifications.get('title').should.eql(['test2', 'test1']);
+     });
+
+  it('must be able to render its view with sample data',
+     function() {
+       var note1 = new models.Notification({
+         title: 'test1', message: 'Hello'}),
+           note2 = new models.Notification({
+         title: 'test2', message: 'I said goodnight!'}),
+           notifications = new models.NotificationList(),
+           container = Y.Node.create('<div id="test">'),
+           env = new juju.Environment(),
+           view = new views.NotificationsView({
+                   container: container,
+                   notifications: notifications,
+                   env: env});
+       view.render();
+       // Verify the expected elements appear in the view
+       container.one('#notify-list').should.not.equal(undefined);
+       container.destroy();
+     });
+
+  it('must be able to limit the size of notification events',
+     function() {
+       var note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello'
+       }),
+           note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+       }),
+           note3 = new models.Notification({
+         title: 'test3',
+         message: 'Never remember'
+       }),
+           notifications = new models.NotificationList({
+         max_size: 2
+       });
+
+       notifications.add([note1, note2]);
+       notifications.size().should.equal(2);
+
+       // Adding a new notification should pop the oldest from the list (we
+       // exceed max_size)
+       notifications.add(note3);
+       notifications.size().should.equal(2);
+       notifications.get('title').should.eql(['test3', 'test2']);
+     });
+
+  it('must be able to get notifications for a given model',
+     function() {
+       var m = new models.Service({id: 'mediawiki'}),
+           note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello',
+         modelId: m
+       }),
+           note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+       }),
+           notifications = new models.NotificationList();
+
+       notifications.add([note1, note2]);
+       notifications.size().should.equal(2);
+       notifications.getNotificationsForModel(m).should.eql(
+       [note1]);
+
+     });
+
+  it('must be able to include and show object links', function() {
+    var container = Y.Node.create('<div id="test">'),
+        conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
+        env = new juju.Environment({conn: conn}),
+        app = new Y.juju.App({env: env, container: container}),
+        db = app.db,
+        mw = db.services.create({id: 'mediawiki',
+                                    name: 'mediawiki'}),
+        notifications = db.notifications,
+        view = new views.NotificationsOverview({
+                      container: container,
+                      notifications: notifications,
+                      app: app,
+                      env: env}).render();
+    // we use overview here for testing as it defaults
+    // to showing all notices
+
+    // we can use app's routing table to derive a link
+    notifications.create({title: 'Service Down',
+      message: 'Your service has an error',
+      link: app.getModelURL(mw)
+    });
+    view.render();
+    var link = container.one('.notice').one('a');
+    link.getAttribute('href').should.equal(
+        '/service/mediawiki/');
+    link.getHTML().should.contain('View Details');
+
+
+    // create a new notice passing the link_title
+    notifications.create({title: 'Service Down',
+      message: 'Your service has an error',
+      link: app.getModelURL(mw),
+      link_title: 'Resolve this'
+    });
+    view.render();
+    link = container.one('.notice').one('a');
+    link.getAttribute('href').should.equal(
+        '/service/mediawiki/');
+    link.getHTML().should.contain('Resolve this');
+  });
+
+  it('must be able to evict irrelevant notices', function() {
+    var container = Y.Node.create(
+        '<div id="test" class="container"></div>'),
+        conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
+        env = new juju.Environment({conn: conn}),
+        app = new Y.juju.App({
+          env: env,
+          container: container,
+          viewContainer: container
+        });
+    var environment_delta = default_env;
+
+    var notifications = app.db.notifications,
+        view = new views.NotificationsView({
+          container: container,
+          notifications: notifications,
+          env: app.env}).render();
+
+
+    app.env.dispatch_result(environment_delta);
+
+
+    notifications.size().should.equal(7);
+    // we have one unit in error
+    view.getShowable().length.should.equal(1);
+
+    // now fire another delta event marking that node as
+    // started
+    app.env.dispatch_result({result: [['unit', 'change', {
+      'machine': 0,
+      'agent-state': 'started',
+      'public-address': '192.168.122.222',
+      'id': 'mysql/0'
+    }]], op: 'delta'});
+    notifications.size().should.equal(8);
+    // This should have evicted the prior notice from seen
+    view.getShowable().length.should.equal(0);
+  });
+
+  it('must properly construct title and message based on level from ' +
+     'event data',
+     function() {
+       var container = Y.Node.create(
+       '<div id="test" class="container"></div>'),
+       app = new Y.juju.App({
+         container: container,
+         viewContainer: container
+       });
+       var environment_delta = {
+         'result': [
+           ['service', 'add', {
+             'charm': 'cs:precise/wordpress-6',
+             'id': 'wordpress'
+           }],
+           ['service', 'add', {
+             'charm': 'cs:precise/mediawiki-3',
+             'id': 'mediawiki'
+           }],
+           ['service', 'add', {
+             'charm': 'cs:precise/mysql-6',
+             'id': 'mysql'
+           }],
+           ['unit', 'add', {
+             'agent-state': 'install-error',
+             'id': 'wordpress/0'
+           }],
+           ['unit', 'add', {
+             'agent-state': 'error',
+             'public-address': '192.168.122.222',
+             'id': 'mysql/0'
+           }],
+           ['unit', 'add', {
+             'public-address': '192.168.122.222',
+             'id': 'mysql/2'
+           }]
+         ],
+         'op': 'delta'
+       };
+
+       var notifications = app.db.notifications,
+       view = new views.NotificationsView({
+         container: container,
+         notifications: notifications,
+         app: app,
+         env: app.env}).render();
+
+       app.env.dispatch_result(environment_delta);
+
+       notifications.size().should.equal(6);
+       // we have one unit in error
+       var showable = view.getShowable();
+       showable.length.should.equal(2);
+       // The first showable notification should indicate an error.
+       showable[0].level.should.equal('error');
+       showable[0].title.should.equal('Error with mysql/0');
+       showable[0].message.should.equal('Agent-state = error.');
+       // The second showable notification should also indicate an error.
+       showable[1].level.should.equal('error');
+       showable[1].title.should.equal('Error with wordpress/0');
+       showable[1].message.should.equal('Agent-state = install-error.');
+       // The first non-error notice should have an 'info' level and less
+       // severe messaging.
+       var notice = notifications.item(0);
+       notice.get('level').should.equal('info');
+       notice.get('title').should.equal('Problem with mysql/2');
+       notice.get('message').should.equal('');
+     });
+
+
+  it('should open on click and close on clickoutside', function(done) {
+    var container = Y.Node.create(
+        '<div id="test-container" style="display: none" class="container"/>'),
+        notifications = new models.NotificationList(),
+        env = new juju.Environment(),
+        view = new views.NotificationsView({
+          container: container,
+          notifications: notifications,
+          env: env}).render(),
+        indicator;
+
+    Y.one('body').append(container);
+    notifications.add({title: 'testing', 'level': 'error'});
+    indicator = container.one('#notify-indicator');
+
+    indicator.simulate('click');
+    indicator.ancestor().hasClass('open').should.equal(true);
+
+    Y.one('body').simulate('click');
+    indicator.ancestor().hasClass('open').should.equal(false);
+
+    container.remove();
+    done();
+  });
+
+});
+
+
+describe('changing notifications to words', function() {
+  var Y, juju;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-notification-controller'],
+        function(Y) {
+          juju = Y.namespace('juju');
+          done();
+        });
+  });
+
+  it('should correctly translate notification operations into English',
+     function() {
+       assert.equal(juju._changeNotificationOpToWords('add'), 'created');
+       assert.equal(juju._changeNotificationOpToWords('remove'), 'removed');
+       assert.equal(juju._changeNotificationOpToWords('not-an-op'), 'changed');
+     });
+});
+
+describe('relation notifications', function() {
+  var Y, juju;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-notification-controller'],
+        function(Y) {
+          juju = Y.namespace('juju');
+          done();
+        });
+  });
+
+  it('should produce reasonable titles', function() {
+    assert.equal(
+        juju._relationNotifications.title(undefined, 'add'),
+        'Relation created');
+    assert.equal(
+        juju._relationNotifications.title(undefined, 'remove'),
+        'Relation removed');
+  });
+
+  it('should generate messages about two-party relations', function() {
+    var changeData =
+        { endpoints:
+              [['endpoint0', {name: 'relation0'}],
+                ['endpoint1', {name: 'relation1'}]]};
+    assert.equal(
+        juju._relationNotifications.message(undefined, 'add', changeData),
+        'Relation between endpoint0 (relation type "relation0") and ' +
+        'endpoint1 (relation type "relation1") was created');
+  });
+
+  it('should generate messages about one-party relations', function() {
+    var changeData =
+        { endpoints:
+              [['endpoint1', {name: 'relation1'}]]};
+    assert.equal(
+        juju._relationNotifications.message(undefined, 'add', changeData),
+        'Relation with endpoint1 (relation type "relation1") was created');
+  });
+});
+
+describe('notification visual feedback', function() {
+  var env, models, notifications, notificationsView, notifierBox, views, Y;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use('juju-env', 'juju-models', 'juju-views',
+        function(Y) {
           var juju = Y.namespace('juju');
           env = new juju.Environment();
           models = Y.namespace('juju.models');
           views = Y.namespace('juju.views');
-        });
-
-        // Instantiate the notifications model list and view.
-        // Also create the notifier box and attach it as first element of the
-        // body.
-        beforeEach(function() {
-          notifications = new models.NotificationList();
-          notificationsView = new views.NotificationsView({
-            env: env,
-            notifications: notifications
-          });
-          notifierBox = Y.Node.create('<div id="notifier-box"></div>');
-          notifierBox.setStyle('display', 'none');
-          Y.one('body').prepend(notifierBox);
-        });
-
-        // Destroy the notifier box created in beforeEach.
-        afterEach(function() {
-          notifierBox.remove();
-          notifierBox.destroy(true);
-        });
-
-        // Assert the notifier box contains the expectedNumber of notifiers.
-        var assertNumNotifiers = function(expectedNumber) {
-          assert.equal(expectedNumber, notifierBox.get('children').size());
-        };
-
-        it('should appear when a new error is notified', function() {
-          notifications.add({title: 'mytitle', level: 'error'});
-          assertNumNotifiers(1);
-        });
-
-        it('should only appear when the DOM contains the notifier box',
-            function() {
-             notifierBox.remove();
-             notifications.add({title: 'mytitle', level: 'error'});
-             assertNumNotifiers(0);
-           });
-
-        it('should not appear when the notification is not an error',
-            function() {
-             notifications.add({title: 'mytitle', level: 'info'});
-             assertNumNotifiers(0);
-           });
-
-        it('should not appear when the notification comes form delta',
-            function() {
-             notifications.add({title: 'mytitle', level: 'error', isDelta:
-               true});
-             assertNumNotifiers(0);
-           });
-
-      });
+          done();
+        });
+  });
+
+  // Instantiate the notifications model list and view.
+  // Also create the notifier box and attach it as first element of the body.
+  beforeEach(function() {
+    notifications = new models.NotificationList();
+    notificationsView = new views.NotificationsView({
+      env: env,
+      notifications: notifications
     });
+    notifierBox = Y.Node.create('<div id="notifier-box"></div>');
+    notifierBox.setStyle('display', 'none');
+    Y.one('body').prepend(notifierBox);
+  });
+
+  // Destroy the notifier box created in beforeEach.
+  afterEach(function() {
+    notifierBox.remove();
+    notifierBox.destroy(true);
+  });
+
+  // Assert the notifier box contains the expectedNumber of notifiers.
+  var assertNumNotifiers = function(expectedNumber) {
+    assert.equal(expectedNumber, notifierBox.get('children').size());
+  };
+
+  it('should appear when a new error is notified', function() {
+    notifications.add({title: 'mytitle', level: 'error'});
+    assertNumNotifiers(1);
+  });
+
+  it('should only appear when the DOM contains the notifier box', function() {
+    notifierBox.remove();
+    notifications.add({title: 'mytitle', level: 'error'});
+    assertNumNotifiers(0);
+  });
+
+  it('should not appear when the notification is not an error', function() {
+    notifications.add({title: 'mytitle', level: 'info'});
+    assertNumNotifiers(0);
+  });
+
+  it('should not appear when the notification comes form delta', function() {
+    notifications.add({title: 'mytitle', level: 'error', isDelta: true});
+    assertNumNotifiers(0);
+  });
+
+});

=== modified file 'test/test_notifier_widget.js'
--- test/test_notifier_widget.js	2012-11-23 16:21:32 +0000
+++ test/test_notifier_widget.js	2012-12-24 03:49:21 +0000
@@ -1,101 +1,104 @@
 'use strict';
 
-YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
-  describe('notifier widget', function() {
-    var Notifier, notifierBox;
-
-    before(function() {
-      Notifier = Y.namespace('juju.widgets').Notifier;
-    });
-
-    // Create the notifier box and attach it as first element of the body.
-    beforeEach(function() {
-      notifierBox = Y.Node.create('<div id="notifier-box"></div>');
-      notifierBox.setStyle('display', 'none');
-      Y.one('body').prepend(notifierBox);
-    });
-
-    // Destroy the notifier box created in beforeEach.
-    afterEach(function() {
-      notifierBox.remove();
-      notifierBox.destroy(true);
-    });
-
-    // Factory rendering and returning a notifier instance.
-    var makeNotifier = function(title, message, timeout) {
-      var notifier = new Notifier({
-        title: title || 'mytitle',
-        message: message || 'mymessage',
-        timeout: timeout || 10000
-      });
-      notifier.render(notifierBox);
-      return notifier;
-    };
-
-    // Assert the notifier box contains the expectedNumber of notifiers.
-    var assertNumNotifiers = function(expectedNumber) {
-      assert.equal(expectedNumber, notifierBox.get('children').size());
-    };
-
-    it('should be able to display a notification', function() {
+
+describe('notifier widget', function() {
+  var Notifier, notifierBox, Y;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
+          Notifier = Y.namespace('juju.widgets').Notifier;
+          done();
+    });
+  });
+
+  // Create the notifier box and attach it as first element of the body.
+  beforeEach(function() {
+    notifierBox = Y.Node.create('<div id="notifier-box"></div>');
+    notifierBox.setStyle('display', 'none');
+    Y.one('body').prepend(notifierBox);
+  });
+
+  // Destroy the notifier box created in beforeEach.
+  afterEach(function() {
+    notifierBox.remove();
+    notifierBox.destroy(true);
+  });
+
+  // Factory rendering and returning a notifier instance.
+  var makeNotifier = function(title, message, timeout) {
+    var notifier = new Notifier({
+      title: title || 'mytitle',
+      message: message || 'mymessage',
+      timeout: timeout || 10000
+    });
+    notifier.render(notifierBox);
+    return notifier;
+  };
+
+  // Assert the notifier box contains the expectedNumber of notifiers.
+  var assertNumNotifiers = function(expectedNumber) {
+    assert.equal(expectedNumber, notifierBox.get('children').size());
+  };
+
+  it('should be able to display a notification', function() {
+    makeNotifier();
+    assertNumNotifiers(1);
+  });
+
+  it('should display the given title and message', function() {
+    makeNotifier('mytitle', 'mymessage');
+    var notifierNode = notifierBox.one('*');
+    assert.equal('mytitle', notifierNode.one('h3').getContent());
+    assert.equal('mymessage', notifierNode.one('div').getContent());
+  });
+
+  it('should be able to display multiple notifications', function() {
+    var number = 10;
+    for (var i = 0; i < number; i += 1) {
       makeNotifier();
+    }
+    assertNumNotifiers(number);
+  });
+
+  it('should display new notifications on top', function() {
+    makeNotifier('mytitle1', 'mymessage1');
+    makeNotifier('mytitle2', 'mymessage2');
+    var notifierNode = notifierBox.one('*');
+    assert.equal('mytitle2', notifierNode.one('h3').getContent());
+    assert.equal('mymessage2', notifierNode.one('div').getContent());
+  });
+
+  it('should destroy notifications after N milliseconds', function(done) {
+    makeNotifier('mytitle', 'mymessage', 1);
+    // A timeout of 250 milliseconds is used so that we ensure the destroying
+    // animation can be completed.
+    setTimeout(function() {
+      assertNumNotifiers(0);
+      done();
+    }, 250);
+  });
+
+  it('should destroy notifications on click', function(done) {
+    makeNotifier();
+    notifierBox.one('*').simulate('click');
+    // A timeout of 250 milliseconds is used so that we ensure the destroying
+    // animation can be completed.
+    setTimeout(function() {
+      assertNumNotifiers(0);
+      done();
+    }, 250);
+  });
+
+  it('should prevent notification removal on mouse enter', function(done) {
+    makeNotifier('mytitle', 'mymessage', 1);
+    notifierBox.one('*').simulate('mouseover');
+    // A timeout of 250 milliseconds is used so that we ensure the node is not
+    // preserved by the destroying animation.
+    setTimeout(function() {
       assertNumNotifiers(1);
-    });
-
-    it('should display the given title and message', function() {
-      makeNotifier('mytitle', 'mymessage');
-      var notifierNode = notifierBox.one('*');
-      assert.equal('mytitle', notifierNode.one('h3').getContent());
-      assert.equal('mymessage', notifierNode.one('div').getContent());
-    });
-
-    it('should be able to display multiple notifications', function() {
-      var number = 10;
-      for (var i = 0; i < number; i += 1) {
-        makeNotifier();
-      }
-      assertNumNotifiers(number);
-    });
-
-    it('should display new notifications on top', function() {
-      makeNotifier('mytitle1', 'mymessage1');
-      makeNotifier('mytitle2', 'mymessage2');
-      var notifierNode = notifierBox.one('*');
-      assert.equal('mytitle2', notifierNode.one('h3').getContent());
-      assert.equal('mymessage2', notifierNode.one('div').getContent());
-    });
-
-    it('should destroy notifications after N milliseconds', function(done) {
-      makeNotifier('mytitle', 'mymessage', 1);
-      // A timeout of 250 milliseconds is used so that we ensure the destroying
-      // animation can be completed.
-      setTimeout(function() {
-        assertNumNotifiers(0);
-        done();
-      }, 250);
-    });
-
-    it('should destroy notifications on click', function(done) {
-      makeNotifier();
-      notifierBox.one('*').simulate('click');
-      // A timeout of 250 milliseconds is used so that we ensure the destroying
-      // animation can be completed.
-      setTimeout(function() {
-        assertNumNotifiers(0);
-        done();
-      }, 250);
-    });
-
-    it('should prevent notification removal on mouse enter', function(done) {
-      makeNotifier('mytitle', 'mymessage', 1);
-      notifierBox.one('*').simulate('mouseover');
-      // A timeout of 250 milliseconds is used so that we ensure the node is not
-      // preserved by the destroying animation.
-      setTimeout(function() {
-        assertNumNotifiers(1);
-        done();
-      }, 250);
-    });
-
+      done();
+    }, 250);
   });
+
 });
+


Follow ups