launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03898
[Merge] lp:~danilo/launchpad/bug-772754-other-subscribers-subscribers into lp:launchpad
Данило Шеган has proposed merging lp:~danilo/launchpad/bug-772754-other-subscribers-subscribers into lp:launchpad with lp:~danilo/launchpad/bug-772754-other-subscribers-lp-names 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-subscribers/+merge/64179
= Bug 772754: Other subscribers list, part 3 =
This is part of ongoing work for providing the "other subscribers" list as indicated in mockup https://launchpadlibrarian.net/71552495/all-in-one.png attached to bug 772754 by Gary.
This branch continues on the previous branches to provide methods to add/remove subscribers and add an unsubscribe action for a subscriber when needed.
It is comprehensively tested.
== 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.html
lib/lp/bugs/javascript/tests/test_subscribers_list.js
--
https://code.launchpad.net/~danilo/launchpad/bug-772754-other-subscribers-subscribers/+merge/64179
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~danilo/launchpad/bug-772754-other-subscribers-subscribers into lp:launchpad.
=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
--- lib/lp/bugs/javascript/subscribers_list.js 2011-06-10 13:23:27 +0000
+++ lib/lp/bugs/javascript/subscribers_list.js 2011-06-10 13:23:28 +0000
@@ -81,7 +81,9 @@
section : 'subscribers-section',
list: 'subscribers-list',
subscriber: 'subscriber',
- no_subscribers: 'no-subscribers-indicator'
+ no_subscribers: 'no-subscribers-indicator',
+ actions: 'subscriber-actions',
+ unsubscribe: 'unsubscribe-action'
};
/**
@@ -102,6 +104,20 @@
/**
+ * Checks if the subscription level is one of the acceptable ones.
+ * Throws an error if not, otherwise returns true.
+ */
+function checkSubscriptionLevel(level) {
+ if (!subscriber_levels.hasOwnProperty(level)) {
+ Y.error(
+ 'Level "' + level + '" is not an acceptable subscription level.');
+ }
+ return true;
+}
+
+
+
+/**
* Manages entire subscribers' list for a single bug.
*
* If the passed in container_box is not present, or if there are multiple
@@ -286,4 +302,245 @@
}
};
-}, "0.1", {"requires": ["node", "lazr.anim", "lp.client"]});
+/**
+ * Get a string value usable as the ID for the node based on
+ * the subscriber name.
+ */
+SubscribersList.prototype._getNodeIdForSubscriberName = function(name) {
+ return CSS_CLASSES.subscriber + '-' + Y.lp.names.launchpad_to_css(name);
+};
+
+/**
+ * Validate and sanitize a subscriber object.
+ * It must have at least a `name` attribute.
+ * If `display_name` is not set, the value from `name` is used instead.
+ *
+ * @method _validateSubscriber
+ * @param subscriber {Object} Object containing `name`, `display_name`,
+ * `web_link` and `is_team` indicator for the subscriber.
+ * If `display_name` is undefined, sets it to the same value as `name`.
+ * If `web_link` is not set, sets it to "/~name".
+ * @return {Object} Modified `subscriber` object.
+ */
+SubscribersList.prototype._validateSubscriber = function(subscriber) {
+ if (!Y.Lang.isString(subscriber.name)) {
+ Y.error('No `name` passed in `subscriber`.');
+ }
+ if (!Y.Lang.isString(subscriber.display_name)) {
+ // Default to `name` for display_name.
+ subscriber.display_name = subscriber.name;
+ }
+ if (!Y.Lang.isString(subscriber.web_link)) {
+ // Default to `/~name` for web_link.
+ subscriber.web_link = '/~' + subscriber.name;
+ }
+ return subscriber;
+};
+
+/**
+ * Creates and returns a node for the `subscriber`.
+ *
+ * It makes a link using subscriber.display_name as the link text,
+ * and linking to /~`subscriber.name`.
+ * Everything is wrapped in a div.subscriber node.
+ *
+ * @method _createSubscriberNode
+ * @param subscriber {Object} Object containing `name`, `display_name`
+ * `web_link` and `is_team` attributes.
+ * @return {Object} Node containing a subscriber link.
+ */
+SubscribersList.prototype._createSubscriberNode = function(subscriber) {
+ var subscriber_node = Y.Node.create('<div></div>')
+ .addClass(CSS_CLASSES.subscriber);
+
+ var subscriber_link = Y.Node.create('<a></a>');
+ subscriber_link.set('href', subscriber.web_link);
+
+ var subscriber_text = Y.Node.create('<span></span>')
+ .addClass('sprite')
+ .set('text', subscriber.display_name);
+ if (subscriber.is_team === true) {
+ subscriber_text.addClass('team');
+ } else {
+ subscriber_text.addClass('person');
+ }
+ subscriber_link.appendChild(subscriber_text);
+
+ subscriber_node.appendChild(subscriber_link);
+ return subscriber_node;
+};
+
+/**
+ * Add or change a subscriber in the subscribers list.
+ *
+ * If subscriber is already in the list and in a different subscription
+ * level section, it is moved to the appropriate section indicated by
+ * `level` parameter. If subscriber is already in the list and subscribed
+ * at the same level, nothing happens.
+ *
+ * @method addSubscriber
+ * @param subscriber {Object} Object containing `name`, `display_name`
+ * `web_link` and `is_team` attributes describing the subscriber.
+ * @param level {String} Level of the subscription.
+ * @param config {Object} Object containing potential 'unsubscribe' callback
+ * in the `unsubscribe_callback` attribute.
+ */
+SubscribersList.prototype.addSubscriber = function(subscriber, level,
+ config) {
+ checkSubscriptionLevel(level);
+ subscriber = this._validateSubscriber(subscriber);
+
+ var section_node = this._getOrCreateSection(level);
+ var list_node = section_node.one('.' + CSS_CLASSES.list);
+
+ var subscriber_id = this._getNodeIdForSubscriberName(subscriber.name);
+ var subscriber_node = this.container_node.one('#' + subscriber_id);
+
+ if (subscriber_node === null) {
+ subscriber_node = this._createSubscriberNode(subscriber);
+ subscriber_node.set('id', subscriber_id);
+ // Insert the subscriber at the start of the list.
+ list_node.prepend(subscriber_node);
+ // Add the unsubscribe action if needed.
+ if (Y.Lang.isValue(config) &&
+ Y.Lang.isFunction(config.unsubscribe_callback)) {
+ this.addUnsubscribeAction(
+ subscriber, config.unsubscribe_callback);
+ }
+ } else {
+ // Move the subscriber node from the existing section to the new one.
+ var existing_section = subscriber_node.ancestor(
+ '.' + CSS_CLASSES.section);
+ if (existing_section === null) {
+ Y.error("Matching subscriber node doesn't seem to be in any " +
+ "subscribers list sections.");
+ }
+ if (existing_section !== section_node) {
+ // We do not destroy the node so we can insert it into
+ // the appropriate position.
+ subscriber_node.remove();
+ this._removeSectionNodeIfEmpty(existing_section);
+ // Insert the subscriber at the start of the list.
+ list_node.prepend(subscriber_node);
+ }
+ // else:
+ // Subscriber is already there in the same section. A no-op.
+ }
+
+ return subscriber_node;
+};
+
+/**
+ * Get a subscriber node for the passed in subscriber.
+ *
+ * If subscriber is not in the list already, it fails with an exception.
+ *
+ * @method _getSubscriberNode
+ * @param subscriber {Object} Object containing at least `name`
+ * for the subscriber.
+ */
+SubscribersList.prototype._getSubscriberNode = function(subscriber) {
+ subscriber = this._validateSubscriber(subscriber);
+
+ var subscriber_id = this._getNodeIdForSubscriberName(subscriber.name);
+ var subscriber_node = this.container_node.one('#' + subscriber_id);
+
+ if (subscriber_node === null) {
+ Y.error('Subscriber is not present in the subscribers list. ' +
+ 'Please call addSubscriber(subscriber) first.');
+ }
+ return subscriber_node;
+};
+
+/**
+ * Create a subscriber actions node to hold actions like unsubscribe.
+ * If the node already exists, returns it instead.
+ *
+ * @method _getOrCreateActionsNode
+ * @param subscriber_node {Object} Node for a particular subscriber.
+ * @return {Object} A node suitable for putting subscriber actions in.
+ */
+SubscribersList.prototype._getOrCreateActionsNode = function(subscriber_node)
+{
+ var actions_node = subscriber_node.one('.' + CSS_CLASSES.actions);
+ if (actions_node === null) {
+ // Create a node to hold all the actions.
+ actions_node = Y.Node.create('<span></span>')
+ .addClass(CSS_CLASSES.actions)
+ .setStyle('float', 'right');
+ subscriber_node.appendChild(actions_node);
+ }
+ return actions_node;
+};
+
+/**
+ * Adds an unsubscribe action for the subscriber.
+ *
+ * It creates a separate actions node which will hold any actions
+ * (including unsubscribe one), and creates a "remove" link with the
+ * on.click action set to call `callback` function with subscriber
+ * passed in as the parameter.
+ *
+ * If `subscriber` does not have at least the `name` attribute,
+ * an exception is thrown.
+ * If `callback` is not a function, it throws an exception.
+ *
+ * @method addUnsubscribeAction
+ * @param subscriber {Object} Object containing `name`, `display_name`
+ * `web_link` and `is_team` attributes describing the subscriber.
+ * @param callback {Function} Function to call on clicking the unsubscribe
+ * button. It will be passed `this` (a SubscribersList) as the first,
+ * and `subscriber` as the second parameter.
+ */
+SubscribersList.prototype.addUnsubscribeAction = function(subscriber,
+ callback) {
+ subscriber = this._validateSubscriber(subscriber);
+ if (!Y.Lang.isFunction(callback)) {
+ Y.error('Passed in callback for unsubscribe action ' +
+ 'is not a function.');
+ }
+ var subscriber_node = this._getSubscriberNode(subscriber);
+ var actions_node = this._getOrCreateActionsNode(subscriber_node);
+ var unsubscribe_node = actions_node.one('.' + CSS_CLASSES.unsubscribe);
+ if (unsubscribe_node === null) {
+ unsubscribe_node = Y.Node.create('<a></a>')
+ .addClass(CSS_CLASSES.unsubscribe)
+ .set('href', '+subscribe')
+ .set('title', 'Unsubscribe ' + subscriber.display_name);
+ unsubscribe_node.appendChild(
+ Y.Node.create('<img></img>')
+ .set('src', '/@@/remove')
+ .set('alt', 'Remove'));
+ var subscriber_list = this;
+ unsubscribe_node.on('click', function(e) {
+ e.halt();
+ callback(subscriber_list, subscriber);
+ });
+ actions_node.appendChild(unsubscribe_node);
+ }
+};
+
+/**
+ * Remove a subscriber node for the `subscriber`.
+ *
+ * If subscriber is not in the list already, it fails with an exception.
+ *
+ * @method removeSubscriber
+ * @param subscriber {Object} Object containing at least `name`
+ * for the subscriber.
+ */
+SubscribersList.prototype.removeSubscriber = function(subscriber) {
+ subscriber = this._validateSubscriber(subscriber);
+ var subscriber_node = this._getSubscriberNode(subscriber);
+ var existing_section = subscriber_node.ancestor(
+ '.' + CSS_CLASSES.section);
+ subscriber_node.remove(true);
+ if (existing_section === null) {
+ Y.error("Matching subscriber node doesn't seem to be in any " +
+ "subscribers list sections.");
+ }
+ this._removeSectionNodeIfEmpty(existing_section);
+};
+
+
+}, "0.1", {"requires": ["node", "lazr.anim", "lp.client", "lp.names"]});
=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.html'
--- lib/lp/bugs/javascript/tests/test_subscribers_list.html 2011-04-28 13:32:29 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers_list.html 2011-06-10 13:23:28 +0000
@@ -20,6 +20,8 @@
src="../../../app/javascript/client.js"></script>
<script type="text/javascript"
src="../../../app/javascript/errors.js"></script>
+ <script type="text/javascript"
+ src="../../../app/javascript/lp-names.js"></script>
<!-- Pre-requisite -->
<script type="text/javascript"
=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-10 13:23:27 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-10 13:23:28 +0000
@@ -816,6 +816,426 @@
}));
+
+/**
+ * Test adding of subscribers and relevant helper methods.
+ */
+suite.add(new Y.Test.Case({
+ name: 'SubscribersList.addSubscriber() test',
+
+ setUp: function() {
+ this.root = Y.Node.create('<div></div>');
+ Y.one('body').appendChild(this.root);
+ },
+
+ tearDown: function() {
+ this.root.remove();
+ },
+
+ _should: {
+ error: {
+ test_validateSubscriber_no_name_error:
+ new Error('No `name` passed in `subscriber`.'),
+ test_addSubscriber_incorrect_level:
+ new Error(
+ 'Level "Test" is not an acceptable subscription level.'),
+ test_addSubscriber_not_in_section_error:
+ new Error(
+ "Matching subscriber node doesn't seem to be in any " +
+ "subscribers list sections.")
+ }
+ },
+
+ test_getNodeIdForSubscriberName: function() {
+ // Returns a CSS class name to use as the ID for subscribers
+ // prefixed with 'subscriber-'.
+ // Uses launchpad_to_css for escaping (eg. it replaces '+' with '_y').
+ var subscribers_list = setUpSubscribersList(this.root);
+ Y.Assert.areEqual(
+ 'subscriber-danilo_y',
+ subscribers_list._getNodeIdForSubscriberName('danilo+'));
+ },
+
+ test_validateSubscriber: function() {
+ // Ensures a passed in subscriber object has at least the
+ // `name` attribute. Presets display_name and web_link
+ // values based on it.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ subscriber = subscribers_list._validateSubscriber(subscriber);
+ Y.Assert.areEqual('user', subscriber.name);
+ Y.Assert.areEqual('user', subscriber.display_name);
+ Y.Assert.areEqual('/~user', subscriber.web_link);
+ },
+
+ test_validateSubscriber_no_name_error: function() {
+ // When no name attribute is present, an exception is thrown.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { };
+ subscribers_list._validateSubscriber(subscriber);
+ },
+
+ test_validateSubscriber_no_overriding: function() {
+ // Attributes display_name and web_link are not overridden if
+ // already set.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = {
+ name: 'user',
+ display_name: 'User Name',
+ web_link: 'http://launchpad.net/'
+ };
+ subscriber = subscribers_list._validateSubscriber(subscriber);
+ Y.Assert.areEqual('user', subscriber.name);
+ Y.Assert.areEqual('User Name', subscriber.display_name);
+ Y.Assert.areEqual('http://launchpad.net/', subscriber.web_link);
+ },
+
+ test_createSubscriberNode: function() {
+ // When passed a subscriber object, it constructs a node
+ // containing a link to the subscriber (using web_link for the
+ // link target, and display name for the text).
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = {
+ name: 'user',
+ display_name: 'User Name',
+ web_link: 'http://launchpad.net/~user'
+ };
+ var node = subscribers_list._createSubscriberNode(subscriber);
+ Y.Assert.isTrue(node.hasClass('subscriber'));
+
+ var link = node.one('a');
+ Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));
+
+ var text = link.one('span');
+ Y.Assert.areEqual('User Name', text.get('text'));
+ Y.Assert.isTrue(text.hasClass('sprite'));
+ Y.Assert.isTrue(text.hasClass('person'));
+ },
+
+ test_createSubscriberNode_team: function() {
+ // When passed a subscriber object which has is_team === true,
+ // a constructed node uses a 'sprite team' CSS classes instead
+ // of 'sprite person' for display.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = {
+ name: 'team',
+ display_name: 'Team Name',
+ web_link: 'http://launchpad.net/~team',
+ is_team: true
+ };
+ var node = subscribers_list._createSubscriberNode(subscriber);
+ var link_text = node.one('a span');
+ Y.Assert.isTrue(link_text.hasClass('sprite'));
+ Y.Assert.isTrue(link_text.hasClass('team'));
+ },
+
+ test_addSubscriber: function() {
+ // When there is no subscriber in the subscriber list,
+ // new node is constructed and appropriate section is added.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var node = subscribers_list.addSubscriber(
+ { name: 'user' }, 'Details');
+
+ // Node is constructed using _createSubscriberNode.
+ Y.Assert.isTrue(node.hasClass('subscriber'));
+ // And the ID is set inside addSubscriber() method.
+ Y.Assert.areEqual('subscriber-user', node.get('id'));
+
+ // And it nested in the subscribers-list of a 'Details' section.
+ var list_node = node.ancestor('.subscribers-list');
+ Y.Assert.isNotNull(list_node);
+ var section_node = list_node.ancestor('.subscribers-section-details');
+ Y.Assert.isNotNull(section_node);
+ },
+
+ test_addSubscriber_incorrect_level: function() {
+ // When an incorrect level is passed in, an exception is thrown.
+ var subscribers_list = setUpSubscribersList(this.root);
+ subscribers_list.addSubscriber(
+ { name: 'user' }, 'Test');
+ },
+
+ test_addSubscriber_change_level: function() {
+ // addSubscriber also allows changing a subscribtion level
+ // for a subscriber when they are moved to a different section.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var node = subscribers_list.addSubscriber(
+ { name: 'user' }, 'Details');
+ Y.Assert.isNotNull(node.ancestor('.subscribers-section-details'));
+
+ // Move the subscriber to lifecycle section.
+ node = subscribers_list.addSubscriber(
+ { name: 'user' }, 'Lifecycle');
+ // It's now in 'Lifecycle' section.
+ Y.Assert.isNotNull(node.ancestor('.subscribers-section-lifecycle'));
+ // And 'Details' section is removed.
+ Y.Assert.isNull(subscribers_list._getSection('Details'));
+ },
+
+ test_addSubscriber_not_in_section_error: function() {
+ // addSubscriber throws an exception if a subscriber node is found,
+ // but it is not properly nested inside a subscribers-section node.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var node = Y.Node.create('<div></div>')
+ .set('id', 'subscriber-user');
+ // We hack the node directly into the entire subscribers list node.
+ subscribers_list.container_node.appendChild(node);
+
+ // And addSubscriber now throws an exception.
+ subscribers_list.addSubscriber(
+ { name: 'user' }, 'Details');
+ },
+
+ test_addSubscriber_ordering: function() {
+ // With multiple subscribers being added to the same section,
+ // the last one is listed first.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var node1 = subscribers_list.addSubscriber(
+ { name: 'user1' }, 'Details');
+ var node2 = subscribers_list.addSubscriber(
+ { name: 'user2' }, 'Details');
+
+ var list_node = subscribers_list._getSection('Details')
+ .one('.subscribers-list');
+ var all_subscribers = list_node.all('.subscriber');
+
+ var returned_nodes = [];
+ var index;
+ for (index = 0; index < all_subscribers.size(); index++) {
+ returned_nodes.push(all_subscribers.item(index));
+ }
+ Y.ArrayAssert.itemsAreSame(
+ [node2, node1],
+ returned_nodes);
+ },
+
+ test_addSubscriber_unsubscribe_callback: function() {
+ // When config.unsubscribe_callback is passed in,
+ // addUnsubscribeAction(subscriber, callback) is
+ // called as well.
+
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ var callback = function() {};
+
+ var callback_setup = false;
+ subscribers_list.addUnsubscribeAction = function(
+ unsub_subscriber, unsub_callback) {
+ callback_setup = true;
+ Y.Assert.areSame(subscriber, unsub_subscriber);
+ Y.Assert.areSame(callback, unsub_callback);
+ };
+ subscribers_list.addSubscriber(subscriber, 'Details',
+ { unsubscribe_callback: callback });
+ // Setting up a callback was performed.
+ Y.Assert.isTrue(callback_setup);
+ }
+
+}));
+
+
+/**
+ * Test adding of unsubscribe action for a subscriber, removal of subscribers
+ * and relevant helper methods.
+ */
+suite.add(new Y.Test.Case({
+ name: 'SubscribersList.addUnsubscribeAction() and ' +
+ 'removeSubscriber() test',
+
+ setUp: function() {
+ this.root = Y.Node.create('<div></div>');
+ Y.one('body').appendChild(this.root);
+ },
+
+ tearDown: function() {
+ this.root.remove();
+ },
+
+ _should: {
+ error: {
+ test_getSubscriberNode_error:
+ new Error('Subscriber is not present in the subscribers list. ' +
+ 'Please call addSubscriber(subscriber) first.'),
+ test_addUnsubscribeAction_error:
+ new Error('Passed in callback for unsubscribe action ' +
+ 'is not a function.'),
+ test_removeSubscriber_error:
+ new Error(
+ 'Subscriber is not present in the subscribers list. ' +
+ 'Please call addSubscriber(subscriber) first.'),
+ test_removeSubscriber_not_in_section_error:
+ new Error(
+ "Matching subscriber node doesn't seem to be in any " +
+ "subscribers list sections.")
+ }
+ },
+
+ test_getSubscriberNode: function() {
+ // Gets a subscriber node from the subscribers list when present.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ var node = subscribers_list.addSubscriber(subscriber, 'Details');
+ Y.Assert.areSame(
+ node, subscribers_list._getSubscriberNode(subscriber));
+ },
+
+ test_getSubscriberNode_error: function() {
+ // When subscriber node is not present, throws an error.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ subscribers_list._getSubscriberNode(subscriber);
+ },
+
+ test_getOrCreateActionsNode: function() {
+ // When no actions node is present, one is created, appended
+ // to the subscriber node, and returned.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber_node = subscribers_list.addSubscriber(
+ { name: 'user' }, "Discussion");
+ var actions_node = subscribers_list._getOrCreateActionsNode(
+ subscriber_node);
+ // Newly created node has 'subscriber-actions' CSS class.
+ Y.Assert.isTrue(actions_node.hasClass('subscriber-actions'));
+
+ // It is also nested inside the subscriber_node.
+ Y.Assert.areSame(subscriber_node, actions_node.get('parentNode'));
+ },
+
+ test_getOrCreateActionsNode_already_exists: function() {
+ // When actions node is present, it is returned.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber_node = subscribers_list.addSubscriber(
+ { name: 'user' }, "Discussion");
+ var old_actions_node = subscribers_list._getOrCreateActionsNode(
+ subscriber_node);
+ var new_actions_node = subscribers_list._getOrCreateActionsNode(
+ subscriber_node);
+ Y.Assert.areSame(old_actions_node, new_actions_node);
+ },
+
+ test_addUnsubscribeAction_node: function() {
+ // Adding an unsubscribe action creates an unsubscribe icon
+ // nested inside the actions node for the subscriber.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user', display_name: 'User Name' };
+ var callback = function() {};
+
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, "Discussion");
+ subscribers_list.addUnsubscribeAction(subscriber, callback);
+ // An actions node is created.
+ var actions_node = subscriber_node.one('.subscriber-actions');
+ Y.Assert.isNotNull(actions_node);
+ // It contains an A tag for the unsubscribe action.
+ var unsub_node = actions_node.one('a.unsubscribe-action');
+ Y.Assert.isNotNull(unsub_node);
+ Y.Assert.areEqual('Unsubscribe User Name', unsub_node.get('title'));
+ var unsub_icon = unsub_node.one('img');
+ Y.Assert.isNotNull(unsub_icon);
+ Y.Assert.areEqual('Remove', unsub_icon.get('alt'));
+ // Getting a URI returns an absolute one, and with this being run
+ // from the local file system, that's what we get.
+ Y.Assert.areEqual('file:///@@/remove', unsub_icon.get('src'));
+ },
+
+ test_addUnsubscribeAction_node_exists: function() {
+ // When an unsubscribe node already exists, a new one is not created.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user', display_name: 'User Name' };
+ var callback = function() {};
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, "Discussion");
+ subscribers_list.addUnsubscribeAction(subscriber, callback);
+ var unsub_node = subscriber_node.one('a.unsubscribe-action');
+
+ subscribers_list.addUnsubscribeAction(subscriber, callback);
+ var unsub_nodes = subscriber_node.all('a.unsubscribe-action');
+ Y.Assert.areEqual(1, unsub_nodes.size());
+ Y.Assert.areSame(unsub_node, unsub_nodes.item(0));
+ },
+
+ test_addUnsubscribeAction_error: function() {
+ // Adding an unsubscribe action with callback not a function
+ // fails with an exception.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, "Discussion");
+ subscribers_list.addUnsubscribeAction(subscriber, "not-function");
+ },
+
+ test_addUnsubscribeAction_callback_on_click: function() {
+ // When unsubscribe link is clicked, callback is activated
+ // and passed in the subscribers_list and subscriber parameters.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user', display_name: 'User Name' };
+
+ var callback_called = false;
+ var callback = function(my_list, my_subscriber) {
+ callback_called = true;
+ Y.Assert.areSame(subscribers_list, my_list);
+ Y.Assert.areSame(subscriber, my_subscriber);
+ };
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, "Discussion");
+ subscribers_list.addUnsubscribeAction(subscriber, callback);
+ var unsub_node = subscriber_node.one('a.unsubscribe-action');
+ unsub_node.simulate('click');
+
+ Y.Assert.isTrue(callback_called);
+ },
+
+ test_removeSubscriber_error: function() {
+ // Removing a non-existent subscriber fails with an error.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ subscribers_list.removeSubscriber(subscriber);
+ },
+
+ test_removeSubscriber_section_removed: function() {
+ // Removing a subscriber works when the subscriber is in the list.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, 'Details');
+ var section_node = subscriber_node.ancestor('.subscribers-section');
+ subscribers_list.removeSubscriber(subscriber);
+ // Entire section is removed along with the subscriber.
+ Y.Assert.areEqual(0, _getAllSections(subscribers_list).length);
+ },
+
+ test_removeSubscriber_section_remains: function() {
+ // Removing a subscriber works when the subscriber is in the list.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var subscriber = { name: 'user' };
+ var other_node = subscribers_list.addSubscriber(
+ { name: 'other' }, 'Details');
+ var subscriber_node = subscribers_list.addSubscriber(
+ subscriber, 'Details');
+ var section_node = subscriber_node.ancestor('.subscribers-section');
+ subscribers_list.removeSubscriber(subscriber);
+ // Section is not removed because it still has 'other' subscriber.
+ var all_sections = _getAllSections(subscribers_list);
+ Y.Assert.areEqual(1, all_sections.length);
+ // User is removed.
+ Y.Assert.isNull(all_sections[0].one('#subscriber-user'));
+ // Other is still in the list.
+ Y.Assert.areSame(
+ other_node, all_sections[0].one('#subscriber-other'));
+ },
+
+ test_removeSubscriber_not_in_section_error: function() {
+ // If subscriber is not in a section, an exception is thrown.
+ var subscribers_list = setUpSubscribersList(this.root);
+ var node = Y.Node.create('<div></div>')
+ .set('id', 'subscriber-user');
+ // We hack the node directly into the entire subscribers list node.
+ subscribers_list.container_node.appendChild(node);
+ subscribers_list.removeSubscriber({ name: 'user' });
+ }
+}));
+
+
var handle_complete = function(data) {
status_node = Y.Node.create(
'<p id="complete">Test status: complete</p>');