Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
4 /**
5 * This file tests the async history API exposed by mozIAsyncHistory.
6 */
8 ////////////////////////////////////////////////////////////////////////////////
9 //// Globals
11 const TEST_DOMAIN = "http://mozilla.org/";
12 const URI_VISIT_SAVED = "uri-visit-saved";
13 const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000;
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 }
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 });
50 return deferred.promise;
51 }
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 };
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 };
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 }
148 ////////////////////////////////////////////////////////////////////////////////
149 //// Test Functions
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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;
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 };
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;
276 for (let guid = 1; guid >= 0; guid--) {
277 place.guid = guid ? TEST_GUID : undefined;
279 for (let visits = 1; visits >= 0; visits--) {
280 place.visits = visits ? [] : undefined;
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 }
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 }
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 }
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 }
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 }
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 });
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 }
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 };
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));
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 };
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));
445 yield promiseAsyncUpdates();
446 }
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));
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));
468 // Check to make sure we do not visit the invalid referrer.
469 do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI));
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();
482 yield promiseAsyncUpdates();
483 }
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));
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));
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();
515 yield promiseAsyncUpdates();
516 }
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 };
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 }
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));
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 };
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));
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();
575 yield promiseAsyncUpdates();
576 }
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 };
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));
595 let placeId = placeInfo.placeId;
596 do_check_neq(placeId, 0);
598 let badPlace = {
599 uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"),
600 visits: [
601 new VisitInfo(),
602 ],
603 placeId: placeId,
604 };
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];
613 do_check_neq(placeInfo.placeId, placeId);
614 do_check_true(yield promiseIsURIVisited(badPlace.uri));
616 yield promiseAsyncUpdates();
617 }
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));
641 const EXPECTED_COUNT_SUCCESS = 2;
642 const EXPECTED_COUNT_FAILURE = 1;
643 let callbackCountSuccess = 0;
644 let callbackCountFailure = 0;
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 }
656 do_check_eq(callbackCountSuccess, EXPECTED_COUNT_SUCCESS);
657 do_check_eq(callbackCountFailure, EXPECTED_COUNT_FAILURE);
658 yield promiseAsyncUpdates();
659 }
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));
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));
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);
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);
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);
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);
714 // Check mozIVisitInfo properties.
715 do_check_true(visit.visitId > 0);
716 }
718 // If we have had all of our callbacks, continue running tests.
719 if (++callbackCount == place.visits.length) {
720 yield promiseAsyncUpdates();
721 }
722 }
723 }
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 }
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);
756 // Note that TRANSITION_EMBED should not be in the database.
757 const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1;
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();
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();
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();
802 // If we have had all of our callbacks, continue running tests.
803 if (++callbackCount == places.length) {
804 yield promiseAsyncUpdates();
805 }
806 }
807 }
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));
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 }
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));
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];
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));
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();
881 yield promiseAsyncUpdates();
882 }
883 }
884 }
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));
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);
910 yield promiseAsyncUpdates();
911 }
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));
925 let placesResult = yield promiseUpdatePlaces(place);
926 if (placesResult.errors.length > 0) {
927 do_throw("Unexpected error.");
928 }
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);
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);
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);
957 yield promiseAsyncUpdates();
958 }
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));
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);
986 yield promiseAsyncUpdates();
987 }
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));
1001 let silentObserver =
1002 new TitleChangedObserver(place.uri, "DO NOT WANT", function() {
1003 do_throw("unexpected callback!");
1004 });
1006 PlacesUtils.history.addObserver(silentObserver, false);
1007 let placesResult = yield promiseUpdatePlaces(place);
1008 if (placesResult.errors.length > 0) {
1009 do_throw("Unexpected error.");
1010 }
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 });
1037 PlacesUtils.history.addObserver(observer, false);
1038 PlacesUtils.asyncHistory.updatePlaces(aPlace);
1039 return deferred.promise;
1040 }
1042 yield promiseTitleChangedObserver(place);
1043 yield promiseAsyncUpdates();
1044 }
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));
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);
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));
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 }
1091 yield promiseVisitObserver(place);
1092 yield promiseAsyncUpdates();
1093 }
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 });
1122 PlacesUtils.asyncHistory.updatePlaces(places, {});
1123 yield promiseAsyncUpdates();
1124 }
1126 ////////////////////////////////////////////////////////////////////////////////
1127 //// Test Runner
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);
1159 function run_test()
1160 {
1161 run_next_test();
1162 }