← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Данило Шеган has proposed merging lp:~danilo/launchpad/bug-772754-other-subscribers-sections into lp:launchpad with lp:~danilo/launchpad/bug-772754-base as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #772754 in Launchpad itself: "After better-bug-notification changes, list of bug subscribers is confusing"
  https://bugs.launchpad.net/launchpad/+bug/772754

For more details, see:
https://code.launchpad.net/~danilo/launchpad/bug-772754-other-subscribers-sections/+merge/64176

= 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).

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
-- 
https://code.launchpad.net/~danilo/launchpad/bug-772754-other-subscribers-sections/+merge/64176
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/bug-772754-other-subscribers-sections into lp:launchpad.
=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
--- lib/lp/bugs/javascript/subscribers_list.js	2011-05-18 18:37:07 +0000
+++ lib/lp/bugs/javascript/subscribers_list.js	2011-06-10 13:11:45 +0000
@@ -76,4 +76,214 @@
 }
 namespace.remove_user_link = remove_user_link;
 
-}, "0.1", {"requires": ["node", "lazr.anim"]});
+
+var CSS_CLASSES = {
+    section : 'subscribers-section',
+    list: 'subscribers-list',
+    subscriber: 'subscriber',
+    no_subscribers: 'no-subscribers-indicator'
+};
+
+/**
+ * Possible subscriber levels with descriptive headers for
+ * sections that will hold them.
+ */
+var subscriber_levels = {
+    'Discussion': 'Notified of all changes',
+    'Details': 'Notified of all changes except comments',
+    'Lifecycle': 'Notified when the bug is closed or reopened',
+    'Maybe': 'Maybe notified'
+};
+
+/**
+ * Order of subscribers sections.
+ */
+var subscriber_level_order = ['Discussion', 'Details', 'Lifecycle', 'Maybe'];
+
+
+/**
+ * Manages entire subscribers' list for a single bug.
+ *
+ * If the passed in container_box is not present, or if there are multiple
+ * nodes matching it, it throws an exception.
+ *
+ * @class SubscribersList
+ * @param config {Object} Configuration object containing at least
+ *   container_box value with the container div CSS selector
+ *   where to add the subscribers list.
+ */
+function SubscribersList(config) {
+    var container_nodes = Y.all(config.container_box);
+    if (container_nodes.size() === 0) {
+        Y.error('Container node must be specified in config.container_box.');
+    } else if (container_nodes.size() > 1) {
+        Y.error("Multiple container nodes for selector '" +
+                config.container_box + "' present in the page. " +
+                "You need to be more explicit.");
+    } else {
+        this.container_node = container_nodes.item(0);
+    }
+}
+namespace.SubscribersList = SubscribersList;
+
+/**
+ * Reset the subscribers list:
+ *  - If no sections with subscribers are left, it adds an indication
+ *    of no subscribers.
+ *  - If there are subscribers left, it ensures there is no indication
+ *    of no subscribers.
+ *
+ * @method resetNoSubscribers
+ */
+SubscribersList.prototype.resetNoSubscribers = function() {
+    var has_sections = (
+        this.container_node.one('.' + CSS_CLASSES.section) !== null);
+    var no_subs;
+    if (has_sections) {
+        // Make sure the indicator for no subscribers is not there.
+        no_subs = this.container_node.one('.' + CSS_CLASSES.no_subscribers);
+        if (no_subs !== null) {
+            no_subs.remove();
+        }
+    } else {
+        no_subs = Y.Node.create('<div></div>')
+            .addClass(CSS_CLASSES.no_subscribers)
+            .set('text', 'No other subscribers.');
+        this.container_node.appendChild(no_subs);
+    }
+};
+
+/**
+ * Get a CSS class to use for the section of the subscribers' list
+ * with subscriptions with the level `level`.
+ *
+ * @method _getSectionCSSClass
+ * @param level {String} Level of the subscription.
+ * @return {String} CSS class to use for the section for the `level`.
+ */
+SubscribersList.prototype._getSectionCSSClass = function(level) {
+    return CSS_CLASSES.section + '-' + level.toLowerCase();
+};
+
+/**
+ * Return the section node for a subscription level.
+ *
+ * @method _getSection
+ * @param level {String} Level of the subscription.
+ * @return {Object} Node containing the section or null.
+ */
+SubscribersList.prototype._getSection = function(level) {
+    return this.container_node.one('.' + this._getSectionCSSClass(level));
+};
+
+/**
+ * Create a subscribers section node depending on their level.
+ *
+ * @method _createSectionNode
+ * @param level {String} Level of the subscription.
+ * @return {Object} Node containing the entire section.
+ */
+SubscribersList.prototype._createSectionNode = function(level) {
+    // Container node for the entire section.
+    var node = Y.Node.create('<div></div>')
+        .addClass(CSS_CLASSES.section)
+        .addClass(this._getSectionCSSClass(level));
+    // Header.
+    node.appendChild(
+        Y.Node.create('<h3></h3>')
+            .set('text', subscriber_levels[level]));
+    // Node listing the actual subscribers.
+    node.appendChild(
+        Y.Node.create('<div></div>')
+            .addClass(CSS_CLASSES.list));
+    return node;
+};
+
+
+/**
+ * Inserts the section node in the appropriate place in the subscribers list.
+ * Uses `subscriber_level_order` to figure out what position should a section
+ * with subscribers on `level` hold.
+ *
+ * @method _insertSectionNode
+ * @param level {String} Level of the subscription.
+ * @param section_node {Object} Node to insert (containing
+ *   the entire section).
+ */
+SubscribersList.prototype._insertSectionNode = function(level, section_node) {
+    var index, existing_level;
+    var existing_level_node = null;
+    for (index=0; index < subscriber_level_order.length; index++) {
+        existing_level = subscriber_level_order[index];
+        if (existing_level === level) {
+            // Insert either at the beginning of the list,
+            // or after the last section which comes before this one.
+            if (existing_level_node === null) {
+                this.container_node.prepend(section_node);
+            } else {
+                existing_level_node.insert(section_node, 'after');
+            }
+        } else {
+            existing_level_node = this._getSection(existing_level);
+        }
+    }
+};
+
+
+/**
+ * Create a subscribers section depending on their level and
+ * add it to the other subscribers list.
+ * If section is already there, returns the existing node for it.
+ *
+ * @method _getOrCreateSection
+ * @param level {String} Level of the subscription.
+ * @return {Object} Node containing the entire section.
+ */
+SubscribersList.prototype._getOrCreateSection = function(level) {
+    var section_node = this._getSection(level);
+    if (section_node === null) {
+        section_node = this._createSectionNode(level);
+        this._insertSectionNode(level, section_node);
+    }
+    // Remove the indication of no subscribers if it's present.
+    this.resetNoSubscribers();
+    return section_node;
+};
+
+/**
+ * Return whether subscribers section has any subscribers or not.
+ *
+ * @method _sectionHasSubscribers
+ * @param node {Y.Node} Node containing the subscribers section.
+ * @return {Boolean} True if there are still subscribers in the section.
+ */
+SubscribersList.prototype._sectionNodeHasSubscribers = function(node) {
+    var list = node.one('.' + CSS_CLASSES.list);
+    if (list !== null) {
+        var has_any = (list.one('.' + CSS_CLASSES.subscriber) !== null);
+        return has_any;
+    } else {
+        Y.error(
+            'No div.subscribers-list found inside the passed `node`.');
+    }
+};
+
+/**
+ * Removes a subscribers section node if there are no remaining subscribers.
+ * Silently passes if nothing to remove.
+ *
+ * @method _removeSectionNodeIfEmpty
+ * @param node {Object} Section node containing all the subscribers.
+ */
+SubscribersList.prototype._removeSectionNodeIfEmpty = function(node) {
+    if (node !== null && !node.hasClass(CSS_CLASSES.section)) {
+        Y.error('Node is not a section node.');
+    }
+    if (node !== null && !this._sectionNodeHasSubscribers(node)) {
+        node.remove();
+        // Add the indication of no subscribers if this was the last section.
+        this.resetNoSubscribers();
+    }
+};
+
+}, "0.1", {"requires": ["node", "lazr.anim", "lp.client"]});

=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
--- lib/lp/bugs/javascript/tests/test_subscribers_list.js	2011-05-18 18:34:19 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js	2011-06-10 13:11:45 +0000
@@ -12,7 +12,7 @@
 /**
  * Set-up all the nodes required for subscribers list testing.
  */
-function setUpSubscribersList(root_node, with_dupes) {
+function setUpOldSubscribersList(root_node, with_dupes) {
     // Set-up subscribers list.
     var direct_links = Y.Node.create('<div></div>')
         .set('id', 'subscribers-links');
@@ -50,7 +50,7 @@
     test_no_subscribers: function() {
         // There are no subscribers left in the subscribers_list
         // (iow, subscribers_links is empty).
-        var subscribers_list = setUpSubscribersList(this.root);
+        var subscribers_list = setUpOldSubscribersList(this.root);
 
         // Resetting the list adds a 'None' div to the
         // subscribers_list (and not to the subscriber_links).
@@ -66,7 +66,7 @@
     test_subscribers: function() {
         // When there is at least one subscriber, nothing
         // happens when reset() is called.
-        var subscribers_list = setUpSubscribersList(this.root);
+        var subscribers_list = setUpOldSubscribersList(this.root);
         var subscribers_links = subscribers_list.one('#subscribers-links');
         subscribers_links.appendChild(
             Y.Node.create('<div>Subscriber</div>'));
@@ -80,7 +80,7 @@
 
     test_empty_duplicates: function() {
         // There are no subscribers among the duplicate subscribers.
-        var subscribers_list = setUpSubscribersList(this.root, true);
+        var subscribers_list = setUpOldSubscribersList(this.root, true);
         var dupe_subscribers = this.root.one('#subscribers-from-duplicates');
 
         // Resetting the list removes the entire duplicate subscribers node.
@@ -92,7 +92,7 @@
     test_duplicates: function() {
         // There are subscribers among the duplicate subscribers,
         // and nothing changes.
-        var subscribers_list = setUpSubscribersList(this.root, true);
+        var subscribers_list = setUpOldSubscribersList(this.root, true);
         var dupe_subscribers = this.root.one('#subscribers-from-duplicates');
         dupe_subscribers.appendChild(Y.Node.create('<div>Subscriber</div>'));
 
@@ -145,7 +145,7 @@
         // If there is no matching subscriber, removal silently passes.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root);
+        setUpOldSubscribersList(this.root);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -169,7 +169,7 @@
         // before animation starts.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root);
+        setUpOldSubscribersList(this.root);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -192,7 +192,7 @@
         // If there is a direct subscriber, removal works fine.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root);
+        setUpOldSubscribersList(this.root);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -215,7 +215,7 @@
         // a duplicate subscription link does nothing.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root);
+        setUpOldSubscribersList(this.root);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -235,7 +235,7 @@
         // If there is a duplicate subscriber, removal works fine.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root, true);
+        setUpOldSubscribersList(this.root, true);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -258,7 +258,7 @@
         // direct subscription user link doesn't do anything.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root, true);
+        setUpOldSubscribersList(this.root, true);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -279,7 +279,7 @@
         // subscribed through duplicate, removal removes only one link.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root, true);
+        setUpOldSubscribersList(this.root, true);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -305,7 +305,7 @@
         // subscribed through duplicate, removal removes only one link.
 
         // Set-up subscribers list.
-        setUpSubscribersList(this.root, true);
+        setUpOldSubscribersList(this.root, true);
 
         var person = new Y.lp.bugs.subscriber.Subscriber({
             uri: 'myself',
@@ -327,6 +327,494 @@
     }
 }));
 
+/**
+ * Set-up all the nodes required for subscribers list testing.
+ */
+function setUpSubscribersList(root_node) {
+    // Set-up subscribers list.
+    var node = Y.Node.create('<div></div>')
+        .set('id', 'other-subscribers-container');
+    root_node.appendChild(node);
+    var config = {
+        container_box: '#other-subscribers-container'
+    };
+    return new module.SubscribersList(config);
+}
+
+/**
+ * Test resetting of the no subscribers indication.
+ */
+suite.add(new Y.Test.Case({
+    name: 'SubscribersList constructor test',
+
+    _should: {
+        error: {
+            test_no_container_error:
+            new Error(
+                'Container node must be specified in config.container_box.'),
+            test_multiple_containers_error:
+            new Error(
+                "Multiple container nodes for selector '.container' "+
+                    "present in the page. You need to be more explicit.")
+        }
+    },
+
+    setUp: function() {
+        this.root = Y.Node.create('<div></div>');
+        Y.one('body').appendChild(this.root);
+    },
+
+    tearDown: function() {
+        this.root.remove();
+    },
+
+    test_no_container_error: function() {
+        // When there is no matching container node in the DOM tree,
+        // an exception is thrown.
+        var sl = new module.SubscribersList({container_box: '#not-found'});
+    },
+
+    test_single_container: function() {
+        // With an exactly single container node matches, all is well.
+        var container_node = Y.Node.create('<div></div>')
+            .set('id', 'container');
+        this.root.appendChild(container_node);
+        var list = new module.SubscribersList({container_box: '#container'});
+        Y.Assert.areSame(container_node, list.container_node);
+    },
+
+    test_multiple_containers_error: function() {
+        // With two nodes matching the given CSS selector,
+        // an exception is thrown.
+        this.root.appendChild(
+            Y.Node.create('<div></div>').addClass('container'));
+        this.root.appendChild(
+            Y.Node.create('<div></div>').addClass('container'));
+        var sl = new module.SubscribersList({container_box: '.container'});
+    }
+}));
+
+
+/**
+ * Test resetting of the no subscribers indication.
+ */
+suite.add(new Y.Test.Case({
+    name: 'SubscribersList.resetNoSubscribers() test',
+
+    setUp: function() {
+        this.root = Y.Node.create('<div></div>');
+        Y.one('body').appendChild(this.root);
+    },
+
+    tearDown: function() {
+        this.root.remove();
+    },
+
+    test_initially_empty: function() {
+        // When the SubscribersList is set-up, it's initially
+        // entirely empty.
+        var subscribers_list = setUpSubscribersList(this.root);
+        Y.Assert.isTrue(
+            subscribers_list.container_node.all().isEmpty());
+    },
+
+    test_no_subscribers: function() {
+        // When resetNoSubscribers() is called on an empty
+        // SubscribersList, indication of no subscribers is added.
+        var subscribers_list = setUpSubscribersList(this.root);
+        subscribers_list.resetNoSubscribers();
+        var no_subs_nodes = this.root.all(
+            '.no-subscribers-indicator');
+        Y.Assert.areEqual(1, no_subs_nodes.size());
+        Y.Assert.areEqual('No other subscribers.',
+                          no_subs_nodes.item(0).get('text'));
+    },
+
+    test_subscribers_no_addition: function() {
+        // When resetNoSubscribers() is called on a SubscribersList
+        // with some subscribers, no indication of no subscribers is added.
+        var subscribers_list = setUpSubscribersList(this.root);
+        // Hack a section node into the list so it appears as if
+        // there are subscribers.
+        subscribers_list.container_node.appendChild(
+            Y.Node.create('<div></div>')
+                .addClass('subscribers-section'));
+
+        // There is no indication of no subscribers added by
+        // resetNoSubscribers.
+        subscribers_list.resetNoSubscribers();
+        var no_subs_nodes = this.root.all(
+            '.no-subscribers-indicator');
+        Y.Assert.isTrue(no_subs_nodes.isEmpty());
+    },
+
+    test_subscribers_remove_previous_indication: function() {
+        // When resetNoSubscribers() is called on a SubscribersList
+        // with some subscribers, existing indication of no subscribers
+        // is removed.
+        var subscribers_list = setUpSubscribersList(this.root);
+        // Hack a section node into the list so it appears as if
+        // there are subscribers.
+        subscribers_list.container_node.appendChild(
+            Y.Node.create('<div></div>')
+                .addClass('subscribers-section'));
+        subscribers_list.container_node.appendChild(
+            Y.Node.create('<div></div>')
+                .addClass('no-subscribers-indicator'));
+
+        // There is no indication of no subscribers anymore after
+        // the call to resetNoSubscribers.
+        subscribers_list.resetNoSubscribers();
+        var no_subs_nodes = this.root.all(
+            '.no-subscribers-indicator');
+        Y.Assert.isTrue(no_subs_nodes.isEmpty());
+    }
+
+}));
+
+
+/**
+ * Function to get a list of all the sections present in the
+ * subscribers_list (a SubscribersList object).
+ */
+function _getAllSections(subscribers_list) {
+    var nodes = [];
+    var node;
+    var all = subscribers_list.container_node.all('.subscribers-section');
+    node = all.shift();
+    while (node !== undefined) {
+        nodes.push(node);
+        node = all.shift();
+    }
+    return nodes;
+}
+
+/**
+ * Test subscribers section creation and helper methods.
+ */
+suite.add(new Y.Test.Case({
+    name: 'SubscribersList._getOrCreateSection() test',
+
+    setUp: function() {
+        this.root = Y.Node.create('<div></div>');
+        Y.one('body').appendChild(this.root);
+    },
+
+    tearDown: function() {
+        this.root.remove();
+    },
+
+    test_getSectionCSSClass: function() {
+        // Returns a CSS class name to use for a section
+        // for subscribers with a particular subscription level.
+        var subscribers_list = setUpSubscribersList(this.root);
+        Y.Assert.areEqual(
+            'subscribers-section-details',
+            subscribers_list._getSectionCSSClass('Details'));
+    },
+
+    test_getSection: function() {
+        // Gets a subscribers section for the subscription level.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node = Y.Node.create('<div></div>')
+            .addClass('subscribers-section-lifecycle')
+            .addClass('subscribers-section');
+        subscribers_list.container_node.appendChild(section_node);
+
+        Y.Assert.areEqual(section_node,
+                          subscribers_list._getSection('lifecycle'));
+    },
+
+    test_getSection_none: function() {
+        // When there is no requested section, returns null.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node = Y.Node.create('<div></div>')
+            .addClass('subscribers-section-lifecycle')
+            .addClass('subscribers-section');
+        subscribers_list.container_node.appendChild(section_node);
+
+        Y.Assert.isNull(subscribers_list._getSection('details'));
+    },
+
+    test_createSectionNode: function() {
+        // Creates a subscribers section for the given subscription level.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node = subscribers_list._createSectionNode('Discussion');
+
+        // A CSS class is added to the node for this particular level.
+        Y.Assert.isTrue(
+            section_node.hasClass('subscribers-section-discussion'));
+        // As well as a generic CSS class to indicate it's a section.
+        Y.Assert.isTrue(section_node.hasClass('subscribers-section'));
+
+        // Header is appropriate for the subscription level.
+        var header = section_node.one('h3');
+        Y.Assert.areEqual('Notified of all changes', header.get('text'));
+
+        // There is a separate node for the subscribers list in this section.
+        Y.Assert.isNotNull(section_node.one('.subscribers-list'));
+    },
+
+    test_insertSectionNode: function() {
+        // Inserts a section node in the subscribers list.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        // Sections we'll be inserting in the order they should end up in.
+        var section_node = subscribers_list._createSectionNode('Details');
+
+        subscribers_list._insertSectionNode('Details', section_node);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node], _getAllSections(subscribers_list));
+    },
+
+    test_insertSectionNode_before: function() {
+        // Inserts a section node in front of the existing section
+        // in the subscribers list.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        // Sections we'll be inserting in the order they should end up in.
+        var section_node1 = subscribers_list._createSectionNode('Discussion');
+        var section_node2 = subscribers_list._createSectionNode('Details');
+
+        subscribers_list._insertSectionNode('Details', section_node2);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node2],
+            _getAllSections(subscribers_list));
+
+        // Details section comes in front of the 'Discussion' section.
+        subscribers_list._insertSectionNode('Discussion', section_node1);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node2],
+            _getAllSections(subscribers_list));
+    },
+
+    test_insertSectionNode_after: function() {
+        // Inserts a section node after the existing section
+        // in the subscribers list.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        // Sections we'll be inserting in the order they should end up in.
+        var section_node1 = subscribers_list._createSectionNode('Discussion');
+        var section_node2 = subscribers_list._createSectionNode('Details');
+
+        subscribers_list._insertSectionNode('Discussion', section_node1);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1],
+            _getAllSections(subscribers_list));
+
+        subscribers_list._insertSectionNode('Details', section_node2);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node2],
+            _getAllSections(subscribers_list));
+    },
+
+    test_insertSectionNode_full_list: function() {
+        // Inserts a section node in the appropriate place in the
+        // subscribers list for all the possible subscription levels.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        // Sections we'll be inserting in the order they should end up in.
+        var section_node1 = subscribers_list._createSectionNode('Discussion');
+        var section_node2 = subscribers_list._createSectionNode('Details');
+        var section_node3 = subscribers_list._createSectionNode('Lifecycle');
+        var section_node4 = subscribers_list._createSectionNode('Maybe');
+
+        subscribers_list._insertSectionNode('Lifecycle', section_node3);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node3], _getAllSections(subscribers_list));
+
+        subscribers_list._insertSectionNode('Discussion', section_node1);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node3],
+            _getAllSections(subscribers_list));
+
+        subscribers_list._insertSectionNode('Details', section_node2);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node2, section_node3],
+            _getAllSections(subscribers_list));
+
+        subscribers_list._insertSectionNode('Maybe', section_node4);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node2, section_node3, section_node4],
+            _getAllSections(subscribers_list));
+    },
+
+    test_getOrCreateSection_get_existing: function() {
+        // When there is an existing section, _getOrCreateSection
+        // returns the existing node.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node = subscribers_list._createSectionNode('Details');
+        subscribers_list._insertSectionNode('Details', section_node);
+
+        Y.Assert.areSame(section_node,
+                         subscribers_list._getOrCreateSection('Details'));
+
+    },
+
+    test_getOrCreateSection_new: function() {
+        // When there is no existing matching section, a new one
+        // is created and added to the subscribers list.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node = subscribers_list._getOrCreateSection('Details');
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node],
+            _getAllSections(subscribers_list));
+    },
+
+    test_getOrCreateSection_positioning: function() {
+        // When new sections are created, they are inserted into proper
+        // positions using _insertSectionNode.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        var section_node2 = subscribers_list._getOrCreateSection('Details');
+        var section_node1 = subscribers_list._getOrCreateSection(
+            'Discussion');
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node1, section_node2],
+            _getAllSections(subscribers_list));
+    },
+
+    test_getOrCreateSection_removes_no_subscribers_indication: function() {
+        // When there is a div indicating no subscribers, _getOrCreateSection
+        // removes it because it's adding a section where subscribers are
+        // to come in.
+        var subscribers_list = setUpSubscribersList(this.root);
+
+        // Add a div saying 'No other subscribers.'
+        subscribers_list.resetNoSubscribers();
+        Y.Assert.isNotNull(this.root.one('.no-subscribers-indicator'));
+
+        // And there is no matching div after _getOrCreateSection call.
+        subscribers_list._getOrCreateSection('Details');
+        Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
+    }
+
+}));
+
+
+/**
+ * Test removal of a subscribers section.
+ */
+suite.add(new Y.Test.Case({
+    name: 'SubscribersList._removeSectionNodeIfEmpty() test',
+
+    _should: {
+        error: {
+            test_sectionNodeHasSubscribers_error:
+            new Error(
+                'No div.subscribers-list found inside the passed `node`.'),
+            test_removeSectionNodeIfEmpty_non_section_error:
+            new Error(
+                'Node is not a section node.')
+        }
+    },
+
+    setUp: function() {
+        this.root = Y.Node.create('<div></div>');
+        Y.one('body').appendChild(this.root);
+    },
+
+    tearDown: function() {
+        this.root.remove();
+    },
+
+    test_sectionNodeHasSubscribers_error: function() {
+        // When called on a node not containing the subscribers list,
+        // it throws an error.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var node = Y.Node.create('<div></div>');
+        subscribers_list._sectionNodeHasSubscribers(node);
+    },
+
+    test_sectionNodeHasSubscribers_no_subscribers: function() {
+        // When called on a proper section node but with no subscribers,
+        // it returns false.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var node = subscribers_list._getOrCreateSection('Details');
+        Y.Assert.isFalse(subscribers_list._sectionNodeHasSubscribers(node));
+    },
+
+    test_sectionNodeHasSubscribers_subscribers: function() {
+        // When called on a proper section node with subscribers,
+        // it returns true.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var node = subscribers_list._getOrCreateSection('Details');
+        var subscriber = Y.Node.create('<div></div>')
+            .addClass('subscriber');
+        node.one('.subscribers-list').appendChild(subscriber);
+        Y.Assert.isTrue(subscribers_list._sectionNodeHasSubscribers(node));
+    },
+
+    test_removeSectionNodeIfEmpty_noop: function() {
+        // When there is no requested section, nothing happens.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var section_node = subscribers_list._getSection('Details');
+        subscribers_list._removeSectionNodeIfEmpty(section_node);
+    },
+
+    test_removeSectionNodeIfEmpty_non_section_error: function() {
+        // When called on a node which is not a section, it throws
+        // an exception.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var section_node = Y.Node.create('<div></div>');
+        subscribers_list._removeSectionNodeIfEmpty(section_node);
+    },
+
+    test_removeSectionNodeIfEmpty_remove: function() {
+        // When there is an empty section, it's removed.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var section_node = subscribers_list._getOrCreateSection('Details');
+
+        subscribers_list._removeSectionNodeIfEmpty(section_node);
+        Y.ArrayAssert.itemsAreEqual(
+            [],
+            _getAllSections(subscribers_list));
+
+        // Indication that there are no subscribers is added.
+        Y.Assert.isNotNull(this.root.one('.no-subscribers-indicator'));
+    },
+
+    test_removeSectionNodeIfEmpty_keep: function() {
+        // When there is a section with a subscriber, it's not removed.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var section_node = subscribers_list._getOrCreateSection('Details');
+
+        // Add a subscriber.
+        section_node.one('.subscribers-list').appendChild(
+            Y.Node.create('<div></div>')
+                .addClass('subscriber'));
+
+        subscribers_list._removeSectionNodeIfEmpty(section_node);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node],
+            _getAllSections(subscribers_list));
+        // Indication that there are no subscribers is not added.
+        Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
+    },
+
+    test_removeSectionNodeIfEmpty_keeps_others: function() {
+        // With two empty sections, only the requested one is removed.
+        var subscribers_list = setUpSubscribersList(this.root);
+        var section_node1 = subscribers_list._getOrCreateSection('Details');
+        var section_node2 = subscribers_list._getOrCreateSection(
+            'Discussion');
+
+        var section_node = subscribers_list._getSection('Details');
+        subscribers_list._removeSectionNodeIfEmpty(section_node);
+        Y.ArrayAssert.itemsAreEqual(
+            [section_node2],
+            _getAllSections(subscribers_list));
+        // Indication that there are no subscribers is not added.
+        Y.Assert.isNull(this.root.one('.no-subscribers-indicator'));
+    }
+
+}));
+
 
 var handle_complete = function(data) {
     status_node = Y.Node.create(


Follow ups