← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/blacklist-bug-796669 into lp:launchpad

 

Raphaël Victor Badin has proposed merging lp:~rvb/launchpad/blacklist-bug-796669 into lp:launchpad with lp:~rvb/launchpad/widgetify-bug-796669 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #796669 in Launchpad itself: "The lp.registry.distroseriesdifferences_details module (javascript) is very hard to test."
  https://bugs.launchpad.net/launchpad/+bug/796669

For more details, see:
https://code.launchpad.net/~rvb/launchpad/blacklist-bug-796669/+merge/70826

This branch refactors the code of the blacklisting slot into a YUI3 widget. It adds tests for this widget.

= Tests =

lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html

= QA =

On this diff page, open up a row and change the blacklisting status, this should open an overlay to let the user enter a comment. Once the content of the overlay is validated, the latest comment should be updated along with the new comment added at the bottom of the list.
https://dogfood.launchpad.net/ubuntu/oneiric/+localpackagediffs
-- 
https://code.launchpad.net/~rvb/launchpad/blacklist-bug-796669/+merge/70826
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/blacklist-bug-796669 into lp:launchpad.
=== modified file 'lib/lp/registry/javascript/distroseriesdifferences_details.js'
--- lib/lp/registry/javascript/distroseriesdifferences_details.js	2011-08-09 12:53:37 +0000
+++ lib/lp/registry/javascript/distroseriesdifferences_details.js	2011-08-09 12:53:38 +0000
@@ -104,11 +104,14 @@
         // The blacklist slot with a class 'blacklist-options' is only
         // available when the user has the right to blacklist.
         var blacklist_slot = container.one('div.blacklist-options');
+
         if (blacklist_slot !== null) {
-            namespace.setup_blacklist_options(
-                blacklist_slot, source_name, api_uri,
-                latest_comment_container,
-                add_comment_placeholder);
+            var blacklist_widget = new BlacklistWidget(
+                {srcNode: blacklist_slot,
+                 sourceName: source_name,
+                 dsdLink: api_uri,
+                 latestCommentContainer: latest_comment_container,
+                 addCommentPlaceholder: add_comment_placeholder});
         }
         // If the user has not the right to blacklist, we disable
         // the blacklist slot.
@@ -192,143 +195,188 @@
 
 namespace.ExpandableRowWidget = ExpandableRowWidget;
 
-
-namespace.blacklist_handler = function(e, dsd_link, source_name,
-                                       latest_comment_container,
-                                       add_comment_placeholder) {
-    // We only want to select the new radio if the update is
-    // successful.
-    e.preventDefault();
-    var blacklist_options_container = this.ancestor('div');
-    namespace.blacklist_comment_overlay(
-        e, dsd_link, source_name, latest_comment_container,
-        add_comment_placeholder, blacklist_options_container);
+/**
+ * BlacklistWidget: the widget used by each row to control the
+ * 'blacklisted' status of the DSD.
+ */
+function BlacklistWidget(config) {
+    BlacklistWidget.superclass.constructor.apply(this, arguments);
+}
+
+BlacklistWidget.NAME = "blacklistWidget";
+
+BlacklistWidget.HTML_PARSER = {
+    relatedRows: function(srcNode) {
+        return [
+            srcNode.ancestor('tr').previous(),
+            srcNode.ancestor('tr')
+            ];
+    }
 };
 
-namespace.blacklist_comment_overlay = function(e, dsd_link, source_name,
-                                              latest_comment_container,
-                                              add_comment_placeholder,
-                                              blacklist_options_container) {
-    var comment_form = Y.Node.create("<form />")
-        .appendChild(Y.Node.create("<textarea></textarea>")
-            .set("name", "comment")
-            .set("rows", "3")
-            .set("cols", "60"));
-
-    /* Buttons */
-    var submit_button = Y.Node.create(
-        '<button type="submit" class="lazr-pos lazr-btn" />')
-           .set("text", "OK");
-    var cancel_button = Y.Node.create(
-        '<button type="button" class="lazr-neg lazr-btn" />')
-            .set("text", "Cancel");
-
-    var submit_callback = function(data) {
-        overlay.hide();
-        var comment = "";
-        if (data.comment !== undefined) {
-            comment = data.comment[0];
-        }
-        namespace.blacklist_submit_handler(
-            e, dsd_link, source_name, comment, latest_comment_container,
-            add_comment_placeholder, blacklist_options_container);
-    };
-    var origin = blacklist_options_container.one('.blacklist-options');
-    var overlay = new Y.lazr.FormOverlay({
-        align: {
-             /* Align the centre of the overlay with the centre of the
+Y.extend(BlacklistWidget, Y.Widget, {
+    initializer: function(cfg) {
+        this.sourceName = cfg.sourceName;
+        this.dsdLink = cfg.dsdLink;
+        this.latestCommentContainer = cfg.latestCommentContainer;
+        this.addCommentPlaceholder = cfg.addCommentPlaceholder;
+        this.relatedRows = cfg.relatedRows;
+        this.container = cfg.container;
+        // We call bindUI directly here because the BlacklistWidget
+        // are built from existing HTML code and hence we don't
+        // use the full potential of YUI'Widget to manage the
+        // display of the widgets.
+        // http://developer.yahoo.com/yui/3/widget/#progressive
+        this.bindUI();
+    },
+
+    /**
+     * Wire the widget methods/events together.
+     *
+     */
+    bindUI: function() {
+        // Wire the click on blacklist form.
+        var handleClick = function(e) {
+            e.preventDefault();
+            var target = e.target;
+            this.blacklist_comment_overlay(target);
+        };
+        Y.on("click", handleClick, this.get('srcNode').all('input'), this);
+
+        // Wire the ok event from the comment overlay.
+        var handleBlacklistChange = function(e, method_name, blacklist_all,
+                                             comment, target) {
+            e.preventDefault();
+            this.blacklist_submit_handler(
+                method_name, blacklist_all, comment, target);
+         };
+        this.on("blacklist_changed", handleBlacklistChange,
+            this);
+    },
+
+    /**
+     * Popup an overlay to let the user enter an optionnal comment.
+     *
+     * @param target {Node}
+     *     The target input node that was clicked.
+     * @returns {Y.lazr.FormOverlay}
+     *     The overlay that was just created.
+     */
+    blacklist_comment_overlay: function(target) {
+        var comment_form = Y.Node.create("<form />")
+            .appendChild(Y.Node.create("<textarea></textarea>")
+                .set("name", "comment")
+                .set("rows", "3")
+                .set("cols", "60"));
+        /* Buttons */
+        var submit_button = Y.Node.create(
+            '<button type="submit" class="lazr-pos lazr-btn" />')
+                .set("text", "OK");
+        var cancel_button = Y.Node.create(
+            '<button type="button" class="lazr-neg lazr-btn" />')
+                .set("text", "Cancel");
+
+        var self = this;
+        var submit_callback = function(data) {
+            overlay.hide();
+            // Get the comment string.
+            var comment = "";
+            if (data.comment !== undefined) {
+                comment = data.comment[0];
+            }
+            // Figure out the new 'ignored' status.
+            var value = target.get('value');
+            var method_name = (value === 'NONE') ?
+                'unblacklist' : 'blacklist';
+            var blacklist_all = (
+                target.get('value') === 'BLACKLISTED_ALWAYS');
+            self.fire(
+                'blacklist_changed', method_name, blacklist_all, comment,
+                target);
+        };
+        var overlay = new Y.lazr.FormOverlay({
+            align: {
+                /* Align the centre of the overlay with the centre of the
                 node containing the blacklist options. */
-             node: origin,
-             points: [
-                 Y.WidgetPositionAlign.CC,
-                 Y.WidgetPositionAlign.CC
-             ]
-         },
-         headerContent: "<h2>Add an optional comment</h2>",
-         form_content: comment_form,
-         form_submit_button: submit_button,
-         form_cancel_button: cancel_button,
-         form_submit_callback: submit_callback,
-         visible: true
-    });
-    overlay.render();
-};
-
-namespace.blacklist_submit_handler = function(e, dsd_link, source_name,
-                                              comment,
-                                              latest_comment_container,
-                                              add_comment_placeholder,
-                                              blacklist_options_container) {
-    // Disable all the inputs.
-    blacklist_options_container.all('input').set('disabled', 'disabled');
-    e.target.prepend('<img src="/@@/spinner" />');
-
-    var method_name = (e.target.get('value') === 'NONE') ?
-        'unblacklist' : 'blacklist';
-    var blacklist_all = (e.target.get('value') === 'BLACKLISTED_ALWAYS');
-
-    var diff_rows = Y.all('tr.' + source_name);
-
-    var config = {
-        on: {
-            success: function(updated_entry, args) {
-                // Let the user know this item is now blacklisted.
-                blacklist_options_container.one('img').remove();
-                blacklist_options_container.all(
-                    'input').set('disabled', false);
-                e.target.set('checked', true);
-                Y.each(diff_rows, function(diff_row) {
-                    var fade_to_gray = new Y.Anim({
-                        node: diff_row,
-                        from: { backgroundColor: '#FFFFFF'},
-                        to: { backgroundColor: '#EEEEEE'}
+                node: this.get('srcNode'),
+                points: [
+                    Y.WidgetPositionAlign.CC,
+                    Y.WidgetPositionAlign.CC]
+            },
+            headerContent: "<h2>Add an optional comment</h2>",
+            form_content: comment_form,
+            form_submit_button: submit_button,
+            form_cancel_button: cancel_button,
+            form_submit_callback: submit_callback,
+            visible: true
+        });
+        overlay.render();
+        return overlay;
+    },
+
+    /**
+     * Submit the blacklist or unblacklist action. Updates the comment
+     * list if successful.
+     *
+     * @param method_name {String}
+     *     'blacklist' or 'unblacklist'.
+     * @param blacklist_all {Boolean}
+     *     Is this a blacklist all versions or blacklist current (only
+     *     relevant if method_name is 'blacklist').
+     * @param comment {String}
+     *     The comment string.
+     * @param target {Node}
+     *     The target input node that was clicked.
+     */
+     blacklist_submit_handler: function(method_name, blacklist_all, comment,
+                                        target) {
+        // Disable all the inputs.
+        this.get('srcNode').all('input').set('disabled', 'disabled');
+        this.get('srcNode').prepend('<img src="/@@/spinner" />');
+
+        //var diff_rows = Y.all('tr.' + source_name);
+        var diff_rows = this.relatedRows;
+
+        var self = this;
+        var config = {
+            on: {
+                success: function(updated_entry, args) {
+                    // Let the user know this item is now blacklisted.
+                    self.get('srcNode').one('img').remove();
+                    self.get('srcNode').all(
+                        'input').set('disabled', false);
+                    target.set('checked', true);
+                    Y.each(diff_rows, function(diff_row) {
+                        var fade_to_gray = new Y.Anim({
+                            node: diff_row,
+                            from: { backgroundColor: '#FFFFFF'},
+                            to: { backgroundColor: '#EEEEEE'}
                         });
-                    if (method_name === 'unblacklist') {
-                        fade_to_gray.set('reverse', true);
+                        if (method_name === 'unblacklist') {
+                            fade_to_gray.set('reverse', true);
                         }
-                    fade_to_gray.run();
-                    });
-                namespace.add_comment(
-                    updated_entry, add_comment_placeholder,
-                    latest_comment_container);
+                        fade_to_gray.run();
+                        });
+                    namespace.add_comment(
+                        updated_entry, self.addCommentPlaceholder,
+                        self.latestCommentContainer);
+                },
+                failure: function(id, response) {
+                    self.get('srcNode').one('img').remove();
+                    self.get('srcNode').all(
+                        'input').set('disabled', false);
+                }
             },
-            failure: function(id, response) {
-                blacklist_options_container.one('img').remove();
-                blacklist_options_container.all(
-                    'input').set('disabled', false);
+            parameters: {
+                all: blacklist_all,
+                comment: comment
             }
-        },
-        parameters: {
-            all: blacklist_all,
-            comment: comment
-        }
-    };
-
-    namespace.lp_client.named_post(dsd_link, method_name, config);
-
-};
-
-/**
- * Link the click event for these blacklist options to the correct
- * api uri.
- *
- * @param blacklist_options {Node} The node containing the blacklist
- *     options.
- * @param source_name {string} The name of the source to update.
- * @param dsd_link {string} The uri for the distroseriesdifference object.
- * @param latest_comment_container {Node} The node containing the last
- *     comment.
- * @param add_comment_placeholder {Node} The node containing the "add
- *     comment" slot.
- */
-namespace.setup_blacklist_options = function(
-    blacklist_options, source_name, dsd_link, latest_comment_container,
-    add_comment_placeholder) {
-    Y.on('click', namespace.blacklist_handler,
-         blacklist_options.all('input'),
-         blacklist_options, dsd_link, source_name,
-         latest_comment_container, add_comment_placeholder);
-};
+        };
+        namespace.lp_client.named_post(this.dsdLink, method_name, config);
+    }
+});
+
+namespace.BlacklistWidget = BlacklistWidget;
 
 /**
  * Update the latest comment on the difference row.
@@ -341,7 +389,7 @@
  */
 namespace.update_latest_comment = function(comment_entry, placeholder) {
     var comment_latest_fragment_url =
-        comment_entry.get("web_link") + "/+latest-comment-fragment";
+        comment_entry.get('web_link') + "/+latest-comment-fragment";
     var config = {
         on: {
             success: function(comment_latest_fragment_html) {
@@ -522,7 +570,6 @@
     });
 };
 
-
 var set_package_diff_status = function(container, new_status, note_msg) {
     container.removeClass('request-derived-diff');
     container.removeClass('PENDING');
@@ -553,7 +600,7 @@
     }
     return undefined;
 };
- 
+
 namespace.add_link_to_package_diff = function(container, url_uri) {
     var y_config = {
         headers: {'Accept': 'application/json;'},
@@ -755,7 +802,7 @@
     namespace.lp_client.named_post(dsd_link, 'requestPackageDiffs', config);
 };
 
-}, "0.1", {"requires": ["event-simulate", "io-base",
+}, "0.1", {"requires": ["io-base", "widget", "event", "overlay",
                         "lp.soyuz.base", "lp.client",
-                        "lazr.anim", "lazr.effects",
+                        "lazr.anim", "lazr.formoverlay",
                         "lp.soyuz.dynamic_dom_updater"]});

=== modified file 'lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html'
--- lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html	2011-08-09 12:53:37 +0000
+++ lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html	2011-08-09 12:53:38 +0000
@@ -17,6 +17,8 @@
   <script type="text/javascript" src="../../../soyuz/javascript/lp_dynamic_dom_updater.js"></script>
   <script type="text/javascript" src="../../../app/javascript/lazr/lazr.js"></script>
   <script type="text/javascript" src="../../../app/javascript/anim/anim.js"></script>
+  <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
+  <script type="text/javascript" src="../../../app/javascript/formoverlay/formoverlay.js"></script>
 
   <!-- The module under test -->
   <script type="text/javascript" src="../distroseriesdifferences_details.js"></script>

=== modified file 'lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js'
--- lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js	2011-08-09 12:53:37 +0000
+++ lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js	2011-08-09 12:53:38 +0000
@@ -1,9 +1,9 @@
-/* Copyright 2009 Canonical Ltd.  This software is licensed under the
+/* Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
    GNU Affero General Public License version 3 (see the file LICENSE). */
 
 YUI().use(
         'lp.testing.runner', 'test', 'console', 'node-event-simulate',
-        'lp.soyuz.base', "lazr.anim", "lazr.effects",
+        'lp.soyuz.base', "lazr.anim", "lazr.formoverlay",
         'lp.soyuz.dynamic_dom_updater', 'event-simulate', "io-base",
         'lp.registry.distroseriesdifferences_details', function(Y) {
 
@@ -11,35 +11,32 @@
 var dsd_details = Y.lp.registry.distroseriesdifferences_details;
 var dsd_uri = '/duntu/dwarty/+source/evolution/+difference/ubuntu/warty';
 
-var table_html = [
-    '<table class="listing"><tbody>',
-    '  <tr class="evolution">',
-    '    <td>',
-    '      <a href="/deribuntu/deriwarty/+source/evolution/+difference/ubuntu/warty"',
-    '         class="js-action toggle-extra treeCollapsed ',
-    '         sprite">evolution</a>',
-    '    </td>',
-    '    <td>',
-    '      <a href="/ubuntu/warty" class="parent-name">Warty</a>',
-    '    </td>',
-    '    <td>',
-    '      <a href="/ubuntu/warty/+source/evolution/2.0.9-1ubuntu2"',
-    '         class="parent-version">',
-    '         2.0.9-1ubuntu2</a>',
-    '    </td>',
-    '    <td>',
-    '      <a href="/deribuntu/deriwarty/+source/evolution/2.0.8-4deribuntu1"',
-    '         class="derived-version">',
-    '         2.0.8-4deribuntu1</a>',
-    '    </td>',
-    '    <td class="packagesets"></td>',
-    '    <td class="last-changed"></td>',
-    '    <td class="latest-comment-fragment"></td>',
-    '  </tr>',
-    '</tbody></table>'
+var first_row = [
+    '<tr id="first_row" class="evolution">',
+    '  <td>',
+    '    <a href="/d/d/+source/evolution/+difference/ubuntu/warty"',
+    '       class="js-action toggle-extra treeCollapsed ',
+    '       sprite">evolution</a>',
+    '  </td>',
+    '  <td>',
+    '    <a href="/ubuntu/warty" class="parent-name">Warty</a>',
+    '  </td>',
+    '  <td>',
+    '    <a href="/ubuntu/warty/+source/evolution/2.0.9-1ubuntu2"',
+    '       class="parent-version">',
+    '       2.0.9-1ubuntu2</a>',
+    '  </td>',
+    '  <td>',
+    '    <a href="/deribuntu/deriwarty/+source/evolution/2.0.8-4deribuntu1"',
+    '       class="derived-version">',
+    '       2.0.8-4deribuntu1</a>',
+    '  </td>',
+    '  <td class="packagesets"></td>',
+    '  <td class="last-changed"></td>',
+    '  <td class="latest-comment-fragment"></td>',
+    '</tr>'
     ].join('');
 
-
 var testExpandableRowWidget = {
 
     name: 'expandable-row-widget',
@@ -47,8 +44,8 @@
     setUp: function() {
         Y.one("#placeholder")
             .empty()
-            .appendChild(Y.Node.create(table_html));
-        this.toggle = Y.one('table.listing a.toggle-extra');
+            .appendChild(Y.Node.create(first_row));
+        this.toggle = Y.one('a.toggle-extra');
      },
 
     test_initializer: function() {
@@ -94,15 +91,48 @@
 
 };
 
-var extra_table_html = [
-    '<table class="listing">',
-    '  <tbody><tr><td>',
+var blacklist_html = [
+    '<div class="blacklist-options" style="float:left">',
+    '  <dl>',
+    '    <dt>Ignored:</dt>',
+    '    <dd>',
+    '      <form>',
+    '        <div>',
+    '          <div class="value">',
+    '            <label for="field.blacklist_options.0">',
+    '              <input type="radio" value="NONE" ',
+    '                name="field.blacklist_options"',
+    '                id="field.blacklist_options.0" checked="checked" ',
+    '                class="radioType">&nbsp;No</input>',
+    '            </label><br>',
+    '            <label for="field.blacklist_options.1">',
+    '              <input type="radio" value="BLACKLISTED_ALWAYS" ',
+    '               name="field.blacklist_options"',
+    '                id="field.blacklist_options.1" class="radioType">',
+    '                &nbsp;All versions</input>',
+    '            </label><br>',
+    '            <label for="field.blacklist_options.2">',
+    '              <input type="radio" value="BLACKLISTED_CURRENT"',
+    '                name="field.blacklist_options"',
+    '                id="field.blacklist_options.2"',
+    '                class="radioType">&nbsp;These versions</input>',
+    '            </label>',
+    '          </div>',
+    '          <input type="hidden" value="1" ',
+    '            name="field.blacklist_options-empty-marker" />',
+    '        </div>',
+    '      </form>',
+    '    </dd>',
+    '  </dl>',
+    '</div>'
+    ].join('');
+
+var second_row = [
+    '<tr id="second_row">',
+    '  <td>',
     '    <div class="diff-extra-container">',
-    '      <input name="field.selected_differences" type="checkbox" />',
-    '      <a class="toggle-extra"',
-    '        href="/deribuntu/deriwarty/+source/evolution/+difference/ubuntu/warty">',
-    '        evolution</a>',
-    '      <span class="package-diff-button"></span>',
+    '      <div>',
+    '      <dl>',
     '      <dt class="package-diff-placeholder">',
     '       <span class="package-diff-compute-request">',
     '        <a class="js-action sprite add" href="">',
@@ -123,9 +153,30 @@
     '          </li>',
     '        </ul>',
     '      </dd>',
+    '      </dl>',
+    '      </div>',
+    blacklist_html,
+    '      <div class="boardComment ">',
+    '        <div class="boardCommentDetails">',
+    '          <a class="sprite person" href="/~mark">Mark S.</a>',
+    '          wrote on 2010-06-26',
+    '        </div>',
+    '        <div class="boardCommentBody">Body</div>',
+    '      </div>',
+    '      <div class="add-comment-placeholder evolution">',
+    '        <a href="" class="widget-hd js-action sprite add">',
+    '        Add comment</a>',
+    '      </div>',
     '    </div>',
-    '  </td></tr></tbody>',
-    '</table>'
+    '  </td>',
+    '</tr>'
+    ].join('');
+
+var whole_table = [
+    '<table class="listing"><tbody>',
+    first_row,
+    second_row,
+    '</tbody></table>'
     ].join('');
 
 var testPackageDiffUpdate = {
@@ -135,7 +186,7 @@
     setUp: function() {
         Y.one("#placeholder")
             .empty()
-            .appendChild(Y.Node.create(extra_table_html));
+            .appendChild(Y.Node.create(second_row));
     },
 
     test_vocabulary_helper: function() {
@@ -166,6 +217,288 @@
     }
 };
 
+var testBlacklistWidget = {
+
+    name: 'package-diff-update-interaction',
+
+    setUp: function() {
+        Y.one("#placeholder")
+            .empty()
+            .appendChild(Y.Node.create(whole_table));
+        this.node = Y.one('.blacklist-options');
+        this.latestCommentContainer = Y.one('td.latest-comment-fragment');
+        this.addCommentPlaceholder = Y.one('div.add-comment-placeholder');
+        this.widget = new dsd_details.BlacklistWidget(
+            {srcNode: this.node,
+             sourceName: 'evolution',
+             dsdLink: '/a/link',
+             latestCommentContainer: this.latestCommentContainer,
+             addCommentPlaceholder: this.addCommentPlaceholder});
+     },
+
+    test_initializer: function() {
+        Y.Assert.areEqual(this.node, this.widget.get('srcNode'));
+        Y.Assert.areEqual('evolution', this.widget.sourceName);
+        Y.Assert.areEqual('/a/link', this.widget.dsdLink);
+        Y.Assert.areEqual(
+            this.latestCommentContainer,
+            this.widget.latestCommentContainer);
+        Y.Assert.areEqual(
+            this.addCommentPlaceholder,
+            this.widget.addCommentPlaceholder);
+    },
+
+    test_wire_blacklist_click: function() {
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_CURRENT"]');
+        var fired = false;
+
+        var blacklist_comment_overlay = function(target) {
+            fired = true;
+            Y.Assert.areEqual(input, target);
+        };
+        this.widget.blacklist_comment_overlay = blacklist_comment_overlay;
+        input.simulate('click');
+
+        Y.Assert.isTrue(fired);
+    },
+
+    test_wire_blacklist_changed: function() {
+        var fired = false;
+
+        var blacklist_submit_handler = function(arg1, arg2, arg3, arg4) {
+            fired = true;
+            Y.Assert.areEqual(1, arg1);
+            Y.Assert.areEqual(2, arg2);
+            Y.Assert.areEqual(3, arg3);
+            Y.Assert.areEqual(4, arg4);
+        };
+        this.widget.blacklist_submit_handler = blacklist_submit_handler;
+        this.widget.fire('blacklist_changed', 1, 2, 3, 4);
+
+        Y.Assert.isTrue(fired);
+    },
+
+    test_containing_rows: function() {
+        var expected = [Y.one('#first_row'), Y.one('#second_row')];
+        Y.ArrayAssert.itemsAreEqual(expected, this.widget.relatedRows);
+    },
+
+    test_blacklist_comment_overlay_creates_overlay: function() {
+        var input = Y.one('div.blacklist-options input');
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        // Check overlay's structure.
+        Y.Assert.isInstanceOf(Y.lazr.FormOverlay, overlay);
+        Y.Assert.isNotNull(overlay.form_node.one('textarea'));
+        Y.Assert.areEqual(
+            'OK',
+            overlay.form_node.one('button[type="submit"]').get('text'));
+        Y.Assert.areEqual(
+            'Cancel',
+            overlay.form_node.one('button[type="button"]').get('text'));
+        Y.Assert.isTrue(overlay.get('visible'));
+    },
+
+    test_blacklist_comment_overlay_cancel_hides_overlay: function() {
+        var input = Y.one('div.blacklist-options input');
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        var cancel_button = overlay.form_node.one('button[type="button"]');
+        Y.Assert.isTrue(overlay.get('visible'));
+        cancel_button.simulate('click');
+        Y.Assert.isFalse(overlay.get('visible'));
+     },
+
+    test_blacklist_comment_overlay_ok_hides_overlay: function() {
+        var input = Y.one('div.blacklist-options input');
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        Y.Assert.isTrue(overlay.get('visible'));
+        overlay.form_node.one('button[type="submit"]').simulate('click');
+        Y.Assert.isFalse(overlay.get('visible'));
+    },
+
+    test_blacklist_comment_overlay_fires_event: function() {
+        var input = Y.one('div.blacklist-options input[value="NONE"]');
+        input.set('checked', true);
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        var event_fired = false;
+        var method = null;
+        var all = null;
+        var comment = null;
+        var target = null;
+
+        var handleEvent = function(e, e_method, e_all, e_comment, e_target) {
+            event_fired = true;
+            method = e_method;
+            all = e_all;
+            comment = e_comment;
+            target = e_target;
+        };
+
+        this.widget.on("blacklist_changed", handleEvent, this.widget);
+
+        overlay.form_node.one('textarea').set('text', 'Test comment');
+        overlay.form_node.one('button[type="submit"]').simulate('click');
+
+        Y.Assert.isTrue(event_fired);
+        Y.Assert.areEqual('unblacklist', method);
+        Y.Assert.areEqual(false, all);
+        Y.Assert.areEqual('Test comment', comment);
+        Y.Assert.areEqual(input, target);
+    },
+
+    test_blacklist_comment_overlay_fires_event_blacklist_all: function() {
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_ALWAYS"]');
+        input.set('checked', true);
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        var method = null;
+        var all = null;
+        var target = null;
+
+        var handleEvent = function(e, e_method, e_all, e_comment, e_target) {
+            method = e_method;
+            all = e_all;
+            target = e_target;
+        };
+        this.widget.on("blacklist_changed", handleEvent, this.widget);
+        overlay.form_node.one('button[type="submit"]').simulate('click');
+
+        Y.Assert.areEqual('blacklist', method);
+        Y.Assert.areEqual(true, all);
+        Y.Assert.areEqual(input, target);
+    },
+
+    test_blacklist_comment_overlay_fires_event_blacklist: function() {
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_CURRENT"]');
+        input.set('checked', true);
+        var overlay = this.widget.blacklist_comment_overlay(input);
+        var method = null;
+        var all = null;
+        var target = null;
+
+        var handleEvent = function(e, e_method, e_all, e_comment, e_target) {
+            method = e_method;
+            all = e_all;
+            target = e_target;
+        };
+        this.widget.on("blacklist_changed", handleEvent, this.widget);
+        overlay.form_node.one('button[type="submit"]').simulate('click');
+
+        Y.Assert.areEqual('blacklist', method);
+        Y.Assert.areEqual(false, all);
+        Y.Assert.areEqual(input, target);
+    },
+
+    assertAllDisabled: function(selector) {
+        var all_input_status = Y.all(selector).get('disabled');
+        Y.Assert.isTrue(all_input_status.every(function(val) {return val;}));
+    },
+
+    assertAllEnabled: function(selector) {
+        var all_input_status = Y.all(selector).get('disabled');
+        Y.Assert.isTrue(all_input_status.every(function(val) {return !val;}));
+    },
+
+    patchNamedPost: function(method_name, expected_parameters) {
+        function Comment() {}
+        Comment.prototype.get = function(key) {
+            if (key === 'comment_date') {
+                return "2011-08-08T13:15:50.636269+00:00";}
+            if (key === 'body_text') {
+                return 'This is the comment';}
+            if (key === 'self_link') {
+                return ["https://lp.net/api/devel/u/d//+source/";,
+                        "evolution/+difference/ubuntu/warty/comments/6"
+                        ].join('');}
+            if (key === 'web_link') {
+                return ["https://lp.net/d/d/+source/evolution/";,
+                        "+difference/ubuntu/warty/comments/6"
+                        ].join('');}
+        };
+        var comment_entry = new Comment();
+        var self = this;
+        dsd_details.lp_client.named_post = function(url, func, config) {
+            Y.Assert.isNotNull(Y.one('img[src="/@@/spinner"]'));
+            Y.Assert.areEqual(func, method_name);
+            Y.ObjectAssert.areEqual(expected_parameters ,config.parameters);
+            self.assertAllDisabled('div.blacklist-options input');
+            config.on.success(comment_entry);
+        };
+    },
+
+    assertBlacklisted: function(input, color) {
+        Y.Assert.isTrue(input.get('checked'));
+        Y.Assert.isNull(Y.one('img[src="/@@/spinner"]'));
+       // Wait 2 seconds for the animation to run.
+        this.wait(function() {
+            Y.Assert.areEqual(
+                color,
+                Y.one('#first_row').getStyle('backgroundColor'));
+            Y.Assert.areEqual(
+                color,
+                Y.one('#second_row').getStyle('backgroundColor'));
+        }, 2000);
+     },
+
+    test_blacklist_submit_handler_blacklist_simple: function() {
+        this.patchNamedPost(
+            'blacklist',
+            {comment: 'Test comment', all: false});
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_CURRENT"]');
+        input.set('checked', false);
+        this.widget.blacklist_submit_handler(
+            'blacklist', false, "Test comment", input);
+
+        this.assertBlacklisted(input, 'rgb(238, 238, 238)');
+        this.assertAllEnabled('div.blacklist-options input');
+    },
+
+    test_blacklist_submit_handler_blacklist_all: function() {
+        this.patchNamedPost(
+            'blacklist',
+            {comment: 'Test comment', all: true});
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_ALWAYS"]');
+        input.set('checked', false);
+        this.widget.blacklist_submit_handler(
+            'blacklist', true, "Test comment", input);
+
+        this.assertBlacklisted(input, 'rgb(238, 238, 238)');
+        this.assertAllEnabled('div.blacklist-options input');
+    },
+
+    test_blacklist_submit_handler_unblacklist: function() {
+        this.patchNamedPost(
+            'unblacklist',
+            {comment: 'Test comment', all: true});
+        var input = Y.one('div.blacklist-options input[value="NONE"]');
+        input.set('checked', false);
+        this.widget.blacklist_submit_handler(
+            'unblacklist', true, "Test comment", input);
+
+        this.assertBlacklisted(input, 'rgb(255, 255, 255)');
+        this.assertAllEnabled('div.blacklist-options input');
+    },
+
+    test_blacklist_submit_handler_failure: function() {
+        var self = this;
+        dsd_details.lp_client.named_post = function(url, func, config) {
+            Y.Assert.isNotNull(Y.one('img[src="/@@/spinner"]'));
+            self.assertAllDisabled('div.blacklist-options input');
+            config.on.failure();
+        };
+        var input = Y.one('div.blacklist-options input');
+        input.set('checked', false);
+        this.widget.blacklist_submit_handler(
+            null, 'unblacklist', true, "Test comment", input);
+
+        Y.Assert.isNull(Y.one('img[src="/@@/spinner"]'));
+        this.assertAllEnabled('div.blacklist-options input');
+    }
+
+};
 
 var testPackageDiffUpdateInteraction = {
 
@@ -174,7 +507,7 @@
     setUp: function() {
         Y.one("#placeholder")
             .empty()
-            .appendChild(Y.Node.create(extra_table_html));
+            .appendChild(Y.Node.create(whole_table));
         var first_poll = true;
         pending_voc = [
             {"token": "PENDING", "selected": true, "title": "Pending"},
@@ -186,12 +519,11 @@
             {"token": "FAILED", "title": "Failed"}];
 
         // Monkey patch request.
-        var lp_prot = Y.lp.client.Launchpad.prototype;
-        lp_prot.named_post = function(url, func, config) {
-            config.on.success();};
-        lp_prot.named_get = function(url, func, config) {
-            config.on.success();};
-        lp_prot.get = function(uri, config) {
+        dsd_details.lp_client.named_post = function(url, func, config) {
+            config.on.success();};
+        dsd_details.lp_client.named_get = function(url, func, config) {
+            config.on.success();};
+        dsd_details.lp_client.get = function(uri, config) {
             if (first_poll === true) {
                 first_poll = false;
                 config.on.success(pending_voc);
@@ -216,8 +548,7 @@
         dsd_details.setup_packages_diff_states(
             placeholder.one('.diff-extra-container'), dsd_uri);
         var func_req;
-        var lp_prot = Y.lp.client.Launchpad.prototype;
-        lp_prot.named_post = function(url, func, config) {
+        dsd_details.lp_client.named_post = function(url, func, config) {
             func_req = func;
             config.on.success();
         };
@@ -242,12 +573,10 @@
         dsd_details.setup_packages_diff_states(
             placeholder.one('.diff-extra-container'), dsd_uri);
         var func_req;
-        var lp_prot = Y.lp.client.Launchpad.prototype;
-        lp_prot.named_post = function(url, func, config) {
+        dsd_details.lp_client.named_post = function(url, func, config) {
             func_req = func;
             config.on.success();
         };
-
         var button = placeholder.one('.package-diff-compute-request');
 
         button.simulate('click');
@@ -286,8 +615,9 @@
 };
 
 suite.add(new Y.Test.Case(testPackageDiffUpdate));
+suite.add(new Y.Test.Case(testExpandableRowWidget));
+suite.add(new Y.Test.Case(testBlacklistWidget));
 suite.add(new Y.Test.Case(testPackageDiffUpdateInteraction));
-suite.add(new Y.Test.Case(testExpandableRowWidget));
 
 Y.lp.testing.Runner.run(suite);