|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 /** |
|
5 * This file tests the async history API exposed by mozIAsyncHistory. |
|
6 */ |
|
7 |
|
8 //////////////////////////////////////////////////////////////////////////////// |
|
9 //// Globals |
|
10 |
|
11 const TEST_DOMAIN = "http://mozilla.org/"; |
|
12 const URI_VISIT_SAVED = "uri-visit-saved"; |
|
13 const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000; |
|
14 |
|
15 //////////////////////////////////////////////////////////////////////////////// |
|
16 //// Helpers |
|
17 /** |
|
18 * Object that represents a mozIVisitInfo object. |
|
19 * |
|
20 * @param [optional] aTransitionType |
|
21 * The transition type of the visit. Defaults to TRANSITION_LINK if not |
|
22 * provided. |
|
23 * @param [optional] aVisitTime |
|
24 * The time of the visit. Defaults to now if not provided. |
|
25 */ |
|
26 function VisitInfo(aTransitionType, |
|
27 aVisitTime) |
|
28 { |
|
29 this.transitionType = |
|
30 aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; |
|
31 this.visitDate = aVisitTime || Date.now() * 1000; |
|
32 } |
|
33 |
|
34 function promiseUpdatePlaces(aPlaces) { |
|
35 let deferred = Promise.defer(); |
|
36 PlacesUtils.asyncHistory.updatePlaces(aPlaces, { |
|
37 _errors: [], |
|
38 _results: [], |
|
39 handleError: function handleError(aResultCode, aPlace) { |
|
40 this._errors.push({ resultCode: aResultCode, info: aPlace}); |
|
41 }, |
|
42 handleResult: function handleResult(aPlace) { |
|
43 this._results.push(aPlace); |
|
44 }, |
|
45 handleCompletion: function handleCompletion() { |
|
46 deferred.resolve({ errors: this._errors, results: this._results }); |
|
47 } |
|
48 }); |
|
49 |
|
50 return deferred.promise; |
|
51 } |
|
52 |
|
53 /** |
|
54 * Listens for a title change notification, and calls aCallback when it gets it. |
|
55 * |
|
56 * @param aURI |
|
57 * The URI of the page we expect a notification for. |
|
58 * @param aExpectedTitle |
|
59 * The expected title of the URI we expect a notification for. |
|
60 * @param aCallback |
|
61 * The method to call when we have gotten the proper notification about |
|
62 * the title changing. |
|
63 */ |
|
64 function TitleChangedObserver(aURI, |
|
65 aExpectedTitle, |
|
66 aCallback) |
|
67 { |
|
68 this.uri = aURI; |
|
69 this.expectedTitle = aExpectedTitle; |
|
70 this.callback = aCallback; |
|
71 } |
|
72 TitleChangedObserver.prototype = { |
|
73 __proto__: NavHistoryObserver.prototype, |
|
74 onTitleChanged: function(aURI, |
|
75 aTitle, |
|
76 aGUID) |
|
77 { |
|
78 do_log_info("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); |
|
79 if (!this.uri.equals(aURI)) { |
|
80 return; |
|
81 } |
|
82 do_check_eq(aTitle, this.expectedTitle); |
|
83 do_check_guid_for_uri(aURI, aGUID); |
|
84 this.callback(); |
|
85 }, |
|
86 }; |
|
87 |
|
88 /** |
|
89 * Listens for a visit notification, and calls aCallback when it gets it. |
|
90 * |
|
91 * @param aURI |
|
92 * The URI of the page we expect a notification for. |
|
93 * @param aCallback |
|
94 * The method to call when we have gotten the proper notification about |
|
95 * being visited. |
|
96 */ |
|
97 function VisitObserver(aURI, |
|
98 aGUID, |
|
99 aCallback) |
|
100 { |
|
101 this.uri = aURI; |
|
102 this.guid = aGUID; |
|
103 this.callback = aCallback; |
|
104 } |
|
105 VisitObserver.prototype = { |
|
106 __proto__: NavHistoryObserver.prototype, |
|
107 onVisit: function(aURI, |
|
108 aVisitId, |
|
109 aTime, |
|
110 aSessionId, |
|
111 aReferringId, |
|
112 aTransitionType, |
|
113 aGUID) |
|
114 { |
|
115 do_log_info("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + |
|
116 ", " + aSessionId + ", " + aReferringId + ", " + |
|
117 aTransitionType + ", " + aGUID + ")"); |
|
118 if (!this.uri.equals(aURI) || this.guid != aGUID) { |
|
119 return; |
|
120 } |
|
121 this.callback(aTime, aTransitionType); |
|
122 }, |
|
123 }; |
|
124 |
|
125 /** |
|
126 * Tests that a title was set properly in the database. |
|
127 * |
|
128 * @param aURI |
|
129 * The uri to check. |
|
130 * @param aTitle |
|
131 * The expected title in the database. |
|
132 */ |
|
133 function do_check_title_for_uri(aURI, |
|
134 aTitle) |
|
135 { |
|
136 let stack = Components.stack.caller; |
|
137 let stmt = DBConn().createStatement( |
|
138 "SELECT title " + |
|
139 "FROM moz_places " + |
|
140 "WHERE url = :url " |
|
141 ); |
|
142 stmt.params.url = aURI.spec; |
|
143 do_check_true(stmt.executeStep(), stack); |
|
144 do_check_eq(stmt.row.title, aTitle, stack); |
|
145 stmt.finalize(); |
|
146 } |
|
147 |
|
148 //////////////////////////////////////////////////////////////////////////////// |
|
149 //// Test Functions |
|
150 |
|
151 function test_interface_exists() |
|
152 { |
|
153 let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); |
|
154 do_check_true(history instanceof Ci.mozIAsyncHistory); |
|
155 } |
|
156 |
|
157 function test_invalid_uri_throws() |
|
158 { |
|
159 // First, test passing in nothing. |
|
160 let place = { |
|
161 visits: [ |
|
162 new VisitInfo(), |
|
163 ], |
|
164 }; |
|
165 try { |
|
166 yield promiseUpdatePlaces(place); |
|
167 do_throw("Should have thrown!"); |
|
168 } |
|
169 catch (e) { |
|
170 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
171 } |
|
172 |
|
173 // Now, test other bogus things. |
|
174 const TEST_VALUES = [ |
|
175 null, |
|
176 undefined, |
|
177 {}, |
|
178 [], |
|
179 TEST_DOMAIN + "test_invalid_id_throws", |
|
180 ]; |
|
181 for (let i = 0; i < TEST_VALUES.length; i++) { |
|
182 place.uri = TEST_VALUES[i]; |
|
183 try { |
|
184 yield promiseUpdatePlaces(place); |
|
185 do_throw("Should have thrown!"); |
|
186 } |
|
187 catch (e) { |
|
188 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
189 } |
|
190 } |
|
191 } |
|
192 |
|
193 function test_invalid_places_throws() |
|
194 { |
|
195 // First, test passing in nothing. |
|
196 try { |
|
197 PlacesUtils.asyncHistory.updatePlaces(); |
|
198 do_throw("Should have thrown!"); |
|
199 } |
|
200 catch (e) { |
|
201 do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); |
|
202 } |
|
203 |
|
204 // Now, test other bogus things. |
|
205 const TEST_VALUES = [ |
|
206 null, |
|
207 undefined, |
|
208 {}, |
|
209 [], |
|
210 "", |
|
211 ]; |
|
212 for (let i = 0; i < TEST_VALUES.length; i++) { |
|
213 let value = TEST_VALUES[i]; |
|
214 try { |
|
215 yield promiseUpdatePlaces(value); |
|
216 do_throw("Should have thrown!"); |
|
217 } |
|
218 catch (e) { |
|
219 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
220 } |
|
221 } |
|
222 } |
|
223 |
|
224 function test_invalid_guid_throws() |
|
225 { |
|
226 // First check invalid length guid. |
|
227 let place = { |
|
228 guid: "BAD_GUID", |
|
229 uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"), |
|
230 visits: [ |
|
231 new VisitInfo(), |
|
232 ], |
|
233 }; |
|
234 try { |
|
235 yield promiseUpdatePlaces(place); |
|
236 do_throw("Should have thrown!"); |
|
237 } |
|
238 catch (e) { |
|
239 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
240 } |
|
241 |
|
242 // Now check invalid character guid. |
|
243 place.guid = "__BADGUID+__"; |
|
244 do_check_eq(place.guid.length, 12); |
|
245 try { |
|
246 yield promiseUpdatePlaces(place); |
|
247 do_throw("Should have thrown!"); |
|
248 } |
|
249 catch (e) { |
|
250 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
251 } |
|
252 } |
|
253 |
|
254 function test_no_visits_throws() |
|
255 { |
|
256 const TEST_URI = |
|
257 NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws"); |
|
258 const TEST_GUID = "_RANDOMGUID_"; |
|
259 const TEST_PLACEID = 2; |
|
260 |
|
261 let log_test_conditions = function(aPlace) { |
|
262 let str = "Testing place with " + |
|
263 (aPlace.uri ? "uri" : "no uri") + ", " + |
|
264 (aPlace.guid ? "guid" : "no guid") + ", " + |
|
265 (aPlace.visits ? "visits array" : "no visits array"); |
|
266 do_log_info(str); |
|
267 }; |
|
268 |
|
269 // Loop through every possible case. Note that we don't actually care about |
|
270 // the case where we have no uri, place id, or guid (covered by another test), |
|
271 // but it is easier to just make sure it too throws than to exclude it. |
|
272 let place = { }; |
|
273 for (let uri = 1; uri >= 0; uri--) { |
|
274 place.uri = uri ? TEST_URI : undefined; |
|
275 |
|
276 for (let guid = 1; guid >= 0; guid--) { |
|
277 place.guid = guid ? TEST_GUID : undefined; |
|
278 |
|
279 for (let visits = 1; visits >= 0; visits--) { |
|
280 place.visits = visits ? [] : undefined; |
|
281 |
|
282 log_test_conditions(place); |
|
283 try { |
|
284 yield promiseUpdatePlaces(place); |
|
285 do_throw("Should have thrown!"); |
|
286 } |
|
287 catch (e) { |
|
288 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
289 } |
|
290 } |
|
291 } |
|
292 } |
|
293 } |
|
294 |
|
295 function test_add_visit_no_date_throws() |
|
296 { |
|
297 let place = { |
|
298 uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"), |
|
299 visits: [ |
|
300 new VisitInfo(), |
|
301 ], |
|
302 }; |
|
303 delete place.visits[0].visitDate; |
|
304 try { |
|
305 yield promiseUpdatePlaces(place); |
|
306 do_throw("Should have thrown!"); |
|
307 } |
|
308 catch (e) { |
|
309 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
310 } |
|
311 } |
|
312 |
|
313 function test_add_visit_no_transitionType_throws() |
|
314 { |
|
315 let place = { |
|
316 uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"), |
|
317 visits: [ |
|
318 new VisitInfo(), |
|
319 ], |
|
320 }; |
|
321 delete place.visits[0].transitionType; |
|
322 try { |
|
323 yield promiseUpdatePlaces(place); |
|
324 do_throw("Should have thrown!"); |
|
325 } |
|
326 catch (e) { |
|
327 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
328 } |
|
329 } |
|
330 |
|
331 function test_add_visit_invalid_transitionType_throws() |
|
332 { |
|
333 // First, test something that has a transition type lower than the first one. |
|
334 let place = { |
|
335 uri: NetUtil.newURI(TEST_DOMAIN + |
|
336 "test_add_visit_invalid_transitionType_throws"), |
|
337 visits: [ |
|
338 new VisitInfo(TRANSITION_LINK - 1), |
|
339 ], |
|
340 }; |
|
341 try { |
|
342 yield promiseUpdatePlaces(place); |
|
343 do_throw("Should have thrown!"); |
|
344 } |
|
345 catch (e) { |
|
346 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
347 } |
|
348 |
|
349 // Now, test something that has a transition type greater than the last one. |
|
350 place.visits[0] = new VisitInfo(TRANSITION_FRAMED_LINK + 1); |
|
351 try { |
|
352 yield promiseUpdatePlaces(place); |
|
353 do_throw("Should have thrown!"); |
|
354 } |
|
355 catch (e) { |
|
356 do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); |
|
357 } |
|
358 } |
|
359 |
|
360 function test_non_addable_uri_errors() |
|
361 { |
|
362 // Array of protocols that nsINavHistoryService::canAddURI returns false for. |
|
363 const URLS = [ |
|
364 "about:config", |
|
365 "imap://cyrus.andrew.cmu.edu/archive.imap", |
|
366 "news://new.mozilla.org/mozilla.dev.apps.firefox", |
|
367 "mailbox:Inbox", |
|
368 "moz-anno:favicon:http://mozilla.org/made-up-favicon", |
|
369 "view-source:http://mozilla.org", |
|
370 "chrome://browser/content/browser.xul", |
|
371 "resource://gre-resources/hiddenWindow.html", |
|
372 "data:,Hello%2C%20World!", |
|
373 "wyciwyg:/0/http://mozilla.org", |
|
374 "javascript:alert('hello wolrd!');", |
|
375 "blob:foo", |
|
376 ]; |
|
377 let places = []; |
|
378 URLS.forEach(function(url) { |
|
379 try { |
|
380 let place = { |
|
381 uri: NetUtil.newURI(url), |
|
382 title: "test for " + url, |
|
383 visits: [ |
|
384 new VisitInfo(), |
|
385 ], |
|
386 }; |
|
387 places.push(place); |
|
388 } |
|
389 catch (e if e.result === Cr.NS_ERROR_FAILURE) { |
|
390 // NetUtil.newURI() can throw if e.g. our app knows about imap:// |
|
391 // but the account is not set up and so the URL is invalid for us. |
|
392 // Note this in the log but ignore as it's not the subject of this test. |
|
393 do_log_info("Could not construct URI for '" + url + "'; ignoring"); |
|
394 } |
|
395 }); |
|
396 |
|
397 let placesResult = yield promiseUpdatePlaces(places); |
|
398 if (placesResult.results.length > 0) { |
|
399 do_throw("Unexpected success."); |
|
400 } |
|
401 for (let place of placesResult.errors) { |
|
402 do_log_info("Checking '" + place.info.uri.spec + "'"); |
|
403 do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG); |
|
404 do_check_false(yield promiseIsURIVisited(place.info.uri)); |
|
405 } |
|
406 yield promiseAsyncUpdates(); |
|
407 } |
|
408 |
|
409 function test_duplicate_guid_errors() |
|
410 { |
|
411 // This test ensures that trying to add a visit, with a guid already found in |
|
412 // another visit, fails. |
|
413 let place = { |
|
414 uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), |
|
415 visits: [ |
|
416 new VisitInfo(), |
|
417 ], |
|
418 }; |
|
419 |
|
420 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
421 let placesResult = yield promiseUpdatePlaces(place); |
|
422 if (placesResult.errors.length > 0) { |
|
423 do_throw("Unexpected error."); |
|
424 } |
|
425 let placeInfo = placesResult.results[0]; |
|
426 do_check_true(yield promiseIsURIVisited(placeInfo.uri)); |
|
427 |
|
428 let badPlace = { |
|
429 uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), |
|
430 visits: [ |
|
431 new VisitInfo(), |
|
432 ], |
|
433 guid: placeInfo.guid, |
|
434 }; |
|
435 |
|
436 do_check_false(yield promiseIsURIVisited(badPlace.uri)); |
|
437 placesResult = yield promiseUpdatePlaces(badPlace); |
|
438 if (placesResult.results.length > 0) { |
|
439 do_throw("Unexpected success."); |
|
440 } |
|
441 let badPlaceInfo = placesResult.errors[0]; |
|
442 do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT); |
|
443 do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri)); |
|
444 |
|
445 yield promiseAsyncUpdates(); |
|
446 } |
|
447 |
|
448 function test_invalid_referrerURI_ignored() |
|
449 { |
|
450 let place = { |
|
451 uri: NetUtil.newURI(TEST_DOMAIN + |
|
452 "test_invalid_referrerURI_ignored"), |
|
453 visits: [ |
|
454 new VisitInfo(), |
|
455 ], |
|
456 }; |
|
457 place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI"); |
|
458 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
459 do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); |
|
460 |
|
461 let placesResult = yield promiseUpdatePlaces(place); |
|
462 if (placesResult.errors.length > 0) { |
|
463 do_throw("Unexpected error."); |
|
464 } |
|
465 let placeInfo = placesResult.results[0]; |
|
466 do_check_true(yield promiseIsURIVisited(placeInfo.uri)); |
|
467 |
|
468 // Check to make sure we do not visit the invalid referrer. |
|
469 do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); |
|
470 |
|
471 // Check to make sure from_visit is zero in database. |
|
472 let stmt = DBConn().createStatement( |
|
473 "SELECT from_visit " + |
|
474 "FROM moz_historyvisits " + |
|
475 "WHERE id = :visit_id" |
|
476 ); |
|
477 stmt.params.visit_id = placeInfo.visits[0].visitId; |
|
478 do_check_true(stmt.executeStep()); |
|
479 do_check_eq(stmt.row.from_visit, 0); |
|
480 stmt.finalize(); |
|
481 |
|
482 yield promiseAsyncUpdates(); |
|
483 } |
|
484 |
|
485 function test_nonnsIURI_referrerURI_ignored() |
|
486 { |
|
487 let place = { |
|
488 uri: NetUtil.newURI(TEST_DOMAIN + |
|
489 "test_nonnsIURI_referrerURI_ignored"), |
|
490 visits: [ |
|
491 new VisitInfo(), |
|
492 ], |
|
493 }; |
|
494 place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI"; |
|
495 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
496 |
|
497 let placesResult = yield promiseUpdatePlaces(place); |
|
498 if (placesResult.errors.length > 0) { |
|
499 do_throw("Unexpected error."); |
|
500 } |
|
501 let placeInfo = placesResult.results[0]; |
|
502 do_check_true(yield promiseIsURIVisited(placeInfo.uri)); |
|
503 |
|
504 // Check to make sure from_visit is zero in database. |
|
505 let stmt = DBConn().createStatement( |
|
506 "SELECT from_visit " + |
|
507 "FROM moz_historyvisits " + |
|
508 "WHERE id = :visit_id" |
|
509 ); |
|
510 stmt.params.visit_id = placeInfo.visits[0].visitId; |
|
511 do_check_true(stmt.executeStep()); |
|
512 do_check_eq(stmt.row.from_visit, 0); |
|
513 stmt.finalize(); |
|
514 |
|
515 yield promiseAsyncUpdates(); |
|
516 } |
|
517 |
|
518 function test_old_referrer_ignored() |
|
519 { |
|
520 // This tests that a referrer for a visit which is not recent (specifically, |
|
521 // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by |
|
522 // updatePlaces. |
|
523 let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1); |
|
524 let referrerPlace = { |
|
525 uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"), |
|
526 visits: [ |
|
527 new VisitInfo(TRANSITION_LINK, oldTime), |
|
528 ], |
|
529 }; |
|
530 |
|
531 // First we must add our referrer to the history so that it is not ignored |
|
532 // as being invalid. |
|
533 do_check_false(yield promiseIsURIVisited(referrerPlace.uri)); |
|
534 let placesResult = yield promiseUpdatePlaces(referrerPlace); |
|
535 if (placesResult.errors.length > 0) { |
|
536 do_throw("Unexpected error."); |
|
537 } |
|
538 |
|
539 // Now that the referrer is added, we can add a page with a valid |
|
540 // referrer to determine if the recency of the referrer is taken into |
|
541 // account. |
|
542 do_check_true(yield promiseIsURIVisited(referrerPlace.uri)); |
|
543 |
|
544 let visitInfo = new VisitInfo(); |
|
545 visitInfo.referrerURI = referrerPlace.uri; |
|
546 let place = { |
|
547 uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"), |
|
548 visits: [ |
|
549 visitInfo, |
|
550 ], |
|
551 }; |
|
552 |
|
553 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
554 placesResult = yield promiseUpdatePlaces(place); |
|
555 if (placesResult.errors.length > 0) { |
|
556 do_throw("Unexpected error."); |
|
557 } |
|
558 let placeInfo = placesResult.results[0]; |
|
559 do_check_true(yield promiseIsURIVisited(place.uri)); |
|
560 |
|
561 // Though the visit will not contain the referrer, we must examine the |
|
562 // database to be sure. |
|
563 do_check_eq(placeInfo.visits[0].referrerURI, null); |
|
564 let stmt = DBConn().createStatement( |
|
565 "SELECT COUNT(1) AS count " + |
|
566 "FROM moz_historyvisits " + |
|
567 "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + |
|
568 "AND from_visit = 0 " |
|
569 ); |
|
570 stmt.params.page_url = place.uri.spec; |
|
571 do_check_true(stmt.executeStep()); |
|
572 do_check_eq(stmt.row.count, 1); |
|
573 stmt.finalize(); |
|
574 |
|
575 yield promiseAsyncUpdates(); |
|
576 } |
|
577 |
|
578 function test_place_id_ignored() |
|
579 { |
|
580 let place = { |
|
581 uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"), |
|
582 visits: [ |
|
583 new VisitInfo(), |
|
584 ], |
|
585 }; |
|
586 |
|
587 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
588 let placesResult = yield promiseUpdatePlaces(place); |
|
589 if (placesResult.errors.length > 0) { |
|
590 do_throw("Unexpected error."); |
|
591 } |
|
592 let placeInfo = placesResult.results[0]; |
|
593 do_check_true(yield promiseIsURIVisited(place.uri)); |
|
594 |
|
595 let placeId = placeInfo.placeId; |
|
596 do_check_neq(placeId, 0); |
|
597 |
|
598 let badPlace = { |
|
599 uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"), |
|
600 visits: [ |
|
601 new VisitInfo(), |
|
602 ], |
|
603 placeId: placeId, |
|
604 }; |
|
605 |
|
606 do_check_false(yield promiseIsURIVisited(badPlace.uri)); |
|
607 placesResult = yield promiseUpdatePlaces(badPlace); |
|
608 if (placesResult.errors.length > 0) { |
|
609 do_throw("Unexpected error."); |
|
610 } |
|
611 placeInfo = placesResult.results[0]; |
|
612 |
|
613 do_check_neq(placeInfo.placeId, placeId); |
|
614 do_check_true(yield promiseIsURIVisited(badPlace.uri)); |
|
615 |
|
616 yield promiseAsyncUpdates(); |
|
617 } |
|
618 |
|
619 function test_handleCompletion_called_when_complete() |
|
620 { |
|
621 // We test a normal visit, and embeded visit, and a uri that would fail |
|
622 // the canAddURI test to make sure that the notification happens after *all* |
|
623 // of them have had a callback. |
|
624 let places = [ |
|
625 { uri: NetUtil.newURI(TEST_DOMAIN + |
|
626 "test_handleCompletion_called_when_complete"), |
|
627 visits: [ |
|
628 new VisitInfo(), |
|
629 new VisitInfo(TRANSITION_EMBED), |
|
630 ], |
|
631 }, |
|
632 { uri: NetUtil.newURI("data:,Hello%2C%20World!"), |
|
633 visits: [ |
|
634 new VisitInfo(), |
|
635 ], |
|
636 }, |
|
637 ]; |
|
638 do_check_false(yield promiseIsURIVisited(places[0].uri)); |
|
639 do_check_false(yield promiseIsURIVisited(places[1].uri)); |
|
640 |
|
641 const EXPECTED_COUNT_SUCCESS = 2; |
|
642 const EXPECTED_COUNT_FAILURE = 1; |
|
643 let callbackCountSuccess = 0; |
|
644 let callbackCountFailure = 0; |
|
645 |
|
646 let placesResult = yield promiseUpdatePlaces(places); |
|
647 for (let place of placesResult.results) { |
|
648 let checker = PlacesUtils.history.canAddURI(place.uri) ? |
|
649 do_check_true : do_check_false; |
|
650 callbackCountSuccess++; |
|
651 } |
|
652 for (let error of placesResult.errors) { |
|
653 callbackCountFailure++; |
|
654 } |
|
655 |
|
656 do_check_eq(callbackCountSuccess, EXPECTED_COUNT_SUCCESS); |
|
657 do_check_eq(callbackCountFailure, EXPECTED_COUNT_FAILURE); |
|
658 yield promiseAsyncUpdates(); |
|
659 } |
|
660 |
|
661 function test_add_visit() |
|
662 { |
|
663 const VISIT_TIME = Date.now() * 1000; |
|
664 let place = { |
|
665 uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"), |
|
666 title: "test_add_visit title", |
|
667 visits: [], |
|
668 }; |
|
669 for (let transitionType = TRANSITION_LINK; |
|
670 transitionType <= TRANSITION_FRAMED_LINK; |
|
671 transitionType++) { |
|
672 place.visits.push(new VisitInfo(transitionType, VISIT_TIME)); |
|
673 } |
|
674 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
675 |
|
676 let callbackCount = 0; |
|
677 let placesResult = yield promiseUpdatePlaces(place); |
|
678 if (placesResult.errors.length > 0) { |
|
679 do_throw("Unexpected error."); |
|
680 } |
|
681 for (let placeInfo of placesResult.results) { |
|
682 do_check_true(yield promiseIsURIVisited(place.uri)); |
|
683 |
|
684 // Check mozIPlaceInfo properties. |
|
685 do_check_true(place.uri.equals(placeInfo.uri)); |
|
686 do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here! |
|
687 do_check_eq(placeInfo.title, place.title); |
|
688 |
|
689 // Check mozIVisitInfo properties. |
|
690 let visits = placeInfo.visits; |
|
691 do_check_eq(visits.length, 1); |
|
692 let visit = visits[0]; |
|
693 do_check_eq(visit.visitDate, VISIT_TIME); |
|
694 do_check_true(visit.transitionType >= TRANSITION_LINK && |
|
695 visit.transitionType <= TRANSITION_FRAMED_LINK); |
|
696 do_check_true(visit.referrerURI === null); |
|
697 |
|
698 // For TRANSITION_EMBED visits, many properties will always be zero or |
|
699 // undefined. |
|
700 if (visit.transitionType == TRANSITION_EMBED) { |
|
701 // Check mozIPlaceInfo properties. |
|
702 do_check_eq(placeInfo.placeId, 0, '//'); |
|
703 do_check_eq(placeInfo.guid, null); |
|
704 |
|
705 // Check mozIVisitInfo properties. |
|
706 do_check_eq(visit.visitId, 0); |
|
707 } |
|
708 // But they should be valid for non-embed visits. |
|
709 else { |
|
710 // Check mozIPlaceInfo properties. |
|
711 do_check_true(placeInfo.placeId > 0); |
|
712 do_check_valid_places_guid(placeInfo.guid); |
|
713 |
|
714 // Check mozIVisitInfo properties. |
|
715 do_check_true(visit.visitId > 0); |
|
716 } |
|
717 |
|
718 // If we have had all of our callbacks, continue running tests. |
|
719 if (++callbackCount == place.visits.length) { |
|
720 yield promiseAsyncUpdates(); |
|
721 } |
|
722 } |
|
723 } |
|
724 |
|
725 function test_properties_saved() |
|
726 { |
|
727 // Check each transition type to make sure it is saved properly. |
|
728 let places = []; |
|
729 for (let transitionType = TRANSITION_LINK; |
|
730 transitionType <= TRANSITION_FRAMED_LINK; |
|
731 transitionType++) { |
|
732 let place = { |
|
733 uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" + |
|
734 transitionType), |
|
735 title: "test_properties_saved test", |
|
736 visits: [ |
|
737 new VisitInfo(transitionType), |
|
738 ], |
|
739 }; |
|
740 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
741 places.push(place); |
|
742 } |
|
743 |
|
744 let callbackCount = 0; |
|
745 let placesResult = yield promiseUpdatePlaces(places); |
|
746 if (placesResult.errors.length > 0) { |
|
747 do_throw("Unexpected error."); |
|
748 } |
|
749 for (let placeInfo of placesResult.results) { |
|
750 let uri = placeInfo.uri; |
|
751 do_check_true(yield promiseIsURIVisited(uri)); |
|
752 let visit = placeInfo.visits[0]; |
|
753 print("TEST-INFO | test_properties_saved | updatePlaces callback for " + |
|
754 "transition type " + visit.transitionType); |
|
755 |
|
756 // Note that TRANSITION_EMBED should not be in the database. |
|
757 const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1; |
|
758 |
|
759 // mozIVisitInfo::date |
|
760 let stmt = DBConn().createStatement( |
|
761 "SELECT COUNT(1) AS count " + |
|
762 "FROM moz_places h " + |
|
763 "JOIN moz_historyvisits v " + |
|
764 "ON h.id = v.place_id " + |
|
765 "WHERE h.url = :page_url " + |
|
766 "AND v.visit_date = :visit_date " |
|
767 ); |
|
768 stmt.params.page_url = uri.spec; |
|
769 stmt.params.visit_date = visit.visitDate; |
|
770 do_check_true(stmt.executeStep()); |
|
771 do_check_eq(stmt.row.count, EXPECTED_COUNT); |
|
772 stmt.finalize(); |
|
773 |
|
774 // mozIVisitInfo::transitionType |
|
775 stmt = DBConn().createStatement( |
|
776 "SELECT COUNT(1) AS count " + |
|
777 "FROM moz_places h " + |
|
778 "JOIN moz_historyvisits v " + |
|
779 "ON h.id = v.place_id " + |
|
780 "WHERE h.url = :page_url " + |
|
781 "AND v.visit_type = :transition_type " |
|
782 ); |
|
783 stmt.params.page_url = uri.spec; |
|
784 stmt.params.transition_type = visit.transitionType; |
|
785 do_check_true(stmt.executeStep()); |
|
786 do_check_eq(stmt.row.count, EXPECTED_COUNT); |
|
787 stmt.finalize(); |
|
788 |
|
789 // mozIPlaceInfo::title |
|
790 stmt = DBConn().createStatement( |
|
791 "SELECT COUNT(1) AS count " + |
|
792 "FROM moz_places h " + |
|
793 "WHERE h.url = :page_url " + |
|
794 "AND h.title = :title " |
|
795 ); |
|
796 stmt.params.page_url = uri.spec; |
|
797 stmt.params.title = placeInfo.title; |
|
798 do_check_true(stmt.executeStep()); |
|
799 do_check_eq(stmt.row.count, EXPECTED_COUNT); |
|
800 stmt.finalize(); |
|
801 |
|
802 // If we have had all of our callbacks, continue running tests. |
|
803 if (++callbackCount == places.length) { |
|
804 yield promiseAsyncUpdates(); |
|
805 } |
|
806 } |
|
807 } |
|
808 |
|
809 function test_guid_saved() |
|
810 { |
|
811 let place = { |
|
812 uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"), |
|
813 guid: "__TESTGUID__", |
|
814 visits: [ |
|
815 new VisitInfo(), |
|
816 ], |
|
817 }; |
|
818 do_check_valid_places_guid(place.guid); |
|
819 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
820 |
|
821 let placesResult = yield promiseUpdatePlaces(place); |
|
822 if (placesResult.errors.length > 0) { |
|
823 do_throw("Unexpected error."); |
|
824 } |
|
825 let placeInfo = placesResult.results[0]; |
|
826 let uri = placeInfo.uri; |
|
827 do_check_true(yield promiseIsURIVisited(uri)); |
|
828 do_check_eq(placeInfo.guid, place.guid); |
|
829 do_check_guid_for_uri(uri, place.guid); |
|
830 yield promiseAsyncUpdates(); |
|
831 } |
|
832 |
|
833 function test_referrer_saved() |
|
834 { |
|
835 let places = [ |
|
836 { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"), |
|
837 visits: [ |
|
838 new VisitInfo(), |
|
839 ], |
|
840 }, |
|
841 { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"), |
|
842 visits: [ |
|
843 new VisitInfo(), |
|
844 ], |
|
845 }, |
|
846 ]; |
|
847 places[1].visits[0].referrerURI = places[0].uri; |
|
848 do_check_false(yield promiseIsURIVisited(places[0].uri)); |
|
849 do_check_false(yield promiseIsURIVisited(places[1].uri)); |
|
850 |
|
851 let resultCount = 0; |
|
852 let placesResult = yield promiseUpdatePlaces(places); |
|
853 if (placesResult.errors.length > 0) { |
|
854 do_throw("Unexpected error."); |
|
855 } |
|
856 for (let placeInfo of placesResult.results) { |
|
857 let uri = placeInfo.uri; |
|
858 do_check_true(yield promiseIsURIVisited(uri)); |
|
859 let visit = placeInfo.visits[0]; |
|
860 |
|
861 // We need to insert all of our visits before we can test conditions. |
|
862 if (++resultCount == places.length) { |
|
863 do_check_true(places[0].uri.equals(visit.referrerURI)); |
|
864 |
|
865 let stmt = DBConn().createStatement( |
|
866 "SELECT COUNT(1) AS count " + |
|
867 "FROM moz_historyvisits " + |
|
868 "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) " + |
|
869 "AND from_visit = ( " + |
|
870 "SELECT id " + |
|
871 "FROM moz_historyvisits " + |
|
872 "WHERE place_id = (SELECT id FROM moz_places WHERE url = :referrer) " + |
|
873 ") " |
|
874 ); |
|
875 stmt.params.page_url = uri.spec; |
|
876 stmt.params.referrer = visit.referrerURI.spec; |
|
877 do_check_true(stmt.executeStep()); |
|
878 do_check_eq(stmt.row.count, 1); |
|
879 stmt.finalize(); |
|
880 |
|
881 yield promiseAsyncUpdates(); |
|
882 } |
|
883 } |
|
884 } |
|
885 |
|
886 function test_guid_change_saved() |
|
887 { |
|
888 // First, add a visit for it. |
|
889 let place = { |
|
890 uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"), |
|
891 visits: [ |
|
892 new VisitInfo(), |
|
893 ], |
|
894 }; |
|
895 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
896 |
|
897 let placesResult = yield promiseUpdatePlaces(place); |
|
898 if (placesResult.errors.length > 0) { |
|
899 do_throw("Unexpected error."); |
|
900 } |
|
901 // Then, change the guid with visits. |
|
902 place.guid = "_GUIDCHANGE_"; |
|
903 place.visits = [new VisitInfo()]; |
|
904 placesResult = yield promiseUpdatePlaces(place); |
|
905 if (placesResult.errors.length > 0) { |
|
906 do_throw("Unexpected error."); |
|
907 } |
|
908 do_check_guid_for_uri(place.uri, place.guid); |
|
909 |
|
910 yield promiseAsyncUpdates(); |
|
911 } |
|
912 |
|
913 function test_title_change_saved() |
|
914 { |
|
915 // First, add a visit for it. |
|
916 let place = { |
|
917 uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"), |
|
918 title: "original title", |
|
919 visits: [ |
|
920 new VisitInfo(), |
|
921 ], |
|
922 }; |
|
923 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
924 |
|
925 let placesResult = yield promiseUpdatePlaces(place); |
|
926 if (placesResult.errors.length > 0) { |
|
927 do_throw("Unexpected error."); |
|
928 } |
|
929 |
|
930 // Now, make sure the empty string clears the title. |
|
931 place.title = ""; |
|
932 place.visits = [new VisitInfo()]; |
|
933 placesResult = yield promiseUpdatePlaces(place); |
|
934 if (placesResult.errors.length > 0) { |
|
935 do_throw("Unexpected error."); |
|
936 } |
|
937 do_check_title_for_uri(place.uri, null); |
|
938 |
|
939 // Then, change the title with visits. |
|
940 place.title = "title change"; |
|
941 place.visits = [new VisitInfo()]; |
|
942 placesResult = yield promiseUpdatePlaces(place); |
|
943 if (placesResult.errors.length > 0) { |
|
944 do_throw("Unexpected error."); |
|
945 } |
|
946 do_check_title_for_uri(place.uri, place.title); |
|
947 |
|
948 // Lastly, check that the title is cleared if we set it to null. |
|
949 place.title = null; |
|
950 place.visits = [new VisitInfo()]; |
|
951 placesResult = yield promiseUpdatePlaces(place); |
|
952 if (placesResult.errors.length > 0) { |
|
953 do_throw("Unexpected error."); |
|
954 } |
|
955 do_check_title_for_uri(place.uri, place.title); |
|
956 |
|
957 yield promiseAsyncUpdates(); |
|
958 } |
|
959 |
|
960 function test_no_title_does_not_clear_title() |
|
961 { |
|
962 const TITLE = "test title"; |
|
963 // First, add a visit for it. |
|
964 let place = { |
|
965 uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"), |
|
966 title: TITLE, |
|
967 visits: [ |
|
968 new VisitInfo(), |
|
969 ], |
|
970 }; |
|
971 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
972 |
|
973 let placesResult = yield promiseUpdatePlaces(place); |
|
974 if (placesResult.errors.length > 0) { |
|
975 do_throw("Unexpected error."); |
|
976 } |
|
977 // Now, make sure that not specifying a title does not clear it. |
|
978 delete place.title; |
|
979 place.visits = [new VisitInfo()]; |
|
980 placesResult = yield promiseUpdatePlaces(place); |
|
981 if (placesResult.errors.length > 0) { |
|
982 do_throw("Unexpected error."); |
|
983 } |
|
984 do_check_title_for_uri(place.uri, TITLE); |
|
985 |
|
986 yield promiseAsyncUpdates(); |
|
987 } |
|
988 |
|
989 function test_title_change_notifies() |
|
990 { |
|
991 // There are three cases to test. The first case is to make sure we do not |
|
992 // get notified if we do not specify a title. |
|
993 let place = { |
|
994 uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"), |
|
995 visits: [ |
|
996 new VisitInfo(), |
|
997 ], |
|
998 }; |
|
999 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
1000 |
|
1001 let silentObserver = |
|
1002 new TitleChangedObserver(place.uri, "DO NOT WANT", function() { |
|
1003 do_throw("unexpected callback!"); |
|
1004 }); |
|
1005 |
|
1006 PlacesUtils.history.addObserver(silentObserver, false); |
|
1007 let placesResult = yield promiseUpdatePlaces(place); |
|
1008 if (placesResult.errors.length > 0) { |
|
1009 do_throw("Unexpected error."); |
|
1010 } |
|
1011 |
|
1012 // The second case to test is that we get the notification when we add |
|
1013 // it for the first time. The first case will fail before our callback if it |
|
1014 // is busted, so we can do this now. |
|
1015 place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title"); |
|
1016 place.title = "title 1"; |
|
1017 function promiseTitleChangedObserver(aPlace) { |
|
1018 let deferred = Promise.defer(); |
|
1019 let callbackCount = 0; |
|
1020 let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() { |
|
1021 switch (++callbackCount) { |
|
1022 case 1: |
|
1023 // The third case to test is to make sure we get a notification when |
|
1024 // we change an existing place. |
|
1025 observer.expectedTitle = place.title = "title 2"; |
|
1026 place.visits = [new VisitInfo()]; |
|
1027 PlacesUtils.asyncHistory.updatePlaces(place); |
|
1028 break; |
|
1029 case 2: |
|
1030 PlacesUtils.history.removeObserver(silentObserver); |
|
1031 PlacesUtils.history.removeObserver(observer); |
|
1032 deferred.resolve(); |
|
1033 break; |
|
1034 }; |
|
1035 }); |
|
1036 |
|
1037 PlacesUtils.history.addObserver(observer, false); |
|
1038 PlacesUtils.asyncHistory.updatePlaces(aPlace); |
|
1039 return deferred.promise; |
|
1040 } |
|
1041 |
|
1042 yield promiseTitleChangedObserver(place); |
|
1043 yield promiseAsyncUpdates(); |
|
1044 } |
|
1045 |
|
1046 function test_visit_notifies() |
|
1047 { |
|
1048 // There are two observers we need to see for each visit. One is an |
|
1049 // nsINavHistoryObserver and the other is the uri-visit-saved observer topic. |
|
1050 let place = { |
|
1051 guid: "abcdefghijkl", |
|
1052 uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"), |
|
1053 visits: [ |
|
1054 new VisitInfo(), |
|
1055 ], |
|
1056 }; |
|
1057 do_check_false(yield promiseIsURIVisited(place.uri)); |
|
1058 |
|
1059 function promiseVisitObserver(aPlace) { |
|
1060 let deferred = Promise.defer(); |
|
1061 let callbackCount = 0; |
|
1062 let finisher = function() { |
|
1063 if (++callbackCount == 2) { |
|
1064 deferred.resolve(); |
|
1065 } |
|
1066 } |
|
1067 let visitObserver = new VisitObserver(place.uri, place.guid, |
|
1068 function(aVisitDate, |
|
1069 aTransitionType) { |
|
1070 let visit = place.visits[0]; |
|
1071 do_check_eq(visit.visitDate, aVisitDate); |
|
1072 do_check_eq(visit.transitionType, aTransitionType); |
|
1073 |
|
1074 PlacesUtils.history.removeObserver(visitObserver); |
|
1075 finisher(); |
|
1076 }); |
|
1077 PlacesUtils.history.addObserver(visitObserver, false); |
|
1078 let observer = function(aSubject, aTopic, aData) { |
|
1079 do_log_info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); |
|
1080 do_check_true(aSubject instanceof Ci.nsIURI); |
|
1081 do_check_true(aSubject.equals(place.uri)); |
|
1082 |
|
1083 Services.obs.removeObserver(observer, URI_VISIT_SAVED); |
|
1084 finisher(); |
|
1085 }; |
|
1086 Services.obs.addObserver(observer, URI_VISIT_SAVED, false); |
|
1087 PlacesUtils.asyncHistory.updatePlaces(place); |
|
1088 return deferred.promise; |
|
1089 } |
|
1090 |
|
1091 yield promiseVisitObserver(place); |
|
1092 yield promiseAsyncUpdates(); |
|
1093 } |
|
1094 |
|
1095 // test with empty mozIVisitInfoCallback object |
|
1096 function test_callbacks_not_supplied() |
|
1097 { |
|
1098 const URLS = [ |
|
1099 "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI |
|
1100 "http://mozilla.org/" // valid URI |
|
1101 ]; |
|
1102 let places = []; |
|
1103 URLS.forEach(function(url) { |
|
1104 try { |
|
1105 let place = { |
|
1106 uri: NetUtil.newURI(url), |
|
1107 title: "test for " + url, |
|
1108 visits: [ |
|
1109 new VisitInfo(), |
|
1110 ], |
|
1111 }; |
|
1112 places.push(place); |
|
1113 } |
|
1114 catch (e if e.result === Cr.NS_ERROR_FAILURE) { |
|
1115 // NetUtil.newURI() can throw if e.g. our app knows about imap:// |
|
1116 // but the account is not set up and so the URL is invalid for us. |
|
1117 // Note this in the log but ignore as it's not the subject of this test. |
|
1118 do_log_info("Could not construct URI for '" + url + "'; ignoring"); |
|
1119 } |
|
1120 }); |
|
1121 |
|
1122 PlacesUtils.asyncHistory.updatePlaces(places, {}); |
|
1123 yield promiseAsyncUpdates(); |
|
1124 } |
|
1125 |
|
1126 //////////////////////////////////////////////////////////////////////////////// |
|
1127 //// Test Runner |
|
1128 |
|
1129 [ |
|
1130 test_interface_exists, |
|
1131 test_invalid_uri_throws, |
|
1132 test_invalid_places_throws, |
|
1133 test_invalid_guid_throws, |
|
1134 test_no_visits_throws, |
|
1135 test_add_visit_no_date_throws, |
|
1136 test_add_visit_no_transitionType_throws, |
|
1137 test_add_visit_invalid_transitionType_throws, |
|
1138 // Note: all asynchronous tests (every test below this point) should wait for |
|
1139 // async updates before calling run_next_test. |
|
1140 test_non_addable_uri_errors, |
|
1141 test_duplicate_guid_errors, |
|
1142 test_invalid_referrerURI_ignored, |
|
1143 test_nonnsIURI_referrerURI_ignored, |
|
1144 test_old_referrer_ignored, |
|
1145 test_place_id_ignored, |
|
1146 test_handleCompletion_called_when_complete, |
|
1147 test_add_visit, |
|
1148 test_properties_saved, |
|
1149 test_guid_saved, |
|
1150 test_referrer_saved, |
|
1151 test_guid_change_saved, |
|
1152 test_title_change_saved, |
|
1153 test_no_title_does_not_clear_title, |
|
1154 test_title_change_notifies, |
|
1155 test_visit_notifies, |
|
1156 test_callbacks_not_supplied, |
|
1157 ].forEach(add_task); |
|
1158 |
|
1159 function run_test() |
|
1160 { |
|
1161 run_next_test(); |
|
1162 } |