← Back to team overview

launchpad-reviewers team mailing list archive

lp:~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747 into lp:launchpad

 

Graham Binns has proposed merging lp:~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #687747 The subscribe wizard should load its contents from $bug/+subscribe/++form++
  https://bugs.launchpad.net/bugs/687747


This branch is the latest in a line of branches to get the new
subscription wizard widget ready to be linked up with the Launchpad
code.

In this branch, the existing code, which was pretty much a placeholder
and very linear, is replaced with lots of asynchronous, event-driven
stuff. This leaves us ready for a final branch or two that will allow us
to start using the subscription wizard.

Changes made in this branch (no lint):

== lib/lp/bugs/javascript/bug_subscription_widget.js ==

 - I've updated the existing code to make it event driven, so that
   calling the initialize_subscription_wizard function sets up a bunch
   of event handlers and then calls the load_subscription_form method in
   order to get the ball rolling.

== lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html ==

 - I've imported a few more bits of JS needed to make the tests work.

== lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js ==

 - I've updated the tests to reflect the event-driven nature of the new
   code and I've added tests to cover the new event handling stuff.
-- 
https://code.launchpad.net/~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747/+merge/43629
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747 into lp:launchpad.
=== modified file 'lib/lp/bugs/javascript/bug_subscription_widget.js'
--- lib/lp/bugs/javascript/bug_subscription_widget.js	2010-12-03 12:39:40 +0000
+++ lib/lp/bugs/javascript/bug_subscription_widget.js	2010-12-14 10:54:13 +0000
@@ -4,11 +4,11 @@
  * Handling of the bug subscription form overlay widget.
  *
  * @module bugs
- * @submodule bug_subscription_widget
+ * @submodule bug_subscription_wizard
  */
-YUI.add('lp.bugs.bug_subscription_widget', function(Y) {
+YUI.add('lp.bugs.bug_subscription_wizard', function(Y) {
 
-var namespace = Y.namespace('lp.bugs.bug_subscription_widget');
+var namespace = Y.namespace('lp.bugs.bug_subscription_wizard');
 
 var submit_button_html =
     '<button type="submit" name="field.actions.subscribe" ' +
@@ -18,76 +18,112 @@
     '<button type="button" name="field.actions.cancel" ' +
     'class="lazr-neg lazr-btn" >Cancel</button>';
 
+namespace.subscription_wizard = null;
+namespace.subscribe_form_body = null;
+
 namespace.create_subscription_wizard = function() {
-    // Construct the form. This is a bit hackish but it saves us from
-    // having to try to get information from TAL into JavaScript and all
-    // the horror that entails.
-    var subscribe_form_body =
-        '<div>' +
-        '    <p>Tell me when</p>' +
-        '    <table>' +
-        '       <tr>' +
-        '         <td>' +
-        '           <input type="radio" name="field.bug_notification_level" ' +
-        '               id="bug-notification-level-comments"' +
-        '               value="Discussion"' +
-        '               class="bug-notification-level" />' +
-        '         </td>' +
-        '         <td>' +
-        '           <label for="bug-notification-level-comments">' +
-        '             A change is made or a comment is added to this bug' +
-        '           </label>' +
-        '         </td>' +
-        '     </tr>' +
-        '     <tr>' +
-        '       <td>' +
-        '         <input type="radio" name="field.bug_notification_level" ' +
-        '             id="bug-notification-level-metadata" value="Details"' +
-        '             class="bug-notification-level" />' +
-        '       </td>' +
-        '       <td>' +
-        '         <label for="bug-notification-level-metadata">' +
-        '             A change is made to the bug; do not notify me about ' +
-        '             new comments.' +
-        '         </label>' +
-        '       </td>' +
-        '     </tr>' +
-        '     <tr>' +
-        '       <td>' +
-        '         <input type="radio" name="field.bug_notification_level" ' +
-        '             id="bug-notification-level-lifecycle" value="Lifecycle"' +
-        '             class="bug-notification-level" />' +
-        '       </td>' +
-        '       <td>' +
-        '         <label for="bug-notification-level-lifecycle">' +
-        '           Changes are made to the bug\'s status.' +
-        '         </label>' +
-        '       </td>' +
-        '     </tr>' +
-        '  </table>' +
-        '</div>';
-
     // Create the do-you-want-to-subscribe FormOverlay.
     var wizard_steps = [
         new Y.lazr.wizard.Step({
-            form_content: subscribe_form_body,
+            form_content: namespace.subscribe_form_body,
             form_submit_button: Y.Node.create(submit_button_html),
             form_cancel_button: Y.Node.create(cancel_button_html),
             funcLoad: function() {},
             funcCleanUp: function() {}
             })
         ];
-    var subscribe_wizard = new Y.lazr.wizard.Wizard({
+    namespace.subscription_wizard = new Y.lazr.wizard.Wizard({
         headerContent: '<h2>Subscribe to this bug</h2>',
         centered: true,
         visible: false,
         steps: wizard_steps
     });
-    subscribe_wizard.render('#subscribe-wizard');
-
-    return subscribe_wizard;
+    namespace.subscription_wizard.render('#subscribe-wizard');
+    Y.fire('subscriptionwizard:ready');
+};
+
+/**
+ * Load the subscription form from a remote source.
+ *
+ * @method load_subscription_form
+ * @param {string} url the URL to load the form from.
+ * @param {Object} io_provider an Object providing a .io() method. This
+ *     will only be used for testing purposes; if io_provider is not
+ *     passed we'll just use Y.io for everything.
+ */
+namespace.load_subscription_form = function(url, io_provider) {
+    if (io_provider === undefined) {
+        io_provider = Y;
+    }
+    function on_success(id, response) {
+        namespace.subscribe_form_body = response.responseText;
+        Y.fire('subscriptionform:loaded');
+    }
+    function on_failure(id, response) {
+        namespace.subscribe_form_body =
+            "Sorry, an error occurred while loading the form.";
+        Y.fire('subscriptionform:loaded');
+    }
+    var cfg = {
+        on: {
+            success: on_success,
+            failure: on_failure
+            },
+        }
+    io_provider.io(url, cfg);
+};
+
+/**
+ * Initialize the subscription wizard and set up event handlers.
+ *
+ * @method initialize_subscription_wizard
+ * @param {Node} target_node The node to which to link the wizard.
+ * @param {string} url the URL to load the form from.
+ * @param {Object} io_provider an Object providing a .io() method.
+ *      This is only used for testing.
+ */
+namespace.initialize_subscription_wizard = function(
+    target_node, url, io_provider)
+{
+    namespace.set_up_event_handlers(target_node);
+    namespace.load_subscription_form(url, io_provider);
+};
+
+/**
+ * Set up the event handlers for the wizard.
+ *
+ * @method set_up_event_handlers
+ * @param {Node} target_node The node to which to link the wizard.
+ */
+    // Set up the event handlers.
+namespace.set_up_event_handlers = function(target_node) {
+    Y.on(
+        'subscriptionform:loaded',
+        Y.bind(function(e) {
+            // We only create the subscription wizard once the form
+            // contents have been loaded from their remote source.
+            namespace.create_subscription_wizard()
+        }
+    ));
+    Y.on(
+        'subscriptionwizard:ready',
+        Y.bind(function(e) {
+            // Once the wizard has loaded, bind the show_wizard method
+            // to the onClick handler for the target_node.
+            target_node.on('click', namespace.show_wizard);
+        }
+    ));
+};
+
+/**
+ * Show the wizard.
+ *
+ * @method show_wizard
+ */
+namespace.show_wizard = function() {
+    namespace.subscription_wizard.show()
 };
 
 }, "0.1", {"requires": [
     "base", "io", "oop", "node", "event", "lazr.formoverlay",
-    "lazr.effects", "lazr.wizard"]});
+    "log", "lazr.effects", "lazr.wizard"]});

=== modified file 'lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html'
--- lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html	2010-11-04 20:05:41 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html	2010-12-14 10:54:13 +0000
@@ -8,14 +8,23 @@
   <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
   <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
   <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet"
+      href="../../../../canonical/launchpad/icing/lazr/build/testing/assets/testlogger.css"/>
 
   <!-- Dependency -->
   <script type="text/javascript"
     src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
   <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/lazr/build/testing/testing.js"></script>
+  <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/lazr/build/testing/mockio.js"></script>
+  <script type="text/javascript"
     src="../../../../canonical/launchpad/icing/lazr/build/overlay/overlay.js">
   </script>
   <script type="text/javascript"
+    src="../../../../canonical/launchpad/icing/lazr/build/formoverlay/formoverlay.js">
+  </script>
+  <script type="text/javascript"
     src="../../../../canonical/launchpad/icing/lazr/build/choiceedit/choiceedit.js">
   </script>
   <script type="text/javascript"

=== modified file 'lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js'
--- lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js	2010-12-03 12:39:40 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js	2010-12-14 10:54:13 +0000
@@ -6,18 +6,75 @@
     combine: false,
     fetchCSS: false
     }).use(
-        'event', 'lp.bugs.bug_subscription_widget', 'node', 'test',
-        'widget-stack', 'console', function(Y) {
+        'event', 'event-simulate', 'lazr.testing.mockio',
+        'lp.bugs.bug_subscription_wizard', 'node', 'test',
+        'widget-stack', 'console', 'log', function(Y) {
 
 // Local aliases
 var Assert = Y.Assert,
     ArrayAssert = Y.ArrayAssert;
 
 var suite = new Y.Test.Suite("Bug subscription widget tests");
+var subscribe_node = Y.Node.create(
+    '<a href="#" id="subscription-widget-link">Click me</a>');
+
+var subscribe_form_body =
+    '<div>' +
+    '    <p>Tell me when</p>' +
+    '    <table>' +
+    '       <tr>' +
+    '         <td>' +
+    '           <input type="radio" ' +
+    '               name="field.bug_notification_level" ' +
+    '               id="bug-notification-level-comments"' +
+    '               value="Discussion"' +
+    '               class="bug-notification-level" />' +
+    '         </td>' +
+    '         <td>' +
+    '           <label for="bug-notification-level-comments">' +
+    '             A change is made or a comment is added to ' +
+    '             this bug' +
+    '           </label>' +
+    '         </td>' +
+    '     </tr>' +
+    '     <tr>' +
+    '       <td>' +
+    '         <input type="radio"' +
+    '             name="field.bug_notification_level" ' +
+    '             id="bug-notification-level-metadata"' +
+    '             value="Details"' +
+    '             class="bug-notification-level" />' +
+    '       </td>' +
+    '       <td>' +
+    '         <label for="bug-notification-level-metadata">' +
+    '             A change is made to the bug; do not notify ' +
+    '             me about new comments.' +
+    '         </label>' +
+    '       </td>' +
+    '     </tr>' +
+    '     <tr>' +
+    '       <td>' +
+    '         <input type="radio"' +
+    '             name="field.bug_notification_level" ' +
+    '             id="bug-notifiction-level-lifecycle"' +
+    '             value="Lifecycle"' +
+    '             class="bug-notification-level" />' +
+    '       </td>' +
+    '       <td>' +
+    '         <label for="bug-notification-level-lifecycle">' +
+    '           Changes are made to the bug\'s status.' +
+    '         </label>' +
+    '       </td>' +
+    '     </tr>' +
+    '  </table>' +
+    '</div>';
+var success_response = Y.lazr.testing.MockIo.makeXhrSuccessResponse(
+    subscribe_form_body);
+var mock_io = new Y.lazr.testing.MockIo();
 
 suite.add(new Y.Test.Case({
 
-    name: 'bug_subscription_widget_basics',
+    name: 'bug_subscription_wizard_basics',
 
     setUp: function() {
         // Monkeypatch LP.client to avoid network traffic and to make
@@ -37,15 +94,25 @@
         LP.client.cache.bug = {
             self_link: "http://bugs.example.com/bugs/1234";
         };
-        this.subscribe_wizard =
-            Y.lp.bugs.bug_subscription_widget.create_subscription_wizard();
-        this.subscribe_wizard.show();
+
+        // Monkeypatch the subscribe_form_body attribute of the
+        // wizard module so that the test doesn't cause the wizard to
+        // try to load a Launchpad page.
+        Y.lp.bugs.bug_subscription_wizard.subscribe_form_body =
+            subscribe_form_body;
+        Y.lp.bugs.bug_subscription_wizard.create_subscription_wizard();
+        this.subscription_wizard =
+            Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
+    },
+
+    tearDown: function() {
+        this.subscription_wizard.destroy();
     },
 
     test_bug_notification_level_values: function() {
         // The bug_notification_level field will have a value that's one
         // of [Discussion, Details, Lifecycle].
-        var form_node = this.subscribe_wizard.form_node;
+        var form_node = this.subscription_wizard.form_node;
         var notification_level_radio_buttons = form_node.queryAll(
             'input[name=field.bug_notification_level]');
         Y.each(notification_level_radio_buttons, function(obj) {
@@ -55,9 +122,150 @@
                 value == 'Details' ||
                 value == 'Lifecycle');
         });
-    }
-
-}));
+    },
+
+    test_wizard_first_step_content: function() {
+        // The form content for the first wizard step will have been
+        // loaded using the _get_subscribe_form_content function defined
+        // above.
+        var steps = this.subscription_wizard.steps;
+        Assert.areEqual(
+            subscribe_form_body,
+            this.subscription_wizard.get('steps')[0].get('form_content'));
+    },
+
+    test_wizard_is_hidden_on_creation: function() {
+        // The wizard is hidden when it is created.
+        var bounding_box =
+            this.subscription_wizard.get('boundingBox');
+        Assert.isFalse(this.subscription_wizard.get('visible'));
+        Assert.isTrue(
+            bounding_box.hasClass('yui3-lazr-wizard-hidden'),
+            "The form is hidden after cancel is clicked.");
+    }
+}));
+
+
+suite.add(new Y.Test.Case({
+
+    name: 'bug_subscription_wizard_async',
+
+    setUp: function() {
+        // Monkeypatch LP.client to avoid network traffic and to make
+        // some things work as expected.
+        window.LP = {
+            client: {
+                links: {},
+                // Empty client constructor.
+                Launchpad: function() {},
+                cache: {}
+            }
+        };
+        LP.client.Launchpad.prototype.named_post =
+            function(url, func, config) {
+                config.on.success();
+            };
+        LP.client.cache.bug = {
+            self_link: "http://bugs.example.com/bugs/1234";
+        };
+
+        // Monkeypatch the subscribe_form_body attribute of the
+        // wizard module so that the test doesn't cause the wizard to
+        // try to load a Launchpad page.
+        Y.lp.bugs.bug_subscription_wizard.subscribe_form_body =
+            subscribe_form_body;
+    },
+
+    tearDown: function() {
+        Y.lp.bugs.bug_subscription_wizard.subscription_wizard.destroy();
+    },
+
+    test_create_subscription_wizard_fires_event: function() {
+        // The create_subscription_wizard function fires a
+        // subscriptionwizard:ready event when it has completed so that
+        // the subscription wizard can then be used in a page.
+        var subscriptionwizard_ready_fired = false;
+        Y.on(
+            'subscriptionwizard:ready',
+            Y.bind(function(e) { subscriptionwizard_ready_fired = true; }));
+        Y.lp.bugs.bug_subscription_wizard.create_subscription_wizard();
+        Assert.isTrue(subscriptionwizard_ready_fired);
+    },
+
+    test_load_subscription_form_fires_event: function() {
+        // The load_subscription_form() function fires a
+        // subscriptionform:loaded event.
+        var subscriptionform_loaded_fired = false;
+        Y.on(
+            'subscriptionform:loaded',
+            Y.bind(function(e) { subscriptionform_loaded_fired = true; }));
+        Y.lp.bugs.bug_subscription_wizard.load_subscription_form(
+            'http://example.com', mock_io);
+        mock_io.simulateXhr(success_response, false);
+        Assert.isTrue(subscriptionform_loaded_fired);
+    },
+
+    test_wizard_created_on_subscriptionform_loaded_event: function() {
+        // When the subscriptionform:loaded event is fired, the wizard
+        // will be created.
+        Y.lp.bugs.bug_subscription_wizard.set_up_event_handlers(
+            subscribe_node);
+        Y.fire('subscriptionform:loaded');
+        var subscription_wizard =
+            Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
+        Assert.areEqual(
+            subscribe_form_body,
+            subscription_wizard.get('steps')[0].get('form_content'))
+    },
+
+    test_load_subscription_form_loads_subscription_form: function() {
+        // The load_subscription_form() function loads the body of the
+        // subscription form form a remote URL.
+        Y.lp.bugs.bug_subscription_wizard.set_up_event_handlers(
+            subscribe_node);
+        Y.lp.bugs.bug_subscription_wizard.load_subscription_form(
+            'http://example.com', mock_io);
+        mock_io.simulateXhr(success_response, false);
+
+        // The contents of the subscribe_form_body for the wizard will
+        // have been loaded.
+        var subscription_wizard =
+            Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
+        Assert.areEqual(
+            subscribe_form_body,
+            subscription_wizard.get('steps')[0].get('form_content'));
+    },
+
+    test_initialize_subscription_wizard_links_everything: function() {
+        // The initialize_subscription_wizard function sets up event
+        // handlers and links the wizard, once created, to the node that
+        // we pass to it.
+        var subscriptionform_loaded_fired = false;
+        var subscriptionwizard_ready_fired = false;
+        Y.on(
+            'subscriptionform:loaded',
+            Y.bind(function(e) { subscriptionform_loaded_fired = true; }));
+        Y.on(
+            'subscriptionwizard:ready',
+            Y.bind(function(e) { subscriptionwizard_ready_fired = true; }));
+        Y.lp.bugs.bug_subscription_wizard.initialize_subscription_wizard(
+            subscribe_node, 'http://example.com', mock_io);
+        mock_io.simulateXhr(success_response, false);
+        Y.Event.simulate(Y.Node.getDOMNode(subscribe_node), 'click');
+
+        // All the events will have fired and the node will be linked to
+        // the wizard via its onClick handler.
+        var subscription_wizard =
+            Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
+        Assert.isTrue(subscriptionform_loaded_fired);
+        Assert.isTrue(subscriptionwizard_ready_fired);
+        Assert.areEqual(
+            subscribe_form_body,
+            subscription_wizard.get('steps')[0].get('form_content'));
+        Assert.isTrue(subscription_wizard.get('visible'));
+    }
+}));
+
 
 var handle_complete = function(data) {
     status_node = Y.Node.create(