Merge lp:~gary/launchpad/bug724609 into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 14351
Proposed branch: lp:~gary/launchpad/bug724609
Merge into: lp:launchpad
Diff against target: 709 lines (+539/-19)
8 files modified
lib/lp/app/javascript/client.js (+18/-9)
lib/lp/app/javascript/server_fixture.js (+67/-1)
lib/lp/app/javascript/tests/test_lp_client.js (+48/-0)
lib/lp/app/javascript/tests/test_lp_client_integration.js (+333/-0)
lib/lp/app/javascript/tests/test_lp_client_integration.py (+62/-0)
lib/lp/app/templates/base-layout-macros.pt (+6/-2)
lib/lp/registry/browser/tests/test_subscription_links.py (+2/-6)
lib/lp/testing/__init__.py (+3/-1)
To merge this branch: bzr merge lp:~gary/launchpad/bug724609
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+82689@code.launchpad.net

Commit message

[r=bac][bug=724609] re-add integration tests for the lp.client library, replacing the old Windmill tests with yuixhr tests.

Description of the change

This branch addresses critical bug 724609 by re-adding integration tests for the lp.client library. These had been written for Windmill, and were removed earlier this year because we discarded Windmill.

If you would like to look at the original, you can find them here. http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/view/13343/lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js

The match is usually roughly one-to-one, though I did take some editorial liberties, and a few of the tests were moved to pure JS unit tests when I felt I could do that.

The original tests had tests for the hosted file support. Much of them were commented out because we weren't actually using enough of it in Launchpad to test the integration. I tried to port the existing tests, but I discovered that Chrome makes our hosted files even more problematic. We redirect our hosted files to another domain for the librarian, and Chrome disallows getting files from another domain for security reasons. We could fix this by having librarian files in the same domain, but when I brought this up to him, Francis told me to just remove the support in lp.client for hosted files, because we have not used it for years. Therefore, in a second and separate branch, I will do this.

I made a few changes to help with testing.

- The lp.client itself gained a new argument so that you can instantiate it so that requests are performed synchronously. This makes it easier to write tests, because you don't have to try and figure out how long to wait for the results using the YUI async test API.

- The yuixhr module gained two abilities.

  * First, you can add arbitrary cleanup functions to be called on teardown.

  * The cleanup function ability is then used by the second ability: you can test a page in an iframe. This is good for integration tests that just need to verify that library code is actually hooked up on a page. You should of course not test the library itself with the iframe.

Lint is happy.

Thank you!

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Gary,

Thanks for this branch and helping to kill of windmill.

* As discussed on IRC, I think waitForIFrame is a bit misleading for a name but I've not got a reasonable suggestion for fixing it.

* When calling waitForIFrame it would be helpful to comment the arguments being passed. I needed to go back and forth to understand what was being passed. Trivial point.

* fix: """{Describe your test suite here}."""

Very nice branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/app/javascript/__init__.py'
2=== modified file 'lib/lp/app/javascript/client.js'
3--- lib/lp/app/javascript/client.js 2011-10-20 20:14:57 +0000
4+++ lib/lp/app/javascript/client.js 2011-11-21 14:41:36 +0000
5@@ -473,7 +473,9 @@
6 'headers': {"Content-Type": hosted_file.content_type,
7 "Content-Disposition": disposition},
8 'arguments': args,
9- 'data': hosted_file.contents};
10+ 'data': hosted_file.contents,
11+ 'sync': this.lp_client.sync
12+ };
13 this.io_provider.io(module.normalize_uri(hosted_file.uri), y_config);
14 },
15
16@@ -483,7 +485,9 @@
17 var args = hosted_file;
18 var y_config = { method: "DELETE",
19 on: on,
20- 'arguments': args };
21+ 'arguments': args,
22+ sync: this.lp_client.sync
23+ };
24 this.io_provider.io(hosted_file.uri, y_config);
25 }
26 };
27@@ -559,11 +563,13 @@
28
29
30 // The service root resource.
31-module.Root = function(client, representation, uri) {
32+Root = function(client, representation, uri) {
33 /* The root of the Launchpad web service. */
34 this.init(client, representation, uri);
35 };
36-module.Root.prototype = new Resource();
37+Root.prototype = new Resource();
38+
39+module.Root = Root;
40
41
42 var Collection = function(client, representation, uri) {
43@@ -622,7 +628,6 @@
44
45 Entry.prototype.lp_save = function(config) {
46 /* Write modifications to this entry back to the web service. */
47- var on = config.on;
48 var representation = {};
49 var entry = this;
50 Y.each(this.dirty_attributes, function(attribute, key) {
51@@ -667,6 +672,7 @@
52 var Launchpad = function(config) {
53 /* A client that makes HTTP requests to Launchpad's web service. */
54 this.io_provider = module.get_configured_io_provider(config);
55+ this.sync = (config ? config.sync : false);
56 };
57
58 Launchpad.prototype = {
59@@ -693,7 +699,8 @@
60 on: on,
61 'arguments': [client, uri, old_on_success, update_cache],
62 'headers': headers,
63- data: data
64+ data: data,
65+ sync: this.sync
66 };
67 return this.io_provider.io(uri, y_config);
68 },
69@@ -745,13 +752,14 @@
70 method: "POST",
71 on: on,
72 'arguments': [client, uri, old_on_success, update_cache],
73- data: data
74+ data: data,
75+ sync: this.sync
76 };
77 this.io_provider.io(uri, y_config);
78 },
79
80 'patch': function(uri, representation, config, headers) {
81- var on = config.on;
82+ var on = Y.merge(config.on);
83 var data = Y.JSON.stringify(representation);
84 uri = module.normalize_uri(uri);
85
86@@ -780,7 +788,8 @@
87 'on': on,
88 'headers': extra_headers,
89 'arguments': args,
90- 'data': data
91+ 'data': data,
92+ 'sync': this.sync
93 };
94 this.io_provider.io(uri, y_config);
95 },
96
97=== modified file 'lib/lp/app/javascript/server_fixture.js'
98--- lib/lp/app/javascript/server_fixture.js 2011-11-03 23:19:44 +0000
99+++ lib/lp/app/javascript/server_fixture.js 2011-11-21 14:41:36 +0000
100@@ -42,7 +42,73 @@
101 return data;
102 };
103
104+module.addCleanup = function(testcase, callable) {
105+ if (Y.Lang.isUndefined(testcase._lp_fixture_cleanups)) {
106+ testcase._lp_fixture_cleanups = [];
107+ }
108+ testcase._lp_fixture_cleanups.push(callable);
109+};
110+
111+module.runWithIFrame = function(config, test) {
112+ // Note that the iframe url must be in the same domain as the test page.
113+ var iframe = Y.Node.create('<iframe/>').set('src', config.uri);
114+ Y.one('body').append(iframe);
115+ module.addCleanup(
116+ config.testcase,
117+ function() {
118+ iframe.remove();
119+ }
120+ );
121+ var timeout = config.timeout;
122+ if (!Y.Lang.isValue(timeout)) {
123+ timeout = 8000;
124+ }
125+ var iframe_is_ready = config.iframe_is_ready;
126+ if (!Y.Lang.isValue(iframe_is_ready)) {
127+ iframe_is_ready = function() {return true;};
128+ }
129+ var wait = config.wait;
130+ if (!Y.Lang.isValue(wait)) {
131+ wait = 100;
132+ }
133+ var start;
134+ var retry_function;
135+ retry_function = function() {
136+ var tested = false;
137+ var win = Y.Node.getDOMNode(iframe.get('contentWindow'));
138+ if (Y.Lang.isValue(win) && Y.Lang.isValue(win.document)) {
139+ var IYUI = YUI({
140+ base: '/+icing/yui/',
141+ filter: 'raw',
142+ combine: false,
143+ fetchCSS: false,
144+ win: win});
145+ if (iframe_is_ready(IYUI)) {
146+ test(IYUI);
147+ tested = true;
148+ }
149+ }
150+ if (!tested) {
151+ var now = new Date().getTime();
152+ if (now-start < timeout) {
153+ config.testcase.wait(retry_function, wait);
154+ } else {
155+ Y.Assert.fail('Page did not load in iframe: ' + url);
156+ }
157+ }
158+ };
159+ start = new Date().getTime();
160+ config.testcase.wait(retry_function, wait);
161+};
162+
163 module.teardown = function(testcase) {
164+ var cleanups = testcase._lp_fixture_cleanups;
165+ var i;
166+ if (!Y.Lang.isUndefined(cleanups)) {
167+ for (i=cleanups.length-1; i>=0 ; i--) {
168+ cleanups[i]();
169+ }
170+ }
171 var fixtures = testcase._lp_fixture_setups;
172 if (Y.Lang.isUndefined(fixtures)) {
173 // Nothing to be done.
174@@ -101,4 +167,4 @@
175 },
176 "0.1",
177 {"requires": [
178- "io", "json", "querystring", "test", "console", "lp.client"]});
179+ "io", "json", "querystring", "test", "console", "lp.client", "node"]});
180
181=== added file 'lib/lp/app/javascript/tests/__init__.py'
182=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
183--- lib/lp/app/javascript/tests/test_lp_client.js 2011-10-11 17:05:30 +0000
184+++ lib/lp/app/javascript/tests/test_lp_client.js 2011-11-21 14:41:36 +0000
185@@ -140,6 +140,54 @@
186 Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
187 Assert.areSame('foo', result.entry.get('resource_type_link'));
188 },
189+ test_get_success_callback: function() {
190+ var mockio = new Y.lp.testing.mockio.MockIo();
191+ var mylist = [];
192+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
193+ client.get('/people', {on:{success: Y.bind(mylist.push, mylist)}});
194+ Assert.areEqual('/api/devel/people', mockio.last_request.url);
195+ mockio.success({
196+ responseText:
197+ '{"entry": {"resource_type_link": "foo"}}',
198+ responseHeaders: {'Content-Type': 'application/json'}
199+ });
200+ var result = mylist[0];
201+ Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
202+ Assert.areSame('foo', result.entry.get('resource_type_link'));
203+ },
204+ test_get_failure_callback: function() {
205+ var mockio = new Y.lp.testing.mockio.MockIo();
206+ var mylist = [];
207+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
208+ client.get(
209+ '/people',
210+ {on: {
211+ failure: function(){
212+ mylist.push(Array.prototype.slice.call(arguments));
213+ }}});
214+ mockio.failure({status: 503});
215+ var result = mylist[0];
216+ Assert.areSame(503, result[1].status);
217+ Assert.areSame('/api/devel/people', result[2][1]);
218+ },
219+ test_named_post_success_callback: function() {
220+ var mockio = new Y.lp.testing.mockio.MockIo();
221+ var mylist = [];
222+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
223+ client.named_post(
224+ '/people', 'newTeam', {on:{success: Y.bind(mylist.push, mylist)}});
225+ Assert.areEqual('/api/devel/people', mockio.last_request.url);
226+ Assert.areEqual('ws.op=newTeam', mockio.last_request.config.data);
227+ Assert.areEqual('POST', mockio.last_request.config.method);
228+ mockio.success({
229+ responseText:
230+ '{"entry": {"resource_type_link": "foo"}}',
231+ responseHeaders: {'Content-Type': 'application/json'}
232+ });
233+ var result = mylist[0];
234+ Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
235+ Assert.areSame('foo', result.entry.get('resource_type_link'));
236+ },
237 test_wrap_resource_nested_mapping: function() {
238 // wrap_resource produces mappings of plain object literals. These can
239 // be nested and have Entries in them.
240
241=== added file 'lib/lp/app/javascript/tests/test_lp_client_integration.js'
242--- lib/lp/app/javascript/tests/test_lp_client_integration.js 1970-01-01 00:00:00 +0000
243+++ lib/lp/app/javascript/tests/test_lp_client_integration.js 2011-11-21 14:41:36 +0000
244@@ -0,0 +1,333 @@
245+YUI({
246+ base: '/+icing/yui/',
247+ filter: 'raw', combine: false, fetchCSS: false
248+}).use('test',
249+ 'escape',
250+ 'node',
251+ 'console',
252+ 'json',
253+ 'cookie',
254+ 'lp.testing.serverfixture',
255+ 'lp.client',
256+ function(Y) {
257+
258+
259+var suite = new Y.Test.Suite(
260+ "Integration tests for lp.client and basic JS infrastructure");
261+var serverfixture = Y.lp.testing.serverfixture;
262+
263+var makeTestConfig = function(config) {
264+ if (Y.Lang.isUndefined(config)) {
265+ config = {};
266+ }
267+ config.on = Y.merge(
268+ {
269+ success: function(result) {
270+ config.successful = true;
271+ config.result = result;
272+ },
273+ failure: function(tid, response, args) {
274+ config.successful = false;
275+ config.result = {tid: tid, response: response, args: args};
276+ }
277+ },
278+ config.on);
279+ return config;
280+};
281+
282+/**
283+ * Test cache data in page load.
284+ */
285+suite.add(new Y.Test.Case({
286+ name: 'Cache data',
287+
288+ tearDown: function() {
289+ serverfixture.teardown(this);
290+ },
291+
292+ test_anonymous_user_has_no_cache_data: function() {
293+ var data = serverfixture.setup(this, 'create_product');
294+ serverfixture.runWithIFrame(
295+ {testcase: this,
296+ uri: data.product.web_link,
297+ iframe_is_ready: function(I) {
298+ I.use('node');
299+ return I.Lang.isValue(I.one('#json-cache-script'));
300+ }
301+ },
302+ function(I) {
303+ var iframe_window = I.config.win;
304+ var LP = iframe_window.LP;
305+ Y.Assert.isUndefined(LP.links.me);
306+ Y.Assert.isNotUndefined(LP.cache.context);
307+ }
308+ );
309+ },
310+
311+ test_logged_in_user_has_cache_data: function() {
312+ var data = serverfixture.setup(this, 'create_product_and_login');
313+ serverfixture.runWithIFrame(
314+ {testcase: this,
315+ uri: data.product.web_link,
316+ iframe_is_ready: function(I) {
317+ I.use('node');
318+ return I.Lang.isValue(I.one('#json-cache-script'));
319+ }
320+ },
321+ function(I) {
322+ var iframe_window = I.config.win;
323+ var LP = iframe_window.LP;
324+ Y.Assert.areSame(
325+ '/~' + data.user.name,
326+ LP.links.me
327+ );
328+ Y.Assert.isNotUndefined(LP.cache.context);
329+ }
330+ );
331+ },
332+
333+ test_get_relative_url: function() {
334+ var client = new Y.lp.client.Launchpad({sync: true});
335+ var config = makeTestConfig();
336+ client.get('/people', config);
337+ Y.Assert.isTrue(config.successful);
338+ Y.Assert.isInstanceOf(Y.lp.client.Collection, config.result);
339+ },
340+
341+ test_get_absolute_url: function() {
342+ var data = serverfixture.setup(this, 'create_product');
343+ var link = data.product.self_link;
344+ Y.Assert.areSame('http', link.slice(0, 4)); // See, it's absolute.
345+ var client = new Y.lp.client.Launchpad({sync: true});
346+ var config = makeTestConfig();
347+ client.get(link, config);
348+ Y.Assert.isTrue(config.successful);
349+ Y.Assert.isInstanceOf(Y.lp.client.Entry, config.result);
350+ },
351+
352+ test_get_collection_with_pagination: function() {
353+ // We could do this with a fixture setup, but I'll rely on the
354+ // sampledata for now. If this becomes a problem, write a quick
355+ // fixture that creates three or four people!
356+ var client = new Y.lp.client.Launchpad({sync: true});
357+ var config = makeTestConfig({start: 2, size: 1});
358+ client.get('/people', config);
359+ Y.Assert.isTrue(config.successful);
360+ Y.Assert.areSame(2, config.result.start);
361+ Y.Assert.areSame(1, config.result.entries.length);
362+ },
363+
364+ test_named_get_integration: function() {
365+ var data = serverfixture.setup(this, 'create_user');
366+ var client = new Y.lp.client.Launchpad({sync: true});
367+ var config = makeTestConfig({parameters: {text: data.user.name}});
368+ client.named_get('people', 'find', config);
369+ Y.Assert.isTrue(config.successful);
370+ Y.Assert.isInstanceOf(Y.lp.client.Collection, config.result);
371+ Y.Assert.areSame(1, config.result.total_size);
372+ },
373+
374+ test_named_post_integration: function() {
375+ var data = serverfixture.setup(this, 'create_bug_and_login');
376+ var client = new Y.lp.client.Launchpad({sync: true});
377+ var config = makeTestConfig();
378+ client.named_post(
379+ data.bug.self_link, 'mute', config);
380+ Y.Assert.isTrue(config.successful);
381+ },
382+
383+ test_follow_link: function() {
384+ var client = new Y.lp.client.Launchpad({sync: true});
385+ var config = makeTestConfig();
386+ client.get('', config);
387+ var root = config.result;
388+ config = makeTestConfig();
389+ root.follow_link('people', config);
390+ Y.Assert.isInstanceOf(Y.lp.client.Collection, config.result);
391+ Y.Assert.areSame(4, config.result.total_size);
392+ },
393+
394+ test_follow_redirected_link: function() {
395+ var data = serverfixture.setup(this, 'create_user_and_login');
396+ var client = new Y.lp.client.Launchpad({sync: true});
397+ var config = makeTestConfig();
398+ client.get('', config);
399+ var root = config.result;
400+ config = makeTestConfig();
401+ root.follow_link('me', config);
402+ Y.Assert.isTrue(config.successful);
403+ Y.Assert.isInstanceOf(Y.lp.client.Entry, config.result);
404+ Y.Assert.areSame(data.user.name, config.result.get('name'));
405+ },
406+
407+ test_get_html_representation: function() {
408+ var data = serverfixture.setup(this, 'create_user');
409+ var client = new Y.lp.client.Launchpad({sync: true});
410+ var config = makeTestConfig({accept: Y.lp.client.XHTML});
411+ client.get(data.user.self_link, config);
412+ Y.Assert.isTrue(config.successful);
413+ Y.Assert.isTrue(/<a href=\"\/\~/.test(config.result));
414+ },
415+
416+ test_get_html_representation_escaped: function() {
417+ var data = serverfixture.setup(
418+ this, 'create_user_with_html_display_name');
419+ var actual_display_name = data.user.display_name;
420+ Y.Assert.areEqual('<strong>naughty</strong>', actual_display_name);
421+ var html_escaped_display_name = '&lt;strong&gt;naughty&lt;/strong&gt;';
422+ var has_actual_display_name = new RegExp(
423+ Y.Escape.regex(actual_display_name));
424+ var has_html_escaped_display_name = new RegExp(
425+ Y.Escape.regex(html_escaped_display_name));
426+ var client = new Y.lp.client.Launchpad({sync: true});
427+ var config = makeTestConfig({accept: Y.lp.client.XHTML});
428+ client.get(data.user.self_link, config);
429+ Y.Assert.isTrue(config.successful);
430+ Y.Assert.isTrue(has_html_escaped_display_name.test(config.result));
431+ Y.Assert.isFalse(has_actual_display_name.test(config.result));
432+ },
433+
434+ test_lp_save_html_representation: function() {
435+ var data = serverfixture.setup(this, 'create_user');
436+ var client = new Y.lp.client.Launchpad({sync: true});
437+ var config = makeTestConfig({accept: Y.lp.client.XHTML});
438+ var user = new Y.lp.client.Entry(
439+ client, data.user, data.user.self_link);
440+ user.lp_save(config);
441+ Y.Assert.isTrue(config.successful);
442+ Y.Assert.isTrue(/<a href=\"\/\~/.test(config.result));
443+ },
444+
445+ test_patch_html_representation: function() {
446+ var data = serverfixture.setup(this, 'create_user');
447+ var client = new Y.lp.client.Launchpad({sync: true});
448+ var config = makeTestConfig({accept: Y.lp.client.XHTML});
449+ client.patch(data.user.self_link, {}, config);
450+ Y.Assert.isTrue(config.successful);
451+ Y.Assert.isTrue(/<a href=\"\/\~/.test(config.result));
452+ },
453+
454+ test_lp_save: function() {
455+ var data = serverfixture.setup(this, 'create_user_and_login');
456+ var client = new Y.lp.client.Launchpad({sync: true});
457+ var config = makeTestConfig();
458+ var user = new Y.lp.client.Entry(
459+ client, data.user, data.user.self_link);
460+ var original_display_name = user.get('display_name');
461+ var new_display_name = original_display_name + '_modified';
462+ user.set('display_name', new_display_name);
463+ user.lp_save(config);
464+ Y.Assert.isTrue(config.successful);
465+ Y.Assert.areEqual(new_display_name, config.result.get('display_name'));
466+ },
467+
468+ test_lp_save_fails_with_mismatched_ETag: function() {
469+ var data = serverfixture.setup(this, 'create_user_and_login');
470+ var client = new Y.lp.client.Launchpad({sync: true});
471+ var config = makeTestConfig();
472+ var user = new Y.lp.client.Entry(
473+ client, data.user, data.user.self_link);
474+ var original_display_name = user.get('display_name');
475+ var new_display_name = original_display_name + '_modified';
476+ user.set('display_name', new_display_name);
477+ user.set('http_etag', 'Non-matching ETag.');
478+ user.lp_save(config);
479+ Y.Assert.isFalse(config.successful);
480+ Y.Assert.areEqual(412, config.result.response.status);
481+ },
482+
483+ test_patch: function() {
484+ var data = serverfixture.setup(this, 'create_user_and_login');
485+ var client = new Y.lp.client.Launchpad({sync: true});
486+ var config = makeTestConfig();
487+ var new_display_name = data.user.display_name + '_modified';
488+ client.patch(
489+ data.user.self_link, {display_name: new_display_name}, config);
490+ Y.Assert.isTrue(config.successful);
491+ Y.Assert.areEqual(new_display_name, config.result.get('display_name'));
492+ },
493+
494+ test_collection_entries: function() {
495+ serverfixture.setup(this, 'login_as_admin');
496+ var client = new Y.lp.client.Launchpad({sync: true});
497+ var config = makeTestConfig();
498+ client.get('/people', config);
499+ var people = config.result;
500+ Y.Assert.isInstanceOf(Y.lp.client.Collection, people);
501+ Y.Assert.areEqual(4, people.total_size);
502+ Y.Assert.areEqual(people.total_size, people.entries.length);
503+ var i = 0;
504+ for (; i < people.entries.length; i++) {
505+ Y.Assert.isInstanceOf(Y.lp.client.Entry, people.entries[i]);
506+ }
507+ var entry = people.entries[0];
508+ var new_display_name = entry.get('display_name') + '_modified';
509+ entry.set('display_name', new_display_name);
510+ config = makeTestConfig();
511+ entry.lp_save(config);
512+ Y.Assert.areEqual(new_display_name, config.result.get('display_name'));
513+ },
514+
515+ test_collection_lp_slice: function() {
516+ var client = new Y.lp.client.Launchpad({sync: true});
517+ var config = makeTestConfig();
518+ client.get('/people', config);
519+ var people = config.result;
520+ people.lp_slice(config.on, 2, 1);
521+ var slice = config.result;
522+ Y.Assert.areEqual(2, slice.start);
523+ Y.Assert.areEqual(1, slice.entries.length);
524+ },
525+
526+ test_collection_named_get: function() {
527+ var data = serverfixture.setup(this, 'create_user');
528+ var client = new Y.lp.client.Launchpad({sync: true});
529+ var config = makeTestConfig();
530+ client.get('/people', config);
531+ var people = config.result;
532+ config = makeTestConfig({parameters: {text: data.user.name}});
533+ people.named_get('find', config);
534+ Y.Assert.isTrue(config.successful);
535+ Y.Assert.isInstanceOf(Y.lp.client.Collection, config.result);
536+ Y.Assert.areEqual(1, config.result.total_size);
537+ Y.Assert.areEqual(1, config.result.entries.length);
538+ },
539+
540+ test_collection_named_post: function() {
541+ serverfixture.setup(this, 'login_as_admin');
542+ var client = new Y.lp.client.Launchpad({sync: true});
543+ var config = makeTestConfig();
544+ client.get('/people', config);
545+ var people = config.result;
546+ config = makeTestConfig(
547+ {parameters: {display_name: 'My lpclient team',
548+ name: 'newlpclientteam'}});
549+ people.named_post('newTeam', config);
550+ Y.Assert.isTrue(config.successful);
551+ var team = config.result;
552+ Y.Assert.isInstanceOf(Y.lp.client.Entry, team);
553+ Y.Assert.areEqual('My lpclient team', team.get('display_name'));
554+ Y.Assert.isTrue(/\~newlpclientteam$/.test(team.lp_original_uri));
555+ },
556+
557+ test_collection_paged_named_get: function() {
558+ var data = serverfixture.setup(this, 'create_user');
559+ var client = new Y.lp.client.Launchpad({sync: true});
560+ var config = makeTestConfig();
561+ client.get('/people', config);
562+ var people = config.result;
563+ config = makeTestConfig({parameters: {text: data.user.name},
564+ start: 10});
565+ people.named_get('find', config);
566+ Y.Assert.isTrue(config.successful);
567+ Y.Assert.isInstanceOf(Y.lp.client.Collection, config.result);
568+ // I believe that the total_size is not correct in this case for
569+ // server-side efficiency in an edge case. It actually reports "10".
570+ // Y.Assert.areEqual(1, config.result.total_size);
571+ Y.Assert.areEqual(0, config.result.entries.length);
572+ }
573+
574+}));
575+
576+serverfixture.run(suite);
577+});
578
579=== added file 'lib/lp/app/javascript/tests/test_lp_client_integration.py'
580--- lib/lp/app/javascript/tests/test_lp_client_integration.py 1970-01-01 00:00:00 +0000
581+++ lib/lp/app/javascript/tests/test_lp_client_integration.py 2011-11-21 14:41:36 +0000
582@@ -0,0 +1,62 @@
583+# Copyright 2011 Canonical Ltd. This software is licensed under the
584+# GNU Affero General Public License version 3 (see the file LICENSE).
585+
586+"""Support for the lp.client YUIXHR tests.
587+"""
588+
589+__metaclass__ = type
590+__all__ = []
591+
592+from lp.testing import person_logged_in
593+from lp.testing.yuixhr import (
594+ login_as_person,
595+ make_suite,
596+ setup,
597+ )
598+from lp.testing.factory import LaunchpadObjectFactory
599+
600+factory = LaunchpadObjectFactory()
601+
602+
603+@setup
604+def create_user(request, data):
605+ data['user'] = factory.makePerson()
606+
607+
608+@create_user.extend
609+def create_user_and_login(request, data):
610+ login_as_person(data['user'])
611+
612+
613+@create_user.extend
614+def create_product(request, data):
615+ with person_logged_in(data['user']):
616+ data['product'] = factory.makeProduct(owner=data['user'])
617+
618+
619+@create_product.extend
620+def create_product_and_login(request, data):
621+ login_as_person(data['user'])
622+
623+
624+@create_product_and_login.extend
625+def create_bug_and_login(request, data):
626+ data['bug'] = factory.makeBug(
627+ product=data['product'], owner=data['user'])
628+ data['bugtask'] = data['bug'].bugtasks[0]
629+
630+
631+@setup
632+def login_as_admin(request, data):
633+ data['admin'] = factory.makeAdministrator()
634+ login_as_person(data['admin'])
635+
636+
637+@setup
638+def create_user_with_html_display_name(request, data):
639+ data['user'] = factory.makePerson(
640+ displayname='<strong>naughty</strong>')
641+
642+
643+def test_suite():
644+ return make_suite(__name__)
645
646=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
647--- lib/lp/app/templates/base-layout-macros.pt 2011-11-11 10:20:12 +0000
648+++ lib/lp/app/templates/base-layout-macros.pt 2011-11-21 14:41:36 +0000
649@@ -194,8 +194,12 @@
650 '${links/?key/fmt:api_url}';">
651 </script>
652 </tal:cache>
653-
654- <script tal:content="string:LP.cache = ${view/getCacheJSON};">
655+ <tal:comment condition="nothing">
656+ The id of the script block below is used to determine whether this
657+ page is loaded by test_lp_client_integration.js.
658+ </tal:comment>
659+ <script id="json-cache-script"
660+ tal:content="string:LP.cache = ${view/getCacheJSON};">
661 </script>
662 </metal:lp-client-cache>
663
664
665=== modified file 'lib/lp/registry/browser/tests/test_subscription_links.py'
666--- lib/lp/registry/browser/tests/test_subscription_links.py 2011-11-04 11:10:23 +0000
667+++ lib/lp/registry/browser/tests/test_subscription_links.py 2011-11-21 14:41:36 +0000
668@@ -6,7 +6,6 @@
669 __metaclass__ = type
670
671 import re
672-import simplejson
673 import unittest
674 from zope.component import getUtility
675 from BeautifulSoup import BeautifulSoup
676@@ -24,6 +23,7 @@
677 from lp.registry.model.milestone import ProjectMilestone
678 from lp.testing import (
679 celebrity_logged_in,
680+ extract_lp_cache,
681 person_logged_in,
682 BrowserTestCase,
683 TestCaseWithFactory,
684@@ -71,11 +71,7 @@
685 None, self.new_edit_link,
686 "Expected edit_bug_mail link missing")
687 # Ensure the LP.cache has been populated.
688- mo = re.search(
689- r'<script>\s*LP.cache\s*=\s*({.*?});\s*</script>', self.contents)
690- if mo is None:
691- self.fail('No JSON cache found')
692- cache = simplejson.loads(mo.group(1))
693+ cache = extract_lp_cache(self.contents)
694 self.assertIn('administratedTeams', cache)
695 # Ensure the call to setup the subscription is in the HTML.
696 # Only check for the presence of setup's configuration step; more
697
698=== modified file 'lib/lp/testing/__init__.py'
699--- lib/lp/testing/__init__.py 2011-11-15 10:39:28 +0000
700+++ lib/lp/testing/__init__.py 2011-11-21 14:41:36 +0000
701@@ -1322,7 +1322,9 @@
702
703
704 def extract_lp_cache(text):
705- match = re.search(r'<script>LP.cache = (\{.*\});</script>', text)
706+ match = re.search(r'<script[^>]*>LP.cache = (\{.*\});</script>', text)
707+ if match is None:
708+ raise ValueError('No JSON cache found.')
709 return simplejson.loads(match.group(1))
710
711