Merge lp:~frankban/juju-gui/bug-1076404-growl into lp:juju-gui/experimental

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 248
Proposed branch: lp:~frankban/juju-gui/bug-1076404-growl
Merge into: lp:juju-gui/experimental
Diff against target: 543 lines (+406/-4)
12 files modified
app/app.js (+2/-1)
app/index.html (+1/-0)
app/models/models.js (+2/-0)
app/modules-debug.js (+4/-0)
app/store/notifications.js (+1/-1)
app/templates/notifier.handlebars (+3/-0)
app/views/notifications.js (+30/-2)
app/widgets/notifier.js (+148/-0)
lib/views/stylesheet.less (+50/-0)
test/index.html (+1/-0)
test/test_notifications.js (+61/-0)
test/test_notifier_widget.js (+103/-0)
To merge this branch: bzr merge lp:~frankban/juju-gui/bug-1076404-growl
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+134660@code.launchpad.net

Description of the change

Growl-style notifications.

This branch introduces growl-style notifications
in the top-right corner of the window.
Implemented a notifier widget: it is a reusable
piece of code that just need a title, a message
and a timeout to display a notification. It is used
to render notification outside view containers, so
that notifications are preserved when the user changes
the current view.

A notifier is created when a notification is added,
following these rules:
- the notification is an error;
- the notification is local (it's not related to the
  delta stream).

The last point involves adding a new field "isDelta"
to the notification model, defaulting to false.
Notifications created when the delta stream arrives are
marked as "isDelta: true".

UI: new notifications appear on top, and disappear after
8 seconds. Mouse over prevents a notification to hide,
mouse click destroys a notification.

Also fixed a bug in the unit view: a callable was undefined
when trying to "retry" or "resolve" a unit.

https://codereview.appspot.com/6851058/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+134660_code.launchpad.net,

Message:
Please take a look.

Description:
Growl-style notifications.

This branch introduces growl-style notifications
in the top-right corner of the window.
Implemented a notifier widget: it is a reusable
piece of code that just need a title, a message
and a timeout to display a notification. It is used
to render notification outside view containers, so
that notifications are preserved when the user changes
the current view.

A notifier is created when a notification is added,
following these rules:
- the notification is an error;
- the notification is local (it's not related to the
   delta stream).

The last point involves adding a new field "isDelta"
to the notification model, defaulting to false.
Notifications created when the delta stream arrives are
marked as "isDelta: true".

UI: new notifications appear on top, and disappear after
8 seconds. Mouse over prevents a notification to hide,
mouse click destroys a notification.

Also fixed a bug in the unit view: a callable was undefined
when trying to "retry" or "resolve" a unit.

This branch is intended to be a first implementation proposal
of the growl notifications. It doesn't solve open issues like:
- how to handle/aggregate multiple notifications;
- how to correctly generate and format titles and messages;
- where notifications should appear in the window.

https://code.launchpad.net/~frankban/juju-gui/bug-1076404-growl/+merge/134660

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6851058/

Affected files:
   A [revision details]
   M app/app.js
   A app/assets/images/notifier-error.png
   M app/index.html
   M app/models/models.js
   M app/modules-debug.js
   M app/store/notifications.js
   A app/templates/notifier.handlebars
   M app/views/notifications.js
   A app/widgets/notifier.js
   M lib/views/stylesheet.less
   M test/index.html
   M test/test_notifications.js
   A test/test_notifier_widget.js

Revision history for this message
Thiago Veronezi (tveronezi) wrote :

Thanks, Francesco.
I like it! I have just one really minor comment.

[]s,
Thiago.

https://codereview.appspot.com/6851058/diff/1/app/views/notifications.js
File app/views/notifications.js (right):

https://codereview.appspot.com/6851058/diff/1/app/views/notifications.js#newcode50
app/views/notifications.js:50: if (notifierBox &&
I think will you always have the 'notifier-box' element. It is
hard-coded in index.html. So probably you can remove this check.

https://codereview.appspot.com/6851058/

Revision history for this message
Francesco Banconi (frankban) wrote :

Thanks for the review Thiago! A reply to your comment follows.

https://codereview.appspot.com/6851058/diff/1/app/views/notifications.js
File app/views/notifications.js (right):

https://codereview.appspot.com/6851058/diff/1/app/views/notifications.js#newcode50
app/views/notifications.js:50: if (notifierBox &&
On 2012/11/16 14:39:29, thiago wrote:
> I think will you always have the 'notifier-box' element. It is
hard-coded in
> index.html. So probably you can remove this check.

The check is here to make tests work. The element is not present in the
test page, and it's created by tests' setUp and tearDown only when
needed, so that notifications are not created as side effects by
non-relevant test cases.

https://codereview.appspot.com/6851058/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

This all looks pretty good to me, Francesco! Thanks for the branch.
Works well, tests pass.

https://codereview.appspot.com/6851058/diff/1/lib/views/stylesheet.less
File lib/views/stylesheet.less (right):

https://codereview.appspot.com/6851058/diff/1/lib/views/stylesheet.less#newcode592
lib/views/stylesheet.less:592: opacity: @opacity - 0.1;
Less is so neat, sometimes :)

https://codereview.appspot.com/6851058/

Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Growl-style notifications.

This branch introduces growl-style notifications
in the top-right corner of the window.
Implemented a notifier widget: it is a reusable
piece of code that just need a title, a message
and a timeout to display a notification. It is used
to render notification outside view containers, so
that notifications are preserved when the user changes
the current view.

A notifier is created when a notification is added,
following these rules:
- the notification is an error;
- the notification is local (it's not related to the
   delta stream).

The last point involves adding a new field "isDelta"
to the notification model, defaulting to false.
Notifications created when the delta stream arrives are
marked as "isDelta: true".

UI: new notifications appear on top, and disappear after
8 seconds. Mouse over prevents a notification to hide,
mouse click destroys a notification.

Also fixed a bug in the unit view: a callable was undefined
when trying to "retry" or "resolve" a unit.

R=thiago, matthew.scott
CC=
https://codereview.appspot.com/6851058

https://codereview.appspot.com/6851058/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'app/app.js'
--- app/app.js 2012-11-07 12:35:06 +0000
+++ app/app.js 2012-11-16 13:56:21 +0000
@@ -280,7 +280,8 @@
280 'unit',280 'unit',
281 // The querystring is used to handle highlighting relation rows in281 // The querystring is used to handle highlighting relation rows in
282 // links from notifications about errors.282 // links from notifications about errors.
283 { unit: unit, db: this.db, env: this.env,283 { getModelURL: Y.bind(this.getModelURL, this),
284 unit: unit, db: this.db, env: this.env,
284 querystring: req.query });285 querystring: req.query });
285 },286 },
286287
287288
=== added file 'app/assets/images/notifier-error.png'
288Binary files app/assets/images/notifier-error.png 1970-01-01 00:00:00 +0000 and app/assets/images/notifier-error.png 2012-11-16 13:56:21 +0000 differ289Binary files app/assets/images/notifier-error.png 1970-01-01 00:00:00 +0000 and app/assets/images/notifier-error.png 2012-11-16 13:56:21 +0000 differ
=== modified file 'app/index.html'
--- app/index.html 2012-11-13 18:18:47 +0000
+++ app/index.html 2012-11-16 13:56:21 +0000
@@ -18,6 +18,7 @@
1818
19 <body>19 <body>
2020
21 <div id="notifier-box"></div>
21 <div id="viewport-wrapper">22 <div id="viewport-wrapper">
22 <div id="vp-left-border"></div>23 <div id="vp-left-border"></div>
23 <div id="viewport">24 <div id="viewport">
2425
=== modified file 'app/models/models.js'
--- app/models/models.js 2012-10-19 01:44:45 +0000
+++ app/models/models.js 2012-11-16 13:56:21 +0000
@@ -309,6 +309,8 @@
309 [model.name,309 [model.name,
310 (model instanceof Y.Model) ? model.get('id') : model.id]);310 (model instanceof Y.Model) ? model.get('id') : model.id]);
311 }},311 }},
312 // Whether or not the notification is related to the delta stream.
313 isDelta: {value: false},
312 link: {},314 link: {},
313 link_title: {315 link_title: {
314 value: 'View Details'316 value: 'View Details'
315317
=== modified file 'app/modules-debug.js'
--- app/modules-debug.js 2012-11-13 16:46:36 +0000
+++ app/modules-debug.js 2012-11-16 13:56:21 +0000
@@ -24,6 +24,10 @@
24 modules: {24 modules: {
25 // Primitives25 // Primitives
2626
27 'notifier': {
28 fullpath: '/juju-ui/widgets/notifier.js'
29 },
30
27 'svg-layouts': {31 'svg-layouts': {
28 fullpath: '/juju-ui/assets/javascripts/svg-layouts.js'32 fullpath: '/juju-ui/assets/javascripts/svg-layouts.js'
29 },33 },
3034
=== modified file 'app/store/notifications.js'
--- app/store/notifications.js 2012-10-15 10:07:49 +0000
+++ app/store/notifications.js 2012-11-16 13:56:21 +0000
@@ -156,7 +156,7 @@
156 var change_type = change[0],156 var change_type = change[0],
157 change_op = change[1],157 change_op = change[1],
158 change_data = change[2],158 change_data = change[2],
159 notify_data = {},159 notify_data = {isDelta: true},
160 rule = rules[change_type],160 rule = rules[change_type],
161 model;161 model;
162162
163163
=== added file 'app/templates/notifier.handlebars'
--- app/templates/notifier.handlebars 1970-01-01 00:00:00 +0000
+++ app/templates/notifier.handlebars 2012-11-16 13:56:21 +0000
@@ -0,0 +1,3 @@
1<i class="sprite notifier-error"></i>
2<h3>{{title}}</h3>
3<div>{{message}}</div>
04
=== modified file 'app/views/notifications.js'
--- app/views/notifications.js 2012-10-05 13:08:43 +0000
+++ app/views/notifications.js 2012-11-16 13:56:21 +0000
@@ -3,10 +3,11 @@
3YUI.add('juju-notifications', function(Y) {3YUI.add('juju-notifications', function(Y) {
44
5 var views = Y.namespace('juju.views'),5 var views = Y.namespace('juju.views'),
6 widgets = Y.namespace('juju.widgets'),
6 Templates = views.Templates;7 Templates = views.Templates;
78
8 /*9 /*
9 * Abstract Base class used to view a ModelList of notifications10 * Abstract Base class used to view a ModelList of notifications
10 */11 */
11 var NotificationsBaseView = Y.Base.create('NotificationsBaseView',12 var NotificationsBaseView = Y.Base.create('NotificationsBaseView',
12 Y.View, [views.JujuBaseView], {13 Y.View, [views.JujuBaseView], {
@@ -24,10 +25,36 @@
24 notifications.after('create', this.slowRender, this);25 notifications.after('create', this.slowRender, this);
25 notifications.after('remove', this.slowRender, this);26 notifications.after('remove', this.slowRender, this);
26 notifications.after('reset', this.slowRender, this);27 notifications.after('reset', this.slowRender, this);
28 // Bind new notifications to the notifier widget.
29 notifications.after('add', this.addNotifier, this);
2730
28 // Env connection state watcher31 // Env connection state watcher
29 env.on('connectedChange', this.slowRender, this);32 env.on('connectedChange', this.slowRender, this);
33 },
3034
35 /**
36 * Create and display a notifier widget when a notification is added.
37 * The notifier is created only if:
38 * - the notifier box exists in the DOM;
39 * - the notification is a local one (not related to the delta stream);
40 * - the notification is an error.
41 *
42 * @method addNotifier
43 * @param {Object} ev An event object (with a "model" attribute).
44 * @return {undefined} Mutates only.
45 */
46 addNotifier: function(ev) {
47 var notification = ev.model,
48 notifierBox = Y.one('#notifier-box');
49 // Show error notifications only if the DOM contain the notifier box.
50 if (notifierBox &&
51 !notification.get('isDelta') &&
52 notification.get('level') === 'error') {
53 new widgets.Notifier({
54 title: notification.get('title'),
55 message: notification.get('message')
56 }).render(notifierBox);
57 }
31 },58 },
3259
33 /*60 /*
@@ -251,6 +278,7 @@
251 'view',278 'view',
252 'juju-view-utils',279 'juju-view-utils',
253 'node',280 'node',
254 'handlebars'281 'handlebars',
282 'notifier'
255 ]283 ]
256});284});
257285
=== added directory 'app/widgets'
=== added file 'app/widgets/notifier.js'
--- app/widgets/notifier.js 1970-01-01 00:00:00 +0000
+++ app/widgets/notifier.js 2012-11-16 13:56:21 +0000
@@ -0,0 +1,148 @@
1'use strict';
2
3YUI.add('notifier', function(Y) {
4
5 var widgets = Y.namespace('juju.widgets');
6
7 /**
8 * Display a notification.
9 * This is the constructor for the notifier widget.
10 *
11 * @class Notifier
12 * @namespace widgets
13 */
14 function Notifier(config) {
15 Notifier.superclass.constructor.apply(this, arguments);
16 }
17
18 Notifier.NAME = 'Notifier';
19 Notifier.ATTRS = {
20 title: {value: ''},
21 message: {value: ''},
22 timeout: {value: 8000}
23 };
24
25 /**
26 * Define the widget class extending Y.Widget.
27 *
28 * @class Notifier
29 * @namespace widgets
30 */
31 Y.extend(Notifier, Y.Widget, {
32
33 CONTENT_TEMPLATE: null,
34 TEMPLATE: Y.namespace('juju.views').Templates.notifier,
35
36 /**
37 * Attach the widget bounding box to the DOM.
38 * Override to insert new instances before existing ones.
39 * This way new notification are displayed on top of the page.
40 * The resulting rendering process is also very simplified.
41 *
42 * @method _renderBox
43 * @protected
44 * @param {Y.Node object} parentNode The node containing this widget.
45 * @return {undefined} Mutates only.
46 */
47 _renderBox: function(parentNode) {
48 parentNode.prepend(this.get('boundingBox'));
49 },
50
51 /**
52 * Create the nodes required by this widget and attach them to the DOM.
53 *
54 * @method renderUI
55 * @return {undefined} Mutates only.
56 */
57 renderUI: function() {
58 var content = this.TEMPLATE({
59 title: this.get('title'),
60 message: this.get('message')
61 });
62 this.get('contentBox').append(content);
63 },
64
65 /**
66 * Attach event listeners which bind the UI to the widget state.
67 * The mouse enter event on a notification node pauses the timer.
68 * The mouse click event on a notification destroys the widget.
69 *
70 * @method bindUI
71 * @return {undefined} Mutates only.
72 */
73 bindUI: function() {
74 var contentBox = this.get('contentBox');
75 contentBox.on(
76 'hover',
77 function() {
78 if (this.timer) {
79 this.timer.pause();
80 }
81 },
82 function() {
83 if (this.timer) {
84 this.timer.resume();
85 }
86 },
87 this
88 );
89 contentBox.on('click', function(ev) {
90 this.hideAndDestroy();
91 ev.halt();
92 }, this);
93 },
94
95 /**
96 * Create and start the timer that will destroy the widget after N seconds.
97 *
98 * @method syncUI
99 * @return {undefined} Mutates only.
100 */
101 syncUI: function() {
102 this.timer = new Y.Timer({
103 length: this.get('timeout'),
104 repeatCount: 1,
105 callback: Y.bind(this.hideAndDestroy, this)
106 });
107 this.timer.start();
108 },
109
110 /**
111 * Hide this widget using an animation and destroy the widget at the end.
112 *
113 * @method hideAndDestroy
114 * @return {undefined} Mutates only.
115 */
116 hideAndDestroy: function() {
117 this.timer.stop();
118 this.timer = null;
119 if (this.get('boundingBox').getDOMNode()) {
120 // Animate and destroy the notification if it still exists in the DOM.
121 var anim = new Y.Anim({
122 node: this.get('boundingBox'),
123 to: {opacity: 0},
124 easing: 'easeIn',
125 duration: 0.2
126 });
127 anim.on('end', this.destroy, this);
128 anim.run();
129 } else {
130 // Otherwise, just destroy the notification.
131 this.destroy();
132 }
133 }
134
135 });
136
137 widgets.Notifier = Notifier;
138
139}, '0.1.0', {requires: [
140 'anim',
141 'event',
142 'event-hover',
143 'handlebars',
144 'gallery-timer',
145 'juju-templates',
146 'node',
147 'widget'
148]});
0149
=== modified file 'lib/views/stylesheet.less'
--- lib/views/stylesheet.less 2012-11-15 14:33:54 +0000
+++ lib/views/stylesheet.less 2012-11-16 13:56:21 +0000
@@ -565,6 +565,56 @@
565}565}
566566
567/*567/*
568 * Notifier widget.
569 */
570#notifier-box {
571 position: absolute;
572 right: 0px;
573 top: 0px;
574 z-index: 9999;
575 .yui3-notifier-content {
576 @background-color: black;
577 @margin-left: 41px;
578 @margin-top: 13px;
579 @opacity: 0.8;
580 .create-border-radius(8px);
581 // Using a variable here because LESS strips commas in mixin args.
582 @box-shadow: 0 2px 4px lighten(@background-color, 10%),
583 0 1px 2px lighten(@background-color, 20%) inset;
584 .create-box-shadow(@box-shadow);
585 background-color: @background-color;
586 margin: 6px;
587 opacity: @opacity;
588 overflow: hidden;
589 width: 277px;
590 &:hover {
591 cursor: pointer;
592 opacity: @opacity - 0.1;
593 }
594 h3 {
595 color: #FDF6E3;
596 font-size: 16px;
597 font-weight: normal;
598 margin-top: @margin-top;
599 margin-left: @margin-left;
600 }
601 div {
602 color: #DDD7C6;
603 font-size: 12px;
604 line-height: 16px;
605 margin-left: @margin-left;
606 margin-bottom: 12px;
607 }
608 .sprite {
609 float: left;
610 margin-top: @margin-top + 5px;
611 margin-left: 12px;
612 opacity: 1;
613 }
614 }
615}
616
617/*
568 * Overview Module618 * Overview Module
569 */619 */
570#overview {620#overview {
571621
=== modified file 'test/index.html'
--- test/index.html 2012-11-13 13:55:16 +0000
+++ test/index.html 2012-11-16 13:56:21 +0000
@@ -33,6 +33,7 @@
33 <script src="test_endpoints.js"></script>33 <script src="test_endpoints.js"></script>
34 <script src="test_application_notifications.js"></script>34 <script src="test_application_notifications.js"></script>
35 <script src="test_charm_store.js"></script>35 <script src="test_charm_store.js"></script>
36 <script src="test_notifier_widget.js"></script>
3637
37 <script>38 <script>
38 YUI().use('node', 'event', function(Y) {39 YUI().use('node', 'event', function(Y) {
3940
=== modified file 'test/test_notifications.js'
--- test/test_notifications.js 2012-10-18 15:12:38 +0000
+++ test/test_notifications.js 2012-11-16 13:56:21 +0000
@@ -414,3 +414,64 @@
414 'Relation with endpoint1 (relation type "relation1") was created');414 'Relation with endpoint1 (relation type "relation1") was created');
415 });415 });
416});416});
417
418describe('notification visual feedback', function() {
419 var env, models, notifications, notificationsView, notifierBox, views, Y;
420
421 before(function(done) {
422 Y = YUI(GlobalConfig).use('juju-env', 'juju-models', 'juju-views',
423 function(Y) {
424 var juju = Y.namespace('juju');
425 env = new juju.Environment();
426 models = Y.namespace('juju.models');
427 views = Y.namespace('juju.views');
428 done();
429 });
430 });
431
432 // Instantiate the notifications model list and view.
433 // Also create the notifier box and attach it as first element of the body.
434 beforeEach(function() {
435 notifications = new models.NotificationList();
436 notificationsView = new views.NotificationsView({
437 env: env,
438 notifications: notifications
439 });
440 notifierBox = Y.Node.create('<div id="notifier-box"></div>');
441 notifierBox.setStyle('display', 'none');
442 Y.one('body').prepend(notifierBox);
443 });
444
445 // Destroy the notifier box created in beforeEach.
446 afterEach(function() {
447 notifierBox.remove();
448 notifierBox.destroy(true);
449 });
450
451 // Assert the notifier box contains the expectedNumber of notifiers.
452 var assertNumNotifiers = function(expectedNumber) {
453 assert.equal(expectedNumber, notifierBox.get('children').size());
454 };
455
456 it('should appear when a new error is notified', function() {
457 notifications.add({title: 'mytitle', level: 'error'});
458 assertNumNotifiers(1);
459 });
460
461 it('should only appear when the DOM contains the notifier box', function() {
462 notifierBox.remove();
463 notifications.add({title: 'mytitle', level: 'error'});
464 assertNumNotifiers(0);
465 });
466
467 it('should not appear when the notification is not an error', function() {
468 notifications.add({title: 'mytitle', level: 'info'});
469 assertNumNotifiers(0);
470 });
471
472 it('should not appear when the notification comes form delta', function() {
473 notifications.add({title: 'mytitle', level: 'error', isDelta: true});
474 assertNumNotifiers(0);
475 });
476
477});
417478
=== added file 'test/test_notifier_widget.js'
--- test/test_notifier_widget.js 1970-01-01 00:00:00 +0000
+++ test/test_notifier_widget.js 2012-11-16 13:56:21 +0000
@@ -0,0 +1,103 @@
1'use strict';
2
3describe('notifier widget', function() {
4 var Notifier, notifierBox, Y;
5
6 before(function(done) {
7 Y = YUI(GlobalConfig).use('notifier', 'node-event-simulate',
8 function(Y) {
9 Notifier = Y.namespace('juju.widgets').Notifier;
10 done();
11 });
12 });
13
14 // Create the notifier box and attach it as first element of the body.
15 beforeEach(function() {
16 notifierBox = Y.Node.create('<div id="notifier-box"></div>');
17 notifierBox.setStyle('display', 'none');
18 Y.one('body').prepend(notifierBox);
19 });
20
21 // Destroy the notifier box created in beforeEach.
22 afterEach(function() {
23 notifierBox.remove();
24 notifierBox.destroy(true);
25 });
26
27 // Factory rendering and returning a notifier instance.
28 var makeNotifier = function(title, message, timeout) {
29 var notifier = new Notifier({
30 title: title || 'mytitle',
31 message: message || 'mymessage',
32 timeout: timeout || 10000
33 });
34 notifier.render(notifierBox);
35 return notifier;
36 };
37
38 // Assert the notifier box contains the expectedNumber of notifiers.
39 var assertNumNotifiers = function(expectedNumber) {
40 assert.equal(expectedNumber, notifierBox.get('children').size());
41 };
42
43 it('should be able to display a notification', function() {
44 makeNotifier();
45 assertNumNotifiers(1);
46 });
47
48 it('should display the given title and message', function() {
49 makeNotifier('mytitle', 'mymessage');
50 var notifierNode = notifierBox.one('*');
51 assert.equal('mytitle', notifierNode.one('h3').getContent());
52 assert.equal('mymessage', notifierNode.one('div').getContent());
53 });
54
55 it('should be able to display multiple notifications', function() {
56 var number = 10;
57 for (var i = 0; i < number; i += 1) {
58 makeNotifier();
59 }
60 assertNumNotifiers(number);
61 });
62
63 it('should display new notifications on top', function() {
64 makeNotifier('mytitle1', 'mymessage1');
65 makeNotifier('mytitle2', 'mymessage2');
66 var notifierNode = notifierBox.one('*');
67 assert.equal('mytitle2', notifierNode.one('h3').getContent());
68 assert.equal('mymessage2', notifierNode.one('div').getContent());
69 });
70
71 it('should destroy notifications after N milliseconds', function(done) {
72 makeNotifier('mytitle', 'mymessage', 1);
73 // A timeout of 250 milliseconds is used so that we ensure the destroying
74 // animation can be completed.
75 setTimeout(function() {
76 assertNumNotifiers(0);
77 done();
78 }, 250);
79 });
80
81 it('should destroy notifications on click', function(done) {
82 makeNotifier();
83 notifierBox.one('*').simulate('click');
84 // A timeout of 250 milliseconds is used so that we ensure the destroying
85 // animation can be completed.
86 setTimeout(function() {
87 assertNumNotifiers(0);
88 done();
89 }, 250);
90 });
91
92 it('should prevent notification removal on mouse enter', function(done) {
93 makeNotifier('mytitle', 'mymessage', 1);
94 notifierBox.one('*').simulate('mouseover');
95 // A timeout of 250 milliseconds is used so that we ensure the node is not
96 // preserved by the destroying animation.
97 setTimeout(function() {
98 assertNumNotifiers(1);
99 done();
100 }, 250);
101 });
102
103});

Subscribers

People subscribed via source and target branches