Merge lp:~gmb/launchpad/show-comment-form into lp:launchpad

Proposed by Graham Binns
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 13914
Proposed branch: lp:~gmb/launchpad/show-comment-form
Merge into: lp:launchpad
Diff against target: 535 lines (+379/-57)
7 files modified
lib/lp/bugs/javascript/bugtask_index.js (+26/-11)
lib/lp/bugs/javascript/tests/test_async_comment_loading.html (+55/-0)
lib/lp/bugs/javascript/tests/test_async_comment_loading.js (+135/-0)
lib/lp/bugs/templates/bugcomment-macros.pt (+46/-2)
lib/lp/bugs/templates/bugtask-index.pt (+12/-44)
standard_test_template.html (+31/-0)
standard_test_template.js (+74/-0)
To merge this branch: bzr merge lp:~gmb/launchpad/show-comment-form
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+74397@code.launchpad.net

Commit message

[r=abentley][ui=none] [r=abentley][bug=838825] Once all comments have been loaded asynchronously, the comment form will now appear.

Description of the change

This branch fixes bug 838825 by making the add comment form appear once all a bug's comments have finished loading asynchronously.

The Javascript change is simple, and I've refactored the comment form template into a macro so that we're not cluttering up bugtask-index.pt more than necessary. I've also cleaned up some lint as a drive-by fix.

QA Plan
=======

 - Make sure that the bugs.batched_comment_loading.enabled flag is set to 'on' on qastaging
 - Visit a bug with lots of comments
 - Click the "show all comments" link
 - All the comments should load
 - The comment form should appear at the bottom

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I don't see any tests, but AFAIK, this is testable. Please add tests or an explanation of why this cannot be tested.

review: Needs Fixing
Revision history for this message
Graham Binns (gmb) wrote :

On 7 September 2011 16:09, Aaron Bentley <email address hidden> wrote:
> Review: Needs Fixing
> I don't see any tests, but AFAIK, this is testable.  Please add tests or an explanation of why this cannot be tested.

This isn't tested for the simple reason that testing it properly
requires some kind of integration test. Deryck and Gary's YUI tests
with fixtures work hasn't landed yet and I'm loath to add Windmill
tests unless I absolutely have to.

This code is feature-flagged, so I'm not concerned about untested code
breaking out into the wild. I consider having tests to be one of the
criteria for considering this feature "done."

I've added a comment to this effect to the JS.

Revision history for this message
Graham Binns (gmb) wrote :

At Aaron's request I've added some JS tests for the async comment loading code. I've also added a couple of standard_test_templates (HTML and JS) to save people from having to go and cargo-cult from others' work like I did.

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks for adding tests. I know it's a pain being the first to test previously-untested code.

I'm sorry you went through the effort of cargo-culting from others' work. There are already templates at https://dev.launchpad.net/JavascriptUnitTesting so please ensure the two sets of templates are in sync (or delete one).

I notice you don't un-monkeypatch Y.lp.client.Launchpad.prototype.named_post, but I guess it only applies to these tests, so that is fine.

review: Approve
Revision history for this message
Graham Binns (gmb) wrote :

Oh! I had no idea those existed. I'll sync them up.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
2--- lib/lp/bugs/javascript/bugtask_index.js 2011-09-05 23:14:24 +0000
3+++ lib/lp/bugs/javascript/bugtask_index.js 2011-09-10 01:15:25 +0000
4@@ -1096,37 +1096,52 @@
5 * comments.
6 */
7 namespace.load_more_comments = function(batched_comments_url,
8- comments_container) {
9+ comments_container,
10+ io_provider) {
11 var spinner = Y.Node.create(
12- '<img src="/@@/spinner" style="text_align: center; display: none" />');
13+ '<img src="/@@/spinner" style="text_align: center; ' +
14+ 'display: none" />');
15 var spinner_span = Y.one('#more-comments-spinner');
16 spinner_span.setStyle('display', 'inline');
17 var handlers = {
18- success: function(transactionid, response, arguments) {
19+ success: function(transactionid, response, args) {
20 var new_comments_node =
21 Y.Node.create("<div></div>");
22 new_comments_node.set(
23 'innerHTML', response.responseText);
24 spinner_span.setStyle('display', 'none');
25 comments_container.appendChild(new_comments_node);
26- var success_anim = Y.lp.anim.green_flash(
27- {node: new_comments_node});
28- success_anim.run();
29+ if (Y.Lang.isValue(Y.lp.anim)) {
30+ var success_anim = Y.lp.anim.green_flash(
31+ {node: new_comments_node});
32+ success_anim.run();
33+ }
34 batch_url_div = Y.one('#next-batch-url');
35 if (Y.Lang.isValue(batch_url_div)) {
36 batched_comments_url = batch_url_div.get(
37 'innerHTML');
38 batch_url_div.remove();
39 namespace.load_more_comments(
40- batched_comments_url, comments_container);
41+ batched_comments_url, comments_container, io_provider);
42 } else {
43- // Remove the comments-hidden message to avoid
44+ // Remove the comments-hidden messages to avoid
45 // confusion.
46- Y.one('#comments-hidden-message').remove();
47+ Y.each(Y.all('.comments-hidden-message'), function(message) {
48+ message.remove();
49+ });
50+ // Show the comment form, if available.
51+ var comment_form_container = Y.one(
52+ '#add-comment-form-container');
53+ if (Y.Lang.isValue(comment_form_container)) {
54+ comment_form_container.toggleClass('hidden');
55+ }
56 }
57 }
58 };
59- var request = Y.io(batched_comments_url, {on: handlers});
60+ if (!Y.Lang.isValue(io_provider)) {
61+ io_provider = Y.io;
62+ }
63+ var request = io_provider.io(batched_comments_url, {on: handlers});
64 };
65
66
67@@ -1152,7 +1167,7 @@
68 });
69 show_comments_link.addClass('js-action');
70 }
71-}
72+};
73
74
75 }, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
76
77=== added file 'lib/lp/bugs/javascript/tests/test_async_comment_loading.html'
78--- lib/lp/bugs/javascript/tests/test_async_comment_loading.html 1970-01-01 00:00:00 +0000
79+++ lib/lp/bugs/javascript/tests/test_async_comment_loading.html 2011-09-10 01:15:25 +0000
80@@ -0,0 +1,55 @@
81+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
82+<html>
83+ <head>
84+ <title>Status Editor</title>
85+
86+ <!-- YUI and test setup -->
87+ <script type="text/javascript"
88+ src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
89+ </script>
90+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
91+ <script type="text/javascript"
92+ src="../../../app/javascript/testing/testrunner.js"></script>
93+
94+ <!-- Dependency -->
95+ <script type="text/javascript"
96+ src="../../../../canonical/launchpad/icing/yui/attribute/attribute.js">
97+ </script>
98+ <script type="text/javascript"
99+ src="../../../../canonical/launchpad/icing/yui/event-custom/event-custom.js">
100+ </script>
101+ <script type="text/javascript"
102+ src="../../../../canonical/launchpad/icing/yui/oop/oop.js">
103+ </script>
104+ <script type="text/javascript"
105+ src="../../../app/javascript/overlay/overlay.js">
106+ </script>
107+ <script type="text/javascript"
108+ src="../../../app/javascript/choiceedit/choiceedit.js">
109+ </script>
110+ <script type="text/javascript"
111+ src="../../../app/javascript/client.js"></script>
112+ <script type="text/javascript"
113+ src="../../../app/javascript/testing/mockio.js"></script>
114+
115+ <!-- The module under test -->
116+ <script type="text/javascript" src="../bugtask_index.js"></script>
117+
118+
119+ <!-- The test suite -->
120+ <script type="text/javascript" src="test_async_comment_loading.js"></script>
121+
122+ <!-- Test layout -->
123+ <link rel="stylesheet"
124+ href="../../../../canonical/launchpad/javascript/test.css" />
125+ <style type="text/css">
126+ /* CSS classes specific to this test */
127+ .unseen { display: none; }
128+ </style>
129+ <div id="more-comments-spinner" style="display: hidden">
130+ Look, I'm a spinner. *spins*
131+ </div>
132+</head>
133+<body class="yui3-skin-sam">
134+</body>
135+</html>
136
137=== added file 'lib/lp/bugs/javascript/tests/test_async_comment_loading.js'
138--- lib/lp/bugs/javascript/tests/test_async_comment_loading.js 1970-01-01 00:00:00 +0000
139+++ lib/lp/bugs/javascript/tests/test_async_comment_loading.js 2011-09-10 01:15:25 +0000
140@@ -0,0 +1,135 @@
141+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
142+
143+YUI({
144+ base: '../../../../canonical/launchpad/icing/yui/',
145+ filter: 'raw',
146+ combine: false,
147+ fetchCSS: false
148+ }).use('event', 'lp.bugs.bugtask_index', 'lp.client', 'node',
149+ 'lp.testing.mockio', 'test', 'widget-stack', 'console',
150+ function(Y) {
151+
152+
153+// Local aliases
154+var Assert = Y.Assert,
155+ ArrayAssert = Y.ArrayAssert;
156+var module = Y.lp.bugs.bugtask_index;
157+var suite = new Y.Test.Suite("Async comment loading tests");
158+
159+var comments_markup =
160+ "<div>This is a comment</div>" +
161+ "<div>So is this</div>" +
162+ "<div>So is this</div>" +
163+ "<div>And this, too.</div>";
164+
165+suite.add(new Y.Test.Case({
166+
167+ name: 'Basic async comment loading tests',
168+
169+ setUp: function() {
170+ // Monkeypatch LP to avoid network traffic and to make
171+ // some things work as expected.
172+ Y.lp.client.Launchpad.prototype.named_post =
173+ function(url, func, config) {
174+ config.on.success();
175+ };
176+ LP = {
177+ 'cache': {
178+ 'bug': {
179+ self_link: "http://bugs.example.com/bugs/1234"
180+ }}};
181+ // Add some HTML to the page for us to use.
182+ this.comments_container = Y.Node.create(
183+ '<div id="comments-container"></div>');
184+ this.add_comment_form_container = Y.Node.create(
185+ '<div id="add-comment-form-container" class="hidden"></div>');
186+ Y.one('body').appendChild(this.comments_container);
187+ Y.one('body').appendChild(this.add_comment_form_container);
188+ },
189+
190+ tearDown: function() {
191+ this.comments_container.remove();
192+ this.add_comment_form_container.remove();
193+ },
194+
195+ /**
196+ * load_more_comments() calls the passed batch_commments_url of the
197+ * current bug task and loads more comments from it.
198+ */
199+ test_load_more_comments_loads_more_comments: function() {
200+ var mockio = new Y.lp.testing.mockio.MockIo();
201+ module.load_more_comments(
202+ '', this.comments_container, mockio);
203+ mockio.success({
204+ responseText: comments_markup,
205+ responseHeaders: {'Content-Type': 'application/xhtml'}
206+ });
207+ Assert.areEqual(
208+ '<div>' + comments_markup + '</div>',
209+ this.comments_container.get('innerHTML'));
210+ },
211+
212+ /**
213+ * load_more_comments() will show the "add comment" form once all
214+ * the comments have loaded.
215+ */
216+ test_load_more_comments_shows_add_comment_form: function() {
217+ var add_comment_form_container = Y.one(
218+ '#add-comment-form-container');
219+ Assert.isTrue(add_comment_form_container.hasClass('hidden'));
220+ var mockio = new Y.lp.testing.mockio.MockIo();
221+ module.load_more_comments(
222+ '', this.comments_container, mockio);
223+ mockio.success({
224+ responseText: comments_markup,
225+ responseHeaders: {'Content-Type': 'application/xhtml'}
226+ });
227+ Assert.isFalse(add_comment_form_container.hasClass('hidden'));
228+ },
229+
230+ /**
231+ * load_more_comments() will call itself recursively until there are
232+ * no more comments to load.
233+ */
234+ test_load_more_comments_is_recursive: function() {
235+ var next_batch_url_div =
236+ '<div id="next-batch-url">https://launchpad.dev/</div>';
237+ var more_comments_to_load_markup =
238+ '<div>Here, have a comment. There are more where this came' +
239+ 'from</div>';
240+ var mockio = new Y.lp.testing.mockio.MockIo();
241+ module.load_more_comments(
242+ '', this.comments_container, mockio);
243+ mockio.success({
244+ responseText: next_batch_url_div + more_comments_to_load_markup,
245+ responseHeaders: {'Content-Type': 'application/xhtml'}
246+ });
247+ mockio.success({
248+ responseText: comments_markup,
249+ responseHeaders: {'Content-Type': 'application/xhtml'}
250+ });
251+ var expected_markup =
252+ '<div>' + more_comments_to_load_markup + '</div>' +
253+ '<div>' + comments_markup + '</div>';
254+ Assert.areEqual(
255+ expected_markup, this.comments_container.get('innerHTML'));
256+ },
257+
258+}));
259+
260+var handle_complete = function(data) {
261+ window.status = '::::' + JSON.stringify(data);
262+ };
263+Y.Test.Runner.on('complete', handle_complete);
264+Y.Test.Runner.add(suite);
265+
266+var yconsole = new Y.Console({
267+ newestOnTop: false
268+});
269+yconsole.render('#log');
270+
271+Y.on('domready', function() {
272+ Y.Test.Runner.run();
273+});
274+
275+});
276
277=== modified file 'lib/lp/bugs/templates/bugcomment-macros.pt'
278--- lib/lp/bugs/templates/bugcomment-macros.pt 2011-08-30 12:29:14 +0000
279+++ lib/lp/bugs/templates/bugcomment-macros.pt 2011-09-10 01:15:25 +0000
280@@ -51,8 +51,7 @@
281 xmlns:metal="http://xml.zope.org/namespaces/metal"
282 metal:define-macro="break">
283 <div id="comments-container"></div>
284- <div class="boardComment" style="border-bottom: 0"
285- id="comments-hidden-message">
286+ <div class="boardComment comments-hidden-message" style="border-bottom: 0">
287 <div class="boardCommentDetails">
288 <table>
289 <tbody>
290@@ -82,4 +81,49 @@
291 </div>
292 </div>
293
294+<div
295+ xmlns:tal="http://xml.zope.org/namespaces/tal"
296+ xmlns:metal="http://xml.zope.org/namespaces/metal"
297+ metal:define-macro="comment-form">
298+ <div tal:define="comment_form nocall:context/@@+addcomment-form;
299+ dummy comment_form/initialize" id="add-comment-form">
300+ <div tal:condition="context/bug/duplicateof"
301+ class="warning message"
302+ id="warning-comment-on-duplicate">
303+ Remember, this bug report is a duplicate of
304+ <a tal:define="duplicateof context/bug/duplicateof"
305+ tal:condition="duplicateof/required:launchpad.View"
306+ tal:attributes="href duplicateof/fmt:url; title
307+ duplicateof/title; style string:margin-right: 4px;
308+ id string:duplicate-of-warning-link;"
309+ tal:content="string:bug #${duplicateof/id}."
310+ >bug #42</a>
311+ <span
312+ tal:define="duplicateof context/bug/duplicateof"
313+ tal:condition="not:duplicateof/required:launchpad.View"
314+ tal:replace="string: a private bug." />
315+ Comment here only if you think the duplicate status is wrong.
316+ </div>
317+ <h2>Add comment</h2>
318+ <form action="+addcomment"
319+ method="post"
320+ enctype="multipart/form-data"
321+ accept-charset="UTF-8">
322+ <tal:comment-input
323+ replace="structure comment_form/widgets/comment" />
324+ <div class="actions"
325+ tal:content="structure
326+ comment_form/actions/field.actions.save/render" />
327+ </form>
328+ <script type="text/javascript">
329+ LPS.use('lp.app.comment', function(Y) {
330+ var comment = new Y.lp.app.comment.Comment();
331+ comment.render();
332+ });
333+ </script>
334+ </div>
335+ <tal:attachment-link
336+ define="add_attachment_link context_menu/addcomment"
337+ replace="structure add_attachment_link/render" />
338+ </div>
339 </tal:root>
340
341=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
342--- lib/lp/bugs/templates/bugtask-index.pt 2011-09-02 01:01:49 +0000
343+++ lib/lp/bugs/templates/bugtask-index.pt 2011-09-10 01:15:25 +0000
344@@ -297,49 +297,8 @@
345 <tal:comment-list-complete
346 tal:condition="not:view/visible_comments_truncated_for_display">
347 <tal:logged-in condition="view/user">
348- <div tal:define="comment_form nocall:context/@@+addcomment-form;
349- dummy comment_form/initialize"
350- id="add-comment-form">
351- <div
352- tal:condition="context/bug/duplicateof"
353- class="warning message"
354- id="warning-comment-on-duplicate">
355- Remember, this bug report is a duplicate of
356- <a
357- tal:define="duplicateof context/bug/duplicateof"
358- tal:condition="duplicateof/required:launchpad.View"
359- tal:attributes="href duplicateof/fmt:url; title
360- duplicateof/title; style string:margin-right: 4px;
361- id string:duplicate-of-warning-link;"
362- tal:content="string:bug #${duplicateof/id}."
363- >bug #42</a>
364- <span
365- tal:define="duplicateof context/bug/duplicateof"
366- tal:condition="not:duplicateof/required:launchpad.View"
367- tal:replace="string: a private bug." />
368- Comment here only if you think the duplicate status is wrong.
369- </div>
370- <h2>Add comment</h2>
371- <form action="+addcomment"
372- method="post"
373- enctype="multipart/form-data"
374- accept-charset="UTF-8">
375- <tal:comment-input
376- replace="structure comment_form/widgets/comment" />
377- <div class="actions"
378- tal:content="structure
379- comment_form/actions/field.actions.save/render" />
380- </form>
381- <script type="text/javascript">
382- LPS.use('lp.app.comment', function(Y) {
383- var comment = new Y.lp.app.comment.Comment();
384- comment.render();
385- });
386- </script>
387- </div>
388- <tal:attachment-link
389- define="add_attachment_link context_menu/addcomment"
390- replace="structure add_attachment_link/render" />
391+ <metal:comment-form
392+ metal:use-macro="context/@@bugcomment-macros/comment-form" />
393 </tal:logged-in>
394
395 <tal:not-logged-in condition="not: view/user">
396@@ -351,7 +310,7 @@
397 </tal:comment-list-complete>
398 <tal:comment-list-truncated
399 tal:condition="view/visible_comments_truncated_for_display">
400- <div class="informational message">
401+ <div class="informational message comments-hidden-message" >
402 Displaying first <span
403 tal:replace="view/visible_initial_comments">23</span>
404 and last <span
405@@ -367,6 +326,15 @@
406 view_all_href">add a comment</a>.
407 </tal:what-next>
408 </div>
409+ <tal:if
410+ condition="request/features/bugs.batched_comment_loading.enabled">
411+ <tal:logged-in condition="view/user">
412+ <div id="add-comment-form-container" class="hidden">
413+ <metal:comment-form
414+ metal:use-macro="context/@@bugcomment-macros/comment-form" />
415+ </div>
416+ </tal:logged-in>
417+ </tal:if>
418 </tal:comment-list-truncated>
419 </tal:comments>
420
421
422=== added file 'standard_test_template.html'
423--- standard_test_template.html 1970-01-01 00:00:00 +0000
424+++ standard_test_template.html 2011-09-10 01:15:25 +0000
425@@ -0,0 +1,31 @@
426+<!--
427+Copyright 2011 Canonical Ltd. This software is licensed under the
428+GNU Affero General Public License version 3 (see the file LICENSE).
429+-->
430+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
431+<html>
432+ <head>
433+ <title>Launchpad TESTLIBRARY</title>
434+ <!-- YUI and test setup -->
435+ <script type="text/javascript"
436+ src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
437+ </script>
438+ <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
439+ <script type="text/javascript"
440+ src="../../../../app/javascript/testing/testrunner.js"></script>
441+
442+ <!-- The module under test -->
443+ <script type="text/javascript" src="../TESTLIBRARY.js"></script>
444+
445+ <!-- The test suite -->
446+ <script type="text/javascript" src="test_TESTLIBRARY.js"></script>
447+ </head>
448+ <body class="yui-skin-sam">
449+
450+ <!-- The example markup required by the script to run -->
451+ <div id="expected-id">
452+ ...
453+ </div>
454+ </body>
455+</html>
456+
457
458=== added file 'standard_test_template.js'
459--- standard_test_template.js 1970-01-01 00:00:00 +0000
460+++ standard_test_template.js 2011-09-10 01:15:25 +0000
461@@ -0,0 +1,74 @@
462+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
463+YUI({
464+ base: '../../../../canonical/launchpad/icing/yui/',
465+ filter: 'raw',
466+ combine: false,
467+ fetchCSS: false
468+
469+// Don't forget to add the module under test to the use() clause.
470+
471+ }).use('event', 'lp.client', 'node', 'test', 'widget-stack',
472+ 'console', function(Y) {
473+
474+// Local aliases
475+var Assert = Y.Assert,
476+ ArrayAssert = Y.ArrayAssert;
477+var mynamespace = Y.lp.mynamespace;
478+var suite = new Y.Test.Suite("mynamespace Tests");
479+
480+suite.add(new Y.Test.Case({
481+ // Test the setup method.
482+ name: 'setup',
483+
484+ _should: {
485+ error: {
486+ test_config_undefined: true,
487+ }
488+ },
489+
490+ setUp: function() {
491+ this.tbody = Y.get('#milestone-rows');
492+ },
493+
494+ tearDown: function() {
495+ delete this.tbody;
496+ mynamespace._milestone_row_uri_template = null;
497+ mynamespace._tbody = null;
498+ },
499+
500+ test_good_config: function() {
501+ // Verify the config data is stored.
502+ var config = {
503+ milestone_row_uri_template: '/uri',
504+ milestone_rows_id: '#milestone-rows'
505+ };
506+ mynamespace.setup(config);
507+ Y.Assert.areSame(
508+ config.milestone_row_uri_template,
509+ mynamespace._milestone_row_uri_template);
510+ Y.Assert.areSame(this.tbody, mynamespace._tbody);
511+ },
512+
513+ test_config_undefined: function() {
514+ // Verify an error is thrown if there is no config.
515+ mynamespace.setup();
516+ },
517+}));
518+
519+
520+var handle_complete = function(data) {
521+ window.status = '::::' + JSON.stringify(data);
522+ };
523+Y.Test.Runner.on('complete', handle_complete);
524+Y.Test.Runner.add(suite);
525+
526+var yconsole = new Y.Console({
527+ newestOnTop: false
528+});
529+yconsole.render('#log');
530+
531+Y.on('domready', function() {
532+ Y.Test.Runner.run();
533+});
534+
535+});