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

Proposed by Graham Binns on 2010-12-14
Status: Merged
Approved by: Graham Binns on 2010-12-17
Approved revision: no longer in the source branch.
Merged at revision: 12100
Proposed branch: lp:~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747
Merge into: lp:launchpad
Diff against target: 480 lines (+320/-67)
3 files modified
lib/lp/bugs/javascript/bug_subscription_widget.js (+93/-57)
lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html (+9/-0)
lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js (+218/-10)
To merge this branch: bzr merge lp:~gmb/launchpad/use-subscribe-form-for-fun-and-profit-bug-687747
Reviewer Review Type Date Requested Status
Deryck Hodge (community) code 2010-12-14 Approve on 2010-12-17
Review via email: mp+43629@code.launchpad.net

Commit Message

[r=deryck][ui=none][bug=687747] The new subscribe wizard will now load asynchronously and will pull its form contents from a specified URL.

Description of the Change

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.

To post a comment you must log in.
Deryck Hodge (deryck) wrote :

This looks good. Thanks for the nice testing! I'm glad to see you working on the widget's base functionality like this first, and testing well, before moving on to implementation. Nicely done.

One minor tip, if you're curious about his:

I had to disable fetchCSS when Windmill was running to prevent YUI trying to go off-site for resources, so as you may have noticed, the logger is hard to read. If you apply this diff locally (setting fetchCSS to true) you can have an easier to read test:

=== modified file 'lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js'
--- lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js 2010-12-14 10:42:45 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js 2010-12-17 16:12:40 +0000
@@ -4,7 +4,7 @@
     base: '../../../../canonical/launchpad/icing/yui/',
     filter: 'raw',
     combine: false,
- fetchCSS: false
+ fetchCSS: true
     }).use(
         'event', 'event-simulate', 'lazr.testing.mockio',
         'lp.bugs.bug_subscription_wizard', 'node', 'test',

Of course, don't commit this until we can work out testing better. But locally it makes the test easier on the eyes.

Cheers,
deryck

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/javascript/bug_subscription_widget.js'
2--- lib/lp/bugs/javascript/bug_subscription_widget.js 2010-12-03 12:39:40 +0000
3+++ lib/lp/bugs/javascript/bug_subscription_widget.js 2010-12-14 10:54:13 +0000
4@@ -4,11 +4,11 @@
5 * Handling of the bug subscription form overlay widget.
6 *
7 * @module bugs
8- * @submodule bug_subscription_widget
9+ * @submodule bug_subscription_wizard
10 */
11-YUI.add('lp.bugs.bug_subscription_widget', function(Y) {
12+YUI.add('lp.bugs.bug_subscription_wizard', function(Y) {
13
14-var namespace = Y.namespace('lp.bugs.bug_subscription_widget');
15+var namespace = Y.namespace('lp.bugs.bug_subscription_wizard');
16
17 var submit_button_html =
18 '<button type="submit" name="field.actions.subscribe" ' +
19@@ -18,76 +18,112 @@
20 '<button type="button" name="field.actions.cancel" ' +
21 'class="lazr-neg lazr-btn" >Cancel</button>';
22
23+namespace.subscription_wizard = null;
24+namespace.subscribe_form_body = null;
25+
26 namespace.create_subscription_wizard = function() {
27- // Construct the form. This is a bit hackish but it saves us from
28- // having to try to get information from TAL into JavaScript and all
29- // the horror that entails.
30- var subscribe_form_body =
31- '<div>' +
32- ' <p>Tell me when</p>' +
33- ' <table>' +
34- ' <tr>' +
35- ' <td>' +
36- ' <input type="radio" name="field.bug_notification_level" ' +
37- ' id="bug-notification-level-comments"' +
38- ' value="Discussion"' +
39- ' class="bug-notification-level" />' +
40- ' </td>' +
41- ' <td>' +
42- ' <label for="bug-notification-level-comments">' +
43- ' A change is made or a comment is added to this bug' +
44- ' </label>' +
45- ' </td>' +
46- ' </tr>' +
47- ' <tr>' +
48- ' <td>' +
49- ' <input type="radio" name="field.bug_notification_level" ' +
50- ' id="bug-notification-level-metadata" value="Details"' +
51- ' class="bug-notification-level" />' +
52- ' </td>' +
53- ' <td>' +
54- ' <label for="bug-notification-level-metadata">' +
55- ' A change is made to the bug; do not notify me about ' +
56- ' new comments.' +
57- ' </label>' +
58- ' </td>' +
59- ' </tr>' +
60- ' <tr>' +
61- ' <td>' +
62- ' <input type="radio" name="field.bug_notification_level" ' +
63- ' id="bug-notification-level-lifecycle" value="Lifecycle"' +
64- ' class="bug-notification-level" />' +
65- ' </td>' +
66- ' <td>' +
67- ' <label for="bug-notification-level-lifecycle">' +
68- ' Changes are made to the bug\'s status.' +
69- ' </label>' +
70- ' </td>' +
71- ' </tr>' +
72- ' </table>' +
73- '</div>';
74-
75 // Create the do-you-want-to-subscribe FormOverlay.
76 var wizard_steps = [
77 new Y.lazr.wizard.Step({
78- form_content: subscribe_form_body,
79+ form_content: namespace.subscribe_form_body,
80 form_submit_button: Y.Node.create(submit_button_html),
81 form_cancel_button: Y.Node.create(cancel_button_html),
82 funcLoad: function() {},
83 funcCleanUp: function() {}
84 })
85 ];
86- var subscribe_wizard = new Y.lazr.wizard.Wizard({
87+ namespace.subscription_wizard = new Y.lazr.wizard.Wizard({
88 headerContent: '<h2>Subscribe to this bug</h2>',
89 centered: true,
90 visible: false,
91 steps: wizard_steps
92 });
93- subscribe_wizard.render('#subscribe-wizard');
94-
95- return subscribe_wizard;
96+ namespace.subscription_wizard.render('#subscribe-wizard');
97+ Y.fire('subscriptionwizard:ready');
98+};
99+
100+/**
101+ * Load the subscription form from a remote source.
102+ *
103+ * @method load_subscription_form
104+ * @param {string} url the URL to load the form from.
105+ * @param {Object} io_provider an Object providing a .io() method. This
106+ * will only be used for testing purposes; if io_provider is not
107+ * passed we'll just use Y.io for everything.
108+ */
109+namespace.load_subscription_form = function(url, io_provider) {
110+ if (io_provider === undefined) {
111+ io_provider = Y;
112+ }
113+ function on_success(id, response) {
114+ namespace.subscribe_form_body = response.responseText;
115+ Y.fire('subscriptionform:loaded');
116+ }
117+ function on_failure(id, response) {
118+ namespace.subscribe_form_body =
119+ "Sorry, an error occurred while loading the form.";
120+ Y.fire('subscriptionform:loaded');
121+ }
122+ var cfg = {
123+ on: {
124+ success: on_success,
125+ failure: on_failure
126+ },
127+ }
128+ io_provider.io(url, cfg);
129+};
130+
131+/**
132+ * Initialize the subscription wizard and set up event handlers.
133+ *
134+ * @method initialize_subscription_wizard
135+ * @param {Node} target_node The node to which to link the wizard.
136+ * @param {string} url the URL to load the form from.
137+ * @param {Object} io_provider an Object providing a .io() method.
138+ * This is only used for testing.
139+ */
140+namespace.initialize_subscription_wizard = function(
141+ target_node, url, io_provider)
142+{
143+ namespace.set_up_event_handlers(target_node);
144+ namespace.load_subscription_form(url, io_provider);
145+};
146+
147+/**
148+ * Set up the event handlers for the wizard.
149+ *
150+ * @method set_up_event_handlers
151+ * @param {Node} target_node The node to which to link the wizard.
152+ */
153+ // Set up the event handlers.
154+namespace.set_up_event_handlers = function(target_node) {
155+ Y.on(
156+ 'subscriptionform:loaded',
157+ Y.bind(function(e) {
158+ // We only create the subscription wizard once the form
159+ // contents have been loaded from their remote source.
160+ namespace.create_subscription_wizard()
161+ }
162+ ));
163+ Y.on(
164+ 'subscriptionwizard:ready',
165+ Y.bind(function(e) {
166+ // Once the wizard has loaded, bind the show_wizard method
167+ // to the onClick handler for the target_node.
168+ target_node.on('click', namespace.show_wizard);
169+ }
170+ ));
171+};
172+
173+/**
174+ * Show the wizard.
175+ *
176+ * @method show_wizard
177+ */
178+namespace.show_wizard = function() {
179+ namespace.subscription_wizard.show()
180 };
181
182 }, "0.1", {"requires": [
183 "base", "io", "oop", "node", "event", "lazr.formoverlay",
184- "lazr.effects", "lazr.wizard"]});
185+ "log", "lazr.effects", "lazr.wizard"]});
186
187=== modified file 'lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html'
188--- lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html 2010-11-04 20:05:41 +0000
189+++ lib/lp/bugs/javascript/tests/test_bug_subscription_widget.html 2010-12-14 10:54:13 +0000
190@@ -8,14 +8,23 @@
191 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
192 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
193 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
194+ <link rel="stylesheet"
195+ href="../../../../canonical/launchpad/icing/lazr/build/testing/assets/testlogger.css"/>
196
197 <!-- Dependency -->
198 <script type="text/javascript"
199 src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
200 <script type="text/javascript"
201+ src="../../../../canonical/launchpad/icing/lazr/build/testing/testing.js"></script>
202+ <script type="text/javascript"
203+ src="../../../../canonical/launchpad/icing/lazr/build/testing/mockio.js"></script>
204+ <script type="text/javascript"
205 src="../../../../canonical/launchpad/icing/lazr/build/overlay/overlay.js">
206 </script>
207 <script type="text/javascript"
208+ src="../../../../canonical/launchpad/icing/lazr/build/formoverlay/formoverlay.js">
209+ </script>
210+ <script type="text/javascript"
211 src="../../../../canonical/launchpad/icing/lazr/build/choiceedit/choiceedit.js">
212 </script>
213 <script type="text/javascript"
214
215=== modified file 'lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js'
216--- lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js 2010-12-03 12:39:40 +0000
217+++ lib/lp/bugs/javascript/tests/test_bug_subscription_widget.js 2010-12-14 10:54:13 +0000
218@@ -6,18 +6,75 @@
219 combine: false,
220 fetchCSS: false
221 }).use(
222- 'event', 'lp.bugs.bug_subscription_widget', 'node', 'test',
223- 'widget-stack', 'console', function(Y) {
224+ 'event', 'event-simulate', 'lazr.testing.mockio',
225+ 'lp.bugs.bug_subscription_wizard', 'node', 'test',
226+ 'widget-stack', 'console', 'log', function(Y) {
227
228 // Local aliases
229 var Assert = Y.Assert,
230 ArrayAssert = Y.ArrayAssert;
231
232 var suite = new Y.Test.Suite("Bug subscription widget tests");
233+var subscribe_node = Y.Node.create(
234+ '<a href="#" id="subscription-widget-link">Click me</a>');
235+
236+var subscribe_form_body =
237+ '<div>' +
238+ ' <p>Tell me when</p>' +
239+ ' <table>' +
240+ ' <tr>' +
241+ ' <td>' +
242+ ' <input type="radio" ' +
243+ ' name="field.bug_notification_level" ' +
244+ ' id="bug-notification-level-comments"' +
245+ ' value="Discussion"' +
246+ ' class="bug-notification-level" />' +
247+ ' </td>' +
248+ ' <td>' +
249+ ' <label for="bug-notification-level-comments">' +
250+ ' A change is made or a comment is added to ' +
251+ ' this bug' +
252+ ' </label>' +
253+ ' </td>' +
254+ ' </tr>' +
255+ ' <tr>' +
256+ ' <td>' +
257+ ' <input type="radio"' +
258+ ' name="field.bug_notification_level" ' +
259+ ' id="bug-notification-level-metadata"' +
260+ ' value="Details"' +
261+ ' class="bug-notification-level" />' +
262+ ' </td>' +
263+ ' <td>' +
264+ ' <label for="bug-notification-level-metadata">' +
265+ ' A change is made to the bug; do not notify ' +
266+ ' me about new comments.' +
267+ ' </label>' +
268+ ' </td>' +
269+ ' </tr>' +
270+ ' <tr>' +
271+ ' <td>' +
272+ ' <input type="radio"' +
273+ ' name="field.bug_notification_level" ' +
274+ ' id="bug-notifiction-level-lifecycle"' +
275+ ' value="Lifecycle"' +
276+ ' class="bug-notification-level" />' +
277+ ' </td>' +
278+ ' <td>' +
279+ ' <label for="bug-notification-level-lifecycle">' +
280+ ' Changes are made to the bug\'s status.' +
281+ ' </label>' +
282+ ' </td>' +
283+ ' </tr>' +
284+ ' </table>' +
285+ '</div>';
286+var success_response = Y.lazr.testing.MockIo.makeXhrSuccessResponse(
287+ subscribe_form_body);
288+var mock_io = new Y.lazr.testing.MockIo();
289
290 suite.add(new Y.Test.Case({
291
292- name: 'bug_subscription_widget_basics',
293+ name: 'bug_subscription_wizard_basics',
294
295 setUp: function() {
296 // Monkeypatch LP.client to avoid network traffic and to make
297@@ -37,15 +94,25 @@
298 LP.client.cache.bug = {
299 self_link: "http://bugs.example.com/bugs/1234"
300 };
301- this.subscribe_wizard =
302- Y.lp.bugs.bug_subscription_widget.create_subscription_wizard();
303- this.subscribe_wizard.show();
304+
305+ // Monkeypatch the subscribe_form_body attribute of the
306+ // wizard module so that the test doesn't cause the wizard to
307+ // try to load a Launchpad page.
308+ Y.lp.bugs.bug_subscription_wizard.subscribe_form_body =
309+ subscribe_form_body;
310+ Y.lp.bugs.bug_subscription_wizard.create_subscription_wizard();
311+ this.subscription_wizard =
312+ Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
313+ },
314+
315+ tearDown: function() {
316+ this.subscription_wizard.destroy();
317 },
318
319 test_bug_notification_level_values: function() {
320 // The bug_notification_level field will have a value that's one
321 // of [Discussion, Details, Lifecycle].
322- var form_node = this.subscribe_wizard.form_node;
323+ var form_node = this.subscription_wizard.form_node;
324 var notification_level_radio_buttons = form_node.queryAll(
325 'input[name=field.bug_notification_level]');
326 Y.each(notification_level_radio_buttons, function(obj) {
327@@ -55,9 +122,150 @@
328 value == 'Details' ||
329 value == 'Lifecycle');
330 });
331- }
332-
333-}));
334+ },
335+
336+ test_wizard_first_step_content: function() {
337+ // The form content for the first wizard step will have been
338+ // loaded using the _get_subscribe_form_content function defined
339+ // above.
340+ var steps = this.subscription_wizard.steps;
341+ Assert.areEqual(
342+ subscribe_form_body,
343+ this.subscription_wizard.get('steps')[0].get('form_content'));
344+ },
345+
346+ test_wizard_is_hidden_on_creation: function() {
347+ // The wizard is hidden when it is created.
348+ var bounding_box =
349+ this.subscription_wizard.get('boundingBox');
350+ Assert.isFalse(this.subscription_wizard.get('visible'));
351+ Assert.isTrue(
352+ bounding_box.hasClass('yui3-lazr-wizard-hidden'),
353+ "The form is hidden after cancel is clicked.");
354+ }
355+}));
356+
357+
358+suite.add(new Y.Test.Case({
359+
360+ name: 'bug_subscription_wizard_async',
361+
362+ setUp: function() {
363+ // Monkeypatch LP.client to avoid network traffic and to make
364+ // some things work as expected.
365+ window.LP = {
366+ client: {
367+ links: {},
368+ // Empty client constructor.
369+ Launchpad: function() {},
370+ cache: {}
371+ }
372+ };
373+ LP.client.Launchpad.prototype.named_post =
374+ function(url, func, config) {
375+ config.on.success();
376+ };
377+ LP.client.cache.bug = {
378+ self_link: "http://bugs.example.com/bugs/1234"
379+ };
380+
381+ // Monkeypatch the subscribe_form_body attribute of the
382+ // wizard module so that the test doesn't cause the wizard to
383+ // try to load a Launchpad page.
384+ Y.lp.bugs.bug_subscription_wizard.subscribe_form_body =
385+ subscribe_form_body;
386+ },
387+
388+ tearDown: function() {
389+ Y.lp.bugs.bug_subscription_wizard.subscription_wizard.destroy();
390+ },
391+
392+ test_create_subscription_wizard_fires_event: function() {
393+ // The create_subscription_wizard function fires a
394+ // subscriptionwizard:ready event when it has completed so that
395+ // the subscription wizard can then be used in a page.
396+ var subscriptionwizard_ready_fired = false;
397+ Y.on(
398+ 'subscriptionwizard:ready',
399+ Y.bind(function(e) { subscriptionwizard_ready_fired = true; }));
400+ Y.lp.bugs.bug_subscription_wizard.create_subscription_wizard();
401+ Assert.isTrue(subscriptionwizard_ready_fired);
402+ },
403+
404+ test_load_subscription_form_fires_event: function() {
405+ // The load_subscription_form() function fires a
406+ // subscriptionform:loaded event.
407+ var subscriptionform_loaded_fired = false;
408+ Y.on(
409+ 'subscriptionform:loaded',
410+ Y.bind(function(e) { subscriptionform_loaded_fired = true; }));
411+ Y.lp.bugs.bug_subscription_wizard.load_subscription_form(
412+ 'http://example.com', mock_io);
413+ mock_io.simulateXhr(success_response, false);
414+ Assert.isTrue(subscriptionform_loaded_fired);
415+ },
416+
417+ test_wizard_created_on_subscriptionform_loaded_event: function() {
418+ // When the subscriptionform:loaded event is fired, the wizard
419+ // will be created.
420+ Y.lp.bugs.bug_subscription_wizard.set_up_event_handlers(
421+ subscribe_node);
422+ Y.fire('subscriptionform:loaded');
423+ var subscription_wizard =
424+ Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
425+ Assert.areEqual(
426+ subscribe_form_body,
427+ subscription_wizard.get('steps')[0].get('form_content'))
428+ },
429+
430+ test_load_subscription_form_loads_subscription_form: function() {
431+ // The load_subscription_form() function loads the body of the
432+ // subscription form form a remote URL.
433+ Y.lp.bugs.bug_subscription_wizard.set_up_event_handlers(
434+ subscribe_node);
435+ Y.lp.bugs.bug_subscription_wizard.load_subscription_form(
436+ 'http://example.com', mock_io);
437+ mock_io.simulateXhr(success_response, false);
438+
439+ // The contents of the subscribe_form_body for the wizard will
440+ // have been loaded.
441+ var subscription_wizard =
442+ Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
443+ Assert.areEqual(
444+ subscribe_form_body,
445+ subscription_wizard.get('steps')[0].get('form_content'));
446+ },
447+
448+ test_initialize_subscription_wizard_links_everything: function() {
449+ // The initialize_subscription_wizard function sets up event
450+ // handlers and links the wizard, once created, to the node that
451+ // we pass to it.
452+ var subscriptionform_loaded_fired = false;
453+ var subscriptionwizard_ready_fired = false;
454+ Y.on(
455+ 'subscriptionform:loaded',
456+ Y.bind(function(e) { subscriptionform_loaded_fired = true; }));
457+ Y.on(
458+ 'subscriptionwizard:ready',
459+ Y.bind(function(e) { subscriptionwizard_ready_fired = true; }));
460+ Y.lp.bugs.bug_subscription_wizard.initialize_subscription_wizard(
461+ subscribe_node, 'http://example.com', mock_io);
462+ mock_io.simulateXhr(success_response, false);
463+ Y.Event.simulate(Y.Node.getDOMNode(subscribe_node), 'click');
464+
465+ // All the events will have fired and the node will be linked to
466+ // the wizard via its onClick handler.
467+ var subscription_wizard =
468+ Y.lp.bugs.bug_subscription_wizard.subscription_wizard;
469+ Assert.isTrue(subscriptionform_loaded_fired);
470+ Assert.isTrue(subscriptionwizard_ready_fired);
471+ Assert.areEqual(
472+ subscribe_form_body,
473+ subscription_wizard.get('steps')[0].get('form_content'));
474+ Assert.isTrue(subscription_wizard.get('visible'));
475+ }
476+}));
477+
478
479 var handle_complete = function(data) {
480 status_node = Y.Node.create(