Merge lp:~allenap/launchpad/me-too-ajax-bug-361097-unit-tests into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~allenap/launchpad/me-too-ajax-bug-361097-unit-tests
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~allenap/launchpad/me-too-ajax-bug-361097-unit-tests
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
Review via email: mp+9647@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

This branch AJAXifies the "me-too" (aka "this bug affects me") part of
the bugs page.

Note that UI review was done on an earlier branch, in a separate merge
proposal:
  https://code.edge.launchpad.net/~allenap/launchpad/me-too-ajax-bug-361097/+merge/9510

File by file:

lib/canonical/launchpad/javascript/bugs/bugtask-index.js

  Added a function setup_me_too that uses a new widget,
  MeTooChoiceSource, to do its business.

  MeTooChoiceSource extends from ChoiceSource to add a few frills,
  like showing and hiding a flame icon (displayed if the user is
  affected by the bug), making the widget display inline (by default
  it displays as a block), and of course, to do an API call to set the
  affected status.

lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html
lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js

  Heaps of boilerplate and some tests for the customizations
  MeTooChoiceSource has over a stock ChoiceSource from lazr-js.

  In lp:~allenap/lazr-js/value-items-mismatch-tests-bug-361097 I
  landed some extra unit tests for ChoiceSource to clarify behaviour
  when config.value does not correspond to any of the items in
  config.items. MeTooChoiceSource relies on this behaviour so it
  seemed sensible to cement it in.

lib/lp/bugs/browser/bugtask.py
lib/lp/bugs/browser/tests/test_bugtask.py

  New property current_user_affected_js_status returns one of "null",
  "true" or "false", i.e. valid Javascript indicating the affected
  status of the user. The tests also cover the existing
  current_user_affected_status.

lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt

  Quite extensive reworking of this pagetest.

lib/lp/bugs/templates/bugtasks-and-nominations-table.pt

  Rip out the old static markup and Javascript code, replace it with
  markup to support the static (no Javascript) and dynamic (Javascript
  enabled) scenarios, and call out to setup_me_too.

  There is a span with a "static" class that contains all the markup
  when Javascript is disabled. There is a span with a "dynamic" class
  that is hidden first of all. The visibility of these spans is
  swapped by MeTooChoiceSource at run time. This means that the
  framework markup can stay in the template.

lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py

  The beginnings of a Windmill test. This took so long and extracted
  so much virtual blood from me that I'm going to complete it in a
  later branch. Testing the overlays and stuff is extremely painful
  and fiddly.

TODO:

  Finish the Windmill test.

Revision history for this message
Deryck Hodge (deryck) wrote :

Hi, Gavin.

This is nice work overall. ~900 lines isn't so bad to review. :)

Some minor things need fixing, most of which I mentioned on IRC:

    ** The inline CSS in test_me_too.html can be dropped in
    favor of:

        <link rel="stylesheet" href="../../test.css" />

    ** The custom method Assert.isEqual should be
    dropped in favor of the builtin Y.Assert.areEqual.

    ** The test_save_data test should be dropped,
    since it doesn't test anything more than an object
    literal passed through a mock API function.

Otherwise, this looks good from a code point of view. It's a nice
piece of functionality, too.

I will note that when I played with this, the overlay opened in weird
locations if I had scrolled down the page some before trying to click
the "affects me too" link. For example, if the link was near the top
of the window, the overlay was partially off screen and I had to
scroll back up. This isn't a huge blocking issue, IMHO, and I assume
covered in the UI review, but maybe we want to track a bug against the
widget if one isn't already open.

I know that these UI/JS branches can seem to go on forever sometimes,
so I understand needing to see some branch landing progress. But what
you have here is nice on its own, too.

Cheers,
deryck

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

On Tue, 04 Aug 2009 16:34:43 -0000
Deryck Hodge <email address hidden> wrote:

> Review: Needs Fixing
> Hi, Gavin.
>
> This is nice work overall. ~900 lines isn't so bad to review. :)

Thanks for the review Deryck.

> Some minor things need fixing, most of which I mentioned on IRC:
>
> ** The inline CSS in test_me_too.html can be dropped in
> favor of:
>
> <link rel="stylesheet" href="../../test.css" />

Neat, didn't know about that, thanks.

...
> I will note that when I played with this, the overlay opened in weird
> locations if I had scrolled down the page some before trying to click
> the "affects me too" link. For example, if the link was near the top
> of the window, the overlay was partially off screen and I had to
> scroll back up. This isn't a huge blocking issue, IMHO, and I assume
> covered in the UI review, but maybe we want to track a bug against the
> widget if one isn't already open.

I can't replicate this. Didn't intellectronica land a fix in lazr-js
related to positioning earlier today? Perhaps pulling your lazr-js
dependency will fix this. Can you try and let me know if it still
breaks?

I've attached a diff of the changes since the review.

1=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
2--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-04 14:57:07 +0000
3+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-04 16:17:45 +0000
4@@ -1331,19 +1331,8 @@
5 bugs.setup_me_too = function(user_is_affected) {
6 var me_too_content = Y.get('#affectsmetoo');
7 var me_too_edit = new MeTooChoiceSource({
8- contentBox: me_too_content,
9- value: user_is_affected,
10- title: 'Does this bug affect you?',
11- items: [
12- {name: 'Yes, it affects me', value: true,
13- source_name: 'This bug affects me too',
14- disabled: false},
15- {name: "No, it doesn't affect me", value: false,
16- source_name: "This bug doesn't affect me",
17- disabled: false}
18- ],
19- elementToFlash: me_too_content,
20- backgroundColor: '#FFFFFF'
21+ contentBox: me_too_content, value: user_is_affected,
22+ elementToFlash: me_too_content
23 });
24 me_too_edit.render();
25 };
26@@ -1369,6 +1358,33 @@
27
28 MeTooChoiceSource.ATTRS = {
29 /**
30+ * The title is always the same, so bake it in here.
31+ *
32+ * @attribute title
33+ * @type String
34+ */
35+ title: {
36+ value: 'Does this bug affect you?'
37+ },
38+
39+ /**
40+ * The items are always the same, so bake them in here.
41+ *
42+ * @attribute items
43+ * @type Array
44+ */
45+ items: {
46+ value: [
47+ { name: 'Yes, it affects me', value: true,
48+ source_name: 'This bug affects me too',
49+ disabled: false },
50+ { name: "No, it doesn't affect me", value: false,
51+ source_name: "This bug doesn't affect me",
52+ disabled: false }
53+ ]
54+ },
55+
56+ /**
57 * Y.Node containing a flame icon, displayed when the user is
58 * affected by the current bug. Should be automatically calculated
59 * by HTML_PARSER.
60
61=== modified file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html'
62--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 2009-08-03 14:38:44 +0000
63+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 2009-08-04 16:47:37 +0000
64@@ -21,33 +21,8 @@
65 <!-- The test suite -->
66 <script type="text/javascript" src="test_me_too.js"></script>
67
68- <style type="text/css">
69- /* Taken and customized from testlogger.css */
70- #log .yui-console-content { width:44em }
71- #log .yui-console .yui-console-bd { height:30em }
72- #log .yui-console .yui-console-controls { display:none; }
73- #log .yui-console .yui-console-hd { display:none; }
74- #log .yui-console .yui-console-ft { position:absolute;top:0em; }
75-
76- #log .yui-console-entry-src { display:none; }
77-
78- #log .yui-console-entry-pass .yui-console-entry-cat {
79- background-color: green;
80- font-weight: bold;
81- color: white;
82- }
83- #log .yui-console-entry-fail .yui-console-entry-cat {
84- background-color: red;
85- font-weight: bold;
86- color: white;
87- }
88- #log .yui-console-entry-ignore .yui-console-entry-cat {
89- background-color: #666;
90- font-weight: bold;
91- color: white;
92- }
93- </style>
94-
95+ <!-- Test layout -->
96+ <link rel="stylesheet" href="../../test.css" />
97 <style type="text/css">
98 /* CSS classes specific to this test */
99 .unseen { display: none; }
100
101=== modified file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js'
102--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 2009-08-04 14:45:29 +0000
103+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 2009-08-04 16:17:40 +0000
104@@ -11,10 +11,6 @@
105 var Assert = Y.Assert,
106 ArrayAssert = Y.ArrayAssert;
107
108-Assert.isEqual = function(a, b, msg) {
109- Assert.isTrue(a == b, msg);
110-};
111-
112 /*
113 * A wrapper for the Y.Event.simulate() function. The wrapper accepts
114 * CSS selectors and Node instances instead of raw nodes.
115@@ -46,11 +42,10 @@
116 setUp: function() {
117 // Monkeypatch LP.client to avoid network traffic and to make
118 // some things work as expected.
119- LP.client.Launchpad.prototype.named_post = (
120+ LP.client.Launchpad.prototype.named_post =
121 function(url, func, config) {
122 config.on.success();
123- }
124- );
125+ };
126 LP.client.cache.bug = {
127 self_link: "http://bugs.example.com/bugs/1234"
128 };
129@@ -76,19 +71,8 @@
130 Y.get("body").appendChild(inpage);
131 var me_too_content = Y.get('#affectsmetoo');
132 this.config = {
133- contentBox: me_too_content,
134- value: null,
135- title: 'This bug:',
136- items: [
137- {name: 'Affects me too', value: true,
138- source_name: 'This bug affects me too',
139- disabled: false},
140- {name: 'Does not affect me', value: false,
141- source_name: "This bug doesn't affect me",
142- disabled: false}
143- ],
144- elementToFlash: me_too_content,
145- backgroundColor: '#FFFFFF'
146+ contentBox: me_too_content, value: null,
147+ elementToFlash: me_too_content
148 };
149 this.choice_edit = new Y.bugs._MeTooChoiceSource(this.config);
150 this.choice_edit.render();
151@@ -109,7 +93,7 @@
152 */
153 test_is_inline: function() {
154 var display = this.choice_edit.get('boundingBox').getStyle('display');
155- Assert.isEqual(
156+ Assert.areEqual(
157 display, 'inline', "Not displayed inline, display is: " + display);
158 },
159
160@@ -206,12 +190,11 @@
161 // assertions in this method because exceptions are swallowed
162 // somewhere. Instead, we save something testable to a local
163 // var.
164- LP.client.Launchpad.prototype.named_post = (
165+ LP.client.Launchpad.prototype.named_post =
166 function(url, func, config) {
167 edit_icon_src_during_save = edit_icon.get('src');
168 config.on[callback]();
169- }
170- );
171+ };
172 simulate(this.choice_edit._choice_list.get('boundingBox'),
173 'li a[href$=true]', 'click');
174 Assert.isNotNull(
175@@ -222,33 +205,6 @@
176 Assert.isNull(
177 edit_icon.get('src').match(/\/spinner$/),
178 "The edit icon is displaying a spinner once the choice has been made.");
179- },
180-
181- /**
182- * Test that the save call back to Launchpad makes sense.
183- */
184- test_save_data: function() {
185- var url, func, config;
186- LP.client.Launchpad.prototype.named_post = (
187- function(f_url, f_func, f_config) {
188- url = f_url, func = f_func, config = f_config;
189- config.on.success();
190- }
191- );
192- // Simulate selecting something.
193- simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
194- simulate(this.choice_edit._choice_list.get('boundingBox'),
195- 'li a[href$=true]', 'click');
196- // See what we've got.
197- Assert.isEqual(url, "http://bugs.example.com/bugs/1234",
198- "LP client call to wrong URL.");
199- Assert.isEqual(func, "markUserAffected",
200- "LP client call to wrong method.");
201- Assert.isNotNull(config.on);
202- Assert.isNotNull(config.on.success);
203- Assert.isNotNull(config.on.failure);
204- Assert.isNotNull(config.parameters);
205- Assert.isTrue(config.parameters.affected);
206 }
207
208 }));
Revision history for this message
Eleanor Berger (intellectronica) wrote :

I suspect the scrolling problems you've experienced are fixed by the branch I landed earlier. If not, let's file a bug.

Revision history for this message
Deryck Hodge (deryck) wrote :

Indeed an updated lazr-js fixed the positioning and/or scrolling problem for me. And the updated diff looks good, too.

Cheers,
deryck

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
2--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-07-29 11:13:40 +0000
3+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-04 14:48:47 +0000
4@@ -1308,6 +1308,139 @@
5 }
6 };
7
8+/**
9+ * Set up the "me too" selection.
10+ *
11+ * Called once, on load, to initialize the page. Call this function if
12+ * the "me too" information is displayed on a bug page and the user is
13+ * logged in.
14+ *
15+ * @method setup_me_too
16+ */
17+bugs.setup_me_too = function(user_is_affected) {
18+ var me_too_content = Y.get('#affectsmetoo');
19+ var me_too_edit = new MeTooChoiceSource({
20+ contentBox: me_too_content,
21+ value: user_is_affected,
22+ title: 'Does this bug affect you?',
23+ items: [
24+ {name: 'Yes, it affects me', value: true,
25+ source_name: 'This bug affects me too',
26+ disabled: false},
27+ {name: "No, it doesn't affect me", value: false,
28+ source_name: "This bug doesn't affect me",
29+ disabled: false}
30+ ],
31+ elementToFlash: me_too_content,
32+ backgroundColor: '#FFFFFF'
33+ });
34+ me_too_edit.render();
35+};
36+
37+/**
38+ * This class is a derivative of ChoiceSource that handles the
39+ * specifics of editing "me too" option.
40+ *
41+ * @class MeTooChoiceSource
42+ * @extends ChoiceSource
43+ * @constructor
44+ */
45+function MeTooChoiceSource() {
46+ MeTooChoiceSource.superclass.constructor.apply(this, arguments);
47+}
48+
49+MeTooChoiceSource.NAME = 'metoocs';
50+MeTooChoiceSource.NS = 'metoocs';
51+
52+MeTooChoiceSource.HTML_PARSER = {
53+ flame_icon: ".dynamic img[src$=/@@/flame-icon]"
54+};
55+
56+MeTooChoiceSource.ATTRS = {
57+ /**
58+ * Y.Node containing a flame icon, displayed when the user is
59+ * affected by the current bug. Should be automatically calculated
60+ * by HTML_PARSER.
61+ *
62+ * Setter function returns Y.get(parameter) so that you can pass
63+ * either a Node (as expected) or a selector.
64+ *
65+ * @attribute value_location
66+ * @type Node
67+ */
68+ flame_icon: {
69+ value: null,
70+ set: function(v) {
71+ return Y.get(v);
72+ }
73+ }
74+};
75+
76+// Put this in the bugs namespace so it can be accessed for testing.
77+bugs._MeTooChoiceSource = MeTooChoiceSource;
78+
79+Y.extend(MeTooChoiceSource, Y.ChoiceSource, {
80+ initializer: function() {
81+ var widget = this;
82+ this.error_handler = new LP.client.ErrorHandler();
83+ this.error_handler.clearProgressUI = function() {
84+ widget._uiClearWaiting();
85+ };
86+ this.error_handler.showError = function(error_msg) {
87+ widget.showError(error_msg);
88+ };
89+ },
90+
91+ showError: function(err) {
92+ display_error(null, err);
93+ },
94+
95+ syncUI: function() {
96+ MeTooChoiceSource.superclass.syncUI.apply(this, arguments);
97+ // Show the flame icon if the user is affected by this bug.
98+ if (this.get('value')) {
99+ this.get('flame_icon').removeClass('unseen');
100+ } else {
101+ this.get('flame_icon').addClass('unseen');
102+ }
103+ },
104+
105+ render: function() {
106+ MeTooChoiceSource.superclass.render.apply(this, arguments);
107+ // Force the ChoiceSource to be rendered inline.
108+ this.get('boundingBox').setStyle('display', 'inline');
109+ // Hide the static content and show the dynamic content.
110+ this.get('contentBox').query('.static').addClass('unseen');
111+ this.get('contentBox').query('.dynamic').removeClass('unseen');
112+ },
113+
114+ _saveData: function() {
115+ // Set the widget to the 'waiting' state.
116+ this._uiSetWaiting();
117+
118+ var value = this.getInput();
119+ var client = new LP.client.Launchpad();
120+ var widget = this;
121+
122+ var config = {
123+ on: {
124+ success: function(entry) {
125+ widget._uiClearWaiting();
126+ MeTooChoiceSource.superclass._saveData.call(
127+ widget, value);
128+ },
129+ failure: this.error_handler.getFailureHandler()
130+ },
131+ parameters: {
132+ affected: value
133+ }
134+ };
135+
136+ client.named_post(
137+ LP.client.cache.bug.self_link, 'markUserAffected', config);
138+ }
139+});
140+
141 /*
142 * Check if the current user can unsubscribe the person
143 * being subscribed.
144@@ -1402,6 +1535,6 @@
145 }
146 }
147 }, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute',
148- 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim',
149+ 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim',
150 'lazr.base', 'lazr.overlay', 'lazr.choiceedit',
151 'lp.picker', 'lp.client.plugins', 'lp.subscriber']});
152
153=== added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html'
154--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 1970-01-01 00:00:00 +0000
155+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.html 2009-08-03 14:38:44 +0000
156@@ -0,0 +1,59 @@
157+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
158+<html>
159+ <head>
160+ <title>Status Editor</title>
161+
162+ <!-- YUI 3.0 Setup -->
163+ <script type="text/javascript" src="../../../icing/yui/3.0.0pr2/build/yui/yui.js"></script>
164+ <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssreset/reset.css"/>
165+ <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssfonts/fonts.css"/>
166+ <link rel="stylesheet" href="../../../icing/yui/3.0.0pr2/build/cssbase/base.css"/>
167+
168+ <!-- Dependency -->
169+ <script type="text/javascript" src="../../../icing/lazr/build/lazr.js"></script>
170+ <script type="text/javascript" src="../../../icing/lazr/build/overlay/overlay.js"></script>
171+ <script type="text/javascript" src="../../../icing/lazr/build/choiceedit/choiceedit.js"></script>
172+ <script type="text/javascript" src="../../../javascript/client/client.js"></script>
173+
174+ <!-- The module under test -->
175+ <script type="text/javascript" src="../bugtask-index.js"></script>
176+
177+ <!-- The test suite -->
178+ <script type="text/javascript" src="test_me_too.js"></script>
179+
180+ <style type="text/css">
181+ /* Taken and customized from testlogger.css */
182+ #log .yui-console-content { width:44em }
183+ #log .yui-console .yui-console-bd { height:30em }
184+ #log .yui-console .yui-console-controls { display:none; }
185+ #log .yui-console .yui-console-hd { display:none; }
186+ #log .yui-console .yui-console-ft { position:absolute;top:0em; }
187+
188+ #log .yui-console-entry-src { display:none; }
189+
190+ #log .yui-console-entry-pass .yui-console-entry-cat {
191+ background-color: green;
192+ font-weight: bold;
193+ color: white;
194+ }
195+ #log .yui-console-entry-fail .yui-console-entry-cat {
196+ background-color: red;
197+ font-weight: bold;
198+ color: white;
199+ }
200+ #log .yui-console-entry-ignore .yui-console-entry-cat {
201+ background-color: #666;
202+ font-weight: bold;
203+ color: white;
204+ }
205+ </style>
206+
207+ <style type="text/css">
208+ /* CSS classes specific to this test */
209+ .unseen { display: none; }
210+ </style>
211+</head>
212+<body class="yui-skin-sam">
213+ <div id="log"></div>
214+</body>
215+</html>
216
217=== added file 'lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js'
218--- lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 1970-01-01 00:00:00 +0000
219+++ lib/canonical/launchpad/javascript/bugs/tests/test_me_too.js 2009-08-04 14:45:29 +0000
220@@ -0,0 +1,267 @@
221+/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
222+
223+YUI({
224+ base: '../../../icing/yui/current/build/',
225+ filter: 'raw',
226+ combine: false
227+ }).use('event', 'bugs.bugtask_index', 'node', 'yuitest', 'widget-stack', 'console',
228+ function(Y) {
229+
230+// Local aliases
231+var Assert = Y.Assert,
232+ ArrayAssert = Y.ArrayAssert;
233+
234+Assert.isEqual = function(a, b, msg) {
235+ Assert.isTrue(a == b, msg);
236+};
237+
238+/*
239+ * A wrapper for the Y.Event.simulate() function. The wrapper accepts
240+ * CSS selectors and Node instances instead of raw nodes.
241+ */
242+function simulate(widget, selector, evtype, options) {
243+ var rawnode = Y.Node.getDOMNode(widget.query(selector));
244+ Y.Event.simulate(rawnode, evtype, options);
245+}
246+
247+/* Helper function to clean up a dynamically added widget instance. */
248+function cleanup_widget(widget) {
249+ // Nuke the boundingBox, but only if we've touched the DOM.
250+ if (widget.get('rendered')) {
251+ var bb = widget.get('boundingBox');
252+ if (bb.get('parentNode')) {
253+ bb.get('parentNode').removeChild(bb);
254+ }
255+ }
256+ // Kill the widget itself.
257+ widget.destroy();
258+}
259+
260+var suite = new Y.Test.Suite("Bugtask Me-Too Choice Edit Tests");
261+
262+suite.add(new Y.Test.Case({
263+
264+ name: 'me_too_choice_edit_basics',
265+
266+ setUp: function() {
267+ // Monkeypatch LP.client to avoid network traffic and to make
268+ // some things work as expected.
269+ LP.client.Launchpad.prototype.named_post = (
270+ function(url, func, config) {
271+ config.on.success();
272+ }
273+ );
274+ LP.client.cache.bug = {
275+ self_link: "http://bugs.example.com/bugs/1234"
276+ };
277+ // add the in-page HTML
278+ var inpage = Y.Node.create([
279+ '<span id="affectsmetoo">',
280+ ' <span class="static">',
281+ ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt="" />',
282+ ' This bug affects me too',
283+ ' <a href="+affectsmetoo">',
284+ ' <img class="editicon" alt="Edit"',
285+ ' src="https://bugs.edge.launchpad.net/@@/edit" />',
286+ ' </a>',
287+ ' </span>',
288+ ' <span class="dynamic unseen">',
289+ ' <img class="editicon" alt="Edit"',
290+ ' src="https://bugs.edge.launchpad.net/@@/edit" />',
291+ ' <a href="+affectsmetoo" class="js-action"',
292+ ' ><span class="value">Does this bug affect you?</span></a>',
293+ ' <img src="https://bugs.edge.launchpad.net/@@/flame-icon" alt=""/>',
294+ ' </span>',
295+ '</span>'].join(''));
296+ Y.get("body").appendChild(inpage);
297+ var me_too_content = Y.get('#affectsmetoo');
298+ this.config = {
299+ contentBox: me_too_content,
300+ value: null,
301+ title: 'This bug:',
302+ items: [
303+ {name: 'Affects me too', value: true,
304+ source_name: 'This bug affects me too',
305+ disabled: false},
306+ {name: 'Does not affect me', value: false,
307+ source_name: "This bug doesn't affect me",
308+ disabled: false}
309+ ],
310+ elementToFlash: me_too_content,
311+ backgroundColor: '#FFFFFF'
312+ };
313+ this.choice_edit = new Y.bugs._MeTooChoiceSource(this.config);
314+ this.choice_edit.render();
315+ },
316+
317+ tearDown: function() {
318+ if (this.choice_edit._choice_list) {
319+ cleanup_widget(this.choice_edit._choice_list);
320+ }
321+ var status = Y.get("document").query("#affectsmetoo");
322+ if (status) {
323+ status.get("parentNode").removeChild(status);
324+ }
325+ },
326+
327+ /**
328+ * The choice edit should be displayed inline.
329+ */
330+ test_is_inline: function() {
331+ var display = this.choice_edit.get('boundingBox').getStyle('display');
332+ Assert.isEqual(
333+ display, 'inline', "Not displayed inline, display is: " + display);
334+ },
335+
336+ /**
337+ * The .static area should be hidden by adding the "unseen" class.
338+ */
339+ test_hide_static: function() {
340+ var static_area = this.choice_edit.get('contentBox').query('.static');
341+ Assert.isTrue(
342+ static_area.hasClass('unseen'), "Static area is not hidden.");
343+ },
344+
345+ /**
346+ * The .dynamic area should be shown by removing the "unseen" class.
347+ */
348+ test_hide_dynamic: function() {
349+ var dynamic_area = this.choice_edit.get('contentBox').query('.dynamic');
350+ Assert.isFalse(
351+ dynamic_area.hasClass('unseen'), "Dynamic area is hidden.");
352+ },
353+
354+ /**
355+ * The flame icon should be hidden initially.
356+ */
357+ test_flame_hidden_initially: function() {
358+ var flame_icon = this.choice_edit.get('flame_icon');
359+ Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden.");
360+ },
361+
362+ /**
363+ * The flame icon should be hidden when the user has made a
364+ * negative choice (i.e. "Does not affect me").
365+ */
366+ test_flame_hidden_with_negative_choice: function() {
367+ simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
368+ simulate(this.choice_edit._choice_list.get('boundingBox'),
369+ 'li a[href$=false]', 'click');
370+ var flame_icon = this.choice_edit.get('flame_icon');
371+ Assert.isTrue(flame_icon.hasClass('unseen'), "Flame is not hidden.");
372+ },
373+
374+ /**
375+ * The flame icon should be shown when the user has made a
376+ * positive choice (i.e. "Affects me too").
377+ */
378+ test_flame_hidden_with_positive_choice: function() {
379+ simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
380+ simulate(this.choice_edit._choice_list.get('boundingBox'),
381+ 'li a[href$=true]', 'click');
382+ var flame_icon = this.choice_edit.get('flame_icon');
383+ Assert.isFalse(flame_icon.hasClass('unseen'), "Flame is hidden.");
384+ },
385+
386+ /**
387+ * The UI should be in a waiting state while the save process is
388+ * executing and return to a non-waiting state once it has
389+ * finished.
390+ */
391+ test_ui_waiting_for_success: function() {
392+ this.do_test_ui_waiting('success');
393+ },
394+
395+ /**
396+ * The UI should be in a waiting state while the save process is
397+ * executing and return to a non-waiting state even if the process
398+ * fails.
399+ */
400+ test_ui_waiting_for_failure: function() {
401+ this.do_test_ui_waiting('failure');
402+ },
403+
404+ /**
405+ * Helper function that does the leg work for the
406+ * test_ui_waiting_* methods.
407+ */
408+ do_test_ui_waiting: function(callback) {
409+ var edit_icon = this.choice_edit.get('editicon');
410+ // The spinner should not be displayed at first.
411+ Assert.isNull(
412+ edit_icon.get('src').match(/\/spinner$/),
413+ "The edit icon is displaying a spinner at rest.");
414+ // The spinner should not be displayed after opening the
415+ // choice list.
416+ simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
417+ Assert.isNull(
418+ edit_icon.get('src').match(/\/spinner$/),
419+ "The edit icon is displaying a spinner after opening the choice list.");
420+ // The spinner should be visible during the interval between a
421+ // choice being made and a response coming back from Launchpad
422+ // that the choice has been saved.
423+ var edit_icon_src_during_save;
424+ // Patch the named_post method to simulate success or failure,
425+ // as determined by the callback argument. We cannot make
426+ // assertions in this method because exceptions are swallowed
427+ // somewhere. Instead, we save something testable to a local
428+ // var.
429+ LP.client.Launchpad.prototype.named_post = (
430+ function(url, func, config) {
431+ edit_icon_src_during_save = edit_icon.get('src');
432+ config.on[callback]();
433+ }
434+ );
435+ simulate(this.choice_edit._choice_list.get('boundingBox'),
436+ 'li a[href$=true]', 'click');
437+ Assert.isNotNull(
438+ edit_icon_src_during_save.match(/\/spinner$/),
439+ "The edit icon is not displaying a spinner during save.");
440+ // The spinner should not be displayed once a choice has been
441+ // saved.
442+ Assert.isNull(
443+ edit_icon.get('src').match(/\/spinner$/),
444+ "The edit icon is displaying a spinner once the choice has been made.");
445+ },
446+
447+ /**
448+ * Test that the save call back to Launchpad makes sense.
449+ */
450+ test_save_data: function() {
451+ var url, func, config;
452+ LP.client.Launchpad.prototype.named_post = (
453+ function(f_url, f_func, f_config) {
454+ url = f_url, func = f_func, config = f_config;
455+ config.on.success();
456+ }
457+ );
458+ // Simulate selecting something.
459+ simulate(this.choice_edit.get('boundingBox'), '.value', 'mousedown');
460+ simulate(this.choice_edit._choice_list.get('boundingBox'),
461+ 'li a[href$=true]', 'click');
462+ // See what we've got.
463+ Assert.isEqual(url, "http://bugs.example.com/bugs/1234",
464+ "LP client call to wrong URL.");
465+ Assert.isEqual(func, "markUserAffected",
466+ "LP client call to wrong method.");
467+ Assert.isNotNull(config.on);
468+ Assert.isNotNull(config.on.success);
469+ Assert.isNotNull(config.on.failure);
470+ Assert.isNotNull(config.parameters);
471+ Assert.isTrue(config.parameters.affected);
472+ }
473+
474+}));
475+
476+Y.Test.Runner.add(suite);
477+
478+var yconsole = new Y.Console({
479+ newestOnTop: false
480+});
481+yconsole.render('#log');
482+
483+Y.on('domready', function() {
484+ Y.Test.Runner.run();
485+});
486+
487+});
488
489=== modified file 'lib/lp/bugs/browser/bugtask.py'
490--- lib/lp/bugs/browser/bugtask.py 2009-07-17 18:46:25 +0000
491+++ lib/lp/bugs/browser/bugtask.py 2009-07-23 10:34:08 +0000
492@@ -2915,13 +2915,15 @@
493 return self.context.isUserAffected(self.user)
494
495 @property
496- def affects_form_value(self):
497- """The value to use in the inline me too form."""
498- affected = self.context.isUserAffected(self.user)
499- if affected is None or affected == False:
500- return 'YES'
501+ def current_user_affected_js_status(self):
502+ """A javascript literal indicating if the user is affected."""
503+ affected = self.current_user_affected_status
504+ if affected is None:
505+ return 'null'
506+ elif affected:
507+ return 'true'
508 else:
509- return 'NO'
510+ return 'false'
511
512
513 class BugTaskTableRowView(LaunchpadView):
514
515=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
516--- lib/lp/bugs/browser/tests/test_bugtask.py 2009-06-25 00:40:31 +0000
517+++ lib/lp/bugs/browser/tests/test_bugtask.py 2009-07-23 10:34:08 +0000
518@@ -1,23 +1,63 @@
519 # Copyright 2009 Canonical Ltd. This software is licensed under the
520 # GNU Affero General Public License version 3 (see the file LICENSE).
521
522+__metaclass__ = type
523+
524+
525 import unittest
526
527 from zope.testing.doctest import DocTestSuite
528
529-from lp.bugs.browser import bugtask
530+from canonical.launchpad.ftests import login
531 from canonical.launchpad.testing.systemdocs import (
532 LayeredDocFileSuite, setUp, tearDown)
533 from canonical.testing import LaunchpadFunctionalLayer
534
535+from lp.bugs.browser import bugtask
536+from lp.bugs.browser.bugtask import BugTasksAndNominationsView
537+from lp.testing import TestCaseWithFactory
538+
539+
540+class TestBugTasksAndNominationsView(TestCaseWithFactory):
541+
542+ layer = LaunchpadFunctionalLayer
543+
544+ def setUp(self):
545+ super(TestBugTasksAndNominationsView, self).setUp()
546+ login('foo.bar@canonical.com')
547+ self.bug = self.factory.makeBug()
548+ self.view = BugTasksAndNominationsView(self.bug, None)
549+
550+ def test_current_user_affected_status(self):
551+ self.failUnlessEqual(
552+ None, self.view.current_user_affected_status)
553+ self.view.context.markUserAffected(self.view.user, True)
554+ self.failUnlessEqual(
555+ True, self.view.current_user_affected_status)
556+ self.view.context.markUserAffected(self.view.user, False)
557+ self.failUnlessEqual(
558+ False, self.view.current_user_affected_status)
559+
560+ def test_current_user_affected_js_status(self):
561+ self.failUnlessEqual(
562+ 'null', self.view.current_user_affected_js_status)
563+ self.view.context.markUserAffected(self.view.user, True)
564+ self.failUnlessEqual(
565+ 'true', self.view.current_user_affected_js_status)
566+ self.view.context.markUserAffected(self.view.user, False)
567+ self.failUnlessEqual(
568+ 'false', self.view.current_user_affected_js_status)
569+
570
571 def test_suite():
572 suite = unittest.TestSuite()
573+ suite.addTest(unittest.makeSuite(TestBugTasksAndNominationsView))
574 suite.addTest(DocTestSuite(bugtask))
575 suite.addTest(LayeredDocFileSuite(
576 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown,
577 layer=LaunchpadFunctionalLayer))
578 return suite
579
580+
581 if __name__ == '__main__':
582 unittest.TextTestRunner().run(test_suite())
583
584=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt'
585--- lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-06-12 16:36:02 +0000
586+++ lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt 2009-07-31 13:49:53 +0000
587@@ -1,7 +1,7 @@
588 = Marking a bug as affecting the user =
589
590-Users can mark bugs as affecting them. Let's create a sample bug to try
591-this on.
592+Users can mark bugs as affecting them. Let's create a sample bug to
593+try this out.
594
595 >>> login(ANONYMOUS)
596 >>> from canonical.launchpad.webapp import canonical_url
597@@ -9,15 +9,27 @@
598 >>> test_bug_url = canonical_url(test_bug)
599 >>> logout()
600
601-The user goes to the bug's index page, and clicks the edit action link
602-near 'This bug affects me too'.
603+The user goes to the bug's index page, and finds a statement that the
604+bug is not marked as affecting them.
605
606 >>> user_browser.open(test_bug_url)
607 >>> print extract_text(find_tag_by_id(
608- ... user_browser.contents, 'affectsmetooform'))
609- This bug doesn't affect me...
610-
611- >>> user_browser.getLink('change').click()
612+ ... user_browser.contents, 'affectsmetoo').find(
613+ ... None, 'static'))
614+ This bug doesn't affect me
615+
616+Next to the statement is a link containing an edit icon.
617+
618+ >>> edit_link = find_tag_by_id(
619+ ... user_browser.contents, 'affectsmetoo').a
620+ >>> print edit_link['href']
621+ +affectsmetoo
622+ >>> print edit_link.img['src']
623+ /@@/edit
624+
625+The user is affected by this bug, so clicks the link.
626+
627+ >>> user_browser.getLink(url='+affectsmetoo').click()
628 >>> print user_browser.url
629 http://bugs.launchpad.dev/.../+bug/.../+affectsmetoo
630 >>> user_browser.getControl(name='field.affects').value
631@@ -28,22 +40,26 @@
632 >>> user_browser.getControl('Change').click()
633
634 The bug page loads again, and now the text is changed, to make it
635-clear to the user that they can change the selection.
636+clear to the user that they have marked this bug as affecting them.
637
638- >>> print extract_text(find_tags_by_class(
639- ... user_browser.contents, 'menu-link-affectsmetoo')[0])
640- change
641+ >>> print extract_text(find_tag_by_id(
642+ ... user_browser.contents, 'affectsmetoo').find(
643+ ... None, 'static'))
644+ This bug affects me too
645
646 Next to it, we also see the 'hot bug' icon, to indicate that the user
647 has marked the bug as affecting them.
648
649 >>> print find_tag_by_id(
650- ... user_browser.contents, 'affectsmetooform').img['src']
651+ ... user_browser.contents, 'affectsmetoo').img['src']
652 /@@/flame-icon
653
654- >>> user_browser.getLink('change').click()
655-
656-The user is changing his selection to 'No' and submits the form.
657+On second thoughts, the user realises that this bug does not affect
658+them, so they click on the edit link once more.
659+
660+ >>> user_browser.getLink(url='+affectsmetoo').click()
661+
662+The user changes his selection to 'No' and submits the form.
663
664 >>> user_browser.getControl(name='field.affects').value = ['NO']
665 >>> user_browser.getControl('Change').click()
666@@ -51,18 +67,43 @@
667 Back at the bug page, the text changes once again.
668
669 >>> print extract_text(find_tag_by_id(
670- ... user_browser.contents, 'affectsmetooform'))
671- This bug doesn't affect me...
672-
673-== One-click interaction ==
674-
675-If the user's browser provides javascript, they don't need to go to
676-another page to change their selection. Instead, an in-page form is
677-submitted when they click the action link.
678-
679- >>> me_too_form = user_browser.getForm(id='affectsmetooform')
680- >>> user_browser.getControl(name='field.affects').value
681- 'YES'
682- >>> me_too_form.submit()
683- >>> user_browser.getControl(name='field.affects').value
684- 'NO'
685+ ... user_browser.contents, 'affectsmetoo').find(
686+ ... None, 'static'))
687+ This bug doesn't affect me
688+
689+
690+== Static and dynamic support ==
691+
692+A bug page contains markup to support both static (no Javascript) and
693+dynamic (Javascript enabled) scenarios.
694+
695+ >>> def class_filter(css_class):
696+ ... def test(node):
697+ ... return css_class in node.get('class', '').split()
698+ ... return test
699+
700+ >>> static_content = find_tag_by_id(
701+ ... user_browser.contents, 'affectsmetoo').find(
702+ ... class_filter('static'))
703+
704+ >>> static_content is not None
705+ True
706+
707+ >>> dynamic_content = find_tag_by_id(
708+ ... user_browser.contents, 'affectsmetoo').find(
709+ ... class_filter('dynamic'))
710+
711+ >>> dynamic_content is not None
712+ True
713+
714+The dynamic content is hidden by the presence of the "unseen" CSS
715+class.
716+
717+ >>> print static_content.get('class')
718+ static
719+
720+ >>> print dynamic_content.get('class')
721+ dynamic unseen
722+
723+It is the responsibilty of Javascript running in the page to unhide
724+the dynamic content and hide the static content.
725
726=== modified file 'lib/lp/bugs/templates/bugtasks-and-nominations-table.pt'
727--- lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-07-17 17:59:07 +0000
728+++ lib/lp/bugs/templates/bugtasks-and-nominations-table.pt 2009-08-04 14:51:23 +0000
729@@ -38,11 +38,9 @@
730
731 </table>
732
733-<div
734- class="actions"
735- tal:define="current_bugtask view/current_bugtask"
736-
737- tal:condition="view/displayAlsoAffectsLinks">
738+<div class="actions"
739+ tal:define="current_bugtask view/current_bugtask"
740+ tal:condition="view/displayAlsoAffectsLinks">
741 <tal:also-affects-links define="context_menu context/menu:context">
742 <tal:addupstream
743 define="link context_menu/addupstream"
744@@ -56,56 +54,50 @@
745 define="link context_menu/nominate"
746 condition="link/enabled"
747 replace="structure link/render" />
748- <form
749- id="affectsmetooform"
750- name="affectsmetooform"
751- method="post"
752- enctype="multipart/form-data"
753- accept-charset="UTF-8"
754- tal:define="link context_menu/affectsmetoo"
755- tal:condition="link/enabled"
756- tal:attributes="action link/url"
757- style="display: inline">
758- <input
759- name="field.affects"
760- type="hidden"
761- tal:attributes="value view/affects_form_value" />
762- <input
763- type="hidden"
764- name="field.actions.change"
765- value="" />
766- <tal:affected condition="view/current_user_affected_status">
767- <img width="14" height="14" src="/@@/flame-icon" alt="" />
768- This bug affects me too
769- </tal:affected>
770- <tal:affected condition="not: view/current_user_affected_status">
771- This bug doesn't affect me
772- </tal:affected>
773- (<tal:affectsmetoo
774- define="link context_menu/affectsmetoo"
775- condition="link/enabled"
776- replace="structure link/render" />)
777- <tal:nothing condition="nothing">
778- The following is a trick to allow users to mark
779- themselves as affected by a bug with only one click.
780- If Javascript is available, we submit the form and
781- immediately go back to the same page, saving them the
782- need to go to a new page.
783- </tal:nothing>
784- <script type="text/javascript">
785- function sendMeTooForm(e) {
786- $('affectsmetooform').submit();
787- e.preventDefault();
788- }
789- function connectMeTooLink() {
790- var me_too_link = getFirstElementByTagAndClassName(
791- 'a', 'menu-link-affectsmetoo');
792- connect(me_too_link, 'onclick', sendMeTooForm);
793- }
794- registerLaunchpadFunction(connectMeTooLink);
795+ <span id="affectsmetoo" style="display: inline"
796+ tal:condition="link/enabled"
797+ tal:define="link context_menu/affectsmetoo;
798+ affected view/current_user_affected_status">
799+
800+ <tal:comment condition="nothing">
801+ This .static section is shown in browsers with javascript
802+ enabled, and before setup_me_too is run.
803+ </tal:comment>
804+ <span class="static">
805+ <tal:affected condition="affected">
806+ <img width="14" height="14" src="/@@/flame-icon" alt="" />
807+ This bug affects me too
808+ </tal:affected>
809+ <tal:not-affected condition="not:affected">
810+ This bug doesn't affect me
811+ </tal:not-affected>
812+ <a href="+affectsmetoo">
813+ <img class="editicon" src="/@@/edit" alt="Edit" />
814+ </a>
815+ </span>
816+
817+ <tal:comment condition="nothing">
818+ This .dynamic section is used by setup_me_too to display
819+ controls and information in the correct places.
820+ </tal:comment>
821+ <span class="dynamic unseen">
822+ <img src="/@@/flame-icon" alt=""/>
823+ <a href="+affectsmetoo" class="js-action"
824+ ><span class="value">Does this bug affect you?</span></a>
825+ <img class="editicon" src="/@@/edit" alt="Edit" />
826+ </span>
827+
828+ <script type="text/javascript" tal:content="string:
829+ YUI().use('event', 'bugs.bugtask_index', function(Y) {
830+ Y.on('load', function(e) {
831+ Y.bugs.setup_me_too(${view/current_user_affected_js_status});
832+ }, window);
833+ });
834+ ">
835 </script>
836- </form>
837+
838+ </span>
839 </tal:also-affects-links>
840-
841 </div>
842+
843 </tal:root>
844
845=== added file 'lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py'
846--- lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 1970-01-01 00:00:00 +0000
847+++ lib/lp/bugs/windmill/tests/test_bugs/test_bug_me_too.py 2009-07-31 15:27:05 +0000
848@@ -0,0 +1,65 @@
849+# Copyright 2009 Canonical Ltd. This software is licensed under the
850+# GNU Affero General Public License version 3 (see the file LICENSE).
851+
852+from canonical.launchpad.windmill.testing import lpuser
853+
854+from windmill.authoring import WindmillTestClient
855+
856+
857+def test_me_too():
858+ """Test the "this bug affects me too" options on bug pages.
859+
860+ This test ensures that, with Javascript enabled, the "me too"
861+ status can be edited in-page.
862+ """
863+ client = WindmillTestClient('Bug "me too" test')
864+ lpuser.SAMPLE_PERSON.ensure_login(client)
865+
866+ # Open bug 11 and wait for it to finish loading.
867+ client.open(url=u'http://bugs.launchpad.dev:8085/jokosher/+bug/11/+index')
868+ client.waits.forPageLoad(timeout=u'20000')
869+
870+ # Wait for setup_me_too to sort out the "me too" elements.
871+ client.waits.forElement(
872+ xpath=(u"//span[@id='affectsmetoo' and "
873+ u"@class='yui-metoocs-content']"))
874+
875+ # Currently this bug does not affect the logged-in user.
876+ client.asserts.assertText(
877+ xpath=u"//span[@id='affectsmetoo']/span[@class='value']",
878+ validator=u"This bug doesn't affect me")
879+
880+ # There is an edit icon next to the text which can be clicked to
881+ # edit the "me too" status. However, we won't click it with
882+ # Windmill because the widget actually responds to mouse-down, and
883+ # Windmill seems to do something funny instead.
884+ client.mouseDown(
885+ xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']")
886+ client.mouseUp(
887+ xpath=u"//span[@id='affectsmetoo']//img[@class='editicon']")
888+
889+ # Wait for the modal dialog to appear.
890+ client.waits.forElement(id=u'yui-pretty-overlay-modal')
891+
892+ # There's a close button if we change our mind.
893+ client.click(
894+ xpath=(u"//div[@id='yui-pretty-overlay-modal']//"
895+ u"a[@class='close-button']"))
896+
897+ # Wait for the modal dialog to disappear. Unfortunately the test
898+ # below doesn't work, nor does testing clientWidth, or anything I
899+ # could think of, so it's commented out for now because chasing
900+ # this is not a good use of time.
901+
902+ # client.asserts.assertElemJS(
903+ # id=u'yui-pretty-overlay-modal',
904+ # js=(u'getComputedStyle(element, '
905+ # u'"visibility").visibility == "hidden"'))
906+
907+ # However, we want to mark this bug as affecting the logged-in
908+ # user. We can also click on the content box of the "me too"
909+ # widget; we are not forced to use the edit icon.
910+ client.click(xpath=u"//span[@id='affectsmetoo']")
911+ client.waits.forElement(id=u'yui-pretty-overlay-modal')
912+
913+ # XXX: Gavin Panella bug=361097 2009-07-31: Finish this.