Merge lp:~rharding/launchpad/lpclient_fix into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 15685
Proposed branch: lp:~rharding/launchpad/lpclient_fix
Merge into: lp:launchpad
Prerequisite: lp:~rharding/launchpad/garden_client
Diff against target: 1471 lines (+651/-597)
6 files modified
lib/lp/app/javascript/client.js (+63/-55)
lib/lp/app/javascript/tests/test_lp_client.html (+0/-6)
lib/lp/app/javascript/tests/test_lp_client.js (+583/-518)
lib/lp/app/javascript/tests/test_lp_client_integration.js (+2/-15)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+1/-1)
lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js (+2/-2)
To merge this branch: bzr merge lp:~rharding/launchpad/lpclient_fix
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+116338@code.launchpad.net

Commit message

Fix client.js to maintain uri on PATCH requests so that web_link:changed event fires off LP.cache updates.

Description of the change

= Summary =

Fixes bug #1004248 introduced by attempting to fix bug #911973. That fix only
considered named_post/get methods and not patch methods.

== Pre Implementation ==

Talked a bunch with Aaron and Deryck while working on issue and best way to
fix.

== Implementation Notes ==

The big fix is to only reassign the uri when the request method is not a
patch. #89 of the diff.

This required letting the wrapper method know what the method was so we pass
that along in the args.

Tests were added to verify that this is not altered during a patch, but is
during a named_get/post. See #776.

The test suite was updated to the new standard format and thus was indented
for the big line change there. No other tests were modified or adjusted.

Since the entry/resource lp_original_uri no longer makes sense, it's just
changed to uri. It's used in other methods that the instance makes so they
were updated to match.

There are a few other small drive by lints to fix spacing and such. See
#8/25/etc.

== Tests ==

xvfb-run ./bin/test -x -cvv --layer=YUITestLayer

== LoC Qualification ==
Need 54 LoC

Worked on cleaning up the client.js, removing the original bug test that isn't
required and isn't run.

See:
https://code.launchpad.net/~rharding/launchpad/garden_client -33
https://code.launchpad.net/~rharding/launchpad/reportbug/+merge/115562 -8
https://code.launchpad.net/~rharding/launchpad/lpnames_yui35 -17

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/client.js'
2--- lib/lp/app/javascript/client.js 2012-07-23 18:06:49 +0000
3+++ lib/lp/app/javascript/client.js 2012-07-24 16:58:23 +0000
4@@ -43,11 +43,11 @@
5 'lp:' + cache_name + ':' + name + ':changed';
6 var new_value_html = entry.getHTML(name);
7 var event = {
8- 'name': name,
9- 'old_value': old_value,
10- 'new_value': new_value,
11- 'new_value_html': new_value_html,
12- 'entry': entry
13+ name: name,
14+ old_value: old_value,
15+ new_value: new_value,
16+ new_value_html: new_value_html,
17+ entry: entry
18 };
19 Y.fire(field_updated_event_name, event);
20 }
21@@ -66,17 +66,16 @@
22 }
23
24 if (fields_changed.length > 0) {
25- var event_name = 'lp:' + cache_name + ':changed';
26- var event_ = {
27- 'fields_changed': fields_changed,
28- 'entry': entry
29- };
30- Y.fire(event_name, event_);
31+ var event_name = 'lp:' + cache_name + ':changed';
32+ var event_ = {
33+ fields_changed: fields_changed,
34+ entry: entry
35+ };
36+ Y.fire(event_name, event_);
37 }
38 };
39
40
41-
42 var module = Y.namespace('lp.client');
43
44 module.HTTP_CREATED = 201;
45@@ -84,6 +83,9 @@
46 module.HTTP_NOT_FOUND = 404;
47
48 module.XHTML = 'application/xhtml+xml';
49+ module.GET = 'get';
50+ module.POST = 'post';
51+ module.PATCH = 'patch';
52
53 /* Log the normal attributes accessible via o[key], and if it is a
54 * YUI node, log all of the attributes accessible via o.get(key).
55@@ -328,7 +330,7 @@
56 if (!entry) {
57 return;
58 }
59- var original_uri = entry.lp_original_uri;
60+ var original_uri = entry.uri;
61 var full_uri = module.get_absolute_uri(original_uri);
62 var name;
63 var cached_object;
64@@ -349,14 +351,29 @@
65
66 module.wrap_resource_on_success = function(ignore, response, args) {
67 var client = args[0];
68+ // The original uri of the caller.
69 var uri = args[1];
70- var old_on_success = args[2];
71+ var callback = args[2];
72 var update_cache = args[3];
73+ var method = args[4];
74 var representation, wrapped;
75- if (old_on_success) {
76+
77+ if (callback) {
78 var media_type = response.getResponseHeader('Content-Type');
79 if (media_type.substring(0,16) === 'application/json') {
80 representation = Y.JSON.parse(response.responseText);
81+
82+ // If the object fetched has a self_link, make that the object's
83+ // uri for use in other api methods off of that object.
84+ // During a PATCH request the caller is the object. Leave the
85+ // original_uri alone. Otherwise make the uri the object
86+ // coming back.
87+ if (Y.Lang.isValue(representation) &&
88+ Y.Lang.isValue(representation.self_link) &&
89+ method !== module.PATCH) {
90+ uri = representation.self_link;
91+ }
92+
93 // If the response contains a notification header, display the
94 // notifications.
95 var notifications = response.getResponseHeader(
96@@ -364,18 +381,14 @@
97 if (notifications !== null && notifications !== "") {
98 module.display_notifications(notifications);
99 }
100- if (Y.Lang.isValue(representation) &&
101- Y.Lang.isValue(representation.self_link)) {
102- uri = representation.self_link;
103- }
104 wrapped = client.wrap_resource(uri, representation);
105- var result = old_on_success(wrapped);
106+ var result = callback(wrapped);
107 if (update_cache) {
108- module.update_cache(wrapped);
109+ module.update_cache(wrapped);
110 }
111 return result;
112 } else {
113- return old_on_success(response.responseText);
114+ return callback(response.responseText);
115 }
116 }
117 };
118@@ -501,7 +514,7 @@
119 'init': function(client, representation, uri) {
120 /* Initialize a resource with its representation and URI. */
121 this.lp_client = client;
122- this.lp_original_uri = uri;
123+ this.uri = uri;
124 var key;
125 for (key in representation) {
126 if (representation.hasOwnProperty(key)) {
127@@ -548,15 +561,13 @@
128
129 'named_get': function(operation_name, config) {
130 /* Get the result of a named GET operation on this resource. */
131- return this.lp_client.named_get(this.lp_original_uri,
132- operation_name,
133+ return this.lp_client.named_get(this.uri, operation_name,
134 config);
135 },
136
137 'named_post': function(operation_name, config) {
138 /* Trigger a named POST operation on this resource. */
139- return this.lp_client.named_post(this.lp_original_uri,
140- operation_name,
141+ return this.lp_client.named_post(this.uri, operation_name,
142 config);
143 }
144 };
145@@ -588,14 +599,14 @@
146 :param start: Where in the collection to start serving entries.
147 :param size: How many entries to serve.
148 */
149- return this.lp_client.get(this.lp_original_uri,
150+ return this.lp_client.get(this.uri,
151 {on: on, start: start, size: size});
152 };
153
154 module.Entry = function(client, representation, uri) {
155 /* A single object from the Launchpad web service. */
156 this.lp_client = client;
157- this.lp_original_uri = uri;
158+ this.uri = uri;
159 this.dirty_attributes = [];
160 var entry = this;
161
162@@ -644,21 +655,21 @@
163 };
164
165 module.Entry.prototype.getHTML = function(key) {
166- var lp_html = this.get('lp_html');
167- if (lp_html) {
168- // First look for the key.
169- var value = lp_html[key];
170- if (value === undefined) {
171- // now look for key_link
172- value = lp_html[key + '_link'];
173- }
174- if (value !== undefined) {
175- var result = Y.Node.create("<span/>");
176- result.setContent(value);
177- return result;
178- }
179- }
180- return null;
181+ var lp_html = this.get('lp_html');
182+ if (lp_html) {
183+ // First look for the key.
184+ var value = lp_html[key];
185+ if (value === undefined) {
186+ // now look for key_link
187+ value = lp_html[key + '_link'];
188+ }
189+ if (value !== undefined) {
190+ var result = Y.Node.create("<span/>");
191+ result.setContent(value);
192+ return result;
193+ }
194+ }
195+ return null;
196 };
197
198 // The Launchpad client itself.
199@@ -690,7 +701,8 @@
200 var client = this;
201 var y_config = {
202 on: on,
203- 'arguments': [client, uri, old_on_success, update_cache],
204+ 'arguments': [
205+ client, uri, old_on_success, update_cache, module.GET],
206 'headers': headers,
207 data: data,
208 sync: this.sync
209@@ -737,9 +749,8 @@
210 { on: { success: old_on_success,
211 failure: on.failure } });
212 }
213- return module.wrap_resource_on_success(undefined,
214- response,
215- args);
216+ return module.wrap_resource_on_success(
217+ undefined, response, args, module.POST);
218 };
219 var client = this;
220 var update_cache = false;
221@@ -761,12 +772,12 @@
222 var old_on_success = on.success;
223 var update_cache = true;
224 on.success = module.wrap_resource_on_success;
225- args = [this, uri, old_on_success, update_cache];
226+ var args = [this, uri, old_on_success, update_cache, module.PATCH];
227
228 var extra_headers = {
229- "X-HTTP-Method-Override": "PATCH",
230- "Content-Type": "application/json",
231- "X-Content-Type-Override": "application/json"
232+ "X-HTTP-Method-Override": "PATCH",
233+ "Content-Type": "application/json",
234+ "X-Content-Type-Override": "application/json"
235 };
236 var name;
237 if (headers !== undefined) {
238@@ -1027,14 +1038,12 @@
239 }
240 });
241
242-
243 }, "0.1", {
244 requires: ["attribute", "base", "io", "querystring", "json-parse",
245 "json-stringify", "lp"]
246 });
247
248 YUI.add('lp.client.plugins', function (Y) {
249-
250 /**
251 * A collection of plugins to hook lp.client into widgets.
252 *
253@@ -1245,7 +1254,6 @@
254 }
255 }
256 });
257-
258 }, "0.1", {
259 requires: ["base", "plugin", "dump", "lazr.editor", "lp.client"]
260 });
261
262=== modified file 'lib/lp/app/javascript/tests/test_lp_client.html'
263--- lib/lp/app/javascript/tests/test_lp_client.html 2012-03-27 04:36:24 +0000
264+++ lib/lp/app/javascript/tests/test_lp_client.html 2012-07-24 16:58:23 +0000
265@@ -3,7 +3,6 @@
266 Copyright 2012 Canonical Ltd. This software is licensed under the
267 GNU Affero General Public License version 3 (see the file LICENSE).
268 -->
269-
270 <html>
271 <head>
272 <title>Test client</title>
273@@ -30,20 +29,15 @@
274 <script type="text/javascript"
275 src="../../../../../build/js/lp/app/lp.js"></script>
276
277-
278 <!-- The module under test. -->
279 <script type="text/javascript" src="../client.js"></script>
280
281- <!-- Placeholder for any css asset for this module. -->
282- <!-- <link rel="stylesheet" href="../assets/client-core.css" /> -->
283-
284 <!-- The test suite. -->
285 <script type="text/javascript" src="test_lp_client.js"></script>
286
287 </head>
288 <body class="yui3-skin-sam">
289 <ul id="suites">
290- <!-- <li>lp.large_indicator.test</li> -->
291 <li>lp.client.test</li>
292 </ul>
293 <div class="context-publication">
294
295=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
296--- lib/lp/app/javascript/tests/test_lp_client.js 2012-06-26 17:16:34 +0000
297+++ lib/lp/app/javascript/tests/test_lp_client.js 2012-07-24 16:58:23 +0000
298@@ -1,522 +1,587 @@
299-/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
300-
301-YUI().use('lp.testing.runner', 'lp.testing.mockio', 'test', 'console',
302- 'lp.client', 'escape', function(Y) {
303-
304-var Assert = Y.Assert; // For easy access to isTrue(), etc.
305-
306-var suite = new Y.Test.Suite("lp.client Tests");
307-
308-suite.add(new Y.Test.Case({
309- name: "lp.client",
310-
311- setUp: function() {
312- },
313-
314- test_normalize_uri: function() {
315- var normalize = Y.lp.client.normalize_uri;
316- Assert.areEqual(
317- normalize("http://www.example.com/api/devel/foo"),
318- "/api/devel/foo");
319- Assert.areEqual(
320- normalize("http://www.example.com/foo/bar"), "/foo/bar");
321- Assert.areEqual(
322- normalize("/foo/bar"), "/api/devel/foo/bar");
323- Assert.areEqual(
324- normalize("/api/devel/foo/bar"), "/api/devel/foo/bar");
325- Assert.areEqual(
326- normalize("foo/bar"), "/api/devel/foo/bar");
327- Assert.areEqual(
328- normalize("api/devel/foo/bar"), "/api/devel/foo/bar");
329- },
330-
331- test_get_io_provider__default: function() {
332- var undefined_provider,
333- io_provider = Y.lp.client.get_io_provider(undefined_provider);
334- Assert.areSame(Y, io_provider);
335- },
336-
337- test_get_io_provider__mockio: function() {
338- // If a mock provider is provided, it is picked as the io_provider.
339- var mockio = new Y.lp.testing.mockio.MockIo(),
340- io_provider = Y.lp.client.get_io_provider(mockio);
341- Assert.areSame(mockio, io_provider);
342- },
343-
344- test_get_configured_io_provider__default: function() {
345- // If no io_provider is configured, Y is the io_provider.
346- var io_provider = Y.lp.client.get_configured_io_provider({});
347- Assert.areSame(Y, io_provider);
348- },
349-
350- test_get_configured_io_provider__default_undefined: function() {
351- // If no configuration is provided, Y is the io_provider.
352- var io_provider = Y.lp.client.get_configured_io_provider();
353- Assert.areSame(Y, io_provider);
354- },
355-
356- test_get_configured_io_provider__mockio: function() {
357- // If an io_provider is configured, it is picked as the io_provider.
358- var mockio = new Y.lp.testing.mockio.MockIo(),
359- io_provider = Y.lp.client.get_configured_io_provider(
360- {io_provider: mockio});
361- Assert.areSame(mockio, io_provider);
362- },
363-
364- test_get_configured_io_provider__different_key: function() {
365- // The io_provider can be stored with a different key.
366- var mockio = new Y.lp.testing.mockio.MockIo(),
367- io_provider = Y.lp.client.get_configured_io_provider(
368- {my_io: mockio}, 'my_io');
369- Assert.areSame(mockio, io_provider);
370- },
371-
372- test_append_qs: function() {
373- var qs = "";
374- qs = Y.lp.client.append_qs(qs, "Pöllä", "Perelló");
375- Assert.areEqual(
376- "P%C3%83%C2%B6ll%C3%83%C2%A4=Perell%C3%83%C2%B3", qs,
377- 'This tests is known to fail in Chrome, it is browser specific.');
378- },
379-
380- test_append_qs_with_array: function() {
381- // append_qs() appends multiple arguments to the query string
382- // when a parameter value is an array.
383- var qs = "";
384- qs = Y.lp.client.append_qs(qs, "foo", ["bar", "baz"]);
385- Assert.areEqual("foo=bar&foo=baz", qs);
386- // All values in the array are encoded correctly too.
387- qs = Y.lp.client.append_qs(qs, "a&b", ["a+b"]);
388- Assert.areEqual("foo=bar&foo=baz&a%26b=a%2Bb", qs);
389- },
390-
391- test_field_uri: function() {
392- var get_field_uri = Y.lp.client.get_field_uri;
393- Assert.areEqual(
394- get_field_uri("http://www.example.com/api/devel/foo", "field"),
395- "/api/devel/foo/field");
396- Assert.areEqual(
397- get_field_uri("/no/slash", "field"),
398- "/api/devel/no/slash/field");
399- Assert.areEqual(
400- get_field_uri("/has/slash/", "field"),
401- "/api/devel/has/slash/field");
402- },
403- test_view_url: function() {
404- entry_repr = {web_link: 'http://example.com/context'};
405- var context = new Y.lp.client.Entry(null, entry_repr, null);
406- expected = '/context/+myview/++mynamespace++';
407- actual = Y.lp.client.get_view_url(context, '+myview', 'mynamespace');
408- Assert.areEqual(expected, actual);
409- },
410- test_get_form_url: function() {
411- entry_repr = {web_link: 'http://example.com/context'};
412- var context = new Y.lp.client.Entry(null, entry_repr, null);
413- expected = '/context/+myview/++form++';
414- actual = Y.lp.client.get_form_url(context, '+myview');
415- Assert.areEqual(expected, actual);
416- },
417- test_load_model: function(){
418- var mockio = new Y.lp.testing.mockio.MockIo();
419- Assert.areEqual(0, mockio.requests.length);
420- var mylist = [];
421- var config = {
422- io_provider: mockio,
423- on: {
424- success: Y.bind(mylist.push, mylist)
425- }
426- };
427- var entry_repr = {web_link: 'http://example.com/context'};
428- var client = new Y.lp.client.Launchpad();
429- var context = new Y.lp.client.Entry(client, entry_repr, null);
430- Y.lp.client.load_model(context, '+myview', config);
431- Assert.areEqual(
432- '/context/+myview/++model++', mockio.last_request.url);
433- mockio.success({
434- responseText:
435- '{"boolean": true, "entry": {"resource_type_link": "foo"}}',
436- responseHeaders: {'Content-Type': 'application/json'}
437- });
438- var result = mylist[0];
439- Assert.areSame(true, result.boolean);
440- Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
441- Assert.areSame('foo', result.entry.get('resource_type_link'));
442- },
443- test_get_success_callback: function() {
444- var mockio = new Y.lp.testing.mockio.MockIo();
445- var mylist = [];
446- var client = new Y.lp.client.Launchpad({io_provider: mockio});
447- client.get('/people', {on:{success: Y.bind(mylist.push, mylist)}});
448- Assert.areEqual('/api/devel/people', mockio.last_request.url);
449- mockio.success({
450- responseText:
451- '{"entry": {"resource_type_link": "foo"}}',
452- responseHeaders: {'Content-Type': 'application/json'}
453- });
454- var result = mylist[0];
455- Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
456- Assert.areSame('foo', result.entry.get('resource_type_link'));
457- },
458- test_get_failure_callback: function() {
459- var mockio = new Y.lp.testing.mockio.MockIo();
460- var mylist = [];
461- var client = new Y.lp.client.Launchpad({io_provider: mockio});
462- client.get(
463- '/people',
464- {on: {
465- failure: function(){
466- mylist.push(Array.prototype.slice.call(arguments));
467- }}});
468- mockio.failure({status: 503});
469- var result = mylist[0];
470- Assert.areSame(503, result[1].status);
471- Assert.areSame('/api/devel/people', result[2][1]);
472- },
473- test_named_post_success_callback: function() {
474- var mockio = new Y.lp.testing.mockio.MockIo();
475- var mylist = [];
476- var client = new Y.lp.client.Launchpad({io_provider: mockio});
477- client.named_post(
478- '/people', 'newTeam', {on:{success: Y.bind(mylist.push, mylist)}});
479- Assert.areEqual('/api/devel/people', mockio.last_request.url);
480- Assert.areEqual('ws.op=newTeam', mockio.last_request.config.data);
481- Assert.areEqual('POST', mockio.last_request.config.method);
482- mockio.success({
483- responseText:
484- '{"entry": {"resource_type_link": "foo"}}',
485- responseHeaders: {'Content-Type': 'application/json'}
486- });
487- var result = mylist[0];
488- Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
489- Assert.areSame('foo', result.entry.get('resource_type_link'));
490- },
491- test_wrap_resource_nested_mapping: function() {
492- // wrap_resource produces mappings of plain object literals. These can
493- // be nested and have Entries in them.
494- var foo = {
495- baa: {},
496- bar: {
497- baz: {
498- resource_type_link: 'qux'}
499- }
500- };
501- foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
502- Y.Assert.isInstanceOf(Y.lp.client.Entry, foo.bar.baz);
503- },
504- test_wrap_resource_nested_array: function() {
505- // wrap_resource produces arrays of array literals. These can
506- // be nested and have Entries in them.
507- var foo = [[{resource_type_link: 'qux'}]];
508- foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
509- Y.Assert.isInstanceOf(Y.lp.client.Entry, foo[0][0]);
510- },
511- test_wrap_resource_creates_array: function() {
512- // wrap_resource creates new arrays, rather than reusing the existing
513- // one.
514- var foo = ['a'];
515- var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
516- Y.Assert.areNotSame(foo, bar);
517- },
518- test_wrap_resource_creates_mapping: function() {
519- // wrap_resource creates new mappings, rather than reusing the
520- // existing one.
521- var foo = {a: 'b'};
522- var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
523- Y.Assert.areNotSame(foo, bar);
524- },
525- test_wrap_resource_null: function() {
526- // wrap_resource handles null correctly.
527- var foo = {
528- bar: null
529- };
530- foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
531- Y.Assert.isNull(foo.bar);
532- }
533-}));
534-
535-suite.add(new Y.Test.Case({
536- name: "update cache",
537-
538- setUp: function() {
539- window.LP = {
540- cache: {
541- context: {
542+/* Copyright (c) 2011-2012 Canonical Ltd. All rights reserved. */
543+YUI.add('lp.client.test', function (Y) {
544+ var Assert = Y.Assert;
545+ var tests = Y.namespace('lp.client.test');
546+ tests.suite = new Y.Test.Suite('client Tests');
547+ tests.suite.add(new Y.Test.Case({
548+ name: "lp.client",
549+
550+ setUp: function() {
551+ },
552+
553+ test_normalize_uri: function() {
554+ var normalize = Y.lp.client.normalize_uri;
555+ Assert.areEqual(
556+ normalize("http://www.example.com/api/devel/foo"),
557+ "/api/devel/foo");
558+ Assert.areEqual(
559+ normalize("http://www.example.com/foo/bar"), "/foo/bar");
560+ Assert.areEqual(
561+ normalize("/foo/bar"), "/api/devel/foo/bar");
562+ Assert.areEqual(
563+ normalize("/api/devel/foo/bar"), "/api/devel/foo/bar");
564+ Assert.areEqual(
565+ normalize("foo/bar"), "/api/devel/foo/bar");
566+ Assert.areEqual(
567+ normalize("api/devel/foo/bar"), "/api/devel/foo/bar");
568+ },
569+
570+ test_get_io_provider__default: function() {
571+ var undefined_provider,
572+ io_provider = Y.lp.client.get_io_provider(undefined_provider);
573+ Assert.areSame(Y, io_provider);
574+ },
575+
576+ test_get_io_provider__mockio: function() {
577+ // If a mock provider is provided, it is picked as the io_provider.
578+ var mockio = new Y.lp.testing.mockio.MockIo(),
579+ io_provider = Y.lp.client.get_io_provider(mockio);
580+ Assert.areSame(mockio, io_provider);
581+ },
582+
583+ test_get_configured_io_provider__default: function() {
584+ // If no io_provider is configured, Y is the io_provider.
585+ var io_provider = Y.lp.client.get_configured_io_provider({});
586+ Assert.areSame(Y, io_provider);
587+ },
588+
589+ test_get_configured_io_provider__default_undefined: function() {
590+ // If no configuration is provided, Y is the io_provider.
591+ var io_provider = Y.lp.client.get_configured_io_provider();
592+ Assert.areSame(Y, io_provider);
593+ },
594+
595+ test_get_configured_io_provider__mockio: function() {
596+ // If an io_provider is configured, it is picked as the
597+ // io_provider.
598+ var mockio = new Y.lp.testing.mockio.MockIo(),
599+ io_provider = Y.lp.client.get_configured_io_provider(
600+ {io_provider: mockio});
601+ Assert.areSame(mockio, io_provider);
602+ },
603+
604+ test_get_configured_io_provider__different_key: function() {
605+ // The io_provider can be stored with a different key.
606+ var mockio = new Y.lp.testing.mockio.MockIo(),
607+ io_provider = Y.lp.client.get_configured_io_provider(
608+ {my_io: mockio}, 'my_io');
609+ Assert.areSame(mockio, io_provider);
610+ },
611+
612+ test_append_qs: function() {
613+ var qs = "";
614+ qs = Y.lp.client.append_qs(qs, "Pöllä", "Perelló");
615+ Assert.areEqual(
616+ "P%C3%83%C2%B6ll%C3%83%C2%A4=Perell%C3%83%C2%B3", qs,
617+ 'This tests is known to fail in Chrome.');
618+ },
619+
620+ test_append_qs_with_array: function() {
621+ // append_qs() appends multiple arguments to the query string
622+ // when a parameter value is an array.
623+ var qs = "";
624+ qs = Y.lp.client.append_qs(qs, "foo", ["bar", "baz"]);
625+ Assert.areEqual("foo=bar&foo=baz", qs);
626+ // All values in the array are encoded correctly too.
627+ qs = Y.lp.client.append_qs(qs, "a&b", ["a+b"]);
628+ Assert.areEqual("foo=bar&foo=baz&a%26b=a%2Bb", qs);
629+ },
630+
631+ test_field_uri: function() {
632+ var get_field_uri = Y.lp.client.get_field_uri;
633+ Assert.areEqual(
634+ get_field_uri("http://www.example.com/api/devel/foo", "field"),
635+ "/api/devel/foo/field");
636+ Assert.areEqual(
637+ get_field_uri("/no/slash", "field"),
638+ "/api/devel/no/slash/field");
639+ Assert.areEqual(
640+ get_field_uri("/has/slash/", "field"),
641+ "/api/devel/has/slash/field");
642+ },
643+ test_view_url: function() {
644+ entry_repr = {web_link: 'http://example.com/context'};
645+ var context = new Y.lp.client.Entry(null, entry_repr, null);
646+ expected = '/context/+myview/++mynamespace++';
647+ actual = Y.lp.client.get_view_url(
648+ context, '+myview', 'mynamespace');
649+ Assert.areEqual(expected, actual);
650+ },
651+ test_get_form_url: function() {
652+ entry_repr = {web_link: 'http://example.com/context'};
653+ var context = new Y.lp.client.Entry(null, entry_repr, null);
654+ expected = '/context/+myview/++form++';
655+ actual = Y.lp.client.get_form_url(context, '+myview');
656+ Assert.areEqual(expected, actual);
657+ },
658+ test_load_model: function(){
659+ var mockio = new Y.lp.testing.mockio.MockIo();
660+ Assert.areEqual(0, mockio.requests.length);
661+ var mylist = [];
662+ var config = {
663+ io_provider: mockio,
664+ on: {
665+ success: Y.bind(mylist.push, mylist)
666+ }
667+ };
668+ var entry_repr = {web_link: 'http://example.com/context'};
669+ var client = new Y.lp.client.Launchpad();
670+ var context = new Y.lp.client.Entry(client, entry_repr, null);
671+ Y.lp.client.load_model(context, '+myview', config);
672+ Assert.areEqual(
673+ '/context/+myview/++model++', mockio.last_request.url);
674+ mockio.success({
675+ responseText:
676+ '{"boolean": true, "entry": {"resource_type_link": "foo"}}',
677+ responseHeaders: {'Content-Type': 'application/json'}
678+ });
679+ var result = mylist[0];
680+ Assert.areSame(true, result.boolean);
681+ Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
682+ Assert.areSame('foo', result.entry.get('resource_type_link'));
683+ },
684+ test_get_success_callback: function() {
685+ var mockio = new Y.lp.testing.mockio.MockIo();
686+ var mylist = [];
687+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
688+ client.get('/people', {on:{success: Y.bind(mylist.push, mylist)}});
689+ Assert.areEqual('/api/devel/people', mockio.last_request.url);
690+ mockio.success({
691+ responseText:
692+ '{"entry": {"resource_type_link": "foo"}}',
693+ responseHeaders: {'Content-Type': 'application/json'}
694+ });
695+ var result = mylist[0];
696+ Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
697+ Assert.areSame('foo', result.entry.get('resource_type_link'));
698+ },
699+ test_get_failure_callback: function() {
700+ var mockio = new Y.lp.testing.mockio.MockIo();
701+ var mylist = [];
702+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
703+ client.get(
704+ '/people',
705+ {on: {
706+ failure: function(){
707+ mylist.push(Array.prototype.slice.call(arguments));
708+ }}});
709+ mockio.failure({status: 503});
710+ var result = mylist[0];
711+ Assert.areSame(503, result[1].status);
712+ Assert.areSame('/api/devel/people', result[2][1]);
713+ },
714+ test_named_post_success_callback: function() {
715+ var mockio = new Y.lp.testing.mockio.MockIo();
716+ var mylist = [];
717+ var client = new Y.lp.client.Launchpad({io_provider: mockio});
718+ client.named_post(
719+ '/people', 'newTeam', {on:{success: Y.bind(mylist.push, mylist)}});
720+ Assert.areEqual('/api/devel/people', mockio.last_request.url);
721+ Assert.areEqual('ws.op=newTeam', mockio.last_request.config.data);
722+ Assert.areEqual('POST', mockio.last_request.config.method);
723+ mockio.success({
724+ responseText:
725+ '{"entry": {"resource_type_link": "foo"}}',
726+ responseHeaders: {'Content-Type': 'application/json'}
727+ });
728+ var result = mylist[0];
729+ Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
730+ Assert.areSame('foo', result.entry.get('resource_type_link'));
731+ },
732+ test_wrap_resource_nested_mapping: function() {
733+ // wrap_resource produces mappings of plain object literals. These
734+ // can be nested and have Entries in them.
735+ var foo = {
736+ baa: {},
737+ bar: {
738+ baz: {
739+ resource_type_link: 'qux'}
740+ }
741+ };
742+ foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
743+ Assert.isInstanceOf(Y.lp.client.Entry, foo.bar.baz);
744+ },
745+ test_wrap_resource_nested_array: function() {
746+ // wrap_resource produces arrays of array literals. These can
747+ // be nested and have Entries in them.
748+ var foo = [[{resource_type_link: 'qux'}]];
749+ foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
750+ Assert.isInstanceOf(Y.lp.client.Entry, foo[0][0]);
751+ },
752+ test_wrap_resource_creates_array: function() {
753+ // wrap_resource creates new arrays, rather than reusing the
754+ // existing one.
755+ var foo = ['a'];
756+ var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
757+ Assert.areNotSame(foo, bar);
758+ },
759+ test_wrap_resource_creates_mapping: function() {
760+ // wrap_resource creates new mappings, rather than reusing the
761+ // existing one.
762+ var foo = {a: 'b'};
763+ var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
764+ Assert.areNotSame(foo, bar);
765+ },
766+ test_wrap_resource_null: function() {
767+ // wrap_resource handles null correctly.
768+ var foo = {
769+ bar: null
770+ };
771+ foo = new Y.lp.client.Launchpad().wrap_resource(null, foo);
772+ Assert.isNull(foo.bar);
773+ }
774+ }));
775+
776+ tests.suite.add(new Y.Test.Case({
777+ name: "lp.wrap_on_success",
778+ original_uri: 'http://launchpad.net/original_uri',
779+ updated_uri: 'http://launchpad.net/object_uri',
780+
781+ setUp: function() {
782+ window.LP = {};
783+ this.called = false;
784+ },
785+
786+ tearDown: function () {
787+ delete window.LP;
788+ delete this.called;
789+ },
790+
791+ _gen_callback: function (uri) {
792+ var that = this;
793+ return function (wrapped) {
794+ that.called = true;
795+ Assert.areEqual(wrapped.uri, uri);
796+ };
797+ },
798+
799+ _fake_response: function () {
800+ return {
801+ responseText: Y.JSON.stringify({
802+ self_link: this.updated_uri,
803+ resource_type_link: 'object'
804+ }),
805+ getResponseHeader: function (key) {
806+ var headers = {'Content-Type': 'application/json'};
807+ return headers[key];
808+ }
809+ };
810+ },
811+
812+ test_wrap_resource_patch_link: function () {
813+ // wrap_resource_on_success will not modify the uri on a patch
814+ // request and keep the original value.
815+ var callback = this._gen_callback(this.original_uri);
816+ Y.lp.client.wrap_resource_on_success(10, this._fake_response(), [
817+ new Y.lp.client.Launchpad(), this.original_uri, callback,
818+ true, 'patch']
819+ );
820+ Assert.isTrue(this.called);
821+ },
822+
823+ test_wrap_resource_patch_named: function () {
824+ // wrap_resource_on_success will modify the uri on a
825+ // named_get/post methods to the value that came back in
826+ // self_link.
827+ var callback = this._gen_callback(this.updated_uri);
828+ Y.lp.client.wrap_resource_on_success(10, this._fake_response(), [
829+ new Y.lp.client.Launchpad(), this.original_uri, callback,
830+ true, 'post']
831+ );
832+ Assert.isTrue(this.called);
833+ }
834+ }));
835+
836+ tests.suite.add(new Y.Test.Case({
837+ name: "update cache",
838+
839+ setUp: function() {
840+ window.LP = {
841+ cache: {
842+ context: {
843+ 'first': "Hello",
844+ 'second': true,
845+ 'third': 42,
846+ 'fourth': "Unaltered",
847+ 'self_link': Y.lp.client.get_absolute_uri("a_self_link")
848+ }
849+ }};
850+ },
851+
852+ tearDown: function() {
853+ delete window.LP;
854+ },
855+
856+ test_update_cache: function() {
857+ // Make sure that the cached objects are in fact updated.
858+ var entry_repr = {
859+ 'first': "World",
860+ 'second': false,
861+ 'third': 24,
862+ 'fourth': "Unaltered",
863+ 'self_link': Y.lp.client.get_absolute_uri("a_self_link")
864+ };
865+ var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
866+ Y.lp.client.update_cache(entry);
867+ Assert.areEqual("World", LP.cache.context.first);
868+ Assert.areEqual(false, LP.cache.context.second);
869+ Assert.areEqual(24, LP.cache.context.third);
870+ Assert.areEqual("Unaltered", LP.cache.context.fourth);
871+ },
872+
873+ test_getHTML: function() {
874+ // Make sure that the getHTML method works as expected.
875+ var entry_repr = {
876 'first': "Hello",
877- 'second': true,
878- 'third': 42,
879+ 'second': "World",
880+ 'self_link': Y.lp.client.get_absolute_uri("a_self_link"),
881+ 'lp_html': {'first': "<p>Hello</p><p>World</p>"}
882+ };
883+ var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
884+ Assert.areEqual(
885+ "<p>Hello</p><p>World</p>",
886+ entry.getHTML('first').get('innerHTML'));
887+ // If there is no html representation, null is returned.
888+ Assert.areEqual(null, entry.getHTML('second'));
889+ },
890+
891+ test_update_cache_raises_events: function() {
892+ // Check that the object changed event is raised.
893+ var raised_event = null;
894+ var handle = Y.on('lp:context:changed', function(e) {
895+ raised_event = e;
896+ });
897+ var entry_repr = {
898+ 'first': "World",
899+ 'second': false,
900+ 'third': 24,
901 'fourth': "Unaltered",
902 'self_link': Y.lp.client.get_absolute_uri("a_self_link")
903- }
904- }};
905- },
906-
907- tearDown: function() {
908- delete window.LP;
909- },
910-
911- test_update_cache: function() {
912- // Make sure that the cached objects are in fact updated.
913- var entry_repr = {
914- 'first': "World",
915- 'second': false,
916- 'third': 24,
917- 'fourth': "Unaltered",
918- 'self_link': Y.lp.client.get_absolute_uri("a_self_link")
919- };
920- var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
921- Y.lp.client.update_cache(entry);
922- Assert.areEqual("World", LP.cache.context.first);
923- Assert.areEqual(false, LP.cache.context.second);
924- Assert.areEqual(24, LP.cache.context.third);
925- Assert.areEqual("Unaltered", LP.cache.context.fourth);
926- },
927-
928- test_getHTML: function() {
929- // Make sure that the getHTML method works as expected.
930- var entry_repr = {
931- 'first': "Hello",
932- 'second': "World",
933- 'self_link': Y.lp.client.get_absolute_uri("a_self_link"),
934- 'lp_html': {'first': "<p>Hello</p><p>World</p>"}
935- };
936- var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
937- Assert.areEqual(
938- "<p>Hello</p><p>World</p>",
939- entry.getHTML('first').get('innerHTML'));
940- // If there is no html representation, null is returned.
941- Assert.areEqual(null, entry.getHTML('second'));
942- },
943-
944- test_update_cache_raises_events: function() {
945- // Check that the object changed event is raised.
946- var raised_event = null;
947- var handle = Y.on('lp:context:changed', function(e) {
948- raised_event = e;
949- });
950- var entry_repr = {
951- 'first': "World",
952- 'second': false,
953- 'third': 24,
954- 'fourth': "Unaltered",
955- 'self_link': Y.lp.client.get_absolute_uri("a_self_link")
956- };
957- var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
958- Y.lp.client.update_cache(entry);
959- handle.detach();
960- Y.ArrayAssert.itemsAreEqual(
961- ['first','second','third'], raised_event.fields_changed);
962- Assert.areEqual(entry, raised_event.entry);
963- },
964-
965- test_update_cache_raises_attribute_events: function() {
966- // Check that the object attribute changed events are raised.
967- var first_event = null;
968- var second_event = null;
969- var third_event = null;
970- var fourth_event = null;
971- var first_handle = Y.on('lp:context:first:changed', function(e) {
972- first_event = e;
973- });
974- var second_handle = Y.on('lp:context:second:changed', function(e) {
975- second_event = e;
976- });
977- var third_handle = Y.on('lp:context:third:changed', function(e) {
978- third_event = e;
979- });
980- var fourth_handle = Y.on('lp:context:fourth:changed', function(e) {
981- fourth_event = e;
982- });
983- var entry_repr = {
984- 'first': "World<boo/>",
985- 'second': false,
986- 'third': 24,
987- 'fourth': "Unaltered",
988- 'self_link': Y.lp.client.get_absolute_uri("a_self_link"),
989- 'lp_html': {'first': "<p>World html<boo/></p>"}
990- };
991- var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
992- Y.lp.client.update_cache(entry);
993- first_handle.detach();
994- second_handle.detach();
995- third_handle.detach();
996- fourth_handle.detach();
997-
998- Assert.areEqual('first', first_event.name);
999- Assert.areEqual('Hello', first_event.old_value);
1000- Assert.areEqual('World<boo/>', first_event.new_value);
1001- Assert.areEqual(
1002- '<p>World html<boo></boo></p>',
1003- first_event.new_value_html.get('innerHTML'));
1004- Assert.areEqual(entry, first_event.entry);
1005-
1006- Assert.areEqual('second', second_event.name);
1007- Assert.areEqual(true, second_event.old_value);
1008- Assert.areEqual(false, second_event.new_value);
1009- Assert.areEqual(entry, second_event.entry);
1010-
1011- Assert.areEqual('third', third_event.name);
1012- Assert.areEqual(42, third_event.old_value);
1013- Assert.areEqual(24, third_event.new_value);
1014- Assert.areEqual(entry, third_event.entry);
1015-
1016- Assert.isNull(fourth_event);
1017- },
1018-
1019- test_update_cache_different_object: function() {
1020- // Check that the object is not modified if the entry has a different
1021- // link.
1022- var entry_repr = {
1023- 'first': "World",
1024- 'second': false,
1025- 'third': 24,
1026- 'fourth': "Unaltered",
1027- 'self_link': Y.lp.client.get_absolute_uri("different_link")
1028- };
1029- var entry = new Y.lp.client.Entry(null, entry_repr, "different_link");
1030- Y.lp.client.update_cache(entry);
1031- Assert.areEqual("Hello", LP.cache.context.first);
1032- Assert.areEqual(true, LP.cache.context.second);
1033- Assert.areEqual(42, LP.cache.context.third);
1034- Assert.areEqual("Unaltered", LP.cache.context.fourth);
1035- }
1036-}));
1037-
1038-suite.add(new Y.Test.Case({
1039- name: "lp.client.notifications",
1040-
1041- setUp: function() {
1042- this.client = new Y.lp.client.Launchpad();
1043- this.args=[this.client, null, this._on_success, false];
1044- this.response = new Y.lp.testing.mockio.MockHttpResponse();
1045- this.response.setResponseHeader('Content-Type', 'application/json');
1046- },
1047-
1048- _on_success: function(entry) {
1049- },
1050-
1051- _checkNotificationNode: function(node_class, node_text) {
1052- var node = Y.one('div#request-notifications div'+node_class);
1053- Assert.areEqual(node_text, node.get("innerHTML"));
1054- },
1055-
1056- _checkNoNotificationNode: function(node_class) {
1057- var node = Y.one('div#request-notifications div'+node_class);
1058- Assert.isNull(node);
1059- },
1060-
1061- test_display_notifications: function() {
1062- var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1063- this.response.setResponseHeader(
1064- 'X-Lazr-Notifications', notifications);
1065- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1066- this._checkNotificationNode('.debug.message', 'A debug');
1067- this._checkNotificationNode('.informational.message', 'An info');
1068-
1069- // Any subsequent request should preserve existing notifications.
1070- var new_notifications = '[ [30, "A warning"], [40, "An error"] ]';
1071- this.response.setResponseHeader(
1072- 'X-Lazr-Notifications', new_notifications);
1073- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1074- this._checkNotificationNode('.debug.message', 'A debug');
1075- this._checkNotificationNode('.informational.message', 'An info');
1076- this._checkNotificationNode('.warning.message', 'A warning');
1077- this._checkNotificationNode('.error.message', 'An error');
1078- },
1079-
1080- test_remove_notifications: function() {
1081- // Make some notifications that will be removed.
1082- var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1083- this.response.setResponseHeader(
1084- 'X-Lazr-Notifications', notifications);
1085- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1086-
1087- // If the notifications header is just the string "null", then the
1088- // current notifications are removed.
1089- this.response.setResponseHeader('X-Lazr-Notifications', "null");
1090- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1091- this._checkNoNotificationNode('.debug.message');
1092- this._checkNoNotificationNode('.informational.message');
1093- },
1094-
1095- test_notifications_not_removed: function() {
1096- // Make some notifications that will be removed.
1097- var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1098- this.response.setResponseHeader(
1099- 'X-Lazr-Notifications', notifications);
1100- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1101-
1102- // If the response does not include a notifications header, then any
1103- // pre-existing notifiactions are not removed.
1104- this.response.setResponseHeader('X-Lazr-Notifications', null);
1105- Y.lp.client.wrap_resource_on_success(null, this.response, this.args);
1106- this._checkNotificationNode('.debug.message', 'A debug');
1107- this._checkNotificationNode('.informational.message', 'An info');
1108- }
1109-}));
1110-
1111-suite.add(new Y.Test.Case({
1112- name: "lp.client.forms",
1113-
1114- setUp: function() {
1115- var form = Y.one("#testform");
1116- this.error_handler = new Y.lp.client.FormErrorHandler({
1117- form: form
1118- });
1119- },
1120-
1121- tearDown: function() {
1122- this.error_handler.clearFormErrors();
1123- },
1124-
1125- test_form_error_handler_ignores_other_responses: function() {
1126- // Only XHR responses not containing validation data are ignored.
1127- var result = this.error_handler.handleError(0, {
1128- status: 400,
1129- statusText: 'Not Validation'
1130- });
1131- Y.Assert.isFalse(result);
1132- },
1133-
1134- test_form_error_handler_handles_responses: function() {
1135- // XHR responses containing validation data are processed.
1136- var error_data = {
1137- 'error_summary': 'Some errors',
1138- 'form_wide_errors': ['Form error'],
1139- errors: {'field.test': 'Field error'}
1140- };
1141- var result = this.error_handler.handleError(0, {
1142- status: 400,
1143- statusText: 'Validation',
1144- responseText: Y.JSON.stringify(error_data)
1145- });
1146- Y.Assert.isTrue(result);
1147- this._assert_error_rendering();
1148- },
1149-
1150- _assert_error_rendering: function() {
1151- var label = Y.one('label[for="field.test"]');
1152- var field_error = label.next('div').next('.message');
1153- Y.Assert.isTrue(Y.one('#field_div').hasClass('error'),
1154- 'Field div has class error');
1155- Y.Assert.areEqual('Field error', field_error.getContent());
1156- Y.all('.error.message').each(function(error_node) {
1157- var error_message = error_node.getContent();
1158- Y.Assert.isTrue(
1159- error_message === '<p>Form error</p>' ||
1160- error_message === 'Some errors',
1161- 'Each error message has the correct content.');
1162- });
1163- },
1164-
1165- test_form_error_handler_renders_errors: function() {
1166- // Form errors are rendered correctly.
1167- this.error_handler.handleFormValidationError(
1168- "Some errors", ["Form error"],
1169- {'field.test': "Field error"});
1170- this._assert_error_rendering();
1171- }
1172-}));
1173-
1174-Y.lp.testing.Runner.run(suite);
1175-
1176+ };
1177+ var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
1178+ Y.lp.client.update_cache(entry);
1179+ handle.detach();
1180+ Y.ArrayAssert.itemsAreEqual(
1181+ ['first','second','third'], raised_event.fields_changed);
1182+ Assert.areEqual(entry, raised_event.entry);
1183+ },
1184+
1185+ test_update_cache_raises_attribute_events: function() {
1186+ // Check that the object attribute changed events are raised.
1187+ var first_event = null;
1188+ var second_event = null;
1189+ var third_event = null;
1190+ var fourth_event = null;
1191+ var first_handle = Y.on('lp:context:first:changed', function(e) {
1192+ first_event = e;
1193+ });
1194+ var second_handle = Y.on('lp:context:second:changed', function(e) {
1195+ second_event = e;
1196+ });
1197+ var third_handle = Y.on('lp:context:third:changed', function(e) {
1198+ third_event = e;
1199+ });
1200+ var fourth_handle = Y.on('lp:context:fourth:changed', function(e) {
1201+ fourth_event = e;
1202+ });
1203+ var entry_repr = {
1204+ 'first': "World<boo/>",
1205+ 'second': false,
1206+ 'third': 24,
1207+ 'fourth': "Unaltered",
1208+ 'self_link': Y.lp.client.get_absolute_uri("a_self_link"),
1209+ 'lp_html': {'first': "<p>World html<boo/></p>"}
1210+ };
1211+ var entry = new Y.lp.client.Entry(null, entry_repr, "a_self_link");
1212+ Y.lp.client.update_cache(entry);
1213+ first_handle.detach();
1214+ second_handle.detach();
1215+ third_handle.detach();
1216+ fourth_handle.detach();
1217+
1218+ Assert.areEqual('first', first_event.name);
1219+ Assert.areEqual('Hello', first_event.old_value);
1220+ Assert.areEqual('World<boo/>', first_event.new_value);
1221+ Assert.areEqual(
1222+ '<p>World html<boo></boo></p>',
1223+ first_event.new_value_html.get('innerHTML'));
1224+ Assert.areEqual(entry, first_event.entry);
1225+
1226+ Assert.areEqual('second', second_event.name);
1227+ Assert.areEqual(true, second_event.old_value);
1228+ Assert.areEqual(false, second_event.new_value);
1229+ Assert.areEqual(entry, second_event.entry);
1230+
1231+ Assert.areEqual('third', third_event.name);
1232+ Assert.areEqual(42, third_event.old_value);
1233+ Assert.areEqual(24, third_event.new_value);
1234+ Assert.areEqual(entry, third_event.entry);
1235+
1236+ Assert.isNull(fourth_event);
1237+ },
1238+
1239+ test_update_cache_different_object: function() {
1240+ // Check that the object is not modified if the entry has a
1241+ // different link.
1242+ var entry_repr = {
1243+ 'first': "World",
1244+ 'second': false,
1245+ 'third': 24,
1246+ 'fourth': "Unaltered",
1247+ 'self_link': Y.lp.client.get_absolute_uri("different_link")
1248+ };
1249+ var entry = new Y.lp.client.Entry(
1250+ null, entry_repr, "different_link");
1251+ Y.lp.client.update_cache(entry);
1252+ Assert.areEqual("Hello", LP.cache.context.first);
1253+ Assert.areEqual(true, LP.cache.context.second);
1254+ Assert.areEqual(42, LP.cache.context.third);
1255+ Assert.areEqual("Unaltered", LP.cache.context.fourth);
1256+ }
1257+ }));
1258+
1259+ tests.suite.add(new Y.Test.Case({
1260+ name: "lp.client.notifications",
1261+
1262+ setUp: function() {
1263+ this.client = new Y.lp.client.Launchpad();
1264+ this.args=[this.client, null, this._on_success, false];
1265+ this.response = new Y.lp.testing.mockio.MockHttpResponse();
1266+ this.response.setResponseHeader('Content-Type', 'application/json');
1267+ },
1268+
1269+ _on_success: function(entry) {
1270+ },
1271+
1272+ _checkNotificationNode: function(node_class, node_text) {
1273+ var node = Y.one('div#request-notifications div'+node_class);
1274+ Assert.areEqual(node_text, node.get("innerHTML"));
1275+ },
1276+
1277+ _checkNoNotificationNode: function(node_class) {
1278+ var node = Y.one('div#request-notifications div'+node_class);
1279+ Assert.isNull(node);
1280+ },
1281+
1282+ test_display_notifications: function() {
1283+ var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1284+ this.response.setResponseHeader(
1285+ 'X-Lazr-Notifications', notifications);
1286+ Y.lp.client.wrap_resource_on_success(
1287+ null, this.response, this.args);
1288+ this._checkNotificationNode('.debug.message', 'A debug');
1289+ this._checkNotificationNode('.informational.message', 'An info');
1290+
1291+ // Any subsequent request should preserve existing notifications.
1292+ var new_notifications = '[ [30, "A warning"], [40, "An error"] ]';
1293+ this.response.setResponseHeader(
1294+ 'X-Lazr-Notifications', new_notifications);
1295+ Y.lp.client.wrap_resource_on_success(
1296+ null, this.response, this.args);
1297+ this._checkNotificationNode('.debug.message', 'A debug');
1298+ this._checkNotificationNode('.informational.message', 'An info');
1299+ this._checkNotificationNode('.warning.message', 'A warning');
1300+ this._checkNotificationNode('.error.message', 'An error');
1301+ },
1302+
1303+ test_remove_notifications: function() {
1304+ // Make some notifications that will be removed.
1305+ var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1306+ this.response.setResponseHeader(
1307+ 'X-Lazr-Notifications', notifications);
1308+ Y.lp.client.wrap_resource_on_success(
1309+ null, this.response, this.args);
1310+
1311+ // If the notifications header is just the string "null", then the
1312+ // current notifications are removed.
1313+ this.response.setResponseHeader('X-Lazr-Notifications', "null");
1314+ Y.lp.client.wrap_resource_on_success(
1315+ null, this.response, this.args);
1316+ this._checkNoNotificationNode('.debug.message');
1317+ this._checkNoNotificationNode('.informational.message');
1318+ },
1319+
1320+ test_notifications_not_removed: function() {
1321+ // Make some notifications that will be removed.
1322+ var notifications = '[ [10, "A debug"], [20, "An info"] ]';
1323+ this.response.setResponseHeader(
1324+ 'X-Lazr-Notifications', notifications);
1325+ Y.lp.client.wrap_resource_on_success(
1326+ null, this.response, this.args);
1327+
1328+ // If the response does not include a notifications header, then
1329+ // any pre-existing notifiactions are not removed.
1330+ this.response.setResponseHeader('X-Lazr-Notifications', null);
1331+ Y.lp.client.wrap_resource_on_success(
1332+ null, this.response, this.args);
1333+ this._checkNotificationNode('.debug.message', 'A debug');
1334+ this._checkNotificationNode('.informational.message', 'An info');
1335+ }
1336+ }));
1337+
1338+ tests.suite.add(new Y.Test.Case({
1339+ name: "lp.client.forms",
1340+
1341+ setUp: function() {
1342+ var form = Y.one("#testform");
1343+ this.error_handler = new Y.lp.client.FormErrorHandler({
1344+ form: form
1345+ });
1346+ },
1347+
1348+ tearDown: function() {
1349+ this.error_handler.clearFormErrors();
1350+ },
1351+
1352+ test_form_error_handler_ignores_other_responses: function() {
1353+ // Only XHR responses not containing validation data are ignored.
1354+ var result = this.error_handler.handleError(0, {
1355+ status: 400,
1356+ statusText: 'Not Validation'
1357+ });
1358+ Assert.isFalse(result);
1359+ },
1360+
1361+ test_form_error_handler_handles_responses: function() {
1362+ // XHR responses containing validation data are processed.
1363+ var error_data = {
1364+ 'error_summary': 'Some errors',
1365+ 'form_wide_errors': ['Form error'],
1366+ errors: {'field.test': 'Field error'}
1367+ };
1368+ var result = this.error_handler.handleError(0, {
1369+ status: 400,
1370+ statusText: 'Validation',
1371+ responseText: Y.JSON.stringify(error_data)
1372+ });
1373+ Assert.isTrue(result);
1374+ this._assert_error_rendering();
1375+ },
1376+
1377+ _assert_error_rendering: function() {
1378+ var label = Y.one('label[for="field.test"]');
1379+ var field_error = label.next('div').next('.message');
1380+ Assert.isTrue(Y.one('#field_div').hasClass('error'),
1381+ 'Field div has class error');
1382+ Assert.areEqual('Field error', field_error.getContent());
1383+ Y.all('.error.message').each(function(error_node) {
1384+ var error_message = error_node.getContent();
1385+ Assert.isTrue(
1386+ error_message === '<p>Form error</p>' ||
1387+ error_message === 'Some errors',
1388+ 'Each error message has the correct content.');
1389+ });
1390+ },
1391+
1392+ test_form_error_handler_renders_errors: function() {
1393+ // Form errors are rendered correctly.
1394+ this.error_handler.handleFormValidationError(
1395+ "Some errors", ["Form error"],
1396+ {'field.test': "Field error"});
1397+ this._assert_error_rendering();
1398+ }
1399+ }));
1400+}, '0.1', {
1401+ requires: ['test', 'lp.testing.helpers', 'console', 'lp.client',
1402+ 'lp.testing.mockio', 'lp.client', 'escape']
1403 });
1404
1405=== modified file 'lib/lp/app/javascript/tests/test_lp_client_integration.js'
1406--- lib/lp/app/javascript/tests/test_lp_client_integration.js 2012-01-10 14:24:19 +0000
1407+++ lib/lp/app/javascript/tests/test_lp_client_integration.js 2012-07-24 16:58:23 +0000
1408@@ -1,4 +1,5 @@
1409 YUI({
1410+
1411 base: '/+icing/yui/',
1412 filter: 'raw', combine: false, fetchCSS: false
1413 }).use('test',
1414@@ -127,20 +128,6 @@
1415 Y.Assert.areSame(1, config.result.total_size);
1416 },
1417
1418- test_named_get_uri: function() {
1419- var data = serverfixture.setup(
1420- this, 'create_product_with_milestone_and_login');
1421- var client = new Y.lp.client.Launchpad({sync: true});
1422- var config = makeTestConfig({parameters: {name: data.milestone.name}});
1423- var product = new Y.lp.client.Entry(
1424- client, data.product, data.product.self_link);
1425- product.named_get('getMilestone', config);
1426- Y.Assert.isTrue(config.successful, 'Getting milestone failed');
1427- var milestone = config.result;
1428- Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
1429- Y.Assert.areSame(data.milestone_self_link, milestone.lp_original_uri);
1430- },
1431-
1432 test_named_post_integration: function() {
1433 var data = serverfixture.setup(this, 'create_bug_and_login');
1434 var client = new Y.lp.client.Launchpad({sync: true});
1435@@ -321,7 +308,7 @@
1436 var team = config.result;
1437 Y.Assert.isInstanceOf(Y.lp.client.Entry, team);
1438 Y.Assert.areEqual('My lpclient team', team.get('display_name'));
1439- Y.Assert.isTrue(/\~newlpclientteam$/.test(team.lp_original_uri));
1440+ Y.Assert.isTrue(/\~newlpclientteam$/.test(team.uri));
1441 },
1442
1443 test_collection_paged_named_get: function() {
1444
1445=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
1446--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-07-20 22:48:23 +0000
1447+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-07-24 16:58:23 +0000
1448@@ -234,7 +234,7 @@
1449 Y.one('body').appendChild(this.content_node);
1450
1451 this.bug_filter = {
1452- lp_original_uri:
1453+ uri:
1454 '/api/devel/firefox/+subscription/mark/+filter/28'
1455 };
1456 this.form_data = {
1457
1458=== modified file 'lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js'
1459--- lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js 2012-07-05 14:28:57 +0000
1460+++ lib/lp/soyuz/javascript/lp_dynamic_dom_updater.js 2012-07-24 16:58:23 +0000
1461@@ -266,8 +266,8 @@
1462 }
1463
1464 // Finally, call the LP api method as required...
1465- if (uri){
1466- if (api_method_name){
1467+ if (uri) {
1468+ if (api_method_name) {
1469 this.get("lp_client").named_get(uri,
1470 api_method_name, this._lp_api_config);
1471 }