← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/add-comment-bug-823777 into lp:launchpad

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #823777 in Launchpad itself: "The Javascript methods used to manage adding a comment on the expandable row of the diff pages should be refactored into a proper YUI widget."
  https://bugs.launchpad.net/launchpad/+bug/823777

For more details, see:
https://code.launchpad.net/~rvb/launchpad/add-comment-bug-823777/+merge/71171

This branch refactors all the Javascript methods to add comment to a DSD on the diff pages. All that code is now encapsulated into a YUI Widget with tests.

= Tests =

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

= QA =

This branch should not add any new functionality but the it's worth checking that it's still possible to:
- blacklist a DSD (and add a comment)
- add a comment using the add comment slot
-- 
https://code.launchpad.net/~rvb/launchpad/add-comment-bug-823777/+merge/71171
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/add-comment-bug-823777 into lp:launchpad.
=== modified file 'lib/lp/registry/javascript/distroseriesdifferences_details.js'
--- lib/lp/registry/javascript/distroseriesdifferences_details.js	2011-08-11 11:57:42 +0000
+++ lib/lp/registry/javascript/distroseriesdifferences_details.js	2011-08-11 11:57:42 +0000
@@ -18,13 +18,6 @@
 
 namespace.io = Y.io;
 
-/*
- * XXX: rvb 2011-08-01 bug=796669: At present this module it is
- * function-passing spaghetti. The duct-tape is getting frayed.
- * It ought to be recomposed as widgets or something a bit more objecty so
- * it can be unit tested without having to set-up the world each time.
- */
-
 function ExpandableRowWidget(config) {
     ExpandableRowWidget.superclass.constructor.apply(this, arguments);
 }
@@ -95,11 +88,17 @@
         // right to add comments.
         var add_comment_placeholder =
             container.one('div.add-comment-placeholder');
+        var comment_widget = null;
         if (add_comment_placeholder !== null) {
-            namespace.setup_add_comment(
-                add_comment_placeholder,
-                latest_comment_container,
-                api_uri);
+            comment_widget = new AddCommentWidget({
+                latestCommentContainer: latest_comment_container,
+                addCommentPlaceholder: add_comment_placeholder,
+                apiUri: api_uri});
+            comment_widget.render(add_comment_placeholder);
+            //namespace.setup_add_comment(
+            //    add_comment_placeholder,
+            //    latest_comment_container,
+            //    api_uri);
         }
         // The blacklist slot with a class 'blacklist-options' is only
         // available when the user has the right to blacklist.
@@ -110,8 +109,8 @@
                 {srcNode: blacklist_slot,
                  sourceName: source_name,
                  dsdLink: api_uri,
-                 latestCommentContainer: latest_comment_container,
-                 addCommentPlaceholder: add_comment_placeholder});
+                 commentWidget: comment_widget
+                });
         }
         // If the user has not the right to blacklist, we disable
         // the blacklist slot.
@@ -218,8 +217,7 @@
     initializer: function(cfg) {
         this.sourceName = cfg.sourceName;
         this.dsdLink = cfg.dsdLink;
-        this.latestCommentContainer = cfg.latestCommentContainer;
-        this.addCommentPlaceholder = cfg.addCommentPlaceholder;
+        this.commentWidget = cfg.commentWidget;
         this.relatedRows = cfg.relatedRows;
         this.container = cfg.container;
         // We call bindUI directly here because the BlacklistWidgets
@@ -380,9 +378,9 @@
                         });
                         fade_to_gray.run();
                     });
-                    namespace.add_comment(
-                        updated_entry, self.addCommentPlaceholder,
-                        self.latestCommentContainer);
+                    if (self.commentWidget !== null) {
+                        self.commentWidget.display_new_comment(updated_entry);
+                    }
                 },
                 failure: function(id, response) {
                     self.unlock();
@@ -428,168 +426,193 @@
 };
 
 /**
- * XXX: rvb 2011-08-10 bug=823777: All this Javascript code to
- * manage the "add comment" slot should be refactored into a
- * YUI widget and tests should be added.
- */
-
-/**
- * This method adds a comment in the UI. It appends a comment to the
- * list of comments and updates the latest comments slot.
- *
- * @param comment_entry {Comment} A comment as returns by the api.
- * @param add_comment_placeholder {Node} The node that contains the
- *     relevant comment fields.
- * @param latest_comment_placeholder {Node} The node that contains the
- *     latest comment.
- */
-namespace.add_comment = function(comment_entry, add_comment_placeholder,
-                                latest_comment_placeholder) {
-    // Grab the XHTML representation of the comment
-    // and prepend it to the list of comments.
-    var config = {
-        on: {
-            success: function(comment_html) {
-                var comment_node = Y.Node.create(comment_html);
-                add_comment_placeholder.insert(comment_node, 'before');
-                var reveal = Y.lazr.effects.slide_out(comment_node);
-                reveal.on("end", function() {
-                    Y.lp.anim.green_flash(
-                        {node: comment_node}).run();
-                });
-                reveal.run();
-            }
-        },
-        accept: Y.lp.client.XHTML
-    };
-    namespace.lp_client.get(comment_entry.get('self_link'), config);
-    namespace.update_latest_comment(
-        comment_entry, latest_comment_placeholder);
-};
-
-/**
- * Handle the add comment event triggered by the 'add comment' form.
- *
- * This method adds a comment via the API and update the UI.
- *
- * @param comment_form {Node} The node that contains the relevant comment
- *     fields.
- * @param latest_comment_placeholder {Node} The node that contains the
- *     latest comment.
- * @param api_uri {string} The uri for the distroseriesdifference to which
- *     the comment is to be added.
- * @param cb_success {function} Called when a comment has successfully
- *     been added. (Deferreds would be awesome right about now.)
- */
-namespace.add_comment_handler = function(comment_form,
-                                         latest_comment_placeholder, api_uri,
-                                         cb_success) {
-    var comment_area = comment_form.one('textarea');
-    var comment_text = comment_area.get('value');
-
-    // Treat empty comments as mistakes.
-    if (Y.Lang.trim(comment_text).length === 0) {
-        Y.lp.anim.red_flash({node: comment_area}).run();
-        return;
-    }
-
-    var success_handler = function(comment_entry) {
-        namespace.add_comment(
-            comment_entry, comment_form, latest_comment_placeholder);
-        comment_form.one('textarea').set('value', '');
-        cb_success();
-    };
-    var failure_handler = function(id, response) {
-        // Re-enable field with red flash.
-        Y.lp.anim.red_flash({node: comment_form}).run();
-    };
-
-    var config = {
-        on: {
-            success: success_handler,
-            failure: failure_handler,
-            start: function() {
-                // Show a spinner.
-                comment_form.one('div.widget-bd')
-                    .append('<img src="/@@/spinner" />');
-                // Disable the textarea and button.
-                comment_form.all('textarea,button')
-                    .setAttribute('disabled', 'disabled');
-            },
-            end: function() {
-                // Remove the spinner.
-                comment_form.all('img').remove();
-                // Enable the form.
-                comment_form.all('textarea,button')
-                    .removeAttribute('disabled');
-            }
-        },
-        parameters: {
-            comment: comment_text
-        }
-    };
-    namespace.lp_client.named_post(api_uri, 'addComment', config);
-};
-
-/**
- * Add the comment fields ready for sliding out.
- *
- * This method adds the markup for a slide-out comment and sets
- * the event handlers.
- *
- * @param placeholder {Node} The node that is to contain the comment
- *     fields.
- * @param api_uri {string} The uri for the distroseriesdifference to which
- *     the comment is to be added.
- */
-namespace.setup_add_comment = function(placeholder,
-                                       latest_comment_placeholder,
-                                       api_uri) {
-    placeholder.insert([
-        '<a class="widget-hd js-action sprite add" href="">',
-        '  Add comment</a>',
-        '<div class="widget-bd lazr-closed" ',
-        '     style="height:0;overflow:hidden">',
-        '  <textarea></textarea><button>Save comment</button>',
-        '</div>'
-        ].join(''), 'replace');
-
-    // The comment area should slide in when the 'Add comment'
-    // action is clicked.
-    var slide_anim = null;
-    var slide = function(direction) {
-        // Slide out if direction is true, slide in if direction
-        // is false, otherwise do the opposite of what's being
-        // animated right now.
-        if (slide_anim === null) {
-            slide_anim = Y.lazr.effects.slide_out(
-                placeholder.one('div.widget-bd'));
-            if (Y.Lang.isBoolean(direction)) {
-                slide_anim.set("reverse", !direction);
-            }
-        }
-        else {
-            if (Y.Lang.isBoolean(direction)) {
-                slide_anim.set("reverse", !direction);
-            }
-            else {
-                slide_anim.set('reverse', !slide_anim.get('reverse'));
-            }
-            slide_anim.stop();
-        }
-        slide_anim.run();
-    };
-    var slide_in = function() { slide(false); };
-
-    placeholder.one('a.widget-hd').on(
-        'click', function(e) { e.preventDefault(); slide(); });
-    placeholder.one('button').on('click', function(e) {
-        e.preventDefault();
-        namespace.add_comment_handler(
-            placeholder, latest_comment_placeholder,
-            api_uri, slide_in);
-    });
-};
+ * AddCommentWidget: the widget used to display a small form to enter
+ * a comment attached to a DSD.
+ */
+function AddCommentWidget(config) {
+    AddCommentWidget.superclass.constructor.apply(this, arguments);
+}
+
+AddCommentWidget.NAME = "addCommentWidget";
+
+AddCommentWidget.ATTRS = {
+    /**
+     * The content of the textarea used to add a new comment.
+     * @type String
+     */
+    comment_text: {
+        getter: function() {
+            return this.addForm.one('textarea').get('value');
+        },
+        setter: function(comment_text) {
+            this.addForm.one('textarea').set('value', comment_text);
+        }
+    }
+};
+
+Y.extend(AddCommentWidget, Y.Widget, {
+    initializer: function(cfg) {
+        this.latestCommentContainer = cfg.latestCommentContainer;
+        this.addCommentPlaceholder = cfg.addCommentPlaceholder;
+        this.apiUri = cfg.apiUri;
+
+        this.addCommentLink = Y.Node.create([
+            '<a class="widget-hd js-action sprite add" href="">',
+            '  Add comment</a>'
+            ].join(''));
+        this.addForm = Y.Node.create([
+            '<div class="widget-bd lazr-closed" ',
+            '     style="height:0;overflow:hidden">',
+            '  <textarea></textarea><button>Save comment</button>',
+            '</div>'
+            ].join(''));
+    },
+
+    renderUI: function() {
+        this.get("contentBox")
+            .append(this.addCommentLink)
+            .append(this.addForm);
+    },
+
+    _fire_anim_end: function(anim, event_name) {
+        var self = this;
+        anim.on("end", function() {
+            self.fire(event_name);
+        });
+    },
+
+    slide_in: function() {
+        var anim = Y.lazr.effects.slide_in(this.addForm);
+        this._fire_anim_end(anim, 'slided_in');
+        anim.run();
+    },
+
+    slide_out: function() {
+        var anim = Y.lazr.effects.slide_out(this.addForm);
+        this._fire_anim_end(anim, 'slided_out');
+        anim.run();
+    },
+
+    bindUI: function() {
+        Y.on("click", function(e) {
+            e.preventDefault();
+            this.slide_out();
+        }, this.addCommentLink, this);
+
+        this.on("comment_added", function(e) {
+            e.preventDefault();
+            var comment_entry = e.details[0];
+            this.display_new_comment(comment_entry);
+        }, this);
+
+        Y.on("click", function(e, comment_entry) {
+            e.preventDefault();
+            this.add_comment_handler();
+        }, this.addForm.one('button'), this);
+    },
+
+    lock: function() {
+        // Show a spinner.
+        this.addForm.append('<img src="/@@/spinner" />');
+        // Disable the textarea and button.
+        this.addForm.all('textarea,button')
+            .setAttribute('disabled', 'disabled');
+    },
+
+    unlock: function() {
+        // Remove the spinner.
+        this.addForm.all('img').remove();
+        // Enable the form and the button.
+        this.addForm.all('textarea,button')
+            .removeAttribute('disabled');
+    },
+
+    clean: function() {
+        this.set('comment_text', '');
+    },
+
+    /**
+     * This method displays a new comment in the UI. It appends a comment
+     * to the list of comments and updates the latest comments slot.
+     *
+     * @param comment_entry {Comment} A comment as returns by the api.
+     */
+    display_new_comment: function(comment_entry) {
+        // Grab the XHTML representation of the comment,
+        // prepend it to the list of comments and update the
+        // 'latest comment' slot.
+        var self = this;
+        var config = {
+            on: {
+                success: function(comment_html) {
+                    var comment_node = Y.Node.create(comment_html);
+                    self.addCommentPlaceholder.insert(comment_node, 'before');
+                    var reveal = Y.lazr.effects.slide_out(comment_node);
+                    reveal.on("end", function() {
+                        Y.lp.anim.green_flash(
+                            {node: comment_node}).run();
+                    });
+                    reveal.run();
+                    namespace.update_latest_comment(
+                        comment_entry, self.latestCommentContainer);
+                 }
+            },
+            accept: Y.lp.client.XHTML
+        };
+        namespace.lp_client.get(comment_entry.get('self_link'), config);
+    },
+
+    /**
+     * Handle the add comment event triggered by the 'add comment' form.
+     *
+     * This method adds the content of the comment form as a comment via
+     * the API.
+     *
+     */
+    add_comment_handler: function() {
+        var comment_text = this.get('comment_text');
+
+        // Treat empty comments as mistakes.
+        if (Y.Lang.trim(comment_text).length === 0) {
+            Y.lp.anim.red_flash({
+                node: this.addForm.one('textarea')
+                }).run();
+            return;
+        }
+
+        var self = this;
+        var success_handler = function(comment_entry) {
+            self.clean();
+            self.slide_in();
+            self.fire('comment_added', comment_entry);
+        };
+        var failure_handler = function(id, response) {
+            // Re-enable field with red flash.
+            var node = self.addForm.one('textarea');
+            Y.lp.anim.red_flash({node: node}).run();
+        };
+
+        var config = {
+            on: {
+                success: success_handler,
+                failure: failure_handler,
+                start: function() {
+                    self.lock();
+                },
+                end: function() {
+                    self.unlock();
+                }
+            },
+            parameters: {
+                comment: comment_text
+            }
+        };
+        namespace.lp_client.named_post(this.apiUri, 'addComment', config);
+    }
+
+});
+
+namespace.AddCommentWidget = AddCommentWidget;
 
 namespace.setup = function() {
     Y.all('table.listing a.toggle-extra').each(function(toggle){
@@ -729,7 +752,6 @@
         build_package_diff_update_config);
 };
 
-
 /**
 * Add a button to start package diff computation.
 *

=== modified file 'lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html'
--- lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html	2011-08-11 11:57:42 +0000
+++ lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html	2011-08-11 11:57:42 +0000
@@ -18,6 +18,7 @@
   <script type="text/javascript" src="../../../app/javascript/lazr/lazr.js"></script>
   <script type="text/javascript" src="../../../app/javascript/extras/extras.js"></script>
   <script type="text/javascript" src="../../../app/javascript/anim/anim.js"></script>
+  <script type="text/javascript" src="../../../app/javascript/effects/effects.js"></script>
   <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
   <script type="text/javascript" src="../../../app/javascript/formoverlay/formoverlay.js"></script>
 

=== modified file 'lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js'
--- lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js	2011-08-11 11:57:42 +0000
+++ lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.js	2011-08-11 11:57:42 +0000
@@ -3,7 +3,7 @@
 
 YUI().use(
         'lp.testing.runner', 'test', 'console', 'node-event-simulate',
-        'lp.soyuz.base', "lp.anim", "lazr.formoverlay",
+        'lp.soyuz.base', "lp.anim", "lazr.formoverlay", "lazr.effects",
         'lp.soyuz.dynamic_dom_updater', 'event-simulate', "io-base",
         'lp.registry.distroseriesdifferences_details', function(Y) {
 
@@ -217,6 +217,32 @@
     }
 };
 
+var assertAllDisabled = function(node, selector) {
+    var all_input_status = node.all(selector).get('disabled');
+    Y.Assert.isTrue(all_input_status.every(function(val) {return val;}));
+};
+
+var assertAllEnabled = function(node, selector) {
+    var all_input_status = node.all(selector).get('disabled');
+    Y.Assert.isTrue(all_input_status.every(function(val) {return !val;}));
+};
+
+function Comment() {}
+
+Comment.prototype.get = function(key) {
+    var data = {
+        comment_date: "2011-08-08T13:15:50.636269+00:00",
+        body_text: 'This is the comment',
+        self_link:  ["https://lp.net/api/devel/u/d//+source/";,
+                     "evolution/+difference/ubuntu/warty/comments/6"
+                    ].join(''),
+        web_link: ["https://lp.net/d/d/+source/evolution/";,
+                   "+difference/ubuntu/warty/comments/6"
+                  ].join('')
+        };
+    return data[key];
+};
+
 var testBlacklistWidget = {
 
     name: 'package-diff-update-interaction',
@@ -226,14 +252,13 @@
             .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.commentWidget = null;
         this.widget = new dsd_details.BlacklistWidget(
             {srcNode: this.node,
              sourceName: 'evolution',
              dsdLink: '/a/link',
-             latestCommentContainer: this.latestCommentContainer,
-             addCommentPlaceholder: this.addCommentPlaceholder});
+             commentWidget: this.commentWidget
+            });
         // Set the animation duration to 0.1 to avoid having to wait for its
         // completion for too long.
         this.widget.ANIM_DURATION = 0.1;
@@ -246,9 +271,7 @@
         Y.Assert.areEqual(
             this.latestCommentContainer,
             this.widget.latestCommentContainer);
-        Y.Assert.areEqual(
-            this.addCommentPlaceholder,
-            this.widget.addCommentPlaceholder);
+        Y.Assert.areEqual(this.commentWidget, this.widget.commentWidget);
     },
 
     test_wire_blacklist_click: function() {
@@ -393,24 +416,16 @@
         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;}));
-    },
-
     assertIsLocked: function() {
-        Y.Assert.isNotNull(Y.one('img[src="/@@/spinner"]'));
-        this.assertAllDisabled('div.blacklist-options input');
+        var node = this.widget.get('srcNode');
+        Y.Assert.isNotNull(node.one('img[src="/@@/spinner"]'));
+        assertAllDisabled(node, 'div.blacklist-options input');
     },
 
     assertIsUnlocked: function() {
-        Y.Assert.isNull(Y.one('img[src="/@@/spinner"]'));
-        this.assertAllEnabled('div.blacklist-options input');
+        var node = this.widget.get('srcNode');
+        Y.Assert.isNull(node.one('img[src="/@@/spinner"]'));
+        assertAllEnabled(node, 'div.blacklist-options input');
     },
 
     test_lock: function() {
@@ -430,20 +445,6 @@
     },
 
     patchNamedPost: function(method_name, expected_parameters) {
-        function Comment() {}
-        Comment.prototype.get = function(key) {
-            var data = {
-                comment_date: "2011-08-08T13:15:50.636269+00:00",
-                body_text: 'This is the comment',
-                self_link:  ["https://lp.net/api/devel/u/d//+source/";,
-                             "evolution/+difference/ubuntu/warty/comments/6"
-                            ].join(''),
-                web_link: ["https://lp.net/d/d/+source/evolution/";,
-                           "+difference/ubuntu/warty/comments/6"
-                           ].join('')
-                };
-            return data[key];
-        };
         var comment_entry = new Comment();
         var self = this;
         dsd_details.lp_client.named_post = function(url, func, config) {
@@ -473,6 +474,13 @@
     },
 
     test_blacklist_submit_handler_blacklist_simple: function() {
+        var mockCommentWidget = Y.Mock();
+        Y.Mock.expect(mockCommentWidget, {
+            method: "display_new_comment",
+            args: [Y.Mock.Value.Object]
+        });
+        this.widget.commentWidget = mockCommentWidget;
+
         this.patchNamedPost(
             'blacklist',
             {comment: 'Test comment', all: false});
@@ -492,7 +500,23 @@
         }
     },
 
+    test_blacklist_submit_handler_blacklist_null_comment_widget: function() {
+        // The widget can cope with a null commentWidget.
+        Y.Assert.isNull(this.widget.commentWidget);
+        var input = Y.one(
+            'div.blacklist-options input[value="BLACKLISTED_CURRENT"]');
+        this.widget.blacklist_submit_handler(
+            'blacklist', false, "Test comment", input);
+    },
+
     test_blacklist_submit_handler_blacklist_all: function() {
+        var mockCommentWidget = Y.Mock();
+        Y.Mock.expect(mockCommentWidget, {
+            method: "display_new_comment",
+            args: [Y.Mock.Value.Object]
+        });
+        this.widget.commentWidget = mockCommentWidget;
+
         this.patchNamedPost(
             'blacklist',
             {comment: 'Test comment', all: true});
@@ -513,6 +537,13 @@
     },
 
     test_blacklist_submit_handler_unblacklist: function() {
+        var mockCommentWidget = Y.Mock();
+        Y.Mock.expect(mockCommentWidget, {
+            method: "display_new_comment",
+            args: [Y.Mock.Value.Object]
+        });
+        this.widget.commentWidget = mockCommentWidget;
+
         this.patchNamedPost(
             'unblacklist',
             {comment: 'Test comment', all: true});
@@ -545,6 +576,272 @@
     }
 };
 
+var testAddCommentWidget = {
+
+    name: 'test-add-comment-widget',
+
+    setUp: function() {
+        Y.one("#placeholder")
+            .empty()
+            .appendChild(Y.Node.create(whole_table));
+        this.latestCommentContainer = Y.one('td.latest-comment-fragment');
+        this.addCommentPlaceholder = Y.one('div.add-comment-placeholder');
+        this.apiUri = '/testuri/';
+        this.widget = new dsd_details.AddCommentWidget({
+            srcNode: this.node,
+            apiUri: this.apiUri,
+            latestCommentContainer: this.latestCommentContainer,
+            addCommentPlaceholder: this.addCommentPlaceholder
+            });
+        this.widget.render(this.addCommentPlaceholder);
+    },
+
+    tearDown: function() {
+        this.widget.destroy();
+    },
+
+    test_initializer: function() {
+        Y.Assert.areEqual(
+            this.latestCommentContainer, this.widget.latestCommentContainer);
+        Y.Assert.areEqual(
+            this.addCommentPlaceholder, this.widget.addCommentPlaceholder);
+        Y.Assert.areEqual(
+            this.apiUri, this.widget.apiUri);
+        // Widget is initially closed.
+        var node = this.widget.get('srcNode');
+        Y.Assert.areEqual(
+            '0pt',
+            node.one('div.widget-bd').getStyle('height'));
+    },
+
+    test_comment_text_getter: function() {
+        this.widget.get('srcNode').one('textarea').set('value', 'Content');
+        Y.Assert.areEqual(
+            'Content',
+            this.widget.get('comment_text'));
+    },
+
+    test_comment_text_setter: function() {
+        this.widget.set('comment_text', 'Content');
+        Y.Assert.areEqual(
+            'Content',
+            this.widget.get('srcNode').one('textarea').get('value'));
+    },
+
+    test_slide_in: function() {
+        // 'Manually' open the widget.
+        var node = this.widget.get('srcNode');
+        node.one('div.widget-bd').setStyle('height', '1000px');
+        var self = this;
+        var listener = function(e) {
+            self.resume(function(){
+                Y.Assert.areEqual(
+                    '0px',
+                    node.one('div.widget-bd').getStyle('height'));
+            });
+        };
+        this.widget.on('slided_in', listener);
+        this.widget.slide_in();
+        this.wait(1000);
+    },
+
+    test_slide_out: function() {
+        var fired = false;
+        var self = this;
+        var listener = function(e) {
+            self.resume(function(){
+                fired = true;
+            });
+        };
+        this.widget.on('slided_out', listener);
+        this.widget.slide_out();
+        this.wait(1000);
+        Y.Assert.isTrue(fired);
+    },
+
+    assertIsLocked: function() {
+        var node = this.widget.get('srcNode');
+        Y.Assert.isNotNull(node.one('img[src="/@@/spinner"]'));
+        assertAllDisabled(node, 'textarea, button');
+    },
+
+    assertIsUnlocked: function() {
+        var node = this.widget.get('srcNode');
+        Y.Assert.isNull(node.one('img[src="/@@/spinner"]'));
+        assertAllEnabled(node, 'textarea, button');
+    },
+
+    test_lock: function() {
+        this.widget.lock();
+        this.assertIsLocked();
+    },
+
+    test_unlock: function() {
+        this.widget.unlock();
+        this.assertIsUnlocked();
+    },
+
+    test_lock_unlock: function() {
+        this.widget.lock();
+        this.widget.unlock();
+        this.assertIsUnlocked();
+    },
+
+    test_wire_click_add_comment_link: function() {
+        var fired = false;
+        var input = this.widget.get('srcNode').one('a.widget-hd');
+        var self = this;
+        var listener = function(e) {
+            self.resume(function(){
+                fired = true;
+            });
+        };
+        this.widget.on('slided_out', listener);
+        input.simulate('click');
+        this.wait();
+        Y.Assert.isTrue(fired);
+    },
+
+    test_wire_comment_added_calls_display_new_comment: function() {
+        var fired = false;
+        var comment_entry = new Comment();
+
+        var display_new_comment = function(entry) {
+            fired = true;
+            Y.ObjectAssert.areEqual(comment_entry, entry);
+        };
+        this.widget.display_new_comment = display_new_comment;
+        this.widget.fire('comment_added', comment_entry);
+
+        Y.Assert.isTrue(fired);
+     },
+
+    test_wire_click_button_calls_add_comment_handler: function() {
+        var input = this.widget.get('srcNode').one('button');
+        var fired = false;
+
+        var add_comment_handler = function() {
+            fired = true;
+        };
+        this.widget.add_comment_handler = add_comment_handler;
+        input.simulate('click');
+
+        Y.Assert.isTrue(fired);
+    },
+
+    test_clean: function() {
+        var comment_text = 'Content';
+        this.widget.get('srcNode').one('textarea').set('value', comment_text);
+        var self = this;
+        var comment_entry = new Comment();
+        var post_called = false;
+        dsd_details.lp_client.named_post = function(url, method, config) {
+            post_called = true;
+            Y.Assert.areEqual('addComment', method);
+            Y.Assert.areEqual(comment_text, config.parameters.comment);
+            config.on.success(comment_entry);
+        };
+        // The event comment_added will be fired.
+        var event_fired = false;
+        event_handler = function(e) {
+            event_fired = true;
+            Y.ObjectAssert.areEqual(comment_entry, e.details[0]);
+        };
+        this.widget.on('comment_added', event_handler);
+
+        this.widget.add_comment_handler();
+
+        Y.Assert.areEqual('', this.widget.get('comment_text'));
+        Y.Assert.isTrue(post_called);
+        Y.Assert.isTrue(event_fired);
+    },
+
+    test_display_new_comment: function() {
+        var comment_html = '<span id="new_comment">Comment content.</span>';
+        var self = this;
+        var comment_entry = new Comment();
+        var get_called = false;
+        dsd_details.lp_client.get = function(url, config) {
+            get_called = true;
+            config.on.success(comment_html);
+        };
+        // The method update_latest_comment will be called with the right
+        // arguments.
+        var update_latest_called = false;
+        dsd_details.update_latest_comment = function(entry, node) {
+            update_latest_called = true;
+            Y.ObjectAssert.areEqual(comment_entry, entry);
+            Y.Assert.areEqual(self.widget.latestCommentContainer, node);
+        };
+        this.widget.display_new_comment(comment_entry);
+
+        // The new comment has been added to the list of comments.
+        Y.Assert.areEqual(
+            'Comment content.',
+            this.widget.addCommentPlaceholder.previous().get('text'));
+        Y.Assert.isTrue(get_called);
+        Y.Assert.isTrue(update_latest_called);
+    },
+
+    test_add_comment_handler_success: function() {
+        var comment_text = 'Content';
+        this.widget.get('srcNode').one('textarea').set('value', comment_text);
+        var comment_entry = new Comment();
+        var post_called = false;
+        dsd_details.lp_client.named_post = function(url, method, config) {
+            post_called = true;
+            Y.Assert.areEqual('addComment', method);
+            Y.Assert.areEqual(comment_text, config.parameters.comment);
+            config.on.success(comment_entry);
+        };
+        // The event comment_added will be fired.
+        var event_fired = false;
+        event_handler = function(e) {
+            event_fired = true;
+            Y.ObjectAssert.areEqual(comment_entry, e.details[0]);
+        };
+        this.widget.on('comment_added', event_handler);
+
+        this.widget.add_comment_handler();
+
+        Y.Assert.areEqual('', this.widget.get('comment_text'));
+        Y.Assert.isTrue(post_called);
+        Y.Assert.isTrue(event_fired);
+        this.assertIsUnlocked();
+    },
+
+    test_add_comment_handler_failure: function() {
+        var comment_text = 'Content';
+        this.widget.get('srcNode').one('textarea').set('value', comment_text);
+        var post_called = false;
+        dsd_details.lp_client.named_post = function(url, method, config) {
+            post_called = true;
+            config.on.failure();
+        };
+        this.widget.add_comment_handler();
+
+        // The content has not been cleaned.
+        Y.Assert.areEqual('Content', this.widget.get('comment_text'));
+        Y.Assert.isTrue(post_called);
+        this.assertIsUnlocked();
+    },
+
+    test_add_comment_handler_empty: function() {
+        // An empty comment is treated as a mistake.
+        var comment_text = '';
+        this.widget.get('srcNode').one('textarea').set('value', comment_text);
+        var self = this;
+        var comment_entry = new Comment();
+        var post_called = false;
+        dsd_details.lp_client.named_post = function(url, method, config) {
+            post_called = true;
+        };
+        this.widget.add_comment_handler();
+        Y.Assert.isFalse(post_called);
+    }
+
+};
+
 var testPackageDiffUpdateInteraction = {
 
     name: 'package-diff-update-interaction',
@@ -662,6 +959,7 @@
 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(testAddCommentWidget));
 suite.add(new Y.Test.Case(testPackageDiffUpdateInteraction));
 
 Y.lp.testing.Runner.run(suite);