Merge lp:~danilo/launchpad/bug-772754-other-subscribers-sections into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 13243
Proposed branch: lp:~danilo/launchpad/bug-772754-other-subscribers-sections
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/bug-772754-base
Diff against target: 844 lines (+721/-14)
2 files modified
lib/lp/bugs/javascript/subscribers_list.js (+221/-1)
lib/lp/bugs/javascript/tests/test_subscribers_list.js (+500/-13)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-772754-other-subscribers-sections
Reviewer Review Type Date Requested Status
Brad Crittenden (community) approve Approve
Review via email: mp+64176@code.launchpad.net

Description of the change

= Bug 772754: Other subscribers list, part 1 =

This branch is the start of providing list of other subscribers using
the newly proposed layout from bug 772754 (look at Gary's mockup at https://launchpadlibrarian.net/71552495/all-in-one.png).

This introduces code to manage different subscription level section
nodes in the list.

It is actually made against Gary's lp:~gary/launchpad/bug-772754-2 branch, but I keep it in a pipeline as bug-772754-base for easier manipulation and merging.

== Tests ==

lp/bugs/javascript/tests/test_subscribers_list.html

== Demo and Q/A ==

N/A

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/subscribers_list.js
  lib/lp/bugs/javascript/tests/test_subscribers_list.js

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Danilo,

I really like your approach to JS development and enjoy learning from your branches.

Things like the CSS_CLASSES dict are a nice touch to encapsulate info in a DRY fashion.

* Y.Node.create('<div></div>') can be written as Y.Node.create('<div />') and many find it more readable. YUI does the right thing, so you can even use it on elements that aren't really allowed to be self-closing.

* Defining 'level' at first use, maybe in getOrCreateSection, would be helpful.

I see you use a lot of bare comparisons against null and undefined whereas others seem to prefer to use the Y.Lang tests. Are they equivalent? Perhaps this is an issue we should decide on and put in our (as yet mythical) JS Guidelines. Your thoughts are appreciated.

Thanks for catching the feature flag that I think should've already been removed.

review: Approve (approve)
Revision history for this message
Данило Шеган (danilo) wrote :

Thank Brad (both for kind words and a review)!

У пет, 10. 06 2011. у 18:09 +0000, Brad Crittenden пише:

> * Y.Node.create('<div></div>') can be written as Y.Node.create('<div />') and many find it more readable. YUI does the right thing, so you can even use it on elements that aren't really allowed to be self-closing.

Oh, I was not aware that YUI was smart enough: I specifically trained
myself to use the more verbose variant so I don't mess it up when it
won't work. I'll fix all the use in my code now.

> * Defining 'level' at first use, maybe in getOrCreateSection, would be helpful.

I assume you mean a more detailed description of the parameter? I've
added that, and referred the reader to `subscriber_levels` as defined
earlier in the JS file, and BugNotificationLevel enum in Python from
there.

> I see you use a lot of bare comparisons against null and undefined
> whereas others seem to prefer to use the Y.Lang tests. Are they
> equivalent? Perhaps this is an issue we should decide on and put in
> our (as yet mythical) JS Guidelines. Your thoughts are appreciated.

I mix them myself. Whenever I find it useful, I use Y.Lang.is* tests,
but sometimes I just naturally go for "=== null", especially with node
comparisons (as all YUI3 node methods return null when no node is
matching). Also, I see a lot of value in Y.Lang.isValue (which checks
for both null and undefined) and similar methods, but Y.Lang.isNull not
so much: it's just a more expensive "=== null" check. I personally,
don't feel we should aim for consistency regarding something this simple
especially at the cost of performance (no matter how negligible the hit
might be).

I still haven't changed this, but if you feel strongly about it, I'd be
happy to accommodate :)

(I won't be landing this until all the branches in sequence are ready to
land)

> Thanks for catching the feature flag that I think should've already been removed.

The feature flags are not yet removed, but all the code that Gary and I
did in these branches is not conditional on them, so it's moot to use
them. We decided to remove the feature flags once these branches land,
considering it's easier to re-do the feature flag removal than these.

Incremental diff attached if you are interested.

Thanks again,
Danilo

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
2--- lib/lp/bugs/javascript/subscribers_list.js 2011-05-18 18:37:07 +0000
3+++ lib/lp/bugs/javascript/subscribers_list.js 2011-06-15 11:03:26 +0000
4@@ -76,4 +76,224 @@
5 }
6 namespace.remove_user_link = remove_user_link;
7
8-}, "0.1", {"requires": ["node", "lazr.anim"]});
9+
10+var CSS_CLASSES = {
11+ section : 'subscribers-section',
12+ list: 'subscribers-list',
13+ subscriber: 'subscriber',
14+ no_subscribers: 'no-subscribers-indicator'
15+};
16+
17+/**
18+ * Possible subscriber levels with descriptive headers for
19+ * sections that will hold them.
20+ *
21+ * These match BugNotificationLevel enum options (as defined in
22+ * lib/lp/bugs/enum.py).
23+ */
24+var subscriber_levels = {
25+ 'Discussion': 'Notified of all changes',
26+ 'Details': 'Notified of all changes except comments',
27+ 'Lifecycle': 'Notified when the bug is closed or reopened',
28+ 'Maybe': 'Maybe notified'
29+};
30+
31+/**
32+ * Order of subscribers sections.
33+ */
34+var subscriber_level_order = ['Discussion', 'Details', 'Lifecycle', 'Maybe'];
35+
36+
37+/**
38+ * Manages entire subscribers' list for a single bug.
39+ *
40+ * If the passed in container_box is not present, or if there are multiple
41+ * nodes matching it, it throws an exception.
42+ *
43+ * @class SubscribersList
44+ * @param config {Object} Configuration object containing at least
45+ * container_box value with the container div CSS selector
46+ * where to add the subscribers list.
47+ */
48+function SubscribersList(config) {
49+ var container_nodes = Y.all(config.container_box);
50+ if (container_nodes.size() === 0) {
51+ Y.error('Container node must be specified in config.container_box.');
52+ } else if (container_nodes.size() > 1) {
53+ Y.error("Multiple container nodes for selector '" +
54+ config.container_box + "' present in the page. " +
55+ "You need to be more explicit.");
56+ } else {
57+ this.container_node = container_nodes.item(0);
58+ }
59+}
60+namespace.SubscribersList = SubscribersList;
61+
62+/**
63+ * Reset the subscribers list:
64+ * - If no sections with subscribers are left, it adds an indication
65+ * of no subscribers.
66+ * - If there are subscribers left, it ensures there is no indication
67+ * of no subscribers.
68+ *
69+ * @method resetNoSubscribers
70+ */
71+SubscribersList.prototype.resetNoSubscribers = function() {
72+ var has_sections = (
73+ this.container_node.one('.' + CSS_CLASSES.section) !== null);
74+ var no_subs;
75+ if (has_sections) {
76+ // Make sure the indicator for no subscribers is not there.
77+ no_subs = this.container_node.one('.' + CSS_CLASSES.no_subscribers);
78+ if (no_subs !== null) {
79+ no_subs.remove();
80+ }
81+ } else {
82+ no_subs = Y.Node.create('<div />')
83+ .addClass(CSS_CLASSES.no_subscribers)
84+ .set('text', 'No other subscribers.');
85+ this.container_node.appendChild(no_subs);
86+ }
87+};
88+
89+/**
90+ * Get a CSS class to use for the section of the subscribers' list
91+ * with subscriptions with the level `level`.
92+ *
93+ * @method _getSectionCSSClass
94+ * @param level {String} Level of the subscription.
95+ * See `subscriber_levels` for a list of acceptable values.
96+ * @return {String} CSS class to use for the section for the `level`.
97+ */
98+SubscribersList.prototype._getSectionCSSClass = function(level) {
99+ return CSS_CLASSES.section + '-' + level.toLowerCase();
100+};
101+
102+/**
103+ * Return the section node for a subscription level.
104+ *
105+ * @method _getSection
106+ * @param level {String} Level of the subscription.
107+ * @return {Object} Node containing the section or null.
108+ */
109+SubscribersList.prototype._getSection = function(level) {
110+ return this.container_node.one('.' + this._getSectionCSSClass(level));
111+};
112+
113+/**
114+ * Create a subscribers section node depending on their level.
115+ *
116+ * @method _createSectionNode
117+ * @param level {String} Level of the subscription.
118+ * See `subscriber_levels` for a list of acceptable values.
119+ * @return {Object} Node containing the entire section.
120+ */
121+SubscribersList.prototype._createSectionNode = function(level) {
122+ // Container node for the entire section.
123+ var node = Y.Node.create('<div />')
124+ .addClass(CSS_CLASSES.section)
125+ .addClass(this._getSectionCSSClass(level));
126+ // Header.
127+ node.appendChild(
128+ Y.Node.create('<h3 />')
129+ .set('text', subscriber_levels[level]));
130+ // Node listing the actual subscribers.
131+ node.appendChild(
132+ Y.Node.create('<div />')
133+ .addClass(CSS_CLASSES.list));
134+ return node;
135+};
136+
137+
138+/**
139+ * Inserts the section node in the appropriate place in the subscribers list.
140+ * Uses `subscriber_level_order` to figure out what position should a section
141+ * with subscribers on `level` hold.
142+ *
143+ * @method _insertSectionNode
144+ * @param level {String} Level of the subscription.
145+ * @param section_node {Object} Node to insert (containing
146+ * the entire section).
147+ */
148+SubscribersList.prototype._insertSectionNode = function(level, section_node) {
149+ var index, existing_level;
150+ var existing_level_node = null;
151+ for (index=0; index < subscriber_level_order.length; index++) {
152+ existing_level = subscriber_level_order[index];
153+ if (existing_level === level) {
154+ // Insert either at the beginning of the list,
155+ // or after the last section which comes before this one.
156+ if (existing_level_node === null) {
157+ this.container_node.prepend(section_node);
158+ } else {
159+ existing_level_node.insert(section_node, 'after');
160+ }
161+ break;
162+ } else {
163+ var existing_node = this._getSection(existing_level);
164+ if (existing_node !== null) {
165+ existing_level_node = existing_node;
166+ }
167+ }
168+ }
169+};
170+
171+
172+/**
173+ * Create a subscribers section depending on their level and
174+ * add it to the other subscribers list.
175+ * If section is already there, returns the existing node for it.
176+ *
177+ * @method _getOrCreateSection
178+ * @param level {String} Level of the subscription.
179+ * See `subscriber_levels` for a list of acceptable values.
180+ * @return {Object} Node containing the entire section.
181+ */
182+SubscribersList.prototype._getOrCreateSection = function(level) {
183+ var section_node = this._getSection(level);
184+ if (section_node === null) {
185+ section_node = this._createSectionNode(level);
186+ this._insertSectionNode(level, section_node);
187+ }
188+ // Remove the indication of no subscribers if it's present.
189+ this.resetNoSubscribers();
190+ return section_node;
191+};
192+
193+/**
194+ * Return whether subscribers section has any subscribers or not.
195+ *
196+ * @method _sectionHasSubscribers
197+ * @param node {Y.Node} Node containing the subscribers section.
198+ * @return {Boolean} True if there are still subscribers in the section.
199+ */
200+SubscribersList.prototype._sectionNodeHasSubscribers = function(node) {
201+ var list = node.one('.' + CSS_CLASSES.list);
202+ if (list !== null) {
203+ var has_any = (list.one('.' + CSS_CLASSES.subscriber) !== null);
204+ return has_any;
205+ } else {
206+ Y.error(
207+ 'No div.subscribers-list found inside the passed `node`.');
208+ }
209+};
210+
211+/**
212+ * Removes a subscribers section node if there are no remaining subscribers.
213+ * Silently passes if nothing to remove.
214+ *
215+ * @method _removeSectionNodeIfEmpty
216+ * @param node {Object} Section node containing all the subscribers.
217+ */
218+SubscribersList.prototype._removeSectionNodeIfEmpty = function(node) {
219+ if (node !== null && !node.hasClass(CSS_CLASSES.section)) {
220+ Y.error('Node is not a section node.');
221+ }
222+ if (node !== null && !this._sectionNodeHasSubscribers(node)) {
223+ node.remove();
224+ // Add the indication of no subscribers if this was the last section.
225+ this.resetNoSubscribers();
226+ }
227+};
228+
229+}, "0.1", {"requires": ["node", "lazr.anim", "lp.client"]});
230
231=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
232--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-07 16:42:11 +0000
233+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-15 11:03:26 +0000
234@@ -12,7 +12,7 @@
235 /**
236 * Set-up all the nodes required for subscribers list testing.
237 */
238-function setUpSubscribersList(root_node, with_dupes) {
239+function setUpOldSubscribersList(root_node, with_dupes) {
240 // Set-up subscribers list.
241 var direct_links = Y.Node.create('<div></div>')
242 .set('id', 'subscribers-links');
243@@ -50,7 +50,7 @@
244 test_no_subscribers: function() {
245 // There are no subscribers left in the subscribers_list
246 // (iow, subscribers_links is empty).
247- var subscribers_list = setUpSubscribersList(this.root);
248+ var subscribers_list = setUpOldSubscribersList(this.root);
249
250 // Resetting the list adds a 'None' div to the
251 // subscribers_list (and not to the subscriber_links).
252@@ -66,7 +66,7 @@
253 test_subscribers: function() {
254 // When there is at least one subscriber, nothing
255 // happens when reset() is called.
256- var subscribers_list = setUpSubscribersList(this.root);
257+ var subscribers_list = setUpOldSubscribersList(this.root);
258 var subscribers_links = subscribers_list.one('#subscribers-links');
259 subscribers_links.appendChild(
260 Y.Node.create('<div>Subscriber</div>'));
261@@ -80,7 +80,7 @@
262
263 test_empty_duplicates: function() {
264 // There are no subscribers among the duplicate subscribers.
265- var subscribers_list = setUpSubscribersList(this.root, true);
266+ var subscribers_list = setUpOldSubscribersList(this.root, true);
267 var dupe_subscribers = this.root.one('#subscribers-from-duplicates');
268
269 // Resetting the list removes the entire duplicate subscribers node.
270@@ -92,7 +92,7 @@
271 test_duplicates: function() {
272 // There are subscribers among the duplicate subscribers,
273 // and nothing changes.
274- var subscribers_list = setUpSubscribersList(this.root, true);
275+ var subscribers_list = setUpOldSubscribersList(this.root, true);
276 var dupe_subscribers = this.root.one('#subscribers-from-duplicates');
277 dupe_subscribers.appendChild(Y.Node.create('<div>Subscriber</div>'));
278
279@@ -145,7 +145,7 @@
280 // If there is no matching subscriber, removal silently passes.
281
282 // Set-up subscribers list.
283- setUpSubscribersList(this.root);
284+ setUpOldSubscribersList(this.root);
285
286 var person = new Y.lp.bugs.subscriber.Subscriber({
287 uri: 'myself',
288@@ -169,7 +169,7 @@
289 // before animation starts.
290
291 // Set-up subscribers list.
292- setUpSubscribersList(this.root);
293+ setUpOldSubscribersList(this.root);
294
295 var person = new Y.lp.bugs.subscriber.Subscriber({
296 uri: 'myself',
297@@ -192,7 +192,7 @@
298 // If there is a direct subscriber, removal works fine.
299
300 // Set-up subscribers list.
301- setUpSubscribersList(this.root);
302+ setUpOldSubscribersList(this.root);
303
304 var person = new Y.lp.bugs.subscriber.Subscriber({
305 uri: 'myself',
306@@ -215,7 +215,7 @@
307 // a duplicate subscription link does nothing.
308
309 // Set-up subscribers list.
310- setUpSubscribersList(this.root);
311+ setUpOldSubscribersList(this.root);
312
313 var person = new Y.lp.bugs.subscriber.Subscriber({
314 uri: 'myself',
315@@ -235,7 +235,7 @@
316 // If there is a duplicate subscriber, removal works fine.
317
318 // Set-up subscribers list.
319- setUpSubscribersList(this.root, true);
320+ setUpOldSubscribersList(this.root, true);
321
322 var person = new Y.lp.bugs.subscriber.Subscriber({
323 uri: 'myself',
324@@ -258,7 +258,7 @@
325 // direct subscription user link doesn't do anything.
326
327 // Set-up subscribers list.
328- setUpSubscribersList(this.root, true);
329+ setUpOldSubscribersList(this.root, true);
330
331 var person = new Y.lp.bugs.subscriber.Subscriber({
332 uri: 'myself',
333@@ -279,7 +279,7 @@
334 // subscribed through duplicate, removal removes only one link.
335
336 // Set-up subscribers list.
337- setUpSubscribersList(this.root, true);
338+ setUpOldSubscribersList(this.root, true);
339
340 var person = new Y.lp.bugs.subscriber.Subscriber({
341 uri: 'myself',
342@@ -305,7 +305,7 @@
343 // subscribed through duplicate, removal removes only one link.
344
345 // Set-up subscribers list.
346- setUpSubscribersList(this.root, true);
347+ setUpOldSubscribersList(this.root, true);
348
349 var person = new Y.lp.bugs.subscriber.Subscriber({
350 uri: 'myself',
351@@ -327,6 +327,493 @@
352 }
353 }));
354
355+/**
356+ * Set-up all the nodes required for subscribers list testing.
357+ */
358+function setUpSubscribersList(root_node) {
359+ // Set-up subscribers list.
360+ var node = Y.Node.create('<div></div>')
361+ .set('id', 'other-subscribers-container');
362+ root_node.appendChild(node);
363+ var config = {
364+ container_box: '#other-subscribers-container'
365+ };
366+ return new module.SubscribersList(config);
367+}
368+
369+/**
370+ * Test resetting of the no subscribers indication.
371+ */
372+suite.add(new Y.Test.Case({
373+ name: 'SubscribersList constructor test',
374+
375+ _should: {
376+ error: {
377+ test_no_container_error:
378+ new Error(
379+ 'Container node must be specified in config.container_box.'),
380+ test_multiple_containers_error:
381+ new Error(
382+ "Multiple container nodes for selector '.container' "+
383+ "present in the page. You need to be more explicit.")
384+ }
385+ },
386+
387+ setUp: function() {
388+ this.root = Y.Node.create('<div></div>');
389+ Y.one('body').appendChild(this.root);
390+ },
391+
392+ tearDown: function() {
393+ this.root.remove();
394+ },
395+
396+ test_no_container_error: function() {
397+ // When there is no matching container node in the DOM tree,
398+ // an exception is thrown.
399+ var sl = new module.SubscribersList({container_box: '#not-found'});
400+ },
401+
402+ test_single_container: function() {
403+ // With an exactly single container node matches, all is well.
404+ var container_node = Y.Node.create('<div></div>')
405+ .set('id', 'container');
406+ this.root.appendChild(container_node);
407+ var list = new module.SubscribersList({container_box: '#container'});
408+ Y.Assert.areSame(container_node, list.container_node);
409+ },
410+
411+ test_multiple_containers_error: function() {
412+ // With two nodes matching the given CSS selector,
413+ // an exception is thrown.
414+ this.root.appendChild(
415+ Y.Node.create('<div></div>').addClass('container'));
416+ this.root.appendChild(
417+ Y.Node.create('<div></div>').addClass('container'));
418+ var sl = new module.SubscribersList({container_box: '.container'});
419+ }
420+}));
421+
422+
423+/**
424+ * Test resetting of the no subscribers indication.
425+ */
426+suite.add(new Y.Test.Case({
427+ name: 'SubscribersList.resetNoSubscribers() test',
428+
429+ setUp: function() {
430+ this.root = Y.Node.create('<div></div>');
431+ Y.one('body').appendChild(this.root);
432+ },
433+
434+ tearDown: function() {
435+ this.root.remove();
436+ },
437+
438+ test_initially_empty: function() {
439+ // When the SubscribersList is set-up, it's initially
440+ // entirely empty.
441+ var subscribers_list = setUpSubscribersList(this.root);
442+ Y.Assert.isTrue(
443+ subscribers_list.container_node.all().isEmpty());
444+ },
445+
446+ test_no_subscribers: function() {
447+ // When resetNoSubscribers() is called on an empty
448+ // SubscribersList, indication of no subscribers is added.
449+ var subscribers_list = setUpSubscribersList(this.root);
450+ subscribers_list.resetNoSubscribers();
451+ var no_subs_nodes = this.root.all(
452+ '.no-subscribers-indicator');
453+ Y.Assert.areEqual(1, no_subs_nodes.size());
454+ Y.Assert.areEqual('No other subscribers.',
455+ no_subs_nodes.item(0).get('text'));
456+ },
457+
458+ test_subscribers_no_addition: function() {
459+ // When resetNoSubscribers() is called on a SubscribersList
460+ // with some subscribers, no indication of no subscribers is added.
461+ var subscribers_list = setUpSubscribersList(this.root);
462+ // Hack a section node into the list so it appears as if
463+ // there are subscribers.
464+ subscribers_list.container_node.appendChild(
465+ Y.Node.create('<div></div>')
466+ .addClass('subscribers-section'));
467+
468+ // There is no indication of no subscribers added by
469+ // resetNoSubscribers.
470+ subscribers_list.resetNoSubscribers();
471+ var no_subs_nodes = this.root.all(
472+ '.no-subscribers-indicator');
473+ Y.Assert.isTrue(no_subs_nodes.isEmpty());
474+ },
475+
476+ test_subscribers_remove_previous_indication: function() {
477+ // When resetNoSubscribers() is called on a SubscribersList
478+ // with some subscribers, existing indication of no subscribers
479+ // is removed.
480+ var subscribers_list = setUpSubscribersList(this.root);
481+ // Hack a section node into the list so it appears as if
482+ // there are subscribers.
483+ subscribers_list.container_node.appendChild(
484+ Y.Node.create('<div></div>')
485+ .addClass('subscribers-section'));
486+ subscribers_list.container_node.appendChild(
487+ Y.Node.create('<div></div>')
488+ .addClass('no-subscribers-indicator'));
489+
490+ // There is no indication of no subscribers anymore after
491+ // the call to resetNoSubscribers.
492+ subscribers_list.resetNoSubscribers();
493+ var no_subs_nodes = this.root.all(
494+ '.no-subscribers-indicator');
495+ Y.Assert.isTrue(no_subs_nodes.isEmpty());
496+ }
497+
498+}));
499+
500+
501+/**
502+ * Function to get a list of all the sections present in the
503+ * subscribers_list (a SubscribersList object).
504+ */
505+function _getAllSections(subscribers_list) {
506+ var nodes = [];
507+ var node;
508+ var all = subscribers_list.container_node.all('.subscribers-section');
509+ node = all.shift();
510+ while (node !== undefined) {
511+ nodes.push(node);
512+ node = all.shift();
513+ }
514+ return nodes;
515+}
516+
517+/**
518+ * Test subscribers section creation and helper methods.
519+ */
520+suite.add(new Y.Test.Case({
521+ name: 'SubscribersList._getOrCreateSection() test',
522+
523+ setUp: function() {
524+ this.root = Y.Node.create('<div></div>');
525+ Y.one('body').appendChild(this.root);
526+ },
527+
528+ tearDown: function() {
529+ this.root.remove();
530+ },
531+
532+ test_getSectionCSSClass: function() {
533+ // Returns a CSS class name to use for a section
534+ // for subscribers with a particular subscription level.
535+ var subscribers_list = setUpSubscribersList(this.root);
536+ Y.Assert.areEqual(
537+ 'subscribers-section-details',
538+ subscribers_list._getSectionCSSClass('Details'));
539+ },
540+
541+ test_getSection: function() {
542+ // Gets a subscribers section for the subscription level.
543+ var subscribers_list = setUpSubscribersList(this.root);
544+
545+ var section_node = Y.Node.create('<div></div>')
546+ .addClass('subscribers-section-lifecycle')
547+ .addClass('subscribers-section');
548+ subscribers_list.container_node.appendChild(section_node);
549+
550+ Y.Assert.areEqual(section_node,
551+ subscribers_list._getSection('lifecycle'));
552+ },
553+
554+ test_getSection_none: function() {
555+ // When there is no requested section, returns null.
556+ var subscribers_list = setUpSubscribersList(this.root);
557+
558+ var section_node = Y.Node.create('<div></div>')
559+ .addClass('subscribers-section-lifecycle')
560+ .addClass('subscribers-section');
561+ subscribers_list.container_node.appendChild(section_node);
562+
563+ Y.Assert.isNull(subscribers_list._getSection('details'));
564+ },
565+
566+ test_createSectionNode: function() {
567+ // Creates a subscribers section for the given subscription level.
568+ var subscribers_list = setUpSubscribersList(this.root);
569+
570+ var section_node = subscribers_list._createSectionNode('Discussion');
571+
572+ // A CSS class is added to the node for this particular level.
573+ Y.Assert.isTrue(
574+ section_node.hasClass('subscribers-section-discussion'));
575+ // As well as a generic CSS class to indicate it's a section.
576+ Y.Assert.isTrue(section_node.hasClass('subscribers-section'));
577+
578+ // Header is appropriate for the subscription level.
579+ var header = section_node.one('h3');
580+ Y.Assert.areEqual('Notified of all changes', header.get('text'));
581+
582+ // There is a separate node for the subscribers list in this section.
583+ Y.Assert.isNotNull(section_node.one('.subscribers-list'));
584+ },
585+
586+ test_insertSectionNode: function() {
587+ // Inserts a section node in the subscribers list.
588+ var subscribers_list = setUpSubscribersList(this.root);
589+
590+ var section_node = subscribers_list._createSectionNode('Details');
591+
592+ subscribers_list._insertSectionNode('Details', section_node);
593+ Y.ArrayAssert.itemsAreEqual(
594+ [section_node], _getAllSections(subscribers_list));
595+ },
596+
597+ test_insertSectionNode_before: function() {
598+ // Inserts a section node in front of the existing section
599+ // in the subscribers list.
600+ var subscribers_list = setUpSubscribersList(this.root);
601+
602+ // Sections we'll be inserting in the order they should end up in.
603+ var section_node1 = subscribers_list._createSectionNode('Discussion');
604+ var section_node2 = subscribers_list._createSectionNode('Details');
605+
606+ subscribers_list._insertSectionNode('Details', section_node2);
607+ Y.ArrayAssert.itemsAreEqual(
608+ [section_node2],
609+ _getAllSections(subscribers_list));
610+
611+ // Details section comes in front of the 'Discussion' section.
612+ subscribers_list._insertSectionNode('Discussion', section_node1);
613+ Y.ArrayAssert.itemsAreEqual(
614+ [section_node1, section_node2],
615+ _getAllSections(subscribers_list));
616+ },
617+
618+ test_insertSectionNode_after: function() {
619+ // Inserts a section node after the existing section
620+ // in the subscribers list.
621+ var subscribers_list = setUpSubscribersList(this.root);
622+
623+ // Sections we'll be inserting in the order they should end up in.
624+ var section_node1 = subscribers_list._createSectionNode('Discussion');
625+ var section_node2 = subscribers_list._createSectionNode('Maybe');
626+
627+ subscribers_list._insertSectionNode('Discussion', section_node1);
628+ Y.ArrayAssert.itemsAreEqual(
629+ [section_node1],
630+ _getAllSections(subscribers_list));
631+
632+ subscribers_list._insertSectionNode('Maybe', section_node2);
633+ Y.ArrayAssert.itemsAreEqual(
634+ [section_node1, section_node2],
635+ _getAllSections(subscribers_list));
636+ },
637+
638+ test_insertSectionNode_full_list: function() {
639+ // Inserts a section node in the appropriate place in the
640+ // subscribers list for all the possible subscription levels.
641+ var subscribers_list = setUpSubscribersList(this.root);
642+
643+ // Sections we'll be inserting in the order they should end up in.
644+ var section_node1 = subscribers_list._createSectionNode('Discussion');
645+ var section_node2 = subscribers_list._createSectionNode('Details');
646+ var section_node3 = subscribers_list._createSectionNode('Lifecycle');
647+ var section_node4 = subscribers_list._createSectionNode('Maybe');
648+
649+ subscribers_list._insertSectionNode('Lifecycle', section_node3);
650+ Y.ArrayAssert.itemsAreEqual(
651+ [section_node3], _getAllSections(subscribers_list));
652+
653+ subscribers_list._insertSectionNode('Discussion', section_node1);
654+ Y.ArrayAssert.itemsAreEqual(
655+ [section_node1, section_node3],
656+ _getAllSections(subscribers_list));
657+
658+ subscribers_list._insertSectionNode('Details', section_node2);
659+ Y.ArrayAssert.itemsAreEqual(
660+ [section_node1, section_node2, section_node3],
661+ _getAllSections(subscribers_list));
662+
663+ subscribers_list._insertSectionNode('Maybe', section_node4);
664+ Y.ArrayAssert.itemsAreEqual(
665+ [section_node1, section_node2, section_node3, section_node4],
666+ _getAllSections(subscribers_list));
667+ },
668+
669+ test_getOrCreateSection_get_existing: function() {
670+ // When there is an existing section, _getOrCreateSection
671+ // returns the existing node.
672+ var subscribers_list = setUpSubscribersList(this.root);
673+
674+ var section_node = subscribers_list._createSectionNode('Details');
675+ subscribers_list._insertSectionNode('Details', section_node);
676+
677+ Y.Assert.areSame(section_node,
678+ subscribers_list._getOrCreateSection('Details'));
679+
680+ },
681+
682+ test_getOrCreateSection_new: function() {
683+ // When there is no existing matching section, a new one
684+ // is created and added to the subscribers list.
685+ var subscribers_list = setUpSubscribersList(this.root);
686+
687+ var section_node = subscribers_list._getOrCreateSection('Details');
688+ Y.ArrayAssert.itemsAreEqual(
689+ [section_node],
690+ _getAllSections(subscribers_list));
691+ },
692+
693+ test_getOrCreateSection_positioning: function() {
694+ // When new sections are created, they are inserted into proper
695+ // positions using _insertSectionNode.
696+ var subscribers_list = setUpSubscribersList(this.root);
697+
698+ var section_node2 = subscribers_list._getOrCreateSection('Details');
699+ var section_node1 = subscribers_list._getOrCreateSection(
700+ 'Discussion');
701+ Y.ArrayAssert.itemsAreEqual(
702+ [section_node1, section_node2],
703+ _getAllSections(subscribers_list));
704+ },
705+
706+ test_getOrCreateSection_removes_no_subscribers_indication: function() {
707+ // When there is a div indicating no subscribers, _getOrCreateSection
708+ // removes it because it's adding a section where subscribers are
709+ // to come in.
710+ var subscribers_list = setUpSubscribersList(this.root);
711+
712+ // Add a div saying 'No other subscribers.'
713+ subscribers_list.resetNoSubscribers();
714+ Y.Assert.isNotNull(this.root.one('.no-subscribers-indicator'));
715+
716+ // And there is no matching div after _getOrCreateSection call.
717+ subscribers_list._getOrCreateSection('Details');
718+ Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
719+ }
720+
721+}));
722+
723+
724+/**
725+ * Test removal of a subscribers section.
726+ */
727+suite.add(new Y.Test.Case({
728+ name: 'SubscribersList._removeSectionNodeIfEmpty() test',
729+
730+ _should: {
731+ error: {
732+ test_sectionNodeHasSubscribers_error:
733+ new Error(
734+ 'No div.subscribers-list found inside the passed `node`.'),
735+ test_removeSectionNodeIfEmpty_non_section_error:
736+ new Error(
737+ 'Node is not a section node.')
738+ }
739+ },
740+
741+ setUp: function() {
742+ this.root = Y.Node.create('<div></div>');
743+ Y.one('body').appendChild(this.root);
744+ },
745+
746+ tearDown: function() {
747+ this.root.remove();
748+ },
749+
750+ test_sectionNodeHasSubscribers_error: function() {
751+ // When called on a node not containing the subscribers list,
752+ // it throws an error.
753+ var subscribers_list = setUpSubscribersList(this.root);
754+ var node = Y.Node.create('<div></div>');
755+ subscribers_list._sectionNodeHasSubscribers(node);
756+ },
757+
758+ test_sectionNodeHasSubscribers_no_subscribers: function() {
759+ // When called on a proper section node but with no subscribers,
760+ // it returns false.
761+ var subscribers_list = setUpSubscribersList(this.root);
762+ var node = subscribers_list._getOrCreateSection('Details');
763+ Y.Assert.isFalse(subscribers_list._sectionNodeHasSubscribers(node));
764+ },
765+
766+ test_sectionNodeHasSubscribers_subscribers: function() {
767+ // When called on a proper section node with subscribers,
768+ // it returns true.
769+ var subscribers_list = setUpSubscribersList(this.root);
770+ var node = subscribers_list._getOrCreateSection('Details');
771+ var subscriber = Y.Node.create('<div></div>')
772+ .addClass('subscriber');
773+ node.one('.subscribers-list').appendChild(subscriber);
774+ Y.Assert.isTrue(subscribers_list._sectionNodeHasSubscribers(node));
775+ },
776+
777+ test_removeSectionNodeIfEmpty_noop: function() {
778+ // When there is no requested section, nothing happens.
779+ var subscribers_list = setUpSubscribersList(this.root);
780+ var section_node = subscribers_list._getSection('Details');
781+ subscribers_list._removeSectionNodeIfEmpty(section_node);
782+ },
783+
784+ test_removeSectionNodeIfEmpty_non_section_error: function() {
785+ // When called on a node which is not a section, it throws
786+ // an exception.
787+ var subscribers_list = setUpSubscribersList(this.root);
788+ var section_node = Y.Node.create('<div></div>');
789+ subscribers_list._removeSectionNodeIfEmpty(section_node);
790+ },
791+
792+ test_removeSectionNodeIfEmpty_remove: function() {
793+ // When there is an empty section, it's removed.
794+ var subscribers_list = setUpSubscribersList(this.root);
795+ var section_node = subscribers_list._getOrCreateSection('Details');
796+
797+ subscribers_list._removeSectionNodeIfEmpty(section_node);
798+ Y.ArrayAssert.itemsAreEqual(
799+ [],
800+ _getAllSections(subscribers_list));
801+
802+ // Indication that there are no subscribers is added.
803+ Y.Assert.isNotNull(this.root.one('.no-subscribers-indicator'));
804+ },
805+
806+ test_removeSectionNodeIfEmpty_keep: function() {
807+ // When there is a section with a subscriber, it's not removed.
808+ var subscribers_list = setUpSubscribersList(this.root);
809+ var section_node = subscribers_list._getOrCreateSection('Details');
810+
811+ // Add a subscriber.
812+ section_node.one('.subscribers-list').appendChild(
813+ Y.Node.create('<div></div>')
814+ .addClass('subscriber'));
815+
816+ subscribers_list._removeSectionNodeIfEmpty(section_node);
817+ Y.ArrayAssert.itemsAreEqual(
818+ [section_node],
819+ _getAllSections(subscribers_list));
820+ // Indication that there are no subscribers is not added.
821+ Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
822+ },
823+
824+ test_removeSectionNodeIfEmpty_keeps_others: function() {
825+ // With two empty sections, only the requested one is removed.
826+ var subscribers_list = setUpSubscribersList(this.root);
827+ var section_node1 = subscribers_list._getOrCreateSection('Details');
828+ var section_node2 = subscribers_list._getOrCreateSection(
829+ 'Discussion');
830+
831+ var section_node = subscribers_list._getSection('Details');
832+ subscribers_list._removeSectionNodeIfEmpty(section_node);
833+ Y.ArrayAssert.itemsAreEqual(
834+ [section_node2],
835+ _getAllSections(subscribers_list));
836+ // Indication that there are no subscribers is not added.
837+ Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
838+ }
839+
840+}));
841+
842
843 var handle_complete = function(data) {
844 window.status = '::::' + JSON.stringify(data);