diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/places/tests | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/places/tests')
405 files changed, 49179 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/.eslintrc.js b/toolkit/components/places/tests/.eslintrc.js new file mode 100644 index 000000000..d5283c966 --- /dev/null +++ b/toolkit/components/places/tests/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js", + "../../../../testing/mochitest/chrome.eslintrc.js", + "../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/PlacesTestUtils.jsm b/toolkit/components/places/tests/PlacesTestUtils.jsm new file mode 100644 index 000000000..36e425cae --- /dev/null +++ b/toolkit/components/places/tests/PlacesTestUtils.jsm @@ -0,0 +1,163 @@ +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "PlacesTestUtils", +]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.importGlobalProperties(["URL"]); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +this.PlacesTestUtils = Object.freeze({ + /** + * Asynchronously adds visits to a page. + * + * @param aPlaceInfo + * Can be an nsIURI, in such a case a single LINK visit will be added. + * Otherwise can be an object describing the visit to add, or an array + * of these objects: + * { uri: nsIURI of the page, + * [optional] transition: one of the TRANSITION_* from nsINavHistoryService, + * [optional] title: title of the page, + * [optional] visitDate: visit date, either in microseconds from the epoch or as a date object + * [optional] referrer: nsIURI of the referrer for this visit + * } + * + * @return {Promise} + * @resolves When all visits have been added successfully. + * @rejects JavaScript exception. + */ + addVisits: Task.async(function* (placeInfo) { + let places = []; + let infos = []; + + if (placeInfo instanceof Ci.nsIURI || + placeInfo instanceof URL || + typeof placeInfo == "string") { + places.push({ uri: placeInfo }); + } + else if (Array.isArray(placeInfo)) { + places = places.concat(placeInfo); + } else if (typeof placeInfo == "object" && placeInfo.uri) { + places.push(placeInfo) + } else { + throw new Error("Unsupported type passed to addVisits"); + } + + // Create a PageInfo for each entry. + for (let place of places) { + let info = {url: place.uri}; + info.title = (typeof place.title === "string") ? place.title : "test visit for " + info.url.spec ; + if (typeof place.referrer == "string") { + place.referrer = NetUtil.newURI(place.referrer); + } else if (place.referrer && place.referrer instanceof URL) { + place.referrer = NetUtil.newURI(place.referrer.href); + } + let visitDate = place.visitDate; + if (visitDate) { + if (!(visitDate instanceof Date)) { + visitDate = PlacesUtils.toDate(visitDate); + } + } else { + visitDate = new Date(); + } + info.visits = [{ + transition: place.transition, + date: visitDate, + referrer: place.referrer + }]; + infos.push(info); + } + return PlacesUtils.history.insertMany(infos); + }), + + /** + * Clear all history. + * + * @return {Promise} + * @resolves When history was cleared successfully. + * @rejects JavaScript exception. + */ + clearHistory() { + let expirationFinished = new Promise(resolve => { + Services.obs.addObserver(function observe(subj, topic, data) { + Services.obs.removeObserver(observe, topic); + resolve(); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + }); + + return Promise.all([expirationFinished, PlacesUtils.history.clear()]); + }, + + /** + * Waits for all pending async statements on the default connection. + * + * @return {Promise} + * @resolves When all pending async statements finished. + * @rejects Never. + * + * @note The result is achieved by asynchronously executing a query requiring + * a write lock. Since all statements on the same connection are + * serialized, the end of this write operation means that all writes are + * complete. Note that WAL makes so that writers don't block readers, but + * this is a problem only across different connections. + */ + promiseAsyncUpdates() { + return PlacesUtils.withConnectionWrapper("promiseAsyncUpdates", Task.async(function* (db) { + try { + yield db.executeCached("BEGIN EXCLUSIVE"); + yield db.executeCached("COMMIT"); + } catch (ex) { + // If we fail to start a transaction, it's because there is already one. + // In such a case we should not try to commit the existing transaction. + } + })); + }, + + /** + * Asynchronously checks if an address is found in the database. + * @param aURI + * nsIURI or address to look for. + * + * @return {Promise} + * @resolves Returns true if the page is found. + * @rejects JavaScript exception. + */ + isPageInDB: Task.async(function* (aURI) { + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url", + { url }); + return rows.length > 0; + }), + + /** + * Asynchronously checks how many visits exist for a specified page. + * @param aURI + * nsIURI or address to look for. + * + * @return {Promise} + * @resolves Returns the number of visits found. + * @rejects JavaScript exception. + */ + visitsInDB: Task.async(function* (aURI) { + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT count(*) FROM moz_historyvisits v + JOIN moz_places h ON h.id = v.place_id + WHERE url_hash = hash(:url) AND url = :url`, + { url }); + return rows[0].getResultByIndex(0); + }) +}); diff --git a/toolkit/components/places/tests/bookmarks/.eslintrc.js b/toolkit/components/places/tests/bookmarks/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/bookmarks/head_bookmarks.js b/toolkit/components/places/tests/bookmarks/head_bookmarks.js new file mode 100644 index 000000000..842a66b31 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. diff --git a/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js new file mode 100644 index 000000000..b6982987b --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js @@ -0,0 +1,103 @@ +function run_test() { + run_next_test(); +} + +/* Bug 1016953 - When a previous bookmark backup exists with the same hash +regardless of date, an automatic backup should attempt to either rename it to +today's date if the backup was for an old date or leave it alone if it was for +the same date. However if the file ext was json it will accidentally rename it +to jsonlz4 while keeping the json contents +*/ + +add_task(function* test_same_date_same_hash() { + // If old file has been created on the same date and has the same hash + // the file should be left alone + let backupFolder = yield PlacesBackups.getBackupFolder(); + // Save to profile dir to obtain hash and nodeCount to append to filename + let tempPath = OS.Path.join(OS.Constants.Path.profileDir, + "bug10169583_bookmarks.json"); + let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath); + + // Save JSON file in backup folder with hash appended + let dateObj = new Date(); + let filename = "bookmarks-" + PlacesBackups.toISODateString(dateObj) + "_" + + count + "_" + hash + ".json"; + let backupFile = OS.Path.join(backupFolder, filename); + yield OS.File.move(tempPath, backupFile); + + // Force a compressed backup which fallbacks to rename + yield PlacesBackups.create(); + let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + // check to ensure not renamed to jsonlz4 + Assert.equal(mostRecentBackupFile, backupFile); + // inspect contents and check if valid json + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let result = yield OS.File.read(mostRecentBackupFile); + let jsonString = converter.convertFromByteArray(result, result.length); + do_print("Check is valid JSON"); + JSON.parse(jsonString); + + // Cleanup + yield OS.File.remove(backupFile); + yield OS.File.remove(tempPath); + PlacesBackups._backupFiles = null; // To force re-cache of backupFiles +}); + +add_task(function* test_same_date_diff_hash() { + // If the old file has been created on the same date, but has a different hash + // the existing file should be overwritten with the newer compressed version + let backupFolder = yield PlacesBackups.getBackupFolder(); + let tempPath = OS.Path.join(OS.Constants.Path.profileDir, + "bug10169583_bookmarks.json"); + let {count} = yield BookmarkJSONUtils.exportToFile(tempPath); + let dateObj = new Date(); + let filename = "bookmarks-" + PlacesBackups.toISODateString(dateObj) + "_" + + count + "_" + "differentHash==" + ".json"; + let backupFile = OS.Path.join(backupFolder, filename); + yield OS.File.move(tempPath, backupFile); + yield PlacesBackups.create(); // Force compressed backup + mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + + // Decode lz4 compressed file to json and check if json is valid + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let result = yield OS.File.read(mostRecentBackupFile, { compression: "lz4" }); + let jsonString = converter.convertFromByteArray(result, result.length); + do_print("Check is valid JSON"); + JSON.parse(jsonString); + + // Cleanup + yield OS.File.remove(mostRecentBackupFile); + yield OS.File.remove(tempPath); + PlacesBackups._backupFiles = null; // To force re-cache of backupFiles +}); + +add_task(function* test_diff_date_same_hash() { + // If the old file has been created on an older day but has the same hash + // it should be renamed with today's date without altering the contents. + let backupFolder = yield PlacesBackups.getBackupFolder(); + let tempPath = OS.Path.join(OS.Constants.Path.profileDir, + "bug10169583_bookmarks.json"); + let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath); + let oldDate = new Date(2014, 1, 1); + let curDate = new Date(); + let oldFilename = "bookmarks-" + PlacesBackups.toISODateString(oldDate) + "_" + + count + "_" + hash + ".json"; + let newFilename = "bookmarks-" + PlacesBackups.toISODateString(curDate) + "_" + + count + "_" + hash + ".json"; + let backupFile = OS.Path.join(backupFolder, oldFilename); + let newBackupFile = OS.Path.join(backupFolder, newFilename); + yield OS.File.move(tempPath, backupFile); + + // Ensure file has been renamed correctly + yield PlacesBackups.create(); + let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + Assert.equal(mostRecentBackupFile, newBackupFile); + + // Cleanup + yield OS.File.remove(mostRecentBackupFile); + yield OS.File.remove(tempPath); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js new file mode 100644 index 000000000..13755e576 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js @@ -0,0 +1,112 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Bug 1017502 - Add a foreign_count column to moz_places +This tests, tests the triggers that adjust the foreign_count when a bookmark is +added or removed and also the maintenance task to fix wrong counts. +*/ + +const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/"); + +function* getForeignCountForURL(conn, url) { + yield PlacesTestUtils.promiseAsyncUpdates(); + url = url instanceof Ci.nsIURI ? url.spec : url; + let rows = yield conn.executeCached( + `SELECT foreign_count FROM moz_places WHERE url_hash = hash(:t_url) + AND url = :t_url`, { t_url: url }); + return rows[0].getResultByName("foreign_count"); +} + +function run_test() { + run_next_test(); +} + +add_task(function* add_remove_change_bookmark_test() { + let conn = yield PlacesUtils.promiseDBConnection(); + + // Simulate a visit to the url + yield PlacesTestUtils.addVisits(T_URI); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); + + // Add 1st bookmark which should increment foreign_count by 1 + let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + T_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "First Run"); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1); + + // Add 2nd bookmark + let id2 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, + T_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "First Run"); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 2); + + // Remove 2nd bookmark which should decrement foreign_count by 1 + PlacesUtils.bookmarks.removeItem(id2); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1); + + // Change first bookmark's URI + const URI2 = NetUtil.newURI("http://www.mozilla.org"); + PlacesUtils.bookmarks.changeBookmarkURI(id1, URI2); + // Check foreign count for original URI + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); + // Check foreign count for new URI + Assert.equal((yield getForeignCountForURL(conn, URI2)), 1); + + // Cleanup - Remove changed bookmark + let id = PlacesUtils.bookmarks.getBookmarkIdsForURI(URI2); + PlacesUtils.bookmarks.removeItem(id); + Assert.equal((yield getForeignCountForURL(conn, URI2)), 0); + +}); + +add_task(function* maintenance_foreign_count_test() { + let conn = yield PlacesUtils.promiseDBConnection(); + + // Simulate a visit to the url + yield PlacesTestUtils.addVisits(T_URI); + + // Adjust the foreign_count for the added entry to an incorrect value + let deferred = Promise.defer(); + let stmt = DBConn().createAsyncStatement( + `UPDATE moz_places SET foreign_count = 10 WHERE url_hash = hash(:t_url) + AND url = :t_url `); + stmt.params.t_url = T_URI.spec; + stmt.executeAsync({ + handleCompletion: function() { + deferred.resolve(); + } + }); + stmt.finalize(); + yield deferred.promise; + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 10); + + // Run maintenance + Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + let promiseMaintenanceFinished = + promiseTopicObserved("places-maintenance-finished"); + PlacesDBUtils.maintenanceOnIdle(); + yield promiseMaintenanceFinished; + + // Check if the foreign_count has been adjusted to the correct value + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); +}); + +add_task(function* add_remove_tags_test() { + let conn = yield PlacesUtils.promiseDBConnection(); + + yield PlacesTestUtils.addVisits(T_URI); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); + + // Check foreign count incremented by 1 for a single tag + PlacesUtils.tagging.tagURI(T_URI, ["test tag"]); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1); + + // Check foreign count is incremented by 2 for two tags + PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 3); + + // Check foreign count is set to 0 when all tags are removed + PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]); + Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_1129529.js b/toolkit/components/places/tests/bookmarks/test_1129529.js new file mode 100644 index 000000000..da1ff708f --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1129529.js @@ -0,0 +1,76 @@ +var now = Date.now() * 1000; + +// Test that importing bookmark data where a bookmark has a tag longer than 100 +// chars imports everything except the tags for that bookmark. +add_task(function* () { + let aData = { + guid: "root________", + index: 0, + id: 1, + type: "text/x-moz-place-container", + dateAdded: now, + lastModified: now, + root: "placesRoot", + children: [{ + guid: "unfiled_____", + index: 0, + id: 2, + type: "text/x-moz-place-container", + dateAdded: now, + lastModified: now, + root: "unfiledBookmarksFolder", + children: [ + { + guid: "___guid1____", + index: 0, + id: 3, + charset: "UTF-8", + tags: "tag0", + type: "text/x-moz-place", + dateAdded: now, + lastModified: now, + uri: "http://test0.com/" + }, + { + guid: "___guid2____", + index: 1, + id: 4, + charset: "UTF-8", + tags: "tag1," + "a" + "0123456789".repeat(10), // 101 chars + type: "text/x-moz-place", + dateAdded: now, + lastModified: now, + uri: "http://test1.com/" + }, + { + guid: "___guid3____", + index: 2, + id: 5, + charset: "UTF-8", + tags: "tag2", + type: "text/x-moz-place", + dateAdded: now, + lastModified: now, + uri: "http://test2.com/" + } + ] + }] + }; + + let contentType = "application/json"; + let uri = "data:" + contentType + "," + JSON.stringify(aData); + yield BookmarkJSONUtils.importFromURL(uri, false); + + let [bookmarks] = yield PlacesBackups.getBookmarksTree(); + let unsortedBookmarks = bookmarks.children[2].children; + Assert.equal(unsortedBookmarks.length, 3); + + for (let i = 0; i < unsortedBookmarks.length; ++i) { + let bookmark = unsortedBookmarks[i]; + Assert.equal(bookmark.charset, "UTF-8"); + Assert.equal(bookmark.dateAdded, now); + Assert.equal(bookmark.lastModified, now); + Assert.equal(bookmark.uri, "http://test" + i + ".com/"); + Assert.equal(bookmark.tags, "tag" + i); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/test_384228.js b/toolkit/components/places/tests/bookmarks/test_384228.js new file mode 100644 index 000000000..9a52c9746 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_384228.js @@ -0,0 +1,98 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * test querying for bookmarks in multiple folders. + */ +add_task(function* search_bookmark_in_folder() { + let testFolder1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 1" + }); + Assert.equal(testFolder1.index, 0); + + let testFolder2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 2" + }); + Assert.equal(testFolder2.index, 1); + + let testFolder3 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 3" + }); + Assert.equal(testFolder3.index, 2); + + let b1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + url: "http://foo.tld/", + title: "title b1 (folder 1)" + }); + Assert.equal(b1.index, 0); + + let b2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + url: "http://foo.tld/", + title: "title b2 (folder 1)" + }); + Assert.equal(b2.index, 1); + + let b3 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder2.guid, + url: "http://foo.tld/", + title: "title b3 (folder 2)" + }); + Assert.equal(b3.index, 0); + + let b4 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder3.guid, + url: "http://foo.tld/", + title: "title b4 (folder 3)" + }); + Assert.equal(b4.index, 0); + + // also test recursive search + let testFolder1_1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 1.1" + }); + Assert.equal(testFolder1_1.index, 2); + + let b5 = yield PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1_1.guid, + url: "http://foo.tld/", + title: "title b5 (folder 1.1)" + }); + Assert.equal(b5.index, 0); + + + // query folder 1, folder 2 and get 4 bookmarks + let folderIds = []; + folderIds.push(yield PlacesUtils.promiseItemId(testFolder1.guid)); + folderIds.push(yield PlacesUtils.promiseItemId(testFolder2.guid)); + + let hs = PlacesUtils.history; + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + query.searchTerms = "title"; + options.queryType = options.QUERY_TYPE_BOOKMARKS; + query.setFolders(folderIds, folderIds.length); + let rootNode = hs.executeQuery(query, options).root; + rootNode.containerOpen = true; + + // should not match item from folder 3 + Assert.equal(rootNode.childCount, 4); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(3).bookmarkGuid, b5.guid); + + rootNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_385829.js b/toolkit/components/places/tests/bookmarks/test_385829.js new file mode 100644 index 000000000..63beee5f3 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_385829.js @@ -0,0 +1,182 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* search_bookmark_by_lastModified_dateDated() { + // test search on folder with various sorts and max results + // see bug #385829 for more details + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 385829 test" + }); + + let now = new Date(); + // ensure some unique values for date added and last modified + // for date added: b1 < b2 < b3 < b4 + // for last modified: b1 > b2 > b3 > b4 + let b1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a1.com/", + title: "1 title", + dateAdded: new Date(now.getTime() + 1000) + }); + let b2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a2.com/", + title: "2 title", + dateAdded: new Date(now.getTime() + 2000) + }); + let b3 = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a3.com/", + title: "3 title", + dateAdded: new Date(now.getTime() + 3000) + }); + let b4 = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a4.com/", + title: "4 title", + dateAdded: new Date(now.getTime() + 4000) + }); + + // make sure lastModified is larger than dateAdded + let modifiedTime = new Date(now.getTime() + 5000); + yield PlacesUtils.bookmarks.update({ + guid: b1.guid, + lastModified: new Date(modifiedTime.getTime() + 4000) + }); + yield PlacesUtils.bookmarks.update({ + guid: b2.guid, + lastModified: new Date(modifiedTime.getTime() + 3000) + }); + yield PlacesUtils.bookmarks.update({ + guid: b3.guid, + lastModified: new Date(modifiedTime.getTime() + 2000) + }); + yield PlacesUtils.bookmarks.update({ + guid: b4.guid, + lastModified: new Date(modifiedTime.getTime() + 1000) + }); + + let hs = PlacesUtils.history; + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + options.maxResults = 3; + let folderIds = []; + folderIds.push(yield PlacesUtils.promiseItemId(folder.guid)); + query.setFolders(folderIds, 1); + + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + + // test SORT_BY_DATEADDED_ASCENDING (live update) + result.sortingMode = options.SORT_BY_DATEADDED_ASCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).dateAdded < + rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded < + rootNode.getChild(2).dateAdded); + + // test SORT_BY_DATEADDED_DESCENDING (live update) + result.sortingMode = options.SORT_BY_DATEADDED_DESCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid); + Assert.ok(rootNode.getChild(0).dateAdded > + rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded > + rootNode.getChild(2).dateAdded); + + // test SORT_BY_LASTMODIFIED_ASCENDING (live update) + result.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid); + Assert.ok(rootNode.getChild(0).lastModified < + rootNode.getChild(1).lastModified); + Assert.ok(rootNode.getChild(1).lastModified < + rootNode.getChild(2).lastModified); + + // test SORT_BY_LASTMODIFIED_DESCENDING (live update) + result.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING; + + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).lastModified > + rootNode.getChild(1).lastModified); + Assert.ok(rootNode.getChild(1).lastModified > + rootNode.getChild(2).lastModified); + rootNode.containerOpen = false; + + // test SORT_BY_DATEADDED_ASCENDING + options.sortingMode = options.SORT_BY_DATEADDED_ASCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).dateAdded < + rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded < + rootNode.getChild(2).dateAdded); + rootNode.containerOpen = false; + + // test SORT_BY_DATEADDED_DESCENDING + options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid); + Assert.ok(rootNode.getChild(0).dateAdded > + rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded > + rootNode.getChild(2).dateAdded); + rootNode.containerOpen = false; + + // test SORT_BY_LASTMODIFIED_ASCENDING + options.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid); + Assert.ok(rootNode.getChild(0).lastModified < + rootNode.getChild(1).lastModified); + Assert.ok(rootNode.getChild(1).lastModified < + rootNode.getChild(2).lastModified); + rootNode.containerOpen = false; + + // test SORT_BY_LASTMODIFIED_DESCENDING + options.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).lastModified > + rootNode.getChild(1).lastModified); + Assert.ok(rootNode.getChild(1).lastModified > + rootNode.getChild(2).lastModified); + rootNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_388695.js b/toolkit/components/places/tests/bookmarks/test_388695.js new file mode 100644 index 000000000..4e313c52f --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_388695.js @@ -0,0 +1,52 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get bookmark service +try { + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +} catch (ex) { + do_throw("Could not get nav-bookmarks-service\n"); +} + +var gTestRoot; +var gURI; +var gItemId1; +var gItemId2; + +// main +function run_test() { + gURI = uri("http://foo.tld.com/"); + gTestRoot = bmsvc.createFolder(bmsvc.placesRoot, "test folder", + bmsvc.DEFAULT_INDEX); + + // test getBookmarkIdsForURI + // getBookmarkIdsForURI sorts by the most recently added/modified (descending) + // + // we cannot rely on dateAdded growing when doing so in a simple iteration, + // see PR_Now() documentation + do_test_pending(); + + gItemId1 = bmsvc.insertBookmark(gTestRoot, gURI, bmsvc.DEFAULT_INDEX, ""); + do_timeout(100, phase2); +} + +function phase2() { + gItemId2 = bmsvc.insertBookmark(gTestRoot, gURI, bmsvc.DEFAULT_INDEX, ""); + var b = bmsvc.getBookmarkIdsForURI(gURI); + do_check_eq(b[0], gItemId2); + do_check_eq(b[1], gItemId1); + do_timeout(100, phase3); +} + +function phase3() { + // trigger last modified change + bmsvc.setItemTitle(gItemId1, ""); + var b = bmsvc.getBookmarkIdsForURI(gURI); + do_check_eq(b[0], gItemId1); + do_check_eq(b[1], gItemId2); + do_test_finished(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_393498.js b/toolkit/components/places/tests/bookmarks/test_393498.js new file mode 100644 index 000000000..601f77a0a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_393498.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var observer = { + __proto__: NavBookmarkObserver.prototype, + + onItemAdded: function (id, folder, index) { + this._itemAddedId = id; + this._itemAddedParent = folder; + this._itemAddedIndex = index; + }, + onItemChanged: function (id, property, isAnnotationProperty, value) { + this._itemChangedId = id; + this._itemChangedProperty = property; + this._itemChanged_isAnnotationProperty = isAnnotationProperty; + this._itemChangedValue = value; + } +}; +PlacesUtils.bookmarks.addObserver(observer, false); + +do_register_cleanup(function () { + PlacesUtils.bookmarks.removeObserver(observer); +}); + +function run_test() { + // We set times in the past to workaround a timing bug due to virtual + // machines and the skew between PR_Now() and Date.now(), see bug 427142 and + // bug 858377 for details. + const PAST_PRTIME = (Date.now() - 86400000) * 1000; + + // Insert a new bookmark. + let testFolder = PlacesUtils.bookmarks.createFolder( + PlacesUtils.placesRootId, "test Folder", + PlacesUtils.bookmarks.DEFAULT_INDEX); + let bookmarkId = PlacesUtils.bookmarks.insertBookmark( + testFolder, uri("http://google.com/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, ""); + + // Sanity check. + do_check_true(observer.itemChangedProperty === undefined); + + // Set dateAdded in the past and verify the bookmarks cache. + PlacesUtils.bookmarks.setItemDateAdded(bookmarkId, PAST_PRTIME); + do_check_eq(observer._itemChangedProperty, "dateAdded"); + do_check_eq(observer._itemChangedValue, PAST_PRTIME); + let dateAdded = PlacesUtils.bookmarks.getItemDateAdded(bookmarkId); + do_check_eq(dateAdded, PAST_PRTIME); + + // After just inserting, modified should be the same as dateAdded. + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId), dateAdded); + + // Set lastModified in the past and verify the bookmarks cache. + PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME); + do_check_eq(observer._itemChangedProperty, "lastModified"); + do_check_eq(observer._itemChangedValue, PAST_PRTIME); + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId), + PAST_PRTIME); + + // Set bookmark title + PlacesUtils.bookmarks.setItemTitle(bookmarkId, "Google"); + + // Test notifications. + do_check_eq(observer._itemChangedId, bookmarkId); + do_check_eq(observer._itemChangedProperty, "title"); + do_check_eq(observer._itemChangedValue, "Google"); + + // Check lastModified has been updated. + is_time_ordered(PAST_PRTIME, + PlacesUtils.bookmarks.getItemLastModified(bookmarkId)); + + // Check that node properties are updated. + let root = PlacesUtils.getFolderContents(testFolder).root; + do_check_eq(root.childCount, 1); + let childNode = root.getChild(0); + + // confirm current dates match node properties + do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(bookmarkId), + childNode.dateAdded); + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId), + childNode.lastModified); + + // Test live update of lastModified when setting title. + PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME); + PlacesUtils.bookmarks.setItemTitle(bookmarkId, "Google"); + + // Check lastModified has been updated. + is_time_ordered(PAST_PRTIME, childNode.lastModified); + // Test that node value matches db value. + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId), + childNode.lastModified); + + // Test live update of the exposed date apis. + PlacesUtils.bookmarks.setItemDateAdded(bookmarkId, PAST_PRTIME); + do_check_eq(childNode.dateAdded, PAST_PRTIME); + PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME); + do_check_eq(childNode.lastModified, PAST_PRTIME); + + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/bookmarks/test_395101.js b/toolkit/components/places/tests/bookmarks/test_395101.js new file mode 100644 index 000000000..a507e7361 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_395101.js @@ -0,0 +1,87 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get bookmark service +try { + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService); +} catch (ex) { + do_throw("Could not get nav-bookmarks-service\n"); +} + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +// get bookmarks root id +var root = bmsvc.bookmarksMenuFolder; + +// main +function run_test() { + // test searching for tagged bookmarks + + // test folder + var folder = bmsvc.createFolder(root, "bug 395101 test", bmsvc.DEFAULT_INDEX); + + // create a bookmark + var testURI = uri("http://a1.com"); + var b1 = bmsvc.insertBookmark(folder, testURI, + bmsvc.DEFAULT_INDEX, "1 title"); + + // tag the bookmarked URI + tagssvc.tagURI(testURI, ["elephant", "walrus", "giraffe", "turkey", "hiPPo", "BABOON", "alf"]); + + // search for the bookmark, using a tag + var query = histsvc.getNewQuery(); + query.searchTerms = "elephant"; + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + query.setFolders([folder], 1); + + var result = histsvc.executeQuery(query, options); + var rootNode = result.root; + rootNode.containerOpen = true; + + do_check_eq(rootNode.childCount, 1); + do_check_eq(rootNode.getChild(0).itemId, b1); + rootNode.containerOpen = false; + + // partial matches are okay + query.searchTerms = "wal"; + result = histsvc.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 1); + rootNode.containerOpen = false; + + // case insensitive search term + query.searchTerms = "WALRUS"; + result = histsvc.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 1); + do_check_eq(rootNode.getChild(0).itemId, b1); + rootNode.containerOpen = false; + + // case insensitive tag + query.searchTerms = "baboon"; + result = histsvc.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 1); + do_check_eq(rootNode.getChild(0).itemId, b1); + rootNode.containerOpen = false; +} diff --git a/toolkit/components/places/tests/bookmarks/test_395593.js b/toolkit/components/places/tests/bookmarks/test_395593.js new file mode 100644 index 000000000..46d8f5b80 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_395593.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +function check_queries_results(aQueries, aOptions, aExpectedItemIds) { + var result = hs.executeQueries(aQueries, aQueries.length, aOptions); + var root = result.root; + root.containerOpen = true; + + // Dump found nodes. + for (let i = 0; i < root.childCount; i++) { + dump("nodes[" + i + "]: " + root.getChild(0).title + "\n"); + } + + do_check_eq(root.childCount, aExpectedItemIds.length); + for (let i = 0; i < root.childCount; i++) { + do_check_eq(root.getChild(i).itemId, aExpectedItemIds[i]); + } + + root.containerOpen = false; +} + +// main +function run_test() { + var id1 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"), + bs.DEFAULT_INDEX, "123 0"); + var id2 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"), + bs.DEFAULT_INDEX, "456"); + var id3 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"), + bs.DEFAULT_INDEX, "123 456"); + var id4 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"), + bs.DEFAULT_INDEX, "789 456"); + + /** + * All of the query objects are ORed together. Within a query, all the terms + * are ANDed together. See nsINavHistory.idl. + */ + var queries = []; + queries.push(hs.getNewQuery()); + queries.push(hs.getNewQuery()); + var options = hs.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + // Test 1 + dump("Test searching for 123 OR 789\n"); + queries[0].searchTerms = "123"; + queries[1].searchTerms = "789"; + check_queries_results(queries, options, [id1, id3, id4]); + + // Test 2 + dump("Test searching for 123 OR 456\n"); + queries[0].searchTerms = "123"; + queries[1].searchTerms = "456"; + check_queries_results(queries, options, [id1, id2, id3, id4]); + + // Test 3 + dump("Test searching for 00 OR 789\n"); + queries[0].searchTerms = "00"; + queries[1].searchTerms = "789"; + check_queries_results(queries, options, [id4]); +} diff --git a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js new file mode 100644 index 000000000..e317cc2e9 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js @@ -0,0 +1,221 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +/* + +Backup/restore tests example: + +var myTest = { + populate: function () { ... add bookmarks ... }, + validate: function () { ... query for your bookmarks ... } +} + +this.push(myTest); + +*/ + +/* + +test summary: +- create folders with content +- create a query bookmark for those folders +- backs up bookmarks +- restores bookmarks +- confirms that the query has the new ids for the same folders + +scenarios: +- 1 folder (folder shortcut) +- n folders (single query) +- n folders (multiple queries) + +*/ + +const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX; + +var test = { + _testRootId: null, + _testRootTitle: "test root", + _folderIds: [], + _bookmarkURIs: [], + _count: 3, + + populate: function populate() { + // folder to hold this test + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId); + this._testRootId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, + this._testRootTitle, DEFAULT_INDEX); + + // create test folders each with a bookmark + for (var i = 0; i < this._count; i++) { + var folderId = + PlacesUtils.bookmarks.createFolder(this._testRootId, "folder" + i, DEFAULT_INDEX); + this._folderIds.push(folderId) + + var bookmarkURI = uri("http://" + i); + PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI, + DEFAULT_INDEX, "bookmark" + i); + this._bookmarkURIs.push(bookmarkURI); + } + + // create a query URI with 1 folder (ie: folder shortcut) + this._queryURI1 = uri("place:folder=" + this._folderIds[0] + "&queryType=1"); + this._queryTitle1 = "query1"; + PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI1, + DEFAULT_INDEX, this._queryTitle1); + + // create a query URI with _count folders + this._queryURI2 = uri("place:folder=" + this._folderIds.join("&folder=") + "&queryType=1"); + this._queryTitle2 = "query2"; + PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI2, + DEFAULT_INDEX, this._queryTitle2); + + // create a query URI with _count queries (each with a folder) + // first get a query object for each folder + var queries = this._folderIds.map(function(aFolderId) { + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([aFolderId], 1); + return query; + }); + var options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + this._queryURI3 = + uri(PlacesUtils.history.queriesToQueryString(queries, queries.length, options)); + this._queryTitle3 = "query3"; + PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI3, + DEFAULT_INDEX, this._queryTitle3); + }, + + clean: function () {}, + + validate: function validate() { + // Throw a wrench in the works by inserting some new bookmarks, + // ensuring folder ids won't be the same, when restoring. + for (let i = 0; i < 10; i++) { + PlacesUtils.bookmarks. + insertBookmark(PlacesUtils.bookmarksMenuFolderId, uri("http://aaaa"+i), DEFAULT_INDEX, ""); + } + + var toolbar = + PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId, + false, true).root; + do_check_true(toolbar.childCount, 1); + + var folderNode = toolbar.getChild(0); + do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + do_check_eq(folderNode.title, this._testRootTitle); + folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode); + folderNode.containerOpen = true; + + // |_count| folders + the query node + do_check_eq(folderNode.childCount, this._count+3); + + for (let i = 0; i < this._count; i++) { + var subFolder = folderNode.getChild(i); + do_check_eq(subFolder.title, "folder"+i); + subFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode); + subFolder.containerOpen = true; + do_check_eq(subFolder.childCount, 1); + var child = subFolder.getChild(0); + do_check_eq(child.title, "bookmark"+i); + do_check_true(uri(child.uri).equals(uri("http://" + i))) + } + + // validate folder shortcut + this.validateQueryNode1(folderNode.getChild(this._count)); + + // validate folders query + this.validateQueryNode2(folderNode.getChild(this._count + 1)); + + // validate multiple queries query + this.validateQueryNode3(folderNode.getChild(this._count + 2)); + + // clean up + folderNode.containerOpen = false; + toolbar.containerOpen = false; + }, + + validateQueryNode1: function validateQueryNode1(aNode) { + do_check_eq(aNode.title, this._queryTitle1); + do_check_true(PlacesUtils.nodeIsFolder(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + do_check_eq(aNode.childCount, 1); + var child = aNode.getChild(0); + do_check_true(uri(child.uri).equals(uri("http://0"))) + do_check_eq(child.title, "bookmark0") + aNode.containerOpen = false; + }, + + validateQueryNode2: function validateQueryNode2(aNode) { + do_check_eq(aNode.title, this._queryTitle2); + do_check_true(PlacesUtils.nodeIsQuery(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + do_check_eq(aNode.childCount, this._count); + for (var i = 0; i < aNode.childCount; i++) { + var child = aNode.getChild(i); + do_check_true(uri(child.uri).equals(uri("http://" + i))) + do_check_eq(child.title, "bookmark" + i) + } + aNode.containerOpen = false; + }, + + validateQueryNode3: function validateQueryNode3(aNode) { + do_check_eq(aNode.title, this._queryTitle3); + do_check_true(PlacesUtils.nodeIsQuery(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + do_check_eq(aNode.childCount, this._count); + for (var i = 0; i < aNode.childCount; i++) { + var child = aNode.getChild(i); + do_check_true(uri(child.uri).equals(uri("http://" + i))) + do_check_eq(child.title, "bookmark" + i) + } + aNode.containerOpen = false; + } +} +tests.push(test); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + // make json file + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + tests.forEach(function(aTest) { + aTest.populate(); + // sanity + aTest.validate(); + }); + + // export json to file + yield BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + tests.forEach(function(aTest) { + aTest.clean(); + }); + + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate + tests.forEach(function(aTest) { + aTest.validate(); + }); + + // clean up + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js new file mode 100644 index 000000000..858496856 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js @@ -0,0 +1,141 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup"; +// Menu, Toolbar, Unsorted, Tags, Mobile +const PLACES_ROOTS_COUNT = 5; +var tests = []; + +/* + +Backup/restore tests example: + +var myTest = { + populate: function () { ... add bookmarks ... }, + validate: function () { ... query for your bookmarks ... } +} + +this.push(myTest); + +*/ + +var test = { + populate: function populate() { + // check initial size + var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, + false, false).root; + do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT ); + rootNode.containerOpen = false; + + var idx = PlacesUtils.bookmarks.DEFAULT_INDEX; + + // create a root to be restore + this._restoreRootTitle = "restore root"; + var restoreRootId = PlacesUtils.bookmarks + .createFolder(PlacesUtils.placesRootId, + this._restoreRootTitle, idx); + // add a test bookmark + this._restoreRootURI = uri("http://restore.uri"); + PlacesUtils.bookmarks.insertBookmark(restoreRootId, this._restoreRootURI, + idx, "restore uri"); + // add a test bookmark to be exclude + this._restoreRootExcludeURI = uri("http://exclude.uri"); + var exItemId = PlacesUtils.bookmarks + .insertBookmark(restoreRootId, + this._restoreRootExcludeURI, + idx, "exclude uri"); + // Annotate the bookmark for exclusion. + PlacesUtils.annotations.setItemAnnotation(exItemId, + EXCLUDE_FROM_BACKUP_ANNO, 1, 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + // create a root to be exclude + this._excludeRootTitle = "exclude root"; + this._excludeRootId = PlacesUtils.bookmarks + .createFolder(PlacesUtils.placesRootId, + this._excludeRootTitle, idx); + // Annotate the root for exclusion. + PlacesUtils.annotations.setItemAnnotation(this._excludeRootId, + EXCLUDE_FROM_BACKUP_ANNO, 1, 0, + PlacesUtils.annotations.EXPIRE_NEVER); + // add a test bookmark exclude by exclusion of its parent + PlacesUtils.bookmarks.insertBookmark(this._excludeRootId, + this._restoreRootExcludeURI, + idx, "exclude uri"); + }, + + validate: function validate(aEmptyBookmarks) { + var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, + false, false).root; + + if (!aEmptyBookmarks) { + // since restore does not remove backup exclude items both + // roots should still exist. + do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT + 2); + // open exclude root and check it still contains one item + var restoreRootIndex = PLACES_ROOTS_COUNT; + var excludeRootIndex = PLACES_ROOTS_COUNT+1; + var excludeRootNode = rootNode.getChild(excludeRootIndex); + do_check_eq(this._excludeRootTitle, excludeRootNode.title); + excludeRootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode); + excludeRootNode.containerOpen = true; + do_check_eq(excludeRootNode.childCount, 1); + var excludeRootChildNode = excludeRootNode.getChild(0); + do_check_eq(excludeRootChildNode.uri, this._restoreRootExcludeURI.spec); + excludeRootNode.containerOpen = false; + } + else { + // exclude root should not exist anymore + do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT + 1); + restoreRootIndex = PLACES_ROOTS_COUNT; + } + + var restoreRootNode = rootNode.getChild(restoreRootIndex); + do_check_eq(this._restoreRootTitle, restoreRootNode.title); + restoreRootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode); + restoreRootNode.containerOpen = true; + do_check_eq(restoreRootNode.childCount, 1); + var restoreRootChildNode = restoreRootNode.getChild(0); + do_check_eq(restoreRootChildNode.uri, this._restoreRootURI.spec); + restoreRootNode.containerOpen = false; + + rootNode.containerOpen = false; + } +} + +function run_test() { + run_next_test(); +} + +add_task(function*() { + // make json file + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + test.populate(); + + yield BookmarkJSONUtils.exportToFile(jsonFile); + + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate without removing all bookmarks + // restore do not remove backup exclude entries + test.validate(false); + + // cleanup + yield PlacesUtils.bookmarks.eraseEverything(); + // manually remove the excluded root + PlacesUtils.bookmarks.removeItem(test._excludeRootId); + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate after a complete bookmarks cleanup + test.validate(true); + + // clean up + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js new file mode 100644 index 000000000..1def75d2d --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js @@ -0,0 +1,158 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +/* + +Backup/restore tests example: + +var myTest = { + populate: function () { ... add bookmarks ... }, + validate: function () { ... query for your bookmarks ... } +} + +this.push(myTest); + +*/ + +tests.push({ + excludeItemsFromRestore: [], + populate: function populate() { + // check initial size + var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, + false, false).root; + do_check_eq(rootNode.childCount, 5); + + // create a test root + this._folderTitle = "test folder"; + this._folderId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, + this._folderTitle, + PlacesUtils.bookmarks.DEFAULT_INDEX); + do_check_eq(rootNode.childCount, 6); + + // add a tag + this._testURI = PlacesUtils._uri("http://test"); + this._tags = ["a", "b"]; + PlacesUtils.tagging.tagURI(this._testURI, this._tags); + + // add a child to each root, including our test root + this._roots = [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.toolbarFolderId, + PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.mobileFolderId, + this._folderId]; + this._roots.forEach(function(aRootId) { + // clean slate + PlacesUtils.bookmarks.removeFolderChildren(aRootId); + // add a test bookmark + PlacesUtils.bookmarks.insertBookmark(aRootId, this._testURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, "test"); + }, this); + + // add a folder to exclude from replacing during restore + // this will still be present post-restore + var excludedFolderId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, + "excluded", + PlacesUtils.bookmarks.DEFAULT_INDEX); + do_check_eq(rootNode.childCount, 7); + this.excludeItemsFromRestore.push(excludedFolderId); + + // add a test bookmark to it + PlacesUtils.bookmarks.insertBookmark(excludedFolderId, this._testURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, "test"); + }, + + inbetween: function inbetween() { + // add some items that should be removed by the restore + + // add a folder + this._litterTitle = "otter"; + PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId, + this._litterTitle, 0); + + // add some tags + PlacesUtils.tagging.tagURI(this._testURI, ["c", "d"]); + }, + + validate: function validate() { + // validate tags restored + var tags = PlacesUtils.tagging.getTagsForURI(this._testURI); + // also validates that litter tags are gone + do_check_eq(this._tags.toString(), tags.toString()); + + var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, + false, false).root; + + // validate litter is gone + do_check_neq(rootNode.getChild(0).title, this._litterTitle); + + // test root count is the same + do_check_eq(rootNode.childCount, 7); + + var foundTestFolder = 0; + for (var i = 0; i < rootNode.childCount; i++) { + var node = rootNode.getChild(i); + + do_print("validating " + node.title); + if (node.itemId != PlacesUtils.tagsFolderId) { + if (node.title == this._folderTitle) { + // check the test folder's properties + do_check_eq(node.type, node.RESULT_TYPE_FOLDER); + do_check_eq(node.title, this._folderTitle); + foundTestFolder++; + } + + // test contents + node.QueryInterface(Ci.nsINavHistoryContainerResultNode).containerOpen = true; + do_check_eq(node.childCount, 1); + var child = node.getChild(0); + do_check_true(PlacesUtils._uri(child.uri).equals(this._testURI)); + + // clean up + node.containerOpen = false; + } + } + do_check_eq(foundTestFolder, 1); + rootNode.containerOpen = false; + } +}); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + // make json file + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + tests.forEach(function(aTest) { + aTest.populate(); + // sanity + aTest.validate(); + + if (aTest.excludedItemsFromRestore) + excludedItemsFromRestore = excludedItems.concat(aTest.excludedItemsFromRestore); + }); + + yield BookmarkJSONUtils.exportToFile(jsonFile); + + tests.forEach(function(aTest) { + aTest.inbetween(); + }); + + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate + tests.forEach(function(aTest) { + aTest.validate(); + }); + + // clean up + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js new file mode 100644 index 000000000..7da1146cf --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +/* + +Backup/restore tests example: + +var myTest = { + populate: function () { ... add bookmarks ... }, + validate: function () { ... query for your bookmarks ... } +} + +this.push(myTest); + +*/ + +var quotesTest = { + _folderTitle: '"quoted folder"', + _folderId: null, + + populate: function () { + this._folderId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, + this._folderTitle, + PlacesUtils.bookmarks.DEFAULT_INDEX); + }, + + clean: function () { + PlacesUtils.bookmarks.removeItem(this._folderId); + }, + + validate: function () { + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + var result = PlacesUtils.history.executeQuery(query, PlacesUtils.history.getNewQueryOptions()); + + var toolbar = result.root; + toolbar.containerOpen = true; + + // test for our quoted folder + do_check_true(toolbar.childCount, 1); + var folderNode = toolbar.getChild(0); + do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + do_check_eq(folderNode.title, this._folderTitle); + + // clean up + toolbar.containerOpen = false; + } +} +tests.push(quotesTest); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + // make json file + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + tests.forEach(function(aTest) { + aTest.populate(); + // sanity + aTest.validate(); + }); + + // export json to file + yield BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + tests.forEach(function(aTest) { + aTest.clean(); + }); + + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate + tests.forEach(function(aTest) { + aTest.validate(); + }); + + // clean up + yield OS.File.remove(jsonFile); + +}); diff --git a/toolkit/components/places/tests/bookmarks/test_448584.js b/toolkit/components/places/tests/bookmarks/test_448584.js new file mode 100644 index 000000000..6e58bd83a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_448584.js @@ -0,0 +1,113 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +// Get database connection +try { + var mDBConn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; +} +catch (ex) { + do_throw("Could not get database connection\n"); +} + +/* + This test is: + - don't try to add invalid uri nodes to a JSON backup +*/ + +var invalidURITest = { + _itemTitle: "invalid uri", + _itemUrl: "http://test.mozilla.org/", + _itemId: null, + + populate: function () { + // add a valid bookmark + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + PlacesUtils._uri(this._itemUrl), + PlacesUtils.bookmarks.DEFAULT_INDEX, + this._itemTitle); + // this bookmark will go corrupt + this._itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + PlacesUtils._uri(this._itemUrl), + PlacesUtils.bookmarks.DEFAULT_INDEX, + this._itemTitle); + }, + + clean: function () { + PlacesUtils.bookmarks.removeItem(this._itemId); + }, + + validate: function (aExpectValidItemsCount) { + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + var options = PlacesUtils.history.getNewQueryOptions(); + var result = PlacesUtils.history.executeQuery(query, options); + + var toolbar = result.root; + toolbar.containerOpen = true; + + // test for our bookmark + do_check_eq(toolbar.childCount, aExpectValidItemsCount); + for (var i = 0; i < toolbar.childCount; i++) { + var folderNode = toolbar.getChild(0); + do_check_eq(folderNode.type, folderNode.RESULT_TYPE_URI); + do_check_eq(folderNode.title, this._itemTitle); + } + + // clean up + toolbar.containerOpen = false; + } +} +tests.push(invalidURITest); + +function run_test() { + run_next_test(); +} + +add_task(function*() { + // make json file + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + tests.forEach(function(aTest) { + aTest.populate(); + // sanity + aTest.validate(2); + // Something in the code went wrong and we finish up losing the place, so + // the bookmark uri becomes null. + var sql = "UPDATE moz_bookmarks SET fk = 1337 WHERE id = ?1"; + var stmt = mDBConn.createStatement(sql); + stmt.bindByIndex(0, aTest._itemId); + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + }); + + yield BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + tests.forEach(function(aTest) { + aTest.clean(); + }); + + // restore json file + try { + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + } catch (ex) { do_throw("couldn't import the exported file: " + ex); } + + // validate + tests.forEach(function(aTest) { + aTest.validate(1); + }); + + // clean up + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_458683.js b/toolkit/components/places/tests/bookmarks/test_458683.js new file mode 100644 index 000000000..c3722aab5 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_458683.js @@ -0,0 +1,131 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +// Get database connection +try { + var mDBConn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; +} +catch (ex) { + do_throw("Could not get database connection\n"); +} + +/* + This test is: + - don't block while doing backup and restore if tag containers contain + bogus items (separators, folders) +*/ + +var invalidTagChildTest = { + _itemTitle: "invalid uri", + _itemUrl: "http://test.mozilla.org/", + _itemId: -1, + _tag: "testTag", + _tagItemId: -1, + + populate: function () { + // add a valid bookmark + this._itemId = PlacesUtils.bookmarks + .insertBookmark(PlacesUtils.toolbarFolderId, + PlacesUtils._uri(this._itemUrl), + PlacesUtils.bookmarks.DEFAULT_INDEX, + this._itemTitle); + + // create a tag + PlacesUtils.tagging.tagURI(PlacesUtils._uri(this._itemUrl), [this._tag]); + // get tag folder id + var options = PlacesUtils.history.getNewQueryOptions(); + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.tagsFolder], 1); + var result = PlacesUtils.history.executeQuery(query, options); + var tagRoot = result.root; + tagRoot.containerOpen = true; + do_check_eq(tagRoot.childCount, 1); + var tagNode = tagRoot.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + this._tagItemId = tagNode.itemId; + tagRoot.containerOpen = false; + + // add a separator and a folder inside tag folder + PlacesUtils.bookmarks.insertSeparator(this._tagItemId, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.createFolder(this._tagItemId, + "test folder", + PlacesUtils.bookmarks.DEFAULT_INDEX); + + // add a separator and a folder inside tag root + PlacesUtils.bookmarks.insertSeparator(PlacesUtils.bookmarks.tagsFolder, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.createFolder(PlacesUtils.bookmarks.tagsFolder, + "test tags root folder", + PlacesUtils.bookmarks.DEFAULT_INDEX); + }, + + clean: function () { + PlacesUtils.tagging.untagURI(PlacesUtils._uri(this._itemUrl), [this._tag]); + PlacesUtils.bookmarks.removeItem(this._itemId); + }, + + validate: function () { + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + var options = PlacesUtils.history.getNewQueryOptions(); + var result = PlacesUtils.history.executeQuery(query, options); + + var toolbar = result.root; + toolbar.containerOpen = true; + + // test for our bookmark + do_check_eq(toolbar.childCount, 1); + for (var i = 0; i < toolbar.childCount; i++) { + var folderNode = toolbar.getChild(0); + do_check_eq(folderNode.type, folderNode.RESULT_TYPE_URI); + do_check_eq(folderNode.title, this._itemTitle); + } + toolbar.containerOpen = false; + + // test for our tag + var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(this._itemUrl)); + do_check_eq(tags.length, 1); + do_check_eq(tags[0], this._tag); + } +} +tests.push(invalidTagChildTest); + +function run_test() { + run_next_test() +} + +add_task(function* () { + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + + // populate db + tests.forEach(function(aTest) { + aTest.populate(); + // sanity + aTest.validate(); + }); + + yield BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + tests.forEach(function(aTest) { + aTest.clean(); + }); + + // restore json file + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + + // validate + tests.forEach(function(aTest) { + aTest.validate(); + }); + + // clean up + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js new file mode 100644 index 000000000..3ce0e6ad7 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Since PlacesBackups.getbackupFiles() is a lazy getter, these tests must +// run in the given order, to avoid making it out-of-sync. + +add_task(function* check_max_backups_is_respected() { + // Get bookmarkBackups directory + let backupFolder = yield PlacesBackups.getBackupFolder(); + + // Create 2 json dummy backups in the past. + let oldJsonPath = OS.Path.join(backupFolder, "bookmarks-2008-01-01.json"); + let oldJsonFile = yield OS.File.open(oldJsonPath, { truncate: true }); + oldJsonFile.close(); + do_check_true(yield OS.File.exists(oldJsonPath)); + + let jsonPath = OS.Path.join(backupFolder, "bookmarks-2008-01-31.json"); + let jsonFile = yield OS.File.open(jsonPath, { truncate: true }); + jsonFile.close(); + do_check_true(yield OS.File.exists(jsonPath)); + + // Export bookmarks to JSON. + // Allow 2 backups, the older one should be removed. + yield PlacesBackups.create(2); + + let count = 0; + let lastBackupPath = null; + let iterator = new OS.File.DirectoryIterator(backupFolder); + try { + yield iterator.forEach(aEntry => { + count++; + if (PlacesBackups.filenamesRegex.test(aEntry.name)) + lastBackupPath = aEntry.path; + }); + } finally { + iterator.close(); + } + + do_check_eq(count, 2); + do_check_neq(lastBackupPath, null); + do_check_false(yield OS.File.exists(oldJsonPath)); + do_check_true(yield OS.File.exists(jsonPath)); +}); + +add_task(function* check_max_backups_greater_than_backups() { + // Get bookmarkBackups directory + let backupFolder = yield PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow 3 backups, none should be removed. + yield PlacesBackups.create(3); + + let count = 0; + let lastBackupPath = null; + let iterator = new OS.File.DirectoryIterator(backupFolder); + try { + yield iterator.forEach(aEntry => { + count++; + if (PlacesBackups.filenamesRegex.test(aEntry.name)) + lastBackupPath = aEntry.path; + }); + } finally { + iterator.close(); + } + do_check_eq(count, 2); + do_check_neq(lastBackupPath, null); +}); + +add_task(function* check_max_backups_null() { + // Get bookmarkBackups directory + let backupFolder = yield PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow infinite backups, none should be removed, a new one is not created + // since one for today already exists. + yield PlacesBackups.create(null); + + let count = 0; + let lastBackupPath = null; + let iterator = new OS.File.DirectoryIterator(backupFolder); + try { + yield iterator.forEach(aEntry => { + count++; + if (PlacesBackups.filenamesRegex.test(aEntry.name)) + lastBackupPath = aEntry.path; + }); + } finally { + iterator.close(); + } + do_check_eq(count, 2); + do_check_neq(lastBackupPath, null); +}); + +add_task(function* check_max_backups_undefined() { + // Get bookmarkBackups directory + let backupFolder = yield PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow infinite backups, none should be removed, a new one is not created + // since one for today already exists. + yield PlacesBackups.create(); + + let count = 0; + let lastBackupPath = null; + let iterator = new OS.File.DirectoryIterator(backupFolder); + try { + yield iterator.forEach(aEntry => { + count++; + if (PlacesBackups.filenamesRegex.test(aEntry.name)) + lastBackupPath = aEntry.path; + }); + } finally { + iterator.close(); + } + do_check_eq(count, 2); + do_check_neq(lastBackupPath, null); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js new file mode 100644 index 000000000..116352666 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + do_test_pending(); + + Task.spawn(function*() { + let backupFolder = yield PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolder); + // Remove all files from backups folder. + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.getNext().QueryInterface(Ci.nsIFile); + entry.remove(false); + } + + // Create a json dummy backup in the future. + let dateObj = new Date(); + dateObj.setYear(dateObj.getFullYear() + 1); + let name = PlacesBackups.getFilenameForDate(dateObj); + do_check_eq(name, "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json"); + files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.getNext().QueryInterface(Ci.nsIFile); + if (PlacesBackups.filenamesRegex.test(entry.leafName)) + entry.remove(false); + } + + let futureBackupFile = bookmarksBackupDir.clone(); + futureBackupFile.append(name); + futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0o600); + do_check_true(futureBackupFile.exists()); + + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0); + + yield PlacesBackups.create(); + // Check that a backup for today has been created. + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1); + let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + do_check_neq(mostRecentBackupFile, null); + do_check_true(PlacesBackups.filenamesRegex.test(OS.Path.basename(mostRecentBackupFile))); + + // Check that future backup has been removed. + do_check_false(futureBackupFile.exists()); + + // Cleanup. + mostRecentBackupFile = new FileUtils.File(mostRecentBackupFile); + mostRecentBackupFile.remove(false); + do_check_false(mostRecentBackupFile.exists()); + + do_test_finished() + }); +} diff --git a/toolkit/components/places/tests/bookmarks/test_675416.js b/toolkit/components/places/tests/bookmarks/test_675416.js new file mode 100644 index 000000000..08b1c3620 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_675416.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + /** + * Requests information to the service, so that bookmark's data is cached. + * @param aItemId + * Id of the bookmark to be cached. + */ + function forceBookmarkCaching(aItemId) { + PlacesUtils.bookmarks.getFolderIdForItem(aItemId); + } + + let observer = { + onBeginUpdateBatch: () => forceBookmarkCaching(itemId1), + onEndUpdateBatch: () => forceBookmarkCaching(itemId1), + onItemAdded: forceBookmarkCaching, + onItemChanged: forceBookmarkCaching, + onItemMoved: forceBookmarkCaching, + onItemRemoved: function(id) { + try { + forceBookmarkCaching(id); + do_throw("trying to fetch a removed bookmark should throw"); + } catch (ex) {} + }, + onItemVisited: forceBookmarkCaching, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) + }; + PlacesUtils.bookmarks.addObserver(observer, false); + + let folderId1 = PlacesUtils.bookmarks + .createFolder(PlacesUtils.bookmarksMenuFolderId, + "Bookmarks", + PlacesUtils.bookmarks.DEFAULT_INDEX); + let itemId1 = PlacesUtils.bookmarks + .insertBookmark(folderId1, + NetUtil.newURI("http:/www.wired.com/wiredscience"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Wired Science"); + + PlacesUtils.bookmarks.removeItem(folderId1); + + let folderId2 = PlacesUtils.bookmarks + .createFolder(PlacesUtils.bookmarksMenuFolderId, + "Science", + PlacesUtils.bookmarks.DEFAULT_INDEX); + let folderId3 = PlacesUtils.bookmarks + .createFolder(folderId2, + "Blogs", + PlacesUtils.bookmarks.DEFAULT_INDEX); + // Check title is correctly reported. + do_check_eq(PlacesUtils.bookmarks.getItemTitle(folderId3), "Blogs"); + do_check_eq(PlacesUtils.bookmarks.getItemTitle(folderId2), "Science"); + + PlacesUtils.bookmarks.removeObserver(observer, false); +} diff --git a/toolkit/components/places/tests/bookmarks/test_711914.js b/toolkit/components/places/tests/bookmarks/test_711914.js new file mode 100644 index 000000000..3712c8a77 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_711914.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + /** + * Requests information to the service, so that bookmark's data is cached. + * @param aItemId + * Id of the bookmark to be cached. + */ + function forceBookmarkCaching(aItemId) { + let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId); + PlacesUtils.bookmarks.getFolderIdForItem(parent); + } + + let observer = { + onBeginUpdateBatch: () => forceBookmarkCaching(itemId1), + onEndUpdateBatch: () => forceBookmarkCaching(itemId1), + onItemAdded: forceBookmarkCaching, + onItemChanged: forceBookmarkCaching, + onItemMoved: forceBookmarkCaching, + onItemRemoved: function (id) { + try { + forceBookmarkCaching(id); + do_throw("trying to fetch a removed bookmark should throw"); + } catch (ex) {} + }, + onItemVisited: forceBookmarkCaching, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) + }; + PlacesUtils.bookmarks.addObserver(observer, false); + + let folder1 = PlacesUtils.bookmarks + .createFolder(PlacesUtils.bookmarksMenuFolderId, + "Folder1", + PlacesUtils.bookmarks.DEFAULT_INDEX); + let folder2 = PlacesUtils.bookmarks + .createFolder(folder1, + "Folder2", + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.insertBookmark(folder2, + NetUtil.newURI("http://mozilla.org/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Mozilla"); + + PlacesUtils.bookmarks.removeFolderChildren(folder1); + + // Check title is correctly reported. + do_check_eq(PlacesUtils.bookmarks.getItemTitle(folder1), "Folder1"); + try { + PlacesUtils.bookmarks.getItemTitle(folder2); + do_throw("trying to fetch a removed bookmark should throw"); + } catch (ex) {} + + PlacesUtils.bookmarks.removeObserver(observer, false); +} diff --git a/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js new file mode 100644 index 000000000..c88323478 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks that automatically created bookmark backups are discarded if they are + * duplicate of an existing ones. + */ +function run_test() { + run_next_test(); +} + +add_task(function*() { + // Create a backup for yesterday in the backups folder. + let backupFolder = yield PlacesBackups.getBackupFolder(); + let dateObj = new Date(); + dateObj.setDate(dateObj.getDate() - 1); + let oldBackupName = PlacesBackups.getFilenameForDate(dateObj); + let oldBackup = OS.Path.join(backupFolder, oldBackupName); + let {count: count, hash: hash} = yield BookmarkJSONUtils.exportToFile(oldBackup); + do_check_true(count > 0); + do_check_eq(hash.length, 24); + oldBackupName = oldBackupName.replace(/\.json/, "_" + count + "_" + hash + ".json"); + yield OS.File.move(oldBackup, OS.Path.join(backupFolder, oldBackupName)); + + // Create a backup. + // This should just rename the existing backup, so in the end there should be + // only one backup with today's date. + yield PlacesBackups.create(); + + // Get the hash of the generated backup + let backupFiles = yield PlacesBackups.getBackupFiles(); + do_check_eq(backupFiles.length, 1); + + let matches = OS.Path.basename(backupFiles[0]).match(PlacesBackups.filenamesRegex); + do_check_eq(matches[1], PlacesBackups.toISODateString(new Date())); + do_check_eq(matches[2], count); + do_check_eq(matches[3], hash); + + // Add a bookmark and create another backup. + let bookmarkId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder, + uri("http://foo.com"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "foo"); + // We must enforce a backup since one for today already exists. The forced + // backup will replace the existing one. + yield PlacesBackups.create(undefined, true); + do_check_eq(backupFiles.length, 1); + recentBackup = yield PlacesBackups.getMostRecentBackup(); + do_check_neq(recentBackup, OS.Path.join(backupFolder, oldBackupName)); + matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex); + do_check_eq(matches[1], PlacesBackups.toISODateString(new Date())); + do_check_eq(matches[2], count + 1); + do_check_neq(matches[3], hash); + + // Clean up + PlacesUtils.bookmarks.removeItem(bookmarkId); + yield PlacesBackups.create(0); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js new file mode 100644 index 000000000..2c84990b3 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + run_next_test(); +} + +add_task(function* compress_bookmark_backups_test() { + // Check for jsonlz4 extension + let todayFilename = PlacesBackups.getFilenameForDate(new Date(2014, 4, 15), true); + do_check_eq(todayFilename, "bookmarks-2014-05-15.jsonlz4"); + + yield PlacesBackups.create(); + + // Check that a backup for today has been created and the regex works fine for lz4. + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1); + let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + do_check_neq(mostRecentBackupFile, null); + do_check_true(PlacesBackups.filenamesRegex.test(OS.Path.basename(mostRecentBackupFile))); + + // The most recent backup file has to be removed since saveBookmarksToJSONFile + // will otherwise over-write the current backup, since it will be made on the + // same date + yield OS.File.remove(mostRecentBackupFile); + do_check_false((yield OS.File.exists(mostRecentBackupFile))); + + // Check that, if the user created a custom backup out of the default + // backups folder, it gets copied (compressed) into it. + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json"); + yield PlacesBackups.saveBookmarksToJSONFile(jsonFile); + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1); + + // Check if import works from lz4 compressed json + let uri = NetUtil.newURI("http://www.mozilla.org/en-US/"); + let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark"); + + // Force create a compressed backup, Remove the bookmark, the restore the backup + yield PlacesBackups.create(undefined, true); + let recentBackup = yield PlacesBackups.getMostRecentBackup(); + PlacesUtils.bookmarks.removeItem(bm); + yield BookmarkJSONUtils.importFromFile(recentBackup, true); + let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let node = root.getChild(0); + do_check_eq(node.uri, uri.spec); + + root.containerOpen = false; + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); + + // Cleanup. + yield OS.File.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js new file mode 100644 index 000000000..4ea07fb39 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * To confirm that metadata i.e. bookmark count is set and retrieved for + * automatic backups. + */ +function run_test() { + run_next_test(); +} + +add_task(function* test_saveBookmarksToJSONFile_and_create() +{ + // Add a bookmark + let uri = NetUtil.newURI("http://getfirefox.com/"); + let bookmarkId = + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!"); + + // Test saveBookmarksToJSONFile() + let backupFile = FileUtils.getFile("TmpD", ["bookmarks.json"]); + backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, parseInt("0600", 8)); + + let nodeCount = yield PlacesBackups.saveBookmarksToJSONFile(backupFile, true); + do_check_true(nodeCount > 0); + do_check_true(backupFile.exists()); + do_check_eq(backupFile.leafName, "bookmarks.json"); + + // Ensure the backup would be copied to our backups folder when the original + // backup is saved somewhere else. + let recentBackup = yield PlacesBackups.getMostRecentBackup(); + let matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex); + do_check_eq(matches[2], nodeCount); + do_check_eq(matches[3].length, 24); + + // Clear all backups in our backups folder. + yield PlacesBackups.create(0); + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0); + + // Test create() which saves bookmarks with metadata on the filename. + yield PlacesBackups.create(); + do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1); + + mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup(); + do_check_neq(mostRecentBackupFile, null); + matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex); + do_check_eq(matches[2], nodeCount); + do_check_eq(matches[3].length, 24); + + // Cleanup + backupFile.remove(false); + yield PlacesBackups.create(0); + PlacesUtils.bookmarks.removeItem(bookmarkId); +}); + diff --git a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js new file mode 100644 index 000000000..f5e9f8187 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks that backups properly include all of the bookmarks if the hierarchy + * in the database is unordered so that a hierarchy is defined before its + * ancestor in the bookmarks table. + */ +function run_test() { + run_next_test(); +} + +add_task(function*() { + let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + NetUtil.newURI("http://mozilla.org/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark"); + let f2 = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, "f2", + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.moveItem(bm, f2, PlacesUtils.bookmarks.DEFAULT_INDEX); + let f1 = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, "f1", + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.moveItem(f2, f1, PlacesUtils.bookmarks.DEFAULT_INDEX); + + // Create a backup. + yield PlacesBackups.create(); + + // Remove the bookmarks, then restore the backup. + PlacesUtils.bookmarks.removeItem(f1); + yield BookmarkJSONUtils.importFromFile((yield PlacesBackups.getMostRecentBackup()), true); + + do_print("Checking first level"); + let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let level1 = root.getChild(0); + do_check_eq(level1.title, "f1"); + do_print("Checking second level"); + PlacesUtils.asContainer(level1).containerOpen = true + let level2 = level1.getChild(0); + do_check_eq(level2.title, "f2"); + do_print("Checking bookmark"); + PlacesUtils.asContainer(level2).containerOpen = true + let bookmark = level2.getChild(0); + do_check_eq(bookmark.title, "bookmark"); + level2.containerOpen = false; + level1.containerOpen = false; + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js new file mode 100644 index 000000000..b900887b5 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks that we don't encodeURI twice when creating bookmarks.html. + */ + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let uri = NetUtil.newURI("http://bt.ktxp.com/search.php?keyword=%E5%A6%84%E6%83%B3%E5%AD%A6%E7%94%9F%E4%BC%9A"); + let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark"); + + let file = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.997030.html"); + if ((yield OS.File.exists(file))) { + yield OS.File.remove(file); + } + yield BookmarkHTMLUtils.exportToFile(file); + + // Remove the bookmarks, then restore the backup. + PlacesUtils.bookmarks.removeItem(bm); + yield BookmarkHTMLUtils.importFromFile(file, true); + + do_print("Checking first level"); + let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let node = root.getChild(0); + do_check_eq(node.uri, uri.spec); + + root.containerOpen = false; + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_async_observers.js b/toolkit/components/places/tests/bookmarks/test_async_observers.js new file mode 100644 index 000000000..86d48ac24 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test checks that bookmarks service is correctly forwarding async + * events like visit or favicon additions. */ + +const NOW = Date.now() * 1000; + +var observer = { + bookmarks: [], + observedBookmarks: 0, + observedVisitId: 0, + deferred: null, + + /** + * Returns a promise that is resolved when the observer determines that the + * test can continue. This is required rather than calling run_next_test + * directly in the observer because there are cases where we must wait for + * other asynchronous events to be completed in addition to this. + */ + setupCompletionPromise: function () + { + this.observedBookmarks = 0; + this.deferred = Promise.defer(); + return this.deferred.promise; + }, + + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemAdded: function () {}, + onItemRemoved: function () {}, + onItemMoved: function () {}, + onItemChanged: function(aItemId, aProperty, aIsAnnotation, aNewValue, + aLastModified, aItemType) + { + do_print("Check that we got the correct change information."); + do_check_neq(this.bookmarks.indexOf(aItemId), -1); + if (aProperty == "favicon") { + do_check_false(aIsAnnotation); + do_check_eq(aNewValue, SMALLPNG_DATA_URI.spec); + do_check_eq(aLastModified, 0); + do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK); + } + else if (aProperty == "cleartime") { + do_check_false(aIsAnnotation); + do_check_eq(aNewValue, ""); + do_check_eq(aLastModified, 0); + do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK); + } + else { + do_throw("Unexpected property change " + aProperty); + } + + if (++this.observedBookmarks == this.bookmarks.length) { + this.deferred.resolve(); + } + }, + onItemVisited: function(aItemId, aVisitId, aTime) + { + do_print("Check that we got the correct visit information."); + do_check_neq(this.bookmarks.indexOf(aItemId), -1); + this.observedVisitId = aVisitId; + do_check_eq(aTime, NOW); + if (++this.observedBookmarks == this.bookmarks.length) { + this.deferred.resolve(); + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + ]) +}; +PlacesUtils.bookmarks.addObserver(observer, false); + +add_task(function* test_add_visit() +{ + let observerPromise = observer.setupCompletionPromise(); + + // Add a visit to the bookmark and wait for the observer. + let visitId; + let deferUpdatePlaces = Promise.defer(); + PlacesUtils.asyncHistory.updatePlaces({ + uri: NetUtil.newURI("http://book.ma.rk/"), + visits: [{ transitionType: TRANSITION_TYPED, visitDate: NOW }] + }, { + handleError: function TAV_handleError() { + deferUpdatePlaces.reject(new Error("Unexpected error in adding visit.")); + }, + handleResult: function (aPlaceInfo) { + visitId = aPlaceInfo.visits[0].visitId; + }, + handleCompletion: function TAV_handleCompletion() { + deferUpdatePlaces.resolve(); + } + }); + + // Wait for both the observer and the asynchronous update, in any order. + yield deferUpdatePlaces.promise; + yield observerPromise; + + // Check that both asynchronous results are consistent. + do_check_eq(observer.observedVisitId, visitId); +}); + +add_task(function* test_add_icon() +{ + let observerPromise = observer.setupCompletionPromise(); + PlacesUtils.favicons.setAndFetchFaviconForPage(NetUtil.newURI("http://book.ma.rk/"), + SMALLPNG_DATA_URI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal()); + yield observerPromise; +}); + +add_task(function* test_remove_page() +{ + let observerPromise = observer.setupCompletionPromise(); + PlacesUtils.history.removePage(NetUtil.newURI("http://book.ma.rk/")); + yield observerPromise; +}); + +add_task(function cleanup() +{ + PlacesUtils.bookmarks.removeObserver(observer, false); +}); + +add_task(function* shutdown() +{ + // Check that async observers don't try to create async statements after + // shutdown. That would cause assertions, since the async thread is gone + // already. Note that in such a case the notifications are not fired, so we + // cannot test for them. + // Put an history notification that triggers AsyncGetBookmarksForURI between + // asyncClose() and the actual connection closing. Enqueuing a main-thread + // event just after places-will-close-connection should ensure it runs before + // places-connection-closed. + // Notice this code is not using helpers cause it depends on a very specific + // order, a change in the helpers code could make this test useless. + let deferred = Promise.defer(); + + Services.obs.addObserver(function onNotification() { + Services.obs.removeObserver(onNotification, "places-will-close-connection"); + do_check_true(true, "Observed fake places shutdown"); + + Services.tm.mainThread.dispatch(() => { + // WARNING: this is very bad, never use out of testing code. + PlacesUtils.bookmarks.QueryInterface(Ci.nsINavHistoryObserver) + .onPageChanged(NetUtil.newURI("http://book.ma.rk/"), + Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON, + "test", "test"); + deferred.resolve(promiseTopicObserved("places-connection-closed")); + }, Ci.nsIThread.DISPATCH_NORMAL); + }, "places-will-close-connection", false); + shutdownPlaces(); + + yield deferred.promise; +}); + +function run_test() +{ + // Add multiple bookmarks to the same uri. + observer.bookmarks.push( + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + NetUtil.newURI("http://book.ma.rk/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Bookmark") + ); + observer.bookmarks.push( + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + NetUtil.newURI("http://book.ma.rk/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Bookmark") + ); + + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bmindex.js b/toolkit/components/places/tests/bookmarks/test_bmindex.js new file mode 100644 index 000000000..c764e4310 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bmindex.js @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const NUM_BOOKMARKS = 20; +const NUM_SEPARATORS = 5; +const NUM_FOLDERS = 10; +const NUM_ITEMS = NUM_BOOKMARKS + NUM_SEPARATORS + NUM_FOLDERS; +const MIN_RAND = -5; +const MAX_RAND = 40; + +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +function check_contiguous_indexes(aBookmarks) { + var indexes = []; + aBookmarks.forEach(function(aBookmarkId) { + let bmIndex = bs.getItemIndex(aBookmarkId); + dump("Index: " + bmIndex + "\n"); + dump("Checking duplicates\n"); + do_check_eq(indexes.indexOf(bmIndex), -1); + dump("Checking out of range, found " + aBookmarks.length + " items\n"); + do_check_true(bmIndex >= 0 && bmIndex < aBookmarks.length); + indexes.push(bmIndex); + }); + dump("Checking all valid indexes have been used\n"); + do_check_eq(indexes.length, aBookmarks.length); +} + +// main +function run_test() { + var bookmarks = []; + // Insert bookmarks with random indexes. + for (let i = 0; bookmarks.length < NUM_BOOKMARKS; i++) { + let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND))); + try { + let id = bs.insertBookmark(bs.unfiledBookmarksFolder, + uri("http://" + i + ".mozilla.org/"), + randIndex, "Test bookmark " + i); + if (randIndex < -1) + do_throw("Creating a bookmark at an invalid index should throw"); + bookmarks.push(id); + } + catch (ex) { + if (randIndex >= -1) + do_throw("Creating a bookmark at a valid index should not throw"); + } + } + check_contiguous_indexes(bookmarks); + + // Insert separators with random indexes. + for (let i = 0; bookmarks.length < NUM_BOOKMARKS + NUM_SEPARATORS; i++) { + let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND))); + try { + let id = bs.insertSeparator(bs.unfiledBookmarksFolder, randIndex); + if (randIndex < -1) + do_throw("Creating a separator at an invalid index should throw"); + bookmarks.push(id); + } + catch (ex) { + if (randIndex >= -1) + do_throw("Creating a separator at a valid index should not throw"); + } + } + check_contiguous_indexes(bookmarks); + + // Insert folders with random indexes. + for (let i = 0; bookmarks.length < NUM_ITEMS; i++) { + let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND))); + try { + let id = bs.createFolder(bs.unfiledBookmarksFolder, + "Test folder " + i, randIndex); + if (randIndex < -1) + do_throw("Creating a folder at an invalid index should throw"); + bookmarks.push(id); + } + catch (ex) { + if (randIndex >= -1) + do_throw("Creating a folder at a valid index should not throw"); + } + } + check_contiguous_indexes(bookmarks); + + // Execute some random bookmark delete. + for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) { + let id = bookmarks.splice(Math.floor(Math.random() * bookmarks.length), 1); + dump("Removing item with id " + id + "\n"); + bs.removeItem(id); + } + check_contiguous_indexes(bookmarks); + + // Execute some random bookmark move. This will also try to move it to + // invalid index values. + for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) { + let randIndex = Math.floor(Math.random() * bookmarks.length); + let id = bookmarks[randIndex]; + let newIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND))); + dump("Moving item with id " + id + " to index " + newIndex + "\n"); + try { + bs.moveItem(id, bs.unfiledBookmarksFolder, newIndex); + if (newIndex < -1) + do_throw("Moving an item to a negative index should throw\n"); + } + catch (ex) { + if (newIndex >= -1) + do_throw("Moving an item to a valid index should not throw\n"); + } + + } + check_contiguous_indexes(bookmarks); + + // Ensure setItemIndex throws if we pass it a negative index. + try { + bs.setItemIndex(bookmarks[0], -1); + do_throw("setItemIndex should throw for a negative index"); + } catch (ex) {} + // Ensure setItemIndex throws if we pass it a bad itemId. + try { + bs.setItemIndex(0, 5); + do_throw("setItemIndex should throw for a bad itemId"); + } catch (ex) {} +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks.js b/toolkit/components/places/tests/bookmarks/test_bookmarks.js new file mode 100644 index 000000000..b67482223 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js @@ -0,0 +1,718 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var bs = PlacesUtils.bookmarks; +var hs = PlacesUtils.history; +var anno = PlacesUtils.annotations; + + +var bookmarksObserver = { + onBeginUpdateBatch: function() { + this._beginUpdateBatch = true; + }, + onEndUpdateBatch: function() { + this._endUpdateBatch = true; + }, + onItemAdded: function(id, folder, index, itemType, uri, title, dateAdded, + guid) { + this._itemAddedId = id; + this._itemAddedParent = folder; + this._itemAddedIndex = index; + this._itemAddedURI = uri; + this._itemAddedTitle = title; + + // Ensure that we've created a guid for this item. + let stmt = DBConn().createStatement( + `SELECT guid + FROM moz_bookmarks + WHERE id = :item_id` + ); + stmt.params.item_id = id; + do_check_true(stmt.executeStep()); + do_check_false(stmt.getIsNull(0)); + do_check_valid_places_guid(stmt.row.guid); + do_check_eq(stmt.row.guid, guid); + stmt.finalize(); + }, + onItemRemoved: function(id, folder, index, itemType) { + this._itemRemovedId = id; + this._itemRemovedFolder = folder; + this._itemRemovedIndex = index; + }, + onItemChanged: function(id, property, isAnnotationProperty, value, + lastModified, itemType, parentId, guid, parentGuid, + oldValue) { + this._itemChangedId = id; + this._itemChangedProperty = property; + this._itemChanged_isAnnotationProperty = isAnnotationProperty; + this._itemChangedValue = value; + this._itemChangedOldValue = oldValue; + }, + onItemVisited: function(id, visitID, time) { + this._itemVisitedId = id; + this._itemVisitedVistId = visitID; + this._itemVisitedTime = time; + }, + onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex, + itemType) { + this._itemMovedId = id + this._itemMovedOldParent = oldParent; + this._itemMovedOldIndex = oldIndex; + this._itemMovedNewParent = newParent; + this._itemMovedNewIndex = newIndex; + }, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + ]) +}; + + +// Get bookmarks menu folder id. +var root = bs.bookmarksMenuFolder; +// Index at which items should begin. +var bmStartIndex = 0; + + +function run_test() { + run_next_test(); +} + +add_task(function* test_bookmarks() { + bs.addObserver(bookmarksObserver, false); + + // test special folders + do_check_true(bs.placesRoot > 0); + do_check_true(bs.bookmarksMenuFolder > 0); + do_check_true(bs.tagsFolder > 0); + do_check_true(bs.toolbarFolder > 0); + do_check_true(bs.unfiledBookmarksFolder > 0); + + // test getFolderIdForItem() with bogus item id will throw + try { + bs.getFolderIdForItem(0); + do_throw("getFolderIdForItem accepted bad input"); + } catch (ex) {} + + // test getFolderIdForItem() with bogus item id will throw + try { + bs.getFolderIdForItem(-1); + do_throw("getFolderIdForItem accepted bad input"); + } catch (ex) {} + + // test root parentage + do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot); + + // create a folder to hold all the tests + // this makes the tests more tolerant of changes to default_places.html + let testRoot = bs.createFolder(root, "places bookmarks xpcshell tests", + bs.DEFAULT_INDEX); + do_check_eq(bookmarksObserver._itemAddedId, testRoot); + do_check_eq(bookmarksObserver._itemAddedParent, root); + do_check_eq(bookmarksObserver._itemAddedIndex, bmStartIndex); + do_check_eq(bookmarksObserver._itemAddedURI, null); + let testStartIndex = 0; + + // test getItemIndex for folders + do_check_eq(bs.getItemIndex(testRoot), bmStartIndex); + + // test getItemType for folders + do_check_eq(bs.getItemType(testRoot), bs.TYPE_FOLDER); + + // insert a bookmark. + // the time before we insert, in microseconds + let beforeInsert = Date.now() * 1000; + do_check_true(beforeInsert > 0); + + let newId = bs.insertBookmark(testRoot, uri("http://google.com/"), + bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId); + do_check_eq(bookmarksObserver._itemAddedParent, testRoot); + do_check_eq(bookmarksObserver._itemAddedIndex, testStartIndex); + do_check_true(bookmarksObserver._itemAddedURI.equals(uri("http://google.com/"))); + do_check_eq(bs.getBookmarkURI(newId).spec, "http://google.com/"); + + let dateAdded = bs.getItemDateAdded(newId); + // dateAdded can equal beforeInsert + do_check_true(is_time_ordered(beforeInsert, dateAdded)); + + // after just inserting, modified should not be set + let lastModified = bs.getItemLastModified(newId); + do_check_eq(lastModified, dateAdded); + + // The time before we set the title, in microseconds. + let beforeSetTitle = Date.now() * 1000; + do_check_true(beforeSetTitle >= beforeInsert); + + // Workaround possible VM timers issues moving lastModified and dateAdded + // to the past. + lastModified -= 1000; + bs.setItemLastModified(newId, lastModified); + dateAdded -= 1000; + bs.setItemDateAdded(newId, dateAdded); + + // set bookmark title + bs.setItemTitle(newId, "Google"); + do_check_eq(bookmarksObserver._itemChangedId, newId); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + do_check_eq(bookmarksObserver._itemChangedValue, "Google"); + + // check that dateAdded hasn't changed + let dateAdded2 = bs.getItemDateAdded(newId); + do_check_eq(dateAdded2, dateAdded); + + // check lastModified after we set the title + let lastModified2 = bs.getItemLastModified(newId); + do_print("test setItemTitle"); + do_print("dateAdded = " + dateAdded); + do_print("beforeSetTitle = " + beforeSetTitle); + do_print("lastModified = " + lastModified); + do_print("lastModified2 = " + lastModified2); + do_check_true(is_time_ordered(lastModified, lastModified2)); + do_check_true(is_time_ordered(dateAdded, lastModified2)); + + // get item title + let title = bs.getItemTitle(newId); + do_check_eq(title, "Google"); + + // test getItemType for bookmarks + do_check_eq(bs.getItemType(newId), bs.TYPE_BOOKMARK); + + // get item title bad input + try { + bs.getItemTitle(-3); + do_throw("getItemTitle accepted bad input"); + } catch (ex) {} + + // get the folder that the bookmark is in + let folderId = bs.getFolderIdForItem(newId); + do_check_eq(folderId, testRoot); + + // test getItemIndex for bookmarks + do_check_eq(bs.getItemIndex(newId), testStartIndex); + + // create a folder at a specific index + let workFolder = bs.createFolder(testRoot, "Work", 0); + do_check_eq(bookmarksObserver._itemAddedId, workFolder); + do_check_eq(bookmarksObserver._itemAddedParent, testRoot); + do_check_eq(bookmarksObserver._itemAddedIndex, 0); + do_check_eq(bookmarksObserver._itemAddedURI, null); + + do_check_eq(bs.getItemTitle(workFolder), "Work"); + bs.setItemTitle(workFolder, "Work #"); + do_check_eq(bs.getItemTitle(workFolder), "Work #"); + + // add item into subfolder, specifying index + let newId2 = bs.insertBookmark(workFolder, + uri("http://developer.mozilla.org/"), + 0, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId2); + do_check_eq(bookmarksObserver._itemAddedParent, workFolder); + do_check_eq(bookmarksObserver._itemAddedIndex, 0); + + // change item + bs.setItemTitle(newId2, "DevMo"); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + + // insert item into subfolder + let newId3 = bs.insertBookmark(workFolder, + uri("http://msdn.microsoft.com/"), + bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId3); + do_check_eq(bookmarksObserver._itemAddedParent, workFolder); + do_check_eq(bookmarksObserver._itemAddedIndex, 1); + + // change item + bs.setItemTitle(newId3, "MSDN"); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + + // remove item + bs.removeItem(newId2); + do_check_eq(bookmarksObserver._itemRemovedId, newId2); + do_check_eq(bookmarksObserver._itemRemovedFolder, workFolder); + do_check_eq(bookmarksObserver._itemRemovedIndex, 0); + + // insert item into subfolder + let newId4 = bs.insertBookmark(workFolder, + uri("http://developer.mozilla.org/"), + bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId4); + do_check_eq(bookmarksObserver._itemAddedParent, workFolder); + do_check_eq(bookmarksObserver._itemAddedIndex, 1); + + // create folder + let homeFolder = bs.createFolder(testRoot, "Home", bs.DEFAULT_INDEX); + do_check_eq(bookmarksObserver._itemAddedId, homeFolder); + do_check_eq(bookmarksObserver._itemAddedParent, testRoot); + do_check_eq(bookmarksObserver._itemAddedIndex, 2); + + // insert item + let newId5 = bs.insertBookmark(homeFolder, uri("http://espn.com/"), + bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId5); + do_check_eq(bookmarksObserver._itemAddedParent, homeFolder); + do_check_eq(bookmarksObserver._itemAddedIndex, 0); + + // change item + bs.setItemTitle(newId5, "ESPN"); + do_check_eq(bookmarksObserver._itemChangedId, newId5); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + + // insert query item + let uri6 = uri("place:domain=google.com&type="+ + Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY); + let newId6 = bs.insertBookmark(testRoot, uri6, bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedParent, testRoot); + do_check_eq(bookmarksObserver._itemAddedIndex, 3); + + // change item + bs.setItemTitle(newId6, "Google Sites"); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + + // test getIdForItemAt + do_check_eq(bs.getIdForItemAt(testRoot, 0), workFolder); + // wrong parent, should return -1 + do_check_eq(bs.getIdForItemAt(1337, 0), -1); + // wrong index, should return -1 + do_check_eq(bs.getIdForItemAt(testRoot, 1337), -1); + // wrong parent and index, should return -1 + do_check_eq(bs.getIdForItemAt(1337, 1337), -1); + + // move folder, appending, to different folder + let oldParentCC = getChildCount(testRoot); + bs.moveItem(workFolder, homeFolder, bs.DEFAULT_INDEX); + do_check_eq(bookmarksObserver._itemMovedId, workFolder); + do_check_eq(bookmarksObserver._itemMovedOldParent, testRoot); + do_check_eq(bookmarksObserver._itemMovedOldIndex, 0); + do_check_eq(bookmarksObserver._itemMovedNewParent, homeFolder); + do_check_eq(bookmarksObserver._itemMovedNewIndex, 1); + + // test that the new index is properly stored + do_check_eq(bs.getItemIndex(workFolder), 1); + do_check_eq(bs.getFolderIdForItem(workFolder), homeFolder); + + // try to get index of the item from within the old parent folder + // check that it has been really removed from there + do_check_neq(bs.getIdForItemAt(testRoot, 0), workFolder); + // check the last item from within the old parent folder + do_check_neq(bs.getIdForItemAt(testRoot, -1), workFolder); + // check the index of the item within the new parent folder + do_check_eq(bs.getIdForItemAt(homeFolder, 1), workFolder); + // try to get index of the last item within the new parent folder + do_check_eq(bs.getIdForItemAt(homeFolder, -1), workFolder); + // XXX expose FolderCount, and check that the old parent has one less child? + do_check_eq(getChildCount(testRoot), oldParentCC-1); + + // move item, appending, to different folder + bs.moveItem(newId5, testRoot, bs.DEFAULT_INDEX); + do_check_eq(bookmarksObserver._itemMovedId, newId5); + do_check_eq(bookmarksObserver._itemMovedOldParent, homeFolder); + do_check_eq(bookmarksObserver._itemMovedOldIndex, 0); + do_check_eq(bookmarksObserver._itemMovedNewParent, testRoot); + do_check_eq(bookmarksObserver._itemMovedNewIndex, 3); + + // test get folder's index + let tmpFolder = bs.createFolder(testRoot, "tmp", 2); + do_check_eq(bs.getItemIndex(tmpFolder), 2); + + // test setKeywordForBookmark + let kwTestItemId = bs.insertBookmark(testRoot, uri("http://keywordtest.com"), + bs.DEFAULT_INDEX, ""); + bs.setKeywordForBookmark(kwTestItemId, "bar"); + + // test getKeywordForBookmark + let k = bs.getKeywordForBookmark(kwTestItemId); + do_check_eq("bar", k); + + // test getURIForKeyword + let u = bs.getURIForKeyword("bar"); + do_check_eq("http://keywordtest.com/", u.spec); + + // test removeFolderChildren + // 1) add/remove each child type (bookmark, separator, folder) + tmpFolder = bs.createFolder(testRoot, "removeFolderChildren", + bs.DEFAULT_INDEX); + bs.insertBookmark(tmpFolder, uri("http://foo9.com/"), bs.DEFAULT_INDEX, ""); + bs.createFolder(tmpFolder, "subfolder", bs.DEFAULT_INDEX); + bs.insertSeparator(tmpFolder, bs.DEFAULT_INDEX); + // 2) confirm that folder has 3 children + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + query.setFolders([tmpFolder], 1); + try { + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 3); + rootNode.containerOpen = false; + } catch (ex) { + do_throw("test removeFolderChildren() - querying for children failed: " + ex); + } + // 3) remove all children + bs.removeFolderChildren(tmpFolder); + // 4) confirm that folder has 0 children + try { + result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 0); + rootNode.containerOpen = false; + } catch (ex) { + do_throw("removeFolderChildren(): " + ex); + } + + // XXX - test folderReadOnly + + // test bookmark id in query output + try { + options = hs.getNewQueryOptions(); + query = hs.getNewQuery(); + query.setFolders([testRoot], 1); + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_print("bookmark itemId test: CC = " + cc); + do_check_true(cc > 0); + for (let i=0; i < cc; ++i) { + let node = rootNode.getChild(i); + if (node.type == node.RESULT_TYPE_FOLDER || + node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_SEPARATOR || + node.type == node.RESULT_TYPE_QUERY) { + do_check_true(node.itemId > 0); + } + else { + do_check_eq(node.itemId, -1); + } + } + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("bookmarks query: " + ex); + } + + // test that multiple bookmarks with same URI show up right in bookmark + // folder queries, todo: also to do for complex folder queries + try { + // test uri + let mURI = uri("http://multiple.uris.in.query"); + + let testFolder = bs.createFolder(testRoot, "test Folder", bs.DEFAULT_INDEX); + // add 2 bookmarks + bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 1"); + bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 2"); + + // query + options = hs.getNewQueryOptions(); + query = hs.getNewQuery(); + query.setFolders([testFolder], 1); + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_eq(cc, 2); + do_check_eq(rootNode.getChild(0).title, "title 1"); + do_check_eq(rootNode.getChild(1).title, "title 2"); + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("bookmarks query: " + ex); + } + + // test change bookmark uri + let newId10 = bs.insertBookmark(testRoot, uri("http://foo10.com/"), + bs.DEFAULT_INDEX, ""); + dateAdded = bs.getItemDateAdded(newId10); + // after just inserting, modified should not be set + lastModified = bs.getItemLastModified(newId10); + do_check_eq(lastModified, dateAdded); + + // Workaround possible VM timers issues moving lastModified and dateAdded + // to the past. + lastModified -= 1000; + bs.setItemLastModified(newId10, lastModified); + dateAdded -= 1000; + bs.setItemDateAdded(newId10, dateAdded); + + bs.changeBookmarkURI(newId10, uri("http://foo11.com/")); + + // check that lastModified is set after we change the bookmark uri + lastModified2 = bs.getItemLastModified(newId10); + do_print("test changeBookmarkURI"); + do_print("dateAdded = " + dateAdded); + do_print("lastModified = " + lastModified); + do_print("lastModified2 = " + lastModified2); + do_check_true(is_time_ordered(lastModified, lastModified2)); + do_check_true(is_time_ordered(dateAdded, lastModified2)); + + do_check_eq(bookmarksObserver._itemChangedId, newId10); + do_check_eq(bookmarksObserver._itemChangedProperty, "uri"); + do_check_eq(bookmarksObserver._itemChangedValue, "http://foo11.com/"); + do_check_eq(bookmarksObserver._itemChangedOldValue, "http://foo10.com/"); + + // test getBookmarkURI + let newId11 = bs.insertBookmark(testRoot, uri("http://foo11.com/"), + bs.DEFAULT_INDEX, ""); + let bmURI = bs.getBookmarkURI(newId11); + do_check_eq("http://foo11.com/", bmURI.spec); + + // test getBookmarkURI with non-bookmark items + try { + bs.getBookmarkURI(testRoot); + do_throw("getBookmarkURI() should throw for non-bookmark items!"); + } catch (ex) {} + + // test getItemIndex + let newId12 = bs.insertBookmark(testRoot, uri("http://foo11.com/"), 1, ""); + let bmIndex = bs.getItemIndex(newId12); + do_check_eq(1, bmIndex); + + // insert a bookmark with title ZZZXXXYYY and then search for it. + // this test confirms that we can find bookmarks that we haven't visited + // (which are "hidden") and that we can find by title. + // see bug #369887 for more details + let newId13 = bs.insertBookmark(testRoot, uri("http://foobarcheese.com/"), + bs.DEFAULT_INDEX, ""); + do_check_eq(bookmarksObserver._itemAddedId, newId13); + do_check_eq(bookmarksObserver._itemAddedParent, testRoot); + do_check_eq(bookmarksObserver._itemAddedIndex, 11); + + // set bookmark title + bs.setItemTitle(newId13, "ZZZXXXYYY"); + do_check_eq(bookmarksObserver._itemChangedId, newId13); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + do_check_eq(bookmarksObserver._itemChangedValue, "ZZZXXXYYY"); + + // check if setting an item annotation triggers onItemChanged + bookmarksObserver._itemChangedId = -1; + anno.setItemAnnotation(newId3, "test-annotation", "foo", 0, 0); + do_check_eq(bookmarksObserver._itemChangedId, newId3); + do_check_eq(bookmarksObserver._itemChangedProperty, "test-annotation"); + do_check_true(bookmarksObserver._itemChanged_isAnnotationProperty); + do_check_eq(bookmarksObserver._itemChangedValue, ""); + + // test search on bookmark title ZZZXXXYYY + try { + options = hs.getNewQueryOptions(); + options.excludeQueries = 1; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + query = hs.getNewQuery(); + query.searchTerms = "ZZZXXXYYY"; + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_eq(cc, 1); + let node = rootNode.getChild(0); + do_check_eq(node.title, "ZZZXXXYYY"); + do_check_true(node.itemId > 0); + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("bookmarks query: " + ex); + } + + // test dateAdded and lastModified properties + // for a search query + try { + options = hs.getNewQueryOptions(); + options.excludeQueries = 1; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + query = hs.getNewQuery(); + query.searchTerms = "ZZZXXXYYY"; + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_eq(cc, 1); + let node = rootNode.getChild(0); + + do_check_eq(typeof node.dateAdded, "number"); + do_check_true(node.dateAdded > 0); + + do_check_eq(typeof node.lastModified, "number"); + do_check_true(node.lastModified > 0); + + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("bookmarks query: " + ex); + } + + // test dateAdded and lastModified properties + // for a folder query + try { + options = hs.getNewQueryOptions(); + query = hs.getNewQuery(); + query.setFolders([testRoot], 1); + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_true(cc > 0); + for (let i = 0; i < cc; i++) { + let node = rootNode.getChild(i); + + if (node.type == node.RESULT_TYPE_URI) { + do_check_eq(typeof node.dateAdded, "number"); + do_check_true(node.dateAdded > 0); + + do_check_eq(typeof node.lastModified, "number"); + do_check_true(node.lastModified > 0); + break; + } + } + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("bookmarks query: " + ex); + } + + // check setItemLastModified() and setItemDateAdded() + let newId14 = bs.insertBookmark(testRoot, uri("http://bar.tld/"), + bs.DEFAULT_INDEX, ""); + dateAdded = bs.getItemDateAdded(newId14); + lastModified = bs.getItemLastModified(newId14); + do_check_eq(lastModified, dateAdded); + bs.setItemLastModified(newId14, 1234000000000000); + let fakeLastModified = bs.getItemLastModified(newId14); + do_check_eq(fakeLastModified, 1234000000000000); + bs.setItemDateAdded(newId14, 4321000000000000); + let fakeDateAdded = bs.getItemDateAdded(newId14); + do_check_eq(fakeDateAdded, 4321000000000000); + + // ensure that removing an item removes its annotations + do_check_true(anno.itemHasAnnotation(newId3, "test-annotation")); + bs.removeItem(newId3); + do_check_false(anno.itemHasAnnotation(newId3, "test-annotation")); + + // bug 378820 + let uri1 = uri("http://foo.tld/a"); + bs.insertBookmark(testRoot, uri1, bs.DEFAULT_INDEX, ""); + yield PlacesTestUtils.addVisits(uri1); + + // bug 646993 - test bookmark titles longer than the maximum allowed length + let title15 = Array(TITLE_LENGTH_MAX + 5).join("X"); + let title15expected = title15.substring(0, TITLE_LENGTH_MAX); + let newId15 = bs.insertBookmark(testRoot, uri("http://evil.com/"), + bs.DEFAULT_INDEX, title15); + + do_check_eq(bs.getItemTitle(newId15).length, + title15expected.length); + do_check_eq(bookmarksObserver._itemAddedTitle, title15expected); + // test title length after updates + bs.setItemTitle(newId15, title15 + " updated"); + do_check_eq(bs.getItemTitle(newId15).length, + title15expected.length); + do_check_eq(bookmarksObserver._itemChangedId, newId15); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + do_check_eq(bookmarksObserver._itemChangedValue, title15expected); + + testSimpleFolderResult(); +}); + +function testSimpleFolderResult() { + // the time before we create a folder, in microseconds + // Workaround possible VM timers issues subtracting 1us. + let beforeCreate = Date.now() * 1000 - 1; + do_check_true(beforeCreate > 0); + + // create a folder + let parent = bs.createFolder(root, "test", bs.DEFAULT_INDEX); + + let dateCreated = bs.getItemDateAdded(parent); + do_print("check that the folder was created with a valid dateAdded"); + do_print("beforeCreate = " + beforeCreate); + do_print("dateCreated = " + dateCreated); + do_check_true(is_time_ordered(beforeCreate, dateCreated)); + + // the time before we insert, in microseconds + // Workaround possible VM timers issues subtracting 1ms. + let beforeInsert = Date.now() * 1000 - 1; + do_check_true(beforeInsert > 0); + + // insert a separator + let sep = bs.insertSeparator(parent, bs.DEFAULT_INDEX); + + let dateAdded = bs.getItemDateAdded(sep); + do_print("check that the separator was created with a valid dateAdded"); + do_print("beforeInsert = " + beforeInsert); + do_print("dateAdded = " + dateAdded); + do_check_true(is_time_ordered(beforeInsert, dateAdded)); + + // re-set item title separately so can test nodes' last modified + let item = bs.insertBookmark(parent, uri("about:blank"), + bs.DEFAULT_INDEX, ""); + bs.setItemTitle(item, "test bookmark"); + + // see above + let folder = bs.createFolder(parent, "test folder", bs.DEFAULT_INDEX); + bs.setItemTitle(folder, "test folder"); + + let longName = Array(TITLE_LENGTH_MAX + 5).join("A"); + let folderLongName = bs.createFolder(parent, longName, bs.DEFAULT_INDEX); + do_check_eq(bookmarksObserver._itemAddedTitle, longName.substring(0, TITLE_LENGTH_MAX)); + + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + query.setFolders([parent], 1); + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + do_check_eq(rootNode.childCount, 4); + + let node = rootNode.getChild(0); + do_check_true(node.dateAdded > 0); + do_check_eq(node.lastModified, node.dateAdded); + do_check_eq(node.itemId, sep); + do_check_eq(node.title, ""); + node = rootNode.getChild(1); + do_check_eq(node.itemId, item); + do_check_true(node.dateAdded > 0); + do_check_true(node.lastModified > 0); + do_check_eq(node.title, "test bookmark"); + node = rootNode.getChild(2); + do_check_eq(node.itemId, folder); + do_check_eq(node.title, "test folder"); + do_check_true(node.dateAdded > 0); + do_check_true(node.lastModified > 0); + node = rootNode.getChild(3); + do_check_eq(node.itemId, folderLongName); + do_check_eq(node.title, longName.substring(0, TITLE_LENGTH_MAX)); + do_check_true(node.dateAdded > 0); + do_check_true(node.lastModified > 0); + + // update with another long title + bs.setItemTitle(folderLongName, longName + " updated"); + do_check_eq(bookmarksObserver._itemChangedId, folderLongName); + do_check_eq(bookmarksObserver._itemChangedProperty, "title"); + do_check_eq(bookmarksObserver._itemChangedValue, longName.substring(0, TITLE_LENGTH_MAX)); + + node = rootNode.getChild(3); + do_check_eq(node.title, longName.substring(0, TITLE_LENGTH_MAX)); + + rootNode.containerOpen = false; +} + +function getChildCount(aFolderId) { + let cc = -1; + try { + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + query.setFolders([aFolderId], 1); + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + cc = rootNode.childCount; + rootNode.containerOpen = false; + } catch (ex) { + do_throw("getChildCount failed: " + ex); + } + return cc; +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js new file mode 100644 index 000000000..e8414359b --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* test_eraseEverything() { + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://example.com/") }); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/") }); + let frecencyForExample = frecencyForUrl("http://example.com/"); + let frecencyForMozilla = frecencyForUrl("http://example.com/"); + Assert.ok(frecencyForExample > 0); + Assert.ok(frecencyForMozilla > 0); + let unfiledFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(unfiledFolder); + let unfiledBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/" }); + checkBookmarkObject(unfiledBookmark); + let unfiledBookmarkInFolder = + yield PlacesUtils.bookmarks.insert({ parentGuid: unfiledFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/" }); + checkBookmarkObject(unfiledBookmarkInFolder); + PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(unfiledBookmarkInFolder.guid)), + "testanno1", "testvalue1", 0, 0); + + let menuFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(menuFolder); + let menuBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/" }); + checkBookmarkObject(menuBookmark); + let menuBookmarkInFolder = + yield PlacesUtils.bookmarks.insert({ parentGuid: menuFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/" }); + checkBookmarkObject(menuBookmarkInFolder); + PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(menuBookmarkInFolder.guid)), + "testanno1", "testvalue1", 0, 0); + + let toolbarFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(toolbarFolder); + let toolbarBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/" }); + checkBookmarkObject(toolbarBookmark); + let toolbarBookmarkInFolder = + yield PlacesUtils.bookmarks.insert({ parentGuid: toolbarFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/" }); + checkBookmarkObject(toolbarBookmarkInFolder); + PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(toolbarBookmarkInFolder.guid)), + "testanno1", "testvalue1", 0, 0); + + yield PlacesTestUtils.promiseAsyncUpdates(); + Assert.ok(frecencyForUrl("http://example.com/") > frecencyForExample); + Assert.ok(frecencyForUrl("http://example.com/") > frecencyForMozilla); + + yield PlacesUtils.bookmarks.eraseEverything(); + + Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample); + Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla); + + // Check there are no orphan annotations. + let conn = yield PlacesUtils.promiseDBConnection(); + let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`); + // Bug 1306445 will eventually remove the mobile root anno. + Assert.equal(annoAttrs.length, 1); + Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO); + let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`); + Assert.equal(annos.length, 1); + Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId); + Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id")); +}); + +add_task(function* test_eraseEverything_roots() { + yield PlacesUtils.bookmarks.eraseEverything(); + + // Ensure the roots have not been removed. + Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid)); + Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid)); + Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.menuGuid)); + Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.tagsGuid)); + Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.rootGuid)); +}); + +add_task(function* test_eraseEverything_reparented() { + // Create a folder with 1 bookmark in it... + let folder1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER + }); + let bookmark1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://example.com/" + }); + // ...and a second folder. + let folder2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER + }); + + // Reparent the bookmark to the 2nd folder. + bookmark1.parentGuid = folder2.guid; + yield PlacesUtils.bookmarks.update(bookmark1); + + // Erase everything. + yield PlacesUtils.bookmarks.eraseEverything(); + + // All the above items should no longer be in the GUIDHelper cache. + for (let guid of [folder1.guid, bookmark1.guid, folder2.guid]) { + yield Assert.rejects(PlacesUtils.promiseItemId(guid), + /no item found for the given GUID/); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js new file mode 100644 index 000000000..9527f02e6 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js @@ -0,0 +1,310 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gAccumulator = { + get callback() { + this.results = []; + return result => this.results.push(result); + } +}; + +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.fetch(), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.fetch(null), + /Input should be a valid object/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012", + parentGuid: "012345678901" }), + /The following properties were expected: index/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012", + index: 0 }), + /The following properties were expected: parentGuid/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({}), + /Unexpected number of conditions provided: 0/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012", + parentGuid: "012345678901", + index: 0 }), + /Unexpected number of conditions provided: 2/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012", + url: "http://example.com"}), + /Unexpected number of conditions provided: 2/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch("test"), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch(123), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "test" }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: null }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: 123 }), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "test", + index: 0 }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: null, + index: 0 }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: 123, + index: 0 }), + /Invalid value for property 'parentGuid'/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", + index: "0" }), + /Invalid value for property 'index'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", + index: null }), + /Invalid value for property 'index'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", + index: -10 }), + /Invalid value for property 'index'/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: "http://te st/" }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: null }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: -10 }), + /Invalid value for property 'url'/); + + Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", "test"), + /onResult callback must be a valid function/); + Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", {}), + /onResult callback must be a valid function/); +}); + +add_task(function* fetch_nonexistent_guid() { + let bm = yield PlacesUtils.bookmarks.fetch({ guid: "123456789012" }, + gAccumulator.callback); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(function* fetch_bookmark() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid, + gAccumulator.callback); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/"); + Assert.equal(bm2.title, "a bookmark"); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_bookmar_empty_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.ok(!("title" in bm2)); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_folder() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm2.title, "a folder"); + Assert.ok(!("url" in bm2)); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_folder_empty_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.ok(!("title" in bm2)); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_separator() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("url" in bm2)); + Assert.ok(!("title" in bm2)); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_byposition_nonexisting_parentGuid() { + let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", + index: 0 }, + gAccumulator.callback); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(function* fetch_byposition_nonexisting_index() { + let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 100 }, + gAccumulator.callback); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(function* fetch_byposition() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch({ parentGuid: bm1.parentGuid, + index: bm1.index }, + gAccumulator.callback); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/"); + Assert.equal(bm2.title, "a bookmark"); +}); + +add_task(function* fetch_byposition_default_index() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/last", + title: "last child" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.fetch({ parentGuid: bm1.parentGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX }, + gAccumulator.callback); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 1); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/last"); + Assert.equal(bm2.title, "last child"); + + yield PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(function* fetch_byurl_nonexisting() { + let bm = yield PlacesUtils.bookmarks.fetch({ url: "http://nonexisting.com/" }, + gAccumulator.callback); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(function* fetch_byurl() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://byurl.com/", + title: "a bookmark" }); + checkBookmarkObject(bm1); + + // Also ensure that fecth-by-url excludes the tags folder. + PlacesUtils.tagging.tagURI(uri(bm1.url.href), ["Test Tag"]); + + let bm2 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url }, + gAccumulator.callback); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://byurl.com/"); + Assert.equal(bm2.title, "a bookmark"); + + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://byurl.com/", + title: "a bookmark" }); + let bm4 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url }, + gAccumulator.callback); + checkBookmarkObject(bm4); + Assert.deepEqual(bm3, bm4); + Assert.equal(gAccumulator.results.length, 2); + gAccumulator.results.forEach(checkBookmarkObject); + Assert.deepEqual(gAccumulator.results[0], bm4); + + // After an update the returned bookmark should change. + yield PlacesUtils.bookmarks.update({ guid: bm1.guid, title: "new title" }); + let bm5 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url }, + gAccumulator.callback); + checkBookmarkObject(bm5); + // Cannot use deepEqual cause lastModified changed. + Assert.equal(bm1.guid, bm5.guid); + Assert.ok(bm5.lastModified > bm1.lastModified); + Assert.equal(gAccumulator.results.length, 2); + gAccumulator.results.forEach(checkBookmarkObject); + Assert.deepEqual(gAccumulator.results[0], bm5); + + // cleanup + PlacesUtils.tagging.untagURI(uri(bm1.url.href), ["Test Tag"]); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js new file mode 100644 index 000000000..35166bd95 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js @@ -0,0 +1,44 @@ +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.getRecent(), + /numberOfItems argument is required/); + Assert.throws(() => PlacesUtils.bookmarks.getRecent("abc"), + /numberOfItems argument must be an integer/); + Assert.throws(() => PlacesUtils.bookmarks.getRecent(1.2), + /numberOfItems argument must be an integer/); + Assert.throws(() => PlacesUtils.bookmarks.getRecent(0), + /numberOfItems argument must be greater than zero/); + Assert.throws(() => PlacesUtils.bookmarks.getRecent(-1), + /numberOfItems argument must be greater than zero/); +}); + +add_task(function* getRecent_returns_recent_bookmarks() { + yield PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "another bookmark" }); + let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/path", + title: "yet another bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + checkBookmarkObject(bm4); + + let results = yield PlacesUtils.bookmarks.getRecent(3); + Assert.equal(results.length, 3); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm4, results[0]); + checkBookmarkObject(results[1]); + Assert.deepEqual(bm3, results[1]); + checkBookmarkObject(results[2]); + Assert.deepEqual(bm2, results[2]); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js new file mode 100644 index 000000000..0f772a92f --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.insert(), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.insert(null), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.insert({}), + /The following properties were expected/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: "test" }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: null }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: 123 }), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: null }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ index: "1" }), + /Invalid value for property 'index'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ index: -10 }), + /Invalid value for property 'index'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: -10 }), + /Invalid value for property 'dateAdded'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: "today" }), + /Invalid value for property 'dateAdded'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: Date.now() }), + /Invalid value for property 'dateAdded'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: -10 }), + /Invalid value for property 'lastModified'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: "today" }), + /Invalid value for property 'lastModified'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: Date.now() }), + /Invalid value for property 'lastModified'/); + let time = new Date(); + let future = new Date(time + 86400000); + Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: future, + lastModified: time }), + /Invalid value for property 'dateAdded'/); + let past = new Date(time - 86400000); + Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: past }), + /Invalid value for property 'lastModified'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: -1 }), + /Invalid value for property 'type'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: 100 }), + /Invalid value for property 'type'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: "bookmark" }), + /Invalid value for property 'type'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: -1 }), + /Invalid value for property 'title'/); + + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: 10 }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://te st" }), + /Invalid value for property 'url'/); + let longurl = "http://www.example.com/"; + for (let i = 0; i < 65536; i++) { + longurl += "a"; + } + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: longurl }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: NetUtil.newURI(longurl) }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "te st" }), + /Invalid value for property 'url'/); +}); + +add_task(function* invalid_properties_for_bookmark_type() { + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + url: "http://www.moz.com/" }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + url: "http://www.moz.com/" }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "test" }), + /Invalid value for property 'title'/); +}); + +add_task(function* long_title_trim() { + let longtitle = "a"; + for (let i = 0; i < 4096; i++) { + longtitle += "a"; + } + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: longtitle }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm.title.length, 4096, "title should have been trimmed"); + Assert.ok(!("url" in bm), "url should not be set"); +}); + +add_task(function* create_separator() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: PlacesUtils.bookmarks.DEFAULT_INDEX }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 1); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("title" in bm), "title should not be set"); +}); + +add_task(function* create_separator_w_title_fail() { + try { + yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "a separator" }); + Assert.ok(false, "Trying to set title for a separator should reject"); + } catch (ex) {} +}); + +add_task(function* create_separator_invalid_parent_fail() { + try { + yield PlacesUtils.bookmarks.insert({ parentGuid: "123456789012", + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "a separator" }); + Assert.ok(false, "Trying to create an item in a non existing parent reject"); + } catch (ex) {} +}); + +add_task(function* create_separator_given_guid() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + guid: "123456789012" }); + checkBookmarkObject(bm); + Assert.equal(bm.guid, "123456789012"); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 2); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("title" in bm), "title should not be set"); +}); + +add_task(function* create_item_given_guid_no_type_fail() { + try { + yield PlacesUtils.bookmarks.insert({ parentGuid: "123456789012" }); + Assert.ok(false, "Trying to create an item with a given guid but no type should reject"); + } catch (ex) {} +}); + +add_task(function* create_separator_big_index() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: 9999 }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 3); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("title" in bm), "title should not be set"); +}); + +add_task(function* create_separator_given_dateAdded() { + let time = new Date(); + let past = new Date(time - 86400000); + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + dateAdded: past }); + checkBookmarkObject(bm); + Assert.equal(bm.dateAdded, past); + Assert.equal(bm.lastModified, past); +}); + +add_task(function* create_folder() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.ok(!("title" in bm), "title should not be set"); + + // And then create a nested folder. + let parentGuid = bm.guid; + bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.strictEqual(bm.title, "a folder"); +}); + +add_task(function* create_bookmark() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + let parentGuid = bm.guid; + + bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.equal(bm.title, "a bookmark"); + + // Check parent lastModified. + let parent = yield PlacesUtils.bookmarks.fetch({ guid: bm.parentGuid }); + Assert.deepEqual(parent.lastModified, bm.dateAdded); + + bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: new URL("http://example.com/") }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 1); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.ok(!("title" in bm), "title should not be set"); +}); + +add_task(function* create_bookmark_frecency() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm); + + yield PlacesTestUtils.promiseAsyncUpdates(); + Assert.ok(frecencyForUrl(bm.url) > 0, "Check frecency has been updated") +}); + +add_task(function* create_bookmark_without_type() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.equal(bm.title, "a bookmark"); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js new file mode 100644 index 000000000..02787425d --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js @@ -0,0 +1,527 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* insert_separator_notification() { + let observer = expectNotifications(); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid}); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ { name: "onItemAdded", + arguments: [ itemId, parentId, bm.index, bm.type, + null, null, bm.dateAdded, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* insert_folder_notification() { + let observer = expectNotifications(); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "a folder" }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ { name: "onItemAdded", + arguments: [ itemId, parentId, bm.index, bm.type, + null, bm.title, bm.dateAdded, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* insert_folder_notitle_notification() { + let observer = expectNotifications(); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ { name: "onItemAdded", + arguments: [ itemId, parentId, bm.index, bm.type, + null, null, bm.dateAdded, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* insert_bookmark_notification() { + let observer = expectNotifications(); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://example.com/"), + title: "a bookmark" }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ { name: "onItemAdded", + arguments: [ itemId, parentId, bm.index, bm.type, + bm.url, bm.title, bm.dateAdded, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* insert_bookmark_notitle_notification() { + let observer = expectNotifications(); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ { name: "onItemAdded", + arguments: [ itemId, parentId, bm.index, bm.type, + bm.url, null, bm.dateAdded, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* insert_bookmark_tag_notification() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://tag.example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag" }); + let observer = expectNotifications(); + let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL("http://tag.example.com/") }); + let tagId = yield PlacesUtils.promiseItemId(tag.guid); + let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid); + + observer.check([ { name: "onItemAdded", + arguments: [ tagId, tagParentId, tag.index, tag.type, + tag.url, null, tag.dateAdded, + tag.guid, tag.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ itemId, "tags", false, "", + bm.lastModified, bm.type, parentId, + bm.guid, bm.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* update_bookmark_lastModified() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://lastmod.example.com/") }); + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + lastModified: new Date() }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + observer.check([ { name: "onItemChanged", + arguments: [ itemId, "lastModified", false, + `${bm.lastModified * 1000}`, bm.lastModified, + bm.type, parentId, bm.guid, bm.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* update_bookmark_title() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://title.example.com/") }); + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + title: "new title" }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + observer.check([ { name: "onItemChanged", + arguments: [ itemId, "title", false, bm.title, + bm.lastModified, bm.type, parentId, bm.guid, + bm.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* update_bookmark_uri() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://url.example.com/") }); + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + url: "http://mozilla.org/" }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + observer.check([ { name: "onItemChanged", + arguments: [ itemId, "uri", false, bm.url.href, + bm.lastModified, bm.type, parentId, bm.guid, + bm.parentGuid, "http://url.example.com/", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* update_move_same_folder() { + // Ensure there are at least two items in place (others test do so for us, + // but we don't have to depend on that). + yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://move.example.com/") }); + let bmItemId = yield PlacesUtils.promiseItemId(bm.guid); + let bmParentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + let bmOldIndex = bm.index; + + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0 }); + Assert.equal(bm.index, 0); + observer.check([ { name: "onItemMoved", + arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index, + bm.type, bm.guid, bm.parentGuid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + + // Test that we get the right index for DEFAULT_INDEX input. + bmOldIndex = 0; + observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX }); + Assert.ok(bm.index > 0); + observer.check([ { name: "onItemMoved", + arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index, + bm.type, bm.guid, bm.parentGuid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* update_move_different_folder() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://move.example.com/") }); + let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let bmItemId = yield PlacesUtils.promiseItemId(bm.guid); + let bmOldParentId = PlacesUtils.unfiledBookmarksFolderId; + let bmOldIndex = bm.index; + + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: folder.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX }); + Assert.equal(bm.index, 0); + let bmNewParentId = yield PlacesUtils.promiseItemId(folder.guid); + observer.check([ { name: "onItemMoved", + arguments: [ bmItemId, bmOldParentId, bmOldIndex, bmNewParentId, + bm.index, bm.type, bm.guid, + PlacesUtils.bookmarks.unfiledGuid, + bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* remove_bookmark() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://remove.example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.remove(bm.guid); + // TODO (Bug 653910): onItemAnnotationRemoved notified even if there were no + // annotations. + observer.check([ { name: "onItemRemoved", + arguments: [ itemId, parentId, bm.index, bm.type, bm.url, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* remove_folder() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectNotifications(); + bm = yield PlacesUtils.bookmarks.remove(bm.guid); + observer.check([ { name: "onItemRemoved", + arguments: [ itemId, parentId, bm.index, bm.type, null, + bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* remove_bookmark_tag_notification() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://untag.example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag" }); + let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL("http://untag.example.com/") }); + let tagId = yield PlacesUtils.promiseItemId(tag.guid); + let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.remove(tag.guid); + + observer.check([ { name: "onItemRemoved", + arguments: [ tagId, tagParentId, tag.index, tag.type, + tag.url, tag.guid, tag.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ itemId, "tags", false, "", + bm.lastModified, bm.type, parentId, + bm.guid, bm.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* remove_folder_notification() { + let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/") }); + let bmItemId = yield PlacesUtils.promiseItemId(bm.guid); + + let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: folder1.guid }); + let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid); + + let bm2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder2.guid, + url: new URL("http://example.com/") }); + let bm2ItemId = yield PlacesUtils.promiseItemId(bm2.guid); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.remove(folder1.guid); + + observer.check([ { name: "onItemRemoved", + arguments: [ bm2ItemId, folder2Id, bm2.index, bm2.type, + bm2.url, bm2.guid, bm2.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder2Id, folder1Id, folder2.index, + folder2.type, null, folder2.guid, + folder2.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ bmItemId, folder1Id, bm.index, bm.type, + bm.url, bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder1Id, folder1ParentId, folder1.index, + folder1.type, null, folder1.guid, + folder1.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); +}); + +add_task(function* eraseEverything_notification() { + // Let's start from a clean situation. + yield PlacesUtils.bookmarks.eraseEverything(); + + let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid); + let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid); + + let toolbarBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: new URL("http://example.com/") }); + let toolbarBmId = yield PlacesUtils.promiseItemId(toolbarBm.guid); + let toolbarBmParentId = yield PlacesUtils.promiseItemId(toolbarBm.parentGuid); + + let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: new URL("http://example.com/") }); + let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid); + let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.eraseEverything(); + + // Bookmarks should always be notified before their parents. + observer.check([ { name: "onItemRemoved", + arguments: [ itemId, parentId, bm.index, bm.type, + bm.url, bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder2Id, folder2ParentId, folder2.index, + folder2.type, null, folder2.guid, + folder2.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder1Id, folder1ParentId, folder1.index, + folder1.type, null, folder1.guid, + folder1.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ menuBmId, menuBmParentId, + menuBm.index, menuBm.type, + menuBm.url, menuBm.guid, + menuBm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ toolbarBmId, toolbarBmParentId, + toolbarBm.index, toolbarBm.type, + toolbarBm.url, toolbarBm.guid, + toolbarBm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + ]); +}); + +add_task(function* eraseEverything_reparented_notification() { + // Let's start from a clean situation. + yield PlacesUtils.bookmarks.eraseEverything(); + + let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/") }); + let itemId = yield PlacesUtils.promiseItemId(bm.guid); + + let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid); + let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid); + + bm.parentGuid = folder2.guid; + bm = yield PlacesUtils.bookmarks.update(bm); + let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.eraseEverything(); + + // Bookmarks should always be notified before their parents. + observer.check([ { name: "onItemRemoved", + arguments: [ itemId, parentId, bm.index, bm.type, + bm.url, bm.guid, bm.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder2Id, folder2ParentId, folder2.index, + folder2.type, null, folder2.guid, + folder2.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemRemoved", + arguments: [ folder1Id, folder1ParentId, folder1.index, + folder1.type, null, folder1.guid, + folder1.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + ]); +}); + +add_task(function* reorder_notification() { + let bookmarks = [ + { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example3.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + ]; + let sorted = []; + for (let bm of bookmarks) { + sorted.push(yield PlacesUtils.bookmarks.insert(bm)); + } + + // Randomly reorder the array. + sorted.sort(() => 0.5 - Math.random()); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid, + sorted.map(bm => bm.guid)); + + let expectedNotifications = []; + for (let i = 0; i < sorted.length; ++i) { + let child = sorted[i]; + let childId = yield PlacesUtils.promiseItemId(child.guid); + expectedNotifications.push({ name: "onItemMoved", + arguments: [ childId, + PlacesUtils.unfiledBookmarksFolderId, + child.index, + PlacesUtils.unfiledBookmarksFolderId, + i, + child.type, + child.guid, + child.parentGuid, + child.parentGuid, + Ci.nsINavBookmarksService.SOURCE_DEFAULT + ] }); + } + observer.check(expectedNotifications); +}); + +function expectNotifications() { + let notifications = []; + let observer = new Proxy(NavBookmarkObserver, { + get(target, name) { + if (name == "check") { + PlacesUtils.bookmarks.removeObserver(observer); + return expectedNotifications => + Assert.deepEqual(notifications, expectedNotifications); + } + + if (name.startsWith("onItem")) { + return (...origArgs) => { + let args = Array.from(origArgs, arg => { + if (arg && arg instanceof Ci.nsIURI) + return new URL(arg.spec); + if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) + return new Date(parseInt(arg/1000)); + return arg; + }); + notifications.push({ name: name, arguments: args }); + } + } + + if (name in target) + return target[name]; + return undefined; + } + }); + PlacesUtils.bookmarks.addObserver(observer, false); + return observer; +} + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js new file mode 100644 index 000000000..19085a282 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.remove(), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.remove(null), + /Input should be a valid object/); + + Assert.throws(() => PlacesUtils.bookmarks.remove("test"), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.remove(123), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: "test" }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: null }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: 123 }), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: null }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/); + + Assert.throws(() => PlacesUtils.bookmarks.remove({ url: "http://te st/" }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ url: null }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.remove({ url: -10 }), + /Invalid value for property 'url'/); +}); + +add_task(function* remove_nonexistent_guid() { + try { + yield PlacesUtils.bookmarks.remove({ guid: "123456789012"}); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided GUID/.test(ex)); + } +}); + +add_task(function* remove_roots_fail() { + let guids = [PlacesUtils.bookmarks.rootGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.mobileGuid]; + for (let guid of guids) { + Assert.throws(() => PlacesUtils.bookmarks.remove(guid), + /It's not possible to remove Places root folders/); + } +}); + +add_task(function* remove_normal_folder_under_root_succeeds() { + let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.rootGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(folder); + let removed_folder = yield PlacesUtils.bookmarks.remove(folder); + Assert.deepEqual(folder, removed_folder); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder.guid)), null); +}); + +add_task(function* remove_bookmark() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/"); + Assert.equal(bm2.title, "a bookmark"); +}); + + +add_task(function* remove_bookmark_orphans() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(bm1); + PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(bm1.guid)), + "testanno", "testvalue", 0, 0); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + // Check there are no orphan annotations. + let conn = yield PlacesUtils.promiseDBConnection(); + let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`); + // Bug 1306445 will eventually remove the mobile root anno. + Assert.equal(annoAttrs.length, 1); + Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO); + let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`); + Assert.equal(annos.length, 1); + Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId); + Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id")); +}); + +add_task(function* remove_bookmark_empty_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.ok(!("title" in bm2)); +}); + +add_task(function* remove_folder() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm2.title, "a folder"); + Assert.ok(!("url" in bm2)); +}); + +add_task(function* test_nested_contents_removed() { + let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + let folder2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + let sep = yield PlacesUtils.bookmarks.insert({ parentGuid: folder2.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + yield PlacesUtils.bookmarks.remove(folder1); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder1.guid)), null); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder2.guid)), null); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(sep.guid)), null); +}); + +add_task(function* remove_folder_empty_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "" }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.ok(!("title" in bm2)); +}); + +add_task(function* remove_separator() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + checkBookmarkObject(bm1); + + let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("url" in bm2)); + Assert.ok(!("title" in bm2)); +}); + +add_task(function* test_nested_content_fails_when_not_allowed() { + let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + yield Assert.rejects(PlacesUtils.bookmarks.remove(folder1, {preventRemovalOfNonEmptyFolders: true}), + /Cannot remove a non-empty folder./); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js new file mode 100644 index 000000000..4f6617280 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.reorder(), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.reorder(null), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.reorder("test"), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.reorder(123), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.reorder({ guid: "test" }), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012"), + /Must provide a sorted array of children GUIDs./); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", {}), + /Must provide a sorted array of children GUIDs./); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", null), + /Must provide a sorted array of children GUIDs./); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", []), + /Must provide a sorted array of children GUIDs./); + + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ null ]), + /Invalid GUID found in the sorted children array/); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ "" ]), + /Invalid GUID found in the sorted children array/); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ {} ]), + /Invalid GUID found in the sorted children array/); + Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ "012345678901", null ]), + /Invalid GUID found in the sorted children array/); +}); + +add_task(function* reorder_nonexistent_guid() { + yield Assert.rejects(PlacesUtils.bookmarks.reorder("123456789012", [ "012345678901" ]), + /No folder found for the provided GUID/, + "Should throw for nonexisting guid"); +}); + +add_task(function* reorder() { + let bookmarks = [ + { url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { url: "http://example2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }, + { url: "http://example3.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + } + ]; + + let sorted = []; + for (let bm of bookmarks) { + sorted.push(yield PlacesUtils.bookmarks.insert(bm)); + } + + // Check the initial append sorting. + Assert.ok(sorted.every((bm, i) => bm.index == i), + "Initial bookmarks sorting is correct"); + + // Apply random sorting and run multiple tests. + for (let t = 0; t < 4; t++) { + sorted.sort(() => 0.5 - Math.random()); + let sortedGuids = sorted.map(child => child.guid); + dump("Expected order: " + sortedGuids.join() + "\n"); + // Add a nonexisting guid to the array, to ensure nothing will break. + sortedGuids.push("123456789012"); + yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid, + sortedGuids); + for (let i = 0; i < sorted.length; ++i) { + let item = yield PlacesUtils.bookmarks.fetch(sorted[i].guid); + Assert.equal(item.index, i); + } + } + + do_print("Test partial sorting"); + // Try a partial sorting by passing only 2 entries. + // The unspecified entries should retain the original order. + sorted = [ sorted[1], sorted[0] ].concat(sorted.slice(2)); + let sortedGuids = [ sorted[0].guid, sorted[1].guid ]; + dump("Expected order: " + sorted.map(b => b.guid).join() + "\n"); + yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid, + sortedGuids); + for (let i = 0; i < sorted.length; ++i) { + let item = yield PlacesUtils.bookmarks.fetch(sorted[i].guid); + Assert.equal(item.index, i); + } + + // Use triangular numbers to detect skipped position. + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute( + `SELECT parent + FROM moz_bookmarks + GROUP BY parent + HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0`); + Assert.equal(rows.length, 0, "All the bookmarks should have consistent positions"); +}); + +add_task(function* move_and_reorder() { + // Start clean. + yield PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = yield PlacesUtils.bookmarks.insert({ + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }); + let f1 = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }); + let bm2 = yield PlacesUtils.bookmarks.insert({ + url: "http://example2.com/", + parentGuid: f1.guid + }); + let f2 = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }); + let bm3 = yield PlacesUtils.bookmarks.insert({ + url: "http://example3.com/", + parentGuid: f2.guid + }); + let bm4 = yield PlacesUtils.bookmarks.insert({ + url: "http://example4.com/", + parentGuid: f2.guid + }); + let bm5 = yield PlacesUtils.bookmarks.insert({ + url: "http://example5.com/", + parentGuid: f2.guid + }); + + // Invert f2 children. + // This is critical to reproduce the bug, cause it inverts the position + // compared to the natural insertion order. + yield PlacesUtils.bookmarks.reorder(f2.guid, [bm5.guid, bm4.guid, bm3.guid]); + + bm1.parentGuid = f1.guid; + bm1.index = 0; + yield PlacesUtils.bookmarks.update(bm1); + + bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + Assert.equal(bm1.index, 0); + bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid); + Assert.equal(bm2.index, 1); + bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid); + Assert.equal(bm3.index, 2); + bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid); + Assert.equal(bm4.index, 1); + bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid); + Assert.equal(bm5.index, 0); + + // No-op reorder on f1 children. + // Nothing should change. Though, due to bug 1293365 this was causing children + // of other folders to get messed up. + yield PlacesUtils.bookmarks.reorder(f1.guid, [bm1.guid, bm2.guid]); + + bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid); + Assert.equal(bm1.index, 0); + bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid); + Assert.equal(bm2.index, 1); + bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid); + Assert.equal(bm3.index, 2); + bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid); + Assert.equal(bm4.index, 1); + bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid); + Assert.equal(bm5.index, 0); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js new file mode 100644 index 000000000..02f7c5460 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js @@ -0,0 +1,223 @@ +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.search(), + /Query object is required/); + Assert.throws(() => PlacesUtils.bookmarks.search(null), + /Query object is required/); + Assert.throws(() => PlacesUtils.bookmarks.search({title: 50}), + /Title option must be a string/); + Assert.throws(() => PlacesUtils.bookmarks.search({url: {url: "wombat"}}), + /Url option must be a string or a URL object/); + Assert.throws(() => PlacesUtils.bookmarks.search(50), + /Query must be an object or a string/); + Assert.throws(() => PlacesUtils.bookmarks.search(true), + /Query must be an object or a string/); +}); + +add_task(function* search_bookmark() { + yield PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://menu.org/", + title: "an on-menu bookmark" }); + let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://toolbar.org/", + title: "an on-toolbar bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + checkBookmarkObject(bm4); + + // finds a result by query + let results = yield PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // finds multiple results + results = yield PlacesUtils.bookmarks.search("example"); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + // finds menu bookmarks + results = yield PlacesUtils.bookmarks.search("an on-menu bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // finds toolbar bookmarks + results = yield PlacesUtils.bookmarks.search("an on-toolbar bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm4, results[0]); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_query_object() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + + let results = yield PlacesUtils.bookmarks.search({query: "example.com"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + + Assert.deepEqual(bm1, results[0]); + + results = yield PlacesUtils.bookmarks.search({query: "example"}); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_url() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "third bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by url + let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // normalizes the url + results = yield PlacesUtils.bookmarks.search({url: "http:/example.com"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = yield PlacesUtils.bookmarks.search({url: "http://example.org/path"}); + Assert.equal(results.length, 2); + + // requires exact match + results = yield PlacesUtils.bookmarks.search({url: "http://example.org/"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "another bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by title + let results = yield PlacesUtils.bookmarks.search({title: "a bookmark"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = yield PlacesUtils.bookmarks.search({title: "another bookmark"}); + Assert.equal(results.length, 2); + + // requires exact match + results = yield PlacesUtils.bookmarks.search({title: "bookmark"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_combinations() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "third bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result if title and url match + let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // does not match if query is not matching but url and title match + results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark", query: "nonexistent"}); + Assert.equal(results.length, 0); + + // does not match if one parameter is not matching + results = yield PlacesUtils.bookmarks.search({url: "http://what.ever", title: "a bookmark"}); + Assert.equal(results.length, 0); + + // query only matches if other fields match as well + results = yield PlacesUtils.bookmarks.search({query: "bookmark", url: "http://example.net/"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // non-matching query will also return no results + results = yield PlacesUtils.bookmarks.search({query: "nonexistent", url: "http://example.net/"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_folder() { + let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a test folder" }); + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: folder.guid, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(folder); + checkBookmarkObject(bm); + + // also finds folders + let results = yield PlacesUtils.bookmarks.search("a test folder"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.equal(folder.title, results[0].title); + Assert.equal(folder.type, results[0].type); + Assert.equal(folder.parentGuid, results[0].parentGuid); + + // finds elements in folders + results = yield PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm, results[0]); + Assert.equal(folder.guid, results[0].parentGuid); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js new file mode 100644 index 000000000..d077fd6f3 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js @@ -0,0 +1,414 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.update(), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.update(null), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.bookmarks.update({}), + /The following properties were expected/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "test" }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ guid: null }), + /Invalid value for property 'guid'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ guid: 123 }), + /Invalid value for property 'guid'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: null }), + /Invalid value for property 'parentGuid'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ index: "1" }), + /Invalid value for property 'index'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ index: -10 }), + /Invalid value for property 'index'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: -10 }), + /Invalid value for property 'dateAdded'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: "today" }), + /Invalid value for property 'dateAdded'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: Date.now() }), + /Invalid value for property 'dateAdded'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: -10 }), + /Invalid value for property 'lastModified'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: "today" }), + /Invalid value for property 'lastModified'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: Date.now() }), + /Invalid value for property 'lastModified'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ type: -1 }), + /Invalid value for property 'type'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ type: 100 }), + /Invalid value for property 'type'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ type: "bookmark" }), + /Invalid value for property 'type'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ url: 10 }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ url: "http://te st" }), + /Invalid value for property 'url'/); + let longurl = "http://www.example.com/"; + for (let i = 0; i < 65536; i++) { + longurl += "a"; + } + Assert.throws(() => PlacesUtils.bookmarks.update({ url: longurl }), + /Invalid value for property 'url'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ url: NetUtil.newURI(longurl) }), + /Invalid value for property 'url'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ url: "te st" }), + /Invalid value for property 'url'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ title: -1 }), + /Invalid value for property 'title'/); + Assert.throws(() => PlacesUtils.bookmarks.update({ title: {} }), + /Invalid value for property 'title'/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012" }), + /Not enough properties to update/); + + Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012", + parentGuid: "012345678901" }), + /The following properties were expected: index/); +}); + +add_task(function* nonexisting_bookmark_throws() { + try { + yield PlacesUtils.bookmarks.update({ guid: "123456789012", + title: "test" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided GUID/.test(ex)); + } +}); + +add_task(function* invalid_properties_for_existing_bookmark() { + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/" }); + + try { + yield PlacesUtils.bookmarks.update({ guid: bm.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/The bookmark type cannot be changed/.test(ex)); + } + + try { + yield PlacesUtils.bookmarks.update({ guid: bm.guid, + dateAdded: new Date() }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/The bookmark dateAdded cannot be changed/.test(ex)); + } + + try { + yield PlacesUtils.bookmarks.update({ guid: bm.guid, + dateAdded: new Date() }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/The bookmark dateAdded cannot be changed/.test(ex)); + } + + try { + yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: "123456789012", + index: 1 }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided parentGuid/.test(ex)); + } + + let past = new Date(Date.now() - 86400000); + try { + yield PlacesUtils.bookmarks.update({ guid: bm.guid, + lastModified: past }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'lastModified'/.test(ex)); + } + + let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + try { + yield PlacesUtils.bookmarks.update({ guid: folder.guid, + url: "http://example.com/" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'url'/.test(ex)); + } + + let separator = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + try { + yield PlacesUtils.bookmarks.update({ guid: separator.guid, + url: "http://example.com/" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'url'/.test(ex)); + } + try { + yield PlacesUtils.bookmarks.update({ guid: separator.guid, + title: "test" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'title'/.test(ex)); + } +}); + +add_task(function* long_title_trim() { + let longtitle = "a"; + for (let i = 0; i < 4096; i++) { + longtitle += "a"; + } + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "title" }); + checkBookmarkObject(bm); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + title: longtitle }); + let newTitle = bm.title; + Assert.equal(newTitle.length, 4096, "title should have been trimmed"); + + bm = yield PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.title, newTitle); +}); + +add_task(function* update_lastModified() { + let yesterday = new Date(Date.now() - 86400000); + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "title", + dateAdded: yesterday }); + checkBookmarkObject(bm); + Assert.deepEqual(bm.lastModified, yesterday); + + let time = new Date(); + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + lastModified: time }); + checkBookmarkObject(bm); + Assert.deepEqual(bm.lastModified, time); + + bm = yield PlacesUtils.bookmarks.fetch(bm.guid); + Assert.deepEqual(bm.lastModified, time); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + lastModified: yesterday }); + Assert.deepEqual(bm.lastModified, yesterday); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + title: "title2" }); + Assert.ok(bm.lastModified >= time); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + title: "" }); + Assert.ok(!("title" in bm)); + + bm = yield PlacesUtils.bookmarks.fetch(bm.guid); + Assert.ok(!("title" in bm)); +}); + +add_task(function* update_url() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "title" }); + checkBookmarkObject(bm); + let lastModified = bm.lastModified; + let frecency = frecencyForUrl(bm.url); + Assert.ok(frecency > 0, "Check frecency has been updated"); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + url: "http://mozilla.org/" }); + checkBookmarkObject(bm); + Assert.ok(bm.lastModified >= lastModified); + Assert.equal(bm.url.href, "http://mozilla.org/"); + + bm = yield PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.url.href, "http://mozilla.org/"); + Assert.ok(bm.lastModified >= lastModified); + + Assert.equal(frecencyForUrl("http://example.com/"), frecency, "Check frecency for example.com"); + Assert.equal(frecencyForUrl("http://mozilla.org/"), frecency, "Check frecency for mozilla.org"); +}); + +add_task(function* update_index() { + let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }) ; + let f1 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + Assert.equal(f1.index, 0); + let f2 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + Assert.equal(f2.index, 1); + let f3 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + Assert.equal(f3.index, 2); + let lastModified = f1.lastModified; + + f1 = yield PlacesUtils.bookmarks.update({ guid: f1.guid, + parentGuid: f1.parentGuid, + index: 1}); + checkBookmarkObject(f1); + Assert.equal(f1.index, 1); + Assert.ok(f1.lastModified >= lastModified); + + parent = yield PlacesUtils.bookmarks.fetch(f1.parentGuid); + Assert.deepEqual(parent.lastModified, f1.lastModified); + + f2 = yield PlacesUtils.bookmarks.fetch(f2.guid); + Assert.equal(f2.index, 0); + + f3 = yield PlacesUtils.bookmarks.fetch(f3.guid); + Assert.equal(f3.index, 2); + + f3 = yield PlacesUtils.bookmarks.update({ guid: f3.guid, + index: 0 }); + f1 = yield PlacesUtils.bookmarks.fetch(f1.guid); + Assert.equal(f1.index, 2); + + f2 = yield PlacesUtils.bookmarks.fetch(f2.guid); + Assert.equal(f2.index, 1); +}); + +add_task(function* update_move_folder_into_descendant_throws() { + let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }) ; + let descendant = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + + try { + yield PlacesUtils.bookmarks.update({ guid: parent.guid, + parentGuid: parent.guid, + index: 0 }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Cannot insert a folder into itself or one of its descendants/.test(ex)); + } + + try { + yield PlacesUtils.bookmarks.update({ guid: parent.guid, + parentGuid: descendant.guid, + index: 0 }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Cannot insert a folder into itself or one of its descendants/.test(ex)); + } +}); + +add_task(function* update_move() { + let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }) ; + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK }) ; + let descendant = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + Assert.equal(descendant.index, 1); + let lastModified = bm.lastModified; + + // This is moving to a nonexisting index by purpose, it will be appended. + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: descendant.guid, + index: 1 }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + Assert.ok(bm.lastModified >= lastModified); + + parent = yield PlacesUtils.bookmarks.fetch(parent.guid); + descendant = yield PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.deepEqual(parent.lastModified, bm.lastModified); + Assert.deepEqual(descendant.lastModified, bm.lastModified); + Assert.equal(descendant.index, 0); + + bm = yield PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + + bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, + parentGuid: parent.guid, + index: 0 }); + Assert.equal(bm.parentGuid, parent.guid); + Assert.equal(bm.index, 0); + + descendant = yield PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.equal(descendant.index, 1); +}); + +add_task(function* update_move_append() { + let folder_a = + yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(folder_a); + let folder_b = + yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + checkBookmarkObject(folder_b); + + /* folder_a: [sep_1, sep_2, sep_3], folder_b: [] */ + let sep_1 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + checkBookmarkObject(sep_1); + let sep_2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + checkBookmarkObject(sep_2); + let sep_3 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + checkBookmarkObject(sep_3); + + function ensurePosition(info, parentGuid, index) { + checkBookmarkObject(info); + Assert.equal(info.parentGuid, parentGuid); + Assert.equal(info.index, index); + } + + // folder_a: [sep_2, sep_3, sep_1], folder_b: [] + sep_1.index = PlacesUtils.bookmarks.DEFAULT_INDEX; + // Note sep_1 includes parentGuid even though we're not moving the item to + // another folder + sep_1 = yield PlacesUtils.bookmarks.update(sep_1); + ensurePosition(sep_1, folder_a.guid, 2); + sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_a.guid, 0); + sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_a.guid, 1); + sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 2); + + // folder_a: [sep_2, sep_1], folder_b: [sep_3] + sep_3.index = PlacesUtils.bookmarks.DEFAULT_INDEX; + sep_3.parentGuid = folder_b.guid; + sep_3 = yield PlacesUtils.bookmarks.update(sep_3); + ensurePosition(sep_3, folder_b.guid, 0); + sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_a.guid, 0); + sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 1); + sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_b.guid, 0); + + // folder_a: [sep_1], folder_b: [sep_3, sep_2] + sep_2.index = Number.MAX_SAFE_INTEGER; + sep_2.parentGuid = folder_b.guid; + sep_2 = yield PlacesUtils.bookmarks.update(sep_2); + ensurePosition(sep_2, folder_b.guid, 1); + sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 0); + sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_b.guid, 0); + sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_b.guid, 1); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js new file mode 100644 index 000000000..f5cf34641 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js @@ -0,0 +1,18 @@ + +// Bug 1192692 - promiseBookmarksTree caches items without adding observers to +// invalidate the cache. +add_task(function* boookmarks_tree_cache() { + // Note that for this test to be effective, it needs to use the "old" sync + // bookmarks methods - using, eg, PlacesUtils.bookmarks.insert() doesn't + // demonstrate the problem as it indirectly arranges for the observers to + // be added. + let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("http://example.com"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A title"); + yield PlacesUtils.promiseBookmarksTree(); + + PlacesUtils.bookmarks.removeItem(id); + + yield Assert.rejects(PlacesUtils.promiseItemGuid(id)); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js b/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js new file mode 100644 index 000000000..55ffecf2f --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js @@ -0,0 +1,68 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get bookmark service +var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +/** + * Ensures that the Places APIs recognize that aBookmarkedUri is bookmarked + * via aBookmarkId and that aUnbookmarkedUri is not bookmarked at all. + * + * @param aBookmarkId + * an item ID whose corresponding URI is aBookmarkedUri + * @param aBookmarkedUri + * a bookmarked URI that has a corresponding item ID aBookmarkId + * @param aUnbookmarkedUri + * a URI that is not currently bookmarked at all + */ +function checkUris(aBookmarkId, aBookmarkedUri, aUnbookmarkedUri) +{ + // Ensure that aBookmarkedUri equals some URI that is bookmarked + var uri = bmsvc.getBookmarkedURIFor(aBookmarkedUri); + do_check_neq(uri, null); + do_check_true(uri.equals(aBookmarkedUri)); + + // Ensure that aBookmarkedUri is considered bookmarked + do_check_true(bmsvc.isBookmarked(aBookmarkedUri)); + + // Ensure that the URI corresponding to aBookmarkId equals aBookmarkedUri + do_check_true(bmsvc.getBookmarkURI(aBookmarkId).equals(aBookmarkedUri)); + + // Ensure that aUnbookmarkedUri does not equal any URI that is bookmarked + uri = bmsvc.getBookmarkedURIFor(aUnbookmarkedUri); + do_check_eq(uri, null); + + // Ensure that aUnbookmarkedUri is not considered bookmarked + do_check_false(bmsvc.isBookmarked(aUnbookmarkedUri)); +} + +// main +function run_test() { + // Create a folder + var folderId = bmsvc.createFolder(bmsvc.toolbarFolder, + "test", + bmsvc.DEFAULT_INDEX); + + // Create 2 URIs + var uri1 = uri("http://www.dogs.com"); + var uri2 = uri("http://www.cats.com"); + + // Bookmark the first one + var bookmarkId = bmsvc.insertBookmark(folderId, + uri1, + bmsvc.DEFAULT_INDEX, + "Dogs"); + + // uri1 is bookmarked via bookmarkId, uri2 is not + checkUris(bookmarkId, uri1, uri2); + + // Change the URI of the bookmark to uri2 + bmsvc.changeBookmarkURI(bookmarkId, uri2); + + // uri2 is now bookmarked via bookmarkId, uri1 is not + checkUris(bookmarkId, uri2, uri1); +} diff --git a/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js b/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js new file mode 100644 index 000000000..c43e8e283 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Test bookmarksService.getBookmarkedURIFor(aURI); + */ + +var hs = PlacesUtils.history; +var bs = PlacesUtils.bookmarks; + +function run_test() { + run_next_test(); +} + +add_task(function* test_getBookmarkedURIFor() { + let now = Date.now() * 1000; + const sourceURI = uri("http://test.mozilla.org/"); + // Add a visit and a bookmark. + yield PlacesTestUtils.addVisits({ uri: sourceURI, visitDate: now }); + do_check_eq(bs.getBookmarkedURIFor(sourceURI), null); + + let sourceItemId = bs.insertBookmark(bs.unfiledBookmarksFolder, + sourceURI, + bs.DEFAULT_INDEX, + "bookmark"); + do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI)); + + // Add a redirected visit. + const permaURI = uri("http://perma.mozilla.org/"); + yield PlacesTestUtils.addVisits({ + uri: permaURI, + transition: TRANSITION_REDIRECT_PERMANENT, + visitDate: now++, + referrer: sourceURI + }); + do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI)); + do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI)); + // Add a bookmark to the destination. + let permaItemId = bs.insertBookmark(bs.unfiledBookmarksFolder, + permaURI, + bs.DEFAULT_INDEX, + "bookmark"); + do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI)); + do_check_true(bs.getBookmarkedURIFor(permaURI).equals(permaURI)); + // Now remove the bookmark on the destination. + bs.removeItem(permaItemId); + // We should see the source as bookmark. + do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI)); + + // Add another redirected visit. + const tempURI = uri("http://perma.mozilla.org/"); + yield PlacesTestUtils.addVisits({ + uri: tempURI, + transition: TRANSITION_REDIRECT_TEMPORARY, + visitDate: now++, + referrer: permaURI + }); + + do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI)); + do_check_true(bs.getBookmarkedURIFor(tempURI).equals(sourceURI)); + // Add a bookmark to the destination. + let tempItemId = bs.insertBookmark(bs.unfiledBookmarksFolder, + tempURI, + bs.DEFAULT_INDEX, + "bookmark"); + do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI)); + do_check_true(bs.getBookmarkedURIFor(tempURI).equals(tempURI)); + + // Now remove the bookmark on the destination. + bs.removeItem(tempItemId); + // We should see the source as bookmark. + do_check_true(bs.getBookmarkedURIFor(tempURI).equals(sourceURI)); + // Remove the source bookmark as well. + bs.removeItem(sourceItemId); + do_check_eq(bs.getBookmarkedURIFor(tempURI), null); + + // Try to pass in a never seen URI, should return null and a new entry should + // not be added to the database. + do_check_eq(bs.getBookmarkedURIFor(uri("http://does.not.exist/")), null); + do_check_false(page_in_database("http://does.not.exist/")); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_keywords.js b/toolkit/components/places/tests/bookmarks/test_keywords.js new file mode 100644 index 000000000..149d6d0b0 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_keywords.js @@ -0,0 +1,310 @@ +const URI1 = NetUtil.newURI("http://test1.mozilla.org/"); +const URI2 = NetUtil.newURI("http://test2.mozilla.org/"); +const URI3 = NetUtil.newURI("http://test3.mozilla.org/"); + +function check_keyword(aURI, aKeyword) { + if (aKeyword) + aKeyword = aKeyword.toLowerCase(); + + for (let bm of PlacesUtils.getBookmarksForURI(aURI)) { + let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bm); + if (keyword && !aKeyword) { + throw (`${aURI.spec} should not have a keyword`); + } else if (aKeyword && keyword == aKeyword) { + Assert.equal(keyword, aKeyword); + } + } + + if (aKeyword) { + let uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword); + Assert.equal(uri.spec, aURI.spec); + // Check case insensitivity. + uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase()); + Assert.equal(uri.spec, aURI.spec); + } +} + +function* check_orphans() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT id FROM moz_keywords k + WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) + `); + Assert.equal(rows.length, 0); +} + +function expectNotifications() { + let notifications = []; + let observer = new Proxy(NavBookmarkObserver, { + get(target, name) { + if (name == "check") { + PlacesUtils.bookmarks.removeObserver(observer); + return expectedNotifications => + Assert.deepEqual(notifications, expectedNotifications); + } + + if (name.startsWith("onItemChanged")) { + return function(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal) { + if (prop != "keyword") + return; + let args = Array.from(arguments, arg => { + if (arg && arg instanceof Ci.nsIURI) + return new URL(arg.spec); + if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) + return new Date(parseInt(arg/1000)); + return arg; + }); + notifications.push({ name: name, arguments: args }); + } + } + + return target[name]; + } + }); + PlacesUtils.bookmarks.addObserver(observer, false); + return observer; +} + +add_task(function test_invalid_input() { + Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(null), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(""), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(null), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(0), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(null, "k"), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(0, "k"), + /NS_ERROR_ILLEGAL_VALUE/); +}); + +add_task(function* test_addBookmarkAndKeyword() { + check_keyword(URI1, null); + let fc = yield foreign_count(URI1); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); + let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI1 }); + observer.check([ { name: "onItemChanged", + arguments: [ itemId, "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); + + check_keyword(URI1, "keyword"); + Assert.equal((yield foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + yield check_orphans(); +}); + +add_task(function* test_addBookmarkToURIHavingKeyword() { + // The uri has already a keyword. + check_keyword(URI1, "keyword"); + let fc = yield foreign_count(URI1); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + check_keyword(URI1, "keyword"); + Assert.equal((yield foreign_count(URI1)), fc + 1); // + 1 bookmark + + PlacesUtils.bookmarks.removeItem(itemId); + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function* test_sameKeywordDifferentURI() { + let fc1 = yield foreign_count(URI1); + let fc2 = yield foreign_count(URI2); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test2"); + check_keyword(URI1, "keyword"); + check_keyword(URI2, null); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "kEyWoRd"); + + let bookmark1 = yield PlacesUtils.bookmarks.fetch({ url: URI1 }); + let bookmark2 = yield PlacesUtils.bookmarks.fetch({ url: URI2 }); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ itemId, "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); + + // The keyword should have been "moved" to the new URI. + check_keyword(URI1, null); + Assert.equal((yield foreign_count(URI1)), fc1 - 1); // - 1 keyword + check_keyword(URI2, "keyword"); + Assert.equal((yield foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function* test_sameURIDifferentKeyword() { + let fc = yield foreign_count(URI2); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test2"); + check_keyword(URI2, "keyword"); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword2"); + + let bookmarks = []; + yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark)); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)), + "keyword", false, "keyword2", + bookmarks[0].lastModified, bookmarks[0].type, + (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)), + bookmarks[0].guid, bookmarks[0].parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)), + "keyword", false, "keyword2", + bookmarks[1].lastModified, bookmarks[1].type, + (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)), + bookmarks[1].guid, bookmarks[1].parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); + + check_keyword(URI2, "keyword2"); + Assert.equal((yield foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function* test_removeBookmarkWithKeyword() { + let fc = yield foreign_count(URI2); + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + + // The keyword should not be removed, since there are other bookmarks yet. + PlacesUtils.bookmarks.removeItem(itemId); + + check_keyword(URI2, "keyword2"); + Assert.equal((yield foreign_count(URI2)), fc); // + 1 bookmark - 1 bookmark + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function* test_unsetKeyword() { + let fc = yield foreign_count(URI2); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + + // The keyword should be removed from any bookmark. + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, null); + + let bookmarks = []; + yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark)); + do_print(bookmarks.length); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)), + "keyword", false, "", + bookmarks[0].lastModified, bookmarks[0].type, + (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)), + bookmarks[0].guid, bookmarks[0].parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)), + "keyword", false, "", + bookmarks[1].lastModified, bookmarks[1].type, + (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)), + bookmarks[1].guid, bookmarks[1].parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[2].guid)), + "keyword", false, "", + bookmarks[2].lastModified, bookmarks[2].type, + (yield PlacesUtils.promiseItemId(bookmarks[2].parentGuid)), + bookmarks[2].guid, bookmarks[2].parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + + check_keyword(URI1, null); + check_keyword(URI2, null); + Assert.equal((yield foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function* test_addRemoveBookmark() { + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI3, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test3"); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); + let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI3 }); + let parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid); + PlacesUtils.bookmarks.removeItem(itemId); + + observer.check([ { name: "onItemChanged", + arguments: [ itemId, + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + parentId, + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } + ]); + + check_keyword(URI3, null); + // Don't check the foreign count since the process is async. + // The new test_keywords.js in unit is checking this though. + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js new file mode 100644 index 000000000..06f45b18e --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js @@ -0,0 +1,640 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that each nsINavBookmarksObserver method gets the correct input. +Cu.import("resource://gre/modules/PromiseUtils.jsm"); + +const GUID_RE = /^[a-zA-Z0-9\-_]{12}$/; + +var gBookmarksObserver = { + expected: [], + setup(expected) { + this.expected = expected; + this.deferred = PromiseUtils.defer(); + return this.deferred.promise; + }, + validate: function (aMethodName, aArguments) { + do_check_eq(this.expected[0].name, aMethodName); + + let args = this.expected.shift().args; + do_check_eq(aArguments.length, args.length); + for (let i = 0; i < aArguments.length; i++) { + do_check_true(args[i].check(aArguments[i]), aMethodName + "(args[" + i + "]: " + args[i].name + ")"); + } + + if (this.expected.length === 0) { + this.deferred.resolve(); + } + }, + + // nsINavBookmarkObserver + onBeginUpdateBatch() { + return this.validate("onBeginUpdateBatch", arguments); + }, + onEndUpdateBatch() { + return this.validate("onEndUpdateBatch", arguments); + }, + onItemAdded() { + return this.validate("onItemAdded", arguments); + }, + onItemRemoved() { + return this.validate("onItemRemoved", arguments); + }, + onItemChanged() { + return this.validate("onItemChanged", arguments); + }, + onItemVisited() { + return this.validate("onItemVisited", arguments); + }, + onItemMoved() { + return this.validate("onItemMoved", arguments); + }, + + // nsISupports + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), +}; + +var gBookmarkSkipObserver = { + skipTags: true, + skipDescendantsOnItemRemoval: true, + + expected: null, + setup(expected) { + this.expected = expected; + this.deferred = PromiseUtils.defer(); + return this.deferred.promise; + }, + validate: function (aMethodName) { + do_check_eq(this.expected.shift(), aMethodName); + if (this.expected.length === 0) { + this.deferred.resolve(); + } + }, + + // nsINavBookmarkObserver + onBeginUpdateBatch() { + return this.validate("onBeginUpdateBatch", arguments); + }, + onEndUpdateBatch() { + return this.validate("onEndUpdateBatch", arguments); + }, + onItemAdded() { + return this.validate("onItemAdded", arguments); + }, + onItemRemoved() { + return this.validate("onItemRemoved", arguments); + }, + onItemChanged() { + return this.validate("onItemChanged", arguments); + }, + onItemVisited() { + return this.validate("onItemVisited", arguments); + }, + onItemMoved() { + return this.validate("onItemMoved", arguments); + }, + + // nsISupports + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), +}; + + +add_task(function setup() { + PlacesUtils.bookmarks.addObserver(gBookmarksObserver, false); + PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver, false); +}); + +add_task(function* batch() { + let promise = Promise.all([ + gBookmarksObserver.setup([ + { name: "onBeginUpdateBatch", + args: [] }, + { name: "onEndUpdateBatch", + args: [] }, + ]), + gBookmarkSkipObserver.setup([ + "onBeginUpdateBatch", "onEndUpdateBatch" + ])]); + PlacesUtils.bookmarks.runInBatchMode({ + runBatched: function () { + // Nothing. + } + }, null); + yield promise; +}); + +add_task(function* onItemAdded_bookmark() { + const TITLE = "Bookmark 1"; + let uri = NetUtil.newURI("http://1.mozilla.org/"); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemAdded" + ]), + gBookmarksObserver.setup([ + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "title", check: v => v === TITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri, PlacesUtils.bookmarks.DEFAULT_INDEX, + TITLE); + yield promise; +}); + +add_task(function* onItemAdded_separator() { + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemAdded" + ]), + gBookmarksObserver.setup([ + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "index", check: v => v === 1 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR }, + { name: "uri", check: v => v === null }, + { name: "title", check: v => v === null }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX); + yield promise; +}); + +add_task(function* onItemAdded_folder() { + const TITLE = "Folder 1"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemAdded" + ]), + gBookmarksObserver.setup([ + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "index", check: v => v === 2 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "title", check: v => v === TITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, + TITLE, + PlacesUtils.bookmarks.DEFAULT_INDEX); + yield promise; +}); + +add_task(function* onItemChanged_title_bookmark() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + const TITLE = "New title"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemChanged" + ]), + gBookmarksObserver.setup([ + { name: "onItemChanged", // This is an unfortunate effect of bug 653910. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "title" }, + { name: "isAnno", check: v => v === false }, + { name: "newValue", check: v => v === TITLE }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.setItemTitle(id, TITLE); + yield promise; +}); + +add_task(function* onItemChanged_tags_bookmark() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let uri = PlacesUtils.bookmarks.getBookmarkURI(id); + const TAG = "tag"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemChanged", "onItemChanged" + ]), + gBookmarksObserver.setup([ + { name: "onItemAdded", // This is the tag folder. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.tagsFolderId }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "title", check: v => v === TAG }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemAdded", // This is the tag. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "title", check: v => v === null }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemChanged", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "tags" }, + { name: "isAnno", check: v => v === false }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", // This is the tag. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", // This is the tag folder. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.tagsFolderId }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemChanged", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "tags" }, + { name: "isAnno", check: v => v === false }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.tagging.tagURI(uri, [TAG]); + PlacesUtils.tagging.untagURI(uri, [TAG]); + yield promise; +}); + +add_task(function* onItemMoved_bookmark() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemMoved", "onItemMoved" + ]), + gBookmarksObserver.setup([ + { name: "onItemMoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "oldParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "oldIndex", check: v => v === 0 }, + { name: "newParentId", check: v => v === PlacesUtils.toolbarFolderId }, + { name: "newIndex", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "newParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemMoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "oldParentId", check: v => v === PlacesUtils.toolbarFolderId }, + { name: "oldIndex", check: v => v === 0 }, + { name: "newParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "newIndex", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "newParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.moveItem(id, PlacesUtils.toolbarFolderId, 0); + PlacesUtils.bookmarks.moveItem(id, PlacesUtils.unfiledBookmarksFolderId, 0); + yield promise; +}); + +add_task(function* onItemMoved_bookmark() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let uri = PlacesUtils.bookmarks.getBookmarkURI(id); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemVisited" + ]), + gBookmarksObserver.setup([ + { name: "onItemVisited", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "visitId", check: v => typeof(v) == "number" && v > 0 }, + { name: "time", check: v => typeof(v) == "number" && v > 0 }, + { name: "transitionType", check: v => v === PlacesUtils.history.TRANSITION_TYPED }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + ] }, + ])]); + PlacesTestUtils.addVisits({ uri: uri, transition: TRANSITION_TYPED }); + yield promise; +}); + +add_task(function* onItemRemoved_bookmark() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let uri = PlacesUtils.bookmarks.getBookmarkURI(id); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemChanged", "onItemRemoved" + ]), + gBookmarksObserver.setup([ + { name: "onItemChanged", // This is an unfortunate effect of bug 653910. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "" }, + { name: "isAnno", check: v => v === true }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.removeItem(id); + yield promise; +}); + +add_task(function* onItemRemoved_separator() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemChanged", "onItemRemoved" + ]), + gBookmarksObserver.setup([ + { name: "onItemChanged", // This is an unfortunate effect of bug 653910. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "" }, + { name: "isAnno", check: v => v === true }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR }, + { name: "uri", check: v => v === null }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.removeItem(id); + yield promise; +}); + +add_task(function* onItemRemoved_folder() { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemChanged", "onItemRemoved" + ]), + gBookmarksObserver.setup([ + { name: "onItemChanged", // This is an unfortunate effect of bug 653910. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "" }, + { name: "isAnno", check: v => v === true }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + PlacesUtils.bookmarks.removeItem(id); + yield promise; +}); + +add_task(function* onItemRemoved_folder_recursive() { + const TITLE = "Folder 3"; + const BMTITLE = "Bookmark 1"; + let uri = NetUtil.newURI("http://1.mozilla.org/"); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "onItemAdded", "onItemAdded", "onItemAdded", "onItemAdded", + "onItemChanged", "onItemRemoved" + ]), + gBookmarksObserver.setup([ + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "title", check: v => v === TITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "title", check: v => v === BMTITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) }, + { name: "index", check: v => v === 1 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "title", check: v => v === TITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemAdded", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0), 1) }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "title", check: v => v === BMTITLE }, + { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemChanged", // This is an unfortunate effect of bug 653910. + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "property", check: v => v === "" }, + { name: "isAnno", check: v => v === true }, + { name: "newValue", check: v => v === "" }, + { name: "lastModified", check: v => typeof(v) == "number" && v > 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "oldValue", check: v => typeof(v) == "string" }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 1 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK }, + { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + { name: "onItemRemoved", + args: [ + { name: "itemId", check: v => typeof(v) == "number" && v > 0 }, + { name: "parentId", check: v => typeof(v) == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER }, + { name: "uri", check: v => v === null }, + { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) }, + { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) }, + ] }, + ])]); + let folder = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, + TITLE, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.insertBookmark(folder, + uri, PlacesUtils.bookmarks.DEFAULT_INDEX, + BMTITLE); + let folder2 = PlacesUtils.bookmarks.createFolder(folder, TITLE, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.insertBookmark(folder2, + uri, PlacesUtils.bookmarks.DEFAULT_INDEX, + BMTITLE); + + PlacesUtils.bookmarks.removeItem(folder); + yield promise; +}); + +add_task(function cleanup() +{ + PlacesUtils.bookmarks.removeObserver(gBookmarksObserver); + PlacesUtils.bookmarks.removeObserver(gBookmarkSkipObserver); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_protectRoots.js b/toolkit/components/places/tests/bookmarks/test_protectRoots.js new file mode 100644 index 000000000..0a59f1653 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_protectRoots.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + const ROOTS = [ + PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.toolbarFolderId, + PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.tagsFolderId, + PlacesUtils.placesRootId, + PlacesUtils.mobileFolderId, + ]; + + for (let root of ROOTS) { + do_check_true(PlacesUtils.isRootItem(root)); + + try { + PlacesUtils.bookmarks.removeItem(root); + do_throw("Trying to remove a root should throw"); + } catch (ex) {} + + try { + PlacesUtils.bookmarks.moveItem(root, PlacesUtils.placesRootId, 0); + do_throw("Trying to move a root should throw"); + } catch (ex) {} + + try { + PlacesUtils.bookmarks.removeFolderChildren(root); + if (root == PlacesUtils.placesRootId) + do_throw("Trying to remove children of the main root should throw"); + } catch (ex) { + if (root != PlacesUtils.placesRootId) + do_throw("Trying to remove children of other roots should not throw"); + } + } +} diff --git a/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js new file mode 100644 index 000000000..537974b38 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js @@ -0,0 +1,70 @@ +/** + * This test ensures that reinserting a folder within a transaction gives it + * a different GUID, and passes the GUID to the observers. + */ + +add_task(function* test_removeFolderTransaction_reinsert() { + let folder = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Test folder", + }); + let folderId = yield PlacesUtils.promiseItemId(folder.guid); + let fx = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + title: "Get Firefox!", + url: "http://getfirefox.com", + }); + let fxId = yield PlacesUtils.promiseItemId(fx.guid); + let tb = yield PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + title: "Get Thunderbird!", + url: "http://getthunderbird.com", + }); + let tbId = yield PlacesUtils.promiseItemId(tb.guid); + + let notifications = []; + function checkNotifications(expected, message) { + deepEqual(notifications, expected, message); + notifications.length = 0; + } + + let observer = { + onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid, + parentGuid) { + notifications.push(["onItemAdded", itemId, parentId, guid, parentGuid]); + }, + onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid) { + notifications.push(["onItemRemoved", itemId, parentId, guid, parentGuid]); + }, + }; + PlacesUtils.bookmarks.addObserver(observer, false); + PlacesUtils.registerShutdownFunction(function() { + PlacesUtils.bookmarks.removeObserver(observer); + }); + + let transaction = PlacesUtils.bookmarks.getRemoveFolderTransaction(folderId); + deepEqual(notifications, [], "We haven't executed the transaction yet"); + + transaction.doTransaction(); + checkNotifications([ + ["onItemRemoved", tbId, folderId, tb.guid, folder.guid], + ["onItemRemoved", fxId, folderId, fx.guid, folder.guid], + ["onItemRemoved", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid, + PlacesUtils.bookmarks.menuGuid], + ], "Executing transaction should remove folder and its descendants"); + + transaction.undoTransaction(); + // At this point, the restored folder has the same ID, but a different GUID. + let newFolderGuid = yield PlacesUtils.promiseItemGuid(folderId); + checkNotifications([ + ["onItemAdded", folderId, PlacesUtils.bookmarksMenuFolderId, newFolderGuid, + PlacesUtils.bookmarks.menuGuid], + ], "Undo should reinsert folder with same ID and different GUID"); + + transaction.redoTransaction(); + checkNotifications([ + ["onItemRemoved", folderId, PlacesUtils.bookmarksMenuFolderId, + newFolderGuid, PlacesUtils.bookmarks.menuGuid], + ], "Redo should forward new GUID to observer"); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_removeItem.js b/toolkit/components/places/tests/bookmarks/test_removeItem.js new file mode 100644 index 000000000..ec846b28e --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_removeItem.js @@ -0,0 +1,30 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + + +const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX; + +function run_test() { + // folder to hold this test + var folderId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, + "", DEFAULT_INDEX); + + // add a bookmark to the new folder + var bookmarkURI = uri("http://iasdjkf"); + do_check_false(PlacesUtils.bookmarks.isBookmarked(bookmarkURI)); + var bookmarkId = PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI, + DEFAULT_INDEX, ""); + do_check_eq(PlacesUtils.bookmarks.getItemTitle(bookmarkId), ""); + + // remove the folder using removeItem + PlacesUtils.bookmarks.removeItem(folderId); + do_check_eq(PlacesUtils.bookmarks.getBookmarkIdsForURI(bookmarkURI).length, 0); + do_check_false(PlacesUtils.bookmarks.isBookmarked(bookmarkURI)); + do_check_eq(PlacesUtils.bookmarks.getItemIndex(bookmarkId), -1); +} diff --git a/toolkit/components/places/tests/bookmarks/test_savedsearches.js b/toolkit/components/places/tests/bookmarks/test_savedsearches.js new file mode 100644 index 000000000..eee2c4489 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_savedsearches.js @@ -0,0 +1,209 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// get bookmarks root id +var root = PlacesUtils.bookmarksMenuFolderId; + +// a search term that matches a default bookmark +const searchTerm = "about"; + +var testRoot; + +// main +function run_test() { + // create a folder to hold all the tests + // this makes the tests more tolerant of changes to the default bookmarks set + // also, name it using the search term, for testing that containers that match don't show up in query results + testRoot = PlacesUtils.bookmarks.createFolder( + root, searchTerm, PlacesUtils.bookmarks.DEFAULT_INDEX); + + run_next_test(); +} + +add_test(function test_savedsearches_bookmarks() { + // add a bookmark that matches the search term + var bookmarkId = PlacesUtils.bookmarks.insertBookmark( + root, uri("http://foo.com"), PlacesUtils.bookmarks.DEFAULT_INDEX, + searchTerm); + + // create a saved-search that matches a default bookmark + var searchId = PlacesUtils.bookmarks.insertBookmark( + testRoot, uri("place:terms=" + searchTerm + "&excludeQueries=1&expandQueries=1&queryType=1"), + PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm); + + // query for the test root, expandQueries=0 + // the query should show up as a regular bookmark + try { + let options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 0; + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([testRoot], 1); + let result = PlacesUtils.history.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_eq(cc, 1); + for (let i = 0; i < cc; i++) { + let node = rootNode.getChild(i); + // test that queries have valid itemId + do_check_true(node.itemId > 0); + // test that the container is closed + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(node.containerOpen, false); + } + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("expandQueries=0 query error: " + ex); + } + + // bookmark saved search + // query for the test root, expandQueries=1 + // the query should show up as a query container, with 1 child + try { + let options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 1; + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([testRoot], 1); + let result = PlacesUtils.history.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + do_check_eq(cc, 1); + for (let i = 0; i < cc; i++) { + let node = rootNode.getChild(i); + // test that query node type is container when expandQueries=1 + do_check_eq(node.type, node.RESULT_TYPE_QUERY); + // test that queries (as containers) have valid itemId + do_check_true(node.itemId > 0); + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + node.containerOpen = true; + + // test that queries have children when excludeItems=1 + // test that query nodes don't show containers (shouldn't have our folder that matches) + // test that queries don't show themselves in query results (shouldn't have our saved search) + do_check_eq(node.childCount, 1); + + // test that bookmark shows in query results + var item = node.getChild(0); + do_check_eq(item.itemId, bookmarkId); + + // XXX - FAILING - test live-update of query results - add a bookmark that matches the query + // var tmpBmId = PlacesUtils.bookmarks.insertBookmark( + // root, uri("http://" + searchTerm + ".com"), + // PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm + "blah"); + // do_check_eq(query.childCount, 2); + + // XXX - test live-update of query results - delete a bookmark that matches the query + // PlacesUtils.bookmarks.removeItem(tmpBMId); + // do_check_eq(query.childCount, 1); + + // test live-update of query results - add a folder that matches the query + PlacesUtils.bookmarks.createFolder( + root, searchTerm + "zaa", PlacesUtils.bookmarks.DEFAULT_INDEX); + do_check_eq(node.childCount, 1); + // test live-update of query results - add a query that matches the query + PlacesUtils.bookmarks.insertBookmark( + root, uri("place:terms=foo&excludeQueries=1&expandQueries=1&queryType=1"), + PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm + "blah"); + do_check_eq(node.childCount, 1); + } + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("expandQueries=1 bookmarks query: " + ex); + } + + // delete the bookmark search + PlacesUtils.bookmarks.removeItem(searchId); + + run_next_test(); +}); + +add_task(function* test_savedsearches_history() { + // add a visit that matches the search term + var testURI = uri("http://" + searchTerm + ".com"); + yield PlacesTestUtils.addVisits({ uri: testURI, title: searchTerm }); + + // create a saved-search that matches the visit we added + var searchId = PlacesUtils.bookmarks.insertBookmark(testRoot, + uri("place:terms=" + searchTerm + "&excludeQueries=1&expandQueries=1&queryType=0"), + PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm); + + // query for the test root, expandQueries=1 + // the query should show up as a query container, with 1 child + try { + var options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 1; + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([testRoot], 1); + var result = PlacesUtils.history.executeQuery(query, options); + var rootNode = result.root; + rootNode.containerOpen = true; + var cc = rootNode.childCount; + do_check_eq(cc, 1); + for (var i = 0; i < cc; i++) { + var node = rootNode.getChild(i); + // test that query node type is container when expandQueries=1 + do_check_eq(node.type, node.RESULT_TYPE_QUERY); + // test that queries (as containers) have valid itemId + do_check_eq(node.itemId, searchId); + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + node.containerOpen = true; + + // test that queries have children when excludeItems=1 + // test that query nodes don't show containers (shouldn't have our folder that matches) + // test that queries don't show themselves in query results (shouldn't have our saved search) + do_check_eq(node.childCount, 1); + + // test that history visit shows in query results + var item = node.getChild(0); + do_check_eq(item.type, item.RESULT_TYPE_URI); + do_check_eq(item.itemId, -1); // history visit + do_check_eq(item.uri, testURI.spec); // history visit + + // test live-update of query results - add a history visit that matches the query + yield PlacesTestUtils.addVisits({ + uri: uri("http://foo.com"), + title: searchTerm + "blah" + }); + do_check_eq(node.childCount, 2); + + // test live-update of query results - delete a history visit that matches the query + PlacesUtils.history.removePage(uri("http://foo.com")); + do_check_eq(node.childCount, 1); + node.containerOpen = false; + } + + // test live-update of moved queries + var tmpFolderId = PlacesUtils.bookmarks.createFolder( + testRoot, "foo", PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.bookmarks.moveItem( + searchId, tmpFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX); + var tmpFolderNode = rootNode.getChild(0); + do_check_eq(tmpFolderNode.itemId, tmpFolderId); + tmpFolderNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + tmpFolderNode.containerOpen = true; + do_check_eq(tmpFolderNode.childCount, 1); + + // test live-update of renamed queries + PlacesUtils.bookmarks.setItemTitle(searchId, "foo"); + do_check_eq(tmpFolderNode.title, "foo"); + + // test live-update of deleted queries + PlacesUtils.bookmarks.removeItem(searchId); + try { + tmpFolderNode = root.getChild(1); + do_throw("query was not removed"); + } catch (ex) {} + + tmpFolderNode.containerOpen = false; + rootNode.containerOpen = false; + } + catch (ex) { + do_throw("expandQueries=1 bookmarks query: " + ex); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/xpcshell.ini b/toolkit/components/places/tests/bookmarks/xpcshell.ini new file mode 100644 index 000000000..c290fd693 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini @@ -0,0 +1,50 @@ +[DEFAULT] +head = head_bookmarks.js +tail = +skip-if = toolkit == 'android' + +[test_1016953-renaming-uncompressed.js] +[test_1017502-bookmarks_foreign_count.js] +[test_384228.js] +[test_385829.js] +[test_388695.js] +[test_393498.js] +[test_395101.js] +[test_395593.js] +[test_405938_restore_queries.js] +[test_417228-exclude-from-backup.js] +[test_417228-other-roots.js] +[test_424958-json-quoted-folders.js] +[test_448584.js] +[test_458683.js] +[test_466303-json-remove-backups.js] +[test_477583_json-backup-in-future.js] +[test_675416.js] +[test_711914.js] +[test_818584-discard-duplicate-backups.js] +[test_818587_compress-bookmarks-backups.js] +[test_818593-store-backup-metadata.js] +[test_992901-backup-unsorted-hierarchy.js] +[test_997030-bookmarks-html-encode.js] +[test_1129529.js] +[test_async_observers.js] +[test_bmindex.js] +[test_bookmarkstree_cache.js] +[test_bookmarks.js] +[test_bookmarks_eraseEverything.js] +[test_bookmarks_fetch.js] +[test_bookmarks_getRecent.js] +[test_bookmarks_insert.js] +[test_bookmarks_notifications.js] +[test_bookmarks_remove.js] +[test_bookmarks_reorder.js] +[test_bookmarks_search.js] +[test_bookmarks_update.js] +[test_changeBookmarkURI.js] +[test_getBookmarkedURIFor.js] +[test_keywords.js] +[test_nsINavBookmarkObserver.js] +[test_protectRoots.js] +[test_removeFolderTransaction_reinsert.js] +[test_removeItem.js] +[test_savedsearches.js] diff --git a/toolkit/components/places/tests/browser/.eslintrc.js b/toolkit/components/places/tests/browser/.eslintrc.js new file mode 100644 index 000000000..7a41a9cde --- /dev/null +++ b/toolkit/components/places/tests/browser/.eslintrc.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js", + "../../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/browser/399606-history.go-0.html b/toolkit/components/places/tests/browser/399606-history.go-0.html new file mode 100644 index 000000000..039708ed7 --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-history.go-0.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>history.go(0)</title> +<script> +setTimeout('history.go(0)', 1000); +</script> +</head> +<body> +Testing history.go(0) +</body> +</html> diff --git a/toolkit/components/places/tests/browser/399606-httprefresh.html b/toolkit/components/places/tests/browser/399606-httprefresh.html new file mode 100644 index 000000000..e43455ee0 --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-httprefresh.html @@ -0,0 +1,8 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> + +<meta http-equiv="refresh" content="1"> +<title>httprefresh</title> +</head><body> +Testing httprefresh +</body></html> diff --git a/toolkit/components/places/tests/browser/399606-location.reload.html b/toolkit/components/places/tests/browser/399606-location.reload.html new file mode 100644 index 000000000..0f46538cd --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-location.reload.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>location.reload()</title> +<script> +setTimeout('location.reload();', 100); +</script> +</head> +<body> +Testing location.reload(); +</body> +</html> diff --git a/toolkit/components/places/tests/browser/399606-location.replace.html b/toolkit/components/places/tests/browser/399606-location.replace.html new file mode 100644 index 000000000..36705402c --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-location.replace.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>location.replace</title> +<script> +setTimeout('location.replace(window.location.href)', 1000); +</script> +</head> +<body> +Testing location.replace +</body> +</html> diff --git a/toolkit/components/places/tests/browser/399606-window.location.href.html b/toolkit/components/places/tests/browser/399606-window.location.href.html new file mode 100644 index 000000000..61a2c8ba0 --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-window.location.href.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>window.location.href</title> +<script> +setTimeout('window.location.href = window.location.href', 1000); +</script> +</head> +<body> +Testing window.location.href +</body> +</html> diff --git a/toolkit/components/places/tests/browser/399606-window.location.html b/toolkit/components/places/tests/browser/399606-window.location.html new file mode 100644 index 000000000..e77f73071 --- /dev/null +++ b/toolkit/components/places/tests/browser/399606-window.location.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>window.location</title> +<script> +setTimeout('window.location = window.location', 1000); +</script> +</head> +<body> +Testing window.location +</body> +</html> diff --git a/toolkit/components/places/tests/browser/461710_iframe.html b/toolkit/components/places/tests/browser/461710_iframe.html new file mode 100644 index 000000000..7480fe58f --- /dev/null +++ b/toolkit/components/places/tests/browser/461710_iframe.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> + <head> + </head> + <body> + <iframe id="iframe"></iframe> + </body> +</html> diff --git a/toolkit/components/places/tests/browser/461710_link_page-2.html b/toolkit/components/places/tests/browser/461710_link_page-2.html new file mode 100644 index 000000000..1fc3e0959 --- /dev/null +++ b/toolkit/components/places/tests/browser/461710_link_page-2.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page 2</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the second visited page</a></p>
+ </body>
+</html>
\ No newline at end of file diff --git a/toolkit/components/places/tests/browser/461710_link_page-3.html b/toolkit/components/places/tests/browser/461710_link_page-3.html new file mode 100644 index 000000000..596661803 --- /dev/null +++ b/toolkit/components/places/tests/browser/461710_link_page-3.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page 3</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the third visited page</a></p>
+ </body>
+</html>
\ No newline at end of file diff --git a/toolkit/components/places/tests/browser/461710_link_page.html b/toolkit/components/places/tests/browser/461710_link_page.html new file mode 100644 index 000000000..6bea50628 --- /dev/null +++ b/toolkit/components/places/tests/browser/461710_link_page.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the visited page</a></p>
+ </body>
+</html>
\ No newline at end of file diff --git a/toolkit/components/places/tests/browser/461710_visited_page.html b/toolkit/components/places/tests/browser/461710_visited_page.html new file mode 100644 index 000000000..90e65116b --- /dev/null +++ b/toolkit/components/places/tests/browser/461710_visited_page.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Visited page</title>
+ </head>
+ <body>
+ <p>This page is marked as visited</p>
+ </body>
+</html>
\ No newline at end of file diff --git a/toolkit/components/places/tests/browser/begin.html b/toolkit/components/places/tests/browser/begin.html new file mode 100644 index 000000000..da4c16dd2 --- /dev/null +++ b/toolkit/components/places/tests/browser/begin.html @@ -0,0 +1,10 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<html> + <body> + <a id="clickme" href="redirect_twice.sjs">Redirect twice</a> + </body> +</html> diff --git a/toolkit/components/places/tests/browser/browser.ini b/toolkit/components/places/tests/browser/browser.ini new file mode 100644 index 000000000..e6abe987f --- /dev/null +++ b/toolkit/components/places/tests/browser/browser.ini @@ -0,0 +1,26 @@ +[DEFAULT] +support-files = + colorAnalyzer/category-discover.png + colorAnalyzer/dictionaryGeneric-16.png + colorAnalyzer/extensionGeneric-16.png + colorAnalyzer/localeGeneric.png + head.js + +[browser_bug248970.js] +[browser_bug399606.js] +[browser_bug461710.js] +[browser_bug646422.js] +[browser_bug680727.js] +[browser_colorAnalyzer.js] +[browser_double_redirect.js] +[browser_favicon_privatebrowsing_perwindowpb.js] +[browser_favicon_setAndFetchFaviconForPage.js] +[browser_favicon_setAndFetchFaviconForPage_failures.js] +[browser_history_post.js] +[browser_notfound.js] +[browser_redirect.js] +[browser_settitle.js] +[browser_visited_notfound.js] +[browser_visituri.js] +[browser_visituri_nohistory.js] +[browser_visituri_privatebrowsing_perwindowpb.js]
\ No newline at end of file diff --git a/toolkit/components/places/tests/browser/browser_bug248970.js b/toolkit/components/places/tests/browser/browser_bug248970.js new file mode 100644 index 000000000..5850a3038 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_bug248970.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test performs checks on the history testing area as outlined +// https://wiki.mozilla.org/Firefox3.1/PrivateBrowsing/TestPlan#History +// http://developer.mozilla.org/en/Using_the_Places_history_service + +var visitedURIs = [ + "http://www.test-link.com/", + "http://www.test-typed.com/", + "http://www.test-bookmark.com/", + "http://www.test-redirect-permanent.com/", + "http://www.test-redirect-temporary.com/", + "http://www.test-embed.com/", + "http://www.test-framed.com/", + "http://www.test-download.com/" +].map(NetUtil.newURI.bind(NetUtil)); + +add_task(function* () { + let windowsToClose = []; + let placeItemsCount = 0; + + registerCleanupFunction(function() { + windowsToClose.forEach(function(win) { + win.close(); + }); + }); + + yield PlacesTestUtils.clearHistory(); + + // Ensure we wait for the default bookmarks import. + yield new Promise(resolve => { + waitForCondition(() => { + placeItemsCount = getPlacesItemsCount(); + return placeItemsCount > 0 + }, resolve, "Should have default bookmarks") + }); + + // Create a handful of history items with various visit types + yield PlacesTestUtils.addVisits([ + { uri: visitedURIs[0], transition: TRANSITION_LINK }, + { uri: visitedURIs[1], transition: TRANSITION_TYPED }, + { uri: visitedURIs[2], transition: TRANSITION_BOOKMARK }, + { uri: visitedURIs[3], transition: TRANSITION_REDIRECT_PERMANENT }, + { uri: visitedURIs[4], transition: TRANSITION_REDIRECT_TEMPORARY }, + { uri: visitedURIs[5], transition: TRANSITION_EMBED }, + { uri: visitedURIs[6], transition: TRANSITION_FRAMED_LINK }, + { uri: visitedURIs[7], transition: TRANSITION_DOWNLOAD } + ]); + + placeItemsCount += 7; + // We added 7 new items to history. + is(getPlacesItemsCount(), placeItemsCount, + "Check the total items count"); + + function* testOnWindow(aIsPrivate, aCount) { + let win = yield new Promise(resolve => { + whenNewWindowLoaded({ private: aIsPrivate }, resolve); + }); + windowsToClose.push(win); + + // History items should be retrievable by query + yield checkHistoryItems(); + + // Updates the place items count + let count = getPlacesItemsCount(); + + // Create Bookmark + let title = "title " + windowsToClose.length; + let keyword = "keyword " + windowsToClose.length; + let url = "http://test-a-" + windowsToClose.length + ".com/"; + + yield PlacesUtils.bookmarks.insert({ url, title, + parentGuid: PlacesUtils.bookmarks.menuGuid }); + yield PlacesUtils.keywords.insert({ url, keyword }); + count++; + + ok((yield PlacesUtils.bookmarks.fetch({ url })), + "Bookmark should be bookmarked, data should be retrievable"); + is(getPlacesItemsCount(), count, + "Check the new bookmark items count"); + is(isBookmarkAltered(), false, "Check if bookmark has been visited"); + } + + // Test on windows. + yield testOnWindow(false); + yield testOnWindow(true); + yield testOnWindow(false); +}); + +/** + * Function performs a really simple query on our places entries, + * and makes sure that the number of entries equal num_places_entries. + */ +function getPlacesItemsCount() { + // Get bookmarks count + let options = PlacesUtils.history.getNewQueryOptions(); + options.includeHidden = true; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), options).root; + root.containerOpen = true; + let cc = root.childCount; + root.containerOpen = false; + + // Get history item count + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), options).root; + root.containerOpen = true; + cc += root.childCount; + root.containerOpen = false; + + return cc; +} + +function* checkHistoryItems() { + for (let i = 0; i < visitedURIs.length; i++) { + let visitedUri = visitedURIs[i]; + ok((yield promiseIsURIVisited(visitedUri)), ""); + if (/embed/.test(visitedUri.spec)) { + is((yield PlacesTestUtils.isPageInDB(visitedUri)), false, "Check if URI is in database"); + } else { + ok((yield PlacesTestUtils.isPageInDB(visitedUri)), "Check if URI is in database"); + } + } +} + +/** + * Function attempts to check if Bookmark-A has been visited + * during private browsing mode, function should return false + * + * @returns false if the accessCount has not changed + * true if the accessCount has changed + */ +function isBookmarkAltered() { + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.maxResults = 1; // should only expect a new bookmark + + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.bookmarksMenuFolder], 1); + + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + is(root.childCount, options.maxResults, "Check new bookmarks results"); + let node = root.getChild(0); + root.containerOpen = false; + + return (node.accessCount != 0); +} diff --git a/toolkit/components/places/tests/browser/browser_bug399606.js b/toolkit/components/places/tests/browser/browser_bug399606.js new file mode 100644 index 000000000..b5eee0f92 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_bug399606.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +gBrowser.selectedTab = gBrowser.addTab(); + +function test() { + waitForExplicitFinish(); + + var URIs = [ + "http://example.com/tests/toolkit/components/places/tests/browser/399606-window.location.href.html", + "http://example.com/tests/toolkit/components/places/tests/browser/399606-history.go-0.html", + "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.replace.html", + "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.reload.html", + "http://example.com/tests/toolkit/components/places/tests/browser/399606-httprefresh.html", + "http://example.com/tests/toolkit/components/places/tests/browser/399606-window.location.html", + ]; + var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + + // Create and add history observer. + var historyObserver = { + visitCount: Array(), + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType) { + info("Received onVisit: " + aURI.spec); + if (aURI.spec in this.visitCount) + this.visitCount[aURI.spec]++; + else + this.visitCount[aURI.spec] = 1; + }, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) + }; + hs.addObserver(historyObserver, false); + + function confirm_results() { + gBrowser.removeCurrentTab(); + hs.removeObserver(historyObserver, false); + for (let aURI in historyObserver.visitCount) { + is(historyObserver.visitCount[aURI], 1, + "onVisit has been received right number of times for " + aURI); + } + PlacesTestUtils.clearHistory().then(finish); + } + + var loadCount = 0; + function handleLoad(aEvent) { + loadCount++; + info("new load count is " + loadCount); + + if (loadCount == 3) { + gBrowser.removeEventListener("DOMContentLoaded", handleLoad, true); + gBrowser.loadURI("about:blank"); + executeSoon(check_next_uri); + } + } + + function check_next_uri() { + if (URIs.length) { + let uri = URIs.shift(); + loadCount = 0; + gBrowser.addEventListener("DOMContentLoaded", handleLoad, true); + gBrowser.loadURI(uri); + } + else { + confirm_results(); + } + } + executeSoon(check_next_uri); +} diff --git a/toolkit/components/places/tests/browser/browser_bug461710.js b/toolkit/components/places/tests/browser/browser_bug461710.js new file mode 100644 index 000000000..12af87a06 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_bug461710.js @@ -0,0 +1,82 @@ +const kRed = "rgb(255, 0, 0)"; +const kBlue = "rgb(0, 0, 255)"; + +const prefix = "http://example.com/tests/toolkit/components/places/tests/browser/461710_"; + +add_task(function* () { + let contentPage = prefix + "iframe.html"; + let normalWindow = yield BrowserTestUtils.openNewBrowserWindow(); + + let browser = normalWindow.gBrowser.selectedBrowser; + BrowserTestUtils.loadURI(browser, contentPage); + yield BrowserTestUtils.browserLoaded(browser, contentPage); + + let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true}); + + browser = privateWindow.gBrowser.selectedBrowser; + BrowserTestUtils.loadURI(browser, contentPage); + yield BrowserTestUtils.browserLoaded(browser, contentPage); + + let tests = [{ + win: normalWindow, + topic: "uri-visit-saved", + subtest: "visited_page.html" + }, { + win: normalWindow, + topic: "visited-status-resolution", + subtest: "link_page.html", + color: kRed, + message: "Visited link coloring should work outside of private mode" + }, { + win: privateWindow, + topic: "visited-status-resolution", + subtest: "link_page-2.html", + color: kBlue, + message: "Visited link coloring should not work inside of private mode" + }, { + win: normalWindow, + topic: "visited-status-resolution", + subtest: "link_page-3.html", + color: kRed, + message: "Visited link coloring should work outside of private mode" + }]; + + let visited_page_url = prefix + tests[0].subtest; + for (let test of tests) { + let promise = new Promise(resolve => { + let uri = NetUtil.newURI(visited_page_url); + Services.obs.addObserver(function observe(aSubject) { + if (uri.equals(aSubject.QueryInterface(Ci.nsIURI))) { + Services.obs.removeObserver(observe, test.topic); + resolve(); + } + }, test.topic, false); + }); + ContentTask.spawn(test.win.gBrowser.selectedBrowser, prefix + test.subtest, function* (aSrc) { + content.document.getElementById("iframe").src = aSrc; + }); + yield promise; + + if (test.color) { + // In e10s waiting for visited-status-resolution is not enough to ensure links + // have been updated, because it only tells us that messages to update links + // have been dispatched. We must still wait for the actual links to update. + yield BrowserTestUtils.waitForCondition(function* () { + let color = yield ContentTask.spawn(test.win.gBrowser.selectedBrowser, null, function* () { + let iframe = content.document.getElementById("iframe"); + let elem = iframe.contentDocument.getElementById("link"); + return content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .getVisitedDependentComputedStyle(elem, "", "color"); + }); + return (color == test.color); + }, test.message); + // The harness will consider the test as failed overall if there were no + // passes or failures, so record it as a pass. + ok(true, test.message); + } + } + + yield BrowserTestUtils.closeWindow(normalWindow); + yield BrowserTestUtils.closeWindow(privateWindow); +}); diff --git a/toolkit/components/places/tests/browser/browser_bug646422.js b/toolkit/components/places/tests/browser/browser_bug646422.js new file mode 100644 index 000000000..1a81de4e1 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_bug646422.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for Bug 646224. Make sure that after changing the URI via + * history.pushState, the history service has a title stored for the new URI. + **/ + +add_task(function* () { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, 'http://example.com'); + + let newTitlePromise = new Promise(resolve => { + let observer = { + onTitleChanged: function(uri, title) { + // If the uri of the page whose title is changing ends with 'new_page', + // then it's the result of our pushState. + if (/new_page$/.test(uri.spec)) { + resolve(title); + PlacesUtils.history.removeObserver(observer); + } + }, + + onBeginUpdateBatch: function() { }, + onEndUpdateBatch: function() { }, + onVisit: function() { }, + onDeleteURI: function() { }, + onClearHistory: function() { }, + onPageChanged: function() { }, + onDeleteVisits: function() { }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) + }; + + PlacesUtils.history.addObserver(observer, false); + }); + + yield ContentTask.spawn(tab.linkedBrowser, null, function* () { + let title = content.document.title; + content.history.pushState('', '', 'new_page'); + Assert.ok(title, "Content window should initially have a title."); + }); + + let newtitle = yield newTitlePromise; + + yield ContentTask.spawn(tab.linkedBrowser, { newtitle }, function* (args) { + Assert.equal(args.newtitle, content.document.title, "Title after pushstate."); + }); + + yield PlacesTestUtils.clearHistory(); + gBrowser.removeTab(tab); +}); diff --git a/toolkit/components/places/tests/browser/browser_bug680727.js b/toolkit/components/places/tests/browser/browser_bug680727.js new file mode 100644 index 000000000..560cbfe6c --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_bug680727.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Ensure that clicking the button in the Offline mode neterror page updates + global history. See bug 680727. */ +/* TEST_PATH=toolkit/components/places/tests/browser/browser_bug680727.js make -C $(OBJDIR) mochitest-browser-chrome */ + + +const kUniqueURI = Services.io.newURI("http://mochi.test:8888/#bug_680727", + null, null); +var gAsyncHistory = + Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); + +var proxyPrefValue; +var ourTab; + +function test() { + waitForExplicitFinish(); + + // Tests always connect to localhost, and per bug 87717, localhost is now + // reachable in offline mode. To avoid this, disable any proxy. + proxyPrefValue = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + + // Clear network cache. + Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService) + .clear(); + + // Go offline, expecting the error page. + Services.io.offline = true; + + BrowserTestUtils.openNewForegroundTab(gBrowser).then(tab => { + ourTab = tab; + BrowserTestUtils.waitForContentEvent(ourTab.linkedBrowser, "DOMContentLoaded") + .then(errorListener); + BrowserTestUtils.loadURI(ourTab.linkedBrowser, kUniqueURI.spec); + }); +} + +// ------------------------------------------------------------------------------ +// listen to loading the neterror page. (offline mode) +function errorListener() { + ok(Services.io.offline, "Services.io.offline is true."); + + // This is an error page. + ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) { + Assert.equal(content.document.documentURI.substring(0, 27), + "about:neterror?e=netOffline", "Document URI is the error page."); + + // But location bar should show the original request. + Assert.equal(content.location.href, uri, "Docshell URI is the original URI."); + }).then(() => { + // Global history does not record URI of a failed request. + return PlacesTestUtils.promiseAsyncUpdates().then(() => { + gAsyncHistory.isURIVisited(kUniqueURI, errorAsyncListener); + }); + }); +} + +function errorAsyncListener(aURI, aIsVisited) { + ok(kUniqueURI.equals(aURI) && !aIsVisited, + "The neterror page is not listed in global history."); + + Services.prefs.setIntPref("network.proxy.type", proxyPrefValue); + + // Now press the "Try Again" button, with offline mode off. + Services.io.offline = false; + + BrowserTestUtils.waitForContentEvent(ourTab.linkedBrowser, "DOMContentLoaded") + .then(reloadListener); + + ContentTask.spawn(ourTab.linkedBrowser, null, function() { + Assert.ok(content.document.getElementById("errorTryAgain"), + "The error page has got a #errorTryAgain element"); + content.document.getElementById("errorTryAgain").click(); + }); +} + +// ------------------------------------------------------------------------------ +// listen to reload of neterror. +function reloadListener() { + // This listener catches "DOMContentLoaded" on being called + // nsIWPL::onLocationChange(...). That is right *AFTER* + // IHistory::VisitURI(...) is called. + ok(!Services.io.offline, "Services.io.offline is false."); + + ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) { + // This is not an error page. + Assert.equal(content.document.documentURI, uri, + "Document URI is not the offline-error page, but the original URI."); + }).then(() => { + // Check if global history remembers the successfully-requested URI. + PlacesTestUtils.promiseAsyncUpdates().then(() => { + gAsyncHistory.isURIVisited(kUniqueURI, reloadAsyncListener); + }); + }); +} + +function reloadAsyncListener(aURI, aIsVisited) { + ok(kUniqueURI.equals(aURI) && aIsVisited, "We have visited the URI."); + PlacesTestUtils.clearHistory().then(finish); +} + +registerCleanupFunction(function* () { + Services.prefs.setIntPref("network.proxy.type", proxyPrefValue); + Services.io.offline = false; + yield BrowserTestUtils.removeTab(ourTab); +}); diff --git a/toolkit/components/places/tests/browser/browser_colorAnalyzer.js b/toolkit/components/places/tests/browser/browser_colorAnalyzer.js new file mode 100644 index 000000000..7b7fe6ec5 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_colorAnalyzer.js @@ -0,0 +1,259 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm"); + +const CA = Cc["@mozilla.org/places/colorAnalyzer;1"]. + getService(Ci.mozIColorAnalyzer); + +const hiddenWindowDoc = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService). + hiddenDOMWindow.document; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Passes the given uri to findRepresentativeColor. + * If expected is null, you expect it to fail. + * If expected is a function, it will call that function. + * If expected is a color, you expect that color to be returned. + * Message is used in the calls to is(). + */ +function frcTest(uri, expected, message) { + return new Promise(resolve => { + CA.findRepresentativeColor(Services.io.newURI(uri, "", null), + function(success, color) { + if (expected == null) { + ok(!success, message); + } else if (typeof expected == "function") { + expected(color, message); + } else { + ok(success, "success: " + message); + is(color, expected, message); + } + resolve(); + }); + }); +} + +/** + * Handy function for getting an image into findRepresentativeColor and testing it. + * Makes a canvas with the given dimensions, calls paintCanvasFunc with the 2d + * context of the canvas, sticks the generated canvas into findRepresentativeColor. + * See frcTest. + */ +function canvasTest(width, height, paintCanvasFunc, expected, message) { + let canvas = hiddenWindowDoc.createElementNS(XHTML_NS, "canvas"); + canvas.width = width; + canvas.height = height; + paintCanvasFunc(canvas.getContext("2d")); + let uri = canvas.toDataURL(); + return frcTest(uri, expected, message); +} + +// simple test - draw a red box in the center, make sure we get red back +add_task(function* test_redSquare() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "red"; + ctx.fillRect(2, 2, 12, 12); + }, 0xFF0000, "redSquare analysis returns red"); +}); + + +// draw a blue square in one corner, red in the other, such that blue overlaps +// red by one pixel, making it the dominant color +add_task(function* test_blueOverlappingRed() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 8, 8); + ctx.fillStyle = "blue"; + ctx.fillRect(7, 7, 8, 8); + }, 0x0000FF, "blueOverlappingRed analysis returns blue"); +}); + +// draw a red gradient next to a solid blue rectangle to ensure that a large +// block of similar colors beats out a smaller block of one color +add_task(function* test_redGradientBlueSolid() { + yield canvasTest(16, 16, function(ctx) { + let gradient = ctx.createLinearGradient(0, 0, 1, 15); + gradient.addColorStop(0, "#FF0000"); + gradient.addColorStop(1, "#FF0808"); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "blue"; + ctx.fillRect(9, 0, 7, 16); + }, function(actual, message) { + ok(actual >= 0xFF0000 && actual <= 0xFF0808, message); + }, "redGradientBlueSolid analysis returns redish"); +}); + +// try a transparent image, should fail +add_task(function* test_transparent() { + yield canvasTest(16, 16, function(ctx) { + // do nothing! + }, null, "transparent analysis fails"); +}); + +add_task(function* test_invalidURI() { + yield frcTest("data:blah,Imnotavaliddatauri", null, "invalid URI analysis fails"); +}); + +add_task(function* test_malformedPNGURI() { + yield frcTest("data:image/png;base64,iVBORblahblahblah", null, + "malformed PNG URI analysis fails"); +}); + +add_task(function* test_unresolvableURI() { + yield frcTest("http://www.example.com/blah/idontexist.png", null, + "unresolvable URI analysis fails"); +}); + +// draw a small blue box on a red background to make sure the algorithm avoids +// using the background color +add_task(function* test_blueOnRedBackground() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "blue"; + ctx.fillRect(4, 4, 8, 8); + }, 0x0000FF, "blueOnRedBackground analysis returns blue"); +}); + +// draw a slightly different color in the corners to make sure the corner colors +// don't have to be exactly equal to be considered the background color +add_task(function* test_variableBackground() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "#FEFEFE"; + ctx.fillRect(15, 0, 1, 1); + ctx.fillStyle = "#FDFDFD"; + ctx.fillRect(15, 15, 1, 1); + ctx.fillStyle = "#FCFCFC"; + ctx.fillRect(0, 15, 1, 1); + ctx.fillStyle = "black"; + ctx.fillRect(4, 4, 8, 8); + }, 0x000000, "variableBackground analysis returns black"); +}); + +// like the above test, but make the colors different enough that they aren't +// considered the background color +add_task(function* test_tooVariableBackground() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "#EEDDCC"; + ctx.fillRect(15, 0, 1, 1); + ctx.fillStyle = "#DDDDDD"; + ctx.fillRect(15, 15, 1, 1); + ctx.fillStyle = "#CCCCCC"; + ctx.fillRect(0, 15, 1, 1); + ctx.fillStyle = "black"; + ctx.fillRect(4, 4, 8, 8); + }, function(actual, message) { + isnot(actual, 0x000000, message); + }, "tooVariableBackground analysis doesn't return black"); +}); + +// draw a small black/white box over transparent background to make sure the +// algorithm doesn't think rgb(0,0,0) == rgba(0,0,0,0) +add_task(function* test_transparentBackgroundConflation() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "black"; + ctx.fillRect(2, 2, 12, 12); + ctx.fillStyle = "white"; + ctx.fillRect(5, 5, 6, 6); + }, 0x000000, "transparentBackgroundConflation analysis returns black"); +}); + + +// make sure we fall back to the background color if we have no other choice +// (instead of failing as if there were no colors) +add_task(function* test_backgroundFallback() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, 16, 16); + }, 0x000000, "backgroundFallback analysis returns black"); +}); + +// draw red rectangle next to a pink one to make sure the algorithm picks the +// more interesting color +add_task(function* test_interestingColorPreference() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "#FFDDDD"; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 3, 16); + }, 0xFF0000, "interestingColorPreference analysis returns red"); +}); + +// draw high saturation but dark red next to slightly less saturated color but +// much lighter, to make sure the algorithm doesn't pick colors that are +// nearly black just because of high saturation (in HSL terms) +add_task(function* test_saturationDependence() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "hsl(0, 100%, 5%)"; + ctx.fillRect(0, 0, 16, 16); + ctx.fillStyle = "hsl(0, 90%, 35%)"; + ctx.fillRect(0, 0, 8, 16); + }, 0xA90808, "saturationDependence analysis returns lighter red"); +}); + +// make sure the preference for interesting colors won't stupidly pick 1 pixel +// of red over 169 black pixels +add_task(function* test_interestingColorPreferenceLenient() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "black"; + ctx.fillRect(1, 1, 13, 13); + ctx.fillStyle = "red"; + ctx.fillRect(3, 3, 1, 1); + }, 0x000000, "interestingColorPreferenceLenient analysis returns black"); +}); + +// ...but 6 pixels of red is more reasonable +add_task(function* test_interestingColorPreferenceNotTooLenient() { + yield canvasTest(16, 16, function(ctx) { + ctx.fillStyle = "black"; + ctx.fillRect(1, 1, 13, 13); + ctx.fillStyle = "red"; + ctx.fillRect(3, 3, 3, 2); + }, 0xFF0000, "interestingColorPreferenceNotTooLenient analysis returns red"); +}); + +var maxPixels = 144; // see ColorAnalyzer MAXIMUM_PIXELS const + +// make sure that images larger than maxPixels*maxPixels fail +add_task(function* test_imageTooLarge() { + yield canvasTest(1+maxPixels, 1+maxPixels, function(ctx) { + ctx.fillStyle = "red"; + ctx.fillRect(0, 0, 1+maxPixels, 1+maxPixels); + }, null, "imageTooLarge analysis fails"); +}); + +// the rest of the tests are for coverage of "real" favicons +// exact color isn't terribly important, just make sure it's reasonable +const filePrefix = getRootDirectory(gTestPath) + "colorAnalyzer/"; + +add_task(function* test_categoryDiscover() { + yield frcTest(filePrefix + "category-discover.png", 0xB28D3A, + "category-discover analysis returns red"); +}); + +add_task(function* test_localeGeneric() { + yield frcTest(filePrefix + "localeGeneric.png", 0x3EC23E, + "localeGeneric analysis returns green"); +}); + +add_task(function* test_dictionaryGeneric() { + yield frcTest(filePrefix + "dictionaryGeneric-16.png", 0x854C30, + "dictionaryGeneric-16 analysis returns brown"); +}); + +add_task(function* test_extensionGeneric() { + yield frcTest(filePrefix + "extensionGeneric-16.png", 0x53BA3F, + "extensionGeneric-16 analysis returns green"); +}); diff --git a/toolkit/components/places/tests/browser/browser_double_redirect.js b/toolkit/components/places/tests/browser/browser_double_redirect.js new file mode 100644 index 000000000..1e5dc9c16 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_double_redirect.js @@ -0,0 +1,63 @@ +// Test for bug 411966. +// When a page redirects multiple times, from_visit should point to the +// previous visit in the chain, not to the first visit in the chain. + +add_task(function* () { + yield PlacesTestUtils.clearHistory(); + + const BASE_URL = "http://example.com/tests/toolkit/components/places/tests/browser/"; + const TEST_URI = NetUtil.newURI(BASE_URL + "begin.html"); + const FIRST_REDIRECTING_URI = NetUtil.newURI(BASE_URL + "redirect_twice.sjs"); + const FINAL_URI = NetUtil.newURI(BASE_URL + "final.html"); + + let promiseVisits = new Promise(resolve => { + PlacesUtils.history.addObserver({ + __proto__: NavHistoryObserver.prototype, + _notified: [], + onVisit: function (uri, id, time, sessionId, referrerId, transition) { + info("Received onVisit: " + uri.spec); + this._notified.push(uri); + + if (!uri.equals(FINAL_URI)) { + return; + } + + is(this._notified.length, 4); + PlacesUtils.history.removeObserver(this); + + Task.spawn(function* () { + // Get all pages visited from the original typed one + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute( + `SELECT url FROM moz_historyvisits + JOIN moz_places h ON h.id = place_id + WHERE from_visit IN + (SELECT v.id FROM moz_historyvisits v + JOIN moz_places p ON p.id = v.place_id + WHERE p.url_hash = hash(:url) AND p.url = :url) + `, { url: TEST_URI.spec }); + + is(rows.length, 1, "Found right number of visits"); + let visitedUrl = rows[0].getResultByName("url"); + // Check that redirect from_visit is not from the original typed one + is(visitedUrl, FIRST_REDIRECTING_URI.spec, "Check referrer for " + visitedUrl); + + resolve(); + }); + } + }, false); + }); + + PlacesUtils.history.markPageAsTyped(TEST_URI); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_URI.spec, + }, function* (browser) { + // Load begin page, click link on page to record visits. + yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", {}, browser); + + yield promiseVisits; + }); + + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js b/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js new file mode 100644 index 000000000..51d82adc6 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + const pageURI = + "http://example.org/tests/toolkit/components/places/tests/browser/favicon.html"; + let windowsToClose = []; + + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + function testOnWindow(aIsPrivate, aCallback) { + whenNewWindowLoaded({private: aIsPrivate}, function(aWin) { + windowsToClose.push(aWin); + executeSoon(() => aCallback(aWin)); + }); + } + + function waitForTabLoad(aWin, aCallback) { + aWin.gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + aWin.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + aCallback(); + }, true); + aWin.gBrowser.selectedBrowser.loadURI(pageURI); + } + + testOnWindow(true, function(win) { + waitForTabLoad(win, function() { + PlacesUtils.favicons.getFaviconURLForPage(NetUtil.newURI(pageURI), + function(uri, dataLen, data, mimeType) { + is(uri, null, "No result should be found"); + finish(); + } + ); + }); + }); +} diff --git a/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js new file mode 100644 index 000000000..60df8ebd7 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the normal operation of setAndFetchFaviconForPage. +function test() { + // Initialization + waitForExplicitFinish(); + let windowsToClose = []; + let favIconLocation = + "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png"; + let favIconURI = NetUtil.newURI(favIconLocation); + let favIconMimeType= "image/png"; + let pageURI; + let favIconData; + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + executeSoon(() => aCallback(aWin)); + }); + } + + // This function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + function getIconFile(aCallback) { + NetUtil.asyncFetch({ + uri: favIconLocation, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON + }, function(inputStream, status) { + if (!Components.isSuccessCode(status)) { + ok(false, "Could not get the icon file"); + // Handle error. + return; + } + + // Check the returned size versus the expected size. + let size = inputStream.available(); + favIconData = NetUtil.readInputStreamToString(inputStream, size); + is(size, favIconData.length, "Check correct icon size"); + // Check that the favicon loaded correctly before starting the actual tests. + is(favIconData.length, 344, "Check correct icon length (344)"); + + if (aCallback) { + aCallback(); + } else { + finish(); + } + }); + } + + function testNormal(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/normal"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testNormalCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + addVisits({uri: pageURI, transition: TRANSITION_TYPED}, aWindow, + function () { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + } + ); + } + + function testAboutURIBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("about:testAboutURI_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testAboutURIBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + } + + function testPrivateBrowsingBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/privateBrowsing_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testPrivateBrowsingBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + } + + function testDisabledHistoryBookmarked(aWindow, aCallback) { + pageURI = NetUtil.newURI("http://example.com/disabledHistory_bookmarked"); + waitForFaviconChanged(pageURI, favIconURI, aWindow, + function testDisabledHistoryBookmarkedCallback() { + checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow, + aCallback); + } + ); + + // Disable history while changing the favicon. + aWindow.Services.prefs.setBoolPref("places.history.enabled", false); + + aWindow.PlacesUtils.bookmarks.insertBookmark( + aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI, + aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec); + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + + // The setAndFetchFaviconForPage function calls CanAddURI synchronously, thus + // we can set the preference back to true immediately. We don't clear the + // preference because not all products enable Places by default. + aWindow.Services.prefs.setBoolPref("places.history.enabled", true); + } + + getIconFile(function () { + testOnWindow({}, function(aWin) { + testNormal(aWin, function () { + testOnWindow({}, function(aWin2) { + testAboutURIBookmarked(aWin2, function () { + testOnWindow({private: true}, function(aWin3) { + testPrivateBrowsingBookmarked(aWin3, function () { + testOnWindow({}, function(aWin4) { + testDisabledHistoryBookmarked(aWin4, finish); + }); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js new file mode 100644 index 000000000..bd73af441 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js @@ -0,0 +1,261 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests setAndFetchFaviconForPage when it is called with invalid + * arguments, and when no favicon is stored for the given arguments. + */ +function test() { + // Initialization + waitForExplicitFinish(); + let windowsToClose = []; + let favIcon16Location = + "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal16.png"; + let favIcon32Location = + "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png"; + let favIcon16URI = NetUtil.newURI(favIcon16Location); + let favIcon32URI = NetUtil.newURI(favIcon32Location); + let lastPageURI = NetUtil.newURI("http://example.com/verification"); + // This error icon must stay in sync with FAVICON_ERRORPAGE_URL in + // nsIFaviconService.idl, aboutCertError.xhtml and netError.xhtml. + let favIconErrorPageURI = + NetUtil.newURI("chrome://global/skin/icons/warning-16.png"); + let favIconsResultCount = 0; + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + executeSoon(() => aCallback(aWin)); + }); + } + + // This function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + function checkFavIconsDBCount(aCallback) { + let stmt = DBConn().createAsyncStatement("SELECT url FROM moz_favicons"); + stmt.executeAsync({ + handleResult: function final_handleResult(aResultSet) { + while (aResultSet.getNextRow()) { + favIconsResultCount++; + } + }, + handleError: function final_handleError(aError) { + throw ("Unexpected error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function final_handleCompletion(aReason) { + // begin testing + info("Previous records in moz_favicons: " + favIconsResultCount); + if (aCallback) { + aCallback(); + } + } + }); + stmt.finalize(); + } + + function testNullPageURI(aWindow, aCallback) { + try { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(null, favIcon16URI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + throw ("Exception expected because aPageURI is null."); + } catch (ex) { + // We expected an exception. + ok(true, "Exception expected because aPageURI is null"); + } + + if (aCallback) { + aCallback(); + } + } + + function testNullFavIconURI(aWindow, aCallback) { + try { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage( + NetUtil.newURI("http://example.com/null_faviconURI"), null, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, Services.scriptSecurityManager.getSystemPrincipal()); + throw ("Exception expected because aFaviconURI is null."); + } catch (ex) { + // We expected an exception. + ok(true, "Exception expected because aFaviconURI is null."); + } + + if (aCallback) { + aCallback(); + } + } + + function testAboutURI(aWindow, aCallback) { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage( + NetUtil.newURI("about:testAboutURI"), favIcon16URI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, Services.scriptSecurityManager.getSystemPrincipal()); + + if (aCallback) { + aCallback(); + } + } + + function testPrivateBrowsingNonBookmarkedURI(aWindow, aCallback) { + let pageURI = NetUtil.newURI("http://example.com/privateBrowsing"); + addVisits({ uri: pageURI, transitionType: TRANSITION_TYPED }, aWindow, + function () { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, + favIcon16URI, true, + aWindow.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + + if (aCallback) { + aCallback(); + } + }); + } + + function testDisabledHistory(aWindow, aCallback) { + let pageURI = NetUtil.newURI("http://example.com/disabledHistory"); + addVisits({ uri: pageURI, transition: TRANSITION_TYPED }, aWindow, + function () { + aWindow.Services.prefs.setBoolPref("places.history.enabled", false); + + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, + favIcon16URI, true, + aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + + // The setAndFetchFaviconForPage function calls CanAddURI synchronously, thus + // we can set the preference back to true immediately . We don't clear the + // preference because not all products enable Places by default. + aWindow.Services.prefs.setBoolPref("places.history.enabled", true); + + if (aCallback) { + aCallback(); + } + }); + } + + function testErrorIcon(aWindow, aCallback) { + let pageURI = NetUtil.newURI("http://example.com/errorIcon"); + addVisits({ uri: pageURI, transition: TRANSITION_TYPED }, aWindow, + function () { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, + favIconErrorPageURI, true, + aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + + if (aCallback) { + aCallback(); + } + }); + } + + function testNonExistingPage(aWindow, aCallback) { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage( + NetUtil.newURI("http://example.com/nonexistingPage"), favIcon16URI, + true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + + if (aCallback) { + aCallback(); + } + } + + function testFinalVerification(aWindow, aCallback) { + // Only the last test should raise the onPageChanged notification, + // executing the waitForFaviconChanged callback. + waitForFaviconChanged(lastPageURI, favIcon32URI, aWindow, + function final_callback() { + // Check that only one record corresponding to the last favicon is present. + let resultCount = 0; + let stmt = DBConn().createAsyncStatement("SELECT url FROM moz_favicons"); + stmt.executeAsync({ + handleResult: function final_handleResult(aResultSet) { + + // If the moz_favicons DB had been previously loaded (before our + // test began), we should focus only in the URI we are testing and + // skip the URIs not related to our test. + if (favIconsResultCount > 0) { + for (let row; (row = aResultSet.getNextRow()); ) { + if (favIcon32URI.spec === row.getResultByIndex(0)) { + is(favIcon32URI.spec, row.getResultByIndex(0), + "Check equal favicons"); + resultCount++; + } + } + } else { + for (let row; (row = aResultSet.getNextRow()); ) { + is(favIcon32URI.spec, row.getResultByIndex(0), + "Check equal favicons"); + resultCount++; + } + } + }, + handleError: function final_handleError(aError) { + throw ("Unexpected error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function final_handleCompletion(aReason) { + is(Ci.mozIStorageStatementCallback.REASON_FINISHED, aReason, + "Check reasons are equal"); + is(1, resultCount, "Check result count"); + if (aCallback) { + aCallback(); + } + } + }); + stmt.finalize(); + }); + + // This is the only test that should cause the waitForFaviconChanged + // callback to be invoked. In turn, the callback will invoke + // finish() causing the tests to finish. + addVisits({ uri: lastPageURI, transition: TRANSITION_TYPED }, aWindow, + function () { + aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(lastPageURI, + favIcon32URI, true, + aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, + Services.scriptSecurityManager.getSystemPrincipal()); + }); + } + + checkFavIconsDBCount(function () { + testOnWindow({}, function(aWin) { + testNullPageURI(aWin, function () { + testOnWindow({}, function(aWin2) { + testNullFavIconURI(aWin2, function() { + testOnWindow({}, function(aWin3) { + testAboutURI(aWin3, function() { + testOnWindow({private: true}, function(aWin4) { + testPrivateBrowsingNonBookmarkedURI(aWin4, function () { + testOnWindow({}, function(aWin5) { + testDisabledHistory(aWin5, function () { + testOnWindow({}, function(aWin6) { + testErrorIcon(aWin6, function() { + testOnWindow({}, function(aWin7) { + testNonExistingPage(aWin7, function() { + testOnWindow({}, function(aWin8) { + testFinalVerification(aWin8, function() { + finish(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/toolkit/components/places/tests/browser/browser_history_post.js b/toolkit/components/places/tests/browser/browser_history_post.js new file mode 100644 index 000000000..c85e720f8 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_history_post.js @@ -0,0 +1,23 @@ +const PAGE_URI = "http://example.com/tests/toolkit/components/places/tests/browser/history_post.html"; +const SJS_URI = NetUtil.newURI("http://example.com/tests/toolkit/components/places/tests/browser/history_post.sjs"); + +add_task(function* () { + yield BrowserTestUtils.withNewTab({gBrowser, url: PAGE_URI}, Task.async(function* (aBrowser) { + yield ContentTask.spawn(aBrowser, null, function* () { + let doc = content.document; + let submit = doc.getElementById("submit"); + let iframe = doc.getElementById("post_iframe"); + let p = new Promise((resolve, reject) => { + iframe.addEventListener("load", function onLoad() { + iframe.removeEventListener("load", onLoad); + resolve(); + }); + }); + submit.click(); + yield p; + }); + let visited = yield promiseIsURIVisited(SJS_URI); + ok(!visited, "The POST page should not be added to history"); + ok(!(yield PlacesTestUtils.isPageInDB(SJS_URI.spec)), "The page should not be in the database"); + })); +}); diff --git a/toolkit/components/places/tests/browser/browser_notfound.js b/toolkit/components/places/tests/browser/browser_notfound.js new file mode 100644 index 000000000..20467eef4 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_notfound.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* () { + const TEST_URL = "http://mochi.test:8888/notFoundPage.html"; + + // Used to verify errors are not marked as typed. + PlacesUtils.history.markPageAsTyped(NetUtil.newURI(TEST_URL)); + + // Create and add history observer. + let visitedPromise = new Promise(resolve => { + let historyObserver = { + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType) { + PlacesUtils.history.removeObserver(historyObserver); + info("Received onVisit: " + aURI.spec); + fieldForUrl(aURI, "frecency", function (aFrecency) { + is(aFrecency, 0, "Frecency should be 0"); + fieldForUrl(aURI, "hidden", function (aHidden) { + is(aHidden, 0, "Page should not be hidden"); + fieldForUrl(aURI, "typed", function (aTyped) { + is(aTyped, 0, "page should not be marked as typed"); + resolve(); + }); + }); + }); + }, + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) + }; + PlacesUtils.history.addObserver(historyObserver, false); + }); + + let newTabPromise = BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + yield Promise.all([visitedPromise, newTabPromise]); + + yield PlacesTestUtils.clearHistory(); + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/places/tests/browser/browser_redirect.js b/toolkit/components/places/tests/browser/browser_redirect.js new file mode 100644 index 000000000..d8a19731a --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_redirect.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* () { + const REDIRECT_URI = NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/browser/redirect.sjs"); + const TARGET_URI = NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/browser/redirect-target.html"); + + // Create and add history observer. + let visitedPromise = new Promise(resolve => { + let historyObserver = { + _redirectNotified: false, + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType) { + info("Received onVisit: " + aURI.spec); + + if (aURI.equals(REDIRECT_URI)) { + this._redirectNotified = true; + // Wait for the target page notification. + return; + } + + PlacesUtils.history.removeObserver(historyObserver); + + ok(this._redirectNotified, "The redirect should have been notified"); + + fieldForUrl(REDIRECT_URI, "frecency", function (aFrecency) { + ok(aFrecency != 0, "Frecency or the redirecting page should not be 0"); + + fieldForUrl(REDIRECT_URI, "hidden", function (aHidden) { + is(aHidden, 1, "The redirecting page should be hidden"); + + fieldForUrl(TARGET_URI, "frecency", function (aFrecency2) { + ok(aFrecency2 != 0, "Frecency of the target page should not be 0"); + + fieldForUrl(TARGET_URI, "hidden", function (aHidden2) { + is(aHidden2, 0, "The target page should not be hidden"); + resolve(); + }); + }); + }); + }); + }, + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) + }; + PlacesUtils.history.addObserver(historyObserver, false); + }); + + let newTabPromise = BrowserTestUtils.openNewForegroundTab(gBrowser, REDIRECT_URI.spec); + yield Promise.all([visitedPromise, newTabPromise]); + + yield PlacesTestUtils.clearHistory(); + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/places/tests/browser/browser_settitle.js b/toolkit/components/places/tests/browser/browser_settitle.js new file mode 100644 index 000000000..68c8deda7 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_settitle.js @@ -0,0 +1,76 @@ +var conn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; + +/** + * Gets a single column value from either the places or historyvisits table. + */ +function getColumn(table, column, url) +{ + var stmt = conn.createStatement( + `SELECT ${column} FROM ${table} WHERE url_hash = hash(:val) AND url = :val`); + try { + stmt.params.val = url; + stmt.executeStep(); + return stmt.row[column]; + } + finally { + stmt.finalize(); + } +} + +add_task(function* () +{ + // Make sure titles are correctly saved for a URI with the proper + // notifications. + + // Create and add history observer. + let titleChangedPromise = new Promise(resolve => { + var historyObserver = { + data: [], + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function(aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType) { + }, + onTitleChanged: function(aURI, aPageTitle, aGUID) { + this.data.push({ uri: aURI, title: aPageTitle, guid: aGUID }); + + // We only expect one title change. + // + // Although we are loading two different pages, the first page does not + // have a title. Since the title starts out as empty and then is set + // to empty, there is no title change notification. + + PlacesUtils.history.removeObserver(this); + resolve(this.data); + }, + onDeleteURI: function() {}, + onClearHistory: function() {}, + onPageChanged: function() {}, + onDeleteVisits: function() {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) + }; + PlacesUtils.history.addObserver(historyObserver, false); + }); + + const url1 = "http://example.com/tests/toolkit/components/places/tests/browser/title1.html"; + yield BrowserTestUtils.openNewForegroundTab(gBrowser, url1); + + const url2 = "http://example.com/tests/toolkit/components/places/tests/browser/title2.html"; + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url2); + yield loadPromise; + + let data = yield titleChangedPromise; + is(data[0].uri.spec, "http://example.com/tests/toolkit/components/places/tests/browser/title2.html"); + is(data[0].title, "Some title"); + is(data[0].guid, getColumn("moz_places", "guid", data[0].uri.spec)); + + data.forEach(function(item) { + var title = getColumn("moz_places", "title", data[0].uri.spec); + is(title, item.title); + }); + + gBrowser.removeCurrentTab(); + yield PlacesTestUtils.clearHistory(); +}); + diff --git a/toolkit/components/places/tests/browser/browser_visited_notfound.js b/toolkit/components/places/tests/browser/browser_visited_notfound.js new file mode 100644 index 000000000..b2b4f25b8 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visited_notfound.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URI = NetUtil.newURI("http://mochi.test:8888/notFoundPage.html"); + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); + }); + + // First add a visit to the page, this will ensure that later we skip + // updating the frecency for a newly not-found page. + addVisits({ uri: TEST_URI }, window, () => { + info("Added visit"); + fieldForUrl(TEST_URI, "frecency", aFrecency => { + ok(aFrecency > 0, "Frecency should be > 0"); + continueTest(aFrecency); + }); + }); +} + +function continueTest(aOldFrecency) { + // Used to verify errors are not marked as typed. + PlacesUtils.history.markPageAsTyped(TEST_URI); + gBrowser.selectedBrowser.loadURI(TEST_URI.spec); + + // Create and add history observer. + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType) { + PlacesUtils.history.removeObserver(historyObserver); + info("Received onVisit: " + aURI.spec); + fieldForUrl(aURI, "frecency", function (aFrecency) { + is(aFrecency, aOldFrecency, "Frecency should be unchanged"); + fieldForUrl(aURI, "hidden", function (aHidden) { + is(aHidden, 0, "Page should not be hidden"); + fieldForUrl(aURI, "typed", function (aTyped) { + is(aTyped, 0, "page should not be marked as typed"); + PlacesTestUtils.clearHistory().then(finish); + }); + }); + }); + } + }; + PlacesUtils.history.addObserver(historyObserver, false); +} diff --git a/toolkit/components/places/tests/browser/browser_visituri.js b/toolkit/components/places/tests/browser/browser_visituri.js new file mode 100644 index 000000000..8ba2b7272 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visituri.js @@ -0,0 +1,84 @@ +/** + * One-time observer callback. + */ +function promiseObserve(name, checkFn) { + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject) { + if (checkFn(subject)) { + Services.obs.removeObserver(observer, name); + resolve(); + } + }, name, false); + }); +} + +var conn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; + +/** + * Gets a single column value from either the places or historyvisits table. + */ +function getColumn(table, column, fromColumnName, fromColumnValue) { + let sql = `SELECT ${column} + FROM ${table} + WHERE ${fromColumnName} = :val + ${fromColumnName == "url" ? "AND url_hash = hash(:val)" : ""} + LIMIT 1`; + let stmt = conn.createStatement(sql); + try { + stmt.params.val = fromColumnValue; + ok(stmt.executeStep(), "Expect to get a row"); + return stmt.row[column]; + } + finally { + stmt.reset(); + } +} + +add_task(function* () { + // Make sure places visit chains are saved correctly with a redirect + // transitions. + + // Part 1: observe history events that fire when a visit occurs. + // Make sure visits appear in order, and that the visit chain is correct. + var expectedUrls = [ + "http://example.com/tests/toolkit/components/places/tests/browser/begin.html", + "http://example.com/tests/toolkit/components/places/tests/browser/redirect_twice.sjs", + "http://example.com/tests/toolkit/components/places/tests/browser/redirect_once.sjs", + "http://example.com/tests/toolkit/components/places/tests/browser/final.html" + ]; + var currentIndex = 0; + + function checkObserver(subject) { + var uri = subject.QueryInterface(Ci.nsIURI); + var expected = expectedUrls[currentIndex]; + is(uri.spec, expected, "Saved URL visit " + uri.spec); + + var placeId = getColumn("moz_places", "id", "url", uri.spec); + var fromVisitId = getColumn("moz_historyvisits", "from_visit", "place_id", placeId); + + if (currentIndex == 0) { + is(fromVisitId, 0, "First visit has no from visit"); + } + else { + var lastVisitId = getColumn("moz_historyvisits", "place_id", "id", fromVisitId); + var fromVisitUrl = getColumn("moz_places", "url", "id", lastVisitId); + is(fromVisitUrl, expectedUrls[currentIndex - 1], + "From visit was " + expectedUrls[currentIndex - 1]); + } + + currentIndex++; + return (currentIndex >= expectedUrls.length); + } + let visitUriPromise = promiseObserve("uri-visit-saved", checkObserver); + + const testUrl = "http://example.com/tests/toolkit/components/places/tests/browser/begin.html"; + yield BrowserTestUtils.openNewForegroundTab(gBrowser, testUrl); + + // Load begin page, click link on page to record visits. + yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", { }, gBrowser.selectedBrowser); + yield visitUriPromise; + + yield PlacesTestUtils.clearHistory(); + + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/places/tests/browser/browser_visituri_nohistory.js b/toolkit/components/places/tests/browser/browser_visituri_nohistory.js new file mode 100644 index 000000000..a3a8e7626 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visituri_nohistory.js @@ -0,0 +1,42 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const INITIAL_URL = "http://example.com/tests/toolkit/components/places/tests/browser/begin.html"; +const FINAL_URL = "http://example.com/tests/toolkit/components/places/tests/browser/final.html"; + +/** + * One-time observer callback. + */ +function promiseObserve(name) +{ + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject) { + Services.obs.removeObserver(observer, name); + resolve(subject); + }, name, false); + }); +} + +add_task(function* () +{ + yield new Promise(resolve => SpecialPowers.pushPrefEnv({"set": [["places.history.enabled", false]]}, resolve)); + + let visitUriPromise = promiseObserve("uri-visit-saved"); + + yield BrowserTestUtils.openNewForegroundTab(gBrowser, INITIAL_URL); + + yield new Promise(resolve => SpecialPowers.popPrefEnv(resolve)); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gBrowser.loadURI(FINAL_URL); + yield browserLoadedPromise; + + let subject = yield visitUriPromise; + let uri = subject.QueryInterface(Ci.nsIURI); + is(uri.spec, FINAL_URL, "received expected visit"); + + yield PlacesTestUtils.clearHistory(); + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js b/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js new file mode 100644 index 000000000..abde69a7d --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // initialization + waitForExplicitFinish(); + let windowsToClose = []; + let initialURL = + "http://example.com/tests/toolkit/components/places/tests/browser/begin.html"; + let finalURL = + "http://example.com/tests/toolkit/components/places/tests/browser/final.html"; + let observer = null; + let enumerator = null; + let currentObserver = null; + let uri = null; + + function doTest(aIsPrivateMode, aWindow, aTestURI, aCallback) { + observer = { + observe: function(aSubject, aTopic, aData) { + // The uri-visit-saved topic should only work when on normal mode. + if (aTopic == "uri-visit-saved") { + // Remove the observers set on per window private mode and normal + // mode. + enumerator = aWindow.Services.obs.enumerateObservers("uri-visit-saved"); + while (enumerator.hasMoreElements()) { + currentObserver = enumerator.getNext(); + aWindow.Services.obs.removeObserver(currentObserver, "uri-visit-saved"); + } + + // The expected visit should be the finalURL because private mode + // should not register a visit with the initialURL. + uri = aSubject.QueryInterface(Ci.nsIURI); + is(uri.spec, finalURL, "Check received expected visit"); + } + } + }; + + aWindow.Services.obs.addObserver(observer, "uri-visit-saved", false); + + BrowserTestUtils.browserLoaded(aWindow.gBrowser.selectedBrowser).then(aCallback); + aWindow.gBrowser.selectedBrowser.loadURI(aTestURI); + } + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(() => aCallback(aWin)); + }); + } + + // This function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + // test first when on private mode + testOnWindow({private: true}, function(aWin) { + doTest(true, aWin, initialURL, function() { + // then test when not on private mode + testOnWindow({}, function(aWin2) { + doTest(false, aWin2, finalURL, function () { + PlacesTestUtils.clearHistory().then(finish); + }); + }); + }); + }); +} diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png b/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png Binary files differnew file mode 100644 index 000000000..a6f5b49b3 --- /dev/null +++ b/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png b/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png Binary files differnew file mode 100644 index 000000000..4ad1a1a82 --- /dev/null +++ b/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png b/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png Binary files differnew file mode 100644 index 000000000..fc6c8a258 --- /dev/null +++ b/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png b/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png Binary files differnew file mode 100644 index 000000000..4d9ac5ad8 --- /dev/null +++ b/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png diff --git a/toolkit/components/places/tests/browser/favicon-normal16.png b/toolkit/components/places/tests/browser/favicon-normal16.png Binary files differnew file mode 100644 index 000000000..62b69a3d0 --- /dev/null +++ b/toolkit/components/places/tests/browser/favicon-normal16.png diff --git a/toolkit/components/places/tests/browser/favicon-normal32.png b/toolkit/components/places/tests/browser/favicon-normal32.png Binary files differnew file mode 100644 index 000000000..5535363c9 --- /dev/null +++ b/toolkit/components/places/tests/browser/favicon-normal32.png diff --git a/toolkit/components/places/tests/browser/favicon.html b/toolkit/components/places/tests/browser/favicon.html new file mode 100644 index 000000000..a0f5ea959 --- /dev/null +++ b/toolkit/components/places/tests/browser/favicon.html @@ -0,0 +1,13 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<html> + <head> + <link rel="shortcut icon" href="http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png"> + </head> + <body> + OK we're done! + </body> +</html> diff --git a/toolkit/components/places/tests/browser/final.html b/toolkit/components/places/tests/browser/final.html new file mode 100644 index 000000000..ccd581918 --- /dev/null +++ b/toolkit/components/places/tests/browser/final.html @@ -0,0 +1,10 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<html> + <body> + OK we're done! + </body> +</html> diff --git a/toolkit/components/places/tests/browser/head.js b/toolkit/components/places/tests/browser/head.js new file mode 100644 index 000000000..897585a81 --- /dev/null +++ b/toolkit/components/places/tests/browser/head.js @@ -0,0 +1,319 @@ +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserTestUtils", + "resource://testing-common/BrowserTestUtils.jsm"); + +const TRANSITION_LINK = PlacesUtils.history.TRANSITION_LINK; +const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED; +const TRANSITION_BOOKMARK = PlacesUtils.history.TRANSITION_BOOKMARK; +const TRANSITION_REDIRECT_PERMANENT = PlacesUtils.history.TRANSITION_REDIRECT_PERMANENT; +const TRANSITION_REDIRECT_TEMPORARY = PlacesUtils.history.TRANSITION_REDIRECT_TEMPORARY; +const TRANSITION_EMBED = PlacesUtils.history.TRANSITION_EMBED; +const TRANSITION_FRAMED_LINK = PlacesUtils.history.TRANSITION_FRAMED_LINK; +const TRANSITION_DOWNLOAD = PlacesUtils.history.TRANSITION_DOWNLOAD; + +/** + * Returns a moz_places field value for a url. + * + * @param aURI + * The URI or spec to get field for. + * param aCallback + * Callback function that will get the property value. + */ +function fieldForUrl(aURI, aFieldName, aCallback) +{ + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection.createAsyncStatement( + `SELECT ${aFieldName} FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url` + ); + stmt.params.page_url = url; + stmt.executeAsync({ + _value: -1, + handleResult: function(aResultSet) { + let row = aResultSet.getNextRow(); + if (!row) + ok(false, "The page should exist in the database"); + this._value = row.getResultByName(aFieldName); + }, + handleError: function() {}, + handleCompletion: function(aReason) { + if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) + ok(false, "The statement should properly succeed"); + aCallback(this._value); + } + }); + stmt.finalize(); +} + +/** + * Generic nsINavHistoryObserver that doesn't implement anything, but provides + * dummy methods to prevent errors about an object not having a certain method. + */ +function NavHistoryObserver() {} + +NavHistoryObserver.prototype = { + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onVisit: function () {}, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + ]) +}; + +/** + * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and + * verifies that it matches the expected page URI and associated favicon URI. + * + * This function also double-checks the GUID parameter of the notification. + * + * @param aExpectedPageURI + * nsIURI object of the page whose favicon should change. + * @param aExpectedFaviconURI + * nsIURI object of the newly associated favicon. + * @param aCallback + * This function is called after the check finished. + */ +function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow, + aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) { + if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { + return; + } + aWindow.PlacesUtils.history.removeObserver(this); + + ok(aURI.equals(aExpectedPageURI), + "Check URIs are equal for the page which favicon changed"); + is(aValue, aExpectedFaviconURI.spec, + "Check changed favicon URI is the expected"); + checkGuidForURI(aURI, aGUID); + + if (aCallback) { + aCallback(); + } + } + }; + aWindow.PlacesUtils.history.addObserver(historyObserver, false); +} + +/** + * Asynchronously adds visits to a page, invoking a callback function when done. + * + * @param aPlaceInfo + * Either an nsIURI, in such a case a single LINK visit will be added. + * Or can be an object describing the visit to add, or an array + * of these objects: + * { uri: nsIURI of the page, + * transition: one of the TRANSITION_* from nsINavHistoryService, + * [optional] title: title of the page, + * [optional] visitDate: visit date in microseconds from the epoch + * [optional] referrer: nsIURI of the referrer for this visit + * } + * @param [optional] aCallback + * Function to be invoked on completion. + * @param [optional] aStack + * The stack frame used to report errors. + */ +function addVisits(aPlaceInfo, aWindow, aCallback, aStack) { + let places = []; + if (aPlaceInfo instanceof Ci.nsIURI) { + places.push({ uri: aPlaceInfo }); + } + else if (Array.isArray(aPlaceInfo)) { + places = places.concat(aPlaceInfo); + } else { + places.push(aPlaceInfo) + } + + // Create mozIVisitInfo for each entry. + let now = Date.now(); + for (let place of places) { + if (!place.title) { + place.title = "test visit for " + place.uri.spec; + } + place.visits = [{ + transitionType: place.transition === undefined ? TRANSITION_LINK + : place.transition, + visitDate: place.visitDate || (now++) * 1000, + referrerURI: place.referrer + }]; + } + + aWindow.PlacesUtils.asyncHistory.updatePlaces( + places, + { + handleError: function AAV_handleError() { + throw ("Unexpected error in adding visit."); + }, + handleResult: function () {}, + handleCompletion: function UP_handleCompletion() { + if (aCallback) + aCallback(); + } + } + ); +} + +/** + * Checks that the favicon for the given page matches the provided data. + * + * @param aPageURI + * nsIURI object for the page to check. + * @param aExpectedMimeType + * Expected MIME type of the icon, for example "image/png". + * @param aExpectedData + * Expected icon data, expressed as an array of byte values. + * @param aCallback + * This function is called after the check finished. + */ +function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData, + aWindow, aCallback) { + aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI, + function (aURI, aDataLen, aData, aMimeType) { + is(aExpectedMimeType, aMimeType, "Check expected MimeType"); + is(aExpectedData.length, aData.length, + "Check favicon data for the given page matches the provided data"); + checkGuidForURI(aPageURI); + aCallback(); + }); +} + +/** + * Tests that a guid was set in moz_places for a given uri. + * + * @param aURI + * The uri to check. + * @param [optional] aGUID + * The expected guid in the database. + */ +function checkGuidForURI(aURI, aGUID) { + let guid = doGetGuidForURI(aURI); + if (aGUID) { + doCheckValidPlacesGuid(aGUID); + is(guid, aGUID, "Check equal guid for URIs"); + } +} + +/** + * Retrieves the guid for a given uri. + * + * @param aURI + * The uri to check. + * @return the associated the guid. + */ +function doGetGuidForURI(aURI) { + let stmt = DBConn().createStatement( + `SELECT guid + FROM moz_places + WHERE url_hash = hash(:url) AND url = :url` + ); + stmt.params.url = aURI.spec; + ok(stmt.executeStep(), "Check get guid for uri from moz_places"); + let guid = stmt.row.guid; + stmt.finalize(); + doCheckValidPlacesGuid(guid); + return guid; +} + +/** + * Tests if a given guid is valid for use in Places or not. + * + * @param aGuid + * The guid to test. + */ +function doCheckValidPlacesGuid(aGuid) { + ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places"); +} + +/** + * Gets the database connection. If the Places connection is invalid it will + * try to create a new connection. + * + * @param [optional] aForceNewConnection + * Forces creation of a new connection to the database. When a + * connection is asyncClosed it cannot anymore schedule async statements, + * though connectionReady will keep returning true (Bug 726990). + * + * @return The database connection or null if unable to get one. + */ +function DBConn(aForceNewConnection) { + let gDBConn; + if (!aForceNewConnection) { + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + if (db.connectionReady) + return db; + } + + // If the Places database connection has been closed, create a new connection. + if (!gDBConn || aForceNewConnection) { + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); + file.append("places.sqlite"); + let dbConn = gDBConn = Services.storage.openDatabase(file); + + // Be sure to cleanly close this connection. + Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { + Services.obs.removeObserver(DBCloseCallback, aTopic); + dbConn.asyncClose(); + }, "profile-before-change", false); + } + + return gDBConn.connectionReady ? gDBConn : null; +} + +function whenNewWindowLoaded(aOptions, aCallback) { + BrowserTestUtils.waitForNewWindow().then(aCallback); + OpenBrowserWindow(aOptions); +} + +/** + * Asynchronously check a url is visited. + * + * @param aURI The URI. + * @param aExpectedValue The expected value. + * @return {Promise} + * @resolves When the check has been added successfully. + * @rejects JavaScript exception. + */ +function promiseIsURIVisited(aURI, aExpectedValue) { + return new Promise(resolve => { + PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) { + resolve(aIsVisited); + }); + }); +} + +function waitForCondition(condition, nextTest, errorMsg) { + let tries = 0; + let interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + let conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 200); + function moveOn() { + clearInterval(interval); + nextTest(); + } +} diff --git a/toolkit/components/places/tests/browser/history_post.html b/toolkit/components/places/tests/browser/history_post.html new file mode 100644 index 000000000..a579a9b8a --- /dev/null +++ b/toolkit/components/places/tests/browser/history_post.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test post pages are not added to history</title> + </head> + <body> + <iframe name="post_iframe" id="post_iframe"></iframe> + <form method="post" action="http://example.com/tests/toolkit/components/places/tests/browser/history_post.sjs" target="post_iframe"> + <input type="submit" id="submit"/> + </form> + </body> +</html> diff --git a/toolkit/components/places/tests/browser/history_post.sjs b/toolkit/components/places/tests/browser/history_post.sjs new file mode 100644 index 000000000..3c86aad7b --- /dev/null +++ b/toolkit/components/places/tests/browser/history_post.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) +{ + response.setStatusLine("1.0", 200, "OK"); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + response.write("Ciao"); +} diff --git a/toolkit/components/places/tests/browser/redirect-target.html b/toolkit/components/places/tests/browser/redirect-target.html new file mode 100644 index 000000000..370026338 --- /dev/null +++ b/toolkit/components/places/tests/browser/redirect-target.html @@ -0,0 +1 @@ +<!DOCTYPE html><html><body><p>Ciao!</p></body></html> diff --git a/toolkit/components/places/tests/browser/redirect.sjs b/toolkit/components/places/tests/browser/redirect.sjs new file mode 100644 index 000000000..f55e78eb1 --- /dev/null +++ b/toolkit/components/places/tests/browser/redirect.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) +{ + let page = "<!DOCTYPE html><html><body><p>Redirecting...</p></body></html>"; + + response.setStatusLine(request.httpVersion, "301", "Moved Permanently"); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", page.length + "", false); + response.setHeader("Location", "redirect-target.html", false); + response.write(page); +} diff --git a/toolkit/components/places/tests/browser/redirect_once.sjs b/toolkit/components/places/tests/browser/redirect_once.sjs new file mode 100644 index 000000000..8b2a8aa55 --- /dev/null +++ b/toolkit/components/places/tests/browser/redirect_once.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 301, "Found"); + response.setHeader("Location", "final.html", false); +} diff --git a/toolkit/components/places/tests/browser/redirect_twice.sjs b/toolkit/components/places/tests/browser/redirect_twice.sjs new file mode 100644 index 000000000..099d20022 --- /dev/null +++ b/toolkit/components/places/tests/browser/redirect_twice.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "redirect_once.sjs", false); +} diff --git a/toolkit/components/places/tests/browser/title1.html b/toolkit/components/places/tests/browser/title1.html new file mode 100644 index 000000000..3c98d693e --- /dev/null +++ b/toolkit/components/places/tests/browser/title1.html @@ -0,0 +1,12 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<html> + <head> + </head> + <body> + title1.html + </body> +</html> diff --git a/toolkit/components/places/tests/browser/title2.html b/toolkit/components/places/tests/browser/title2.html new file mode 100644 index 000000000..28a6b69b5 --- /dev/null +++ b/toolkit/components/places/tests/browser/title2.html @@ -0,0 +1,14 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<html> + <head> + <title>Some title</title> + </head> + <body> + title2.html + </body> +</html> + diff --git a/toolkit/components/places/tests/chrome/.eslintrc.js b/toolkit/components/places/tests/chrome/.eslintrc.js new file mode 100644 index 000000000..bf379df8d --- /dev/null +++ b/toolkit/components/places/tests/chrome/.eslintrc.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/chrome.eslintrc.js", + "../../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/chrome/bad_links.atom b/toolkit/components/places/tests/chrome/bad_links.atom new file mode 100644 index 000000000..446927252 --- /dev/null +++ b/toolkit/components/places/tests/chrome/bad_links.atom @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <link href="http://example.org/"/> + <updated>2003-12-13T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> + + <entry> + + <title>First good item</title> + <link href="http://example.org/first"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> + <updated>2003-12-13T18:30:02Z</updated> + + <summary>Some text.</summary> + </entry> + + <entry> + + <title>data: link</title> + <link href="data:text/plain,Hi"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b</id> + <updated>2003-12-13T18:30:03Z</updated> + + <summary>Some text.</summary> + </entry> + + <entry> + + <title>javascript: link</title> + <link href="javascript:alert('Hi')"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6c</id> + <updated>2003-12-13T18:30:04Z</updated> + + <summary>Some text.</summary> + </entry> + + <entry> + + <title>file: link</title> + <link href="file:///var/"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6d</id> + <updated>2003-12-13T18:30:05Z</updated> + + <summary>Some text.</summary> + </entry> + + <entry> + + <title>chrome: link</title> + <link href="chrome://browser/content/browser.js"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6e</id> + <updated>2003-12-13T18:30:06Z</updated> + + <summary>Some text.</summary> + </entry> + + <entry> + + <title>Last good item</title> + <link href="http://example.org/last"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b</id> + <updated>2003-12-13T18:30:07Z</updated> + + <summary>Some text.</summary> + </entry> + + +</feed> diff --git a/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul b/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul new file mode 100644 index 000000000..d7bbfda67 --- /dev/null +++ b/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test disableglobalhistory attribute on remote browsers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="run_test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script> + + <browser id="inprocess_disabled" src="about:blank" type="content" disableglobalhistory="true" /> + <browser id="inprocess_enabled" src="about:blank" type="content" /> + + <browser id="remote_disabled" src="about:blank" type="content" disableglobalhistory="true" /> + <browser id="remote_enabled" src="about:blank" type="content" /> + + <script type="text/javascript;version=1.7"> + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + function expectUseGlobalHistory(id, expected) { + let browser = document.getElementById(id); + return ContentTask.spawn(browser, {id, expected}, function*({id, expected}) { + Assert.equal(docShell.useGlobalHistory, expected, + "Got the right useGlobalHistory state in the docShell of " + id); + }); + } + + function run_test() { + spawn_task(function*() { + yield expectUseGlobalHistory("inprocess_disabled", false); + yield expectUseGlobalHistory("inprocess_enabled", true); + + yield expectUseGlobalHistory("remote_disabled", false); + yield expectUseGlobalHistory("remote_enabled", true); + window.opener.done(); + }); + }; + + </script> +</window>
\ No newline at end of file diff --git a/toolkit/components/places/tests/chrome/chrome.ini b/toolkit/components/places/tests/chrome/chrome.ini new file mode 100644 index 000000000..5ac753e73 --- /dev/null +++ b/toolkit/components/places/tests/chrome/chrome.ini @@ -0,0 +1,12 @@ +[DEFAULT] + +[test_303567.xul] +[test_341972a.xul] +[test_341972b.xul] +[test_342484.xul] +[test_371798.xul] +[test_381357.xul] +[test_favicon_annotations.xul] +[test_reloadLivemarks.xul] +[test_browser_disableglobalhistory.xul] +support-files = browser_disableglobalhistory.xul
\ No newline at end of file diff --git a/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss b/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss new file mode 100644 index 000000000..612b0a5c2 --- /dev/null +++ b/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<rss version="2.0"> + <channel> + <title>feed title</title> + <ttl>180</ttl> + <item> + <title>linked feed item</title> + <link>http://feed-item-link.com</link> + </item> + <item> + <title>link-less feed item</title> + </item> + <item> + <title>linked feed item</title> + <link>http://feed-item-link.com</link> + </item> + </channel> +</rss> diff --git a/toolkit/components/places/tests/chrome/link-less-items.rss b/toolkit/components/places/tests/chrome/link-less-items.rss new file mode 100644 index 000000000..a30d4a353 --- /dev/null +++ b/toolkit/components/places/tests/chrome/link-less-items.rss @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<rss version="2.0"> + <channel> + <title>feed title</title> + <link>http://feed-link.com</link> + <ttl>180</ttl> + <item> + <title>linked feed item</title> + <link>http://feed-item-link.com</link> + </item> + <item> + <title>link-less feed item</title> + </item> + <item> + <title>linked feed item</title> + <link>http://feed-item-link.com</link> + </item> + </channel> +</rss> diff --git a/toolkit/components/places/tests/chrome/rss_as_html.rss b/toolkit/components/places/tests/chrome/rss_as_html.rss new file mode 100644 index 000000000..e82305035 --- /dev/null +++ b/toolkit/components/places/tests/chrome/rss_as_html.rss @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +<channel> +<title>sadfasdfasdfasfasdf</title> +<link>http://www.example.com</link> +<description>asdfasdfasdf.example.com</description> +<language>de</language> +<copyright>asdfasdfasdfasdf</copyright> +<lastBuildDate>Tue, 11 Mar 2008 18:52:52 +0100</lastBuildDate> +<docs>http://blogs.law.harvard.edu/tech/rss</docs> +<ttl>10</ttl> +<item> +<title>The First Title</title> +<link>http://www.example.com/index.html</link> +<pubDate>Tue, 11 Mar 2008 18:24:43 +0100</pubDate> +<content:encoded> +<![CDATA[ +<p> +askdlfjas;dfkjas;fkdj +</p> +]]> +</content:encoded> +<description>aklsjdhfasdjfahasdfhj</description> +<guid>http://foo.example.com/asdfasdf</guid> +</item> +</channel> +</rss> diff --git a/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^ b/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^ new file mode 100644 index 000000000..04fbaa08f --- /dev/null +++ b/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^ @@ -0,0 +1,2 @@ +HTTP 200 OK +Content-Type: text/html diff --git a/toolkit/components/places/tests/chrome/sample_feed.atom b/toolkit/components/places/tests/chrome/sample_feed.atom new file mode 100644 index 000000000..add75efb4 --- /dev/null +++ b/toolkit/components/places/tests/chrome/sample_feed.atom @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <link href="http://example.org/"/> + <updated>2003-12-13T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> + + <entry> + + <title>Atom-Powered Robots Run Amok</title> + <link href="http://example.org/2003/12/13/atom03"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> + <updated>2003-12-13T18:30:02Z</updated> + + <summary>Some text.</summary> + </entry> + +</feed> diff --git a/toolkit/components/places/tests/chrome/test_303567.xul b/toolkit/components/places/tests/chrome/test_303567.xul new file mode 100644 index 000000000..37ae77cbb --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_303567.xul @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Add Bad Livemarks" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +// Test that for feeds with items that have no link: +// * the link-less items are present in the database. +// * the feed's site URI is substituted for each item's link. +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +const LIVEMARKS = [ + { feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items.rss"), + siteURI: NetUtil.newURI("http://mochi.test:8888/"), + urls: [ + "http://feed-item-link.com/", + "http://feed-link.com/", + "http://feed-item-link.com/", + ], + message: "Ensure link-less livemark item picked up site uri.", + }, + { feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss"), + siteURI: null, + urls: [ + "http://feed-item-link.com/", + "http://feed-item-link.com/", + ], + message: "Ensure livemark item links did not inherit site uri." + }, +]; + +function runTest() +{ + let loadCount = 0; + + function testLivemark(aLivemarkData) { + PlacesUtils.livemarks.addLivemark( + { title: "foo" + , parentGuid: PlacesUtils.bookmarks.toolbarGuid + , feedURI: aLivemarkData.feedURI + , siteURI: aLivemarkData.siteURI + }) + .then(function (aLivemark) { + is (aLivemark.feedURI.spec, aLivemarkData.feedURI.spec, + "Get correct feedURI"); + if (aLivemarkData.siteURI) { + is (aLivemark.siteURI.spec, aLivemarkData.siteURI.spec, + "Get correct siteURI"); + } + else { + is (aLivemark.siteURI, null, "Get correct siteURI"); + } + + waitForLivemarkLoad(aLivemark, function (aLivemark) { + let nodes = aLivemark.getNodesForContainer({}); + is(nodes.length, aLivemarkData.urls.length, + "Ensure all the livemark items were created."); + aLivemarkData.urls.forEach(function (aUrl, aIndex) { + let node = nodes[aIndex]; + is(node.uri, aUrl, aLivemarkData.message); + }); + + PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => { + if (++loadCount == LIVEMARKS.length) + SimpleTest.finish(); + }); + }); + }, function () { + is(true, false, "Should not fail adding a livemark"); + } + ); + } + + LIVEMARKS.forEach(testLivemark); +} + +function waitForLivemarkLoad(aLivemark, aCallback) { + // Don't need a real node here. + let node = {}; + let resultObserver = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(node) { + isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED, + "Loading livemark should success"); + if (aLivemark.status == Ci.mozILivemark.STATUS_READY) { + aLivemark.unregisterForUpdates(node, resultObserver); + aCallback(aLivemark); + } + } + }; + aLivemark.registerForUpdates(node, resultObserver); + aLivemark.reload(); +} + +]]> +</script> +</window> diff --git a/toolkit/components/places/tests/chrome/test_341972a.xul b/toolkit/components/places/tests/chrome/test_341972a.xul new file mode 100644 index 000000000..7c78136a9 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_341972a.xul @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Update Livemark SiteURI" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +/* + Test updating livemark siteURI to the value from the feed + */ +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +function runTest() { + const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/sample_feed.atom"; + const INITIALSITESPEC = "http://mochi.test:8888/"; + const FEEDSITESPEC = "http://example.org/"; + + PlacesUtils.livemarks.addLivemark( + { title: "foo" + , parentGuid: PlacesUtils.bookmarks.toolbarGuid + , feedURI: NetUtil.newURI(FEEDSPEC) + , siteURI: NetUtil.newURI(INITIALSITESPEC) + }) + .then(function (aLivemark) { + is(aLivemark.siteURI.spec, INITIALSITESPEC, + "Has correct initial livemark site URI"); + + waitForLivemarkLoad(aLivemark, function (aLivemark) { + is(aLivemark.siteURI.spec, FEEDSITESPEC, + "livemark site URI set to value in feed"); + + PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => { + SimpleTest.finish(); + }); + }); + }, function () { + is(true, false, "Should not fail adding a livemark"); + } + ); +} + +function waitForLivemarkLoad(aLivemark, aCallback) { + // Don't need a real node here. + let node = {}; + let resultObserver = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(node) { + isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED, + "Loading livemark should success"); + if (aLivemark.status == Ci.mozILivemark.STATUS_READY) { + aLivemark.unregisterForUpdates(node, resultObserver); + aCallback(aLivemark); + } + } + }; + aLivemark.registerForUpdates(node, resultObserver); + aLivemark.reload(); +} + +]]> +</script> + +</window> diff --git a/toolkit/components/places/tests/chrome/test_341972b.xul b/toolkit/components/places/tests/chrome/test_341972b.xul new file mode 100644 index 000000000..86cdc75f3 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_341972b.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Update Livemark SiteURI, null to start" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +/* + Test updating livemark siteURI to the value from the feed, when it's null + */ +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +function runTest() { + const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/sample_feed.atom"; + const FEEDSITESPEC = "http://example.org/"; + + PlacesUtils.livemarks.addLivemark( + { title: "foo" + , parentGuid: PlacesUtils.bookmarks.toolbarGuid + , feedURI: NetUtil.newURI(FEEDSPEC) + }) + .then(function (aLivemark) { + is(aLivemark.siteURI, null, "Has null livemark site URI"); + + waitForLivemarkLoad(aLivemark, function (aLivemark) { + is(aLivemark.siteURI.spec, FEEDSITESPEC, + "livemark site URI set to value in feed"); + + PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => { + SimpleTest.finish(); + }); + }); + }, function () { + is(true, false, "Should not fail adding a livemark"); + } + ); +} + +function waitForLivemarkLoad(aLivemark, aCallback) { + // Don't need a real node here. + let node = {}; + let resultObserver = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(node) { + isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED, + "Loading livemark should success"); + if (aLivemark.status == Ci.mozILivemark.STATUS_READY) { + aLivemark.unregisterForUpdates(node, resultObserver); + aCallback(aLivemark); + } + } + }; + aLivemark.registerForUpdates(node, resultObserver); + aLivemark.reload(); +} + +]]> +</script> + +</window> diff --git a/toolkit/components/places/tests/chrome/test_342484.xul b/toolkit/components/places/tests/chrome/test_342484.xul new file mode 100644 index 000000000..353313abb --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_342484.xul @@ -0,0 +1,88 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Add Bad Livemarks" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +/* + Test loading feeds with items that aren't allowed + */ +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +function runTest() { + const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/bad_links.atom"; + const GOOD_URLS = ["http://example.org/first", "http://example.org/last"]; + + PlacesUtils.livemarks.addLivemark( + { title: "foo" + , parentGuid: PlacesUtils.bookmarks.toolbarGuid + , feedURI: NetUtil.newURI(FEEDSPEC) + , siteURI: NetUtil.newURI("http:/mochi.test/") + }) + .then(function (aLivemark) { + waitForLivemarkLoad(aLivemark, function (aLivemark) { + let nodes = aLivemark.getNodesForContainer({}); + + is(nodes.length, 2, "Created the two good livemark items"); + for (let i = 0; i < nodes.length; ++i) { + let node = nodes[i]; + ok(GOOD_URLS.includes(node.uri), "livemark item created with bad uri " + node.uri); + } + + PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => { + SimpleTest.finish(); + }); + }); + }, function () { + is(true, false, "Should not fail adding a livemark"); + } + ); +} + +function waitForLivemarkLoad(aLivemark, aCallback) { + // Don't need a real node here. + let node = {}; + let resultObserver = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(node) { + isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED, + "Loading livemark should success"); + if (aLivemark.status == Ci.mozILivemark.STATUS_READY) { + aLivemark.unregisterForUpdates(node, resultObserver); + aCallback(aLivemark); + } + } + }; + aLivemark.registerForUpdates(node, resultObserver); + aLivemark.reload(); +} + +]]> +</script> + +</window> diff --git a/toolkit/components/places/tests/chrome/test_371798.xul b/toolkit/components/places/tests/chrome/test_371798.xul new file mode 100644 index 000000000..241db75c3 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_371798.xul @@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Bug 371798" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +// Test the asynchronous live-updating of bookmarks query results +SimpleTest.waitForExplicitFinish(); + +var {utils: Cu, interfaces: Ci} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +const TEST_URI = NetUtil.newURI("http://foo.com"); + +function promiseOnItemChanged() { + return new Promise(resolve => { + PlacesUtils.bookmarks.addObserver({ + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemAdded() {}, + onItemRemoved() {}, + onItemVisited() {}, + onItemMoved() {}, + + onItemChanged() { + PlacesUtils.bookmarks.removeObserver(this); + resolve(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) + }, false); + }); +} + +Task.spawn(function* () { + // add 2 bookmarks to the toolbar, same URI, different titles (set later) + let bm1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URI + }); + + let bm2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URI + }); + + // query for bookmarks + let rootNode = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + + // set up observer + let promiseObserved = promiseOnItemChanged(); + + // modify the bookmark's title + yield PlacesUtils.bookmarks.update({ + guid: bm2.guid, title: "foo" + }); + + // wait for notification + yield promiseObserved; + + // Continue after our observer gets notified of onItemChanged + // which is triggered by updating the item's title. + // After receiving the notification, our original query should also + // have been live-updated, so we can iterate through its children, + // to check that only the modified bookmark has changed. + + // result node should be updated + let cc = rootNode.childCount; + for (let i = 0; i < cc; ++i) { + let node = rootNode.getChild(i); + // test that bm1 does not have new title + if (node.bookmarkGuid == bm1.guid) + ok(node.title != "foo", + "Changing a bookmark's title did not affect the title of other bookmarks with the same URI"); + } + rootNode.containerOpen = false; + + // clean up + yield PlacesUtils.bookmarks.remove(bm1); + yield PlacesUtils.bookmarks.remove(bm2); +}).catch(err => { + ok(false, `uncaught error: ${err}`); +}).then(() => { + SimpleTest.finish(); +}); +]]> +</script> + +</window> diff --git a/toolkit/components/places/tests/chrome/test_381357.xul b/toolkit/components/places/tests/chrome/test_381357.xul new file mode 100644 index 000000000..6bd6cb024 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_381357.xul @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Add Livemarks from RSS feed served as text/html" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +/* + Test loading feeds with text/html + */ +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +function runTest() { + const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/rss_as_html.rss"; + + PlacesUtils.livemarks.addLivemark( + { title: "foo" + , parentGuid: PlacesUtils.bookmarks.toolbarGuid + , feedURI: NetUtil.newURI(FEEDSPEC) + , siteURI: NetUtil.newURI("http:/mochi.test/") + }) + .then(function (aLivemark) { + waitForLivemarkLoad(aLivemark, function (aLivemark) { + let nodes = aLivemark.getNodesForContainer({}); + + is(nodes[0].title, "The First Title", + "livemark site URI set to value in feed"); + + PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => { + SimpleTest.finish(); + }); + }); + }, function () { + is(true, false, "Should not fail adding a livemark"); + SimpleTest.finish(); + } + ); +} + +function waitForLivemarkLoad(aLivemark, aCallback) { + // Don't need a real node here. + let node = {}; + let resultObserver = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(node) { + isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED, + "Loading livemark should success"); + if (aLivemark.status == Ci.mozILivemark.STATUS_READY) { + aLivemark.unregisterForUpdates(node, resultObserver); + aCallback(aLivemark); + } + } + }; + aLivemark.registerForUpdates(node, resultObserver); + aLivemark.reload(); +} + +]]> +</script> + +</window> diff --git a/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul b/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul new file mode 100644 index 000000000..3a84f3030 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window title="Test disableglobalhistory attribute on remote browsers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> + + <script type="text/javascript;version=1.7"> + SimpleTest.waitForExplicitFinish(); + + let w = window.open('browser_disableglobalhistory.xul', '_blank', 'chrome,resizable=yes,width=400,height=600'); + + function done() { + w.close(); + SimpleTest.finish(); + } + </script> + +</window>
\ No newline at end of file diff --git a/toolkit/components/places/tests/chrome/test_favicon_annotations.xul b/toolkit/components/places/tests/chrome/test_favicon_annotations.xul new file mode 100644 index 000000000..b7647cbc6 --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_favicon_annotations.xul @@ -0,0 +1,168 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!-- + * This file tests the moz-anno protocol, which was added in Bug 316077 and how + * it loads favicons. +--> + +<window title="Favicon Annotation Protocol Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"> + <![CDATA[ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +let fs = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + +// Test descriptions that will be printed in the case of failure. +let testDescriptions = [ + "moz-anno URI with no data in the database loads default icon", + "URI added to the database is properly loaded", +]; + +// URIs to load (will be compared with expectedURIs of the same index). +let testURIs = [ + "http://mozilla.org/2009/made-up-favicon/places-rocks/", + "http://mozilla.org/should-be-barney/", +]; + +// URIs to load for expected results. +let expectedURIs = [ + fs.defaultFavicon.spec, + "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%04gAMA%00%00%AF%C87%05%8A%E9%00%00%00%19tEXtSoftware%00Adobe%20ImageReadyq%C9e%3C%00%00%01%D6IDATx%DAb%FC%FF%FF%3F%03%25%00%20%80%98%909%EF%DF%BFg%EF%EC%EC%FC%AD%AC%AC%FC%DF%95%91%F1%BF%89%89%C9%7F%20%FF%D7%EA%D5%AB%B7%DF%BBwO%16%9B%01%00%01%C4%00r%01%08%9F9s%C6%CD%D8%D8%F8%BF%0B%03%C3%FF3%40%BC%0A%88%EF%02q%1A%10%BB%40%F1%AAU%ABv%C1%D4%C30%40%00%81%89%993g%3E%06%1A%F6%3F%14%AA%11D%97%03%F1%7Fc%08%0D%E2%2B))%FD%17%04%89%A1%19%00%10%40%0C%D00%F8%0F3%00%C8%F8%BF%1B%E4%0Ac%88a%E5%60%17%19%FF%0F%0D%0D%05%1B%02v%D9%DD%BB%0A0%03%00%02%08%AC%B9%A3%A3%E3%17%03%D4v%90%01%EF%18%106%C3%0Cz%07%C5%BB%A1%DE%82y%07%20%80%A0%A6%08B%FCn%0C1%60%26%D4%20d%C3VA%C3%06%26%BE%0A%EA-%80%00%82%B9%E0%F7L4%0D%EF%90%F8%C6%60%2F%0A%82%BD%01%13%07%0700%D0%01%02%88%11%E4%02P%B41%DC%BB%C7%D0%014%0D%E8l%06W%20%06%BA%88%A1%1C%1AS%15%40%7C%16%CA6.%2Fgx%BFg%0F%83%CB%D9%B3%0C%7B%80%7C%80%00%02%BB%00%E8%9F%ED%20%1B%3A%A0%A6%9F%81%DA%DC%01%C5%B0%80%ED%80%FA%BF%BC%BC%FC%3F%83%12%90%9D%96%F6%1F%20%80%18%DE%BD%7B%C7%0E%8E%05AD%20%FEGr%A6%A0%A0%E0%7F%25P%80%02%9D%0F%D28%13%18%23%C6%C0%B0%02E%3D%C8%F5%00%01%04%8F%05P%A8%BA%40my%87%E4%12c%A8%8D%20%8B%D0%D3%00%08%03%04%10%9C%01R%E4%82d%3B%C8%A0%99%C6%90%90%C6%A5%19%84%01%02%08%9E%17%80%C9x%F7%7B%A0%DBVC%F9%A0%C0%5C%7D%16%2C%CE%00%F4%C6O%5C%99%09%20%800L%04y%A5%03%1A%95%A0%80%05%05%14.%DBA%18%20%80%18)%CD%CE%00%01%06%00%0C'%94%C7%C0k%C9%2C%00%00%00%00IEND%AEB%60%82", +]; + + +/** + * The event listener placed on our test windows used to determine when it is + * safe to compare the two windows. + */ +let _results = []; +function loadEventHandler() +{ + _results.push(snapshotWindow(window)); + + loadNextTest(); +} + +/** + * This runs the comparison. + */ +function compareResults(aIndex, aImage1, aImage2) +{ + let [correct, data1, data2] = compareSnapshots(aImage1, aImage2, true); + SimpleTest.ok(correct, + "Test '" + testDescriptions[aIndex] + "' matches expectations. " + + "Data from window 1 is '" + data1 + "'. " + + "Data from window 2 is '" + data2 + "'"); +} + +/** + * Loads the next set of URIs to compare against. + */ +let _counter = -1; +function loadNextTest() +{ + _counter++; + // If we have no more tests, finish. + if (_counter / 2 == testDescriptions.length) { + for (let i = 0; i < _results.length; i = i + 2) + compareResults(i / 2, _results[i], _results[i + 1]); + + SimpleTest.finish(); + return; + } + + let nextURI = function() { + let index = Math.floor(_counter / 2); + if ((_counter % 2) == 0) + return "moz-anno:favicon:" + testURIs[index]; + return expectedURIs[index]; + } + + let img = document.getElementById("favicon"); + img.setAttribute("src", nextURI()); +} + +function test() +{ + SimpleTest.waitForExplicitFinish(); + let db = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsPIPlacesDatabase). + DBConnection; + + // Empty any old favicons + db.executeSimpleSQL("DELETE FROM moz_favicons"); + + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let uri = function(aSpec) { + return ios.newURI(aSpec, null, null); + }; + + let pageURI = uri("http://example.com/favicon_annotations"); + let history = Cc["@mozilla.org/browser/history;1"] + .getService(Ci.mozIAsyncHistory); + history.updatePlaces( + { + uri: pageURI, + visits: [{ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED, + visitDate: Date.now() * 1000 + }], + }, + { + handleError: function UP_handleError() { + ok(false, "Unexpected error in adding visit."); + }, + handleResult: function () {}, + handleCompletion: function UP_handleCompletion() { + // Set the favicon data. Note that the "moz-anno:" protocol requires + // the favicon to be stored in the database, but the + // replaceFaviconDataFromDataURL function will not save the favicon + // unless it is associated with a page. Thus, we must associate the + // icon with a page explicitly in order for it to be visible through + // the protocol. + var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + + fs.replaceFaviconDataFromDataURL(uri(testURIs[1]), expectedURIs[1], + (Date.now() + 60 * 60 * 24 * 1000) * 1000, + systemPrincipal); + + fs.setAndFetchFaviconForPage(pageURI, uri(testURIs[1]), true, + fs.FAVICON_LOAD_NON_PRIVATE, + null, systemPrincipal); + + // And start our test process. + loadNextTest(); + } + } + ); + + +} + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <img id="favicon" onload="loadEventHandler();"/> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> +</window> diff --git a/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul b/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul new file mode 100644 index 000000000..43772d09f --- /dev/null +++ b/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Reload Livemarks" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest()" onunload="cleanup()"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml" /> + +<script type="application/javascript"> +<![CDATA[ +// Test that for concurrent reload of livemarks. + +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + +let gLivemarks = [ + { id: -1, + title: "foo", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items.rss") + }, + { id: -1, + title: "bar", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss") + }, +]; + +function runTest() +{ + addLivemarks(function () { + reloadLivemarks(false, function () { + reloadLivemarks(true, function () { + removeLivemarks(SimpleTest.finish); + }); + }); + // Ensure this normal reload doesn't overwrite the forced one. + PlacesUtils.livemarks.reloadLivemarks(); + }); +} + +function addLivemarks(aCallback) { + info("Adding livemarks"); + let count = gLivemarks.length; + gLivemarks.forEach(function(aLivemarkData) { + PlacesUtils.livemarks.addLivemark(aLivemarkData) + .then(function (aLivemark) { + ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark added"); + aLivemarkData.id = aLivemark.id; + if (--count == 0) { + aCallback(); + } + }, + function () { + is(true, false, "Should not fail adding a livemark."); + aCallback(); + }); + }); +} + +function reloadLivemarks(aForceUpdate, aCallback) { + info("Reloading livemarks with forceUpdate: " + aForceUpdate); + let count = gLivemarks.length; + gLivemarks.forEach(function(aLivemarkData) { + PlacesUtils.livemarks.getLivemark(aLivemarkData) + .then(aLivemark => { + ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark found"); + aLivemarkData._observer = new resultObserver(aLivemark, function() { + if (++count == gLivemarks.length) { + aCallback(); + } + }); + if (--count == 0) { + PlacesUtils.livemarks.reloadLivemarks(aForceUpdate); + } + }, + function() { + is(true, false, "Should not fail getting a livemark."); + aCallback(); + } + ); + }); +} + +function removeLivemarks(aCallback) { + info("Removing livemarks"); + let count = gLivemarks.length; + gLivemarks.forEach(function(aLivemarkData) { + PlacesUtils.livemarks.removeLivemark(aLivemarkData).then( + function (aLivemark) { + if (--count == 0) { + aCallback(); + } + }, + function() { + is(true, false, "Should not fail adding a livemark."); + aCallback(); + } + ); + }); +} + +function resultObserver(aLivemark, aCallback) { + this._node = {}; + this._livemark = aLivemark; + this._callback = aCallback; + this._livemark.registerForUpdates(this._node, this); +} +resultObserver.prototype = { + nodeInserted: function() {}, + nodeRemoved: function() {}, + nodeAnnotationChanged: function() {}, + nodeTitleChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeMoved: function() {}, + ontainerStateChanged: function () {}, + sortingChanged: function() {}, + batching: function() {}, + invalidateContainer: function(aContainer) { + // Wait for load finish. + if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) + return; + + this._terminate(); + this._callback(); + }, + _terminate: function () { + if (!this._terminated) { + this._livemark.unregisterForUpdates(this._node); + this._terminated = true; + } + } +}; + +function cleanup() { + gLivemarks.forEach(function(aLivemarkData) { + if (aLivemarkData._observer) + aLivemarkData._observer._terminate(); + }); +} +]]> +</script> +</window> diff --git a/toolkit/components/places/tests/cpp/mock_Link.h b/toolkit/components/places/tests/cpp/mock_Link.h new file mode 100644 index 000000000..92ef25d6a --- /dev/null +++ b/toolkit/components/places/tests/cpp/mock_Link.h @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is a mock Link object which can be used in tests. + */ + +#ifndef mock_Link_h__ +#define mock_Link_h__ + +#include "mozilla/MemoryReporting.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/URLSearchParams.h" + +class mock_Link : public mozilla::dom::Link +{ +public: + NS_DECL_ISUPPORTS + + explicit mock_Link(void (*aHandlerFunction)(nsLinkState), + bool aRunNextTest = true) + : mozilla::dom::Link(nullptr) + , mHandler(aHandlerFunction) + , mRunNextTest(aRunNextTest) + { + // Create a cyclic ownership, so that the link will be released only + // after its status has been updated. This will ensure that, when it should + // run the next test, it will happen at the end of the test function, if + // the link status has already been set before. Indeed the link status is + // updated on a separate connection, thus may happen at any time. + mDeathGrip = this; + } + + virtual void SetLinkState(nsLinkState aState) override + { + // Notify our callback function. + mHandler(aState); + + // Break the cycle so the object can be destroyed. + mDeathGrip = nullptr; + } + + virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const override + { + return 0; // the value shouldn't matter + } + +protected: + ~mock_Link() { + // Run the next test if we are supposed to. + if (mRunNextTest) { + run_next_test(); + } + } + +private: + void (*mHandler)(nsLinkState); + bool mRunNextTest; + RefPtr<Link> mDeathGrip; +}; + +NS_IMPL_ISUPPORTS( + mock_Link, + mozilla::dom::Link +) + +//////////////////////////////////////////////////////////////////////////////// +//// Needed Link Methods + +namespace mozilla { +namespace dom { + +Link::Link(Element* aElement) +: mElement(aElement) +, mLinkState(eLinkState_NotLink) +, mRegistered(false) +{ +} + +Link::~Link() +{ +} + +bool +Link::ElementHasHref() const +{ + NS_NOTREACHED("Unexpected call to Link::ElementHasHref"); + return false; // suppress compiler warning +} + +void +Link::SetLinkState(nsLinkState aState) +{ + NS_NOTREACHED("Unexpected call to Link::SetLinkState"); +} + +void +Link::ResetLinkState(bool aNotify, bool aHasHref) +{ + NS_NOTREACHED("Unexpected call to Link::ResetLinkState"); +} + +nsIURI* +Link::GetURI() const +{ + NS_NOTREACHED("Unexpected call to Link::GetURI"); + return nullptr; // suppress compiler warning +} + +size_t +Link::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + NS_NOTREACHED("Unexpected call to Link::SizeOfExcludingThis"); + return 0; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(URLSearchParams) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(URLSearchParams) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(URLSearchParams) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(URLSearchParams) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(URLSearchParams) +NS_IMPL_CYCLE_COLLECTING_RELEASE(URLSearchParams) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(URLSearchParams) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + + +URLSearchParams::URLSearchParams(nsISupports* aParent, + URLSearchParamsObserver* aObserver) +{ +} + +URLSearchParams::~URLSearchParams() +{ +} + +JSObject* +URLSearchParams::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return nullptr; +} + +void +URLSearchParams::ParseInput(const nsACString& aInput) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::ParseInput"); +} + +void +URLSearchParams::Serialize(nsAString& aValue) const +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Serialize"); +} + +void +URLSearchParams::Get(const nsAString& aName, nsString& aRetval) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Get"); +} + +void +URLSearchParams::GetAll(const nsAString& aName, nsTArray<nsString >& aRetval) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::GetAll"); +} + +void +URLSearchParams::Set(const nsAString& aName, const nsAString& aValue) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Set"); +} + +void +URLSearchParams::Append(const nsAString& aName, const nsAString& aValue) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Append"); +} + +void +URLSearchParams::AppendInternal(const nsAString& aName, const nsAString& aValue) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::AppendInternal"); +} + +bool +URLSearchParams::Has(const nsAString& aName) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Has"); + return false; +} + +void +URLSearchParams::Delete(const nsAString& aName) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::Delete"); +} + +void +URLSearchParams::DeleteAll() +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::DeleteAll"); +} + +void +URLSearchParams::NotifyObserver() +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::NotifyObserver"); +} + +NS_IMETHODIMP +URLSearchParams::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, + nsACString& aContentType, nsACString& aCharset) +{ + NS_NOTREACHED("Unexpected call to URLSearchParams::GetSendInfo"); + return NS_OK; +} + +} // namespace dom +} // namespace mozilla + +#endif // mock_Link_h__ diff --git a/toolkit/components/places/tests/cpp/moz.build b/toolkit/components/places/tests/cpp/moz.build new file mode 100644 index 000000000..f6bd91bd7 --- /dev/null +++ b/toolkit/components/places/tests/cpp/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoCppUnitTests([ + 'test_IHistory', +]) + +if CONFIG['JS_SHARED_LIBRARY']: + USE_LIBS += [ + 'js', + ] diff --git a/toolkit/components/places/tests/cpp/places_test_harness.h b/toolkit/components/places/tests/cpp/places_test_harness.h new file mode 100644 index 000000000..557a25f90 --- /dev/null +++ b/toolkit/components/places/tests/cpp/places_test_harness.h @@ -0,0 +1,413 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestHarness.h" +#include "nsMemory.h" +#include "nsThreadUtils.h" +#include "nsDocShellCID.h" + +#include "nsToolkitCompsCID.h" +#include "nsINavHistoryService.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "mozilla/IHistory.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStoragePendingStatement.h" +#include "nsPIPlacesDatabase.h" +#include "nsIObserver.h" +#include "prinrval.h" +#include "prtime.h" +#include "mozilla/Attributes.h" + +#define WAITFORTOPIC_TIMEOUT_SECONDS 5 + + +static size_t gTotalTests = 0; +static size_t gPassedTests = 0; + +#define do_check_true(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected true, got false at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_false(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (!aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected false, got true at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_success(aResult) \ + do_check_true(NS_SUCCEEDED(aResult)) + +#ifdef LINUX +// XXX Linux opt builds on tinderbox are orange due to linking with stdlib. +// This is sad and annoying, but it's a workaround that works. +#define do_check_eq(aExpected, aActual) \ + do_check_true(aExpected == aActual) +#else +#include <sstream> + +#define do_check_eq(aActual, aExpected) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aExpected == aActual) { \ + gPassedTests++; \ + } else { \ + std::ostringstream temp; \ + temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \ + temp << aActual <<"' at line " << __LINE__; \ + fail(temp.str().c_str()); \ + } \ + PR_END_MACRO +#endif + +struct Test +{ + void (*func)(void); + const char* const name; +}; +#define TEST(aName) \ + {aName, #aName} + +/** + * Runs the next text. + */ +void run_next_test(); + +/** + * To be used around asynchronous work. + */ +void do_test_pending(); +void do_test_finished(); + +/** + * Spins current thread until a topic is received. + */ +class WaitForTopicSpinner final : public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + + explicit WaitForTopicSpinner(const char* const aTopic) + : mTopicReceived(false) + , mStartTime(PR_IntervalNow()) + { + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + do_check_true(observerService); + (void)observerService->AddObserver(this, aTopic, false); + } + + void Spin() { + while (!mTopicReceived) { + if ((PR_IntervalNow() - mStartTime) > (WAITFORTOPIC_TIMEOUT_SECONDS * PR_USEC_PER_SEC)) { + // Timed out waiting for the topic. + do_check_true(false); + break; + } + (void)NS_ProcessNextEvent(); + } + } + + NS_IMETHOD Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) override + { + mTopicReceived = true; + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + do_check_true(observerService); + (void)observerService->RemoveObserver(this, aTopic); + return NS_OK; + } + +private: + ~WaitForTopicSpinner() {} + + bool mTopicReceived; + PRIntervalTime mStartTime; +}; +NS_IMPL_ISUPPORTS( + WaitForTopicSpinner, + nsIObserver +) + +/** + * Spins current thread until an async statement is executed. + */ +class AsyncStatementSpinner final : public mozIStorageStatementCallback +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + + AsyncStatementSpinner(); + void SpinUntilCompleted(); + uint16_t completionReason; + +protected: + ~AsyncStatementSpinner() {} + + volatile bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, + mozIStorageStatementCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() +: completionReason(0) +, mCompleted(false) +{ +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet *aResultSet) +{ + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError *aError) +{ + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) +{ + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() +{ + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +struct PlaceRecord +{ + int64_t id; + int32_t hidden; + int32_t typed; + int32_t visitCount; + nsCString guid; +}; + +struct VisitRecord +{ + int64_t id; + int64_t lastVisitId; + int32_t transitionType; +}; + +already_AddRefed<mozilla::IHistory> +do_get_IHistory() +{ + nsCOMPtr<mozilla::IHistory> history = do_GetService(NS_IHISTORY_CONTRACTID); + do_check_true(history); + return history.forget(); +} + +already_AddRefed<nsINavHistoryService> +do_get_NavHistory() +{ + nsCOMPtr<nsINavHistoryService> serv = + do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); + do_check_true(serv); + return serv.forget(); +} + +already_AddRefed<mozIStorageConnection> +do_get_db() +{ + nsCOMPtr<nsINavHistoryService> history = do_get_NavHistory(); + nsCOMPtr<nsPIPlacesDatabase> database = do_QueryInterface(history); + do_check_true(database); + + nsCOMPtr<mozIStorageConnection> dbConn; + nsresult rv = database->GetDBConnection(getter_AddRefs(dbConn)); + do_check_success(rv); + return dbConn.forget(); +} + +/** + * Get the place record from the database. + * + * @param aURI The unique URI of the place we are looking up + * @param result Out parameter where the result is stored + */ +void +do_get_place(nsIURI* aURI, PlaceRecord& result) +{ + nsCOMPtr<mozIStorageConnection> dbConn = do_get_db(); + nsCOMPtr<mozIStorageStatement> stmt; + + nsCString spec; + nsresult rv = aURI->GetSpec(spec); + do_check_success(rv); + + rv = dbConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, hidden, typed, visit_count, guid FROM moz_places " + "WHERE url_hash = hash(?1) AND url = ?1" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + rv = stmt->BindUTF8StringByIndex(0, spec); + do_check_success(rv); + + bool hasResults; + rv = stmt->ExecuteStep(&hasResults); + do_check_success(rv); + if (!hasResults) { + result.id = 0; + return; + } + + rv = stmt->GetInt64(0, &result.id); + do_check_success(rv); + rv = stmt->GetInt32(1, &result.hidden); + do_check_success(rv); + rv = stmt->GetInt32(2, &result.typed); + do_check_success(rv); + rv = stmt->GetInt32(3, &result.visitCount); + do_check_success(rv); + rv = stmt->GetUTF8String(4, result.guid); + do_check_success(rv); +} + +/** + * Gets the most recent visit to a place. + * + * @param placeID ID from the moz_places table + * @param result Out parameter where visit is stored + */ +void +do_get_lastVisit(int64_t placeId, VisitRecord& result) +{ + nsCOMPtr<mozIStorageConnection> dbConn = do_get_db(); + nsCOMPtr<mozIStorageStatement> stmt; + + nsresult rv = dbConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, from_visit, visit_type FROM moz_historyvisits " + "WHERE place_id=?1 " + "LIMIT 1" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + rv = stmt->BindInt64ByIndex(0, placeId); + do_check_success(rv); + + bool hasResults; + rv = stmt->ExecuteStep(&hasResults); + do_check_success(rv); + + if (!hasResults) { + result.id = 0; + return; + } + + rv = stmt->GetInt64(0, &result.id); + do_check_success(rv); + rv = stmt->GetInt64(1, &result.lastVisitId); + do_check_success(rv); + rv = stmt->GetInt32(2, &result.transitionType); + do_check_success(rv); +} + +void +do_wait_async_updates() { + nsCOMPtr<mozIStorageConnection> db = do_get_db(); + nsCOMPtr<mozIStorageAsyncStatement> stmt; + + db->CreateAsyncStatement(NS_LITERAL_CSTRING("BEGIN EXCLUSIVE"), + getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> pending; + (void)stmt->ExecuteAsync(nullptr, getter_AddRefs(pending)); + + db->CreateAsyncStatement(NS_LITERAL_CSTRING("COMMIT"), + getter_AddRefs(stmt)); + RefPtr<AsyncStatementSpinner> spinner = new AsyncStatementSpinner(); + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pending)); + + spinner->SpinUntilCompleted(); +} + +/** + * Adds a URI to the database. + * + * @param aURI + * The URI to add to the database. + */ +void +addURI(nsIURI* aURI) +{ + nsCOMPtr<mozilla::IHistory> history = do_GetService(NS_IHISTORY_CONTRACTID); + do_check_true(history); + nsresult rv = history->VisitURI(aURI, nullptr, mozilla::IHistory::TOP_LEVEL); + do_check_success(rv); + + do_wait_async_updates(); +} + +static const char TOPIC_PROFILE_CHANGE[] = "profile-before-change"; +static const char TOPIC_PLACES_CONNECTION_CLOSED[] = "places-connection-closed"; + +class WaitForConnectionClosed final : public nsIObserver +{ + RefPtr<WaitForTopicSpinner> mSpinner; + + ~WaitForConnectionClosed() {} + +public: + NS_DECL_ISUPPORTS + + WaitForConnectionClosed() + { + nsCOMPtr<nsIObserverService> os = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + MOZ_ASSERT(os); + if (os) { + MOZ_ALWAYS_SUCCEEDS(os->AddObserver(this, TOPIC_PROFILE_CHANGE, false)); + } + mSpinner = new WaitForTopicSpinner(TOPIC_PLACES_CONNECTION_CLOSED); + } + + NS_IMETHOD Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) override + { + nsCOMPtr<nsIObserverService> os = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + MOZ_ASSERT(os); + if (os) { + MOZ_ALWAYS_SUCCEEDS(os->RemoveObserver(this, aTopic)); + } + + mSpinner->Spin(); + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(WaitForConnectionClosed, nsIObserver) diff --git a/toolkit/components/places/tests/cpp/places_test_harness_tail.h b/toolkit/components/places/tests/cpp/places_test_harness_tail.h new file mode 100644 index 000000000..4bbd45ccb --- /dev/null +++ b/toolkit/components/places/tests/cpp/places_test_harness_tail.h @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWidgetsCID.h" +#include "nsIComponentRegistrar.h" +#ifdef MOZ_CRASHREPORTER +#include "nsICrashReporter.h" +#endif + +#ifndef TEST_NAME +#error "Must #define TEST_NAME before including places_test_harness_tail.h" +#endif + +#ifndef TEST_FILE +#error "Must #define TEST_FILE before include places_test_harness_tail.h" +#endif + +int gTestsIndex = 0; + +#define TEST_INFO_STR "TEST-INFO | (%s) | " + +class RunNextTest : public mozilla::Runnable +{ +public: + NS_IMETHOD Run() override + { + NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); + if (gTestsIndex < int(mozilla::ArrayLength(gTests))) { + do_test_pending(); + Test &test = gTests[gTestsIndex++]; + (void)fprintf(stderr, TEST_INFO_STR "Running %s.\n", TEST_FILE, + test.name); + test.func(); + } + + do_test_finished(); + return NS_OK; + } +}; + +void +run_next_test() +{ + nsCOMPtr<nsIRunnable> event = new RunNextTest(); + do_check_success(NS_DispatchToCurrentThread(event)); +} + +int gPendingTests = 0; + +void +do_test_pending() +{ + NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); + gPendingTests++; +} + +void +do_test_finished() +{ + NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); + NS_ASSERTION(gPendingTests > 0, "Invalid pending test count!"); + gPendingTests--; +} + +void +disable_idle_service() +{ + (void)fprintf(stderr, TEST_INFO_STR "Disabling Idle Service.\n", TEST_FILE); + static NS_DEFINE_IID(kIdleCID, NS_IDLE_SERVICE_CID); + nsresult rv; + nsCOMPtr<nsIFactory> idleFactory = do_GetClassObject(kIdleCID, &rv); + do_check_success(rv); + nsCOMPtr<nsIComponentRegistrar> registrar; + rv = NS_GetComponentRegistrar(getter_AddRefs(registrar)); + do_check_success(rv); + rv = registrar->UnregisterFactory(kIdleCID, idleFactory); + do_check_success(rv); +} + +int +main(int aArgc, + char** aArgv) +{ + ScopedXPCOM xpcom(TEST_NAME); + if (xpcom.failed()) + return -1; + // Initialize a profile folder to ensure a clean shutdown. + nsCOMPtr<nsIFile> profile = xpcom.GetProfileDirectory(); + if (!profile) { + fail("Couldn't get the profile directory."); + return -1; + } + +#ifdef MOZ_CRASHREPORTER + char* enabled = PR_GetEnv("MOZ_CRASHREPORTER"); + if (enabled && !strcmp(enabled, "1")) { + // bug 787458: move this to an even-more-common location to use in all + // C++ unittests + nsCOMPtr<nsICrashReporter> crashreporter = + do_GetService("@mozilla.org/toolkit/crash-reporter;1"); + if (crashreporter) { + fprintf(stderr, "Setting up crash reporting\n"); + + nsCOMPtr<nsIProperties> dirsvc = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); + if (!dirsvc) + NS_RUNTIMEABORT("Couldn't get directory service"); + nsCOMPtr<nsIFile> cwd; + nsresult rv = dirsvc->Get(NS_OS_CURRENT_WORKING_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(cwd)); + if (NS_FAILED(rv)) + NS_RUNTIMEABORT("Couldn't get CWD"); + crashreporter->SetEnabled(true); + crashreporter->SetMinidumpPath(cwd); + } + } +#endif + + RefPtr<WaitForConnectionClosed> spinClose = new WaitForConnectionClosed(); + + // Tinderboxes are constantly on idle. Since idle tasks can interact with + // tests, causing random failures, disable the idle service. + disable_idle_service(); + + do_test_pending(); + run_next_test(); + + // Spin the event loop until we've run out of tests to run. + while (gPendingTests) { + (void)NS_ProcessNextEvent(); + } + + // And let any other events finish before we quit. + (void)NS_ProcessPendingEvents(nullptr); + + // Check that we have passed all of our tests, and output accordingly. + if (gPassedTests == gTotalTests) { + passed(TEST_FILE); + } + + (void)fprintf(stderr, TEST_INFO_STR "%u of %u tests passed\n", + TEST_FILE, unsigned(gPassedTests), unsigned(gTotalTests)); + + return gPassedTests == gTotalTests ? 0 : -1; +} diff --git a/toolkit/components/places/tests/cpp/test_IHistory.cpp b/toolkit/components/places/tests/cpp/test_IHistory.cpp new file mode 100644 index 000000000..90998ce8c --- /dev/null +++ b/toolkit/components/places/tests/cpp/test_IHistory.cpp @@ -0,0 +1,639 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "places_test_harness.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "mozilla/Attributes.h" +#include "nsNetUtil.h" + +#include "mock_Link.h" +using namespace mozilla; +using namespace mozilla::dom; + +/** + * This file tests the IHistory interface. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helper Methods + +void +expect_visit(nsLinkState aState) +{ + do_check_true(aState == eLinkState_Visited); +} + +void +expect_no_visit(nsLinkState aState) +{ + do_check_true(aState == eLinkState_Unvisited); +} + +already_AddRefed<nsIURI> +new_test_uri() +{ + // Create a unique spec. + static int32_t specNumber = 0; + nsAutoCString spec = NS_LITERAL_CSTRING("http://mozilla.org/"); + spec.AppendInt(specNumber++); + + // Create the URI for the spec. + nsCOMPtr<nsIURI> testURI; + nsresult rv = NS_NewURI(getter_AddRefs(testURI), spec); + do_check_success(rv); + return testURI.forget(); +} + +class VisitURIObserver final : public nsIObserver +{ + ~VisitURIObserver() {} + +public: + NS_DECL_ISUPPORTS + + explicit VisitURIObserver(int aExpectedVisits = 1) : + mVisits(0), + mExpectedVisits(aExpectedVisits) + { + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + do_check_true(observerService); + (void)observerService->AddObserver(this, + "uri-visit-saved", + false); + } + + void WaitForNotification() + { + while (mVisits < mExpectedVisits) { + (void)NS_ProcessNextEvent(); + } + } + + NS_IMETHOD Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) override + { + mVisits++; + + if (mVisits == mExpectedVisits) { + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + (void)observerService->RemoveObserver(this, "uri-visit-saved"); + } + + return NS_OK; + } +private: + int mVisits; + int mExpectedVisits; +}; +NS_IMPL_ISUPPORTS( + VisitURIObserver, + nsIObserver +) + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void +test_set_places_enabled() +{ + // Ensure places is enabled for everyone. + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + do_check_success(rv); + + rv = prefBranch->SetBoolPref("places.history.enabled", true); + do_check_success(rv); + + // Run the next test. + run_next_test(); +} + + +void +test_wait_checkpoint() +{ + // This "fake" test is here to wait for the initial WAL checkpoint we force + // after creating the database schema, since that may happen at any time, + // and cause concurrent readers to access an older checkpoint. + nsCOMPtr<mozIStorageConnection> db = do_get_db(); + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + RefPtr<AsyncStatementSpinner> spinner = new AsyncStatementSpinner(); + nsCOMPtr<mozIStoragePendingStatement> pending; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pending)); + spinner->SpinUntilCompleted(); + + // Run the next test. + run_next_test(); +} + +// These variables are shared between part 1 and part 2 of the test. Part 2 +// sets the nsCOMPtr's to nullptr, freeing the reference. +namespace test_unvisited_does_not_notify { + nsCOMPtr<nsIURI> testURI; + RefPtr<Link> testLink; +} // namespace test_unvisited_does_not_notify +void +test_unvisited_does_not_notify_part1() +{ + using namespace test_unvisited_does_not_notify; + + // This test is done in two parts. The first part registers for a URI that + // should not be visited. We then run another test that will also do a + // lookup and will be notified. Since requests are answered in the order they + // are requested (at least as long as the same URI isn't asked for later), we + // will know that the Link was not notified. + + // First, we need a test URI. + testURI = new_test_uri(); + + // Create our test Link. + testLink = new mock_Link(expect_no_visit); + + // Now, register our Link to be notified. + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, testLink); + do_check_success(rv); + + // Run the next test. + run_next_test(); +} + +void +test_visited_notifies() +{ + // First, we add our test URI to history. + nsCOMPtr<nsIURI> testURI = new_test_uri(); + addURI(testURI); + + // Create our test Link. The callback function will release the reference we + // have on the Link. + RefPtr<Link> link = new mock_Link(expect_visit); + + // Now, register our Link to be notified. + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, link); + do_check_success(rv); + + // Note: test will continue upon notification. +} + +void +test_unvisited_does_not_notify_part2() +{ + using namespace test_unvisited_does_not_notify; + + // We would have had a failure at this point had the content node been told it + // was visited. Therefore, it is safe to unregister our content node. + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->UnregisterVisitedCallback(testURI, testLink); + do_check_success(rv); + + // Clear the stored variables now. + testURI = nullptr; + testLink = nullptr; + + // Run the next test. + run_next_test(); +} + +void +test_same_uri_notifies_both() +{ + // First, we add our test URI to history. + nsCOMPtr<nsIURI> testURI = new_test_uri(); + addURI(testURI); + + // Create our two test Links. The callback function will release the + // reference we have on the Links. Only the second Link should run the next + // test! + RefPtr<Link> link1 = new mock_Link(expect_visit, false); + RefPtr<Link> link2 = new mock_Link(expect_visit); + + // Now, register our Link to be notified. + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, link1); + do_check_success(rv); + rv = history->RegisterVisitedCallback(testURI, link2); + do_check_success(rv); + + // Note: test will continue upon notification. +} + +void +test_unregistered_visited_does_not_notify() +{ + // This test must have a test that has a successful notification after it. + // The Link would have been notified by now if we were buggy and notified + // unregistered Links (due to request serialization). + + nsCOMPtr<nsIURI> testURI = new_test_uri(); + RefPtr<Link> link = new mock_Link(expect_no_visit); + + // Now, register our Link to be notified. + nsCOMPtr<IHistory> history(do_get_IHistory()); + nsresult rv = history->RegisterVisitedCallback(testURI, link); + do_check_success(rv); + + // Unregister the Link. + rv = history->UnregisterVisitedCallback(testURI, link); + do_check_success(rv); + + // And finally add a visit for the URI. + addURI(testURI); + + // If history tries to notify us, we'll either crash because the Link will + // have been deleted (we are the only thing holding a reference to it), or our + // expect_no_visit call back will produce a failure. Either way, the test + // will be reported as a failure. + + // Run the next test. + run_next_test(); +} + +void +test_new_visit_notifies_waiting_Link() +{ + // Create our test Link. The callback function will release the reference we + // have on the link. + RefPtr<Link> link = new mock_Link(expect_visit); + + // Now, register our content node to be notified. + nsCOMPtr<nsIURI> testURI = new_test_uri(); + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, link); + do_check_success(rv); + + // Add ourselves to history. + addURI(testURI); + + // Note: test will continue upon notification. +} + +void +test_RegisterVisitedCallback_returns_before_notifying() +{ + // Add a URI so that it's already in history. + nsCOMPtr<nsIURI> testURI = new_test_uri(); + addURI(testURI); + + // Create our test Link. + RefPtr<Link> link = new mock_Link(expect_no_visit); + + // Now, register our content node to be notified. It should not be notified. + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, link); + do_check_success(rv); + + // Remove ourselves as an observer. We would have failed if we had been + // notified. + rv = history->UnregisterVisitedCallback(testURI, link); + do_check_success(rv); + + run_next_test(); +} + +namespace test_observer_topic_dispatched_helpers { + #define URI_VISITED "visited" + #define URI_NOT_VISITED "not visited" + #define URI_VISITED_RESOLUTION_TOPIC "visited-status-resolution" + class statusObserver final : public nsIObserver + { + ~statusObserver() {} + + public: + NS_DECL_ISUPPORTS + + statusObserver(nsIURI* aURI, + const bool aExpectVisit, + bool& _notified) + : mURI(aURI) + , mExpectVisit(aExpectVisit) + , mNotified(_notified) + { + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + do_check_true(observerService); + (void)observerService->AddObserver(this, + URI_VISITED_RESOLUTION_TOPIC, + false); + } + + NS_IMETHOD Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) override + { + // Make sure we got notified of the right topic. + do_check_false(strcmp(aTopic, URI_VISITED_RESOLUTION_TOPIC)); + + // If this isn't for our URI, do not do anything. + nsCOMPtr<nsIURI> notifiedURI = do_QueryInterface(aSubject); + do_check_true(notifiedURI); + + bool isOurURI; + nsresult rv = notifiedURI->Equals(mURI, &isOurURI); + do_check_success(rv); + if (!isOurURI) { + return NS_OK; + } + + // Check that we have either the visited or not visited string. + bool visited = !!NS_LITERAL_STRING(URI_VISITED).Equals(aData); + bool notVisited = !!NS_LITERAL_STRING(URI_NOT_VISITED).Equals(aData); + do_check_true(visited || notVisited); + + // Check to make sure we got the state we expected. + do_check_eq(visited, mExpectVisit); + + // Indicate that we've been notified. + mNotified = true; + + // Remove ourselves as an observer. + nsCOMPtr<nsIObserverService> observerService = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + (void)observerService->RemoveObserver(this, + URI_VISITED_RESOLUTION_TOPIC); + return NS_OK; + } + private: + nsCOMPtr<nsIURI> mURI; + const bool mExpectVisit; + bool& mNotified; + }; + NS_IMPL_ISUPPORTS( + statusObserver, + nsIObserver + ) +} // namespace test_observer_topic_dispatched_helpers +void +test_observer_topic_dispatched() +{ + using namespace test_observer_topic_dispatched_helpers; + + // Create two URIs, making sure only one is in history. + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + nsCOMPtr<nsIURI> notVisitedURI = new_test_uri(); + bool urisEqual; + nsresult rv = visitedURI->Equals(notVisitedURI, &urisEqual); + do_check_success(rv); + do_check_false(urisEqual); + addURI(visitedURI); + + // Need two Link objects as well - one for each URI. + RefPtr<Link> visitedLink = new mock_Link(expect_visit, false); + RefPtr<Link> visitedLinkCopy = visitedLink; + RefPtr<Link> notVisitedLink = new mock_Link(expect_no_visit); + + // Add the right observers for the URIs to check results. + bool visitedNotified = false; + nsCOMPtr<nsIObserver> visitedObs = + new statusObserver(visitedURI, true, visitedNotified); + bool notVisitedNotified = false; + nsCOMPtr<nsIObserver> unvisitedObs = + new statusObserver(notVisitedURI, false, notVisitedNotified); + + // Register our Links to be notified. + nsCOMPtr<IHistory> history = do_get_IHistory(); + rv = history->RegisterVisitedCallback(visitedURI, visitedLink); + do_check_success(rv); + rv = history->RegisterVisitedCallback(notVisitedURI, notVisitedLink); + do_check_success(rv); + + // Spin the event loop as long as we have not been properly notified. + while (!visitedNotified || !notVisitedNotified) { + (void)NS_ProcessNextEvent(); + } + + // Unregister our observer that would not have been released. + rv = history->UnregisterVisitedCallback(notVisitedURI, notVisitedLink); + do_check_success(rv); + + run_next_test(); +} + +void +test_visituri_inserts() +{ + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + PlaceRecord place; + do_get_place(visitedURI, place); + + do_check_true(place.id > 0); + do_check_false(place.hidden); + do_check_false(place.typed); + do_check_eq(place.visitCount, 1); + + run_next_test(); +} + +void +test_visituri_updates() +{ + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + RefPtr<VisitURIObserver> finisher; + + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + PlaceRecord place; + do_get_place(visitedURI, place); + + do_check_eq(place.visitCount, 2); + + run_next_test(); +} + +void +test_visituri_preserves_shown_and_typed() +{ + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + // this simulates the uri visit happening in a frame. Normally frame + // transitions would be hidden unless it was previously loaded top-level + history->VisitURI(visitedURI, lastURI, 0); + + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(2); + finisher->WaitForNotification(); + + PlaceRecord place; + do_get_place(visitedURI, place); + do_check_false(place.hidden); + + run_next_test(); +} + +void +test_visituri_creates_visit() +{ + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + PlaceRecord place; + VisitRecord visit; + do_get_place(visitedURI, place); + do_get_lastVisit(place.id, visit); + + do_check_true(visit.id > 0); + do_check_eq(visit.lastVisitId, 0); + do_check_eq(visit.transitionType, nsINavHistoryService::TRANSITION_LINK); + + run_next_test(); +} + +void +test_visituri_transition_typed() +{ + nsCOMPtr<nsINavHistoryService> navHistory = do_get_NavHistory(); + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + + navHistory->MarkPageAsTyped(visitedURI); + history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL); + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + PlaceRecord place; + VisitRecord visit; + do_get_place(visitedURI, place); + do_get_lastVisit(place.id, visit); + + do_check_true(visit.transitionType == nsINavHistoryService::TRANSITION_TYPED); + + run_next_test(); +} + +void +test_visituri_transition_embed() +{ + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsCOMPtr<nsIURI> lastURI = new_test_uri(); + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + + history->VisitURI(visitedURI, lastURI, 0); + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + PlaceRecord place; + VisitRecord visit; + do_get_place(visitedURI, place); + do_get_lastVisit(place.id, visit); + + do_check_eq(place.id, 0); + do_check_eq(visit.id, 0); + + run_next_test(); +} + +void +test_new_visit_adds_place_guid() +{ + // First, add a visit and wait. This will also add a place. + nsCOMPtr<nsIURI> visitedURI = new_test_uri(); + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->VisitURI(visitedURI, nullptr, + mozilla::IHistory::TOP_LEVEL); + do_check_success(rv); + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + // Check that we have a guid for our visit. + PlaceRecord place; + do_get_place(visitedURI, place); + do_check_eq(place.visitCount, 1); + do_check_eq(place.guid.Length(), 12); + + run_next_test(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// IPC-only Tests + +void +test_two_null_links_same_uri() +{ + // Tests that we do not crash when we have had two nullptr Links passed to + // RegisterVisitedCallback and then the visit occurs (bug 607469). This only + // happens in IPC builds. + nsCOMPtr<nsIURI> testURI = new_test_uri(); + + nsCOMPtr<IHistory> history = do_get_IHistory(); + nsresult rv = history->RegisterVisitedCallback(testURI, nullptr); + do_check_success(rv); + rv = history->RegisterVisitedCallback(testURI, nullptr); + do_check_success(rv); + + rv = history->VisitURI(testURI, nullptr, mozilla::IHistory::TOP_LEVEL); + do_check_success(rv); + + RefPtr<VisitURIObserver> finisher = new VisitURIObserver(); + finisher->WaitForNotification(); + + run_next_test(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Harness + +/** + * Note: for tests marked "Order Important!", please see the test for details. + */ +Test gTests[] = { + TEST(test_set_places_enabled), // Must come first! + TEST(test_wait_checkpoint), // Must come second! + TEST(test_unvisited_does_not_notify_part1), // Order Important! + TEST(test_visited_notifies), + TEST(test_unvisited_does_not_notify_part2), // Order Important! + TEST(test_same_uri_notifies_both), + TEST(test_unregistered_visited_does_not_notify), // Order Important! + TEST(test_new_visit_notifies_waiting_Link), + TEST(test_RegisterVisitedCallback_returns_before_notifying), + TEST(test_observer_topic_dispatched), + TEST(test_visituri_inserts), + TEST(test_visituri_updates), + TEST(test_visituri_preserves_shown_and_typed), + TEST(test_visituri_creates_visit), + TEST(test_visituri_transition_typed), + TEST(test_visituri_transition_embed), + TEST(test_new_visit_adds_place_guid), + + // The rest of these tests are tests that are only run in IPC builds. + TEST(test_two_null_links_same_uri), +}; + +const char* file = __FILE__; +#define TEST_NAME "IHistory" +#define TEST_FILE file +#include "places_test_harness_tail.h" diff --git a/toolkit/components/places/tests/expiration/.eslintrc.js b/toolkit/components/places/tests/expiration/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/expiration/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/expiration/head_expiration.js b/toolkit/components/places/tests/expiration/head_expiration.js new file mode 100644 index 000000000..2be4af307 --- /dev/null +++ b/toolkit/components/places/tests/expiration/head_expiration.js @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + + +// Simulates an expiration at shutdown. +function shutdownExpiration() +{ + let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver); + expire.observe(null, "places-will-close-connection", null); +} + + +/** + * Causes expiration component to start, otherwise it would wait for the first + * history notification. + */ +function force_expiration_start() { + Cc["@mozilla.org/places/expiration;1"] + .getService(Ci.nsIObserver) + .observe(null, "testing-mode", null); +} + + +/** + * Forces an expiration run. + * + * @param [optional] aLimit + * Limit for the expiration. Pass -1 for unlimited. + * Any other non-positive value will just expire orphans. + * + * @return {Promise} + * @resolves When expiration finishes. + * @rejects Never. + */ +function promiseForceExpirationStep(aLimit) { + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver); + expire.observe(null, "places-debug-start-expiration", aLimit); + return promise; +} + + +/** + * Expiration preferences helpers. + */ + +function setInterval(aNewInterval) { + Services.prefs.setIntPref("places.history.expiration.interval_seconds", aNewInterval); +} +function getInterval() { + return Services.prefs.getIntPref("places.history.expiration.interval_seconds"); +} +function clearInterval() { + try { + Services.prefs.clearUserPref("places.history.expiration.interval_seconds"); + } + catch (ex) {} +} + + +function setMaxPages(aNewMaxPages) { + Services.prefs.setIntPref("places.history.expiration.max_pages", aNewMaxPages); +} +function getMaxPages() { + return Services.prefs.getIntPref("places.history.expiration.max_pages"); +} +function clearMaxPages() { + try { + Services.prefs.clearUserPref("places.history.expiration.max_pages"); + } + catch (ex) {} +} + + +function setHistoryEnabled(aHistoryEnabled) { + Services.prefs.setBoolPref("places.history.enabled", aHistoryEnabled); +} +function getHistoryEnabled() { + return Services.prefs.getBoolPref("places.history.enabled"); +} +function clearHistoryEnabled() { + try { + Services.prefs.clearUserPref("places.history.enabled"); + } + catch (ex) {} +} + +/** + * Returns a PRTime in the past usable to add expirable visits. + * + * param [optional] daysAgo + * Expiration ignores any visit added in the last 7 days, so by default + * this will be set to 7. + * @note to be safe against DST issues we go back one day more. + */ +function getExpirablePRTime(daysAgo = 7) { + let dateObj = new Date(); + // Normalize to midnight + dateObj.setHours(0); + dateObj.setMinutes(0); + dateObj.setSeconds(0); + dateObj.setMilliseconds(0); + dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000); + return dateObj.getTime() * 1000; +} diff --git a/toolkit/components/places/tests/expiration/test_analyze_runs.js b/toolkit/components/places/tests/expiration/test_analyze_runs.js new file mode 100644 index 000000000..1a84e1b38 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_analyze_runs.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Constants + +const TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING = "autocomplete-will-enter-text"; + +// Helpers + +/** + * Ensures that we have no data in the tables created by ANALYZE. + */ +function clearAnalyzeData() { + let db = DBConn(); + if (!db.tableExists("sqlite_stat1")) { + return; + } + db.executeSimpleSQL("DELETE FROM sqlite_stat1"); +} + +/** + * Checks that we ran ANALYZE on the specified table. + * + * @param aTableName + * The table to check if ANALYZE was ran. + * @param aRan + * True if it was expected to run, false otherwise + */ +function do_check_analyze_ran(aTableName, aRan) { + let db = DBConn(); + do_check_true(db.tableExists("sqlite_stat1")); + let stmt = db.createStatement("SELECT idx FROM sqlite_stat1 WHERE tbl = :table"); + stmt.params.table = aTableName; + try { + if (aRan) { + do_check_true(stmt.executeStep()); + do_check_neq(stmt.row.idx, null); + } + else { + do_check_false(stmt.executeStep()); + } + } + finally { + stmt.finalize(); + } +} + +// Tests + +function run_test() { + run_next_test(); +} + +add_task(function* init_tests() { + const TEST_URI = NetUtil.newURI("http://mozilla.org/"); + const TEST_TITLE = "This is a test"; + + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: TEST_TITLE, + url: TEST_URI + }); + yield PlacesTestUtils.addVisits(TEST_URI); + let thing = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput, + Ci.nsIAutoCompletePopup, + Ci.nsIAutoCompleteController]), + get popup() { return thing; }, + get controller() { return thing; }, + popupOpen: true, + selectedIndex: 0, + getValueAt: function() { return TEST_URI.spec; }, + searchString: TEST_TITLE, + }; + Services.obs.notifyObservers(thing, TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING, + null); +}); + +add_task(function* test_timed() { + clearAnalyzeData(); + + // Set a low interval and wait for the timed expiration to start. + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + setInterval(3); + yield promise; + setInterval(3600); + + do_check_analyze_ran("moz_places", false); + do_check_analyze_ran("moz_bookmarks", false); + do_check_analyze_ran("moz_historyvisits", false); + do_check_analyze_ran("moz_inputhistory", true); +}); + +add_task(function* test_debug() { + clearAnalyzeData(); + + yield promiseForceExpirationStep(1); + + do_check_analyze_ran("moz_places", true); + do_check_analyze_ran("moz_bookmarks", true); + do_check_analyze_ran("moz_historyvisits", true); + do_check_analyze_ran("moz_inputhistory", true); +}); + +add_task(function* test_clear_history() { + clearAnalyzeData(); + + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + let listener = Cc["@mozilla.org/places/expiration;1"] + .getService(Ci.nsINavHistoryObserver); + listener.onClearHistory(); + yield promise; + + do_check_analyze_ran("moz_places", true); + do_check_analyze_ran("moz_bookmarks", false); + do_check_analyze_ran("moz_historyvisits", true); + do_check_analyze_ran("moz_inputhistory", true); +}); diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_history.js b/toolkit/components/places/tests/expiration/test_annos_expire_history.js new file mode 100644 index 000000000..f9568a769 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_annos_expire_history.js @@ -0,0 +1,93 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * EXPIRE_WITH_HISTORY annotations should be expired when a page has no more + * visits, even if the page still exists in the database. + * This expiration policy is only valid for page annotations. + */ + +var as = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + +function run_test() { + run_next_test(); +} + +add_task(function* test_annos_expire_history() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire all expirable pages. + setMaxPages(0); + + // Add some visited page and a couple expire with history annotations for each. + let now = getExpirablePRTime(); + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_WITH_HISTORY); + as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_WITH_HISTORY); + } + + let pages = as.getPagesWithAnnotation("page_expire1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_expire2"); + do_check_eq(pages.length, 5); + + // Add some bookmarked page and a couple session annotations for each. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://item_anno." + i + ".mozilla.org/"); + // We also add a visit before bookmarking. + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pageURI, + title: null + }); + // Notice we use page annotations here, items annotations can't use this + // kind of expiration policy. + as.setPageAnnotation(pageURI, "item_persist1", "test", 0, as.EXPIRE_WITH_HISTORY); + as.setPageAnnotation(pageURI, "item_persist2", "test", 0, as.EXPIRE_WITH_HISTORY); + } + + let items = as.getPagesWithAnnotation("item_persist1"); + do_check_eq(items.length, 5); + items = as.getPagesWithAnnotation("item_persist2"); + do_check_eq(items.length, 5); + + // Add other visited page and a couple expire with history annotations for each. + // We won't expire these visits, so the annotations should survive. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_WITH_HISTORY); + as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_WITH_HISTORY); + } + + pages = as.getPagesWithAnnotation("page_persist1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_persist2"); + do_check_eq(pages.length, 5); + + // Expire all visits for the first 5 pages and the bookmarks. + yield promiseForceExpirationStep(10); + + pages = as.getPagesWithAnnotation("page_expire1"); + do_check_eq(pages.length, 0); + pages = as.getPagesWithAnnotation("page_expire2"); + do_check_eq(pages.length, 0); + items = as.getItemsWithAnnotation("item_persist1"); + do_check_eq(items.length, 0); + items = as.getItemsWithAnnotation("item_persist2"); + do_check_eq(items.length, 0); + pages = as.getPagesWithAnnotation("page_persist1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_persist2"); + do_check_eq(pages.length, 5); +}); diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_never.js b/toolkit/components/places/tests/expiration/test_annos_expire_never.js new file mode 100644 index 000000000..f146f25b5 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_annos_expire_never.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * EXPIRE_NEVER annotations should be expired when a page is removed from the + * database. + * If the annotation is a page annotation this will happen when the page is + * expired, namely when the page has no visits and is not bookmarked. + * Otherwise if it's an item annotation the annotation will be expired when + * the item is removed, thus expiration won't handle this case at all. + */ + +var as = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + +function run_test() { + run_next_test(); +} + +add_task(function* test_annos_expire_never() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire all expirable pages. + setMaxPages(0); + + // Add some visited page and a couple expire never annotations for each. + let now = getExpirablePRTime(); + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_NEVER); + as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_NEVER); + } + + let pages = as.getPagesWithAnnotation("page_expire1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_expire2"); + do_check_eq(pages.length, 5); + + // Add some bookmarked page and a couple expire never annotations for each. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://item_anno." + i + ".mozilla.org/"); + // We also add a visit before bookmarking. + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pageURI, + title: null + }); + let id = yield PlacesUtils.promiseItemId(bm.guid); + as.setItemAnnotation(id, "item_persist1", "test", 0, as.EXPIRE_NEVER); + as.setItemAnnotation(id, "item_persist2", "test", 0, as.EXPIRE_NEVER); + } + + let items = as.getItemsWithAnnotation("item_persist1"); + do_check_eq(items.length, 5); + items = as.getItemsWithAnnotation("item_persist2"); + do_check_eq(items.length, 5); + + // Add other visited page and a couple expire never annotations for each. + // We won't expire these visits, so the annotations should survive. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_NEVER); + as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_NEVER); + } + + pages = as.getPagesWithAnnotation("page_persist1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_persist2"); + do_check_eq(pages.length, 5); + + // Expire all visits for the first 5 pages and the bookmarks. + yield promiseForceExpirationStep(10); + + pages = as.getPagesWithAnnotation("page_expire1"); + do_check_eq(pages.length, 0); + pages = as.getPagesWithAnnotation("page_expire2"); + do_check_eq(pages.length, 0); + items = as.getItemsWithAnnotation("item_persist1"); + do_check_eq(items.length, 5); + items = as.getItemsWithAnnotation("item_persist2"); + do_check_eq(items.length, 5); + pages = as.getPagesWithAnnotation("page_persist1"); + do_check_eq(pages.length, 5); + pages = as.getPagesWithAnnotation("page_persist2"); + do_check_eq(pages.length, 5); +}); diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_policy.js b/toolkit/components/places/tests/expiration/test_annos_expire_policy.js new file mode 100644 index 000000000..2fe50e13e --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_annos_expire_policy.js @@ -0,0 +1,189 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Annotations can be set with a timed expiration policy. + * Supported policies are: + * - EXPIRE_DAYS: annotation would be expired after 7 days + * - EXPIRE_WEEKS: annotation would be expired after 30 days + * - EXPIRE_MONTHS: annotation would be expired after 180 days + */ + +var as = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + +/** + * Creates an aged annotation. + * + * @param aIdentifier Either a page url or an item id. + * @param aIdentifier Name of the annotation. + * @param aValue Value for the annotation. + * @param aExpirePolicy Expiration policy of the annotation. + * @param aAgeInDays Age in days of the annotation. + * @param [optional] aLastModifiedAgeInDays Age in days of the annotation, for lastModified. + */ +var now = Date.now(); +function add_old_anno(aIdentifier, aName, aValue, aExpirePolicy, + aAgeInDays, aLastModifiedAgeInDays) { + let expireDate = (now - (aAgeInDays * 86400 * 1000)) * 1000; + let lastModifiedDate = 0; + if (aLastModifiedAgeInDays) + lastModifiedDate = (now - (aLastModifiedAgeInDays * 86400 * 1000)) * 1000; + + let sql; + if (typeof(aIdentifier) == "number") { + // Item annotation. + as.setItemAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy); + // Update dateAdded for the last added annotation. + sql = "UPDATE moz_items_annos SET dateAdded = :expire_date, lastModified = :last_modified " + + "WHERE id = (SELECT id FROM moz_items_annos " + + "WHERE item_id = :id " + + "ORDER BY dateAdded DESC LIMIT 1)"; + } + else if (aIdentifier instanceof Ci.nsIURI) { + // Page annotation. + as.setPageAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy); + // Update dateAdded for the last added annotation. + sql = "UPDATE moz_annos SET dateAdded = :expire_date, lastModified = :last_modified " + + "WHERE id = (SELECT a.id FROM moz_annos a " + + "LEFT JOIN moz_places h on h.id = a.place_id " + + "WHERE h.url_hash = hash(:id) AND h.url = :id " + + "ORDER BY a.dateAdded DESC LIMIT 1)"; + } + else + do_throw("Wrong identifier type"); + + let stmt = DBConn().createStatement(sql); + stmt.params.id = (typeof(aIdentifier) == "number") ? aIdentifier + : aIdentifier.spec; + stmt.params.expire_date = expireDate; + stmt.params.last_modified = lastModifiedDate; + try { + stmt.executeStep(); + } + finally { + stmt.finalize(); + } +} + +function run_test() { + run_next_test(); +} + +add_task(function* test_annos_expire_policy() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire all expirable pages. + setMaxPages(0); + + let now_specific_to_test = getExpirablePRTime(); + // Add some bookmarked page and timed annotations for each. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://item_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now_specific_to_test++ }); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pageURI, + title: null + }); + let id = yield PlacesUtils.promiseItemId(bm.guid); + // Add a 6 days old anno. + add_old_anno(id, "persist_days", "test", as.EXPIRE_DAYS, 6); + // Add a 8 days old anno, modified 5 days ago. + add_old_anno(id, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6); + // Add a 8 days old anno. + add_old_anno(id, "expire_days", "test", as.EXPIRE_DAYS, 8); + + // Add a 29 days old anno. + add_old_anno(id, "persist_weeks", "test", as.EXPIRE_WEEKS, 29); + // Add a 31 days old anno, modified 29 days ago. + add_old_anno(id, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29); + // Add a 31 days old anno. + add_old_anno(id, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + + // Add a 179 days old anno. + add_old_anno(id, "persist_months", "test", as.EXPIRE_MONTHS, 179); + // Add a 181 days old anno, modified 179 days ago. + add_old_anno(id, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179); + // Add a 181 days old anno. + add_old_anno(id, "expire_months", "test", as.EXPIRE_MONTHS, 181); + + // Add a 6 days old anno. + add_old_anno(pageURI, "persist_days", "test", as.EXPIRE_DAYS, 6); + // Add a 8 days old anno, modified 5 days ago. + add_old_anno(pageURI, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6); + // Add a 8 days old anno. + add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8); + + // Add a 29 days old anno. + add_old_anno(pageURI, "persist_weeks", "test", as.EXPIRE_WEEKS, 29); + // Add a 31 days old anno, modified 29 days ago. + add_old_anno(pageURI, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29); + // Add a 31 days old anno. + add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + + // Add a 179 days old anno. + add_old_anno(pageURI, "persist_months", "test", as.EXPIRE_MONTHS, 179); + // Add a 181 days old anno, modified 179 days ago. + add_old_anno(pageURI, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179); + // Add a 181 days old anno. + add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181); + } + + // Add some visited page and timed annotations for each. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now_specific_to_test++ }); + // Add a 6 days old anno. + add_old_anno(pageURI, "persist_days", "test", as.EXPIRE_DAYS, 6); + // Add a 8 days old anno, modified 5 days ago. + add_old_anno(pageURI, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6); + // Add a 8 days old anno. + add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8); + + // Add a 29 days old anno. + add_old_anno(pageURI, "persist_weeks", "test", as.EXPIRE_WEEKS, 29); + // Add a 31 days old anno, modified 29 days ago. + add_old_anno(pageURI, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29); + // Add a 31 days old anno. + add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + + // Add a 179 days old anno. + add_old_anno(pageURI, "persist_months", "test", as.EXPIRE_MONTHS, 179); + // Add a 181 days old anno, modified 179 days ago. + add_old_anno(pageURI, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179); + // Add a 181 days old anno. + add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181); + } + + // Expire all visits for the bookmarks. + yield promiseForceExpirationStep(5); + + ["expire_days", "expire_weeks", "expire_months"].forEach(function(aAnno) { + let pages = as.getPagesWithAnnotation(aAnno); + do_check_eq(pages.length, 0); + }); + + ["expire_days", "expire_weeks", "expire_months"].forEach(function(aAnno) { + let items = as.getItemsWithAnnotation(aAnno); + do_check_eq(items.length, 0); + }); + + ["persist_days", "persist_lm_days", "persist_weeks", "persist_lm_weeks", + "persist_months", "persist_lm_months"].forEach(function(aAnno) { + let pages = as.getPagesWithAnnotation(aAnno); + do_check_eq(pages.length, 10); + }); + + ["persist_days", "persist_lm_days", "persist_weeks", "persist_lm_weeks", + "persist_months", "persist_lm_months"].forEach(function(aAnno) { + let items = as.getItemsWithAnnotation(aAnno); + do_check_eq(items.length, 5); + }); +}); diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_session.js b/toolkit/components/places/tests/expiration/test_annos_expire_session.js new file mode 100644 index 000000000..68c995f80 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_annos_expire_session.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Session annotations should be expired when browsing session ends. + */ + +var as = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + +function run_test() { + run_next_test(); +} + +add_task(function* test_annos_expire_session() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Add some visited page and a couple session annotations for each. + let now = Date.now() * 1000; + for (let i = 0; i < 10; i++) { + let pageURI = uri("http://session_page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ }); + as.setPageAnnotation(pageURI, "test1", "test", 0, as.EXPIRE_SESSION); + as.setPageAnnotation(pageURI, "test2", "test", 0, as.EXPIRE_SESSION); + } + + // Add some bookmarked page and a couple session annotations for each. + for (let i = 0; i < 10; i++) { + let pageURI = uri("http://session_item_anno." + i + ".mozilla.org/"); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pageURI, + title: null + }); + let id = yield PlacesUtils.promiseItemId(bm.guid); + as.setItemAnnotation(id, "test1", "test", 0, as.EXPIRE_SESSION); + as.setItemAnnotation(id, "test2", "test", 0, as.EXPIRE_SESSION); + } + + + let pages = as.getPagesWithAnnotation("test1"); + do_check_eq(pages.length, 10); + pages = as.getPagesWithAnnotation("test2"); + do_check_eq(pages.length, 10); + let items = as.getItemsWithAnnotation("test1"); + do_check_eq(items.length, 10); + items = as.getItemsWithAnnotation("test2"); + do_check_eq(items.length, 10); + + let deferred = Promise.defer(); + waitForConnectionClosed(function() { + let stmt = DBConn(true).createAsyncStatement( + `SELECT id FROM moz_annos + UNION ALL + SELECT id FROM moz_items_annos + WHERE expiration = :expiration` + ); + stmt.params.expiration = as.EXPIRE_SESSION; + stmt.executeAsync({ + handleResult: function(aResultSet) { + dump_table("moz_annos"); + dump_table("moz_items_annos"); + do_throw("Should not find any leftover session annotations"); + }, + handleError: function(aError) { + do_throw("Error code " + aError.result + " with message '" + + aError.message + "' returned."); + }, + handleCompletion: function(aReason) { + do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED); + deferred.resolve(); + } + }); + stmt.finalize(); + }); + yield deferred.promise; +}); diff --git a/toolkit/components/places/tests/expiration/test_clearHistory.js b/toolkit/components/places/tests/expiration/test_clearHistory.js new file mode 100644 index 000000000..d3879d7ad --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_clearHistory.js @@ -0,0 +1,157 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * History.clear() should expire everything but bookmarked pages and valid + * annos. + */ + +var hs = PlacesUtils.history; +var as = PlacesUtils.annotations; + +/** + * Creates an aged annotation. + * + * @param aIdentifier Either a page url or an item id. + * @param aIdentifier Name of the annotation. + * @param aValue Value for the annotation. + * @param aExpirePolicy Expiration policy of the annotation. + * @param aAgeInDays Age in days of the annotation. + * @param [optional] aLastModifiedAgeInDays Age in days of the annotation, for lastModified. + */ +var now = Date.now(); +function add_old_anno(aIdentifier, aName, aValue, aExpirePolicy, + aAgeInDays, aLastModifiedAgeInDays) { + let expireDate = (now - (aAgeInDays * 86400 * 1000)) * 1000; + let lastModifiedDate = 0; + if (aLastModifiedAgeInDays) + lastModifiedDate = (now - (aLastModifiedAgeInDays * 86400 * 1000)) * 1000; + + let sql; + if (typeof(aIdentifier) == "number") { + // Item annotation. + as.setItemAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy); + // Update dateAdded for the last added annotation. + sql = "UPDATE moz_items_annos SET dateAdded = :expire_date, lastModified = :last_modified " + + "WHERE id = ( " + + "SELECT a.id FROM moz_items_annos a " + + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " + + "WHERE a.item_id = :id " + + "AND n.name = :anno_name " + + "ORDER BY a.dateAdded DESC LIMIT 1 " + + ")"; + } + else if (aIdentifier instanceof Ci.nsIURI) { + // Page annotation. + as.setPageAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy); + // Update dateAdded for the last added annotation. + sql = "UPDATE moz_annos SET dateAdded = :expire_date, lastModified = :last_modified " + + "WHERE id = ( " + + "SELECT a.id FROM moz_annos a " + + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " + + "JOIN moz_places h on h.id = a.place_id " + + "WHERE h.url_hash = hash(:id) AND h.url = :id " + + "AND n.name = :anno_name " + + "ORDER BY a.dateAdded DESC LIMIT 1 " + + ")"; + } + else + do_throw("Wrong identifier type"); + + let stmt = DBConn().createStatement(sql); + stmt.params.id = (typeof(aIdentifier) == "number") ? aIdentifier + : aIdentifier.spec; + stmt.params.expire_date = expireDate; + stmt.params.last_modified = lastModifiedDate; + stmt.params.anno_name = aName; + try { + stmt.executeStep(); + } + finally { + stmt.finalize(); + } +} + +function run_test() { + run_next_test(); +} + +add_task(function* test_historyClear() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire all expirable pages. + setMaxPages(0); + + // Add some bookmarked page with visit and annotations. + for (let i = 0; i < 5; i++) { + let pageURI = uri("http://item_anno." + i + ".mozilla.org/"); + // This visit will be expired. + yield PlacesTestUtils.addVisits({ uri: pageURI }); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: pageURI, + title: null + }); + let id = yield PlacesUtils.promiseItemId(bm.guid); + // Will persist because it's an EXPIRE_NEVER item anno. + as.setItemAnnotation(id, "persist", "test", 0, as.EXPIRE_NEVER); + // Will persist because the page is bookmarked. + as.setPageAnnotation(pageURI, "persist", "test", 0, as.EXPIRE_NEVER); + // All EXPIRE_SESSION annotations are expected to expire on clear history. + as.setItemAnnotation(id, "expire_session", "test", 0, as.EXPIRE_SESSION); + as.setPageAnnotation(pageURI, "expire_session", "test", 0, as.EXPIRE_SESSION); + // Annotations with timed policy will expire regardless bookmarked status. + add_old_anno(id, "expire_days", "test", as.EXPIRE_DAYS, 8); + add_old_anno(id, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + add_old_anno(id, "expire_months", "test", as.EXPIRE_MONTHS, 181); + add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8); + add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181); + } + + // Add some visited page and annotations for each. + for (let i = 0; i < 5; i++) { + // All page annotations related to these expired pages are expected to + // expire as well. + let pageURI = uri("http://page_anno." + i + ".mozilla.org/"); + yield PlacesTestUtils.addVisits({ uri: pageURI }); + as.setPageAnnotation(pageURI, "expire", "test", 0, as.EXPIRE_NEVER); + as.setPageAnnotation(pageURI, "expire_session", "test", 0, as.EXPIRE_SESSION); + add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8); + add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31); + add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181); + } + + // Expire all visits for the bookmarks + yield PlacesUtils.history.clear(); + + ["expire_days", "expire_weeks", "expire_months", "expire_session", + "expire"].forEach(function(aAnno) { + let pages = as.getPagesWithAnnotation(aAnno); + do_check_eq(pages.length, 0); + }); + + ["expire_days", "expire_weeks", "expire_months", "expire_session", + "expire"].forEach(function(aAnno) { + let items = as.getItemsWithAnnotation(aAnno); + do_check_eq(items.length, 0); + }); + + let pages = as.getPagesWithAnnotation("persist"); + do_check_eq(pages.length, 5); + + let items = as.getItemsWithAnnotation("persist"); + do_check_eq(items.length, 5); + + for (let itemId of items) { + // Check item exists. + let guid = yield PlacesUtils.promiseItemGuid(itemId); + do_check_true((yield PlacesUtils.bookmarks.fetch({guid})), "item exists"); + } +}); diff --git a/toolkit/components/places/tests/expiration/test_debug_expiration.js b/toolkit/components/places/tests/expiration/test_debug_expiration.js new file mode 100644 index 000000000..456c03363 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_debug_expiration.js @@ -0,0 +1,225 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * What this is aimed to test: + * + * Expiration can be manually triggered through a debug topic, but that should + * only expire orphan entries, unless -1 is passed as limit. + */ + +var gNow = getExpirablePRTime(60); + +add_task(function* test_expire_orphans() +{ + // Add visits to 2 pages and force a orphan expiration. Visits should survive. + yield PlacesTestUtils.addVisits({ + uri: uri("http://page1.mozilla.org/"), + visitDate: gNow++ + }); + yield PlacesTestUtils.addVisits({ + uri: uri("http://page2.mozilla.org/"), + visitDate: gNow++ + }); + // Create a orphan place. + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://page3.mozilla.org/", + title: "" + }); + yield PlacesUtils.bookmarks.remove(bm); + + // Expire now. + yield promiseForceExpirationStep(0); + + // Check that visits survived. + do_check_eq(visits_in_database("http://page1.mozilla.org/"), 1); + do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1); + do_check_false(page_in_database("http://page3.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_expire_orphans_optionalarg() +{ + // Add visits to 2 pages and force a orphan expiration. Visits should survive. + yield PlacesTestUtils.addVisits({ + uri: uri("http://page1.mozilla.org/"), + visitDate: gNow++ + }); + yield PlacesTestUtils.addVisits({ + uri: uri("http://page2.mozilla.org/"), + visitDate: gNow++ + }); + // Create a orphan place. + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://page3.mozilla.org/", + title: "" + }); + yield PlacesUtils.bookmarks.remove(bm); + + // Expire now. + yield promiseForceExpirationStep(); + + // Check that visits survived. + do_check_eq(visits_in_database("http://page1.mozilla.org/"), 1); + do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1); + do_check_false(page_in_database("http://page3.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_expire_limited() +{ + yield PlacesTestUtils.addVisits([ + { // Should be expired cause it's the oldest visit + uri: "http://old.mozilla.org/", + visitDate: gNow++ + }, + { // Should not be expired cause we limit 1 + uri: "http://new.mozilla.org/", + visitDate: gNow++ + }, + ]); + + // Expire now. + yield promiseForceExpirationStep(1); + + // Check that newer visit survived. + do_check_eq(visits_in_database("http://new.mozilla.org/"), 1); + // Other visits should have been expired. + do_check_false(page_in_database("http://old.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_expire_limited_longurl() +{ + let longurl = "http://long.mozilla.org/" + "a".repeat(232); + yield PlacesTestUtils.addVisits([ + { // Should be expired cause it's the oldest visit + uri: "http://old.mozilla.org/", + visitDate: gNow++ + }, + { // Should be expired cause it's a long url older than 60 days. + uri: longurl, + visitDate: gNow++ + }, + { // Should not be expired cause younger than 60 days. + uri: longurl, + visitDate: getExpirablePRTime(58) + } + ]); + + yield promiseForceExpirationStep(1); + + // Check that some visits survived. + do_check_eq(visits_in_database(longurl), 1); + // Other visits should have been expired. + do_check_false(page_in_database("http://old.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_expire_limited_exoticurl() +{ + yield PlacesTestUtils.addVisits([ + { // Should be expired cause it's the oldest visit + uri: "http://old.mozilla.org/", + visitDate: gNow++ + }, + { // Should be expired cause it's a long url older than 60 days. + uri: "http://download.mozilla.org", + visitDate: gNow++, + transition: 7 + }, + { // Should not be expired cause younger than 60 days. + uri: "http://nonexpirable-download.mozilla.org", + visitDate: getExpirablePRTime(58), + transition: 7 + } + ]); + + yield promiseForceExpirationStep(1); + + // Check that some visits survived. + do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1); + // The visits are gone, the url is not yet, cause we limited the expiration + // to one entry, and we already removed http://old.mozilla.org/. + // The page normally would be expired by the next expiration run. + do_check_eq(visits_in_database("http://download.mozilla.org/"), 0); + // Other visits should have been expired. + do_check_false(page_in_database("http://old.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_expire_unlimited() +{ + let longurl = "http://long.mozilla.org/" + "a".repeat(232); + yield PlacesTestUtils.addVisits([ + { + uri: "http://old.mozilla.org/", + visitDate: gNow++ + }, + { + uri: "http://new.mozilla.org/", + visitDate: gNow++ + }, + // Add expirable visits. + { + uri: "http://download.mozilla.org/", + visitDate: gNow++, + transition: PlacesUtils.history.TRANSITION_DOWNLOAD + }, + { + uri: longurl, + visitDate: gNow++ + }, + + // Add non-expirable visits + { + uri: "http://nonexpirable.mozilla.org/", + visitDate: getExpirablePRTime(5) + }, + { + uri: "http://nonexpirable-download.mozilla.org/", + visitDate: getExpirablePRTime(5), + transition: PlacesUtils.history.TRANSITION_DOWNLOAD + }, + { + uri: longurl, + visitDate: getExpirablePRTime(5) + } + ]); + + yield promiseForceExpirationStep(-1); + + // Check that some visits survived. + do_check_eq(visits_in_database("http://nonexpirable.mozilla.org/"), 1); + do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1); + do_check_eq(visits_in_database(longurl), 1); + // Other visits should have been expired. + do_check_false(page_in_database("http://old.mozilla.org/")); + do_check_false(page_in_database("http://download.mozilla.org/")); + do_check_false(page_in_database("http://new.mozilla.org/")); + + // Clean up. + yield PlacesTestUtils.clearHistory(); +}); + +function run_test() +{ + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + // Set maxPages to a low value, so it's easy to go over it. + setMaxPages(1); + + run_next_test(); +} diff --git a/toolkit/components/places/tests/expiration/test_idle_daily.js b/toolkit/components/places/tests/expiration/test_idle_daily.js new file mode 100644 index 000000000..05e5a8125 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_idle_daily.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that expiration runs on idle-daily. + +function run_test() { + do_test_pending(); + + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) { + Services.obs.removeObserver(observeExpiration, + PlacesUtils.TOPIC_EXPIRATION_FINISHED); + do_test_finished(); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + + let expire = Cc["@mozilla.org/places/expiration;1"]. + getService(Ci.nsIObserver); + expire.observe(null, "idle-daily", null); +} diff --git a/toolkit/components/places/tests/expiration/test_notifications.js b/toolkit/components/places/tests/expiration/test_notifications.js new file mode 100644 index 000000000..06e585c6c --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_notifications.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Ensure that History (through category cache) notifies us just once. + */ + +var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +var gObserver = { + notifications: 0, + observe: function(aSubject, aTopic, aData) { + this.notifications++; + } +}; +os.addObserver(gObserver, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + +function run_test() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + PlacesTestUtils.clearHistory(); + + do_timeout(2000, check_result); + do_test_pending(); +} + +function check_result() { + os.removeObserver(gObserver, PlacesUtils.TOPIC_EXPIRATION_FINISHED); + do_check_eq(gObserver.notifications, 1); + do_test_finished(); +} diff --git a/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js new file mode 100644 index 000000000..f70cd2b58 --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js @@ -0,0 +1,114 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Expiring a full page should fire an onDeleteURI notification. + */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +var tests = [ + + { desc: "Add 1 bookmarked page.", + addPages: 1, + addBookmarks: 1, + expectedNotifications: 0, // No expirable pages. + }, + + { desc: "Add 2 pages, 1 bookmarked.", + addPages: 2, + addBookmarks: 1, + expectedNotifications: 1, // Only one expirable page. + }, + + { desc: "Add 10 pages, none bookmarked.", + addPages: 10, + addBookmarks: 0, + expectedNotifications: 10, // Will expire everything. + }, + + { desc: "Add 10 pages, all bookmarked.", + addPages: 10, + addBookmarks: 10, + expectedNotifications: 0, // No expirable pages. + }, + +]; + +function run_test() { + run_next_test(); +} + +add_task(function* test_notifications_onDeleteURI() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire anything that is expirable. + setMaxPages(0); + + for (let testIndex = 1; testIndex <= tests.length; testIndex++) { + let currentTest = tests[testIndex -1]; + print("\nTEST " + testIndex + ": " + currentTest.desc); + currentTest.receivedNotifications = 0; + + // Setup visits. + let now = getExpirablePRTime(); + for (let i = 0; i < currentTest.addPages; i++) { + let page = "http://" + testIndex + "." + i + ".mozilla.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ }); + } + + // Setup bookmarks. + currentTest.bookmarks = []; + for (let i = 0; i < currentTest.addBookmarks; i++) { + let page = "http://" + testIndex + "." + i + ".mozilla.org/"; + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: null, + url: page + }); + currentTest.bookmarks.push(page); + } + + // Observe history. + historyObserver = { + onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {}, + onEndUpdateBatch: function PEX_onEndUpdateBatch() {}, + onClearHistory: function() {}, + onVisit: function() {}, + onTitleChanged: function() {}, + onDeleteURI: function(aURI, aGUID, aReason) { + currentTest.receivedNotifications++; + // Check this uri was not bookmarked. + do_check_eq(currentTest.bookmarks.indexOf(aURI.spec), -1); + do_check_valid_places_guid(aGUID); + do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED); + }, + onPageChanged: function() {}, + onDeleteVisits: function(aURI, aTime) { }, + }; + hs.addObserver(historyObserver, false); + + // Expire now. + yield promiseForceExpirationStep(-1); + + hs.removeObserver(historyObserver, false); + + do_check_eq(currentTest.receivedNotifications, + currentTest.expectedNotifications); + + // Clean up. + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); + } + + clearMaxPages(); + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js new file mode 100644 index 000000000..e6b99ff8b --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js @@ -0,0 +1,142 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Expiring only visits for a page, but not the full page, should fire an + * onDeleteVisits notification. + */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +var tests = [ + + { desc: "Add 1 bookmarked page.", + addPages: 1, + visitsPerPage: 1, + addBookmarks: 1, + limitExpiration: -1, + expectedNotifications: 1, // Will expire visits for 1 page. + }, + + { desc: "Add 2 pages, 1 bookmarked.", + addPages: 2, + visitsPerPage: 1, + addBookmarks: 1, + limitExpiration: -1, + expectedNotifications: 1, // Will expire visits for 1 page. + }, + + { desc: "Add 10 pages, none bookmarked.", + addPages: 10, + visitsPerPage: 1, + addBookmarks: 0, + limitExpiration: -1, + expectedNotifications: 0, // Will expire only full pages. + }, + + { desc: "Add 10 pages, all bookmarked.", + addPages: 10, + visitsPerPage: 1, + addBookmarks: 10, + limitExpiration: -1, + expectedNotifications: 10, // Will expire visist for all pages. + }, + + { desc: "Add 10 pages with lot of visits, none bookmarked.", + addPages: 10, + visitsPerPage: 10, + addBookmarks: 0, + limitExpiration: 10, + expectedNotifications: 10, // Will expire 1 visist for each page, but won't + }, // expire pages since they still have visits. + +]; + +function run_test() { + run_next_test(); +} + +add_task(function* test_notifications_onDeleteVisits() { + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + // Expire anything that is expirable. + setMaxPages(0); + + for (let testIndex = 1; testIndex <= tests.length; testIndex++) { + let currentTest = tests[testIndex -1]; + print("\nTEST " + testIndex + ": " + currentTest.desc); + currentTest.receivedNotifications = 0; + + // Setup visits. + let timeInMicroseconds = getExpirablePRTime(8); + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; + } + + for (let j = 0; j < currentTest.visitsPerPage; j++) { + for (let i = 0; i < currentTest.addPages; i++) { + let page = "http://" + testIndex + "." + i + ".mozilla.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: newTimeInMicroseconds() }); + } + } + + // Setup bookmarks. + currentTest.bookmarks = []; + for (let i = 0; i < currentTest.addBookmarks; i++) { + let page = "http://" + testIndex + "." + i + ".mozilla.org/"; + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: null, + url: page + }); + currentTest.bookmarks.push(page); + } + + // Observe history. + historyObserver = { + onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {}, + onEndUpdateBatch: function PEX_onEndUpdateBatch() {}, + onClearHistory: function() {}, + onVisit: function() {}, + onTitleChanged: function() {}, + onDeleteURI: function(aURI, aGUID, aReason) { + // Check this uri was not bookmarked. + do_check_eq(currentTest.bookmarks.indexOf(aURI.spec), -1); + do_check_valid_places_guid(aGUID); + do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED); + }, + onPageChanged: function() {}, + onDeleteVisits: function(aURI, aTime, aGUID, aReason) { + currentTest.receivedNotifications++; + do_check_guid_for_uri(aURI, aGUID); + do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED); + }, + }; + hs.addObserver(historyObserver, false); + + // Expire now. + yield promiseForceExpirationStep(currentTest.limitExpiration); + + hs.removeObserver(historyObserver, false); + + do_check_eq(currentTest.receivedNotifications, + currentTest.expectedNotifications); + + // Clean up. + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); + } + + clearMaxPages(); + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/expiration/test_outdated_analyze.js b/toolkit/components/places/tests/expiration/test_outdated_analyze.js new file mode 100644 index 000000000..9cf61f06b --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_outdated_analyze.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that expiration executes ANALYZE when statistics are outdated. + +const TEST_URL = "http://www.mozilla.org/"; + +XPCOMUtils.defineLazyServiceGetter(this, "gHistory", + "@mozilla.org/browser/history;1", + "mozIAsyncHistory"); + +/** + * Object that represents a mozIVisitInfo object. + * + * @param [optional] aTransitionType + * The transition type of the visit. Defaults to TRANSITION_LINK if not + * provided. + * @param [optional] aVisitTime + * The time of the visit. Defaults to now if not provided. + */ +function VisitInfo(aTransitionType, aVisitTime) { + this.transitionType = + aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; + this.visitDate = aVisitTime || Date.now() * 1000; +} + +function run_test() { + do_test_pending(); + + // Init expiration before "importing". + force_expiration_start(); + + // Add a bunch of pages (at laast IMPORT_PAGES_THRESHOLD pages). + let places = []; + for (let i = 0; i < 100; i++) { + places.push({ + uri: NetUtil.newURI(TEST_URL + i), + title: "Title" + i, + visits: [new VisitInfo] + }); + } + gHistory.updatePlaces(places); + + // Set interval to a small value to expire on it. + setInterval(1); // 1s + + Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) { + Services.obs.removeObserver(observeExpiration, + PlacesUtils.TOPIC_EXPIRATION_FINISHED); + + // Check that statistica are up-to-date. + let stmt = DBConn().createAsyncStatement( + "SELECT (SELECT COUNT(*) FROM moz_places) - " + + "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 " + + "WHERE idx = 'moz_places_url_hashindex')" + ); + stmt.executeAsync({ + handleResult: function(aResultSet) { + let row = aResultSet.getNextRow(); + this._difference = row.getResultByIndex(0); + }, + handleError: function(aError) { + do_throw("Unexpected error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function(aReason) { + do_check_true(this._difference === 0); + do_test_finished(); + } + }); + stmt.finalize(); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); +} diff --git a/toolkit/components/places/tests/expiration/test_pref_interval.js b/toolkit/components/places/tests/expiration/test_pref_interval.js new file mode 100644 index 000000000..44c749d7a --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_pref_interval.js @@ -0,0 +1,61 @@ +/** + * What this is aimed to test: + * + * Expiration relies on an interval, that is user-preffable setting + * "places.history.expiration.interval_seconds". + * On pref change it will stop current interval timer and fire a new one, + * that will obey the new value. + * If the pref is set to a number <= 0 we will use the default value. + */ + +// Default timer value for expiration in seconds. Must have same value as +// PREF_INTERVAL_SECONDS_NOTSET in nsPlacesExpiration. +const DEFAULT_TIMER_DELAY_SECONDS = 3 * 60; + +// Sync this with the const value in the component. +const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3; + +var tests = [ + + // This test should be the first, so the interval won't be influenced by + // status of history. + { desc: "Set interval to 1s.", + interval: 1, + expectedTimerDelay: 1 + }, + + { desc: "Set interval to a negative value.", + interval: -1, + expectedTimerDelay: DEFAULT_TIMER_DELAY_SECONDS + }, + + { desc: "Set interval to 0.", + interval: 0, + expectedTimerDelay: DEFAULT_TIMER_DELAY_SECONDS + }, + + { desc: "Set interval to a large value.", + interval: 100, + expectedTimerDelay: 100 + }, + +]; + +add_task(function* test() { + // The pref should not exist by default. + Assert.throws(() => getInterval()); + + // Force the component, so it will start observing preferences. + force_expiration_start(); + + for (let currentTest of tests) { + print(currentTest.desc); + let promise = promiseTopicObserved("test-interval-changed"); + setInterval(currentTest.interval); + let [, data] = yield promise; + Assert.equal(data, currentTest.expectedTimerDelay * EXPIRE_AGGRESSIVITY_MULTIPLIER); + } + + clearInterval(); +}); + diff --git a/toolkit/components/places/tests/expiration/test_pref_maxpages.js b/toolkit/components/places/tests/expiration/test_pref_maxpages.js new file mode 100644 index 000000000..6a237afbb --- /dev/null +++ b/toolkit/components/places/tests/expiration/test_pref_maxpages.js @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * What this is aimed to test: + * + * Expiration will obey to hardware spec, but user can set a custom maximum + * number of pages to retain, to restrict history, through + * "places.history.expiration.max_pages". + * This limit is used at next expiration run. + * If the pref is set to a number < 0 we will use the default value. + */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +var tests = [ + + { desc: "Set max_pages to a negative value, with 1 page.", + maxPages: -1, + addPages: 1, + expectedNotifications: 0, // Will ignore and won't expire anything. + }, + + { desc: "Set max_pages to 0.", + maxPages: 0, + addPages: 1, + expectedNotifications: 1, + }, + + { desc: "Set max_pages to 0, with 2 pages.", + maxPages: 0, + addPages: 2, + expectedNotifications: 2, // Will expire everything. + }, + + // Notice if we are over limit we do a full step of expiration. So we ensure + // that we will expire if we are over the limit, but we don't ensure that we + // will expire exactly up to the limit. Thus in this case we expire + // everything. + { desc: "Set max_pages to 1 with 2 pages.", + maxPages: 1, + addPages: 2, + expectedNotifications: 2, // Will expire everything (in this case). + }, + + { desc: "Set max_pages to 10, with 9 pages.", + maxPages: 10, + addPages: 9, + expectedNotifications: 0, // We are at the limit, won't expire anything. + }, + + { desc: "Set max_pages to 10 with 10 pages.", + maxPages: 10, + addPages: 10, + expectedNotifications: 0, // We are below the limit, won't expire anything. + }, +]; + +function run_test() { + run_next_test(); +} + +add_task(function* test_pref_maxpages() { + // The pref should not exist by default. + try { + getMaxPages(); + do_throw("interval pref should not exist by default"); + } + catch (ex) {} + + // Set interval to a large value so we don't expire on it. + setInterval(3600); // 1h + + for (let testIndex = 1; testIndex <= tests.length; testIndex++) { + let currentTest = tests[testIndex -1]; + print("\nTEST " + testIndex + ": " + currentTest.desc); + currentTest.receivedNotifications = 0; + + // Setup visits. + let now = getExpirablePRTime(); + for (let i = 0; i < currentTest.addPages; i++) { + let page = "http://" + testIndex + "." + i + ".mozilla.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ }); + } + + // Observe history. + let historyObserver = { + onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {}, + onEndUpdateBatch: function PEX_onEndUpdateBatch() {}, + onClearHistory: function() {}, + onVisit: function() {}, + onTitleChanged: function() {}, + onDeleteURI: function(aURI) { + print("onDeleteURI " + aURI.spec); + currentTest.receivedNotifications++; + }, + onPageChanged: function() {}, + onDeleteVisits: function(aURI, aTime) { + print("onDeleteVisits " + aURI.spec + " " + aTime); + }, + }; + hs.addObserver(historyObserver, false); + + setMaxPages(currentTest.maxPages); + + // Expire now. + yield promiseForceExpirationStep(-1); + + hs.removeObserver(historyObserver, false); + + do_check_eq(currentTest.receivedNotifications, + currentTest.expectedNotifications); + + // Clean up. + yield PlacesTestUtils.clearHistory(); + } + + clearMaxPages(); + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/expiration/xpcshell.ini b/toolkit/components/places/tests/expiration/xpcshell.ini new file mode 100644 index 000000000..cda7ac052 --- /dev/null +++ b/toolkit/components/places/tests/expiration/xpcshell.ini @@ -0,0 +1,22 @@ +[DEFAULT] +head = head_expiration.js +tail = +skip-if = toolkit == 'android' + +[test_analyze_runs.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_annos_expire_history.js] +[test_annos_expire_never.js] +[test_annos_expire_policy.js] +[test_annos_expire_session.js] +[test_clearHistory.js] +[test_debug_expiration.js] +[test_idle_daily.js] +[test_notifications.js] +[test_notifications_onDeleteURI.js] +[test_notifications_onDeleteVisits.js] +[test_outdated_analyze.js] +[test_pref_interval.js] +[test_pref_maxpages.js] +skip-if = os == "linux" # bug 1284083 diff --git a/toolkit/components/places/tests/favicons/.eslintrc.js b/toolkit/components/places/tests/favicons/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/favicons/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png Binary files differnew file mode 100644 index 000000000..723008771 --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png Binary files differnew file mode 100644 index 000000000..9932c18fb --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png b/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png Binary files differnew file mode 100644 index 000000000..9f16bef43 --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png b/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png Binary files differnew file mode 100644 index 000000000..ed158d161 --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png diff --git a/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png Binary files differnew file mode 100644 index 000000000..585c9e897 --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png diff --git a/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png Binary files differnew file mode 100644 index 000000000..e07dabc79 --- /dev/null +++ b/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png diff --git a/toolkit/components/places/tests/favicons/favicon-big16.ico b/toolkit/components/places/tests/favicons/favicon-big16.ico Binary files differnew file mode 100644 index 000000000..d44438903 --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-big16.ico diff --git a/toolkit/components/places/tests/favicons/favicon-big32.jpg b/toolkit/components/places/tests/favicons/favicon-big32.jpg Binary files differnew file mode 100644 index 000000000..b2131bf0c --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-big32.jpg diff --git a/toolkit/components/places/tests/favicons/favicon-big4.jpg b/toolkit/components/places/tests/favicons/favicon-big4.jpg Binary files differnew file mode 100644 index 000000000..b84fcd35a --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-big4.jpg diff --git a/toolkit/components/places/tests/favicons/favicon-big48.ico b/toolkit/components/places/tests/favicons/favicon-big48.ico Binary files differnew file mode 100644 index 000000000..f22522411 --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-big48.ico diff --git a/toolkit/components/places/tests/favicons/favicon-big64.png b/toolkit/components/places/tests/favicons/favicon-big64.png Binary files differnew file mode 100644 index 000000000..2756cf0cb --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-big64.png diff --git a/toolkit/components/places/tests/favicons/favicon-normal16.png b/toolkit/components/places/tests/favicons/favicon-normal16.png Binary files differnew file mode 100644 index 000000000..62b69a3d0 --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-normal16.png diff --git a/toolkit/components/places/tests/favicons/favicon-normal32.png b/toolkit/components/places/tests/favicons/favicon-normal32.png Binary files differnew file mode 100644 index 000000000..5535363c9 --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-normal32.png diff --git a/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg b/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg Binary files differnew file mode 100644 index 000000000..422ee7ea0 --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg diff --git a/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg b/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg Binary files differnew file mode 100644 index 000000000..e8514966a --- /dev/null +++ b/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg diff --git a/toolkit/components/places/tests/favicons/head_favicons.js b/toolkit/components/places/tests/favicons/head_favicons.js new file mode 100644 index 000000000..cc81791e8 --- /dev/null +++ b/toolkit/components/places/tests/favicons/head_favicons.js @@ -0,0 +1,105 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + + +// This error icon must stay in sync with FAVICON_ERRORPAGE_URL in +// nsIFaviconService.idl, aboutCertError.xhtml and netError.xhtml. +const FAVICON_ERRORPAGE_URI = + NetUtil.newURI("chrome://global/skin/icons/warning-16.png"); + +/** + * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and + * verifies that it matches the expected page URI and associated favicon URI. + * + * This function also double-checks the GUID parameter of the notification. + * + * @param aExpectedPageURI + * nsIURI object of the page whose favicon should change. + * @param aExpectedFaviconURI + * nsIURI object of the newly associated favicon. + * @param aCallback + * This function is called after the check finished. + */ +function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, + aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) { + if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { + return; + } + PlacesUtils.history.removeObserver(this); + + do_check_true(aURI.equals(aExpectedPageURI)); + do_check_eq(aValue, aExpectedFaviconURI.spec); + do_check_guid_for_uri(aURI, aGUID); + aCallback(); + } + }; + PlacesUtils.history.addObserver(historyObserver, false); +} + +/** + * Checks that the favicon for the given page matches the provided data. + * + * @param aPageURI + * nsIURI object for the page to check. + * @param aExpectedMimeType + * Expected MIME type of the icon, for example "image/png". + * @param aExpectedData + * Expected icon data, expressed as an array of byte values. + * @param aCallback + * This function is called after the check finished. + */ +function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData, + aCallback) { + PlacesUtils.favicons.getFaviconDataForPage(aPageURI, + function (aURI, aDataLen, aData, aMimeType) { + do_check_eq(aExpectedMimeType, aMimeType); + do_check_true(compareArrays(aExpectedData, aData)); + do_check_guid_for_uri(aPageURI); + aCallback(); + }); +} + +/** + * Checks that the given page has no associated favicon. + * + * @param aPageURI + * nsIURI object for the page to check. + * @param aCallback + * This function is called after the check finished. + */ +function checkFaviconMissingForPage(aPageURI, aCallback) { + PlacesUtils.favicons.getFaviconURLForPage(aPageURI, + function (aURI, aDataLen, aData, aMimeType) { + do_check_true(aURI === null); + aCallback(); + }); +} + +function promiseFaviconMissingForPage(aPageURI) { + return new Promise(resolve => checkFaviconMissingForPage(aPageURI, resolve)); +} + +function promiseFaviconChanged(aExpectedPageURI, aExpectedFaviconURI) { + return new Promise(resolve => waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, resolve)); +} diff --git a/toolkit/components/places/tests/favicons/test_expireAllFavicons.js b/toolkit/components/places/tests/favicons/test_expireAllFavicons.js new file mode 100644 index 000000000..c5d8edfdd --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_expireAllFavicons.js @@ -0,0 +1,39 @@ +/** + * This file tests that favicons are correctly expired by expireAllFavicons. + */ + +"use strict"; + +const TEST_PAGE_URI = NetUtil.newURI("http://example.com/"); +const BOOKMARKED_PAGE_URI = NetUtil.newURI("http://example.com/bookmarked"); + +add_task(function* test_expireAllFavicons() { + // Add a visited page. + yield PlacesTestUtils.addVisits({ uri: TEST_PAGE_URI, transition: TRANSITION_TYPED }); + + // Set a favicon for our test page. + yield promiseSetIconForPage(TEST_PAGE_URI, SMALLPNG_DATA_URI); + + // Add a page with a bookmark. + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: BOOKMARKED_PAGE_URI, + title: "Test bookmark" + }); + + // Set a favicon for our bookmark. + yield promiseSetIconForPage(BOOKMARKED_PAGE_URI, SMALLPNG_DATA_URI); + + // Start expiration only after data has been saved in the database. + let promise = promiseTopicObserved(PlacesUtils.TOPIC_FAVICONS_EXPIRED); + PlacesUtils.favicons.expireAllFavicons(); + yield promise; + + // Check that the favicons for the pages we added were removed. + yield promiseFaviconMissingForPage(TEST_PAGE_URI); + yield promiseFaviconMissingForPage(BOOKMARKED_PAGE_URI); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/favicons/test_favicons_conversions.js b/toolkit/components/places/tests/favicons/test_favicons_conversions.js new file mode 100644 index 000000000..fa0d332ec --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_favicons_conversions.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the image conversions done by the favicon service. + */ + +// Globals + +// The pixel values we get on Windows are sometimes +/- 1 value compared to +// other platforms, so we need to skip some image content tests. +var isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); + +/** + * Checks the conversion of the given test image file. + * + * @param aFileName + * File that contains the favicon image, located in the test folder. + * @param aFileMimeType + * MIME type of the image contained in the file. + * @param aFileLength + * Expected length of the file. + * @param aExpectConversion + * If false, the icon should be stored as is. If true, the expected data + * is loaded from a file named "expected-" + aFileName + ".png". + * @param aVaryOnWindows + * Indicates that the content of the converted image can be different on + * Windows and should not be checked on that platform. + * @param aCallback + * This function is called after the check finished. + */ +function checkFaviconDataConversion(aFileName, aFileMimeType, aFileLength, + aExpectConversion, aVaryOnWindows, + aCallback) { + let pageURI = NetUtil.newURI("http://places.test/page/" + aFileName); + PlacesTestUtils.addVisits({ uri: pageURI, transition: TRANSITION_TYPED }).then( + function () { + let faviconURI = NetUtil.newURI("http://places.test/icon/" + aFileName); + let fileData = readFileOfLength(aFileName, aFileLength); + + PlacesUtils.favicons.replaceFaviconData(faviconURI, fileData, fileData.length, + aFileMimeType); + PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, faviconURI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function CFDC_verify(aURI, aDataLen, aData, aMimeType) { + if (!aExpectConversion) { + do_check_true(compareArrays(aData, fileData)); + do_check_eq(aMimeType, aFileMimeType); + } else { + if (!aVaryOnWindows || !isWindows) { + let expectedFile = do_get_file("expected-" + aFileName + ".png"); + do_check_true(compareArrays(aData, readFileData(expectedFile))); + } + do_check_eq(aMimeType, "image/png"); + } + + aCallback(); + }, Services.scriptSecurityManager.getSystemPrincipal()); + }); +} + +// Tests + +function run_test() { + run_next_test(); +} + +add_test(function test_storing_a_normal_16x16_icon() { + // 16x16 png, 286 bytes. + // optimized: no + checkFaviconDataConversion("favicon-normal16.png", "image/png", 286, + false, false, run_next_test); +}); + +add_test(function test_storing_a_normal_32x32_icon() { + // 32x32 png, 344 bytes. + // optimized: no + checkFaviconDataConversion("favicon-normal32.png", "image/png", 344, + false, false, run_next_test); +}); + +add_test(function test_storing_a_big_16x16_icon() { + // in: 16x16 ico, 1406 bytes. + // optimized: no + checkFaviconDataConversion("favicon-big16.ico", "image/x-icon", 1406, + false, false, run_next_test); +}); + +add_test(function test_storing_an_oversize_4x4_icon() { + // in: 4x4 jpg, 4751 bytes. + // optimized: yes + checkFaviconDataConversion("favicon-big4.jpg", "image/jpeg", 4751, + true, false, run_next_test); +}); + +add_test(function test_storing_an_oversize_32x32_icon() { + // in: 32x32 jpg, 3494 bytes. + // optimized: yes + checkFaviconDataConversion("favicon-big32.jpg", "image/jpeg", 3494, + true, true, run_next_test); +}); + +add_test(function test_storing_an_oversize_48x48_icon() { + // in: 48x48 ico, 56646 bytes. + // (howstuffworks.com icon, contains 13 icons with sizes from 16x16 to + // 48x48 in varying depths) + // optimized: yes + checkFaviconDataConversion("favicon-big48.ico", "image/x-icon", 56646, + true, false, run_next_test); +}); + +add_test(function test_storing_an_oversize_64x64_icon() { + // in: 64x64 png, 10698 bytes. + // optimized: yes + checkFaviconDataConversion("favicon-big64.png", "image/png", 10698, + true, false, run_next_test); +}); + +add_test(function test_scaling_an_oversize_160x3_icon() { + // in: 160x3 jpg, 5095 bytes. + // optimized: yes + checkFaviconDataConversion("favicon-scale160x3.jpg", "image/jpeg", 5095, + true, false, run_next_test); +}); + +add_test(function test_scaling_an_oversize_3x160_icon() { + // in: 3x160 jpg, 5059 bytes. + // optimized: yes + checkFaviconDataConversion("favicon-scale3x160.jpg", "image/jpeg", 5059, + true, false, run_next_test); +}); diff --git a/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js new file mode 100644 index 000000000..73eea7436 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests getFaviconDataForPage. + */ + +// Globals + +const FAVICON_URI = NetUtil.newURI(do_get_file("favicon-normal32.png")); +const FAVICON_DATA = readFileData(do_get_file("favicon-normal32.png")); +const FAVICON_MIMETYPE = "image/png"; + +// Tests + +function run_test() +{ + // Check that the favicon loaded correctly before starting the actual tests. + do_check_eq(FAVICON_DATA.length, 344); + run_next_test(); +} + +add_test(function test_normal() +{ + let pageURI = NetUtil.newURI("http://example.com/normal"); + + PlacesTestUtils.addVisits(pageURI).then(function () { + PlacesUtils.favicons.setAndFetchFaviconForPage( + pageURI, FAVICON_URI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function () { + PlacesUtils.favicons.getFaviconDataForPage(pageURI, + function (aURI, aDataLen, aData, aMimeType) { + do_check_true(aURI.equals(FAVICON_URI)); + do_check_eq(FAVICON_DATA.length, aDataLen); + do_check_true(compareArrays(FAVICON_DATA, aData)); + do_check_eq(FAVICON_MIMETYPE, aMimeType); + run_next_test(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + }); +}); + +add_test(function test_missing() +{ + let pageURI = NetUtil.newURI("http://example.com/missing"); + + PlacesUtils.favicons.getFaviconDataForPage(pageURI, + function (aURI, aDataLen, aData, aMimeType) { + // Check also the expected data types. + do_check_true(aURI === null); + do_check_true(aDataLen === 0); + do_check_true(aData.length === 0); + do_check_true(aMimeType === ""); + run_next_test(); + }); +}); diff --git a/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js new file mode 100644 index 000000000..fb2e23ff9 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests getFaviconURLForPage. + */ + +// Tests + +function run_test() +{ + run_next_test(); +} + +add_test(function test_normal() +{ + let pageURI = NetUtil.newURI("http://example.com/normal"); + + PlacesTestUtils.addVisits(pageURI).then(function () { + PlacesUtils.favicons.setAndFetchFaviconForPage( + pageURI, SMALLPNG_DATA_URI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function () { + PlacesUtils.favicons.getFaviconURLForPage(pageURI, + function (aURI, aDataLen, aData, aMimeType) { + do_check_true(aURI.equals(SMALLPNG_DATA_URI)); + + // Check also the expected data types. + do_check_true(aDataLen === 0); + do_check_true(aData.length === 0); + do_check_true(aMimeType === ""); + run_next_test(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + }); +}); + +add_test(function test_missing() +{ + let pageURI = NetUtil.newURI("http://example.com/missing"); + + PlacesUtils.favicons.getFaviconURLForPage(pageURI, + function (aURI, aDataLen, aData, aMimeType) { + // Check also the expected data types. + do_check_true(aURI === null); + do_check_true(aDataLen === 0); + do_check_true(aData.length === 0); + do_check_true(aMimeType === ""); + run_next_test(); + }); +}); diff --git a/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js b/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js new file mode 100644 index 000000000..d055d8d61 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js @@ -0,0 +1,90 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test ensures that the mime type is set for moz-anno channels of favicons + * properly. Added with work in bug 481227. + */ + +// Constants +Cu.import("resource://gre/modules/NetUtil.jsm"); + +const testFaviconData = "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%04gAMA%00%00%AF%C87%05%8A%E9%00%00%00%19tEXtSoftware%00Adobe%20ImageReadyq%C9e%3C%00%00%01%D6IDATx%DAb%FC%FF%FF%3F%03%25%00%20%80%98%909%EF%DF%BFg%EF%EC%EC%FC%AD%AC%AC%FC%DF%95%91%F1%BF%89%89%C9%7F%20%FF%D7%EA%D5%AB%B7%DF%BBwO%16%9B%01%00%01%C4%00r%01%08%9F9s%C6%CD%D8%D8%F8%BF%0B%03%C3%FF3%40%BC%0A%88%EF%02q%1A%10%BB%40%F1%AAU%ABv%C1%D4%C30%40%00%81%89%993g%3E%06%1A%F6%3F%14%AA%11D%97%03%F1%7Fc%08%0D%E2%2B))%FD%17%04%89%A1%19%00%10%40%0C%D00%F8%0F3%00%C8%F8%BF%1B%E4%0Ac%88a%E5%60%17%19%FF%0F%0D%0D%05%1B%02v%D9%DD%BB%0A0%03%00%02%08%AC%B9%A3%A3%E3%17%03%D4v%90%01%EF%18%106%C3%0Cz%07%C5%BB%A1%DE%82y%07%20%80%A0%A6%08B%FCn%0C1%60%26%D4%20d%C3VA%C3%06%26%BE%0A%EA-%80%00%82%B9%E0%F7L4%0D%EF%90%F8%C6%60%2F%0A%82%BD%01%13%07%0700%D0%01%02%88%11%E4%02P%B41%DC%BB%C7%D0%014%0D%E8l%06W%20%06%BA%88%A1%1C%1AS%15%40%7C%16%CA6.%2Fgx%BFg%0F%83%CB%D9%B3%0C%7B%80%7C%80%00%02%BB%00%E8%9F%ED%20%1B%3A%A0%A6%9F%81%DA%DC%01%C5%B0%80%ED%80%FA%BF%BC%BC%FC%3F%83%12%90%9D%96%F6%1F%20%80%18%DE%BD%7B%C7%0E%8E%05AD%20%FEGr%A6%A0%A0%E0%7F%25P%80%02%9D%0F%D28%13%18%23%C6%C0%B0%02E%3D%C8%F5%00%01%04%8F%05P%A8%BA%40my%87%E4%12c%A8%8D%20%8B%D0%D3%00%08%03%04%10%9C%01R%E4%82d%3B%C8%A0%99%C6%90%90%C6%A5%19%84%01%02%08%9E%17%80%C9x%F7%7B%A0%DBVC%F9%A0%C0%5C%7D%16%2C%CE%00%F4%C6O%5C%99%09%20%800L%04y%A5%03%1A%95%A0%80%05%05%14.%DBA%18%20%80%18)%CD%CE%00%01%06%00%0C'%94%C7%C0k%C9%2C%00%00%00%00IEND%AEB%60%82"; +const moz_anno_favicon_prefix = "moz-anno:favicon:"; + +// streamListener + +function streamListener(aExpectedContentType) +{ + this._expectedContentType = aExpectedContentType; +} +streamListener.prototype = +{ + onStartRequest: function(aRequest, aContext) + { + // We have other tests that make sure the data is what we expect. We just + // need to check the content type here. + let channel = aRequest.QueryInterface(Ci.nsIChannel); + dump("*** Checking " + channel.URI.spec + "\n"); + do_check_eq(channel.contentType, this._expectedContentType); + + // If we somehow throw before doing the above check, the test will pass, so + // we do this for extra sanity. + this._checked = true; + }, + onStopRequest: function() + { + do_check_true(this._checked); + do_test_finished(); + }, + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) + { + aRequest.cancel(Cr.NS_ERROR_ABORT); + } +}; + +// Test Runner + +function run_test() +{ + let fs = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + + // Test that the default icon has the content type of image/png. + let channel = NetUtil.newChannel({ + uri: fs.defaultFavicon, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON + }); + channel.asyncOpen2(new streamListener("image/png")); + do_test_pending(); + + // Test URI that we don't know anything about. Will end up being the default + // icon, so expect image/png. + channel = NetUtil.newChannel({ + uri: moz_anno_favicon_prefix + "http://mozilla.org", + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON + }); + channel.asyncOpen2(new streamListener("image/png")); + do_test_pending(); + + // Test that the content type of a favicon we add ends up being image/png. + let testURI = uri("http://mozilla.org/"); + // Add the data before opening + fs.replaceFaviconDataFromDataURL(testURI, testFaviconData, + (Date.now() + 60 * 60 * 24 * 1000) * 1000, + Services.scriptSecurityManager.getSystemPrincipal()); + + // Open the channel + channel = NetUtil.newChannel({ + uri: moz_anno_favicon_prefix + testURI.spec, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON + }); + channel.asyncOpen2(new streamListener("image/png")); + do_test_pending(); +} diff --git a/toolkit/components/places/tests/favicons/test_page-icon_protocol.js b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js new file mode 100644 index 000000000..5533d5135 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js @@ -0,0 +1,66 @@ +const ICON_DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; +const TEST_URI = NetUtil.newURI("http://mozilla.org/"); +const ICON_URI = NetUtil.newURI("http://mozilla.org/favicon.ico"); + +function fetchIconForSpec(spec) { + return new Promise((resolve, reject) => { + NetUtil.asyncFetch({ + uri: NetUtil.newURI("page-icon:" + TEST_URI.spec), + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON + }, (input, status, request) => { + if (!Components.isSuccessCode(status)) { + reject(new Error("unable to load icon")); + return; + } + + try { + let data = NetUtil.readInputStreamToString(input, input.available()); + let contentType = request.QueryInterface(Ci.nsIChannel).contentType; + input.close(); + resolve({ data, contentType }); + } catch (ex) { + reject(ex); + } + }); + }); +} + +var gDefaultFavicon; +var gFavicon; + +add_task(function* setup() { + yield PlacesTestUtils.addVisits({ uri: TEST_URI }); + + PlacesUtils.favicons.replaceFaviconDataFromDataURL( + ICON_URI, ICON_DATA, (Date.now() + 8640000) * 1000, + Services.scriptSecurityManager.getSystemPrincipal()); + + yield new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + TEST_URI, ICON_URI, false, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, Services.scriptSecurityManager.getSystemPrincipal()); + }); + + gDefaultFavicon = yield fetchIconForSpec(PlacesUtils.favicons.defaultFavicon); + gFavicon = yield fetchIconForSpec(ICON_DATA); +}); + +add_task(function* known_url() { + let {data, contentType} = yield fetchIconForSpec(TEST_URI.spec); + Assert.equal(contentType, gFavicon.contentType); + Assert.ok(data == gFavicon.data, "Got the favicon data"); +}); + +add_task(function* unknown_url() { + let {data, contentType} = yield fetchIconForSpec("http://www.moz.org/"); + Assert.equal(contentType, gDefaultFavicon.contentType); + Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data"); +}); + +add_task(function* invalid_url() { + let {data, contentType} = yield fetchIconForSpec("test"); + Assert.equal(contentType, gDefaultFavicon.contentType); + Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data"); +}); diff --git a/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js b/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js new file mode 100644 index 000000000..df61c22cd --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js @@ -0,0 +1,74 @@ +/** + * Test for bug 451499 <https://bugzilla.mozilla.org/show_bug.cgi?id=451499>: + * Wrong folder icon appears on smart bookmarks. + */ + +"use strict"; + +const PAGE_URI = NetUtil.newURI("http://example.com/test_query_result"); + +add_task(function* test_query_result_favicon_changed_on_child() { + // Bookmark our test page, so it will appear in the query resultset. + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "test_bookmark", + url: PAGE_URI + }); + + // Get the last 10 bookmarks added to the menu or the toolbar. + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.toolbarFolderId], 2); + + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.maxResults = 10; + options.excludeQueries = 1; + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + + let result = PlacesUtils.history.executeQuery(query, options); + let resultObserver = { + __proto__: NavHistoryResultObserver.prototype, + containerStateChanged(aContainerNode, aOldState, aNewState) { + if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) { + // We set a favicon on PAGE_URI while the container is open. The + // favicon for the page must have data associated with it in order for + // the icon changed notifications to be sent, so we use a valid image + // data URI. + PlacesUtils.favicons.setAndFetchFaviconForPage(PAGE_URI, + SMALLPNG_DATA_URI, + false, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal()); + } + }, + nodeIconChanged(aNode) { + do_throw("The icon should be set only for the page," + + " not for the containing query."); + } + }; + result.addObserver(resultObserver, false); + + // Open the container and wait for containerStateChanged. We should start + // observing before setting |containerOpen| as that's caused by the + // setAndFetchFaviconForPage() call caused by the containerStateChanged + // observer above. + let promise = promiseFaviconChanged(PAGE_URI, SMALLPNG_DATA_URI); + result.root.containerOpen = true; + yield promise; + + // We must wait for the asynchronous database thread to finish the + // operation, and then for the main thread to process any pending + // notifications that came from the asynchronous thread, before we can be + // sure that nodeIconChanged was not invoked in the meantime. + yield PlacesTestUtils.promiseAsyncUpdates(); + result.removeObserver(resultObserver); + + // Free the resources immediately. + result.root.containerOpen = false; +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js new file mode 100644 index 000000000..ac53e70e9 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests for mozIAsyncFavicons::replaceFaviconData() + */ + +var iconsvc = PlacesUtils.favicons; +var histsvc = PlacesUtils.history; +var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +var originalFavicon = { + file: do_get_file("favicon-normal16.png"), + uri: uri(do_get_file("favicon-normal16.png")), + data: readFileData(do_get_file("favicon-normal16.png")), + mimetype: "image/png" +}; + +var uniqueFaviconId = 0; +function createFavicon(fileName) { + let tempdir = Services.dirsvc.get("TmpD", Ci.nsILocalFile); + + // remove any existing file at the path we're about to copy to + let outfile = tempdir.clone(); + outfile.append(fileName); + try { outfile.remove(false); } catch (e) {} + + originalFavicon.file.copyToFollowingLinks(tempdir, fileName); + + let stream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0); + + // append some data that sniffers/encoders will ignore that will distinguish + // the different favicons we'll create + uniqueFaviconId++; + let uniqueStr = "uid:" + uniqueFaviconId; + stream.write(uniqueStr, uniqueStr.length); + stream.close(); + + do_check_eq(outfile.leafName.substr(0, fileName.length), fileName); + + return { + file: outfile, + uri: uri(outfile), + data: readFileData(outfile), + mimetype: "image/png" + }; +} + +function checkCallbackSucceeded(callbackMimetype, callbackData, sourceMimetype, sourceData) { + do_check_eq(callbackMimetype, sourceMimetype); + do_check_true(compareArrays(callbackData, sourceData)); +} + +function run_test() { + // check that the favicon loaded correctly + do_check_eq(originalFavicon.data.length, 286); + run_next_test(); +} + +add_task(function* test_replaceFaviconData_validHistoryURI() { + do_print("test replaceFaviconData for valid history uri"); + + let pageURI = uri("http://test1.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let favicon = createFavicon("favicon1.png"); + + iconsvc.replaceFaviconData(favicon.uri, favicon.data, favicon.data.length, + favicon.mimetype); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconData_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data); + checkFaviconDataForPage( + pageURI, favicon.mimetype, favicon.data, + function test_replaceFaviconData_validHistoryURI_callback() { + favicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, systemPrincipal); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconData_overrideDefaultFavicon() { + do_print("test replaceFaviconData to override a later setAndFetchFaviconForPage"); + + let pageURI = uri("http://test2.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon2.png"); + let secondFavicon = createFavicon("favicon3.png"); + + iconsvc.replaceFaviconData( + firstFavicon.uri, secondFavicon.data, secondFavicon.data.length, + secondFavicon.mimetype); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconData_overrideDefaultFavicon_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconData_overrideDefaultFavicon_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, systemPrincipal); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconData_replaceExisting() { + do_print("test replaceFaviconData to override a previous setAndFetchFaviconForPage"); + + let pageURI = uri("http://test3.bar"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon4.png"); + let secondFavicon = createFavicon("favicon5.png"); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconData_replaceExisting_firstSet_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, firstFavicon.mimetype, firstFavicon.data); + checkFaviconDataForPage( + pageURI, firstFavicon.mimetype, firstFavicon.data, + function test_replaceFaviconData_overrideDefaultFavicon_firstCallback() { + iconsvc.replaceFaviconData( + firstFavicon.uri, secondFavicon.data, secondFavicon.data.length, + secondFavicon.mimetype); + PlacesTestUtils.promiseAsyncUpdates().then(() => { + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconData_overrideDefaultFavicon_secondCallback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }, systemPrincipal); + }); + }); + }, systemPrincipal); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconData_unrelatedReplace() { + do_print("test replaceFaviconData to not make unrelated changes"); + + let pageURI = uri("http://test4.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let favicon = createFavicon("favicon6.png"); + let unrelatedFavicon = createFavicon("favicon7.png"); + + iconsvc.replaceFaviconData( + unrelatedFavicon.uri, unrelatedFavicon.data, unrelatedFavicon.data.length, + unrelatedFavicon.mimetype); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, favicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconData_unrelatedReplace_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data); + checkFaviconDataForPage( + pageURI, favicon.mimetype, favicon.data, + function test_replaceFaviconData_unrelatedReplace_callback() { + favicon.file.remove(false); + unrelatedFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, systemPrincipal); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconData_badInputs() { + do_print("test replaceFaviconData to throw on bad inputs"); + + let favicon = createFavicon("favicon8.png"); + + let ex = null; + try { + iconsvc.replaceFaviconData( + favicon.uri, favicon.data, favicon.data.length, ""); + } catch (e) { + ex = e; + } finally { + do_check_true(!!ex); + } + + ex = null; + try { + iconsvc.replaceFaviconData( + null, favicon.data, favicon.data.length, favicon.mimeType); + } catch (e) { + ex = e; + } finally { + do_check_true(!!ex); + } + + ex = null; + try { + iconsvc.replaceFaviconData( + favicon.uri, null, 0, favicon.mimeType); + } catch (e) { + ex = e; + } finally { + do_check_true(!!ex); + } + + favicon.file.remove(false); + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconData_twiceReplace() { + do_print("test replaceFaviconData on multiple replacements"); + + let pageURI = uri("http://test5.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon9.png"); + let secondFavicon = createFavicon("favicon10.png"); + + iconsvc.replaceFaviconData( + firstFavicon.uri, firstFavicon.data, firstFavicon.data.length, + firstFavicon.mimetype); + iconsvc.replaceFaviconData( + firstFavicon.uri, secondFavicon.data, secondFavicon.data.length, + secondFavicon.mimetype); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconData_twiceReplace_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconData_twiceReplace_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }, systemPrincipal); + }, systemPrincipal); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js new file mode 100644 index 000000000..69a5ba852 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests for mozIAsyncFavicons::replaceFaviconData() + */ + +var iconsvc = PlacesUtils.favicons; +var histsvc = PlacesUtils.history; + +var originalFavicon = { + file: do_get_file("favicon-normal16.png"), + uri: uri(do_get_file("favicon-normal16.png")), + data: readFileData(do_get_file("favicon-normal16.png")), + mimetype: "image/png" +}; + +var uniqueFaviconId = 0; +function createFavicon(fileName) { + let tempdir = Services.dirsvc.get("TmpD", Ci.nsILocalFile); + + // remove any existing file at the path we're about to copy to + let outfile = tempdir.clone(); + outfile.append(fileName); + try { outfile.remove(false); } catch (e) {} + + originalFavicon.file.copyToFollowingLinks(tempdir, fileName); + + let stream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0); + + // append some data that sniffers/encoders will ignore that will distinguish + // the different favicons we'll create + uniqueFaviconId++; + let uniqueStr = "uid:" + uniqueFaviconId; + stream.write(uniqueStr, uniqueStr.length); + stream.close(); + + do_check_eq(outfile.leafName.substr(0, fileName.length), fileName); + + return { + file: outfile, + uri: uri(outfile), + data: readFileData(outfile), + mimetype: "image/png" + }; +} + +function createDataURLForFavicon(favicon) { + return "data:" + favicon.mimetype + ";base64," + toBase64(favicon.data); +} + +function checkCallbackSucceeded(callbackMimetype, callbackData, sourceMimetype, sourceData) { + do_check_eq(callbackMimetype, sourceMimetype); + do_check_true(compareArrays(callbackData, sourceData)); +} + +function run_test() { + // check that the favicon loaded correctly + do_check_eq(originalFavicon.data.length, 286); + run_next_test(); +} + +add_task(function* test_replaceFaviconDataFromDataURL_validHistoryURI() { + do_print("test replaceFaviconDataFromDataURL for valid history uri"); + + let pageURI = uri("http://test1.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let favicon = createFavicon("favicon1.png"); + iconsvc.replaceFaviconDataFromDataURL(favicon.uri, createDataURLForFavicon(favicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data); + checkFaviconDataForPage( + pageURI, favicon.mimetype, favicon.data, + function test_replaceFaviconDataFromDataURL_validHistoryURI_callback() { + favicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() { + do_print("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage"); + + let pageURI = uri("http://test2.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon2.png"); + let secondFavicon = createFavicon("favicon3.png"); + + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_replaceExisting() { + do_print("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage"); + + let pageURI = uri("http://test3.bar"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon4.png"); + let secondFavicon = createFavicon("favicon5.png"); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_replaceExisting_firstSet_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, firstFavicon.mimetype, firstFavicon.data); + checkFaviconDataForPage( + pageURI, firstFavicon.mimetype, firstFavicon.data, + function test_replaceFaviconDataFromDataURL_replaceExisting_firstCallback() { + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconDataFromDataURL_replaceExisting_secondCallback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_unrelatedReplace() { + do_print("test replaceFaviconDataFromDataURL to not make unrelated changes"); + + let pageURI = uri("http://test4.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let favicon = createFavicon("favicon6.png"); + let unrelatedFavicon = createFavicon("favicon7.png"); + + iconsvc.replaceFaviconDataFromDataURL(unrelatedFavicon.uri, createDataURLForFavicon(unrelatedFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, favicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_unrelatedReplace_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data); + checkFaviconDataForPage( + pageURI, favicon.mimetype, favicon.data, + function test_replaceFaviconDataFromDataURL_unrelatedReplace_callback() { + favicon.file.remove(false); + unrelatedFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_badInputs() { + do_print("test replaceFaviconDataFromDataURL to throw on bad inputs"); + + let favicon = createFavicon("favicon8.png"); + + let ex = null; + try { + iconsvc.replaceFaviconDataFromDataURL(favicon.uri, "", 0, + Services.scriptSecurityManager.getSystemPrincipal()); + } catch (e) { + ex = e; + } finally { + do_check_true(!!ex); + } + + ex = null; + try { + iconsvc.replaceFaviconDataFromDataURL(null, createDataURLForFavicon(favicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + } catch (e) { + ex = e; + } finally { + do_check_true(!!ex); + } + + favicon.file.remove(false); + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_twiceReplace() { + do_print("test replaceFaviconDataFromDataURL on multiple replacements"); + + let pageURI = uri("http://test5.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon9.png"); + let secondFavicon = createFavicon("favicon10.png"); + + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_twiceReplace_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconDataFromDataURL_twiceReplace_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_afterRegularAssign() { + do_print("test replaceFaviconDataFromDataURL after replaceFaviconData"); + + let pageURI = uri("http://test6.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon11.png"); + let secondFavicon = createFavicon("favicon12.png"); + + iconsvc.replaceFaviconData( + firstFavicon.uri, firstFavicon.data, firstFavicon.data.length, + firstFavicon.mimetype); + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_afterRegularAssign_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconDataFromDataURL_afterRegularAssign_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_replaceFaviconDataFromDataURL_beforeRegularAssign() { + do_print("test replaceFaviconDataFromDataURL before replaceFaviconData"); + + let pageURI = uri("http://test7.bar/"); + yield PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = createFavicon("favicon13.png"); + let secondFavicon = createFavicon("favicon14.png"); + + iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon), 0, + Services.scriptSecurityManager.getSystemPrincipal()); + iconsvc.replaceFaviconData( + firstFavicon.uri, secondFavicon.data, secondFavicon.data.length, + secondFavicon.mimetype); + + let deferSetAndFetchFavicon = Promise.defer(); + iconsvc.setAndFetchFaviconForPage( + pageURI, firstFavicon.uri, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceFaviconDataFromDataURL_beforeRegularAssign_check(aURI, aDataLen, aData, aMimeType) { + checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data); + checkFaviconDataForPage( + pageURI, secondFavicon.mimetype, secondFavicon.data, + function test_replaceFaviconDataFromDataURL_beforeRegularAssign_callback() { + firstFavicon.file.remove(false); + secondFavicon.file.remove(false); + deferSetAndFetchFavicon.resolve(); + }); + }, Services.scriptSecurityManager.getSystemPrincipal()); + yield deferSetAndFetchFavicon.promise; + + yield PlacesTestUtils.clearHistory(); +}); + +/* toBase64 copied from image/test/unit/test_encoder_png.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + '0123456789+/'; +const base64Pad = '='; +function toBase64(data) { + let result = ''; + let length = data.length; + let i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; + result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; + result += toBase64Table[data[i+2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length%3) { + i = length - (length%3); + result += toBase64Table[data[i] >> 2]; + if ((length%3) == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; + result += toBase64Table[(data[i+1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +} diff --git a/toolkit/components/places/tests/favicons/xpcshell.ini b/toolkit/components/places/tests/favicons/xpcshell.ini new file mode 100644 index 000000000..851f193c7 --- /dev/null +++ b/toolkit/components/places/tests/favicons/xpcshell.ini @@ -0,0 +1,32 @@ +[DEFAULT] +head = head_favicons.js +tail = +skip-if = toolkit == 'android' +support-files = + expected-favicon-big32.jpg.png + expected-favicon-big4.jpg.png + expected-favicon-big48.ico.png + expected-favicon-big64.png.png + expected-favicon-scale160x3.jpg.png + expected-favicon-scale3x160.jpg.png + favicon-big16.ico + favicon-big32.jpg + favicon-big4.jpg + favicon-big48.ico + favicon-big64.png + favicon-normal16.png + favicon-normal32.png + favicon-scale160x3.jpg + favicon-scale3x160.jpg + +[test_expireAllFavicons.js] +[test_favicons_conversions.js] +# Bug 676989: test fails consistently on Android +fail-if = os == "android" +[test_getFaviconDataForPage.js] +[test_getFaviconURLForPage.js] +[test_moz-anno_favicon_mime_type.js] +[test_page-icon_protocol.js] +[test_query_result_favicon_changed_on_child.js] +[test_replaceFaviconData.js] +[test_replaceFaviconDataFromDataURL.js] diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js new file mode 100644 index 000000000..ddb6dcbd7 --- /dev/null +++ b/toolkit/components/places/tests/head_common.js @@ -0,0 +1,869 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const CURRENT_SCHEMA_VERSION = 35; +const FIRST_UPGRADABLE_SCHEMA_VERSION = 11; + +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; +const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; + +// Shortcuts to transitions type. +const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; +const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; +const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK; +const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED; +const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK; +const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT; +const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; +const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; +const TRANSITION_RELOAD = Ci.nsINavHistoryService.TRANSITION_RELOAD; + +const TITLE_LENGTH_MAX = 4096; + +Cu.importGlobalProperties(["URL"]); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", + "resource://gre/modules/BookmarkJSONUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", + "resource://gre/modules/BookmarkHTMLUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", + "resource://gre/modules/PlacesBackups.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions", + "resource://gre/modules/PlacesTransactions.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", + "resource://gre/modules/Sqlite.jsm"); + +// This imports various other objects in addition to PlacesUtils. +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() { + return NetUtil.newURI( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" + + "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="); +}); +XPCOMUtils.defineLazyGetter(this, "SMALLSVG_DATA_URI", function() { + return NetUtil.newURI( + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy5" + + "3My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBmaWxs" + + "PSIjNDI0ZTVhIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iN" + + "DQiIHN0cm9rZT0iIzQyNGU1YSIgc3Ryb2tlLXdpZHRoPSIxMSIgZmlsbD" + + "0ibm9uZSIvPg0KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjI0LjYiIHI9IjY" + + "uNCIvPg0KICA8cmVjdCB4PSI0NSIgeT0iMzkuOSIgd2lkdGg9IjEwLjEi" + + "IGhlaWdodD0iNDEuOCIvPg0KPC9zdmc%2BDQo%3D"); +}); + +var gTestDir = do_get_cwd(); + +// Initialize profile. +var gProfD = do_get_profile(true); + +// Remove any old database. +clearDB(); + +/** + * Shortcut to create a nsIURI. + * + * @param aSpec + * URLString of the uri. + */ +function uri(aSpec) { + return NetUtil.newURI(aSpec); +} + + +/** + * Gets the database connection. If the Places connection is invalid it will + * try to create a new connection. + * + * @param [optional] aForceNewConnection + * Forces creation of a new connection to the database. When a + * connection is asyncClosed it cannot anymore schedule async statements, + * though connectionReady will keep returning true (Bug 726990). + * + * @return The database connection or null if unable to get one. + */ +var gDBConn; +function DBConn(aForceNewConnection) { + if (!aForceNewConnection) { + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + if (db.connectionReady) + return db; + } + + // If the Places database connection has been closed, create a new connection. + if (!gDBConn || aForceNewConnection) { + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); + file.append("places.sqlite"); + let dbConn = gDBConn = Services.storage.openDatabase(file); + + // Be sure to cleanly close this connection. + promiseTopicObserved("profile-before-change").then(() => dbConn.asyncClose()); + } + + return gDBConn.connectionReady ? gDBConn : null; +} + +/** + * Reads data from the provided inputstream. + * + * @return an array of bytes. + */ +function readInputStreamData(aStream) { + let bistream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + try { + bistream.setInputStream(aStream); + let expectedData = []; + let avail; + while ((avail = bistream.available())) { + expectedData = expectedData.concat(bistream.readByteArray(avail)); + } + return expectedData; + } finally { + bistream.close(); + } +} + +/** + * Reads the data from the specified nsIFile. + * + * @param aFile + * The nsIFile to read from. + * @return an array of bytes. + */ +function readFileData(aFile) { + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + // init the stream as RD_ONLY, -1 == default permissions. + inputStream.init(aFile, 0x01, -1, null); + + // Check the returned size versus the expected size. + let size = inputStream.available(); + let bytes = readInputStreamData(inputStream); + if (size != bytes.length) { + throw "Didn't read expected number of bytes"; + } + return bytes; +} + +/** + * Reads the data from the named file, verifying the expected file length. + * + * @param aFileName + * This file should be located in the same folder as the test. + * @param aExpectedLength + * Expected length of the file. + * + * @return The array of bytes read from the file. + */ +function readFileOfLength(aFileName, aExpectedLength) { + let data = readFileData(do_get_file(aFileName)); + do_check_eq(data.length, aExpectedLength); + return data; +} + + +/** + * Returns the base64-encoded version of the given string. This function is + * similar to window.btoa, but is available to xpcshell tests also. + * + * @param aString + * Each character in this string corresponds to a byte, and must be a + * code point in the range 0-255. + * + * @return The base64-encoded string. + */ +function base64EncodeString(aString) { + var stream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + stream.setData(aString, aString.length); + var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"] + .createInstance(Ci.nsIScriptableBase64Encoder); + return encoder.encodeToString(stream, aString.length); +} + + +/** + * Compares two arrays, and returns true if they are equal. + * + * @param aArray1 + * First array to compare. + * @param aArray2 + * Second array to compare. + */ +function compareArrays(aArray1, aArray2) { + if (aArray1.length != aArray2.length) { + print("compareArrays: array lengths differ\n"); + return false; + } + + for (let i = 0; i < aArray1.length; i++) { + if (aArray1[i] != aArray2[i]) { + print("compareArrays: arrays differ at index " + i + ": " + + "(" + aArray1[i] + ") != (" + aArray2[i] +")\n"); + return false; + } + } + + return true; +} + + +/** + * Deletes a previously created sqlite file from the profile folder. + */ +function clearDB() { + try { + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); + file.append("places.sqlite"); + if (file.exists()) + file.remove(false); + } catch (ex) { dump("Exception: " + ex); } +} + + +/** + * Dumps the rows of a table out to the console. + * + * @param aName + * The name of the table or view to output. + */ +function dump_table(aName) +{ + let stmt = DBConn().createStatement("SELECT * FROM " + aName); + + print("\n*** Printing data from " + aName); + let count = 0; + while (stmt.executeStep()) { + let columns = stmt.numEntries; + + if (count == 0) { + // Print the column names. + for (let i = 0; i < columns; i++) + dump(stmt.getColumnName(i) + "\t"); + dump("\n"); + } + + // Print the rows. + for (let i = 0; i < columns; i++) { + switch (stmt.getTypeOfIndex(i)) { + case Ci.mozIStorageValueArray.VALUE_TYPE_NULL: + dump("NULL\t"); + break; + case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER: + dump(stmt.getInt64(i) + "\t"); + break; + case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT: + dump(stmt.getDouble(i) + "\t"); + break; + case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT: + dump(stmt.getString(i) + "\t"); + break; + } + } + dump("\n"); + + count++; + } + print("*** There were a total of " + count + " rows of data.\n"); + + stmt.finalize(); +} + + +/** + * Checks if an address is found in the database. + * @param aURI + * nsIURI or address to look for. + * @return place id of the page or 0 if not found + */ +function page_in_database(aURI) +{ + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let stmt = DBConn().createStatement( + "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url" + ); + stmt.params.url = url; + try { + if (!stmt.executeStep()) + return 0; + return stmt.getInt64(0); + } + finally { + stmt.finalize(); + } +} + +/** + * Checks how many visits exist for a specified page. + * @param aURI + * nsIURI or address to look for. + * @return number of visits found. + */ +function visits_in_database(aURI) +{ + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let stmt = DBConn().createStatement( + `SELECT count(*) FROM moz_historyvisits v + JOIN moz_places h ON h.id = v.place_id + WHERE url_hash = hash(:url) AND url = :url` + ); + stmt.params.url = url; + try { + if (!stmt.executeStep()) + return 0; + return stmt.getInt64(0); + } + finally { + stmt.finalize(); + } +} + +/** + * Checks that we don't have any bookmark + */ +function check_no_bookmarks() { + let query = PlacesUtils.history.getNewQuery(); + let folders = [ + PlacesUtils.bookmarks.toolbarFolder, + PlacesUtils.bookmarks.bookmarksMenuFolder, + PlacesUtils.bookmarks.unfiledBookmarksFolder, + ]; + query.setFolders(folders, 3); + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + if (root.childCount != 0) + do_throw("Unable to remove all bookmarks"); + root.containerOpen = false; +} + +/** + * Allows waiting for an observer notification once. + * + * @param aTopic + * Notification topic to observe. + * + * @return {Promise} + * @resolves The array [aSubject, aData] from the observed notification. + * @rejects Never. + */ +function promiseTopicObserved(aTopic) +{ + return new Promise(resolve => { + Services.obs.addObserver(function observe(aObsSubject, aObsTopic, aObsData) { + Services.obs.removeObserver(observe, aObsTopic); + resolve([aObsSubject, aObsData]); + }, aTopic, false); + }); +} + +/** + * Simulates a Places shutdown. + */ +var shutdownPlaces = function() { + do_print("shutdownPlaces: starting"); + let promise = new Promise(resolve => { + Services.obs.addObserver(resolve, "places-connection-closed", false); + }); + let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver); + hs.observe(null, "profile-change-teardown", null); + do_print("shutdownPlaces: sent profile-change-teardown"); + hs.observe(null, "test-simulate-places-shutdown", null); + do_print("shutdownPlaces: sent test-simulate-places-shutdown"); + return promise.then(() => { + do_print("shutdownPlaces: complete"); + }); +}; + +const FILENAME_BOOKMARKS_HTML = "bookmarks.html"; +const FILENAME_BOOKMARKS_JSON = "bookmarks-" + + (PlacesBackups.toISODateString(new Date())) + ".json"; + +/** + * Creates a bookmarks.html file in the profile folder from a given source file. + * + * @param aFilename + * Name of the file to copy to the profile folder. This file must + * exist in the directory that contains the test files. + * + * @return nsIFile object for the file. + */ +function create_bookmarks_html(aFilename) { + if (!aFilename) + do_throw("you must pass a filename to create_bookmarks_html function"); + remove_bookmarks_html(); + let bookmarksHTMLFile = gTestDir.clone(); + bookmarksHTMLFile.append(aFilename); + do_check_true(bookmarksHTMLFile.exists()); + bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML); + let profileBookmarksHTMLFile = gProfD.clone(); + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); + do_check_true(profileBookmarksHTMLFile.exists()); + return profileBookmarksHTMLFile; +} + + +/** + * Remove bookmarks.html file from the profile folder. + */ +function remove_bookmarks_html() { + let profileBookmarksHTMLFile = gProfD.clone(); + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); + if (profileBookmarksHTMLFile.exists()) { + profileBookmarksHTMLFile.remove(false); + do_check_false(profileBookmarksHTMLFile.exists()); + } +} + + +/** + * Check bookmarks.html file exists in the profile folder. + * + * @return nsIFile object for the file. + */ +function check_bookmarks_html() { + let profileBookmarksHTMLFile = gProfD.clone(); + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); + do_check_true(profileBookmarksHTMLFile.exists()); + return profileBookmarksHTMLFile; +} + + +/** + * Creates a JSON backup in the profile folder folder from a given source file. + * + * @param aFilename + * Name of the file to copy to the profile folder. This file must + * exist in the directory that contains the test files. + * + * @return nsIFile object for the file. + */ +function create_JSON_backup(aFilename) { + if (!aFilename) + do_throw("you must pass a filename to create_JSON_backup function"); + let bookmarksBackupDir = gProfD.clone(); + bookmarksBackupDir.append("bookmarkbackups"); + if (!bookmarksBackupDir.exists()) { + bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + do_check_true(bookmarksBackupDir.exists()); + } + let profileBookmarksJSONFile = bookmarksBackupDir.clone(); + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); + if (profileBookmarksJSONFile.exists()) { + profileBookmarksJSONFile.remove(); + } + let bookmarksJSONFile = gTestDir.clone(); + bookmarksJSONFile.append(aFilename); + do_check_true(bookmarksJSONFile.exists()); + bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON); + profileBookmarksJSONFile = bookmarksBackupDir.clone(); + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); + do_check_true(profileBookmarksJSONFile.exists()); + return profileBookmarksJSONFile; +} + + +/** + * Remove bookmarksbackup dir and all backups from the profile folder. + */ +function remove_all_JSON_backups() { + let bookmarksBackupDir = gProfD.clone(); + bookmarksBackupDir.append("bookmarkbackups"); + if (bookmarksBackupDir.exists()) { + bookmarksBackupDir.remove(true); + do_check_false(bookmarksBackupDir.exists()); + } +} + + +/** + * Check a JSON backup file for today exists in the profile folder. + * + * @param aIsAutomaticBackup The boolean indicates whether it's an automatic + * backup. + * @return nsIFile object for the file. + */ +function check_JSON_backup(aIsAutomaticBackup) { + let profileBookmarksJSONFile; + if (aIsAutomaticBackup) { + let bookmarksBackupDir = gProfD.clone(); + bookmarksBackupDir.append("bookmarkbackups"); + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.getNext().QueryInterface(Ci.nsIFile); + if (PlacesBackups.filenamesRegex.test(entry.leafName)) { + profileBookmarksJSONFile = entry; + break; + } + } + } else { + profileBookmarksJSONFile = gProfD.clone(); + profileBookmarksJSONFile.append("bookmarkbackups"); + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); + } + do_check_true(profileBookmarksJSONFile.exists()); + return profileBookmarksJSONFile; +} + +/** + * Returns the frecency of a url. + * + * @param aURI + * The URI or spec to get frecency for. + * @return the frecency value. + */ +function frecencyForUrl(aURI) +{ + let url = aURI; + if (aURI instanceof Ci.nsIURI) { + url = aURI.spec; + } else if (aURI instanceof URL) { + url = aURI.href; + } + let stmt = DBConn().createStatement( + "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1" + ); + stmt.bindByIndex(0, url); + try { + if (!stmt.executeStep()) { + throw new Error("No result for frecency."); + } + return stmt.getInt32(0); + } finally { + stmt.finalize(); + } +} + +/** + * Returns the hidden status of a url. + * + * @param aURI + * The URI or spec to get hidden for. + * @return @return true if the url is hidden, false otherwise. + */ +function isUrlHidden(aURI) +{ + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let stmt = DBConn().createStatement( + "SELECT hidden FROM moz_places WHERE url_hash = hash(?1) AND url = ?1" + ); + stmt.bindByIndex(0, url); + if (!stmt.executeStep()) + throw new Error("No result for hidden."); + let hidden = stmt.getInt32(0); + stmt.finalize(); + + return !!hidden; +} + +/** + * Compares two times in usecs, considering eventual platform timers skews. + * + * @param aTimeBefore + * The older time in usecs. + * @param aTimeAfter + * The newer time in usecs. + * @return true if times are ordered, false otherwise. + */ +function is_time_ordered(before, after) { + // Windows has an estimated 16ms timers precision, since Date.now() and + // PR_Now() use different code atm, the results can be unordered by this + // amount of time. See bug 558745 and bug 557406. + let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); + // Just to be safe we consider 20ms. + let skew = isWindows ? 20000000 : 0; + return after - before > -skew; +} + +/** + * Shutdowns Places, invoking the callback when the connection has been closed. + * + * @param aCallback + * Function to be called when done. + */ +function waitForConnectionClosed(aCallback) +{ + promiseTopicObserved("places-connection-closed").then(aCallback); + shutdownPlaces(); +} + +/** + * Tests if a given guid is valid for use in Places or not. + * + * @param aGuid + * The guid to test. + * @param [optional] aStack + * The stack frame used to report the error. + */ +function do_check_valid_places_guid(aGuid, + aStack) +{ + if (!aStack) { + aStack = Components.stack.caller; + } + do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack); +} + +/** + * Retrieves the guid for a given uri. + * + * @param aURI + * The uri to check. + * @param [optional] aStack + * The stack frame used to report the error. + * @return the associated the guid. + */ +function do_get_guid_for_uri(aURI, + aStack) +{ + if (!aStack) { + aStack = Components.stack.caller; + } + let stmt = DBConn().createStatement( + `SELECT guid + FROM moz_places + WHERE url_hash = hash(:url) AND url = :url` + ); + stmt.params.url = aURI.spec; + do_check_true(stmt.executeStep(), aStack); + let guid = stmt.row.guid; + stmt.finalize(); + do_check_valid_places_guid(guid, aStack); + return guid; +} + +/** + * Tests that a guid was set in moz_places for a given uri. + * + * @param aURI + * The uri to check. + * @param [optional] aGUID + * The expected guid in the database. + */ +function do_check_guid_for_uri(aURI, + aGUID) +{ + let caller = Components.stack.caller; + let guid = do_get_guid_for_uri(aURI, caller); + if (aGUID) { + do_check_valid_places_guid(aGUID, caller); + do_check_eq(guid, aGUID, caller); + } +} + +/** + * Retrieves the guid for a given bookmark. + * + * @param aId + * The bookmark id to check. + * @param [optional] aStack + * The stack frame used to report the error. + * @return the associated the guid. + */ +function do_get_guid_for_bookmark(aId, + aStack) +{ + if (!aStack) { + aStack = Components.stack.caller; + } + let stmt = DBConn().createStatement( + `SELECT guid + FROM moz_bookmarks + WHERE id = :item_id` + ); + stmt.params.item_id = aId; + do_check_true(stmt.executeStep(), aStack); + let guid = stmt.row.guid; + stmt.finalize(); + do_check_valid_places_guid(guid, aStack); + return guid; +} + +/** + * Tests that a guid was set in moz_places for a given bookmark. + * + * @param aId + * The bookmark id to check. + * @param [optional] aGUID + * The expected guid in the database. + */ +function do_check_guid_for_bookmark(aId, + aGUID) +{ + let caller = Components.stack.caller; + let guid = do_get_guid_for_bookmark(aId, caller); + if (aGUID) { + do_check_valid_places_guid(aGUID, caller); + do_check_eq(guid, aGUID, caller); + } +} + +/** + * Compares 2 arrays returning whether they contains the same elements. + * + * @param a1 + * First array to compare. + * @param a2 + * Second array to compare. + * @param [optional] sorted + * Whether the comparison should take in count position of the elements. + * @return true if the arrays contain the same elements, false otherwise. + */ +function do_compare_arrays(a1, a2, sorted) +{ + if (a1.length != a2.length) + return false; + + if (sorted) { + return a1.every((e, i) => e == a2[i]); + } + return a1.filter(e => !a2.includes(e)).length == 0 && + a2.filter(e => !a1.includes(e)).length == 0; +} + +/** + * Generic nsINavBookmarkObserver that doesn't implement anything, but provides + * dummy methods to prevent errors about an object not having a certain method. + */ +function NavBookmarkObserver() {} + +NavBookmarkObserver.prototype = { + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemAdded: function () {}, + onItemRemoved: function () {}, + onItemChanged: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + ]) +}; + +/** + * Generic nsINavHistoryObserver that doesn't implement anything, but provides + * dummy methods to prevent errors about an object not having a certain method. + */ +function NavHistoryObserver() {} + +NavHistoryObserver.prototype = { + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onVisit: function () {}, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + ]) +}; + +/** + * Generic nsINavHistoryResultObserver that doesn't implement anything, but + * provides dummy methods to prevent errors about an object not having a certain + * method. + */ +function NavHistoryResultObserver() {} + +NavHistoryResultObserver.prototype = { + batching: function () {}, + containerStateChanged: function () {}, + invalidateContainer: function () {}, + nodeAnnotationChanged: function () {}, + nodeDateAddedChanged: function () {}, + nodeHistoryDetailsChanged: function () {}, + nodeIconChanged: function () {}, + nodeInserted: function () {}, + nodeKeywordChanged: function () {}, + nodeLastModifiedChanged: function () {}, + nodeMoved: function () {}, + nodeRemoved: function () {}, + nodeTagsChanged: function () {}, + nodeTitleChanged: function () {}, + nodeURIChanged: function () {}, + sortingChanged: function () {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryResultObserver, + ]) +}; + +/** + * Asynchronously check a url is visited. + * + * @param aURI The URI. + * @return {Promise} + * @resolves When the check has been added successfully. + * @rejects JavaScript exception. + */ +function promiseIsURIVisited(aURI) { + let deferred = Promise.defer(); + + PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) { + deferred.resolve(aIsVisited); + }); + + return deferred.promise; +} + +/** + * Asynchronously set the favicon associated with a page. + * @param aPageURI + * The page's URI + * @param aIconURI + * The URI of the favicon to be set. + */ +function promiseSetIconForPage(aPageURI, aIconURI) { + let deferred = Promise.defer(); + PlacesUtils.favicons.setAndFetchFaviconForPage( + aPageURI, aIconURI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + () => { deferred.resolve(); }, + Services.scriptSecurityManager.getSystemPrincipal()); + return deferred.promise; +} + +function checkBookmarkObject(info) { + do_check_valid_places_guid(info.guid); + do_check_valid_places_guid(info.parentGuid); + Assert.ok(typeof info.index == "number", "index should be a number"); + Assert.ok(info.dateAdded.constructor.name == "Date", "dateAdded should be a Date"); + Assert.ok(info.lastModified.constructor.name == "Date", "lastModified should be a Date"); + Assert.ok(info.lastModified >= info.dateAdded, "lastModified should never be smaller than dateAdded"); + Assert.ok(typeof info.type == "number", "type should be a number"); +} + +/** + * Reads foreign_count value for a given url. + */ +function* foreign_count(url) { + if (url instanceof Ci.nsIURI) + url = url.spec; + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT foreign_count FROM moz_places + WHERE url_hash = hash(:url) AND url = :url + `, { url }); + return rows.length == 0 ? 0 : rows[0].getResultByName("foreign_count"); +} diff --git a/toolkit/components/places/tests/history/.eslintrc.js b/toolkit/components/places/tests/history/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/history/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/history/head_history.js b/toolkit/components/places/tests/history/head_history.js new file mode 100644 index 000000000..870802dc1 --- /dev/null +++ b/toolkit/components/places/tests/history/head_history.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} diff --git a/toolkit/components/places/tests/history/test_insert.js b/toolkit/components/places/tests/history/test_insert.js new file mode 100644 index 000000000..e2884af8c --- /dev/null +++ b/toolkit/components/places/tests/history/test_insert.js @@ -0,0 +1,257 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +// Tests for `History.insert` and `History.insertMany`, as implemented in History.jsm + +"use strict"; + +add_task(function* test_insert_error_cases() { + const TEST_URL = "http://mozilla.com"; + + Assert.throws( + () => PlacesUtils.history.insert(), + /TypeError: pageInfo must be an object/, + "passing a null into History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert(1), + /TypeError: pageInfo must be an object/, + "passing a non object into History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({}), + /TypeError: PageInfo object must have a url property/, + "passing an object without a url to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({url: 123}), + /TypeError: Invalid url or guid: 123/, + "passing an object with an invalid url to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({url: TEST_URL}), + /TypeError: PageInfo object must have an array of visits/, + "passing an object without a visits property to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({url: TEST_URL, visits: 1}), + /TypeError: PageInfo object must have an array of visits/, + "passing an object with a non-array visits property to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({url: TEST_URL, visits: []}), + /TypeError: PageInfo object must have an array of visits/, + "passing an object with an empty array as the visits property to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({ + url: TEST_URL, + visits: [ + { + transition: TRANSITION_LINK, + date: "a" + } + ]}), + /TypeError: Expected a Date, got a/, + "passing a visit object with an invalid date to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({ + url: TEST_URL, + visits: [ + { + transition: TRANSITION_LINK + }, + { + transition: TRANSITION_LINK, + date: "a" + } + ]}), + /TypeError: Expected a Date, got a/, + "passing a second visit object with an invalid date to History.insert should throw a TypeError" + ); + let futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 1000); + Assert.throws( + () => PlacesUtils.history.insert({ + url: TEST_URL, + visits: [ + { + transition: TRANSITION_LINK, + date: futureDate, + } + ]}), + `TypeError: date: ${futureDate} is not a valid date`, + "passing a visit object with a future date to History.insert should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insert({ + url: TEST_URL, + visits: [ + {transition: "a"} + ]}), + /TypeError: transition: a is not a valid transition type/, + "passing a visit object with an invalid transition to History.insert should throw a TypeError" + ); +}); + +add_task(function* test_history_insert() { + const TEST_URL = "http://mozilla.com/"; + + let inserter = Task.async(function*(name, filter, referrer, date, transition) { + do_print(name); + do_print(`filter: ${filter}, referrer: ${referrer}, date: ${date}, transition: ${transition}`); + + let uri = NetUtil.newURI(TEST_URL + Math.random()); + let title = "Visit " + Math.random(); + + let pageInfo = { + title, + visits: [ + {transition: transition, referrer: referrer, date: date, } + ] + }; + + pageInfo.url = yield filter(uri); + + let result = yield PlacesUtils.history.insert(pageInfo); + + Assert.ok(PlacesUtils.isValidGuid(result.guid), "guid for pageInfo object is valid"); + Assert.equal(uri.spec, result.url.href, "url is correct for pageInfo object"); + Assert.equal(title, result.title, "title is correct for pageInfo object"); + Assert.equal(TRANSITION_LINK, result.visits[0].transition, "transition is correct for pageInfo object"); + if (referrer) { + Assert.equal(referrer, result.visits[0].referrer.href, "url of referrer for visit is correct"); + } else { + Assert.equal(null, result.visits[0].referrer, "url of referrer for visit is correct"); + } + if (date) { + Assert.equal(Number(date), + Number(result.visits[0].date), + "date of visit is correct"); + } + + Assert.ok(yield PlacesTestUtils.isPageInDB(uri), "Page was added"); + Assert.ok(yield PlacesTestUtils.visitsInDB(uri), "Visit was added"); + }); + + try { + for (let referrer of [TEST_URL, null]) { + for (let date of [new Date(), null]) { + for (let transition of [TRANSITION_LINK, null]) { + yield inserter("Testing History.insert() with an nsIURI", x => x, referrer, date, transition); + yield inserter("Testing History.insert() with a string url", x => x.spec, referrer, date, transition); + yield inserter("Testing History.insert() with a URL object", x => new URL(x.spec), referrer, date, transition); + } + } + } + } finally { + yield PlacesTestUtils.clearHistory(); + } +}); + +add_task(function* test_insert_multiple_error_cases() { + let validPageInfo = { + url: "http://mozilla.com", + visits: [ + {transition: TRANSITION_LINK} + ] + }; + + Assert.throws( + () => PlacesUtils.history.insertMany(), + /TypeError: pageInfos must be an array/, + "passing a null into History.insertMany should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insertMany([]), + /TypeError: pageInfos may not be an empty array/, + "passing an empty array into History.insertMany should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.insertMany([validPageInfo, {}]), + /TypeError: PageInfo object must have a url property/, + "passing a second invalid PageInfo object to History.insertMany should throw a TypeError" + ); +}); + +add_task(function* test_history_insertMany() { + const BAD_URLS = ["about:config", "chrome://browser/content/browser.xul"]; + const GOOD_URLS = [1, 2, 3].map(x => { return `http://mozilla.com/${x}`; }); + + let makePageInfos = Task.async(function*(urls, filter = x => x) { + let pageInfos = []; + for (let url of urls) { + let uri = NetUtil.newURI(url); + + let pageInfo = { + title: `Visit to ${url}`, + visits: [ + {transition: TRANSITION_LINK} + ] + }; + + pageInfo.url = yield filter(uri); + pageInfos.push(pageInfo); + } + return pageInfos; + }); + + let inserter = Task.async(function*(name, filter, useCallbacks) { + do_print(name); + do_print(`filter: ${filter}`); + do_print(`useCallbacks: ${useCallbacks}`); + yield PlacesTestUtils.clearHistory(); + + let result; + let allUrls = GOOD_URLS.concat(BAD_URLS); + let pageInfos = yield makePageInfos(allUrls, filter); + + if (useCallbacks) { + let onResultUrls = []; + let onErrorUrls = []; + result = yield PlacesUtils.history.insertMany(pageInfos, pageInfo => { + let url = pageInfo.url.href; + Assert.ok(GOOD_URLS.includes(url), "onResult callback called for correct url"); + onResultUrls.push(url); + Assert.equal(`Visit to ${url}`, pageInfo.title, "onResult callback provides the correct title"); + Assert.ok(PlacesUtils.isValidGuid(pageInfo.guid), "onResult callback provides a valid guid"); + }, pageInfo => { + let url = pageInfo.url.href; + Assert.ok(BAD_URLS.includes(url), "onError callback called for correct uri"); + onErrorUrls.push(url); + Assert.equal(undefined, pageInfo.title, "onError callback provides the correct title"); + Assert.equal(undefined, pageInfo.guid, "onError callback provides the expected guid"); + }); + Assert.equal(GOOD_URLS.sort().toString(), onResultUrls.sort().toString(), "onResult callback was called for each good url"); + Assert.equal(BAD_URLS.sort().toString(), onErrorUrls.sort().toString(), "onError callback was called for each bad url"); + } else { + result = yield PlacesUtils.history.insertMany(pageInfos); + } + + Assert.equal(undefined, result, "insertMany returned undefined"); + + for (let url of allUrls) { + let expected = GOOD_URLS.includes(url); + Assert.equal(expected, yield PlacesTestUtils.isPageInDB(url), `isPageInDB for ${url} is ${expected}`); + Assert.equal(expected, yield PlacesTestUtils.visitsInDB(url), `visitsInDB for ${url} is ${expected}`); + } + }); + + try { + for (let useCallbacks of [false, true]) { + yield inserter("Testing History.insertMany() with an nsIURI", x => x, useCallbacks); + yield inserter("Testing History.insertMany() with a string url", x => x.spec, useCallbacks); + yield inserter("Testing History.insertMany() with a URL object", x => new URL(x.spec), useCallbacks); + } + // Test rejection when no items added + let pageInfos = yield makePageInfos(BAD_URLS); + PlacesUtils.history.insertMany(pageInfos).then(() => { + Assert.ok(false, "History.insertMany rejected promise with all bad URLs"); + }, error => { + Assert.equal("No items were added to history.", error.message, "History.insertMany rejected promise with all bad URLs"); + }); + } finally { + yield PlacesTestUtils.clearHistory(); + } +}); diff --git a/toolkit/components/places/tests/history/test_remove.js b/toolkit/components/places/tests/history/test_remove.js new file mode 100644 index 000000000..7423f6464 --- /dev/null +++ b/toolkit/components/places/tests/history/test_remove.js @@ -0,0 +1,360 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +// Tests for `History.remove`, as implemented in History.jsm + +"use strict"; + +Cu.importGlobalProperties(["URL"]); + + +// Test removing a single page +add_task(function* test_remove_single() { + yield PlacesTestUtils.clearHistory(); + yield PlacesUtils.bookmarks.eraseEverything(); + + + let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random()); + yield PlacesTestUtils.addVisits(WITNESS_URI); + Assert.ok(page_in_database(WITNESS_URI)); + + let remover = Task.async(function*(name, filter, options) { + do_print(name); + do_print(JSON.stringify(options)); + do_print("Setting up visit"); + + let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random()); + let title = "Visit " + Math.random(); + yield PlacesTestUtils.addVisits({uri: uri, title: title}); + Assert.ok(visits_in_database(uri), "History entry created"); + + let removeArg = yield filter(uri); + + if (options.addBookmark) { + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test bookmark"); + } + + let shouldRemove = !options.addBookmark; + let observer; + let promiseObserved = new Promise((resolve, reject) => { + observer = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function(aUri) { + reject(new Error("Unexpected call to onVisit " + aUri.spec)); + }, + onTitleChanged: function(aUri) { + reject(new Error("Unexpected call to onTitleChanged " + aUri.spec)); + }, + onClearHistory: function() { + reject("Unexpected call to onClearHistory"); + }, + onPageChanged: function(aUri) { + reject(new Error("Unexpected call to onPageChanged " + aUri.spec)); + }, + onFrecencyChanged: function(aURI) { + try { + Assert.ok(!shouldRemove, "Observing onFrecencyChanged"); + Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri"); + } finally { + resolve(); + } + }, + onManyFrecenciesChanged: function() { + try { + Assert.ok(!shouldRemove, "Observing onManyFrecenciesChanged"); + } finally { + resolve(); + } + }, + onDeleteURI: function(aURI) { + try { + Assert.ok(shouldRemove, "Observing onDeleteURI"); + Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri"); + } finally { + resolve(); + } + }, + onDeleteVisits: function(aURI) { + Assert.equal(aURI.spec, uri.spec, "Observing onDeleteVisits on the right uri"); + } + }; + }); + PlacesUtils.history.addObserver(observer, false); + + do_print("Performing removal"); + let removed = false; + if (options.useCallback) { + let onRowCalled = false; + let guid = do_get_guid_for_uri(uri); + removed = yield PlacesUtils.history.remove(removeArg, page => { + Assert.equal(onRowCalled, false, "Callback has not been called yet"); + onRowCalled = true; + Assert.equal(page.url.href, uri.spec, "Callback provides the correct url"); + Assert.equal(page.guid, guid, "Callback provides the correct guid"); + Assert.equal(page.title, title, "Callback provides the correct title"); + }); + Assert.ok(onRowCalled, "Callback has been called"); + } else { + removed = yield PlacesUtils.history.remove(removeArg); + } + + yield promiseObserved; + PlacesUtils.history.removeObserver(observer); + + Assert.equal(visits_in_database(uri), 0, "History entry has disappeared"); + Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits"); + Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here"); + if (shouldRemove) { + Assert.ok(removed, "Something was removed"); + Assert.equal(page_in_database(uri), 0, "Page has disappeared"); + } else { + Assert.ok(!removed, "The page was not removed, as there was a bookmark"); + Assert.notEqual(page_in_database(uri), 0, "The page is still present"); + } + }); + + try { + for (let useCallback of [false, true]) { + for (let addBookmark of [false, true]) { + let options = { useCallback: useCallback, addBookmark: addBookmark }; + yield remover("Testing History.remove() with a single URI", x => x, options); + yield remover("Testing History.remove() with a single string url", x => x.spec, options); + yield remover("Testing History.remove() with a single string guid", x => do_get_guid_for_uri(x), options); + yield remover("Testing History.remove() with a single URI in an array", x => [x], options); + yield remover("Testing History.remove() with a single string url in an array", x => [x.spec], options); + yield remover("Testing History.remove() with a single string guid in an array", x => [do_get_guid_for_uri(x)], options); + } + } + } finally { + yield PlacesTestUtils.clearHistory(); + } + return; +}); + +// Test removing a list of pages +add_task(function* test_remove_many() { + const SIZE = 10; + + yield PlacesTestUtils.clearHistory(); + yield PlacesUtils.bookmarks.eraseEverything(); + + do_print("Adding a witness page"); + let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random()); + yield PlacesTestUtils.addVisits(WITNESS_URI); + Assert.ok(page_in_database(WITNESS_URI), "Witness page added"); + + do_print("Generating samples"); + let pages = []; + for (let i = 0; i < SIZE; ++i) { + let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove?sample=" + i + "&salt=" + Math.random()); + let title = "Visit " + i + ", " + Math.random(); + let hasBookmark = i % 3 == 0; + let page = { + uri: uri, + title: title, + hasBookmark: hasBookmark, + // `true` once `onResult` has been called for this page + onResultCalled: false, + // `true` once `onDeleteVisits` has been called for this page + onDeleteVisitsCalled: false, + // `true` once `onFrecencyChangedCalled` has been called for this page + onFrecencyChangedCalled: false, + // `true` once `onDeleteURI` has been called for this page + onDeleteURICalled: false, + }; + do_print("Pushing: " + uri.spec); + pages.push(page); + + yield PlacesTestUtils.addVisits(page); + page.guid = do_get_guid_for_uri(uri); + if (hasBookmark) { + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test bookmark " + i); + } + Assert.ok(page_in_database(uri), "Page added"); + } + + do_print("Mixing key types and introducing dangling keys"); + let keys = []; + for (let i = 0; i < SIZE; ++i) { + if (i % 4 == 0) { + keys.push(pages[i].uri); + keys.push(NetUtil.newURI("http://example.org/dangling/nsIURI/" + i)); + } else if (i % 4 == 1) { + keys.push(new URL(pages[i].uri.spec)); + keys.push(new URL("http://example.org/dangling/URL/" + i)); + } else if (i % 4 == 2) { + keys.push(pages[i].uri.spec); + keys.push("http://example.org/dangling/stringuri/" + i); + } else { + keys.push(pages[i].guid); + keys.push(("guid_" + i + "_01234567890").substr(0, 12)); + } + } + + let observer = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function(aURI) { + Assert.ok(false, "Unexpected call to onVisit " + aURI.spec); + }, + onTitleChanged: function(aURI) { + Assert.ok(false, "Unexpected call to onTitleChanged " + aURI.spec); + }, + onClearHistory: function() { + Assert.ok(false, "Unexpected call to onClearHistory"); + }, + onPageChanged: function(aURI) { + Assert.ok(false, "Unexpected call to onPageChanged " + aURI.spec); + }, + onFrecencyChanged: function(aURI) { + let origin = pages.find(x => x.uri.spec == aURI.spec); + Assert.ok(origin); + Assert.ok(origin.hasBookmark, "Observing onFrecencyChanged on a page with a bookmark"); + origin.onFrecencyChangedCalled = true; + // We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as + }, + onManyFrecenciesChanged: function() { + Assert.ok(false, "Observing onManyFrecenciesChanges, this is most likely correct but not covered by this test"); + }, + onDeleteURI: function(aURI) { + let origin = pages.find(x => x.uri.spec == aURI.spec); + Assert.ok(origin); + Assert.ok(!origin.hasBookmark, "Observing onDeleteURI on a page without a bookmark"); + Assert.ok(!origin.onDeleteURICalled, "Observing onDeleteURI for the first time"); + origin.onDeleteURICalled = true; + }, + onDeleteVisits: function(aURI) { + let origin = pages.find(x => x.uri.spec == aURI.spec); + Assert.ok(origin); + Assert.ok(!origin.onDeleteVisitsCalled, "Observing onDeleteVisits for the first time"); + origin.onDeleteVisitsCalled = true; + } + }; + PlacesUtils.history.addObserver(observer, false); + + do_print("Removing the pages and checking the callbacks"); + let removed = yield PlacesUtils.history.remove(keys, page => { + let origin = pages.find(candidate => candidate.uri.spec == page.url.href); + + Assert.ok(origin, "onResult has a valid page"); + Assert.ok(!origin.onResultCalled, "onResult has not seen this page yet"); + origin.onResultCalled = true; + Assert.equal(page.guid, origin.guid, "onResult has the right guid"); + Assert.equal(page.title, origin.title, "onResult has the right title"); + }); + Assert.ok(removed, "Something was removed"); + + PlacesUtils.history.removeObserver(observer); + + do_print("Checking out results"); + // By now the observers should have been called. + for (let i = 0; i < pages.length; ++i) { + let page = pages[i]; + do_print("Page: " + i); + Assert.ok(page.onResultCalled, "We have reached the page from the callback"); + Assert.ok(visits_in_database(page.uri) == 0, "History entry has disappeared"); + Assert.equal(page_in_database(page.uri) != 0, page.hasBookmark, "Page is present only if it also has bookmarks"); + Assert.equal(page.onFrecencyChangedCalled, page.onDeleteVisitsCalled, "onDeleteVisits was called iff onFrecencyChanged was called"); + Assert.ok(page.onFrecencyChangedCalled ^ page.onDeleteURICalled, "Either onFrecencyChanged or onDeleteURI was called"); + } + + Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits"); + Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here"); +}); + +add_task(function* cleanup() { + yield PlacesTestUtils.clearHistory(); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +// Test the various error cases +add_task(function* test_error_cases() { + Assert.throws( + () => PlacesUtils.history.remove(), + /TypeError: Invalid url/, + "History.remove with no argument should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove(null), + /TypeError: Invalid url/, + "History.remove with `null` should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove(undefined), + /TypeError: Invalid url/, + "History.remove with `undefined` should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove("not a guid, obviously"), + /TypeError: .* is not a valid URL/, + "History.remove with an ill-formed guid/url argument should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove({"not the kind of object we know how to handle": true}), + /TypeError: Invalid url/, + "History.remove with an unexpected object should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove([]), + /TypeError: Expected at least one page/, + "History.remove with an empty array should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove([null]), + /TypeError: Invalid url or guid/, + "History.remove with an array containing null should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove(["http://example.org", "not a guid, obviously"]), + /TypeError: .* is not a valid URL/, + "History.remove with an array containing an ill-formed guid/url argument should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove(["0123456789ab"/* valid guid*/, null]), + /TypeError: Invalid url or guid: null/, + "History.remove with an array containing a guid and a second argument that is null should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove(["http://example.org", {"not the kind of object we know how to handle": true}]), + /TypeError: Invalid url/, + "History.remove with an array containing an unexpected objecgt should throw a TypeError" + ); + Assert.throws( + () => PlacesUtils.history.remove("http://example.org", "not a function, obviously"), + /TypeError: Invalid function/, + "History.remove with a second argument that is not a function argument should throw a TypeError" + ); + try { + PlacesUtils.history.remove("http://example.org/I/have/clearly/not/been/added", null); + Assert.ok(true, "History.remove should ignore `null` as a second argument"); + } catch (ex) { + Assert.ok(false, "History.remove should ignore `null` as a second argument"); + } +}); + +add_task(function* test_orphans() { + let uri = NetUtil.newURI("http://moz.org/"); + yield PlacesTestUtils.addVisits({ uri }); + + PlacesUtils.favicons.setAndFetchFaviconForPage( + uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, Services.scriptSecurityManager.getSystemPrincipal()); + PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + yield PlacesUtils.history.remove(uri); + Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed"); + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) + + (SELECT count(*) FROM moz_favicons) AS count`); + Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans"); +}); diff --git a/toolkit/components/places/tests/history/test_removeVisits.js b/toolkit/components/places/tests/history/test_removeVisits.js new file mode 100644 index 000000000..8df0c81a9 --- /dev/null +++ b/toolkit/components/places/tests/history/test_removeVisits.js @@ -0,0 +1,316 @@ +const JS_NOW = Date.now(); +const DB_NOW = JS_NOW * 1000; +const TEST_URI = uri("http://example.com/"); +const PLACE_URI = uri("place:queryType=0&sort=8&maxResults=10"); + +function* cleanup() { + yield PlacesTestUtils.clearHistory(); + yield PlacesUtils.bookmarks.eraseEverything(); + // This is needed to remove place: entries. + DBConn().executeSimpleSQL("DELETE FROM moz_places"); +} + +add_task(function* remove_visits_outside_unbookmarked_uri() { + do_print("*** TEST: Remove some visits outside valid timeframe from an unbookmarked URI"); + + do_print("Add 10 visits for the URI from way in the past."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + + do_print("Remove visits using timerange outside the URI's visits."); + let filter = { + beginDate: new Date(JS_NOW - 10), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that all visits still exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 10); + for (let i = 0; i < root.childCount; i++) { + let visitTime = root.getChild(i).time; + do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000)); + } + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return true."); + do_check_true(yield promiseIsURIVisited(TEST_URI)); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Frecency should be positive.") + do_check_true(frecencyForUrl(TEST_URI) > 0); + + yield cleanup(); +}); + +add_task(function* remove_visits_outside_bookmarked_uri() { + do_print("*** TEST: Remove some visits outside valid timeframe from a bookmarked URI"); + + do_print("Add 10 visits for the URI from way in the past."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + do_print("Bookmark the URI."); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Remove visits using timerange outside the URI's visits."); + let filter = { + beginDate: new Date(JS_NOW - 10), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that all visits still exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 10); + for (let i = 0; i < root.childCount; i++) { + let visitTime = root.getChild(i).time; + do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000)); + } + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return true."); + do_check_true(yield promiseIsURIVisited(TEST_URI)); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Frecency should be positive.") + do_check_true(frecencyForUrl(TEST_URI) > 0); + + yield cleanup(); +}); + +add_task(function* remove_visits_unbookmarked_uri() { + do_print("*** TEST: Remove some visits from an unbookmarked URI"); + + do_print("Add 10 visits for the URI from now to 9 usecs in the past."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + + do_print("Remove the 5 most recent visits."); + let filter = { + beginDate: new Date(JS_NOW - 4), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that only the older 5 visits still exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 5); + for (let i = 0; i < root.childCount; i++) { + let visitTime = root.getChild(i).time; + do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000); + } + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return true."); + do_check_true(yield promiseIsURIVisited(TEST_URI)); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Frecency should be positive.") + do_check_true(frecencyForUrl(TEST_URI) > 0); + + yield cleanup(); +}); + +add_task(function* remove_visits_bookmarked_uri() { + do_print("*** TEST: Remove some visits from a bookmarked URI"); + + do_print("Add 10 visits for the URI from now to 9 usecs in the past."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + do_print("Bookmark the URI."); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Remove the 5 most recent visits."); + let filter = { + beginDate: new Date(JS_NOW - 4), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that only the older 5 visits still exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 5); + for (let i = 0; i < root.childCount; i++) { + let visitTime = root.getChild(i).time; + do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000); + } + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return true."); + do_check_true(yield promiseIsURIVisited(TEST_URI)); + yield PlacesTestUtils.promiseAsyncUpdates() + + do_print("Frecency should be positive.") + do_check_true(frecencyForUrl(TEST_URI) > 0); + + yield cleanup(); +}); + +add_task(function* remove_all_visits_unbookmarked_uri() { + do_print("*** TEST: Remove all visits from an unbookmarked URI"); + + do_print("Add some visits for the URI."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + + do_print("Remove all visits."); + let filter = { + beginDate: new Date(JS_NOW - 10), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should no longer exist in moz_places."); + do_check_false(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that no visits exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 0); + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return false."); + do_check_false(yield promiseIsURIVisited(TEST_URI)); + + yield cleanup(); +}); + +add_task(function* remove_all_visits_bookmarked_uri() { + do_print("*** TEST: Remove all visits from a bookmarked URI"); + + do_print("Add some visits for the URI."); + let visits = []; + for (let i = 0; i < 10; i++) { + visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) }); + } + yield PlacesTestUtils.addVisits(visits); + do_print("Bookmark the URI."); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + yield PlacesTestUtils.promiseAsyncUpdates(); + let initialFrecency = frecencyForUrl(TEST_URI); + + do_print("Remove all visits."); + let filter = { + beginDate: new Date(JS_NOW - 10), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + + do_print("Run a history query and check that no visits exist."); + let query = PlacesUtils.history.getNewQuery(); + let opts = PlacesUtils.history.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_VISIT; + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + let root = PlacesUtils.history.executeQuery(query, opts).root; + root.containerOpen = true; + do_check_eq(root.childCount, 0); + root.containerOpen = false; + + do_print("asyncHistory.isURIVisited should return false."); + do_check_false(yield promiseIsURIVisited(TEST_URI)); + + do_print("nsINavBookmarksService.isBookmarked should return true."); + do_check_true(PlacesUtils.bookmarks.isBookmarked(TEST_URI)); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Frecency should be smaller.") + do_check_true(frecencyForUrl(TEST_URI) < initialFrecency); + + yield cleanup(); +}); + +add_task(function* remove_all_visits_bookmarked_uri() { + do_print("*** TEST: Remove some visits from a zero frecency URI retains zero frecency"); + + do_print("Add some visits for the URI."); + yield PlacesTestUtils.addVisits([ + { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: (DB_NOW - 86400000000000) }, + { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: DB_NOW } + ]); + + do_print("Remove newer visit."); + let filter = { + beginDate: new Date(JS_NOW - 10), + endDate: new Date(JS_NOW) + }; + yield PlacesUtils.history.removeVisitsByFilter(filter); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI should still exist in moz_places."); + do_check_true(page_in_database(TEST_URI.spec)); + do_print("Frecency should be zero.") + do_check_eq(frecencyForUrl(TEST_URI), 0); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js new file mode 100644 index 000000000..699420e43 --- /dev/null +++ b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js @@ -0,0 +1,345 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +// Tests for `History.removeVisitsByFilter`, as implemented in History.jsm + +"use strict"; + +Cu.importGlobalProperties(["URL"]); + +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); + +add_task(function* test_removeVisitsByFilter() { + let referenceDate = new Date(1999, 9, 9, 9, 9); + + // Populate a database with 20 entries, remove a subset of entries, + // ensure consistency. + let remover = Task.async(function*(options) { + do_print("Remover with options " + JSON.stringify(options)); + let SAMPLE_SIZE = options.sampleSize; + + yield PlacesTestUtils.clearHistory(); + yield PlacesUtils.bookmarks.eraseEverything(); + + // Populate the database. + // Create `SAMPLE_SIZE` visits, from the oldest to the newest. + + let bookmarkIndices = new Set(options.bookmarks); + let visits = []; + let frecencyChangePromises = new Map(); + let uriDeletePromises = new Map(); + let getURL = options.url ? + i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/byurl/" + Math.floor(i / (SAMPLE_SIZE / 5)) + "/" : + i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/" + i + "/" + Math.random(); + for (let i = 0; i < SAMPLE_SIZE; ++i) { + let spec = getURL(i); + let uri = NetUtil.newURI(spec); + let jsDate = new Date(Number(referenceDate) + 3600 * 1000 * i); + let dbDate = jsDate * 1000; + let hasBookmark = bookmarkIndices.has(i); + let hasOwnBookmark = hasBookmark; + if (!hasOwnBookmark && options.url) { + // Also mark as bookmarked if one of the earlier bookmarked items has the same URL. + hasBookmark = + options.bookmarks.filter(n => n < i).some(n => visits[n].uri.spec == spec && visits[n].test.hasBookmark); + } + do_print("Generating " + uri.spec + ", " + dbDate); + let visit = { + uri, + title: "visit " + i, + visitDate: dbDate, + test: { + // `visitDate`, as a Date + jsDate: jsDate, + // `true` if we expect that the visit will be removed + toRemove: false, + // `true` if `onRow` informed of the removal of this visit + announcedByOnRow: false, + // `true` if there is a bookmark for this URI, i.e. of the page + // should not be entirely removed. + hasBookmark: hasBookmark, + onFrecencyChanged: null, + onDeleteURI: null, + }, + }; + visits.push(visit); + if (hasOwnBookmark) { + do_print("Adding a bookmark to visit " + i); + yield PlacesUtils.bookmarks.insert({ + url: uri, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test bookmark" + }); + do_print("Bookmark added"); + } + } + + do_print("Adding visits"); + yield PlacesTestUtils.addVisits(visits); + + do_print("Preparing filters"); + let filter = { + }; + let beginIndex = 0; + let endIndex = visits.length - 1; + if ("begin" in options) { + let ms = Number(visits[options.begin].test.jsDate) - 1000; + filter.beginDate = new Date(ms); + beginIndex = options.begin; + } + if ("end" in options) { + let ms = Number(visits[options.end].test.jsDate) + 1000; + filter.endDate = new Date(ms); + endIndex = options.end; + } + if ("limit" in options) { + endIndex = beginIndex + options.limit - 1; // -1 because the start index is inclusive. + filter.limit = options.limit; + } + let removedItems = visits.slice(beginIndex); + endIndex -= beginIndex; + if (options.url) { + let rawURL = ""; + switch (options.url) { + case 1: + filter.url = new URL(removedItems[0].uri.spec); + rawURL = filter.url.href; + break; + case 2: + filter.url = removedItems[0].uri; + rawURL = filter.url.spec; + break; + case 3: + filter.url = removedItems[0].uri.spec; + rawURL = filter.url; + break; + } + endIndex = Math.min(endIndex, removedItems.findIndex((v, index) => v.uri.spec != rawURL) - 1); + } + removedItems.splice(endIndex + 1); + let remainingItems = visits.filter(v => !removedItems.includes(v)); + for (let i = 0; i < removedItems.length; i++) { + let test = removedItems[i].test; + do_print("Marking visit " + (beginIndex + i) + " as expecting removal"); + test.toRemove = true; + if (test.hasBookmark || + (options.url && remainingItems.some(v => v.uri.spec == removedItems[i].uri.spec))) { + frecencyChangePromises.set(removedItems[i].uri.spec, PromiseUtils.defer()); + } else if (!options.url || i == 0) { + uriDeletePromises.set(removedItems[i].uri.spec, PromiseUtils.defer()); + } + } + + let observer = { + deferred: PromiseUtils.defer(), + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function(uri) { + this.deferred.reject(new Error("Unexpected call to onVisit " + uri.spec)); + }, + onTitleChanged: function(uri) { + this.deferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec)); + }, + onClearHistory: function() { + this.deferred.reject("Unexpected call to onClearHistory"); + }, + onPageChanged: function(uri) { + this.deferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec)); + }, + onFrecencyChanged: function(aURI) { + do_print("onFrecencyChanged " + aURI.spec); + let deferred = frecencyChangePromises.get(aURI.spec); + Assert.ok(!!deferred, "Observing onFrecencyChanged"); + deferred.resolve(); + }, + onManyFrecenciesChanged: function() { + do_print("Many frecencies changed"); + for (let [, deferred] of frecencyChangePromises) { + deferred.resolve(); + } + }, + onDeleteURI: function(aURI) { + do_print("onDeleteURI " + aURI.spec); + let deferred = uriDeletePromises.get(aURI.spec); + Assert.ok(!!deferred, "Observing onDeleteURI"); + deferred.resolve(); + }, + onDeleteVisits: function(aURI) { + // Not sure we can test anything. + } + }; + PlacesUtils.history.addObserver(observer, false); + + let cbarg; + if (options.useCallback) { + do_print("Setting up callback"); + cbarg = [info => { + for (let visit of visits) { + do_print("Comparing " + info.date + " and " + visit.test.jsDate); + if (Math.abs(visit.test.jsDate - info.date) < 100) { // Assume rounding errors + Assert.ok(!visit.test.announcedByOnRow, + "This is the first time we announce the removal of this visit"); + Assert.ok(visit.test.toRemove, + "This is a visit we intended to remove"); + visit.test.announcedByOnRow = true; + return; + } + } + Assert.ok(false, "Could not find the visit we attempt to remove"); + }]; + } else { + do_print("No callback"); + cbarg = []; + } + let result = yield PlacesUtils.history.removeVisitsByFilter(filter, ...cbarg); + + Assert.ok(result, "Removal succeeded"); + + // Make sure that we have eliminated exactly the entries we expected + // to eliminate. + for (let i = 0; i < visits.length; ++i) { + let visit = visits[i]; + do_print("Controlling the results on visit " + i); + let remainingVisitsForURI = remainingItems.filter(v => visit.uri.spec == v.uri.spec).length; + Assert.equal( + visits_in_database(visit.uri), + remainingVisitsForURI, + "Visit is still present iff expected"); + if (options.useCallback) { + Assert.equal( + visit.test.toRemove, + visit.test.announcedByOnRow, + "Visit removal has been announced by onResult iff expected"); + } + if (visit.test.hasBookmark || remainingVisitsForURI) { + Assert.notEqual(page_in_database(visit.uri), 0, "The page should still appear in the db"); + } else { + Assert.equal(page_in_database(visit.uri), 0, "The page should have been removed from the db"); + } + } + + // Make sure that the observer has been called wherever applicable. + do_print("Checking URI delete promises."); + yield Promise.all(Array.from(uriDeletePromises.values())); + do_print("Checking frecency change promises."); + yield Promise.all(Array.from(frecencyChangePromises.values())); + PlacesUtils.history.removeObserver(observer); + }); + + let size = 20; + for (let range of [ + {begin: 0}, + {end: 19}, + {begin: 0, end: 10}, + {begin: 3, end: 4}, + {begin: 5, end: 8, limit: 2}, + {begin: 10, end: 18, limit: 5}, + ]) { + for (let bookmarks of [[], [5, 6]]) { + let options = { + sampleSize: size, + bookmarks: bookmarks, + }; + if ("begin" in range) { + options.begin = range.begin; + } + if ("end" in range) { + options.end = range.end; + } + if ("limit" in range) { + options.limit = range.limit; + } + yield remover(options); + options.url = 1; + yield remover(options); + options.url = 2; + yield remover(options); + options.url = 3; + yield remover(options); + } + } + yield PlacesTestUtils.clearHistory(); +}); + +// Test the various error cases +add_task(function* test_error_cases() { + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter(), + /TypeError: Expected a filter/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter("obviously, not a filter"), + /TypeError: Expected a filter/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({}), + /TypeError: Expected a non-empty filter/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: "now"}), + /TypeError: Expected a Date/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: Date.now()}), + /TypeError: Expected a Date/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date()}, "obviously, not a callback"), + /TypeError: Invalid function/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}), + /TypeError: `beginDate` should be at least as old/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({limit: {}}), + /Expected a non-zero positive integer as a limit/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({limit: -1}), + /Expected a non-zero positive integer as a limit/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({limit: 0.1}), + /Expected a non-zero positive integer as a limit/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({limit: Infinity}), + /Expected a non-zero positive integer as a limit/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({url: {}}), + /Expected a valid URL for `url`/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({url: 0}), + /Expected a valid URL for `url`/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}), + /TypeError: `beginDate` should be at least as old/ + ); + Assert.throws( + () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}), + /TypeError: `beginDate` should be at least as old/ + ); +}); + +add_task(function* test_orphans() { + let uri = NetUtil.newURI("http://moz.org/"); + yield PlacesTestUtils.addVisits({ uri }); + + PlacesUtils.favicons.setAndFetchFaviconForPage( + uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, Services.scriptSecurityManager.getSystemPrincipal()); + PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + yield PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(1999, 9, 9, 9, 9), + endDate: new Date() }); + Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed"); + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) + + (SELECT count(*) FROM moz_favicons) AS count`); + Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans"); +}); diff --git a/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js new file mode 100644 index 000000000..832df9d9a --- /dev/null +++ b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js @@ -0,0 +1,52 @@ +// Test that repeated additions of the same URI through updatePlaces, properly +// update from_visit and notify titleChanged. + +add_task(function* test() { + let uri = "http://test.com/"; + + let promiseTitleChangedNotifications = new Promise(resolve => { + let historyObserver = { + _count: 0, + __proto__: NavHistoryObserver.prototype, + onTitleChanged(aURI, aTitle, aGUID) { + Assert.equal(aURI.spec, uri, "Should notify the proper url"); + if (++this._count == 2) { + PlacesUtils.history.removeObserver(historyObserver); + resolve(); + } + } + }; + PlacesUtils.history.addObserver(historyObserver, false); + }); + + // This repeats the url on purpose, don't merge it into a single place entry. + yield PlacesTestUtils.addVisits([ + { uri, title: "test" }, + { uri, referrer: uri, title: "test2" }, + ]); + + let options = PlacesUtils.history.getNewQueryOptions(); + let query = PlacesUtils.history.getNewQuery(); + query.uri = NetUtil.newURI(uri); + options.resultType = options.RESULTS_AS_VISIT; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + Assert.equal(root.childCount, 2); + + let child = root.getChild(0); + Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK"); + Assert.equal(child.visitId, 1, "Visit ID should be 1"); + Assert.equal(child.fromVisitId, -1, "Should have no referrer visit ID"); + Assert.equal(child.title, "test2", "Should have the correct title"); + + child = root.getChild(1); + Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK"); + Assert.equal(child.visitId, 2, "Visit ID should be 2"); + Assert.equal(child.fromVisitId, 1, "First visit should be the referring visit"); + Assert.equal(child.title, "test2", "Should have the correct title"); + + root.containerOpen = false; + + yield promiseTitleChangedNotifications; +}); diff --git a/toolkit/components/places/tests/history/xpcshell.ini b/toolkit/components/places/tests/history/xpcshell.ini new file mode 100644 index 000000000..ee182e090 --- /dev/null +++ b/toolkit/components/places/tests/history/xpcshell.ini @@ -0,0 +1,9 @@ +[DEFAULT] +head = head_history.js +tail = + +[test_insert.js] +[test_remove.js] +[test_removeVisits.js] +[test_removeVisitsByFilter.js] +[test_updatePlaces_sameUri_titleChanged.js] diff --git a/toolkit/components/places/tests/migration/.eslintrc.js b/toolkit/components/places/tests/migration/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/migration/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/migration/head_migration.js b/toolkit/components/places/tests/migration/head_migration.js new file mode 100644 index 000000000..1ebecd4c0 --- /dev/null +++ b/toolkit/components/places/tests/migration/head_migration.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict" + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + +const DB_FILENAME = "places.sqlite"; + +/** + * Sets the database to use for the given test. This should be the very first + * thing in the test, otherwise this database will not be used! + * + * @param aFileName + * The filename of the database to use. This database must exist in + * toolkit/components/places/tests/migration! + * @return {Promise} + */ +var setupPlacesDatabase = Task.async(function* (aFileName) { + let currentDir = yield OS.File.getCurrentDirectory(); + + let src = OS.Path.join(currentDir, aFileName); + Assert.ok((yield OS.File.exists(src)), "Database file found"); + + // Ensure that our database doesn't already exist. + let dest = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + Assert.ok(!(yield OS.File.exists(dest)), "Database file should not exist yet"); + + yield OS.File.copy(src, dest); +}); + +// This works provided all tests in this folder use add_task. +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/migration/places_v10.sqlite b/toolkit/components/places/tests/migration/places_v10.sqlite Binary files differnew file mode 100644 index 000000000..80a8ecd6a --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v10.sqlite diff --git a/toolkit/components/places/tests/migration/places_v11.sqlite b/toolkit/components/places/tests/migration/places_v11.sqlite Binary files differnew file mode 100644 index 000000000..bef27d5f5 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v11.sqlite diff --git a/toolkit/components/places/tests/migration/places_v17.sqlite b/toolkit/components/places/tests/migration/places_v17.sqlite Binary files differnew file mode 100644 index 000000000..5183cde83 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v17.sqlite diff --git a/toolkit/components/places/tests/migration/places_v19.sqlite b/toolkit/components/places/tests/migration/places_v19.sqlite Binary files differnew file mode 100644 index 000000000..11e2e6247 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v19.sqlite diff --git a/toolkit/components/places/tests/migration/places_v21.sqlite b/toolkit/components/places/tests/migration/places_v21.sqlite Binary files differnew file mode 100644 index 000000000..f72930826 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v21.sqlite diff --git a/toolkit/components/places/tests/migration/places_v22.sqlite b/toolkit/components/places/tests/migration/places_v22.sqlite Binary files differnew file mode 100644 index 000000000..30bf840b0 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v22.sqlite diff --git a/toolkit/components/places/tests/migration/places_v23.sqlite b/toolkit/components/places/tests/migration/places_v23.sqlite Binary files differnew file mode 100644 index 000000000..b519b97d2 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v23.sqlite diff --git a/toolkit/components/places/tests/migration/places_v24.sqlite b/toolkit/components/places/tests/migration/places_v24.sqlite Binary files differnew file mode 100644 index 000000000..b35f958a6 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v24.sqlite diff --git a/toolkit/components/places/tests/migration/places_v25.sqlite b/toolkit/components/places/tests/migration/places_v25.sqlite Binary files differnew file mode 100644 index 000000000..2afd1da1f --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v25.sqlite diff --git a/toolkit/components/places/tests/migration/places_v26.sqlite b/toolkit/components/places/tests/migration/places_v26.sqlite Binary files differnew file mode 100644 index 000000000..b4b238179 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v26.sqlite diff --git a/toolkit/components/places/tests/migration/places_v27.sqlite b/toolkit/components/places/tests/migration/places_v27.sqlite Binary files differnew file mode 100644 index 000000000..57dfb7562 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v27.sqlite diff --git a/toolkit/components/places/tests/migration/places_v28.sqlite b/toolkit/components/places/tests/migration/places_v28.sqlite Binary files differnew file mode 100644 index 000000000..9a27db324 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v28.sqlite diff --git a/toolkit/components/places/tests/migration/places_v29.sqlite b/toolkit/components/places/tests/migration/places_v29.sqlite Binary files differnew file mode 100644 index 000000000..f6de0fe8a --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v29.sqlite diff --git a/toolkit/components/places/tests/migration/places_v30.sqlite b/toolkit/components/places/tests/migration/places_v30.sqlite Binary files differnew file mode 100644 index 000000000..9cbabe005 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v30.sqlite diff --git a/toolkit/components/places/tests/migration/places_v31.sqlite b/toolkit/components/places/tests/migration/places_v31.sqlite Binary files differnew file mode 100644 index 000000000..9d33b9eff --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v31.sqlite diff --git a/toolkit/components/places/tests/migration/places_v32.sqlite b/toolkit/components/places/tests/migration/places_v32.sqlite Binary files differnew file mode 100644 index 000000000..239f6c5fe --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v32.sqlite diff --git a/toolkit/components/places/tests/migration/places_v33.sqlite b/toolkit/components/places/tests/migration/places_v33.sqlite Binary files differnew file mode 100644 index 000000000..6071dc6a6 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v33.sqlite diff --git a/toolkit/components/places/tests/migration/places_v34.sqlite b/toolkit/components/places/tests/migration/places_v34.sqlite Binary files differnew file mode 100644 index 000000000..474628996 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v34.sqlite diff --git a/toolkit/components/places/tests/migration/places_v35.sqlite b/toolkit/components/places/tests/migration/places_v35.sqlite Binary files differnew file mode 100644 index 000000000..5e157d778 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v35.sqlite diff --git a/toolkit/components/places/tests/migration/places_v6.sqlite b/toolkit/components/places/tests/migration/places_v6.sqlite Binary files differnew file mode 100644 index 000000000..2852a4cf9 --- /dev/null +++ b/toolkit/components/places/tests/migration/places_v6.sqlite diff --git a/toolkit/components/places/tests/migration/test_current_from_downgraded.js b/toolkit/components/places/tests/migration/test_current_from_downgraded.js new file mode 100644 index 000000000..6d36cab14 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_downgraded.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase(`places_v${CURRENT_SCHEMA_VERSION}.sqlite`); + // Downgrade the schema version to the first supported one. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path: path }); + yield db.setSchemaVersion(FIRST_UPGRADABLE_SCHEMA_VERSION); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v11.js b/toolkit/components/places/tests/migration/test_current_from_v11.js new file mode 100644 index 000000000..43b8fb1f6 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v11.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v11.sqlite"); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_moz_hosts() { + let db = yield PlacesUtils.promiseDBConnection(); + + // This will throw if the column does not exist. + yield db.execute("SELECT host, frecency, typed, prefix FROM moz_hosts"); + + // moz_hosts is populated asynchronously, so we need to wait. + yield PlacesTestUtils.promiseAsyncUpdates(); + + // check the number of entries in moz_hosts equals the number of + // unique rev_host in moz_places + let rows = yield db.execute( + `SELECT (SELECT COUNT(host) FROM moz_hosts), + (SELECT COUNT(DISTINCT rev_host) + FROM moz_places + WHERE LENGTH(rev_host) > 1) + `); + + Assert.equal(rows.length, 1); + let mozHostsCount = rows[0].getResultByIndex(0); + let mozPlacesCount = rows[0].getResultByIndex(1); + + Assert.ok(mozPlacesCount > 0, "There is some url in the database"); + Assert.equal(mozPlacesCount, mozHostsCount, "moz_hosts has the expected number of entries"); +}); + +add_task(function* test_journal() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute("PRAGMA journal_mode"); + Assert.equal(rows.length, 1); + // WAL journal mode should be set on this database. + Assert.equal(rows[0].getResultByIndex(0), "wal"); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v19.js b/toolkit/components/places/tests/migration/test_current_from_v19.js new file mode 100644 index 000000000..b8d837e68 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v19.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ANNO_LEGACYGUID = "placesInternal/GUID"; + +var getTotalGuidAnnotationsCount = Task.async(function* (db) { + let rows = yield db.execute( + `SELECT count(*) + FROM moz_items_annos a + JOIN moz_anno_attributes b ON a.anno_attribute_id = b.id + WHERE b.name = :attr_name + `, { attr_name: ANNO_LEGACYGUID }); + return rows[0].getResultByIndex(0); +}); + +add_task(function* setup() { + yield setupPlacesDatabase("places_v19.sqlite"); +}); + +add_task(function* initial_state() { + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path: path }); + + Assert.equal((yield getTotalGuidAnnotationsCount(db)), 1, + "There should be 1 obsolete guid annotation"); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_bookmark_guid_annotation_removed() +{ + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield getTotalGuidAnnotationsCount(db)), 0, + "There should be no more obsolete GUID annotations."); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v24.js b/toolkit/components/places/tests/migration/test_current_from_v24.js new file mode 100644 index 000000000..0561b4922 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v24.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v24.sqlite"); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_bookmark_guid_annotation_removed() +{ + yield PlacesUtils.bookmarks.eraseEverything(); + + let db = yield PlacesUtils.promiseDBConnection(); + let m = new Map([ + [PlacesUtils.placesRootId, PlacesUtils.bookmarks.rootGuid], + [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.menuGuid], + [PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.toolbarGuid], + [PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.bookmarks.unfiledGuid], + [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid], + [PlacesUtils.mobileFolderId, PlacesUtils.bookmarks.mobileGuid], + ]); + + let rows = yield db.execute(`SELECT id, guid FROM moz_bookmarks`); + for (let row of rows) { + let id = row.getResultByName("id"); + let guid = row.getResultByName("guid"); + Assert.equal(m.get(id), guid, "The root folder has the correct GUID"); + } +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v25.js b/toolkit/components/places/tests/migration/test_current_from_v25.js new file mode 100644 index 000000000..b066975fc --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v25.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v25.sqlite"); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_dates_rounded() { + let root = yield PlacesUtils.promiseBookmarksTree(); + function ensureDates(node) { + // When/if promiseBookmarksTree returns these as Date objects, switch this + // test to use getItemDateAdded and getItemLastModified. And when these + // methods are removed, this test can be eliminated altogether. + Assert.strictEqual(typeof(node.dateAdded), "number"); + Assert.strictEqual(typeof(node.lastModified), "number"); + Assert.strictEqual(node.dateAdded % 1000, 0); + Assert.strictEqual(node.lastModified % 1000, 0); + if ("children" in node) + node.children.forEach(ensureDates); + } + ensureDates(root); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v26.js b/toolkit/components/places/tests/migration/test_current_from_v26.js new file mode 100644 index 000000000..7ff4bc352 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v26.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v26.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + // Add pages. + yield db.execute(`INSERT INTO moz_places (url, guid) + VALUES ("http://test1.com/", "test1_______") + , ("http://test2.com/", "test2_______") + , ("http://test3.com/", "test3_______") + `); + // Add keywords. + yield db.execute(`INSERT INTO moz_keywords (keyword) + VALUES ("kw1") + , ("kw2") + , ("kw3") + , ("kw4") + , ("kw5") + `); + // Add bookmarks. + let now = Date.now() * 1000; + let index = 0; + yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid) + VALUES (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark1___") + /* same uri, different keyword */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark2___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark3___") + /* same uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark4___") + /* same uri, same keyword as 2 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark5___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw3'), "bookmark6___") + , (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw4'), "bookmark7___") + /* same uri and post_data as bookmark7, different keyword */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw5'), "bookmark8___") + `); + // Add postData. + yield db.execute(`INSERT INTO moz_anno_attributes (name) + VALUES ("bookmarkProperties/POSTData") + , ("someOtherAnno")`); + yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content) + VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1") + , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2") + , ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz") + , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark7___"), "postData3") + , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark8___"), "postData3") + `); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_keywords() { + // When 2 urls have the same keyword, if one has postData it will be + // preferred. + let entry1 = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry1.url.href, "http://test2.com/"); + Assert.equal(entry1.postData, "postData1"); + let entry2 = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry2.url.href, "http://test2.com/"); + Assert.equal(entry2.postData, "postData2"); + let entry3 = yield PlacesUtils.keywords.fetch("kw3"); + Assert.equal(entry3.url.href, "http://test1.com/"); + Assert.equal(entry3.postData, null); + let entry4 = yield PlacesUtils.keywords.fetch("kw4"); + Assert.equal(entry4, null); + let entry5 = yield PlacesUtils.keywords.fetch("kw5"); + Assert.equal(entry5.url.href, "http://test3.com/"); + Assert.equal(entry5.postData, "postData3"); + + Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords + Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords + Assert.equal((yield foreign_count("http://test3.com/")), 3); // 2 bookmark2 + 1 keywords +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v27.js b/toolkit/components/places/tests/migration/test_current_from_v27.js new file mode 100644 index 000000000..1675901eb --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v27.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v27.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + // Add pages. + yield db.execute(`INSERT INTO moz_places (url, guid) + VALUES ("http://test1.com/", "test1_______") + , ("http://test2.com/", "test2_______") + `); + // Add keywords. + yield db.execute(`INSERT INTO moz_keywords (keyword, place_id, post_data) + VALUES ("kw1", (SELECT id FROM moz_places WHERE guid = "test2_______"), "broken data") + , ("kw2", (SELECT id FROM moz_places WHERE guid = "test2_______"), NULL) + , ("kw3", (SELECT id FROM moz_places WHERE guid = "test1_______"), "zzzzzzzzzz") + `); + // Add bookmarks. + let now = Date.now() * 1000; + let index = 0; + yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid) + VALUES (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark1___") + /* same uri, different keyword */ + , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark2___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark3___") + /* same uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark4___") + /* same uri, same keyword as 2 */ + , (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark5___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = "kw3"), "bookmark6___") + `); + // Add postData. + yield db.execute(`INSERT INTO moz_anno_attributes (name) + VALUES ("bookmarkProperties/POSTData") + , ("someOtherAnno")`); + yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content) + VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1") + , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2") + , ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz") + `); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_keywords() { + // When 2 urls have the same keyword, if one has postData it will be + // preferred. + let entry1 = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry1.url.href, "http://test2.com/"); + Assert.equal(entry1.postData, "postData1"); + let entry2 = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry2.url.href, "http://test2.com/"); + Assert.equal(entry2.postData, "postData2"); + let entry3 = yield PlacesUtils.keywords.fetch("kw3"); + Assert.equal(entry3.url.href, "http://test1.com/"); + Assert.equal(entry3.postData, null); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v31.js b/toolkit/components/places/tests/migration/test_current_from_v31.js new file mode 100644 index 000000000..6b9131daa --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v31.js @@ -0,0 +1,46 @@ +// Add pages. +let shorturl = "http://example.com/" + "a".repeat(1981); +let longurl = "http://example.com/" + "a".repeat(1982); +let bmurl = "http://example.com/" + "a".repeat(1983); + +add_task(function* setup() { + yield setupPlacesDatabase("places_v31.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + + yield db.execute(`INSERT INTO moz_places (url, guid, foreign_count) + VALUES (:shorturl, "test1_______", 0) + , (:longurl, "test2_______", 0) + , (:bmurl, "test3_______", 1) + `, { shorturl, longurl, bmurl }); + // Add visits. + yield db.execute(`INSERT INTO moz_historyvisits (place_id) + VALUES ((SELECT id FROM moz_places WHERE url = :shorturl)) + , ((SELECT id FROM moz_places WHERE url = :longurl)) + `, { shorturl, longurl }); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_longurls() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute(`SELECT 1 FROM moz_places where url = :longurl`, + { longurl }); + Assert.equal(rows.length, 0, "Long url should have been removed"); + rows = yield db.execute(`SELECT 1 FROM moz_places where url = :shorturl`, + { shorturl }); + Assert.equal(rows.length, 1, "Short url should have been retained"); + rows = yield db.execute(`SELECT 1 FROM moz_places where url = :bmurl`, + { bmurl }); + Assert.equal(rows.length, 1, "Bookmarked url should have been retained"); + rows = yield db.execute(`SELECT count(*) FROM moz_historyvisits`); + Assert.equal(rows.length, 1, "Orphan visists should have been removed"); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v34.js b/toolkit/components/places/tests/migration/test_current_from_v34.js new file mode 100644 index 000000000..115bcec67 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v34.js @@ -0,0 +1,141 @@ +Cu.importGlobalProperties(["URL", "crypto"]); + +const { TYPE_BOOKMARK, TYPE_FOLDER } = Ci.nsINavBookmarksService; +const { EXPIRE_NEVER, TYPE_INT32 } = Ci.nsIAnnotationService; + +function makeGuid() { + return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), { + pad: false, + }); +} + +// These queries are more or less copied directly from Bookmarks.jsm, but +// operate on the old, pre-migration DB. We can't use any of the Places SQL +// functions yet, because those are only registered for the main connection. +function* insertItem(db, info) { + let [parentInfo] = yield db.execute(` + SELECT b.id, (SELECT count(*) FROM moz_bookmarks + WHERE parent = b.id) AS childCount + FROM moz_bookmarks b + WHERE b.guid = :parentGuid`, + { parentGuid: info.parentGuid }); + + let guid = makeGuid(); + yield db.execute(` + INSERT INTO moz_bookmarks (fk, type, parent, position, guid) + VALUES ((SELECT id FROM moz_places WHERE url = :url), + :type, :parent, :position, :guid)`, + { url: info.url || "nonexistent", type: info.type, guid, + // Just append items. + position: parentInfo.getResultByName("childCount"), + parent: parentInfo.getResultByName("id") }); + + let id = (yield db.execute(` + SELECT id FROM moz_bookmarks WHERE guid = :guid LIMIT 1`, + { guid }))[0].getResultByName("id"); + + return { id, guid }; +} + +function insertBookmark(db, info) { + return db.executeTransaction(function* () { + if (info.type == TYPE_BOOKMARK) { + // We don't have access to the hash function here, so we omit the + // `url_hash` column. These will be fixed up automatically during + // migration. + let url = new URL(info.url); + let placeGuid = makeGuid(); + yield db.execute(` + INSERT INTO moz_places (url, rev_host, hidden, frecency, guid) + VALUES (:url, :rev_host, 0, -1, :guid)`, + { url: url.href, guid: placeGuid, + rev_host: PlacesUtils.getReversedHost(url) }); + } + return yield* insertItem(db, info); + }); +} + +function* insertAnno(db, itemId, name, value) { + yield db.execute(`INSERT OR IGNORE INTO moz_anno_attributes (name) + VALUES (:name)`, { name }); + yield db.execute(` + INSERT INTO moz_items_annos + (item_id, anno_attribute_id, content, flags, + expiration, type, dateAdded, lastModified) + VALUES (:itemId, + (SELECT id FROM moz_anno_attributes + WHERE name = :name), + 1, 0, :expiration, :type, 0, 0) + `, { itemId, name, expiration: EXPIRE_NEVER, type: TYPE_INT32 }); +} + +function insertMobileFolder(db) { + return db.executeTransaction(function* () { + let item = yield* insertItem(db, { + type: TYPE_FOLDER, + parentGuid: "root________", + }); + yield* insertAnno(db, item.id, "mobile/bookmarksRoot", 1); + return item; + }); +} + +var mobileId, mobileGuid, fxGuid; +var dupeMobileId, dupeMobileGuid, tbGuid; + +add_task(function* setup() { + yield setupPlacesDatabase("places_v34.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + + do_print("Create mobile folder with bookmarks"); + ({ id: mobileId, guid: mobileGuid } = yield insertMobileFolder(db)); + ({ guid: fxGuid } = yield insertBookmark(db, { + type: TYPE_BOOKMARK, + url: "http://getfirefox.com", + parentGuid: mobileGuid, + })); + + // We should only have one mobile folder, but, in case an old version of Sync + // did the wrong thing and created multiple mobile folders, we should merge + // their contents into the new mobile root. + do_print("Create second mobile folder with different bookmarks"); + ({ id: dupeMobileId, guid: dupeMobileGuid } = yield insertMobileFolder(db)); + ({ guid: tbGuid } = yield insertBookmark(db, { + type: TYPE_BOOKMARK, + url: "http://getthunderbird.com", + parentGuid: dupeMobileGuid, + })); + + yield db.close(); +}); + +add_task(function* database_is_valid() { + // Accessing the database for the first time triggers migration. + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_mobile_root() { + let fxBmk = yield PlacesUtils.bookmarks.fetch(fxGuid); + equal(fxBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid, + "Firefox bookmark should be moved to new mobile root"); + equal(fxBmk.index, 0, "Firefox bookmark should be first child of new root"); + + let tbBmk = yield PlacesUtils.bookmarks.fetch(tbGuid); + equal(tbBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid, + "Thunderbird bookmark should be moved to new mobile root"); + equal(tbBmk.index, 1, + "Thunderbird bookmark should be second child of new root"); + + let mobileRootId = PlacesUtils.promiseItemId( + PlacesUtils.bookmarks.mobileGuid); + let annoItemIds = PlacesUtils.annotations.getItemsWithAnnotation( + PlacesUtils.MOBILE_ROOT_ANNO, {}); + deepEqual(annoItemIds, [mobileRootId], + "Only mobile root should have mobile anno"); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js b/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js new file mode 100644 index 000000000..871fe8993 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v34.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + // Remove all the roots. + yield db.execute("DELETE FROM moz_bookmarks"); + yield db.close(); +}); + +add_task(function* database_is_valid() { + // Accessing the database for the first time triggers migration. + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); diff --git a/toolkit/components/places/tests/migration/test_current_from_v6.js b/toolkit/components/places/tests/migration/test_current_from_v6.js new file mode 100644 index 000000000..a3f9dc229 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v6.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests migration from a preliminary schema version 6 that + * lacks frecency column and moz_inputhistory table. + */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v6.sqlite"); +}); + +add_task(function* corrupt_database_not_exists() { + let corruptPath = OS.Path.join(OS.Constants.Path.profileDir, + "places.sqlite.corrupt"); + Assert.ok(!(yield OS.File.exists(corruptPath)), "Corrupt file should not exist"); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* check_columns() { + // Check the database has been replaced, these would throw otherwise. + let db = yield PlacesUtils.promiseDBConnection(); + yield db.execute("SELECT frecency from moz_places"); + yield db.execute("SELECT 1 from moz_inputhistory"); +}); + +add_task(function* corrupt_database_exists() { + let corruptPath = OS.Path.join(OS.Constants.Path.profileDir, + "places.sqlite.corrupt"); + Assert.ok((yield OS.File.exists(corruptPath)), "Corrupt file should exist"); +}); diff --git a/toolkit/components/places/tests/migration/xpcshell.ini b/toolkit/components/places/tests/migration/xpcshell.ini new file mode 100644 index 000000000..aae0f75ee --- /dev/null +++ b/toolkit/components/places/tests/migration/xpcshell.ini @@ -0,0 +1,36 @@ +[DEFAULT] +head = head_migration.js +tail = + +support-files = + places_v6.sqlite + places_v10.sqlite + places_v11.sqlite + places_v17.sqlite + places_v19.sqlite + places_v21.sqlite + places_v22.sqlite + places_v23.sqlite + places_v24.sqlite + places_v25.sqlite + places_v26.sqlite + places_v27.sqlite + places_v28.sqlite + places_v30.sqlite + places_v31.sqlite + places_v32.sqlite + places_v33.sqlite + places_v34.sqlite + places_v35.sqlite + +[test_current_from_downgraded.js] +[test_current_from_v6.js] +[test_current_from_v11.js] +[test_current_from_v19.js] +[test_current_from_v24.js] +[test_current_from_v25.js] +[test_current_from_v26.js] +[test_current_from_v27.js] +[test_current_from_v31.js] +[test_current_from_v34.js] +[test_current_from_v34_no_roots.js] diff --git a/toolkit/components/places/tests/moz.build b/toolkit/components/places/tests/moz.build new file mode 100644 index 000000000..a40c0e93a --- /dev/null +++ b/toolkit/components/places/tests/moz.build @@ -0,0 +1,67 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TEST_DIRS += ['cpp'] + +TESTING_JS_MODULES += [ + 'PlacesTestUtils.jsm', +] + +XPCSHELL_TESTS_MANIFESTS += [ + 'bookmarks/xpcshell.ini', + 'expiration/xpcshell.ini', + 'favicons/xpcshell.ini', + 'history/xpcshell.ini', + 'migration/xpcshell.ini', + 'queries/xpcshell.ini', + 'unifiedcomplete/xpcshell.ini', + 'unit/xpcshell.ini', +] + +BROWSER_CHROME_MANIFESTS += ['browser/browser.ini'] +MOCHITEST_CHROME_MANIFESTS += [ + 'chrome/chrome.ini', +] + +TEST_HARNESS_FILES.xpcshell.toolkit.components.places.tests += [ + 'head_common.js', +] + +TEST_HARNESS_FILES.testing.mochitest.tests.toolkit.components.places.tests.browser += [ + 'browser/399606-history.go-0.html', + 'browser/399606-httprefresh.html', + 'browser/399606-location.reload.html', + 'browser/399606-location.replace.html', + 'browser/399606-window.location.href.html', + 'browser/399606-window.location.html', + 'browser/461710_iframe.html', + 'browser/461710_link_page-2.html', + 'browser/461710_link_page-3.html', + 'browser/461710_link_page.html', + 'browser/461710_visited_page.html', + 'browser/begin.html', + 'browser/favicon-normal16.png', + 'browser/favicon-normal32.png', + 'browser/favicon.html', + 'browser/final.html', + 'browser/history_post.html', + 'browser/history_post.sjs', + 'browser/redirect-target.html', + 'browser/redirect.sjs', + 'browser/redirect_once.sjs', + 'browser/redirect_twice.sjs', + 'browser/title1.html', + 'browser/title2.html', +] + +TEST_HARNESS_FILES.testing.mochitest.tests.toolkit.components.places.tests.chrome += [ + 'chrome/bad_links.atom', + 'chrome/link-less-items-no-site-uri.rss', + 'chrome/link-less-items.rss', + 'chrome/rss_as_html.rss', + 'chrome/rss_as_html.rss^headers^', + 'chrome/sample_feed.atom', +] diff --git a/toolkit/components/places/tests/queries/.eslintrc.js b/toolkit/components/places/tests/queries/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/queries/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/queries/head_queries.js b/toolkit/components/places/tests/queries/head_queries.js new file mode 100644 index 000000000..d37b3365f --- /dev/null +++ b/toolkit/components/places/tests/queries/head_queries.js @@ -0,0 +1,370 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + + +// Some Useful Date constants - PRTime uses microseconds, so convert +const DAY_MICROSEC = 86400000000; +const today = PlacesUtils.toPRTime(Date.now()); +const yesterday = today - DAY_MICROSEC; +const lastweek = today - (DAY_MICROSEC * 7); +const daybefore = today - (DAY_MICROSEC * 2); +const old = today - (DAY_MICROSEC * 3); +const futureday = today + (DAY_MICROSEC * 3); +const olderthansixmonths = today - (DAY_MICROSEC * 31 * 7); + + +/** + * Generalized function to pull in an array of objects of data and push it into + * the database. It does NOT do any checking to see that the input is + * appropriate. This function is an asynchronous task, it can be called using + * "Task.spawn" or using the "yield" function inside another task. + */ +function* task_populateDB(aArray) +{ + // Iterate over aArray and execute all instructions. + for (let arrayItem of aArray) { + try { + // make the data object into a query data object in order to create proper + // default values for anything left unspecified + var qdata = new queryData(arrayItem); + if (qdata.isVisit) { + // Then we should add a visit for this node + yield PlacesTestUtils.addVisits({ + uri: uri(qdata.uri), + transition: qdata.transType, + visitDate: qdata.lastVisit, + referrer: qdata.referrer ? uri(qdata.referrer) : null, + title: qdata.title + }); + if (qdata.visitCount && !qdata.isDetails) { + // Set a fake visit_count, this is not a real count but can be used + // to test sorting by visit_count. + let stmt = DBConn().createAsyncStatement( + "UPDATE moz_places SET visit_count = :vc WHERE url_hash = hash(:url) AND url = :url"); + stmt.params.vc = qdata.visitCount; + stmt.params.url = qdata.uri; + try { + stmt.executeAsync(); + } + catch (ex) { + print("Error while setting visit_count."); + } + finally { + stmt.finalize(); + } + } + } + + if (qdata.isRedirect) { + // This must be async to properly enqueue after the updateFrecency call + // done by the visit addition. + let stmt = DBConn().createAsyncStatement( + "UPDATE moz_places SET hidden = 1 WHERE url_hash = hash(:url) AND url = :url"); + stmt.params.url = qdata.uri; + try { + stmt.executeAsync(); + } + catch (ex) { + print("Error while setting hidden."); + } + finally { + stmt.finalize(); + } + } + + if (qdata.isDetails) { + // Then we add extraneous page details for testing + yield PlacesTestUtils.addVisits({ + uri: uri(qdata.uri), + visitDate: qdata.lastVisit, + title: qdata.title + }); + } + + if (qdata.markPageAsTyped) { + PlacesUtils.history.markPageAsTyped(uri(qdata.uri)); + } + + if (qdata.isPageAnnotation) { + if (qdata.removeAnnotation) + PlacesUtils.annotations.removePageAnnotation(uri(qdata.uri), + qdata.annoName); + else { + PlacesUtils.annotations.setPageAnnotation(uri(qdata.uri), + qdata.annoName, + qdata.annoVal, + qdata.annoFlags, + qdata.annoExpiration); + } + } + + if (qdata.isItemAnnotation) { + if (qdata.removeAnnotation) + PlacesUtils.annotations.removeItemAnnotation(qdata.itemId, + qdata.annoName); + else { + PlacesUtils.annotations.setItemAnnotation(qdata.itemId, + qdata.annoName, + qdata.annoVal, + qdata.annoFlags, + qdata.annoExpiration); + } + } + + if (qdata.isFolder) { + yield PlacesUtils.bookmarks.insert({ + parentGuid: qdata.parentGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: qdata.title, + index: qdata.index + }); + } + + if (qdata.isLivemark) { + yield PlacesUtils.livemarks.addLivemark({ title: qdata.title + , parentId: (yield PlacesUtils.promiseItemId(qdata.parentGuid)) + , index: qdata.index + , feedURI: uri(qdata.feedURI) + , siteURI: uri(qdata.uri) + }); + } + + if (qdata.isBookmark) { + let data = { + parentGuid: qdata.parentGuid, + index: qdata.index, + title: qdata.title, + url: qdata.uri + }; + + if (qdata.dateAdded) { + data.dateAdded = new Date(qdata.dateAdded / 1000); + } + + if (qdata.lastModified) { + data.lastModified = new Date(qdata.lastModified / 1000); + } + + yield PlacesUtils.bookmarks.insert(data); + + if (qdata.keyword) { + yield PlacesUtils.keywords.insert({ url: qdata.uri, + keyword: qdata.keyword }); + } + } + + if (qdata.isTag) { + PlacesUtils.tagging.tagURI(uri(qdata.uri), qdata.tagArray); + } + + if (qdata.isSeparator) { + yield PlacesUtils.bookmarks.insert({ + parentGuid: qdata.parentGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: qdata.index + }); + } + } catch (ex) { + // use the arrayItem object here in case instantiation of qdata failed + do_print("Problem with this URI: " + arrayItem.uri); + do_throw("Error creating database: " + ex + "\n"); + } + } +} + + +/** + * The Query Data Object - this object encapsulates data for our queries and is + * used to parameterize our calls to the Places APIs to put data into the + * database. It also has some interesting meta functions to determine which APIs + * should be called, and to determine if this object should show up in the + * resulting query. + * Its parameter is an object specifying which attributes you want to set. + * For ex: + * var myobj = new queryData({isVisit: true, uri:"http://mozilla.com", title="foo"}); + * Note that it doesn't do any input checking on that object. + */ +function queryData(obj) { + this.isVisit = obj.isVisit ? obj.isVisit : false; + this.isBookmark = obj.isBookmark ? obj.isBookmark: false; + this.uri = obj.uri ? obj.uri : ""; + this.lastVisit = obj.lastVisit ? obj.lastVisit : today; + this.referrer = obj.referrer ? obj.referrer : null; + this.transType = obj.transType ? obj.transType : Ci.nsINavHistoryService.TRANSITION_TYPED; + this.isRedirect = obj.isRedirect ? obj.isRedirect : false; + this.isDetails = obj.isDetails ? obj.isDetails : false; + this.title = obj.title ? obj.title : ""; + this.markPageAsTyped = obj.markPageAsTyped ? obj.markPageAsTyped : false; + this.isPageAnnotation = obj.isPageAnnotation ? obj.isPageAnnotation : false; + this.removeAnnotation= obj.removeAnnotation ? true : false; + this.annoName = obj.annoName ? obj.annoName : ""; + this.annoVal = obj.annoVal ? obj.annoVal : ""; + this.annoFlags = obj.annoFlags ? obj.annoFlags : 0; + this.annoExpiration = obj.annoExpiration ? obj.annoExpiration : 0; + this.isItemAnnotation = obj.isItemAnnotation ? obj.isItemAnnotation : false; + this.itemId = obj.itemId ? obj.itemId : 0; + this.annoMimeType = obj.annoMimeType ? obj.annoMimeType : ""; + this.isTag = obj.isTag ? obj.isTag : false; + this.tagArray = obj.tagArray ? obj.tagArray : null; + this.isLivemark = obj.isLivemark ? obj.isLivemark : false; + this.parentGuid = obj.parentGuid || PlacesUtils.bookmarks.rootGuid; + this.feedURI = obj.feedURI ? obj.feedURI : ""; + this.index = obj.index ? obj.index : PlacesUtils.bookmarks.DEFAULT_INDEX; + this.isFolder = obj.isFolder ? obj.isFolder : false; + this.contractId = obj.contractId ? obj.contractId : ""; + this.lastModified = obj.lastModified ? obj.lastModified : null; + this.dateAdded = obj.dateAdded ? obj.dateAdded : null; + this.keyword = obj.keyword ? obj.keyword : ""; + this.visitCount = obj.visitCount ? obj.visitCount : 0; + this.isSeparator = obj.hasOwnProperty("isSeparator") && obj.isSeparator; + + // And now, the attribute for whether or not this object should appear in the + // resulting query + this.isInQuery = obj.isInQuery ? obj.isInQuery : false; +} + +// All attributes are set in the constructor above +queryData.prototype = { } + + +/** + * Helper function to compare an array of query objects with a result set. + * It assumes the array of query objects contains the SAME SORT as the result + * set. It checks the the uri, title, time, and bookmarkIndex properties of + * the results, where appropriate. + */ +function compareArrayToResult(aArray, aRoot) { + do_print("Comparing Array to Results"); + + var wasOpen = aRoot.containerOpen; + if (!wasOpen) + aRoot.containerOpen = true; + + // check expected number of results against actual + var expectedResultCount = aArray.filter(function(aEl) { return aEl.isInQuery; }).length; + if (expectedResultCount != aRoot.childCount) { + // Debugging code for failures. + dump_table("moz_places"); + dump_table("moz_historyvisits"); + do_print("Found children:"); + for (let i = 0; i < aRoot.childCount; i++) { + do_print(aRoot.getChild(i).uri); + } + do_print("Expected:"); + for (let i = 0; i < aArray.length; i++) { + if (aArray[i].isInQuery) + do_print(aArray[i].uri); + } + } + do_check_eq(expectedResultCount, aRoot.childCount); + + var inQueryIndex = 0; + for (var i = 0; i < aArray.length; i++) { + if (aArray[i].isInQuery) { + var child = aRoot.getChild(inQueryIndex); + // do_print("testing testData[" + i + "] vs result[" + inQueryIndex + "]"); + if (!aArray[i].isFolder && !aArray[i].isSeparator) { + do_print("testing testData[" + aArray[i].uri + "] vs result[" + child.uri + "]"); + if (aArray[i].uri != child.uri) { + dump_table("moz_places"); + do_throw("Expected " + aArray[i].uri + " found " + child.uri); + } + } + if (!aArray[i].isSeparator && aArray[i].title != child.title) + do_throw("Expected " + aArray[i].title + " found " + child.title); + if (aArray[i].hasOwnProperty("lastVisit") && + aArray[i].lastVisit != child.time) + do_throw("Expected " + aArray[i].lastVisit + " found " + child.time); + if (aArray[i].hasOwnProperty("index") && + aArray[i].index != PlacesUtils.bookmarks.DEFAULT_INDEX && + aArray[i].index != child.bookmarkIndex) + do_throw("Expected " + aArray[i].index + " found " + child.bookmarkIndex); + + inQueryIndex++; + } + } + + if (!wasOpen) + aRoot.containerOpen = false; + do_print("Comparing Array to Results passes"); +} + + +/** + * Helper function to check to see if one object either is or is not in the + * result set. It can accept either a queryData object or an array of queryData + * objects. If it gets an array, it only compares the first object in the array + * to see if it is in the result set. + * Returns: True if item is in query set, and false if item is not in query set + * If input is an array, returns True if FIRST object in array is in + * query set. To compare entire array, use the function above. + */ +function isInResult(aQueryData, aRoot) { + var rv = false; + var uri; + var wasOpen = aRoot.containerOpen; + if (!wasOpen) + aRoot.containerOpen = true; + + // If we have an array, pluck out the first item. If an object, pluc out the + // URI, we just compare URI's here. + if ("uri" in aQueryData) { + uri = aQueryData.uri; + } else { + uri = aQueryData[0].uri; + } + + for (var i=0; i < aRoot.childCount; i++) { + if (uri == aRoot.getChild(i).uri) { + rv = true; + break; + } + } + if (!wasOpen) + aRoot.containerOpen = false; + return rv; +} + + +/** + * A nice helper function for debugging things. It prints the contents of a + * result set. + */ +function displayResultSet(aRoot) { + + var wasOpen = aRoot.containerOpen; + if (!wasOpen) + aRoot.containerOpen = true; + + if (!aRoot.hasChildren) { + // Something wrong? Empty result set? + do_print("Result Set Empty"); + return; + } + + for (var i=0; i < aRoot.childCount; ++i) { + do_print("Result Set URI: " + aRoot.getChild(i).uri + " Title: " + + aRoot.getChild(i).title + " Visit Time: " + aRoot.getChild(i).time); + } + if (!wasOpen) + aRoot.containerOpen = false; +} diff --git a/toolkit/components/places/tests/queries/readme.txt b/toolkit/components/places/tests/queries/readme.txt new file mode 100644 index 000000000..19414f96e --- /dev/null +++ b/toolkit/components/places/tests/queries/readme.txt @@ -0,0 +1,16 @@ +These are tests specific to the Places Query API. + +We are tracking the coverage of these tests here: +http://wiki.mozilla.org/QA/TDAI/Projects/Places_Tests + +When creating one of these tests, you need to update those tables so that we +know how well our test coverage is of this area. Furthermore, when adding tests +ensure to cover live update (changing the query set) by performing the following +operations on the query set you get after running the query: +* Adding a new item to the query set +* Updating an existing item so that it matches the query set +* Change an existing item so that it does not match the query set +* Do multiple of the above inside an Update Batch transaction. +* Try these transactions in different orders. + +Use the stub test to help you create a test with the proper structure. diff --git a/toolkit/components/places/tests/queries/test_415716.js b/toolkit/components/places/tests/queries/test_415716.js new file mode 100644 index 000000000..754a73e7c --- /dev/null +++ b/toolkit/components/places/tests/queries/test_415716.js @@ -0,0 +1,108 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function modHistoryTypes(val) { + switch (val % 8) { + case 0: + case 1: + return TRANSITION_LINK; + case 2: + return TRANSITION_TYPED; + case 3: + return TRANSITION_BOOKMARK; + case 4: + return TRANSITION_EMBED; + case 5: + return TRANSITION_REDIRECT_PERMANENT; + case 6: + return TRANSITION_REDIRECT_TEMPORARY; + case 7: + return TRANSITION_DOWNLOAD; + case 8: + return TRANSITION_FRAMED_LINK; + } + return TRANSITION_TYPED; +} + +function run_test() +{ + run_next_test(); +} + +/** + * Builds a test database by hand using various times, annotations and + * visit numbers for this test + */ +add_task(function* test_buildTestDatabase() +{ + // This is the set of visits that we will match - our min visit is 2 so that's + // why we add more visits to the same URIs. + let testURI = uri("http://www.foo.com"); + let places = []; + + for (let i = 0; i < 12; ++i) { + places.push({ + uri: testURI, + transition: modHistoryTypes(i), + visitDate: today + }); + } + + testURI = uri("http://foo.com/youdontseeme.html"); + let testAnnoName = "moz-test-places/testing123"; + let testAnnoVal = "test"; + for (let i = 0; i < 12; ++i) { + places.push({ + uri: testURI, + transition: modHistoryTypes(i), + visitDate: today + }); + } + + yield PlacesTestUtils.addVisits(places); + + PlacesUtils.annotations.setPageAnnotation(testURI, testAnnoName, + testAnnoVal, 0, 0); +}); + +/** + * This test will test Queries that use relative Time Range, minVists, maxVisits, + * annotation. + * The Query: + * Annotation == "moz-test-places/testing123" && + * TimeRange == "now() - 2d" && + * minVisits == 2 && + * maxVisits == 10 + */ +add_task(function test_execute() +{ + let query = PlacesUtils.history.getNewQuery(); + query.annotation = "moz-test-places/testing123"; + query.beginTime = daybefore * 1000; + query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_NOW; + query.endTime = today * 1000; + query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_NOW; + query.minVisits = 2; + query.maxVisits = 10; + + // Options + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + + // Results + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + let cc = root.childCount; + dump("----> cc is: " + cc + "\n"); + for (let i = 0; i < root.childCount; ++i) { + let resultNode = root.getChild(i); + let accesstime = Date(resultNode.time / 1000); + dump("----> result: " + resultNode.uri + " Date: " + accesstime.toLocaleString() + "\n"); + } + do_check_eq(cc, 0); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js b/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js new file mode 100644 index 000000000..199fc0865 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js @@ -0,0 +1,210 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const DAY_MSEC = 86400000; +const MIN_MSEC = 60000; +const HOUR_MSEC = 3600000; +// Jan 6 2008 at 8am is our begin edge of the query +var beginTimeDate = new Date(2008, 0, 6, 8, 0, 0, 0); +// Jan 15 2008 at 9:30pm is our ending edge of the query +var endTimeDate = new Date(2008, 0, 15, 21, 30, 0, 0); + +// These as millisecond values +var beginTime = beginTimeDate.getTime(); +var endTime = endTimeDate.getTime(); + +// Some range dates inside our query - mult by 1000 to convert to PRTIME +var jan7_800 = (beginTime + DAY_MSEC) * 1000; +var jan6_815 = (beginTime + (MIN_MSEC * 15)) * 1000; +var jan11_800 = (beginTime + (DAY_MSEC * 5)) * 1000; +var jan14_2130 = (endTime - DAY_MSEC) * 1000; +var jan15_2045 = (endTime - (MIN_MSEC * 45)) * 1000; +var jan12_1730 = (endTime - (DAY_MSEC * 3) - (HOUR_MSEC*4)) * 1000; + +// Dates outside our query - mult by 1000 to convert to PRTIME +var jan6_700 = (beginTime - HOUR_MSEC) * 1000; +var jan5_800 = (beginTime - DAY_MSEC) * 1000; +var dec27_800 = (beginTime - (DAY_MSEC * 10)) * 1000; +var jan15_2145 = (endTime + (MIN_MSEC * 15)) * 1000; +var jan16_2130 = (endTime + (DAY_MSEC)) * 1000; +var jan25_2130 = (endTime + (DAY_MSEC * 10)) * 1000; + +// So that we can easily use these too, convert them to PRTIME +beginTime *= 1000; +endTime *= 1000; + +/** + * Array of objects to build our test database + */ +var goodAnnoName = "moz-test-places/testing123"; +var val = "test"; +var badAnnoName = "text/foo"; + +// The test data for our database, note that the ordering of the results that +// will be returned by the query (the isInQuery: true objects) is IMPORTANT. +// see compareArrayToResult in head_queries.js for more info. +var testData = [ + // Test ftp protocol - vary the title length + {isInQuery: true, isVisit: true, isDetails: true, + uri: "ftp://foo.com/ftp", lastVisit: jan12_1730, + title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"}, + + // Test flat domain with annotation + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://foo.com/", annoName: goodAnnoName, annoVal: val, + lastVisit: jan14_2130, title: "moz"}, + + // Test subdomain included with isRedirect=true, different transtype + {isInQuery: true, isVisit: true, isDetails: true, title: "moz", + isRedirect: true, uri: "http://mail.foo.com/redirect", lastVisit: jan11_800, + transType: PlacesUtils.history.TRANSITION_LINK}, + + // Test subdomain inclued at the leading time edge + {isInQuery: true, isVisit: true, isDetails: true, + uri: "http://mail.foo.com/yiihah", title: "moz", lastVisit: jan6_815}, + + // Test www. style URI is included, with an annotation + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://www.foo.com/yiihah", annoName: goodAnnoName, annoVal: val, + lastVisit: jan7_800, title: "moz"}, + + // Test https protocol + {isInQuery: true, isVisit: true, isDetails: true, title: "moz", + uri: "https://foo.com/", lastVisit: jan15_2045}, + + // Test begin edge of time + {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla", + uri: "https://foo.com/begin.html", lastVisit: beginTime}, + + // Test end edge of time + {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla", + uri: "https://foo.com/end.html", lastVisit: endTime}, + + // Test an image link, with annotations + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + title: "mozzie the dino", uri: "https://foo.com/mozzie.png", + annoName: goodAnnoName, annoVal: val, lastVisit: jan14_2130}, + + // Begin the invalid queries: Test too early + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://foo.com/tooearly.php", lastVisit: jan6_700}, + + // Test Bad Annotation + {isInQuery: false, isVisit:true, isDetails: true, isPageAnnotation: true, + title: "moz", uri: "http://foo.com/badanno.htm", lastVisit: jan12_1730, + annoName: badAnnoName, annoVal: val}, + + // Test bad URI + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://somefoo.com/justwrong.htm", lastVisit: jan11_800}, + + // Test afterward, one to update + {isInQuery: false, isVisit:true, isDetails: true, title: "changeme", + uri: "http://foo.com/changeme1.htm", lastVisit: jan12_1730}, + + // Test invalid title + {isInQuery: false, isVisit:true, isDetails: true, title: "changeme2", + uri: "http://foo.com/changeme2.htm", lastVisit: jan7_800}, + + // Test changing the lastVisit + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://foo.com/changeme3.htm", lastVisit: dec27_800}]; + +/** + * This test will test a Query using several terms and do a bit of negative + * testing for items that should be ignored while querying over history. + * The Query:WHERE absoluteTime(matches) AND searchTerms AND URI + * AND annotationIsNot(match) GROUP BY Domain, Day SORT BY uri,ascending + * excludeITems(should be ignored) + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_abstime_annotation_domain() +{ + // Initialize database + yield task_populateDB(testData); + + // Query + var query = PlacesUtils.history.getNewQuery(); + query.beginTime = beginTime; + query.endTime = endTime; + query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH; + query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH; + query.searchTerms = "moz"; + query.domain = "foo.com"; + query.domainIsHost = false; + query.annotation = "text/foo"; + query.annotationIsNot = true; + + // Options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_URI_ASCENDING; + options.resultType = options.RESULTS_AS_URI; + // The next two options should be ignored + // can't use this one, breaks test - bug 419779 + // options.excludeItems = true; + + // Results + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // Ensure the result set is correct + compareArrayToResult(testData, root); + + // Make some changes to the result set + // Let's add something first + var addItem = [{isInQuery: true, isVisit: true, isDetails: true, title: "moz", + uri: "http://www.foo.com/i-am-added.html", lastVisit: jan11_800}]; + yield task_populateDB(addItem); + do_print("Adding item foo.com/i-am-added.html"); + do_check_eq(isInResult(addItem, root), true); + + // Let's update something by title + var change1 = [{isDetails: true, uri: "http://foo.com/changeme1", + lastVisit: jan12_1730, title: "moz moz mozzie"}]; + yield task_populateDB(change1); + do_print("LiveUpdate by changing title"); + do_check_eq(isInResult(change1, root), true); + + // Let's update something by annotation + // Updating a page by removing an annotation does not cause it to join this + // query set. I tend to think that it should cause that page to join this + // query set, because this visit fits all theother specified criteria once the + // annotation is removed. Uncommenting this will fail the test. + // Bug 424050 + /* var change2 = [{isPageAnnotation: true, uri: "http://foo.com/badannotaion.html", + annoName: "text/mozilla", annoVal: "test"}]; + yield task_populateDB(change2); + do_print("LiveUpdate by removing annotation"); + do_check_eq(isInResult(change2, root), true);*/ + + // Let's update by adding a visit in the time range for an existing URI + var change3 = [{isDetails: true, uri: "http://foo.com/changeme3.htm", + title: "moz", lastVisit: jan15_2045}]; + yield task_populateDB(change3); + do_print("LiveUpdate by adding visit within timerange"); + do_check_eq(isInResult(change3, root), true); + + // And delete something from the result set - using annotation + // Once again, bug 424050 prevents this from passing + /* var change4 = [{isPageAnnotation: true, uri: "ftp://foo.com/ftp", + annoVal: "test", annoName: badAnnoName}]; + yield task_populateDB(change4); + do_print("LiveUpdate by deleting item from set by adding annotation"); + do_check_eq(isInResult(change4, root), false);*/ + + // Delete something by changing the title + var change5 = [{isDetails: true, uri: "http://foo.com/end.html", title: "deleted"}]; + yield task_populateDB(change5); + do_print("LiveUpdate by deleting item by changing title"); + do_check_eq(isInResult(change5, root), false); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js new file mode 100644 index 000000000..145d2cb59 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js @@ -0,0 +1,162 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const DAY_MSEC = 86400000; +const MIN_MSEC = 60000; +const HOUR_MSEC = 3600000; +// Jan 6 2008 at 8am is our begin edge of the query +var beginTimeDate = new Date(2008, 0, 6, 8, 0, 0, 0); +// Jan 15 2008 at 9:30pm is our ending edge of the query +var endTimeDate = new Date(2008, 0, 15, 21, 30, 0, 0); + +// These as millisecond values +var beginTime = beginTimeDate.getTime(); +var endTime = endTimeDate.getTime(); + +// Some range dates inside our query - mult by 1000 to convert to PRTIME +var jan7_800 = (beginTime + DAY_MSEC) * 1000; +var jan6_815 = (beginTime + (MIN_MSEC * 15)) * 1000; +var jan11_800 = (beginTime + (DAY_MSEC * 5)) * 1000; +var jan14_2130 = (endTime - DAY_MSEC) * 1000; +var jan15_2045 = (endTime - (MIN_MSEC * 45)) * 1000; +var jan12_1730 = (endTime - (DAY_MSEC * 3) - (HOUR_MSEC*4)) * 1000; + +// Dates outside our query - mult by 1000 to convert to PRTIME +var jan6_700 = (beginTime - HOUR_MSEC) * 1000; +var jan5_800 = (beginTime - DAY_MSEC) * 1000; +var dec27_800 = (beginTime - (DAY_MSEC * 10)) * 1000; +var jan15_2145 = (endTime + (MIN_MSEC * 15)) * 1000; +var jan16_2130 = (endTime + (DAY_MSEC)) * 1000; +var jan25_2130 = (endTime + (DAY_MSEC * 10)) * 1000; + +// So that we can easily use these too, convert them to PRTIME +beginTime *= 1000; +endTime *= 1000; + +/** + * Array of objects to build our test database + */ +var goodAnnoName = "moz-test-places/testing123"; +var val = "test"; +var badAnnoName = "text/foo"; + +// The test data for our database, note that the ordering of the results that +// will be returned by the query (the isInQuery: true objects) is IMPORTANT. +// see compareArrayToResult in head_queries.js for more info. +var testData = [ + + // Test flat domain with annotation + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://foo.com/", annoName: goodAnnoName, annoVal: val, + lastVisit: jan14_2130, title: "moz"}, + + // Begin the invalid queries: + // Test www. style URI is not included, with an annotation + {isInQuery: false, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://www.foo.com/yiihah", annoName: goodAnnoName, annoVal: val, + lastVisit: jan7_800, title: "moz"}, + + // Test subdomain not inclued at the leading time edge + {isInQuery: false, isVisit: true, isDetails: true, + uri: "http://mail.foo.com/yiihah", title: "moz", lastVisit: jan6_815}, + + // Test https protocol + {isInQuery: false, isVisit: true, isDetails: true, title: "moz", + uri: "https://foo.com/", lastVisit: jan15_2045}, + + // Test ftp protocol + {isInQuery: false, isVisit: true, isDetails: true, + uri: "ftp://foo.com/ftp", lastVisit: jan12_1730, + title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"}, + + // Test too early + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://foo.com/tooearly.php", lastVisit: jan6_700}, + + // Test Bad Annotation + {isInQuery: false, isVisit:true, isDetails: true, isPageAnnotation: true, + title: "moz", uri: "http://foo.com/badanno.htm", lastVisit: jan12_1730, + annoName: badAnnoName, annoVal: val}, + + // Test afterward, one to update + {isInQuery: false, isVisit:true, isDetails: true, title: "changeme", + uri: "http://foo.com/changeme1.htm", lastVisit: jan12_1730}, + + // Test invalid title + {isInQuery: false, isVisit:true, isDetails: true, title: "changeme2", + uri: "http://foo.com/changeme2.htm", lastVisit: jan7_800}, + + // Test changing the lastVisit + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://foo.com/changeme3.htm", lastVisit: dec27_800}]; + +/** + * This test will test a Query using several terms and do a bit of negative + * testing for items that should be ignored while querying over history. + * The Query:WHERE absoluteTime(matches) AND searchTerms AND URI + * AND annotationIsNot(match) GROUP BY Domain, Day SORT BY uri,ascending + * excludeITems(should be ignored) + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_abstime_annotation_uri() +{ + // Initialize database + yield task_populateDB(testData); + + // Query + var query = PlacesUtils.history.getNewQuery(); + query.beginTime = beginTime; + query.endTime = endTime; + query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH; + query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH; + query.searchTerms = "moz"; + query.uri = uri("http://foo.com"); + query.annotation = "text/foo"; + query.annotationIsNot = true; + + // Options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_URI_ASCENDING; + options.resultType = options.RESULTS_AS_URI; + // The next two options should be ignored + // can't use this one, breaks test - bug 419779 + // options.excludeItems = true; + + // Results + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // Ensure the result set is correct + compareArrayToResult(testData, root); + + // live update. + do_print("change title"); + var change1 = [{isDetails: true, uri:"http://foo.com/", + title: "mo"}, ]; + yield task_populateDB(change1); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); + + var change2 = [{isDetails: true, uri:"http://foo.com/", + title: "moz", lastvisit: endTime}, ]; + yield task_populateDB(change2); + dump_table("moz_places"); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); + + // Let's delete something from the result set - using annotation + var change3 = [{isPageAnnotation: true, + uri: "http://foo.com/", + annoName: badAnnoName, annoVal: "test"}]; + yield task_populateDB(change3); + do_print("LiveUpdate by removing annotation"); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_async.js b/toolkit/components/places/tests/queries/test_async.js new file mode 100644 index 000000000..0ec99f8fc --- /dev/null +++ b/toolkit/components/places/tests/queries/test_async.js @@ -0,0 +1,371 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = [ + { + desc: "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " + + "close container with a single child", + + loading: function (node, newState, oldState) { + this.checkStateChanged("loading", 1); + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); + }, + + opened: function (node, newState, oldState) { + this.checkStateChanged("opened", 1); + this.checkState("loading", 1); + this.checkArgs("opened", node, oldState, node.STATE_LOADING); + + print("Checking node children"); + compareArrayToResult(this.data, node); + + print("Closing container"); + node.containerOpen = false; + }, + + closed: function (node, newState, oldState) { + this.checkStateChanged("closed", 1); + this.checkState("opened", 1); + this.checkArgs("closed", node, oldState, node.STATE_OPENED); + this.success(); + } + }, + + { + desc: "nsNavHistoryFolderResultNode: After async open and no changes, " + + "second open should be synchronous", + + loading: function (node, newState, oldState) { + this.checkStateChanged("loading", 1); + this.checkState("closed", 0); + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); + }, + + opened: function (node, newState, oldState) { + let cnt = this.checkStateChanged("opened", 1, 2); + let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED; + this.checkArgs("opened", node, oldState, expectOldState); + + print("Checking node children"); + compareArrayToResult(this.data, node); + + print("Closing container"); + node.containerOpen = false; + }, + + closed: function (node, newState, oldState) { + let cnt = this.checkStateChanged("closed", 1, 2); + this.checkArgs("closed", node, oldState, node.STATE_OPENED); + + switch (cnt) { + case 1: + node.containerOpen = true; + break; + case 2: + this.success(); + break; + } + } + }, + + { + desc: "nsNavHistoryFolderResultNode: After closing container in " + + "loading(), opened() should not be called", + + loading: function (node, newState, oldState) { + this.checkStateChanged("loading", 1); + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); + print("Closing container"); + node.containerOpen = false; + }, + + opened: function (node, newState, oldState) { + do_throw("opened should not be called"); + }, + + closed: function (node, newState, oldState) { + this.checkStateChanged("closed", 1); + this.checkState("loading", 1); + this.checkArgs("closed", node, oldState, node.STATE_LOADING); + this.success(); + } + } +]; + + +/** + * Instances of this class become the prototypes of the test objects above. + * Each test can therefore use the methods of this class, or they can override + * them if they want. To run a test, call setup() and then run(). + */ +function Test() { + // This maps a state name to the number of times it's been observed. + this.stateCounts = {}; + // Promise object resolved when the next test can be run. + this.deferNextTest = Promise.defer(); +} + +Test.prototype = { + /** + * Call this when an observer observes a container state change to sanity + * check the arguments. + * + * @param aNewState + * The name of the new state. Used only for printing out helpful info. + * @param aNode + * The node argument passed to containerStateChanged. + * @param aOldState + * The old state argument passed to containerStateChanged. + * @param aExpectOldState + * The expected old state. + */ + checkArgs: function (aNewState, aNode, aOldState, aExpectOldState) { + print("Node passed on " + aNewState + " should be result.root"); + do_check_eq(this.result.root, aNode); + print("Old state passed on " + aNewState + " should be " + aExpectOldState); + + // aOldState comes from xpconnect and will therefore be defined. It may be + // zero, though, so use strict equality just to make sure aExpectOldState is + // also defined. + do_check_true(aOldState === aExpectOldState); + }, + + /** + * Call this when an observer observes a container state change. It registers + * the state change and ensures that it has been observed the given number + * of times. See checkState for parameter explanations. + * + * @return The number of times aState has been observed, including the new + * observation. + */ + checkStateChanged: function (aState, aExpectedMin, aExpectedMax) { + print(aState + " state change observed"); + if (!this.stateCounts.hasOwnProperty(aState)) + this.stateCounts[aState] = 0; + this.stateCounts[aState]++; + return this.checkState(aState, aExpectedMin, aExpectedMax); + }, + + /** + * Ensures that the state has been observed the given number of times. + * + * @param aState + * The name of the state. + * @param aExpectedMin + * The state must have been observed at least this number of times. + * @param aExpectedMax + * The state must have been observed at most this number of times. + * This parameter is optional. If undefined, it's set to + * aExpectedMin. + * @return The number of times aState has been observed, including the new + * observation. + */ + checkState: function (aState, aExpectedMin, aExpectedMax) { + let cnt = this.stateCounts[aState] || 0; + if (aExpectedMax === undefined) + aExpectedMax = aExpectedMin; + if (aExpectedMin === aExpectedMax) { + print(aState + " should be observed only " + aExpectedMin + + " times (actual = " + cnt + ")"); + } + else { + print(aState + " should be observed at least " + aExpectedMin + + " times and at most " + aExpectedMax + " times (actual = " + + cnt + ")"); + } + do_check_true(cnt >= aExpectedMin && cnt <= aExpectedMax); + return cnt; + }, + + /** + * Asynchronously opens the root of the test's result. + */ + openContainer: function () { + // Set up the result observer. It delegates to this object's callbacks and + // wraps them in a try-catch so that errors don't get eaten. + let self = this; + this.observer = { + containerStateChanged: function (container, oldState, newState) { + print("New state passed to containerStateChanged() should equal the " + + "container's current state"); + do_check_eq(newState, container.state); + + try { + switch (newState) { + case Ci.nsINavHistoryContainerResultNode.STATE_LOADING: + self.loading(container, newState, oldState); + break; + case Ci.nsINavHistoryContainerResultNode.STATE_OPENED: + self.opened(container, newState, oldState); + break; + case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED: + self.closed(container, newState, oldState); + break; + default: + do_throw("Unexpected new state! " + newState); + } + } + catch (err) { + do_throw(err); + } + }, + }; + this.result.addObserver(this.observer, false); + + print("Opening container"); + this.result.root.containerOpen = true; + }, + + /** + * Starts the test and returns a promise resolved when the test completes. + */ + run: function () { + this.openContainer(); + return this.deferNextTest.promise; + }, + + /** + * This must be called before run(). It adds a bookmark and sets up the + * test's result. Override if need be. + */ + setup: function*() { + // Populate the database with different types of bookmark items. + this.data = DataHelper.makeDataArray([ + { type: "bookmark" }, + { type: "separator" }, + { type: "folder" }, + { type: "bookmark", uri: "place:terms=foo" } + ]); + yield task_populateDB(this.data); + + // Make a query. + this.query = PlacesUtils.history.getNewQuery(); + this.query.setFolders([DataHelper.defaults.bookmark.parent], 1); + this.opts = PlacesUtils.history.getNewQueryOptions(); + this.opts.asyncEnabled = true; + this.result = PlacesUtils.history.executeQuery(this.query, this.opts); + }, + + /** + * Call this when the test has succeeded. It cleans up resources and starts + * the next test. + */ + success: function () { + this.result.removeObserver(this.observer); + + // Resolve the promise object that indicates that the next test can be run. + this.deferNextTest.resolve(); + } +}; + +/** + * This makes it a little bit easier to use the functions of head_queries.js. + */ +var DataHelper = { + defaults: { + bookmark: { + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + uri: "http://example.com/", + title: "test bookmark" + }, + + folder: { + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test folder" + }, + + separator: { + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + } + }, + + /** + * Converts an array of simple bookmark item descriptions to the more verbose + * format required by task_populateDB() in head_queries.js. + * + * @param aData + * An array of objects, each of which describes a bookmark item. + * @return An array of objects suitable for passing to populateDB(). + */ + makeDataArray: function DH_makeDataArray(aData) { + let self = this; + return aData.map(function (dat) { + let type = dat.type; + dat = self._makeDataWithDefaults(dat, self.defaults[type]); + switch (type) { + case "bookmark": + return { + isBookmark: true, + uri: dat.uri, + parentGuid: dat.parentGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: dat.title, + isInQuery: true + }; + case "separator": + return { + isSeparator: true, + parentGuid: dat.parentGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + isInQuery: true + }; + case "folder": + return { + isFolder: true, + parentGuid: dat.parentGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: dat.title, + isInQuery: true + }; + default: + do_throw("Unknown data type when populating DB: " + type); + return undefined; + } + }); + }, + + /** + * Returns a copy of aData, except that any properties that are undefined but + * defined in aDefaults are set to the corresponding values in aDefaults. + * + * @param aData + * An object describing a bookmark item. + * @param aDefaults + * An object describing the default bookmark item. + * @return A copy of aData with defaults values set. + */ + _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) { + let dat = {}; + for (let [prop, val] of Object.entries(aDefaults)) { + dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val; + } + return dat; + } +}; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_async() +{ + for (let test of tests) { + yield PlacesUtils.bookmarks.eraseEverything(); + + test.__proto__ = new Test(); + yield test.setup(); + + print("------ Running test: " + test.desc); + yield test.run(); + } + + yield PlacesUtils.bookmarks.eraseEverything(); + print("All tests done, exiting"); +}); diff --git a/toolkit/components/places/tests/queries/test_containersQueries_sorting.js b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js new file mode 100644 index 000000000..ab9f2bf90 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js @@ -0,0 +1,411 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Testing behavior of bug 473157 + * "Want to sort history in container view without sorting the containers" + * and regression bug 488783 + * Tags list no longer sorted (alphabetized). + * This test is for global testing sorting containers queries. + */ + +// Globals and Constants + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bh = hs.QueryInterface(Ci.nsIBrowserHistory); +var tagging = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + +var resultTypes = [ + {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY, name: "RESULTS_AS_DATE_QUERY"}, + {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY, name: "RESULTS_AS_SITE_QUERY"}, + {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY, name: "RESULTS_AS_DATE_SITE_QUERY"}, + {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY, name: "RESULTS_AS_TAG_QUERY"}, +]; + +var sortingModes = [ + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING, name: "SORT_BY_TITLE_ASCENDING"}, + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING, name: "SORT_BY_TITLE_DESCENDING"}, + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING, name: "SORT_BY_DATE_ASCENDING"}, + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, name: "SORT_BY_DATE_DESCENDING"}, + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING, name: "SORT_BY_DATEADDED_ASCENDING"}, + {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING, name: "SORT_BY_DATEADDED_DESCENDING"}, +]; + +// These pages will be added from newest to oldest and from less visited to most +// visited. +var pages = [ + "http://www.mozilla.org/c/", + "http://www.mozilla.org/a/", + "http://www.mozilla.org/b/", + "http://www.mozilla.com/c/", + "http://www.mozilla.com/a/", + "http://www.mozilla.com/b/", +]; + +var tags = [ + "mozilla", + "Development", + "test", +]; + +// Test Runner + +/** + * Enumerates all the sequences of the cartesian product of the arrays contained + * in aSequences. Examples: + * + * cartProd([[1, 2, 3], ["a", "b"]], callback); + * // callback is called 3 * 2 = 6 times with the following arrays: + * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"] + * + * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback); + * // callback is called 1 * 3 * 2 = 6 times with the following arrays: + * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"], + * // ["a", 3, "X"], ["a", 3, "Y"] + * + * cartProd([[1], [2], [3], [4]], callback); + * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array: + * // [1, 2, 3, 4] + * + * cartProd([], callback); + * // callback is 0 times + * + * cartProd([[1, 2, 3, 4]], callback); + * // callback is called 4 times with the following arrays: + * // [1], [2], [3], [4] + * + * @param aSequences + * an array that contains an arbitrary number of arrays + * @param aCallback + * a function that is passed each sequence of the product as it's + * computed + * @return the total number of sequences in the product + */ +function cartProd(aSequences, aCallback) +{ + if (aSequences.length === 0) + return 0; + + // For each sequence in aSequences, we maintain a pointer (an array index, + // really) to the element we're currently enumerating in that sequence + var seqEltPtrs = aSequences.map(i => 0); + + var numProds = 0; + var done = false; + while (!done) { + numProds++; + + // prod = sequence in product we're currently enumerating + var prod = []; + for (var i = 0; i < aSequences.length; i++) { + prod.push(aSequences[i][seqEltPtrs[i]]); + } + aCallback(prod); + + // The next sequence in the product differs from the current one by just a + // single element. Determine which element that is. We advance the + // "rightmost" element pointer to the "right" by one. If we move past the + // end of that pointer's sequence, reset the pointer to the first element + // in its sequence and then try the sequence to the "left", and so on. + + // seqPtr = index of rightmost input sequence whose element pointer is not + // past the end of the sequence + var seqPtr = aSequences.length - 1; + while (!done) { + // Advance the rightmost element pointer. + seqEltPtrs[seqPtr]++; + + // The rightmost element pointer is past the end of its sequence. + if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) { + seqEltPtrs[seqPtr] = 0; + seqPtr--; + + // All element pointers are past the ends of their sequences. + if (seqPtr < 0) + done = true; + } + else break; + } + } + return numProds; +} + +/** + * Test a query based on passed-in options. + * + * @param aSequence + * array of options we will use to query. + */ +function test_query_callback(aSequence) { + do_check_eq(aSequence.length, 2); + var resultType = aSequence[0]; + var sortingMode = aSequence[1]; + print("\n\n*** Testing default sorting for resultType (" + resultType.name + ") and sortingMode (" + sortingMode.name + ")"); + + // Skip invalid combinations sorting queries by none. + if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY && + (sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING || + sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) { + // This is a bookmark query, we can't sort by visit date. + sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + } + if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY || + resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY || + resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) { + // This is an history query, we can't sort by date added. + if (sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING || + sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING) + sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + } + + // Create a new query with required options. + var query = PlacesUtils.history.getNewQuery(); + var options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = resultType.value; + options.sortingMode = sortingMode.value; + + // Compare resultset with expectedData. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY || + resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) { + // Date containers are always sorted by date descending. + check_children_sorting(root, + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + } + else + check_children_sorting(root, sortingMode.value); + + // Now Check sorting of the first child container. + var container = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + container.containerOpen = true; + + if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) { + // Has more than one level of containers, first we check the sorting of + // the first level (site containers), those won't inherit sorting... + check_children_sorting(container, + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); + // ...then we check sorting of the contained urls, we can't inherit sorting + // since the above level does not inherit it, so they will be sorted by + // title ascending. + let innerContainer = container.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + innerContainer.containerOpen = true; + check_children_sorting(innerContainer, + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); + innerContainer.containerOpen = false; + } + else if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) { + // Sorting mode for tag contents is hardcoded for now, to allow for faster + // duplicates filtering. + check_children_sorting(container, + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE); + } + else + check_children_sorting(container, sortingMode.value); + + container.containerOpen = false; + root.containerOpen = false; + + test_result_sortingMode_change(result, resultType, sortingMode); +} + +/** + * Sets sortingMode on aResult and checks for correct sorting of children. + * Containers should not change their sorting, while contained uri nodes should. + * + * @param aResult + * nsINavHistoryResult generated by our query. + * @param aResultType + * required result type. + * @param aOriginalSortingMode + * the sorting mode from query's options. + */ +function test_result_sortingMode_change(aResult, aResultType, aOriginalSortingMode) { + var root = aResult.root; + // Now we set sortingMode on the result and check that containers are not + // sorted while children are. + sortingModes.forEach(function sortingModeChecker(aForcedSortingMode) { + print("\n* Test setting sortingMode (" + aForcedSortingMode.name + ") " + + "on result with resultType (" + aResultType.name + ") " + + "currently sorted as (" + aOriginalSortingMode.name + ")"); + + aResult.sortingMode = aForcedSortingMode.value; + root.containerOpen = true; + + if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY || + aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) { + // Date containers are always sorted by date descending. + check_children_sorting(root, + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + } + else if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY && + (aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING || + aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) { + // Site containers don't have a good time property to sort by. + check_children_sorting(root, + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE); + } + else + check_children_sorting(root, aOriginalSortingMode.value); + + // Now Check sorting of the first child container. + var container = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + container.containerOpen = true; + + if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) { + // Has more than one level of containers, first we check the sorting of + // the first level (site containers), those won't be sorted... + check_children_sorting(container, + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); + // ...then we check sorting of the second level of containers, result + // will sort them through recursiveSort. + let innerContainer = container.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + innerContainer.containerOpen = true; + check_children_sorting(innerContainer, aForcedSortingMode.value); + innerContainer.containerOpen = false; + } + else { + if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY || + aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY || + aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY) { + // Date containers are always sorted by date descending. + check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE); + } + else if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY && + (aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING || + aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) { + // Site containers don't have a good time property to sort by. + check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE); + } + else + check_children_sorting(root, aOriginalSortingMode.value); + + // Children should always be sorted. + check_children_sorting(container, aForcedSortingMode.value); + } + + container.containerOpen = false; + root.containerOpen = false; + }); +} + +/** + * Test if children of aRootNode are correctly sorted. + * @param aRootNode + * already opened root node from our query's result. + * @param aExpectedSortingMode + * The sortingMode we expect results to be. + */ +function check_children_sorting(aRootNode, aExpectedSortingMode) { + var results = []; + print("Found children:"); + for (let i = 0; i < aRootNode.childCount; i++) { + results[i] = aRootNode.getChild(i); + print(i + " " + results[i].title); + } + + // Helper for case insensitive string comparison. + function caseInsensitiveStringComparator(a, b) { + var aLC = a.toLowerCase(); + var bLC = b.toLowerCase(); + if (aLC < bLC) + return -1; + if (aLC > bLC) + return 1; + return 0; + } + + // Get a comparator based on expected sortingMode. + var comparator; + switch (aExpectedSortingMode) { + case Ci.nsINavHistoryQueryOptions.SORT_BY_NONE: + comparator = function (a, b) { + return 0; + } + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING: + comparator = function (a, b) { + return caseInsensitiveStringComparator(a.title, b.title); + } + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING: + comparator = function (a, b) { + return -caseInsensitiveStringComparator(a.title, b.title); + } + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING: + comparator = function (a, b) { + return a.time - b.time; + } + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING: + comparator = function (a, b) { + return b.time - a.time; + } + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING: + comparator = function (a, b) { + return a.dateAdded - b.dateAdded; + } + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING: + comparator = function (a, b) { + return b.dateAdded - a.dateAdded; + } + break; + default: + do_throw("Unknown sorting type: " + aExpectedSortingMode); + } + + // Make an independent copy of the results array and sort it. + var sortedResults = results.slice(); + sortedResults.sort(comparator); + // Actually compare returned children with our sorted array. + for (let i = 0; i < sortedResults.length; i++) { + if (sortedResults[i].title != results[i].title) + print(i + " index wrong, expected " + sortedResults[i].title + + " found " + results[i].title); + do_check_eq(sortedResults[i].title, results[i].title); + } +} + +// Main + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_containersQueries_sorting() +{ + // Add visits, bookmarks and tags to our database. + var timeInMilliseconds = Date.now(); + var visitCount = 0; + var dayOffset = 0; + var visits = []; + pages.forEach(aPageUrl => visits.push( + { isVisit: true, + isBookmark: true, + transType: Ci.nsINavHistoryService.TRANSITION_TYPED, + uri: aPageUrl, + title: aPageUrl, + // subtract 5 hours per iteration, to expose more than one day container. + lastVisit: (timeInMilliseconds - (18000 * 1000 * dayOffset++)) * 1000, + visitCount: visitCount++, + isTag: true, + tagArray: tags, + isInQuery: true })); + yield task_populateDB(visits); + + cartProd([resultTypes, sortingModes], test_query_callback); +}); diff --git a/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js new file mode 100644 index 000000000..fbbacf6c9 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that tags changes are correctly live-updated in a history +// query. + +let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000); + +function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; +} + +var gTestData = [ + { + isVisit: true, + uri: "http://example.com/1/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "example1", + }, + { + isVisit: true, + uri: "http://example.com/2/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "example2", + }, + { + isVisit: true, + uri: "http://example.com/3/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "example3", + }, +]; + +function newQueryWithOptions() +{ + return [ PlacesUtils.history.getNewQuery(), + PlacesUtils.history.getNewQueryOptions() ]; +} + +function testQueryContents(aQuery, aOptions, aCallback) +{ + let root = PlacesUtils.history.executeQuery(aQuery, aOptions).root; + root.containerOpen = true; + aCallback(root); + root.containerOpen = false; +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_initialize() +{ + yield task_populateDB(gTestData); +}); + +add_task(function pages_query() +{ + let [query, options] = newQueryWithOptions(); + testQueryContents(query, options, function (root) { + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.tags, null); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + do_check_eq(node.tags, "test-tag"); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + do_check_eq(node.tags, null); + } + }); +}); + +add_task(function visits_query() +{ + let [query, options] = newQueryWithOptions(); + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + testQueryContents(query, options, function (root) { + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.tags, null); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + do_check_eq(node.tags, "test-tag"); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + do_check_eq(node.tags, null); + } + }); +}); + +add_task(function bookmarks_query() +{ + let [query, options] = newQueryWithOptions(); + query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1); + testQueryContents(query, options, function (root) { + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.tags, null); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + do_check_eq(node.tags, "test-tag"); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + do_check_eq(node.tags, null); + } + }); +}); + +add_task(function pages_searchterm_query() +{ + let [query, options] = newQueryWithOptions(); + query.searchTerms = "example"; + testQueryContents(query, options, function (root) { + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.tags, null); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + do_check_eq(node.tags, "test-tag"); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + do_check_eq(node.tags, null); + } + }); +}); + +add_task(function visits_searchterm_query() +{ + let [query, options] = newQueryWithOptions(); + query.searchTerms = "example"; + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + testQueryContents(query, options, function (root) { + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.tags, null); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + do_check_eq(node.tags, "test-tag"); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + do_check_eq(node.tags, null); + } + }); +}); + +add_task(function pages_searchterm_is_tag_query() +{ + let [query, options] = newQueryWithOptions(); + query.searchTerms = "test-tag"; + testQueryContents(query, options, function (root) { + compareArrayToResult([], root); + gTestData.forEach(function (data) { + let uri = NetUtil.newURI(data.uri); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + data.title); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + compareArrayToResult([data], root); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + compareArrayToResult([], root); + }); + }); +}); + +add_task(function visits_searchterm_is_tag_query() +{ + let [query, options] = newQueryWithOptions(); + query.searchTerms = "test-tag"; + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + testQueryContents(query, options, function (root) { + compareArrayToResult([], root); + gTestData.forEach(function (data) { + let uri = NetUtil.newURI(data.uri); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + data.title); + PlacesUtils.tagging.tagURI(uri, ["test-tag"]); + compareArrayToResult([data], root); + PlacesUtils.tagging.untagURI(uri, ["test-tag"]); + compareArrayToResult([], root); + }); + }); +}); diff --git a/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js new file mode 100644 index 000000000..eec87fe0e --- /dev/null +++ b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that tags changes are correctly live-updated in a history +// query. + +let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000); + +function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; +} + +var gTestData = [ + { + isVisit: true, + uri: "http://example.com/1/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + title: "title1", + }, + { + isVisit: true, + uri: "http://example.com/2/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + title: "title2", + }, + { + isVisit: true, + uri: "http://example.com/3/", + lastVisit: newTimeInMicroseconds(), + isInQuery: true, + title: "title3", + }, +]; + +function searchNodeHavingUrl(aRoot, aUrl) { + for (let i = 0; i < aRoot.childCount; i++) { + if (aRoot.getChild(i).uri == aUrl) { + return aRoot.getChild(i); + } + } + return undefined; +} + +function newQueryWithOptions() +{ + return [ PlacesUtils.history.getNewQuery(), + PlacesUtils.history.getNewQueryOptions() ]; +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* pages_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + do_check_eq(node.title, gTestData[i].title); + let uri = NetUtil.newURI(node.uri); + yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"}); + do_check_eq(node.title, "changedTitle"); + yield PlacesTestUtils.addVisits({uri: uri, title: gTestData[i].title}); + do_check_eq(node.title, gTestData[i].title); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* visits_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + + for (let testData of gTestData) { + let uri = NetUtil.newURI(testData.uri); + let node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, testData.title); + yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"}); + node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, "changedTitle"); + yield PlacesTestUtils.addVisits({uri: uri, title: testData.title}); + node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, testData.title); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* pages_searchterm_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + query.searchTerms = "example"; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = NetUtil.newURI(node.uri); + do_check_eq(node.title, gTestData[i].title); + yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"}); + do_check_eq(node.title, "changedTitle"); + yield PlacesTestUtils.addVisits({uri: uri, title: gTestData[i].title}); + do_check_eq(node.title, gTestData[i].title); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* visits_searchterm_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + query.searchTerms = "example"; + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root); + for (let testData of gTestData) { + let uri = NetUtil.newURI(testData.uri); + let node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, testData.title); + yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"}); + node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, "changedTitle"); + yield PlacesTestUtils.addVisits({uri: uri, title: testData.title}); + node = searchNodeHavingUrl(root, testData.uri); + do_check_eq(node.title, testData.title); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* pages_searchterm_is_title_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + query.searchTerms = "match"; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult([], root); + for (let data of gTestData) { + let uri = NetUtil.newURI(data.uri); + let origTitle = data.title; + data.title = "match"; + yield PlacesTestUtils.addVisits({ uri: uri, title: data.title, + visitDate: data.lastVisit }); + compareArrayToResult([data], root); + data.title = origTitle; + yield PlacesTestUtils.addVisits({ uri: uri, title: data.title, + visitDate: data.lastVisit }); + compareArrayToResult([], root); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* visits_searchterm_is_title_query() +{ + yield task_populateDB(gTestData); + + let [query, options] = newQueryWithOptions(); + query.searchTerms = "match"; + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult([], root); + for (let data of gTestData) { + let uri = NetUtil.newURI(data.uri); + let origTitle = data.title; + data.title = "match"; + yield PlacesTestUtils.addVisits({ uri: uri, title: data.title, + visitDate: data.lastVisit }); + compareArrayToResult([data], root); + data.title = origTitle; + yield PlacesTestUtils.addVisits({ uri: uri, title: data.title, + visitDate: data.lastVisit }); + compareArrayToResult([], root); + } + + root.containerOpen = false; + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/queries/test_onlyBookmarked.js b/toolkit/components/places/tests/queries/test_onlyBookmarked.js new file mode 100644 index 000000000..45704c109 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_onlyBookmarked.js @@ -0,0 +1,128 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * The next thing we do is create a test database for us. Each test runs with + * its own database (tail_queries.js will clear it after the run). Take a look + * at the queryData object in head_queries.js, and you'll see how this object + * works. You can call it anything you like, but I usually use "testData". + * I'll include a couple of example entries in the database. + * + * Note that to use the compareArrayToResult API, you need to put all the + * results that are in the query set at the top of the testData list, and those + * results MUST be in the same sort order as the items in the resulting query. + */ + +var testData = [ + // Add a bookmark that should be in the results + { isBookmark: true, + uri: "http://bookmarked.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + isInQuery: true }, + + // Add a bookmark that should not be in the results + { isBookmark: true, + uri: "http://bookmarked-elsewhere.com/", + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + isInQuery: false }, + + // Add an un-bookmarked visit + { isVisit: true, + uri: "http://notbookmarked.com/", + isInQuery: false } +]; + + +/** + * run_test is where the magic happens. This is automatically run by the test + * harness. It is where you do the work of creating the query, running it, and + * playing with the result set. + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_onlyBookmarked() +{ + // This function in head_queries.js creates our database with the above data + yield task_populateDB(testData); + + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.toolbarFolderId], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_HISTORY; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // You can use this to compare the data in the array with the result set, + // if the array's isInQuery: true items are sorted the same way as the result + // set. + do_print("begin first test"); + compareArrayToResult(testData, root); + do_print("end first test"); + + // Test live-update + var liveUpdateTestData = [ + // Add a bookmark that should show up + { isBookmark: true, + uri: "http://bookmarked2.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + isInQuery: true }, + + // Add a bookmark that should not show up + { isBookmark: true, + uri: "http://bookmarked-elsewhere2.com/", + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + isInQuery: false } + ]; + + yield task_populateDB(liveUpdateTestData); // add to the db + + // add to the test data + testData.push(liveUpdateTestData[0]); + testData.push(liveUpdateTestData[1]); + + // re-query and test + do_print("begin live-update test"); + compareArrayToResult(testData, root); + do_print("end live-update test"); +/* + // we are actually not updating during a batch. + // see bug 432706 for details. + + // Here's a batch update + var updateBatch = { + runBatched: function (aUserData) { + liveUpdateTestData[0].uri = "http://bookmarked3.com"; + liveUpdateTestData[1].uri = "http://bookmarked-elsewhere3.com"; + populateDB(liveUpdateTestData); + testData.push(liveUpdateTestData[0]); + testData.push(liveUpdateTestData[1]); + } + }; + + PlacesUtils.history.runInBatchMode(updateBatch, null); + + // re-query and test + do_print("begin batched test"); + compareArrayToResult(testData, root); + do_print("end batched test"); +*/ + // Close the container when finished + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_queryMultipleFolder.js b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js new file mode 100644 index 000000000..694728a43 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function run_test() { + run_next_test(); +} + +add_task(function* test_queryMultipleFolders() { + // adding bookmarks in the folders + let folderIds = []; + let bookmarkIds = []; + for (let i = 0; i < 3; ++i) { + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: `Folder${i}` + }); + folderIds.push(yield PlacesUtils.promiseItemId(folder.guid)); + + for (let j = 0; j < 7; ++j) { + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: (yield PlacesUtils.promiseItemGuid(folderIds[i])), + url: `http://Bookmark${i}_${j}.com`, + title: "" + }); + bookmarkIds.push(yield PlacesUtils.promiseItemId(bm.guid)); + } + } + + // using queryStringToQueries + let query = {}; + let options = {}; + let maxResults = 20; + let queryString = "place:" + folderIds.map((id) => { + return "folder=" + id; + }).join('&') + "&sort=5&maxResults=" + maxResults; + PlacesUtils.history.queryStringToQueries(queryString, query, {}, options); + let rootNode = PlacesUtils.history.executeQuery(query.value[0], options.value).root; + rootNode.containerOpen = true; + let resultLength = rootNode.childCount; + Assert.equal(resultLength, maxResults); + for (let i = 0; i < resultLength; ++i) { + let node = rootNode.getChild(i); + Assert.equal(bookmarkIds[i], node.itemId, node.uri); + } + rootNode.containerOpen = false; + + // using getNewQuery and getNewQueryOptions + query = PlacesUtils.history.getNewQuery(); + options = PlacesUtils.history.getNewQueryOptions(); + query.setFolders(folderIds, folderIds.length); + options.sortingMode = options.SORT_BY_URI_ASCENDING; + options.maxResults = maxResults; + rootNode = PlacesUtils.history.executeQuery(query, options).root; + rootNode.containerOpen = true; + resultLength = rootNode.childCount; + Assert.equal(resultLength, maxResults); + for (let i = 0; i < resultLength; ++i) { + let node = rootNode.getChild(i); + Assert.equal(bookmarkIds[i], node.itemId, node.uri); + } + rootNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_querySerialization.js b/toolkit/components/places/tests/queries/test_querySerialization.js new file mode 100644 index 000000000..24cf8aa9b --- /dev/null +++ b/toolkit/components/places/tests/queries/test_querySerialization.js @@ -0,0 +1,797 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests Places query serialization. Associated bug is + * https://bugzilla.mozilla.org/show_bug.cgi?id=370197 + * + * The simple idea behind this test is to try out different combinations of + * query switches and ensure that queries are the same before serialization + * as they are after de-serialization. + * + * In the code below, "switch" refers to a query option -- "option" in a broad + * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to + * them as switches, not options). Both nsINavHistoryQuery and + * nsINavHistoryQueryOptions allow you to specify switches that affect query + * strings. nsINavHistoryQuery instances have attributes hasBeginTime, + * hasEndTime, hasSearchTerms, and so on. nsINavHistoryQueryOptions instances + * have attributes sortingMode, resultType, excludeItems, etc. + * + * Ideally we would like to test all 2^N subsets of switches, where N is the + * total number of switches; switches might interact in erroneous or other ways + * we do not expect. However, since N is large (21 at this time), that's + * impractical for a single test in a suite. + * + * Instead we choose all possible subsets of a certain, smaller size. In fact + * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to + * CHOOSE_HOW_MANY_SWITCHES_HI. + * + * There are two more wrinkles. First, for some switches we'd like to be able to + * test multiple values. For example, it seems like a good idea to test both an + * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms. + * When switches have more than one value for a test run, we use the Cartesian + * product of their values to generate all possible combinations of values. + * + * Second, we need to also test serialization of multiple nsINavHistoryQuery + * objects at once. To do this, we remember the previous NUM_MULTIPLE_QUERIES + * queries we tested individually and then serialize them together. We do this + * each time we test an individual query. Thus the set of queries we test + * together loses one query and gains another each time. + * + * To summarize, here's how this test works: + * + * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI: + * - From the total set of switches choose all possible subsets of size n. + * For each of those subsets s: + * - Collect the test runs of each switch in subset s and take their + * Cartesian product. For each sequence in the product: + * - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects + * with the chosen switches and test run values. + * - Serialize the query. + * - De-serialize and ensure that the de-serialized query objects equal + * the originals. + * - For each of the previous NUM_MULTIPLE_QUERIES + * nsINavHistoryQueryOptions objects o we created: + * - Serialize the previous NUM_MULTIPLE_QUERIES nsINavHistoryQuery + * objects together with o. + * - De-serialize and ensure that the de-serialized query objects + * equal the originals. + */ + +const CHOOSE_HOW_MANY_SWITCHES_LO = 1; +const CHOOSE_HOW_MANY_SWITCHES_HI = 2; + +const NUM_MULTIPLE_QUERIES = 2; + +// The switches are represented by objects below, in arrays querySwitches and +// queryOptionSwitches. Use them to set up test runs. +// +// Some switches have special properties (where noted), but all switches must +// have the following properties: +// +// matches: A function that takes two nsINavHistoryQuery objects (in the case +// of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions +// objects (for nsINavHistoryQueryOptions switches) and returns true +// if the values of the switch in the two objects are equal. This is +// the foundation of how we determine if two queries are equal. +// runs: An array of functions. Each function takes an nsINavHistoryQuery +// object and an nsINavHistoryQueryOptions object. The functions +// should set the attributes of one of the two objects as appropriate +// to their switches. This is how switch values are set for each test +// run. +// +// The following properties are optional: +// +// desc: An informational string to print out during runs when the switch +// is chosen. Hopefully helpful if the test fails. + +// nsINavHistoryQuery switches +const querySwitches = [ + // hasBeginTime + { + // flag and subswitches are used by the flagSwitchMatches function. Several + // of the nsINavHistoryQuery switches (like this one) are really guard flags + // that indicate if other "subswitches" are enabled. + flag: "hasBeginTime", + subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"], + desc: "nsINavHistoryQuery.hasBeginTime", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.beginTime = Date.now() * 1000; + aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH; + }, + function (aQuery, aQueryOptions) { + aQuery.beginTime = Date.now() * 1000; + aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY; + } + ] + }, + // hasEndTime + { + flag: "hasEndTime", + subswitches: ["endTime", "endTimeReference", "absoluteEndTime"], + desc: "nsINavHistoryQuery.hasEndTime", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.endTime = Date.now() * 1000; + aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH; + }, + function (aQuery, aQueryOptions) { + aQuery.endTime = Date.now() * 1000; + aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY; + } + ] + }, + // hasSearchTerms + { + flag: "hasSearchTerms", + subswitches: ["searchTerms"], + desc: "nsINavHistoryQuery.hasSearchTerms", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.searchTerms = "shrimp and white wine"; + }, + function (aQuery, aQueryOptions) { + aQuery.searchTerms = ""; + } + ] + }, + // hasDomain + { + flag: "hasDomain", + subswitches: ["domain", "domainIsHost"], + desc: "nsINavHistoryQuery.hasDomain", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.domain = "mozilla.com"; + aQuery.domainIsHost = false; + }, + function (aQuery, aQueryOptions) { + aQuery.domain = "www.mozilla.com"; + aQuery.domainIsHost = true; + }, + function (aQuery, aQueryOptions) { + aQuery.domain = ""; + } + ] + }, + // hasUri + { + flag: "hasUri", + subswitches: ["uri"], + desc: "nsINavHistoryQuery.hasUri", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.uri = uri("http://mozilla.com"); + }, + ] + }, + // hasAnnotation + { + flag: "hasAnnotation", + subswitches: ["annotation", "annotationIsNot"], + desc: "nsINavHistoryQuery.hasAnnotation", + matches: flagSwitchMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.annotation = "bookmarks/toolbarFolder"; + aQuery.annotationIsNot = false; + }, + function (aQuery, aQueryOptions) { + aQuery.annotation = "bookmarks/toolbarFolder"; + aQuery.annotationIsNot = true; + } + ] + }, + // minVisits + { + // property is used by function simplePropertyMatches. + property: "minVisits", + desc: "nsINavHistoryQuery.minVisits", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.minVisits = 0x7fffffff; // 2^31 - 1 + } + ] + }, + // maxVisits + { + property: "maxVisits", + desc: "nsINavHistoryQuery.maxVisits", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.maxVisits = 0x7fffffff; // 2^31 - 1 + } + ] + }, + // onlyBookmarked + { + property: "onlyBookmarked", + desc: "nsINavHistoryQuery.onlyBookmarked", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.onlyBookmarked = true; + } + ] + }, + // getFolders + { + desc: "nsINavHistoryQuery.getFolders", + matches: function (aQuery1, aQuery2) { + var q1Folders = aQuery1.getFolders(); + var q2Folders = aQuery2.getFolders(); + if (q1Folders.length !== q2Folders.length) + return false; + for (let i = 0; i < q1Folders.length; i++) { + if (q2Folders.indexOf(q1Folders[i]) < 0) + return false; + } + for (let i = 0; i < q2Folders.length; i++) { + if (q1Folders.indexOf(q2Folders[i]) < 0) + return false; + } + return true; + }, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.setFolders([], 0); + }, + function (aQuery, aQueryOptions) { + aQuery.setFolders([PlacesUtils.placesRootId], 1); + }, + function (aQuery, aQueryOptions) { + aQuery.setFolders([PlacesUtils.placesRootId, PlacesUtils.tagsFolderId], 2); + } + ] + }, + // tags + { + desc: "nsINavHistoryQuery.getTags", + matches: function (aQuery1, aQuery2) { + if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot) + return false; + var q1Tags = aQuery1.tags; + var q2Tags = aQuery2.tags; + if (q1Tags.length !== q2Tags.length) + return false; + for (let i = 0; i < q1Tags.length; i++) { + if (q2Tags.indexOf(q1Tags[i]) < 0) + return false; + } + for (let i = 0; i < q2Tags.length; i++) { + if (q1Tags.indexOf(q2Tags[i]) < 0) + return false; + } + return true; + }, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.tags = []; + }, + function (aQuery, aQueryOptions) { + aQuery.tags = [""]; + }, + function (aQuery, aQueryOptions) { + aQuery.tags = [ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ]; + }, + function (aQuery, aQueryOptions) { + aQuery.tags = [ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ]; + aQuery.tagsAreNot = true; + } + ] + }, + // transitions + { + desc: "tests nsINavHistoryQuery.getTransitions", + matches: function (aQuery1, aQuery2) { + var q1Trans = aQuery1.getTransitions(); + var q2Trans = aQuery2.getTransitions(); + if (q1Trans.length !== q2Trans.length) + return false; + for (let i = 0; i < q1Trans.length; i++) { + if (q2Trans.indexOf(q1Trans[i]) < 0) + return false; + } + for (let i = 0; i < q2Trans.length; i++) { + if (q1Trans.indexOf(q2Trans[i]) < 0) + return false; + } + return true; + }, + runs: [ + function (aQuery, aQueryOptions) { + aQuery.setTransitions([], 0); + }, + function (aQuery, aQueryOptions) { + aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], + 1); + }, + function (aQuery, aQueryOptions) { + aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_TYPED, + Ci.nsINavHistoryService.TRANSITION_BOOKMARK], 2); + } + ] + }, +]; + +// nsINavHistoryQueryOptions switches +const queryOptionSwitches = [ + // sortingMode + { + desc: "nsINavHistoryQueryOptions.sortingMode", + matches: function (aOptions1, aOptions2) { + if (aOptions1.sortingMode === aOptions2.sortingMode) { + switch (aOptions1.sortingMode) { + case aOptions1.SORT_BY_ANNOTATION_ASCENDING: + case aOptions1.SORT_BY_ANNOTATION_DESCENDING: + return aOptions1.sortingAnnotation === aOptions2.sortingAnnotation; + } + return true; + } + return false; + }, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING; + }, + function (aQuery, aQueryOptions) { + aQueryOptions.sortingMode = aQueryOptions.SORT_BY_ANNOTATION_ASCENDING; + aQueryOptions.sortingAnnotation = "bookmarks/toolbarFolder"; + } + ] + }, + // resultType + { + // property is used by function simplePropertyMatches. + property: "resultType", + desc: "nsINavHistoryQueryOptions.resultType", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI; + }, + function (aQuery, aQueryOptions) { + aQueryOptions.resultType = aQueryOptions.RESULTS_AS_FULL_VISIT; + } + ] + }, + // excludeItems + { + property: "excludeItems", + desc: "nsINavHistoryQueryOptions.excludeItems", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.excludeItems = true; + } + ] + }, + // excludeQueries + { + property: "excludeQueries", + desc: "nsINavHistoryQueryOptions.excludeQueries", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.excludeQueries = true; + } + ] + }, + // expandQueries + { + property: "expandQueries", + desc: "nsINavHistoryQueryOptions.expandQueries", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.expandQueries = true; + } + ] + }, + // includeHidden + { + property: "includeHidden", + desc: "nsINavHistoryQueryOptions.includeHidden", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.includeHidden = true; + } + ] + }, + // maxResults + { + property: "maxResults", + desc: "nsINavHistoryQueryOptions.maxResults", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1 + } + ] + }, + // queryType + { + property: "queryType", + desc: "nsINavHistoryQueryOptions.queryType", + matches: simplePropertyMatches, + runs: [ + function (aQuery, aQueryOptions) { + aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY; + }, + function (aQuery, aQueryOptions) { + aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_UNIFIED; + } + ] + }, +]; + +/** + * Enumerates all the sequences of the cartesian product of the arrays contained + * in aSequences. Examples: + * + * cartProd([[1, 2, 3], ["a", "b"]], callback); + * // callback is called 3 * 2 = 6 times with the following arrays: + * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"] + * + * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback); + * // callback is called 1 * 3 * 2 = 6 times with the following arrays: + * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"], + * // ["a", 3, "X"], ["a", 3, "Y"] + * + * cartProd([[1], [2], [3], [4]], callback); + * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array: + * // [1, 2, 3, 4] + * + * cartProd([], callback); + * // callback is 0 times + * + * cartProd([[1, 2, 3, 4]], callback); + * // callback is called 4 times with the following arrays: + * // [1], [2], [3], [4] + * + * @param aSequences + * an array that contains an arbitrary number of arrays + * @param aCallback + * a function that is passed each sequence of the product as it's + * computed + * @return the total number of sequences in the product + */ +function cartProd(aSequences, aCallback) +{ + if (aSequences.length === 0) + return 0; + + // For each sequence in aSequences, we maintain a pointer (an array index, + // really) to the element we're currently enumerating in that sequence + var seqEltPtrs = aSequences.map(i => 0); + + var numProds = 0; + var done = false; + while (!done) { + numProds++; + + // prod = sequence in product we're currently enumerating + let prod = []; + for (let i = 0; i < aSequences.length; i++) { + prod.push(aSequences[i][seqEltPtrs[i]]); + } + aCallback(prod); + + // The next sequence in the product differs from the current one by just a + // single element. Determine which element that is. We advance the + // "rightmost" element pointer to the "right" by one. If we move past the + // end of that pointer's sequence, reset the pointer to the first element + // in its sequence and then try the sequence to the "left", and so on. + + // seqPtr = index of rightmost input sequence whose element pointer is not + // past the end of the sequence + let seqPtr = aSequences.length - 1; + while (!done) { + // Advance the rightmost element pointer. + seqEltPtrs[seqPtr]++; + + // The rightmost element pointer is past the end of its sequence. + if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) { + seqEltPtrs[seqPtr] = 0; + seqPtr--; + + // All element pointers are past the ends of their sequences. + if (seqPtr < 0) + done = true; + } + else break; + } + } + return numProds; +} + +/** + * Enumerates all the subsets in aSet of size aHowMany. There are + * C(aSet.length, aHowMany) such subsets. aCallback will be passed each subset + * as it is generated. Note that aSet and the subsets enumerated are -- even + * though they're arrays -- not sequences; the ordering of their elements is not + * important. Example: + * + * choose([1, 2, 3, 4], 2, callback); + * // callback is called C(4, 2) = 6 times with the following sets (arrays): + * // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4] + * + * @param aSet + * an array from which to choose elements, aSet.length > 0 + * @param aHowMany + * the number of elements to choose, > 0 and <= aSet.length + * @return the total number of sets chosen + */ +function choose(aSet, aHowMany, aCallback) +{ + // ptrs = indices of the elements in aSet we're currently choosing + var ptrs = []; + for (let i = 0; i < aHowMany; i++) { + ptrs.push(i); + } + + var numFound = 0; + var done = false; + while (!done) { + numFound++; + aCallback(ptrs.map(p => aSet[p])); + + // The next subset to be chosen differs from the current one by just a + // single element. Determine which element that is. Advance the "rightmost" + // pointer to the "right" by one. If we move past the end of set, move the + // next non-adjacent rightmost pointer to the right by one, and reset all + // succeeding pointers so that they're adjacent to it. When all pointers + // are clustered all the way to the right, we're done. + + // Advance the rightmost pointer. + ptrs[ptrs.length - 1]++; + + // The rightmost pointer has gone past the end of set. + if (ptrs[ptrs.length - 1] >= aSet.length) { + // Find the next rightmost pointer that is not adjacent to the current one. + let si = aSet.length - 2; // aSet index + let pi = ptrs.length - 2; // ptrs index + while (pi >= 0 && ptrs[pi] === si) { + pi--; + si--; + } + + // All pointers are adjacent and clustered all the way to the right. + if (pi < 0) + done = true; + else { + // pi = index of rightmost pointer with a gap between it and its + // succeeding pointer. Move it right and reset all succeeding pointers + // so that they're adjacent to it. + ptrs[pi]++; + for (let i = 0; i < ptrs.length - pi - 1; i++) { + ptrs[i + pi + 1] = ptrs[pi] + i + 1; + } + } + } + } + return numFound; +} + +/** + * Convenience function for nsINavHistoryQuery switches that act as flags. This + * is attached to switch objects. See querySwitches array above. + * + * @param aQuery1 + * an nsINavHistoryQuery object + * @param aQuery2 + * another nsINavHistoryQuery object + * @return true if this switch is the same in both aQuery1 and aQuery2 + */ +function flagSwitchMatches(aQuery1, aQuery2) +{ + if (aQuery1[this.flag] && aQuery2[this.flag]) { + for (let p in this.subswitches) { + if (p in aQuery1 && p in aQuery2) { + if (aQuery1[p] instanceof Ci.nsIURI) { + if (!aQuery1[p].equals(aQuery2[p])) + return false; + } + else if (aQuery1[p] !== aQuery2[p]) + return false; + } + } + } + else if (aQuery1[this.flag] || aQuery2[this.flag]) + return false; + + return true; +} + +/** + * Tests if aObj1 and aObj2 are equal. This function is general and may be used + * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects. aSwitches + * determines which set of switches is used for comparison. Pass in either + * querySwitches or queryOptionSwitches. + * + * @param aSwitches + * determines which set of switches applies to aObj1 and aObj2, either + * querySwitches or queryOptionSwitches + * @param aObj1 + * an nsINavHistoryQuery or nsINavHistoryQueryOptions object + * @param aObj2 + * another nsINavHistoryQuery or nsINavHistoryQueryOptions object + * @return true if aObj1 and aObj2 are equal + */ +function queryObjsEqual(aSwitches, aObj1, aObj2) +{ + for (let i = 0; i < aSwitches.length; i++) { + if (!aSwitches[i].matches(aObj1, aObj2)) + return false; + } + return true; +} + +/** + * This drives the test runs. See the comment at the top of this file. + * + * @param aHowManyLo + * the size of the switch subsets to start with + * @param aHowManyHi + * the size of the switch subsets to end with (inclusive) + */ +function runQuerySequences(aHowManyLo, aHowManyHi) +{ + var allSwitches = querySwitches.concat(queryOptionSwitches); + var prevQueries = []; + var prevOpts = []; + + // Choose aHowManyLo switches up to aHowManyHi switches. + for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) { + let numIters = 0; + print("CHOOSING " + howMany + " SWITCHES"); + + // Choose all subsets of size howMany from allSwitches. + choose(allSwitches, howMany, function (chosenSwitches) { + print(numIters); + numIters++; + + // Collect the runs. + // runs = [ [runs from switch 1], ..., [runs from switch howMany] ] + var runs = chosenSwitches.map(function (s) { + if (s.desc) + print(" " + s.desc); + return s.runs; + }); + + // cartProd(runs) => [ + // [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ], + // ..., + // [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ], + // ..., ..., + // [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ], + // ..., + // [switch 1 run N, switch 2 run N, ..., switch howMany run N ], + // ] + cartProd(runs, function (runSet) { + // Create a new query, apply the switches in runSet, and test it. + var query = PlacesUtils.history.getNewQuery(); + var opts = PlacesUtils.history.getNewQueryOptions(); + for (let i = 0; i < runSet.length; i++) { + runSet[i](query, opts); + } + serializeDeserialize([query], opts); + + // Test the previous NUM_MULTIPLE_QUERIES queries together. + prevQueries.push(query); + prevOpts.push(opts); + if (prevQueries.length >= NUM_MULTIPLE_QUERIES) { + // We can serialize multiple nsINavHistoryQuery objects together but + // only one nsINavHistoryQueryOptions object with them. So, test each + // of the previous NUM_MULTIPLE_QUERIES nsINavHistoryQueryOptions. + for (let i = 0; i < prevOpts.length; i++) { + serializeDeserialize(prevQueries, prevOpts[i]); + } + prevQueries.shift(); + prevOpts.shift(); + } + }); + }); + } + print("\n"); +} + +/** + * Serializes the nsINavHistoryQuery objects in aQueryArr and the + * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the + * serialization, and ensures (using do_check_* functions) that the + * de-serialized objects equal the originals. + * + * @param aQueryArr + * an array containing nsINavHistoryQuery objects + * @param aQueryOptions + * an nsINavHistoryQueryOptions object + */ +function serializeDeserialize(aQueryArr, aQueryOptions) +{ + var queryStr = PlacesUtils.history.queriesToQueryString(aQueryArr, + aQueryArr.length, + aQueryOptions); + print(" " + queryStr); + var queryArr2 = {}; + var opts2 = {}; + PlacesUtils.history.queryStringToQueries(queryStr, queryArr2, {}, opts2); + queryArr2 = queryArr2.value; + opts2 = opts2.value; + + // The two sets of queries cannot be the same if their lengths differ. + do_check_eq(aQueryArr.length, queryArr2.length); + + // Although the query serialization code as it is written now practically + // ensures that queries appear in the query string in the same order they + // appear in both the array to be serialized and the array resulting from + // de-serialization, the interface does not guarantee any ordering. So, for + // each query in aQueryArr, find its equivalent in queryArr2 and delete it + // from queryArr2. If queryArr2 is empty after looping through aQueryArr, + // the two sets of queries are equal. + for (let i = 0; i < aQueryArr.length; i++) { + let j = 0; + for (; j < queryArr2.length; j++) { + if (queryObjsEqual(querySwitches, aQueryArr[i], queryArr2[j])) + break; + } + if (j < queryArr2.length) + queryArr2.splice(j, 1); + } + do_check_eq(queryArr2.length, 0); + + // Finally check the query options objects. + do_check_true(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2)); +} + +/** + * Convenience function for switches that have simple values. This is attached + * to switch objects. See querySwitches and queryOptionSwitches arrays above. + * + * @param aObj1 + * an nsINavHistoryQuery or nsINavHistoryQueryOptions object + * @param aObj2 + * another nsINavHistoryQuery or nsINavHistoryQueryOptions object + * @return true if this switch is the same in both aObj1 and aObj2 + */ +function simplePropertyMatches(aObj1, aObj2) +{ + return aObj1[this.property] === aObj2[this.property]; +} + +function run_test() +{ + runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI); +} diff --git a/toolkit/components/places/tests/queries/test_redirects.js b/toolkit/components/places/tests/queries/test_redirects.js new file mode 100644 index 000000000..1be5a626f --- /dev/null +++ b/toolkit/components/places/tests/queries/test_redirects.js @@ -0,0 +1,311 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Array of visits we will add to the database, will be populated later +// in the test. +var visits = []; + +/** + * Takes a sequence of query options, and compare query results obtained through + * them with a custom filtered array of visits, based on the values we are + * expecting from the query. + * + * @param aSequence + * an array that contains query options in the form: + * [includeHidden, maxResults, sortingMode] + */ +function check_results_callback(aSequence) { + // Sanity check: we should receive 3 parameters. + do_check_eq(aSequence.length, 3); + let includeHidden = aSequence[0]; + let maxResults = aSequence[1]; + let sortingMode = aSequence[2]; + print("\nTESTING: includeHidden(" + includeHidden + ")," + + " maxResults(" + maxResults + ")," + + " sortingMode(" + sortingMode + ")."); + + function isHidden(aVisit) { + return aVisit.transType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK || + aVisit.isRedirect; + } + + // Build expectedData array. + let expectedData = visits.filter(function (aVisit, aIndex, aArray) { + // Embed visits never appear in results. + if (aVisit.transType == Ci.nsINavHistoryService.TRANSITION_EMBED) + return false; + + if (!includeHidden && isHidden(aVisit)) { + // If the page has any non-hidden visit, then it's visible. + if (visits.filter(function (refVisit) { + return refVisit.uri == aVisit.uri && !isHidden(refVisit); + }).length == 0) + return false; + } + + return true; + }); + + // Remove duplicates, since queries are RESULTS_AS_URI (unique pages). + let seen = []; + expectedData = expectedData.filter(function (aData) { + if (seen.includes(aData.uri)) { + return false; + } + seen.push(aData.uri); + return true; + }); + + // Sort expectedData. + function getFirstIndexFor(aEntry) { + for (let i = 0; i < visits.length; i++) { + if (visits[i].uri == aEntry.uri) + return i; + } + return undefined; + } + function comparator(a, b) { + if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING) { + return b.lastVisit - a.lastVisit; + } + if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING) { + return b.visitCount - a.visitCount; + } + return getFirstIndexFor(a) - getFirstIndexFor(b); + } + expectedData.sort(comparator); + + // Crop results to maxResults if it's defined. + if (maxResults) { + expectedData = expectedData.slice(0, maxResults); + } + + // Create a new query with required options. + let query = PlacesUtils.history.getNewQuery(); + let options = PlacesUtils.history.getNewQueryOptions(); + options.includeHidden = includeHidden; + options.sortingMode = sortingMode; + if (maxResults) + options.maxResults = maxResults; + + // Compare resultset with expectedData. + let result = PlacesUtils.history.executeQuery(query, options); + let root = result.root; + root.containerOpen = true; + compareArrayToResult(expectedData, root); + root.containerOpen = false; +} + +/** + * Enumerates all the sequences of the cartesian product of the arrays contained + * in aSequences. Examples: + * + * cartProd([[1, 2, 3], ["a", "b"]], callback); + * // callback is called 3 * 2 = 6 times with the following arrays: + * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"] + * + * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback); + * // callback is called 1 * 3 * 2 = 6 times with the following arrays: + * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"], + * // ["a", 3, "X"], ["a", 3, "Y"] + * + * cartProd([[1], [2], [3], [4]], callback); + * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array: + * // [1, 2, 3, 4] + * + * cartProd([], callback); + * // callback is 0 times + * + * cartProd([[1, 2, 3, 4]], callback); + * // callback is called 4 times with the following arrays: + * // [1], [2], [3], [4] + * + * @param aSequences + * an array that contains an arbitrary number of arrays + * @param aCallback + * a function that is passed each sequence of the product as it's + * computed + * @return the total number of sequences in the product + */ +function cartProd(aSequences, aCallback) +{ + if (aSequences.length === 0) + return 0; + + // For each sequence in aSequences, we maintain a pointer (an array index, + // really) to the element we're currently enumerating in that sequence + let seqEltPtrs = aSequences.map(i => 0); + + let numProds = 0; + let done = false; + while (!done) { + numProds++; + + // prod = sequence in product we're currently enumerating + let prod = []; + for (let i = 0; i < aSequences.length; i++) { + prod.push(aSequences[i][seqEltPtrs[i]]); + } + aCallback(prod); + + // The next sequence in the product differs from the current one by just a + // single element. Determine which element that is. We advance the + // "rightmost" element pointer to the "right" by one. If we move past the + // end of that pointer's sequence, reset the pointer to the first element + // in its sequence and then try the sequence to the "left", and so on. + + // seqPtr = index of rightmost input sequence whose element pointer is not + // past the end of the sequence + let seqPtr = aSequences.length - 1; + while (!done) { + // Advance the rightmost element pointer. + seqEltPtrs[seqPtr]++; + + // The rightmost element pointer is past the end of its sequence. + if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) { + seqEltPtrs[seqPtr] = 0; + seqPtr--; + + // All element pointers are past the ends of their sequences. + if (seqPtr < 0) + done = true; + } + else break; + } + } + return numProds; +} + +function run_test() +{ + run_next_test(); +} + +/** + * Populate the visits array and add visits to the database. + * We will generate visit-chains like: + * visit -> redirect_temp -> redirect_perm + */ +add_task(function* test_add_visits_to_database() +{ + yield PlacesUtils.bookmarks.eraseEverything(); + + // We don't really bother on this, but we need a time to add visits. + let timeInMicroseconds = Date.now() * 1000; + let visitCount = 1; + + // Array of all possible transition types we could be redirected from. + let t = [ + Ci.nsINavHistoryService.TRANSITION_LINK, + Ci.nsINavHistoryService.TRANSITION_TYPED, + Ci.nsINavHistoryService.TRANSITION_BOOKMARK, + // Embed visits are not added to the database and we don't want redirects + // to them, thus just avoid addition. + // Ci.nsINavHistoryService.TRANSITION_EMBED, + Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK, + // Would make hard sorting by visit date because last_visit_date is actually + // calculated excluding download transitions, but the query includes + // downloads. + // TODO: Bug 488966 could fix this behavior. + //Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, + ]; + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds - 1000; + return timeInMicroseconds; + } + + // we add a visit for each of the above transition types. + t.forEach(transition => visits.push( + { isVisit: true, + transType: transition, + uri: "http://" + transition + ".example.com/", + title: transition + "-example", + isRedirect: true, + lastVisit: newTimeInMicroseconds(), + visitCount: (transition == Ci.nsINavHistoryService.TRANSITION_EMBED || + transition == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK) ? 0 : visitCount++, + isInQuery: true })); + + // Add a REDIRECT_TEMPORARY layer of visits for each of the above visits. + t.forEach(transition => visits.push( + { isVisit: true, + transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY, + uri: "http://" + transition + ".redirect.temp.example.com/", + title: transition + "-redirect-temp-example", + lastVisit: newTimeInMicroseconds(), + isRedirect: true, + referrer: "http://" + transition + ".example.com/", + visitCount: visitCount++, + isInQuery: true })); + + // Add a REDIRECT_PERMANENT layer of visits for each of the above redirects. + t.forEach(transition => visits.push( + { isVisit: true, + transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT, + uri: "http://" + transition + ".redirect.perm.example.com/", + title: transition + "-redirect-perm-example", + lastVisit: newTimeInMicroseconds(), + isRedirect: true, + referrer: "http://" + transition + ".redirect.temp.example.com/", + visitCount: visitCount++, + isInQuery: true })); + + // Add a REDIRECT_PERMANENT layer of visits that loop to the first visit. + // These entries should not change visitCount or lastVisit, otherwise + // guessing an order would be a nightmare. + function getLastValue(aURI, aProperty) { + for (let i = 0; i < visits.length; i++) { + if (visits[i].uri == aURI) { + return visits[i][aProperty]; + } + } + do_throw("Unknown uri."); + return null; + } + t.forEach(transition => visits.push( + { isVisit: true, + transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT, + uri: "http://" + transition + ".example.com/", + title: getLastValue("http://" + transition + ".example.com/", "title"), + lastVisit: getLastValue("http://" + transition + ".example.com/", "lastVisit"), + isRedirect: true, + referrer: "http://" + transition + ".redirect.perm.example.com/", + visitCount: getLastValue("http://" + transition + ".example.com/", "visitCount"), + isInQuery: true })); + + // Add an unvisited bookmark in the database, it should never appear. + visits.push({ isBookmark: true, + uri: "http://unvisited.bookmark.com/", + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "Unvisited Bookmark", + isInQuery: false }); + + // Put visits in the database. + yield task_populateDB(visits); +}); + +add_task(function* test_redirects() +{ + // Frecency and hidden are updated asynchronously, wait for them. + yield PlacesTestUtils.promiseAsyncUpdates(); + + // This array will be used by cartProd to generate a matrix of all possible + // combinations. + let includeHidden_options = [true, false]; + let maxResults_options = [5, 10, 20, null]; + // These sortingMode are choosen to toggle using special queries for history + // menu and most visited smart bookmark. + let sorting_options = [Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, + Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING, + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING]; + // Will execute check_results_callback() for each generated combination. + cartProd([includeHidden_options, maxResults_options, sorting_options], + check_results_callback); + + yield PlacesUtils.bookmarks.eraseEverything(); + + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js b/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js new file mode 100644 index 000000000..f1cbfd4d8 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js @@ -0,0 +1,127 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var testData = [ + { isInQuery: true, + isDetails: true, + title: "bmoz", + uri: "http://foo.com/", + isBookmark: true, + isTag: true, + tagArray: ["bugzilla"] }, + + { isInQuery: true, + isDetails: true, + title: "C Moz", + uri: "http://foo.com/changeme1.html", + isBookmark: true, + isTag: true, + tagArray: ["moz", "bugzilla"] }, + + { isInQuery: false, + isDetails: true, + title: "amo", + uri: "http://foo2.com/", + isBookmark: true, + isTag: true, + tagArray: ["moz"] }, + + { isInQuery: false, + isDetails: true, + title: "amo", + uri: "http://foo.com/changeme2.html", + isBookmark: true }, +]; + +function getIdForTag(aTagName) { + var id = -1; + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.tagsFolderId], 1); + var options = PlacesUtils.history.getNewQueryOptions(); + var root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + var cc = root.childCount; + do_check_eq(root.childCount, 2); + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + if (node.title == aTagName) { + id = node.itemId; + break; + } + } + root.containerOpen = false; + return id; +} + + /** + * This test will test Queries that use relative search terms and URI options + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_results_as_tag_contents_query() +{ + yield task_populateDB(testData); + + // Get tag id. + let tagId = getIdForTag("bugzilla"); + do_check_true(tagId > 0); + + var options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_TAG_CONTENTS; + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([tagId], 1); + + var root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + displayResultSet(root); + // Cannot use compare array to results, since results ordering is hardcoded + // and depending on lastModified (that could have VM timers issues). + testData.forEach(function(aEntry) { + if (aEntry.isInResult) + do_check_true(isInResult({uri: "http://foo.com/added.html"}, root)); + }); + + // If that passes, check liveupdate + // Add to the query set + var change1 = { isVisit: true, + isDetails: true, + uri: "http://foo.com/added.html", + title: "mozadded", + isBookmark: true, + isTag: true, + tagArray: ["moz", "bugzilla"] }; + do_print("Adding item to query"); + yield task_populateDB([change1]); + do_print("These results should have been LIVE UPDATED with the new addition"); + displayResultSet(root); + do_check_true(isInResult(change1, root)); + + // Add one by adding a tag, remove one by removing search term. + do_print("Updating items"); + var change2 = [{ isDetails: true, + uri: "http://foo3.com/", + title: "foo"}, + { isDetails: true, + uri: "http://foo.com/changeme2.html", + title: "zydeco", + isBookmark:true, + isTag: true, + tagArray: ["bugzilla", "moz"] }]; + yield task_populateDB(change2); + do_check_false(isInResult({uri: "http://fooz.com/"}, root)); + do_check_true(isInResult({uri: "http://foo.com/changeme2.html"}, root)); + + // Test removing a tag updates us. + do_print("Deleting item"); + PlacesUtils.tagging.untagURI(uri("http://foo.com/changeme2.html"), ["bugzilla"]); + do_check_false(isInResult({uri: "http://foo.com/changeme2.html"}, root)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_results-as-visit.js b/toolkit/components/places/tests/queries/test_results-as-visit.js new file mode 100644 index 000000000..d0f270bd2 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_results-as-visit.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +var testData = []; +var timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000); + +function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; +} + +function createTestData() { + function generateVisits(aPage) { + for (var i = 0; i < aPage.visitCount; i++) { + testData.push({ isInQuery: aPage.inQuery, + isVisit: true, + title: aPage.title, + uri: aPage.uri, + lastVisit: newTimeInMicroseconds(), + isTag: aPage.tags && aPage.tags.length > 0, + tagArray: aPage.tags }); + } + } + + var pages = [ + { uri: "http://foo.com/", title: "amo", tags: ["moz"], visitCount: 3, inQuery: true }, + { uri: "http://moilla.com/", title: "bMoz", tags: ["bugzilla"], visitCount: 5, inQuery: true }, + { uri: "http://foo.mail.com/changeme1.html", title: "c Moz", visitCount: 7, inQuery: true }, + { uri: "http://foo.mail.com/changeme2.html", tags: ["moz"], title: "", visitCount: 1, inQuery: false }, + { uri: "http://foo.mail.com/changeme3.html", title: "zydeco", visitCount: 5, inQuery: false }, + ]; + pages.forEach(generateVisits); +} + +/** + * This test will test Queries that use relative search terms and URI options + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_results_as_visit() +{ + createTestData(); + yield task_populateDB(testData); + var query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "moz"; + query.minVisits = 2; + + // Options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_ASCENDING; + options.resultType = options.RESULTS_AS_VISIT; + + // Results + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + do_print("Number of items in result set: " + root.childCount); + for (let i=0; i < root.childCount; ++i) { + do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title); + } + + // Check our inital result set + compareArrayToResult(testData, root); + + // If that passes, check liveupdate + // Add to the query set + do_print("Adding item to query") + var tmp = []; + for (let i=0; i < 2; i++) { + tmp.push({ isVisit: true, + uri: "http://foo.com/added.html", + title: "ab moz" }); + } + yield task_populateDB(tmp); + for (let i=0; i < 2; i++) + do_check_eq(root.getChild(i).title, "ab moz"); + + // Update an existing URI + do_print("Updating Item"); + var change2 = [{ isVisit: true, + title: "moz", + uri: "http://foo.mail.com/changeme2.html" }]; + yield task_populateDB(change2); + do_check_true(isInResult(change2, root)); + + // Update some visits - add one and take one out of query set, and simply + // change one so that it still applies to the query. + do_print("Updating More Items"); + var change3 = [{ isVisit: true, + lastVisit: newTimeInMicroseconds(), + uri: "http://foo.mail.com/changeme1.html", + title: "foo"}, + { isVisit: true, + lastVisit: newTimeInMicroseconds(), + uri: "http://foo.mail.com/changeme3.html", + title: "moz", + isTag: true, + tagArray: ["foo", "moz"] }]; + yield task_populateDB(change3); + do_check_false(isInResult({uri: "http://foo.mail.com/changeme1.html"}, root)); + do_check_true(isInResult({uri: "http://foo.mail.com/changeme3.html"}, root)); + + // And now, delete one + do_print("Delete item outside of batch"); + var change4 = [{ isVisit: true, + lastVisit: newTimeInMicroseconds(), + uri: "http://moilla.com/", + title: "mo,z" }]; + yield task_populateDB(change4); + do_check_false(isInResult(change4, root)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js new file mode 100644 index 000000000..038367c0b --- /dev/null +++ b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js @@ -0,0 +1,84 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests the interaction of includeHidden and searchTerms search options. + +var timeInMicroseconds = Date.now() * 1000; + +const VISITS = [ + { isVisit: true, + transType: TRANSITION_TYPED, + uri: "http://redirect.example.com/", + title: "example", + isRedirect: true, + lastVisit: timeInMicroseconds-- + }, + { isVisit: true, + transType: TRANSITION_TYPED, + uri: "http://target.example.com/", + title: "example", + lastVisit: timeInMicroseconds-- + } +]; + +const HIDDEN_VISITS = [ + { isVisit: true, + transType: TRANSITION_FRAMED_LINK, + uri: "http://hidden.example.com/", + title: "red", + lastVisit: timeInMicroseconds-- + }, +]; + +const TEST_DATA = [ + { searchTerms: "example", + includeHidden: true, + expectedResults: 2 + }, + { searchTerms: "example", + includeHidden: false, + expectedResults: 1 + }, + { searchTerms: "red", + includeHidden: true, + expectedResults: 1 + } +]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_initalize() +{ + yield task_populateDB(VISITS); +}); + +add_task(function* test_searchTerms_includeHidden() +{ + for (let data of TEST_DATA) { + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = data.searchTerms; + let options = PlacesUtils.history.getNewQueryOptions(); + options.includeHidden = data.includeHidden; + + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + let cc = root.childCount; + // Live update with hidden visits. + yield task_populateDB(HIDDEN_VISITS); + let cc_update = root.childCount; + + root.containerOpen = false; + + do_check_eq(cc, data.expectedResults); + do_check_eq(cc_update, data.expectedResults + (data.includeHidden ? 1 : 0)); + + PlacesUtils.bhistory.removePage(uri("http://hidden.example.com/")); + } +}); diff --git a/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js new file mode 100644 index 000000000..7bd91f057 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that bookmarklets are returned by searches with searchTerms. + +var testData = [ + { isInQuery: true + , isBookmark: true + , title: "bookmark 1" + , uri: "http://mozilla.org/script/" + }, + + { isInQuery: true + , isBookmark: true + , title: "bookmark 2" + , uri: "javascript:alert('moz');" + } +]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_initalize() +{ + yield task_populateDB(testData); +}); + +add_test(function test_search_by_title() +{ + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "bookmark"; + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult(testData, root); + root.containerOpen = false; + + run_next_test(); +}); + +add_test(function test_search_by_schemeToken() +{ + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "script"; + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult(testData, root); + root.containerOpen = false; + + run_next_test(); +}); + +add_test(function test_search_by_uriAndTitle() +{ + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "moz"; + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult(testData, root); + root.containerOpen = false; + + run_next_test(); +}); diff --git a/toolkit/components/places/tests/queries/test_searchterms-domain.js b/toolkit/components/places/tests/queries/test_searchterms-domain.js new file mode 100644 index 000000000..4f42e7000 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_searchterms-domain.js @@ -0,0 +1,125 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + // The test data for our database, note that the ordering of the results that + // will be returned by the query (the isInQuery: true objects) is IMPORTANT. + // see compareArrayToResult in head_queries.js for more info. + var testData = [ + // Test ftp protocol - vary the title length, embed search term + {isInQuery: true, isVisit: true, isDetails: true, + uri: "ftp://foo.com/ftp", lastVisit: lastweek, + title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"}, + + // Test flat domain with annotation, search term in sentence + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://foo.com/", annoName: "moz/test", annoVal: "val", + lastVisit: lastweek, title: "you know, moz is cool"}, + + // Test subdomain included with isRedirect=true, different transtype + {isInQuery: true, isVisit: true, isDetails: true, title: "amozzie", + isRedirect: true, uri: "http://mail.foo.com/redirect", lastVisit: old, + referrer: "http://myreferrer.com", transType: PlacesUtils.history.TRANSITION_LINK}, + + // Test subdomain inclued, search term at end + {isInQuery: true, isVisit: true, isDetails: true, + uri: "http://mail.foo.com/yiihah", title: "blahmoz", lastVisit: daybefore}, + + // Test www. style URI is included, with a tag + {isInQuery: true, isVisit: true, isDetails: true, isTag: true, + uri: "http://www.foo.com/yiihah", tagArray: ["moz"], + lastVisit: yesterday, title: "foo"}, + + // Test https protocol + {isInQuery: true, isVisit: true, isDetails: true, title: "moz", + uri: "https://foo.com/", lastVisit: today}, + + // Begin the invalid queries: wrong search term + {isInQuery: false, isVisit:true, isDetails: true, title: "m o z", + uri: "http://foo.com/tooearly.php", lastVisit: today}, + + // Test bad URI + {isInQuery: false, isVisit:true, isDetails: true, title: "moz", + uri: "http://sffoo.com/justwrong.htm", lastVisit: yesterday}, + + // Test what we do with escaping in titles + {isInQuery: false, isVisit:true, isDetails: true, title: "m%0o%0z", + uri: "http://foo.com/changeme1.htm", lastVisit: yesterday}, + + // Test another invalid title - for updating later + {isInQuery: false, isVisit:true, isDetails: true, title: "m,oz", + uri: "http://foo.com/changeme2.htm", lastVisit: yesterday}]; + +/** + * This test will test Queries that use relative search terms and domain options + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_searchterms_domain() +{ + yield task_populateDB(testData); + var query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "moz"; + query.domain = "foo.com"; + query.domainIsHost = false; + + // Options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_ASCENDING; + options.resultType = options.RESULTS_AS_URI; + + // Results + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + do_print("Number of items in result set: " + root.childCount); + for (var i=0; i < root.childCount; ++i) { + do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title); + } + + // Check our inital result set + compareArrayToResult(testData, root); + + // If that passes, check liveupdate + // Add to the query set + do_print("Adding item to query"); + var change1 = [{isVisit: true, isDetails: true, uri: "http://foo.com/added.htm", + title: "moz", transType: PlacesUtils.history.TRANSITION_LINK}]; + yield task_populateDB(change1); + do_check_true(isInResult(change1, root)); + + // Update an existing URI + do_print("Updating Item"); + var change2 = [{isDetails: true, uri: "http://foo.com/changeme1.htm", + title: "moz" }]; + yield task_populateDB(change2); + do_check_true(isInResult(change2, root)); + + // Add one and take one out of query set, and simply change one so that it + // still applies to the query. + do_print("Updating More Items"); + var change3 = [{isDetails: true, uri:"http://foo.com/changeme2.htm", + title: "moz"}, + {isDetails: true, uri: "http://mail.foo.com/yiihah", + title: "moz now updated"}, + {isDetails: true, uri: "ftp://foo.com/ftp", title: "gone"}]; + yield task_populateDB(change3); + do_check_true(isInResult({uri: "http://foo.com/changeme2.htm"}, root)); + do_check_true(isInResult({uri: "http://mail.foo.com/yiihah"}, root)); + do_check_false(isInResult({uri: "ftp://foo.com/ftp"}, root)); + + // And now, delete one + do_print("Deleting items"); + var change4 = [{isDetails: true, uri: "https://foo.com/", + title: "mo,z"}]; + yield task_populateDB(change4); + do_check_false(isInResult(change4, root)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_searchterms-uri.js b/toolkit/components/places/tests/queries/test_searchterms-uri.js new file mode 100644 index 000000000..af4efe196 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_searchterms-uri.js @@ -0,0 +1,87 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + // The test data for our database, note that the ordering of the results that + // will be returned by the query (the isInQuery: true objects) is IMPORTANT. + // see compareArrayToResult in head_queries.js for more info. + var testData = [ + // Test flat domain with annotation, search term in sentence + {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true, + uri: "http://foo.com/", annoName: "moz/test", annoVal: "val", + lastVisit: lastweek, title: "you know, moz is cool"}, + + // Test https protocol + {isInQuery: false, isVisit: true, isDetails: true, title: "moz", + uri: "https://foo.com/", lastVisit: today}, + + // Begin the invalid queries: wrong search term + {isInQuery: false, isVisit:true, isDetails: true, title: "m o z", + uri: "http://foo.com/wrongsearch.php", lastVisit: today}, + + // Test subdomain inclued, search term at end + {isInQuery: false, isVisit: true, isDetails: true, + uri: "http://mail.foo.com/yiihah", title: "blahmoz", lastVisit: daybefore}, + + // Test ftp protocol - vary the title length, embed search term + {isInQuery: false, isVisit: true, isDetails: true, + uri: "ftp://foo.com/ftp", lastVisit: lastweek, + title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"}, + + // Test what we do with escaping in titles + {isInQuery: false, isVisit:true, isDetails: true, title: "m%0o%0z", + uri: "http://foo.com/changeme1.htm", lastVisit: yesterday}, + + // Test another invalid title - for updating later + {isInQuery: false, isVisit:true, isDetails: true, title: "m,oz", + uri: "http://foo.com/changeme2.htm", lastVisit: yesterday}]; + +/** + * This test will test Queries that use relative search terms and URI options + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_searchterms_uri() +{ + yield task_populateDB(testData); + var query = PlacesUtils.history.getNewQuery(); + query.searchTerms = "moz"; + query.uri = uri("http://foo.com"); + + // Options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_ASCENDING; + options.resultType = options.RESULTS_AS_URI; + + // Results + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + do_print("Number of items in result set: " + root.childCount); + for (var i=0; i < root.childCount; ++i) { + do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title); + } + + // Check our inital result set + compareArrayToResult(testData, root); + + // live update. + do_print("change title"); + var change1 = [{isDetails: true, uri:"http://foo.com/", + title: "mo"}, ]; + yield task_populateDB(change1); + + do_check_false(isInResult({uri: "http://foo.com/"}, root)); + var change2 = [{isDetails: true, uri:"http://foo.com/", + title: "moz"}, ]; + yield task_populateDB(change2); + do_check_true(isInResult({uri: "http://foo.com/"}, root)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js new file mode 100644 index 000000000..7ca50e6de --- /dev/null +++ b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js @@ -0,0 +1,225 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + * ***** END LICENSE BLOCK ***** */ + +// This test ensures that the date and site type of |place:| query maintains +// its quantifications correctly. Namely, it ensures that the date part of the +// query is not lost when the domain queries are made. + +// We specifically craft these entries so that if a by Date and Site sorting is +// applied, we find one domain in the today range, and two domains in the older +// than six months range. +// The correspondence between item in |testData| and date range is stored in +// leveledTestData. +var testData = [ + { + isVisit: true, + uri: "file:///directory/1", + lastVisit: today, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "http://example.com/1", + lastVisit: today, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "http://example.com/2", + lastVisit: today, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "file:///directory/2", + lastVisit: olderthansixmonths, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "http://example.com/3", + lastVisit: olderthansixmonths, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "http://example.com/4", + lastVisit: olderthansixmonths, + title: "test visit", + isInQuery: true + }, + { + isVisit: true, + uri: "http://example.net/1", + lastVisit: olderthansixmonths + 1000, + title: "test visit", + isInQuery: true + } +]; +var domainsInRange = [2, 3]; +var leveledTestData = [// Today + [[0], // Today, local files + [1, 2]], // Today, example.com + // Older than six months + [[3], // Older than six months, local files + [4, 5], // Older than six months, example.com + [6] // Older than six months, example.net + ]]; + +// This test data is meant for live updating. The |levels| property indicates +// date range index and then domain index. +var testDataAddedLater = [ + { + isVisit: true, + uri: "http://example.com/5", + lastVisit: olderthansixmonths, + title: "test visit", + isInQuery: true, + levels: [1, 1] + }, + { + isVisit: true, + uri: "http://example.com/6", + lastVisit: olderthansixmonths, + title: "test visit", + isInQuery: true, + levels: [1, 1] + }, + { + isVisit: true, + uri: "http://example.com/7", + lastVisit: today, + title: "test visit", + isInQuery: true, + levels: [0, 1] + }, + { + isVisit: true, + uri: "file:///directory/3", + lastVisit: today, + title: "test visit", + isInQuery: true, + levels: [0, 0] + } +]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_sort_date_site_grouping() +{ + yield task_populateDB(testData); + + // On Linux, the (local files) folder is shown after sites unlike Mac/Windows. + // Thus, we avoid running this test on Linux but this should be re-enabled + // after bug 624024 is resolved. + let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Components.classes); + if (isLinux) + return; + + // In this test, there are three levels of results: + // 1st: Date queries. e.g., today, last week, or older than 6 months. + // 2nd: Domain queries restricted to a date. e.g. mozilla.com today. + // 3rd: Actual visits. e.g. mozilla.com/index.html today. + // + // We store all the third level result roots so that we can easily close all + // containers and test live updating into specific results. + let roots = []; + + let query = PlacesUtils.history.getNewQuery(); + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY; + + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + // This corresponds to the number of date ranges. + do_check_eq(root.childCount, leveledTestData.length); + + // We pass off to |checkFirstLevel| to check the first level of results. + for (let index = 0; index < leveledTestData.length; index++) { + let node = root.getChild(index); + checkFirstLevel(index, node, roots); + } + + // Test live updating. + for (let visit of testDataAddedLater) { + yield task_populateDB([visit]); + let oldLength = testData.length; + let i = visit.levels[0]; + let j = visit.levels[1]; + testData.push(visit); + leveledTestData[i][j].push(oldLength); + compareArrayToResult(leveledTestData[i][j]. + map(x => testData[x]), roots[i][j]); + } + + for (let i = 0; i < roots.length; i++) { + for (let j = 0; j < roots[i].length; j++) + roots[i][j].containerOpen = false; + } + + root.containerOpen = false; +}); + +function checkFirstLevel(index, node, roots) { + PlacesUtils.asContainer(node).containerOpen = true; + + do_check_true(PlacesUtils.nodeIsDay(node)); + PlacesUtils.asQuery(node); + let queries = node.getQueries(); + let options = node.queryOptions; + + do_check_eq(queries.length, 1); + let query = queries[0]; + + do_check_true(query.hasBeginTime && query.hasEndTime); + + // Here we check the second level of results. + let root = PlacesUtils.history.executeQuery(query, options).root; + roots.push([]); + root.containerOpen = true; + + do_check_eq(root.childCount, leveledTestData[index].length); + for (var secondIndex = 0; secondIndex < root.childCount; secondIndex++) { + let child = PlacesUtils.asQuery(root.getChild(secondIndex)); + checkSecondLevel(index, secondIndex, child, roots); + } + root.containerOpen = false; + node.containerOpen = false; +} + +function checkSecondLevel(index, secondIndex, child, roots) { + let queries = child.getQueries(); + let options = child.queryOptions; + + do_check_eq(queries.length, 1); + let query = queries[0]; + + do_check_true(query.hasDomain); + do_check_true(query.hasBeginTime && query.hasEndTime); + + let root = PlacesUtils.history.executeQuery(query, options).root; + // We should now have that roots[index][secondIndex] is set to the second + // level's results root. + roots[index].push(root); + + // We pass off to compareArrayToResult to check the third level of + // results. + root.containerOpen = true; + compareArrayToResult(leveledTestData[index][secondIndex]. + map(x => testData[x]), root); + // We close |root|'s container later so that we can test live + // updates into it. +} diff --git a/toolkit/components/places/tests/queries/test_sorting.js b/toolkit/components/places/tests/queries/test_sorting.js new file mode 100644 index 000000000..4d8e1146d --- /dev/null +++ b/toolkit/components/places/tests/queries/test_sorting.js @@ -0,0 +1,1265 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var tests = []; + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, + + *setup() { + do_print("Sorting test 1: SORT BY NONE"); + + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/b", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "y", + keyword: "b", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "z", + keyword: "a", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "x", + keyword: "c", + isInQuery: true }, + ]; + + this._sortedData = this._unsortedData; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + // no reverse sorting for SORT BY NONE + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING, + + *setup() { + do_print("Sorting test 2: SORT BY TITLE"); + + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/b1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "y", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "z", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "x", + isInQuery: true }, + + // if titles are equal, should fall back to URI + { isBookmark: true, + uri: "http://example.com/b2", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "y", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[2], + this._unsortedData[0], + this._unsortedData[3], + this._unsortedData[1], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING, + + *setup() { + do_print("Sorting test 3: SORT BY DATE"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isVisit: true, + isDetails: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + uri: "http://example.com/c1", + lastVisit: timeInMicroseconds - 2000, + title: "x1", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + uri: "http://example.com/a", + lastVisit: timeInMicroseconds - 1000, + title: "z", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 2, + uri: "http://example.com/b", + lastVisit: timeInMicroseconds - 3000, + title: "y", + isInQuery: true }, + + // if dates are equal, should fall back to title + { isVisit: true, + isDetails: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 3, + uri: "http://example.com/c2", + lastVisit: timeInMicroseconds - 2000, + title: "x2", + isInQuery: true }, + + // if dates and title are equal, should fall back to bookmark index + { isVisit: true, + isDetails: true, + isBookmark: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 4, + uri: "http://example.com/c2", + lastVisit: timeInMicroseconds - 2000, + title: "x2", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[2], + this._unsortedData[0], + this._unsortedData[3], + this._unsortedData[4], + this._unsortedData[1], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING, + + *setup() { + do_print("Sorting test 4: SORT BY URI"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isBookmark: true, + isDetails: true, + lastVisit: timeInMicroseconds, + uri: "http://example.com/b", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + title: "y", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + title: "x", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 2, + title: "z", + isInQuery: true }, + + // if URIs are equal, should fall back to date + { isBookmark: true, + isDetails: true, + lastVisit: timeInMicroseconds + 1000, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 3, + title: "x", + isInQuery: true }, + + // if no URI (e.g., node is a folder), should fall back to title + { isFolder: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 4, + title: "a", + isInQuery: true }, + + // if URIs and dates are equal, should fall back to bookmark index + { isBookmark: true, + isDetails: true, + lastVisit: timeInMicroseconds + 1000, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 5, + title: "x", + isInQuery: true }, + + // if no URI and titles are equal, should fall back to bookmark index + { isFolder: true, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 6, + title: "a", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[4], + this._unsortedData[6], + this._unsortedData[2], + this._unsortedData[0], + this._unsortedData[1], + this._unsortedData[3], + this._unsortedData[5], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING, + + *setup() { + do_print("Sorting test 5: SORT BY VISITCOUNT"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/a", + lastVisit: timeInMicroseconds, + title: "z", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + lastVisit: timeInMicroseconds, + title: "x", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/b1", + lastVisit: timeInMicroseconds, + title: "y1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 2, + isInQuery: true }, + + // if visitCounts are equal, should fall back to date + { isBookmark: true, + uri: "http://example.com/b2", + lastVisit: timeInMicroseconds + 1000, + title: "y2a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 3, + isInQuery: true }, + + // if visitCounts and dates are equal, should fall back to bookmark index + { isBookmark: true, + uri: "http://example.com/b2", + lastVisit: timeInMicroseconds + 1000, + title: "y2b", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 4, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[0], + this._unsortedData[2], + this._unsortedData[3], + this._unsortedData[4], + this._unsortedData[1], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + // add visits to increase visit count + yield PlacesTestUtils.addVisits([ + { uri: uri("http://example.com/a"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 }, + { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 }, + { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds }, + ]); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING, + + *setup() { + do_print("Sorting test 6: SORT BY KEYWORD"); + + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "z", + keyword: "a", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "x", + keyword: "c", + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/b1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "y9", + keyword: "b", + isInQuery: true }, + + // without a keyword, should fall back to title + { isBookmark: true, + uri: "http://example.com/null2", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "null8", + keyword: null, + isInQuery: true }, + + // without a keyword, should fall back to title + { isBookmark: true, + uri: "http://example.com/null1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "null9", + keyword: null, + isInQuery: true }, + + // if keywords are equal, should fall back to title + { isBookmark: true, + uri: "http://example.com/b1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "y8", + keyword: "b", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[3], + this._unsortedData[4], + this._unsortedData[0], + this._unsortedData[5], + this._unsortedData[2], + this._unsortedData[1], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING, + + *setup() { + do_print("Sorting test 7: SORT BY DATEADDED"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/b1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + title: "y1", + dateAdded: timeInMicroseconds - 1000, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + title: "z", + dateAdded: timeInMicroseconds - 2000, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 2, + title: "x", + dateAdded: timeInMicroseconds, + isInQuery: true }, + + // if dateAddeds are equal, should fall back to title + { isBookmark: true, + uri: "http://example.com/b2", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 3, + title: "y2", + dateAdded: timeInMicroseconds - 1000, + isInQuery: true }, + + // if dateAddeds and titles are equal, should fall back to bookmark index + { isBookmark: true, + uri: "http://example.com/b3", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 4, + title: "y3", + dateAdded: timeInMicroseconds - 1000, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[3], + this._unsortedData[4], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING, + + *setup() { + do_print("Sorting test 8: SORT BY LASTMODIFIED"); + + var timeInMicroseconds = Date.now() * 1000; + var timeAddedInMicroseconds = timeInMicroseconds - 10000; + + this._unsortedData = [ + { isBookmark: true, + uri: "http://example.com/b1", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + title: "y1", + dateAdded: timeAddedInMicroseconds, + lastModified: timeInMicroseconds - 1000, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/a", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + title: "z", + dateAdded: timeAddedInMicroseconds, + lastModified: timeInMicroseconds - 2000, + isInQuery: true }, + + { isBookmark: true, + uri: "http://example.com/c", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 2, + title: "x", + dateAdded: timeAddedInMicroseconds, + lastModified: timeInMicroseconds, + isInQuery: true }, + + // if lastModifieds are equal, should fall back to title + { isBookmark: true, + uri: "http://example.com/b2", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 3, + title: "y2", + dateAdded: timeAddedInMicroseconds, + lastModified: timeInMicroseconds - 1000, + isInQuery: true }, + + // if lastModifieds and titles are equal, should fall back to bookmark + // index + { isBookmark: true, + uri: "http://example.com/b3", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 4, + title: "y3", + dateAdded: timeAddedInMicroseconds, + lastModified: timeInMicroseconds - 1000, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[3], + this._unsortedData[4], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING, + + *setup() { + do_print("Sorting test 9: SORT BY TAGS"); + + this._unsortedData = [ + { isBookmark: true, + uri: "http://url2.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title x", + isTag: true, + tagArray: ["x", "y", "z"], + isInQuery: true }, + + { isBookmark: true, + uri: "http://url1a.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title y1", + isTag: true, + tagArray: ["a", "b"], + isInQuery: true }, + + { isBookmark: true, + uri: "http://url3a.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title w1", + isInQuery: true }, + + { isBookmark: true, + uri: "http://url0.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title z", + isTag: true, + tagArray: ["a", "y", "z"], + isInQuery: true }, + + // if tags are equal, should fall back to title + { isBookmark: true, + uri: "http://url1b.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title y2", + isTag: true, + tagArray: ["b", "a"], + isInQuery: true }, + + // if tags are equal, should fall back to title + { isBookmark: true, + uri: "http://url3b.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "title w2", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[2], + this._unsortedData[5], + this._unsortedData[1], + this._unsortedData[4], + this._unsortedData[3], + this._unsortedData[0], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1); + query.onlyBookmarked = true; + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +// SORT_BY_ANNOTATION_* (int32) + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING, + + *setup() { + do_print("Sorting test 10: SORT BY ANNOTATION (int32)"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isVisit: true, + isDetails: true, + lastVisit: timeInMicroseconds, + uri: "http://example.com/b1", + title: "y1", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 2, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + lastVisit: timeInMicroseconds, + uri: "http://example.com/a", + title: "z", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 1, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + lastVisit: timeInMicroseconds, + uri: "http://example.com/c", + title: "x", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 3, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + // if annotations are equal, should fall back to title + { isVisit: true, + isDetails: true, + lastVisit: timeInMicroseconds, + uri: "http://example.com/b2", + title: "y2", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 2, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[3], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingAnnotation = "sorting"; + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +// SORT_BY_ANNOTATION_* (int64) + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING, + + *setup() { + do_print("Sorting test 11: SORT BY ANNOTATION (int64)"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: timeInMicroseconds, + title: "I", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 0xffffffff1, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://is.com/", + lastVisit: timeInMicroseconds, + title: "love", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 0xffffffff0, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://best.com/", + lastVisit: timeInMicroseconds, + title: "moz", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 0xffffffff2, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingAnnotation = "sorting"; + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +// SORT_BY_ANNOTATION_* (string) + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING, + + *setup() { + do_print("Sorting test 12: SORT BY ANNOTATION (string)"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: timeInMicroseconds, + title: "I", + isPageAnnotation: true, + annoName: "sorting", + annoVal: "a", + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://is.com/", + lastVisit: timeInMicroseconds, + title: "love", + isPageAnnotation: true, + annoName: "sorting", + annoVal: "", + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://best.com/", + lastVisit: timeInMicroseconds, + title: "moz", + isPageAnnotation: true, + annoName: "sorting", + annoVal: "z", + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingAnnotation = "sorting"; + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +// SORT_BY_ANNOTATION_* (double) + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING, + + *setup() { + do_print("Sorting test 13: SORT BY ANNOTATION (double)"); + + var timeInMicroseconds = Date.now() * 1000; + this._unsortedData = [ + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: timeInMicroseconds, + title: "I", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 1.2, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://is.com/", + lastVisit: timeInMicroseconds, + title: "love", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 1.1, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://best.com/", + lastVisit: timeInMicroseconds, + title: "moz", + isPageAnnotation: true, + annoName: "sorting", + annoVal: 1.3, + annoFlags: 0, + annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER, + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[1], + this._unsortedData[0], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + // Query + var query = PlacesUtils.history.getNewQuery(); + + // query options + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingAnnotation = "sorting"; + options.sortingMode = this._sortingMode; + + // Results - this gets the result set and opens it for reading and modification. + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +// SORT_BY_FRECENCY_* + +tests.push({ + _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_ASCENDING, + + *setup() { + do_print("Sorting test 13: SORT BY FRECENCY "); + + let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000); + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; + } + + this._unsortedData = [ + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: newTimeInMicroseconds(), + title: "I", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: newTimeInMicroseconds(), + title: "I", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://moz.com/", + lastVisit: newTimeInMicroseconds(), + title: "I", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://is.com/", + lastVisit: newTimeInMicroseconds(), + title: "love", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://best.com/", + lastVisit: newTimeInMicroseconds(), + title: "moz", + isInQuery: true }, + + { isVisit: true, + isDetails: true, + uri: "http://best.com/", + lastVisit: newTimeInMicroseconds(), + title: "moz", + isInQuery: true }, + ]; + + this._sortedData = [ + this._unsortedData[3], + this._unsortedData[5], + this._unsortedData[2], + ]; + + // This function in head_queries.js creates our database with the above data + yield task_populateDB(this._unsortedData); + }, + + check: function() { + var query = PlacesUtils.history.getNewQuery(); + var options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = this._sortingMode; + + var root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + compareArrayToResult(this._sortedData, root); + root.containerOpen = false; + }, + + check_reverse: function() { + this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING; + this._sortedData.reverse(); + this.check(); + } +}); + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_sorting() +{ + for (let test of tests) { + yield test.setup(); + yield PlacesTestUtils.promiseAsyncUpdates(); + test.check(); + // sorting reversed, usually SORT_BY have ASC and DESC + test.check_reverse(); + // Execute cleanup tasks + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); + } +}); diff --git a/toolkit/components/places/tests/queries/test_tags.js b/toolkit/components/places/tests/queries/test_tags.js new file mode 100644 index 000000000..afda3f03f --- /dev/null +++ b/toolkit/components/places/tests/queries/test_tags.js @@ -0,0 +1,743 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests bookmark and history queries with tags. See bug 399799. + */ + +"use strict"; + +add_task(function* tags_getter_setter() { + do_print("Tags getter/setter should work correctly"); + do_print("Without setting tags, tags getter should return empty array"); + var [query] = makeQuery(); + do_check_eq(query.tags.length, 0); + + do_print("Setting tags to an empty array, tags getter should return "+ + "empty array"); + [query] = makeQuery([]); + do_check_eq(query.tags.length, 0); + + do_print("Setting a few tags, tags getter should return correct array"); + var tags = ["bar", "baz", "foo"]; + [query] = makeQuery(tags); + setsAreEqual(query.tags, tags, true); + + do_print("Setting some dupe tags, tags getter return unique tags"); + [query] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]); + setsAreEqual(query.tags, ["bar", "baz", "foo"], true); +}); + +add_task(function* invalid_setter_calls() { + do_print("Invalid calls to tags setter should fail"); + try { + var query = PlacesUtils.history.getNewQuery(); + query.tags = null; + do_throw("Passing null to SetTags should fail"); + } + catch (exc) {} + + try { + query = PlacesUtils.history.getNewQuery(); + query.tags = "this should not work"; + do_throw("Passing a string to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([null]); + do_throw("Passing one-element array with null to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([undefined]); + do_throw("Passing one-element array with undefined to SetTags " + + "should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", null, "bar"]); + do_throw("Passing mixture of tags and null to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", undefined, "bar"]); + do_throw("Passing mixture of tags and undefined to SetTags " + + "should fail"); + } + catch (exc) {} + + try { + makeQuery([1, 2, 3]); + do_throw("Passing numbers to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", 1, 2, 3]); + do_throw("Passing mixture of tags and numbers to SetTags should fail"); + } + catch (exc) {} + + try { + var str = PlacesUtils.toISupportsString("foo"); + query = PlacesUtils.history.getNewQuery(); + query.tags = str; + do_throw("Passing nsISupportsString to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([str]); + do_throw("Passing array of nsISupportsStrings to SetTags should fail"); + } + catch (exc) {} +}); + +add_task(function* not_setting_tags() { + do_print("Not setting tags at all should not affect query URI"); + checkQueryURI(); +}); + +add_task(function* empty_array_tags() { + do_print("Setting tags with an empty array should not affect query URI"); + checkQueryURI([]); +}); + +add_task(function* set_tags() { + do_print("Setting some tags should result in correct query URI"); + checkQueryURI([ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ]); +}); + +add_task(function* no_tags_tagsAreNot() { + do_print("Not setting tags at all but setting tagsAreNot should " + + "affect query URI"); + checkQueryURI(null, true); +}); + +add_task(function* empty_array_tags_tagsAreNot() { + do_print("Setting tags with an empty array and setting tagsAreNot " + + "should affect query URI"); + checkQueryURI([], true); +}); + +add_task(function* () { + do_print("Setting some tags and setting tagsAreNot should result in " + + "correct query URI"); + checkQueryURI([ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ], true); +}); + +add_task(function* tag_to_uri() { + do_print("Querying history on tag associated with a URI should return " + + "that URI"); + yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* tags_to_uri() { + do_print("Querying history on many tags associated with a URI should " + + "return that URI"); + yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bar"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* repeated_tag() { + do_print("Specifying the same tag multiple times in a history query " + + "should not matter"); + yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "foo"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* many_tags_no_uri() { + do_print("Querying history on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); + yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + }); +}); + +add_task(function* nonexistent_tags() { + do_print("Querying history on nonexistent tags should return no results"); + yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["bogus", "gnarly"]); + executeAndCheckQueryResults(query, opts, []); + }); +}); + +add_task(function* tag_to_bookmark() { + do_print("Querying bookmarks on tag associated with a URI should " + + "return that URI"); + yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* many_tags_to_bookmark() { + do_print("Querying bookmarks on many tags associated with a URI " + + "should return that URI"); + yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bar"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* repeated_tag_to_bookmarks() { + do_print("Specifying the same tag multiple times in a bookmark query " + + "should not matter"); + yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "foo"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); +}); + +add_task(function* many_tags_no_bookmark() { + do_print("Querying bookmarks on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); + yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + }); +}); + +add_task(function* nonexistent_tags_bookmark() { + do_print("Querying bookmarks on nonexistent tag should return no results"); + yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["bogus", "gnarly"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + }); +}); + +add_task(function* tagsAreNot_history() { + do_print("Querying history using tagsAreNot should work correctly"); + var urisAndTags = { + "http://example.com/1": ["foo", "bar"], + "http://example.com/2": ["baz", "qux"], + "http://example.com/3": null + }; + + do_print("Add visits and tag the URIs"); + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + yield PlacesTestUtils.addVisits(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_print(' Querying for "foo" should match only /2 and /3'); + var [query, opts] = makeQuery(["foo"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "bar" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bar"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "bogus" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bogus"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "baz" should match only /3'); + [query, opts] = makeQuery(["foo", "baz"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/3"]); + + do_print(' Querying for "bogus" should match all'); + [query, opts] = makeQuery(["bogus"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/1", + "http://example.com/2", + "http://example.com/3"]); + + // Clean up. + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + yield task_cleanDatabase(); +}); + +add_task(function* tagsAreNot_bookmarks() { + do_print("Querying bookmarks using tagsAreNot should work correctly"); + var urisAndTags = { + "http://example.com/1": ["foo", "bar"], + "http://example.com/2": ["baz", "qux"], + "http://example.com/3": null + }; + + do_print("Add bookmarks and tag the URIs"); + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + yield addBookmark(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_print(' Querying for "foo" should match only /2 and /3'); + var [query, opts] = makeQuery(["foo"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "bar" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bar"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "bogus" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bogus"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_print(' Querying for "foo" and "baz" should match only /3'); + [query, opts] = makeQuery(["foo", "baz"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/3"]); + + do_print(' Querying for "bogus" should match all'); + [query, opts] = makeQuery(["bogus"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/1", + "http://example.com/2", + "http://example.com/3"]); + + // Clean up. + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + yield task_cleanDatabase(); +}); + +add_task(function* duplicate_tags() { + do_print("Duplicate existing tags (i.e., multiple tag folders with " + + "same name) should not throw off query results"); + var tagName = "foo"; + + do_print("Add bookmark and tag it normally"); + yield addBookmark(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); + + do_print("Manually create tag folder with same name as tag and insert " + + "bookmark"); + let dupTag = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.tagsGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: tagName + }); + + yield PlacesUtils.bookmarks.insert({ + parentGuid: dupTag.guid, + title: "title", + url: TEST_URI + }); + + do_print("Querying for tag should match URI"); + var [query, opts] = makeQuery([tagName]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); + + PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); + yield task_cleanDatabase(); +}); + +add_task(function* folder_named_as_tag() { + do_print("Regular folders with the same name as tag should not throw " + + "off query results"); + var tagName = "foo"; + + do_print("Add bookmark and tag it"); + yield addBookmark(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); + + do_print("Create folder with same name as tag"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: tagName + }); + + do_print("Querying for tag should match URI"); + var [query, opts] = makeQuery([tagName]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); + + PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); + yield task_cleanDatabase(); +}); + +add_task(function* ORed_queries() { + do_print("Multiple queries ORed together should work"); + var urisAndTags = { + "http://example.com/1": [], + "http://example.com/2": [] + }; + + // Search with lots of tags to make sure tag parameter substitution in SQL + // can handle it with more than one query. + for (let i = 0; i < 11; i++) { + urisAndTags["http://example.com/1"].push("/1 tag " + i); + urisAndTags["http://example.com/2"].push("/2 tag " + i); + } + + do_print("Add visits and tag the URIs"); + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + yield PlacesTestUtils.addVisits(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_print("Query for /1 OR query for /2 should match both /1 and /2"); + var [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + var [query2] = makeQuery(urisAndTags["http://example.com/2"]); + var root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + do_print("Query for /1 OR query on bogus tag should match only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2] = makeQuery(["bogus"]); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_print("Query for /1 OR query for /1 should match only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2] = makeQuery(urisAndTags["http://example.com/1"]); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_print("Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " + + "should match both /1 and /2"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"], true); + [query2] = makeQuery(urisAndTags["http://example.com/2"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + do_print("Query for /1 OR query for /2 with tagsAreNot should match " + + "only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2] = makeQuery(urisAndTags["http://example.com/2"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_print("Query for /1 OR query for /1 with tagsAreNot should match " + + "both URIs"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2] = makeQuery(urisAndTags["http://example.com/1"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + // Clean up. + for (let [pURI, tags] of Object.entries(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + yield task_cleanDatabase(); +}); + +// The tag keys in query URIs, i.e., "place:tag=foo&!tags=1" +// --- ----- +const QUERY_KEY_TAG = "tag"; +const QUERY_KEY_NOT_TAGS = "!tags"; + +const TEST_URI = uri("http://example.com/"); + +/** + * Adds a bookmark. + * + * @param aURI + * URI of the page (an nsIURI) + */ +function addBookmark(aURI) { + return PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: aURI.spec, + url: aURI + }); +} + +/** + * Asynchronous task that removes all pages from history and bookmarks. + */ +function* task_cleanDatabase(aCallback) { + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +} + +/** + * Sets up a query with the specified tags, converts it to a URI, and makes sure + * the URI is what we expect it to be. + * + * @param aTags + * The query's tags will be set to those in this array + * @param aTagsAreNot + * The query's tagsAreNot property will be set to this + */ +function checkQueryURI(aTags, aTagsAreNot) { + var pairs = (aTags || []).sort().map(t => QUERY_KEY_TAG + "=" + encodeTag(t)); + if (aTagsAreNot) + pairs.push(QUERY_KEY_NOT_TAGS + "=1"); + var expURI = "place:" + pairs.join("&"); + var [query, opts] = makeQuery(aTags, aTagsAreNot); + var actualURI = queryURI(query, opts); + do_print("Query URI should be what we expect for the given tags"); + do_check_eq(actualURI, expURI); +} + +/** + * Asynchronous task that executes a callback task in a "scoped" database state. + * A bookmark is added and tagged before the callback is called, and afterward + * the database is cleared. + * + * @param aTags + * A bookmark will be added and tagged with this array of tags + * @param aCallback + * A task function that will be called after the bookmark has been tagged + */ +function* task_doWithBookmark(aTags, aCallback) { + yield addBookmark(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, aTags); + yield aCallback(TEST_URI); + PlacesUtils.tagging.untagURI(TEST_URI, aTags); + yield task_cleanDatabase(); +} + +/** + * Asynchronous task that executes a callback function in a "scoped" database + * state. A history visit is added and tagged before the callback is called, + * and afterward the database is cleared. + * + * @param aTags + * A history visit will be added and tagged with this array of tags + * @param aCallback + * A function that will be called after the visit has been tagged + */ +function* task_doWithVisit(aTags, aCallback) { + yield PlacesTestUtils.addVisits(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, aTags); + yield aCallback(TEST_URI); + PlacesUtils.tagging.untagURI(TEST_URI, aTags); + yield task_cleanDatabase(); +} + +/** + * queriesToQueryString() encodes every character in the query URI that doesn't + * match /[a-zA-Z]/. There's no simple JavaScript function that does the same, + * but encodeURIComponent() comes close, only missing some punctuation. This + * function takes care of all of that. + * + * @param aTag + * A tag name to encode + * @return A UTF-8 escaped string suitable for inclusion in a query URI + */ +function encodeTag(aTag) { + return encodeURIComponent(aTag). + replace(/[-_.!~*'()]/g, // ' + s => "%" + s.charCodeAt(0).toString(16)); +} + +/** + * Executes the given query and compares the results to the given URIs. + * See queryResultsAre(). + * + * @param aQuery + * An nsINavHistoryQuery + * @param aQueryOpts + * An nsINavHistoryQueryOptions + * @param aExpectedURIs + * Array of URIs (as strings) that aResultRoot should contain + */ +function executeAndCheckQueryResults(aQuery, aQueryOpts, aExpectedURIs) { + var root = PlacesUtils.history.executeQuery(aQuery, aQueryOpts).root; + root.containerOpen = true; + queryResultsAre(root, aExpectedURIs); + root.containerOpen = false; +} + +/** + * Returns new query and query options objects. The query's tags will be + * set to aTags. aTags may be null, in which case setTags() is not called at + * all on the query. + * + * @param aTags + * The query's tags will be set to those in this array + * @param aTagsAreNot + * The query's tagsAreNot property will be set to this + * @return [query, queryOptions] + */ +function makeQuery(aTags, aTagsAreNot) { + aTagsAreNot = !!aTagsAreNot; + do_print("Making a query " + + (aTags ? + "with tags " + aTags.toSource() : + "without calling setTags() at all") + + " and with tagsAreNot=" + + aTagsAreNot); + var query = PlacesUtils.history.getNewQuery(); + query.tagsAreNot = aTagsAreNot; + if (aTags) { + query.tags = aTags; + var uniqueTags = []; + aTags.forEach(function (t) { + if (typeof(t) === "string" && uniqueTags.indexOf(t) < 0) + uniqueTags.push(t); + }); + uniqueTags.sort(); + } + + do_print("Made query should be correct for tags and tagsAreNot"); + if (uniqueTags) + setsAreEqual(query.tags, uniqueTags, true); + var expCount = uniqueTags ? uniqueTags.length : 0; + do_check_eq(query.tags.length, expCount); + do_check_eq(query.tagsAreNot, aTagsAreNot); + + return [query, PlacesUtils.history.getNewQueryOptions()]; +} + +/** + * Ensures that the URIs of aResultRoot are the same as those in aExpectedURIs. + * + * @param aResultRoot + * The nsINavHistoryContainerResultNode root of an nsINavHistoryResult + * @param aExpectedURIs + * Array of URIs (as strings) that aResultRoot should contain + */ +function queryResultsAre(aResultRoot, aExpectedURIs) { + var rootWasOpen = aResultRoot.containerOpen; + if (!rootWasOpen) + aResultRoot.containerOpen = true; + var actualURIs = []; + for (let i = 0; i < aResultRoot.childCount; i++) { + actualURIs.push(aResultRoot.getChild(i).uri); + } + setsAreEqual(actualURIs, aExpectedURIs); + if (!rootWasOpen) + aResultRoot.containerOpen = false; +} + +/** + * Converts the given query into its query URI. + * + * @param aQuery + * An nsINavHistoryQuery + * @param aQueryOpts + * An nsINavHistoryQueryOptions + * @return The query's URI + */ +function queryURI(aQuery, aQueryOpts) { + return PlacesUtils.history.queriesToQueryString([aQuery], 1, aQueryOpts); +} + +/** + * Ensures that the arrays contain the same elements and, optionally, in the + * same order. + */ +function setsAreEqual(aArr1, aArr2, aIsOrdered) { + do_check_eq(aArr1.length, aArr2.length); + if (aIsOrdered) { + for (let i = 0; i < aArr1.length; i++) { + do_check_eq(aArr1[i], aArr2[i]); + } + } + else { + aArr1.forEach(u => do_check_true(aArr2.indexOf(u) >= 0)); + aArr2.forEach(u => do_check_true(aArr1.indexOf(u) >= 0)); + } +} + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/queries/test_transitions.js b/toolkit/components/places/tests/queries/test_transitions.js new file mode 100644 index 000000000..bbd4c9e01 --- /dev/null +++ b/toolkit/components/places/tests/queries/test_transitions.js @@ -0,0 +1,178 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + * ***** END LICENSE BLOCK ***** */ +var beginTime = Date.now(); +var testData = [ + { + isVisit: true, + title: "page 0", + uri: "http://mozilla.com/", + transType: Ci.nsINavHistoryService.TRANSITION_TYPED + }, + { + isVisit: true, + title: "page 1", + uri: "http://google.com/", + transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + }, + { + isVisit: true, + title: "page 2", + uri: "http://microsoft.com/", + transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + }, + { + isVisit: true, + title: "page 3", + uri: "http://en.wikipedia.org/", + transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK + }, + { + isVisit: true, + title: "page 4", + uri: "http://fr.wikipedia.org/", + transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + }, + { + isVisit: true, + title: "page 5", + uri: "http://apple.com/", + transType: Ci.nsINavHistoryService.TRANSITION_TYPED + }, + { + isVisit: true, + title: "page 6", + uri: "http://campus-bike-store.com/", + transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + }, + { + isVisit: true, + title: "page 7", + uri: "http://uwaterloo.ca/", + transType: Ci.nsINavHistoryService.TRANSITION_TYPED + }, + { + isVisit: true, + title: "page 8", + uri: "http://pugcleaner.com/", + transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK + }, + { + isVisit: true, + title: "page 9", + uri: "http://de.wikipedia.org/", + transType: Ci.nsINavHistoryService.TRANSITION_TYPED + }, + { + isVisit: true, + title: "arewefastyet", + uri: "http://arewefastyet.com/", + transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + }, + { + isVisit: true, + title: "arewefastyet", + uri: "http://arewefastyet.com/", + transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK + }]; +// sets of indices of testData array by transition type +var testDataTyped = [0, 5, 7, 9]; +var testDataDownload = [1, 2, 4, 6, 10]; +var testDataBookmark = [3, 8, 11]; + +/** + * run_test is where the magic happens. This is automatically run by the test + * harness. It is where you do the work of creating the query, running it, and + * playing with the result set. + */ +function run_test() +{ + run_next_test(); +} + +add_task(function* test_transitions() +{ + let timeNow = Date.now(); + for (let item of testData) { + yield PlacesTestUtils.addVisits({ + uri: uri(item.uri), + transition: item.transType, + visitDate: timeNow++ * 1000, + title: item.title + }); + } + + // dump_table("moz_places"); + // dump_table("moz_historyvisits"); + + var numSortFunc = function (a, b) { return (a - b); }; + var arrs = testDataTyped.concat(testDataDownload).concat(testDataBookmark) + .sort(numSortFunc); + + // Four tests which compare the result of a query to an expected set. + var data = arrs.filter(function (index) { + return (testData[index].uri.match(/arewefastyet\.com/) && + testData[index].transType == + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD); + }); + + compareQueryToTestData("place:domain=arewefastyet.com&transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, + data.slice()); + + compareQueryToTestData("place:transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, + testDataDownload.slice()); + + compareQueryToTestData("place:transition=" + + Ci.nsINavHistoryService.TRANSITION_TYPED, + testDataTyped.slice()); + + compareQueryToTestData("place:transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + + "&transition=" + + Ci.nsINavHistoryService.TRANSITION_BOOKMARK, + data); + + // Tests the live update property of transitions. + var query = {}; + var options = {}; + PlacesUtils.history. + queryStringToQueries("place:transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, + query, {}, options); + query = (query.value)[0]; + options = PlacesUtils.history.getNewQueryOptions(); + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(testDataDownload.length, root.childCount); + yield PlacesTestUtils.addVisits({ + uri: uri("http://getfirefox.com"), + transition: TRANSITION_DOWNLOAD + }); + do_check_eq(testDataDownload.length + 1, root.childCount); + root.containerOpen = false; +}); + +/* + * Takes a query and a set of indices. The indices correspond to elements + * of testData that are the result of the query. + */ +function compareQueryToTestData(queryStr, data) { + var query = {}; + var options = {}; + PlacesUtils.history.queryStringToQueries(queryStr, query, {}, options); + query = query.value[0]; + options = options.value; + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + for (var i = 0; i < data.length; i++) { + data[i] = testData[data[i]]; + data[i].isInQuery = true; + } + compareArrayToResult(data, root); +} diff --git a/toolkit/components/places/tests/queries/xpcshell.ini b/toolkit/components/places/tests/queries/xpcshell.ini new file mode 100644 index 000000000..7ff864679 --- /dev/null +++ b/toolkit/components/places/tests/queries/xpcshell.ini @@ -0,0 +1,34 @@ +[DEFAULT] +head = head_queries.js +tail = +skip-if = toolkit == 'android' + +[test_415716.js] +[test_abstime-annotation-domain.js] +[test_abstime-annotation-uri.js] +[test_async.js] +[test_containersQueries_sorting.js] +[test_history_queries_tags_liveUpdate.js] +[test_history_queries_titles_liveUpdate.js] +[test_onlyBookmarked.js] +[test_queryMultipleFolder.js] +[test_querySerialization.js] +[test_redirects.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_results-as-tag-contents-query.js] +[test_results-as-visit.js] +[test_searchterms-domain.js] +[test_searchterms-uri.js] +[test_searchterms-bookmarklets.js] +[test_sort-date-site-grouping.js] +[test_sorting.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_tags.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_transitions.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_searchTerms_includeHidden.js] diff --git a/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js b/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml b/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml new file mode 100644 index 000000000..f4baad28a --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-rel-searchform.xml</ShortName> +<Url type="text/html" method="GET" template="http://example.com/?search" rel="searchform"/> +</SearchPlugin> diff --git a/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml b/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml new file mode 100644 index 000000000..a322a7c86 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-suggestions.xml</ShortName> +<Url type="application/x-suggestions+json" + method="GET" + template="http://localhost:9000/suggest?{searchTerms}"/> +<Url type="text/html" + method="GET" + template="http://localhost:9000/search" + rel="searchform"/> +</SearchPlugin> diff --git a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js new file mode 100644 index 000000000..11e917e18 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js @@ -0,0 +1,505 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +const FRECENCY_DEFAULT = 10000; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://testing-common/httpd.js"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + +const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 "; + +function run_test() { + run_next_test(); +} + +function* cleanup() { + Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled"); + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.typed"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + let suggestPrefs = [ + "history", + "bookmark", + "history.onlyTyped", + "openpage", + "searches", + ]; + for (let type of suggestPrefs) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +} +do_register_cleanup(cleanup); + +/** + * @param aSearches + * Array of AutoCompleteSearch names. + */ +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + popup: { + selectedIndex: -1, + invalidate: function () {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]) + }, + popupOpen: false, + + disableAutoComplete: false, + completeDefaultIndex: true, + completeSelectedIndex: true, + forceComplete: false, + + minResultsForPopup: 0, + maxRows: 0, + + showCommentColumn: false, + showImageColumn: false, + + timeout: 10, + searchParam: "", + + get searchCount() { + return this.searches.length; + }, + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + textValue: "", + // Text selection range + _selStart: 0, + _selEnd: 0, + get selectionStart() { + return this._selStart; + }, + get selectionEnd() { + return this._selEnd; + }, + selectTextRange: function(aStart, aEnd) { + this._selStart = aStart; + this._selEnd = aEnd; + }, + + onSearchBegin: function () {}, + onSearchComplete: function () {}, + + onTextEntered: () => false, + onTextReverted: () => false, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput]) +} + +// A helper for check_autocomplete to check a specific match against data from +// the controller. +function _check_autocomplete_matches(match, result) { + let { uri, title, tags, style } = match; + if (tags) + title += " \u2013 " + tags.sort().join(", "); + if (style) + style = style.sort(); + else + style = ["favicon"]; + + do_print(`Checking against expected "${uri.spec}", "${title}"`); + // Got a match on both uri and title? + if (stripPrefix(uri.spec) != stripPrefix(result.value) || title != result.comment) { + return false; + } + + let actualStyle = result.style.split(/\s+/).sort(); + if (style) + Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style"); + if (uri.spec.startsWith("moz-action:")) { + Assert.ok(actualStyle.includes("action"), "moz-action results should always have 'action' in their style"); + } + + if (match.icon) + Assert.equal(result.image, match.icon, "Match should have expected image"); + + return true; +} + +function* check_autocomplete(test) { + // At this point frecency could still be updating due to latest pages + // updates. + // This is not a problem in real life, but autocomplete tests should + // return reliable resultsets, thus we have to wait. + yield PlacesTestUtils.promiseAsyncUpdates(); + + // Make an AutoCompleteInput that uses our searches and confirms results. + let input = new AutoCompleteInput(["unifiedcomplete"]); + input.textValue = test.search; + + if (test.searchParam) + input.searchParam = test.searchParam; + + // Caret must be at the end for autoFill to happen. + let strLen = test.search.length; + input.selectTextRange(strLen, strLen); + Assert.equal(input.selectionStart, strLen, "Selection starts at end"); + Assert.equal(input.selectionEnd, strLen, "Selection ends at the end"); + + let controller = Cc["@mozilla.org/autocomplete/controller;1"] + .getService(Ci.nsIAutoCompleteController); + controller.input = input; + + let numSearchesStarted = 0; + input.onSearchBegin = () => { + do_print("onSearchBegin received"); + numSearchesStarted++; + }; + let searchCompletePromise = new Promise(resolve => { + input.onSearchComplete = () => { + do_print("onSearchComplete received"); + resolve(); + } + }); + let expectedSearches = 1; + if (test.incompleteSearch) { + controller.startSearch(test.incompleteSearch); + expectedSearches++; + } + + do_print("Searching for: '" + test.search + "'"); + controller.startSearch(test.search); + yield searchCompletePromise; + + Assert.equal(numSearchesStarted, expectedSearches, "All searches started"); + + // Check to see the expected uris and titles match up. If 'enable-actions' + // is specified, we check that the first specified match is the first + // controller value (as this is the "special" always selected item), but the + // rest can match in any order. + // If 'enable-actions' is not specified, they can match in any order. + if (test.matches) { + // Do not modify the test original matches. + let matches = test.matches.slice(); + + if (matches.length) { + let firstIndexToCheck = 0; + if (test.searchParam && test.searchParam.includes("enable-actions")) { + firstIndexToCheck = 1; + do_print("Checking first match is first autocomplete entry") + let result = { + value: controller.getValueAt(0), + comment: controller.getCommentAt(0), + style: controller.getStyleAt(0), + image: controller.getImageAt(0), + } + do_print(`First match is "${result.value}", "${result.comment}"`); + Assert.ok(_check_autocomplete_matches(matches[0], result), "first item is correct"); + do_print("Checking rest of the matches"); + } + + for (let i = firstIndexToCheck; i < controller.matchCount; i++) { + let result = { + value: controller.getValueAt(i), + comment: controller.getCommentAt(i), + style: controller.getStyleAt(i), + image: controller.getImageAt(i), + } + do_print(`Looking for "${result.value}", "${result.comment}" in expected results...`); + let lowerBound = test.checkSorting ? i : firstIndexToCheck; + let upperBound = test.checkSorting ? i + 1 : matches.length; + let found = false; + for (let j = lowerBound; j < upperBound; ++j) { + // Skip processed expected results + if (matches[j] == undefined) + continue; + if (_check_autocomplete_matches(matches[j], result)) { + do_print("Got a match at index " + j + "!"); + // Make it undefined so we don't process it again + matches[j] = undefined; + found = true; + break; + } + } + + if (!found) + do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`); // ' (Emacs syntax highlighting fix) + } + } + + Assert.equal(controller.matchCount, matches.length, + "Got as many results as expected"); + + // If we expect results, make sure we got matches. + do_check_eq(controller.searchStatus, matches.length ? + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH : + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH); + } + + if (test.autofilled) { + // Check the autoFilled result. + Assert.equal(input.textValue, test.autofilled, + "Autofilled value is correct"); + + // Now force completion and check correct casing of the result. + // This ensures the controller is able to do its magic case-preserving + // stuff and correct replacement of the user's casing with result's one. + controller.handleEnter(false); + Assert.equal(input.textValue, test.completed, + "Completed value is correct"); + } +} + +var addBookmark = Task.async(function* (aBookmarkObj) { + Assert.ok(!!aBookmarkObj.uri, "Bookmark object contains an uri"); + let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId + : PlacesUtils.unfiledBookmarksFolderId; + + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: (yield PlacesUtils.promiseItemGuid(parentId)), + title: aBookmarkObj.title || "A bookmark", + url: aBookmarkObj.uri + }); + yield PlacesUtils.promiseItemId(bm.guid); + + if (aBookmarkObj.keyword) { + yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword, + url: aBookmarkObj.uri.spec, + postData: aBookmarkObj.postData + }); + } + + if (aBookmarkObj.tags) { + PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags); + } +}); + +function addOpenPages(aUri, aCount=1, aUserContextId=0) { + let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Ci.mozIPlacesAutoComplete); + for (let i = 0; i < aCount; i++) { + ac.registerOpenPage(aUri, aUserContextId); + } +} + +function removeOpenPages(aUri, aCount=1, aUserContextId=0) { + let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Ci.mozIPlacesAutoComplete); + for (let i = 0; i < aCount; i++) { + ac.unregisterOpenPage(aUri, aUserContextId); + } +} + +function changeRestrict(aType, aChar) { + let branch = "browser.urlbar."; + // "title" and "url" are different from everything else, so special case them. + if (aType == "title" || aType == "url") + branch += "match."; + else + branch += "restrict."; + + do_print("changing restrict for " + aType + " to '" + aChar + "'"); + Services.prefs.setCharPref(branch + aType, aChar); +} + +function resetRestrict(aType) { + let branch = "browser.urlbar."; + // "title" and "url" are different from everything else, so special case them. + if (aType == "title" || aType == "url") + branch += "match."; + else + branch += "restrict."; + + Services.prefs.clearUserPref(branch + aType); +} + +/** + * Strip prefixes from the URI that we don't care about for searching. + * + * @param spec + * The text to modify. + * @return the modified spec. + */ +function stripPrefix(spec) +{ + ["http://", "https://", "ftp://"].some(scheme => { + if (spec.startsWith(scheme)) { + spec = spec.slice(scheme.length); + return true; + } + return false; + }); + + if (spec.startsWith("www.")) { + spec = spec.slice(4); + } + return spec; +} + +function makeActionURI(action, params) { + let encodedParams = {}; + for (let key in params) { + encodedParams[key] = encodeURIComponent(params[key]); + } + let url = "moz-action:" + action + "," + JSON.stringify(encodedParams); + return NetUtil.newURI(url); +} + +// Creates a full "match" entry for a search result, suitable for passing as +// an entry to check_autocomplete. +function makeSearchMatch(input, extra = {}) { + // Note that counter-intuitively, the order the object properties are defined + // in the object passed to makeActionURI is important for check_autocomplete + // to match them :( + let params = { + engineName: extra.engineName || "MozSearch", + input, + searchQuery: "searchQuery" in extra ? extra.searchQuery : input, + }; + if ("alias" in extra) { + // May be undefined, which is expected, but in that case make sure it's not + // included in the params of the moz-action URL. + params.alias = extra.alias; + } + let style = [ "action", "searchengine" ]; + if (Array.isArray(extra.style)) { + style.push(...extra.style); + } + if (extra.heuristic) { + style.push("heuristic"); + } + return { + uri: makeActionURI("searchengine", params), + title: params.engineName, + style, + } +} + +// Creates a full "match" entry for a search result, suitable for passing as +// an entry to check_autocomplete. +function makeVisitMatch(input, url, extra = {}) { + // Note that counter-intuitively, the order the object properties are defined + // in the object passed to makeActionURI is important for check_autocomplete + // to match them :( + let params = { + url, + input, + } + let style = [ "action", "visiturl" ]; + if (extra.heuristic) { + style.push("heuristic"); + } + return { + uri: makeActionURI("visiturl", params), + title: extra.title || url, + style, + } +} + +function makeSwitchToTabMatch(url, extra = {}) { + return { + uri: makeActionURI("switchtab", {url}), + title: extra.title || url, + style: [ "action", "switchtab" ], + } +} + +function makeExtensionMatch(extra = {}) { + let style = [ "action", "extension" ]; + if (extra.heuristic) { + style.push("heuristic"); + } + + return { + uri: makeActionURI("extension", { + content: extra.content, + keyword: extra.keyword, + }), + title: extra.description, + style, + }; +} + +function setFaviconForHref(href, iconHref) { + return new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + NetUtil.newURI(href), + NetUtil.newURI(iconHref), + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }); +} + +function makeTestServer(port=-1) { + let httpServer = new HttpServer(); + httpServer.start(port); + do_register_cleanup(() => httpServer.stop(() => {})); + return httpServer; +} + +function* addTestEngine(basename, httpServer=undefined) { + httpServer = httpServer || makeTestServer(); + httpServer.registerDirectory("/", do_get_cwd()); + let dataUrl = + "http://localhost:" + httpServer.identity.primaryPort + "/data/"; + + do_print("Adding engine: " + basename); + return yield new Promise(resolve => { + Services.obs.addObserver(function obs(subject, topic, data) { + let engine = subject.QueryInterface(Ci.nsISearchEngine); + do_print("Observed " + data + " for " + engine.name); + if (data != "engine-added" || engine.name != basename) { + return; + } + + Services.obs.removeObserver(obs, "browser-search-engine-modified"); + do_register_cleanup(() => Services.search.removeEngine(engine)); + resolve(engine); + }, "browser-search-engine-modified", false); + + do_print("Adding engine from URL: " + dataUrl + basename); + Services.search.addEngine(dataUrl + basename, null, null, false); + }); +} + +// Ensure we have a default search engine and the keyword.enabled preference +// set. +add_task(function* ensure_search_engine() { + // keyword.enabled is necessary for the tests to see keyword searches. + Services.prefs.setBoolPref("keyword.enabled", true); + + // Initialize the search service, but first set this geo IP pref to a dummy + // string. When the search service is initialized, it contacts the URI named + // in this pref, which breaks the test since outside connections aren't + // allowed. + let geoPref = "browser.search.geoip.url"; + Services.prefs.setCharPref(geoPref, ""); + do_register_cleanup(() => Services.prefs.clearUserPref(geoPref)); + yield new Promise(resolve => { + Services.search.init(resolve); + }); + + // Remove any existing engines before adding ours. + for (let engine of Services.search.getEngines()) { + Services.search.removeEngine(engine); + } + Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET", + "http://s.example.com/search"); + let engine = Services.search.getEngineByName("MozSearch"); + Services.search.currentEngine = engine; +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416211.js b/toolkit/components/places/tests/unifiedcomplete/test_416211.js new file mode 100644 index 000000000..e02906ddc --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_416211.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test bug 416211 to make sure results that match the tag show the bookmark + * title instead of the page title. + */ + +add_task(function* test_tag_match_has_bookmark_title() { + do_print("Make sure the tag match gives the bookmark title"); + let uri = NetUtil.newURI("http://theuri/"); + yield PlacesTestUtils.addVisits({ uri: uri, title: "Page title" }); + yield addBookmark({ uri: uri, + title: "Bookmark title", + tags: [ "superTag" ]}); + yield check_autocomplete({ + search: "superTag", + matches: [ { uri: uri, title: "Bookmark title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ] + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416214.js b/toolkit/components/places/tests/unifiedcomplete/test_416214.js new file mode 100644 index 000000000..a30b3fe74 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_416214.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test autocomplete for non-English URLs that match the tag bug 416214. Also + * test bug 417441 by making sure escaped ascii characters like "+" remain + * escaped. + * + * - add a visit for a page with a non-English URL + * - add a tag for the page + * - search for the tag + * - test number of matches (should be exactly one) + * - make sure the url is decoded + */ + +add_task(function* test_tag_match_url() { + do_print("Make sure tag matches return the right url as well as '+' remain escaped"); + let uri1 = NetUtil.newURI("http://escaped/ユニコード"); + let uri2 = NetUtil.newURI("http://asciiescaped/blocking-firefox3%2B"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" } + ]); + yield addBookmark({ uri: uri1, + title: "title", + tags: [ "superTag" ], + style: [ "bookmark-tag" ] }); + yield addBookmark({ uri: uri2, + title: "title", + tags: [ "superTag" ], + style: [ "bookmark-tag" ] }); + yield check_autocomplete({ + search: "superTag", + matches: [ { uri: uri1, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] }, + { uri: uri2, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ] + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_417798.js b/toolkit/components/places/tests/unifiedcomplete/test_417798.js new file mode 100644 index 000000000..bed14b2ce --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_417798.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 417798 to make sure javascript: URIs don't show up unless the + * user searches for javascript: explicitly. + */ + +add_task(function* test_javascript_match() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + + let uri1 = NetUtil.newURI("http://abc/def"); + let uri2 = NetUtil.newURI("javascript:5"); + yield PlacesTestUtils.addVisits([ { uri: uri1, title: "Title with javascript:" } ]); + yield addBookmark({ uri: uri2, + title: "Title with javascript:" }); + + do_print("Match non-javascript: with plain search"); + yield check_autocomplete({ + search: "a", + matches: [ { uri: uri1, title: "Title with javascript:" } ] + }); + + do_print("Match non-javascript: with almost javascript:"); + yield check_autocomplete({ + search: "javascript", + matches: [ { uri: uri1, title: "Title with javascript:" } ] + }); + + do_print("Match javascript:"); + yield check_autocomplete({ + search: "javascript:", + matches: [ { uri: uri1, title: "Title with javascript:" }, + { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ] + }); + + do_print("Match nothing with non-first javascript:"); + yield check_autocomplete({ + search: "5 javascript:", + matches: [ ] + }); + + do_print("Match javascript: with multi-word search"); + yield check_autocomplete({ + search: "javascript: 5", + matches: [ { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_418257.js b/toolkit/components/places/tests/unifiedcomplete/test_418257.js new file mode 100644 index 000000000..323c2a7af --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_418257.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 418257 by making sure tags are returned with the title as part of + * the "comment" if there are tags even if we didn't match in the tags. They + * are separated from the title by a endash. + */ + +add_task(function* test_javascript_match() { + let uri1 = NetUtil.newURI("http://page1"); + let uri2 = NetUtil.newURI("http://page2"); + let uri3 = NetUtil.newURI("http://page3"); + let uri4 = NetUtil.newURI("http://page4"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "tagged" }, + { uri: uri2, title: "tagged" }, + { uri: uri3, title: "tagged" }, + { uri: uri4, title: "tagged" } + ]); + yield addBookmark({ uri: uri1, + title: "tagged", + tags: [ "tag1" ] }); + yield addBookmark({ uri: uri2, + title: "tagged", + tags: [ "tag1", "tag2" ] }); + yield addBookmark({ uri: uri3, + title: "tagged", + tags: [ "tag1", "tag3" ] }); + yield addBookmark({ uri: uri4, + title: "tagged", + tags: [ "tag1", "tag2", "tag3" ] }); + + do_print("Make sure tags come back in the title when matching tags"); + yield check_autocomplete({ + search: "page1 tag", + matches: [ { uri: uri1, title: "tagged", tags: [ "tag1" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Check tags in title for page2"); + yield check_autocomplete({ + search: "page2 tag", + matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Make sure tags appear even when not matching the tag"); + yield check_autocomplete({ + search: "page3", + matches: [ { uri: uri3, title: "tagged", tags: [ "tag1", "tag3" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Multiple tags come in commas for page4"); + yield check_autocomplete({ + search: "page4", + matches: [ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Extra test just to make sure we match the title"); + yield check_autocomplete({ + search: "tag2", + matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] }, + { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_422277.js b/toolkit/components/places/tests/unifiedcomplete/test_422277.js new file mode 100644 index 000000000..df6f7601a --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_422277.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 422277 to make sure bad escaped uris don't get escaped. This makes + * sure we don't hit an assertion for "not a UTF8 string". + */ + +add_task(function* test_javascript_match() { + do_print("Bad escaped uri stays escaped"); + let uri1 = NetUtil.newURI("http://site/%EAid"); + yield PlacesTestUtils.addVisits([ { uri: uri1, title: "title" } ]); + yield check_autocomplete({ + search: "site", + matches: [ { uri: uri1, title: "title" } ] + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js new file mode 100644 index 000000000..cd2dfdb17 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Functional tests for inline autocomplete + +const PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled"; + +add_task(function* test_disabling_autocomplete() { + do_print("Check disabling autocomplete disables autofill"); + Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, false); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://visit.mozilla.org"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "vis", + autofilled: "vis", + completed: "vis" + }); + yield cleanup(); +}); + +add_task(function* test_urls_order() { + do_print("Add urls, check for correct order"); + let places = [{ uri: NetUtil.newURI("http://visit1.mozilla.org") }, + { uri: NetUtil.newURI("http://visit2.mozilla.org"), + transition: TRANSITION_TYPED }]; + yield PlacesTestUtils.addVisits(places); + yield check_autocomplete({ + search: "vis", + autofilled: "visit2.mozilla.org/", + completed: "visit2.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_ignore_prefix() { + do_print("Add urls, make sure www and http are ignored"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit1.mozilla.org")); + yield check_autocomplete({ + search: "visit1", + autofilled: "visit1.mozilla.org/", + completed: "visit1.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_after_host() { + do_print("Autocompleting after an existing host completes to the url"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit3.mozilla.org")); + yield check_autocomplete({ + search: "visit3.mozilla.org/", + autofilled: "visit3.mozilla.org/", + completed: "visit3.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_respect_www() { + do_print("Searching for www.me should yield www.me.mozilla.org/"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.me.mozilla.org")); + yield check_autocomplete({ + search: "www.me", + autofilled: "www.me.mozilla.org/", + completed: "www.me.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_bookmark_first() { + do_print("With a bookmark and history, the query result should be the bookmark"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield addBookmark({ uri: NetUtil.newURI("http://bookmark1.mozilla.org/") }); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo")); + yield check_autocomplete({ + search: "bookmark", + autofilled: "bookmark1.mozilla.org/", + completed: "bookmark1.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_full_path() { + do_print("Check to make sure we get the proper results with full paths"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, + { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; + yield PlacesTestUtils.addVisits(places); + yield check_autocomplete({ + search: "smokey", + autofilled: "smokey.mozilla.org/", + completed: "smokey.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_complete_to_slash() { + do_print("Check to make sure we autocomplete to the following '/'"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, + { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; + yield PlacesTestUtils.addVisits(places); + yield check_autocomplete({ + search: "smokey.mozilla.org/fo", + autofilled: "smokey.mozilla.org/foo/", + completed: "http://smokey.mozilla.org/foo/", + }); + yield cleanup(); +}); + +add_task(function* test_complete_to_slash_with_www() { + do_print("Check to make sure we autocomplete to the following '/'"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + let places = [{ uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, + { uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; + yield PlacesTestUtils.addVisits(places); + yield check_autocomplete({ + search: "smokey.mozilla.org/fo", + autofilled: "smokey.mozilla.org/foo/", + completed: "http://www.smokey.mozilla.org/foo/", + }); + yield cleanup(); +}); + +add_task(function* test_complete_querystring() { + do_print("Check to make sure we autocomplete after ?"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious")); + yield check_autocomplete({ + search: "smokey.mozilla.org/foo?", + autofilled: "smokey.mozilla.org/foo?bacon=delicious", + completed: "http://smokey.mozilla.org/foo?bacon=delicious", + }); + yield cleanup(); +}); + +add_task(function* test_complete_fragment() { + do_print("Check to make sure we autocomplete after #"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar")); + yield check_autocomplete({ + search: "smokey.mozilla.org/foo?bacon=delicious#bar", + autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar", + completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar", + }); + yield cleanup(); +}); + +add_task(function* test_autocomplete_enabled_pref() { + Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, false); + let types = ["history", "bookmark", "openpage"]; + for (type of types) { + do_check_eq(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false, + "suggest." + type + "pref should be false"); + } + Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, true); + for (type of types) { + do_check_eq(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true, + "suggest." + type + "pref should be true"); + } + + // Clear prefs. + Services.prefs.clearUserPref(PREF_AUTOCOMPLETE_ENABLED); + for (type of types) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js new file mode 100644 index 000000000..ecc96266b --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Need to test that removing a page from autocomplete actually removes a page + * Description From Shawn Wilsher :sdwilsh 2009-02-18 11:29:06 PST + * We don't test the code path of onValueRemoved + * for the autocomplete implementation + * Bug 479089 + */ + +add_task(function* test_autocomplete_on_value_removed() { + let listener = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]. + getService(Components.interfaces.nsIAutoCompleteSimpleResultListener); + + let testUri = NetUtil.newURI("http://foo.mozilla.com/"); + yield PlacesTestUtils.addVisits({ + uri: testUri, + referrer: uri("http://mozilla.com/") + }); + + let query = PlacesUtils.history.getNewQuery(); + let options = PlacesUtils.history.getNewQueryOptions(); + // look for this uri only + query.uri = testUri; + + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, 1); + // call the untested code path + listener.onValueRemoved(null, testUri.spec, true); + // make sure it is GONE from the DB + Assert.equal(root.childCount, 0); + // close the container + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js b/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js new file mode 100644 index 000000000..482fcf485 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js @@ -0,0 +1,310 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test autoFill for different default behaviors. + */ + +add_task(function* test_default_behavior_host() { + let uri1 = NetUtil.newURI("http://typed/"); + let uri2 = NetUtil.newURI("http://visited/"); + let uri3 = NetUtil.newURI("http://bookmarked/"); + let uri4 = NetUtil.newURI("http://tpbk/"); + let uri5 = NetUtil.newURI("http://tagged/"); + + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "typed", transition: TRANSITION_TYPED }, + { uri: uri2, title: "visited" }, + { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED }, + ]); + yield addBookmark( { uri: uri3, title: "bookmarked" } ); + yield addBookmark( { uri: uri4, title: "tpbk" } ); + yield addBookmark( { uri: uri5, title: "title", tags: ["foo"] } ); + + yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png"); + yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png"); + + // RESTRICT TO HISTORY. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + + do_print("Restrict history, common visit, should not autoFill"); + yield check_autocomplete({ + search: "vi", + matches: [ { uri: uri2, title: "visited" } ], + autofilled: "vi", + completed: "vi" + }); + + do_print("Restrict history, typed visit, should autoFill"); + yield check_autocomplete({ + search: "ty", + matches: [ { uri: uri1, title: "typed", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/information-16.png" } ], + autofilled: "typed/", + completed: "typed/" + }); + + // Don't autoFill this one cause it's not typed. + do_print("Restrict history, bookmark, should not autoFill"); + yield check_autocomplete({ + search: "bo", + matches: [ ], + autofilled: "bo", + completed: "bo" + }); + + // Note we don't show this one cause it's not typed. + do_print("Restrict history, typed bookmark, should autoFill"); + yield check_autocomplete({ + search: "tp", + matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ], + autofilled: "tpbk/", + completed: "tpbk/" + }); + + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + + // We are not restricting on typed, so we autoFill the bookmark even if we + // are restricted to history. We accept that cause not doing that + // would be a perf hit and the privacy implications are very weak. + do_print("Restrict history, bookmark, autoFill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "bo", + matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/error-16.png" } ], + autofilled: "bookmarked/", + completed: "bookmarked/" + }); + + do_print("Restrict history, common visit, autoFill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "vi", + matches: [ { uri: uri2, title: "visited", style: [ "autofill", "heuristic" ] } ], + autofilled: "visited/", + completed: "visited/" + }); + + // RESTRICT TO TYPED. + // This should basically ignore autoFill.typed and acts as if it would be set. + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); + + // Typed behavior basically acts like history, but filters on typed. + do_print("Restrict typed, common visit, autoFill.typed = false, should not autoFill"); + yield check_autocomplete({ + search: "vi", + matches: [ ], + autofilled: "vi", + completed: "vi" + }); + + do_print("Restrict typed, typed visit, autofill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "ty", + matches: [ { uri: uri1, title: "typed", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/information-16.png"} ], + autofilled: "typed/", + completed: "typed/" + }); + + do_print("Restrict typed, bookmark, autofill.typed = false, should not autoFill"); + yield check_autocomplete({ + search: "bo", + matches: [ ], + autofilled: "bo", + completed: "bo" + }); + + do_print("Restrict typed, typed bookmark, autofill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "tp", + matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ], + autofilled: "tpbk/", + completed: "tpbk/" + }); + + // RESTRICT BOOKMARKS. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true); + + do_print("Restrict bookmarks, common visit, should not autoFill"); + yield check_autocomplete({ + search: "vi", + matches: [ ], + autofilled: "vi", + completed: "vi" + }); + + do_print("Restrict bookmarks, typed visit, should not autoFill"); + yield check_autocomplete({ + search: "ty", + matches: [ ], + autofilled: "ty", + completed: "ty" + }); + + // Don't autoFill this one cause it's not typed. + do_print("Restrict bookmarks, bookmark, should not autoFill"); + yield check_autocomplete({ + search: "bo", + matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ], + icon: "chrome://global/skin/icons/error-16.png"} ], + autofilled: "bo", + completed: "bo" + }); + + // Note we don't show this one cause it's not typed. + do_print("Restrict bookmarks, typed bookmark, should autoFill"); + yield check_autocomplete({ + search: "tp", + matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ], + autofilled: "tpbk/", + completed: "tpbk/" + }); + + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + + do_print("Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "bo", + matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/error-16.png" } ], + autofilled: "bookmarked/", + completed: "bookmarked/" + }); + + // Don't autofill because it's a title. + do_print("Restrict bookmarks, title, autofill.typed = false, should not autoFill"); + yield check_autocomplete({ + search: "# ta", + matches: [ ], + autofilled: "# ta", + completed: "# ta" + }); + + // Don't autofill because it's a tag. + do_print("Restrict bookmarks, tag, autofill.typed = false, should not autoFill"); + yield check_autocomplete({ + search: "+ ta", + matches: [ { uri: uri5, title: "title", tags: [ "foo" ], style: [ "tag" ] } ], + autofilled: "+ ta", + completed: "+ ta" + }); + + yield cleanup(); +}); + +add_task(function* test_default_behavior_url() { + let uri1 = NetUtil.newURI("http://typed/ty/"); + let uri2 = NetUtil.newURI("http://visited/vi/"); + let uri3 = NetUtil.newURI("http://bookmarked/bo/"); + let uri4 = NetUtil.newURI("http://tpbk/tp/"); + + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "typed", transition: TRANSITION_TYPED }, + { uri: uri2, title: "visited" }, + { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED }, + ]); + yield addBookmark( { uri: uri3, title: "bookmarked" } ); + yield addBookmark( { uri: uri4, title: "tpbk" } ); + + yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png"); + yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png"); + + // RESTRICT TO HISTORY. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true); + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + + do_print("URL: Restrict history, common visit, should not autoFill"); + yield check_autocomplete({ + search: "visited/v", + matches: [ { uri: uri2, title: "visited" } ], + autofilled: "visited/v", + completed: "visited/v" + }); + + do_print("URL: Restrict history, typed visit, should autoFill"); + yield check_autocomplete({ + search: "typed/t", + matches: [ { uri: uri1, title: "typed/ty/", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/information-16.png"} ], + autofilled: "typed/ty/", + completed: "http://typed/ty/" + }); + + // Don't autoFill this one cause it's not typed. + do_print("URL: Restrict history, bookmark, should not autoFill"); + yield check_autocomplete({ + search: "bookmarked/b", + matches: [ ], + autofilled: "bookmarked/b", + completed: "bookmarked/b" + }); + + // Note we don't show this one cause it's not typed. + do_print("URL: Restrict history, typed bookmark, should autoFill"); + yield check_autocomplete({ + search: "tpbk/t", + matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill", "heuristic" ] } ], + autofilled: "tpbk/tp/", + completed: "http://tpbk/tp/" + }); + + // RESTRICT BOOKMARKS. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + + do_print("URL: Restrict bookmarks, common visit, should not autoFill"); + yield check_autocomplete({ + search: "visited/v", + matches: [ ], + autofilled: "visited/v", + completed: "visited/v" + }); + + do_print("URL: Restrict bookmarks, typed visit, should not autoFill"); + yield check_autocomplete({ + search: "typed/t", + matches: [ ], + autofilled: "typed/t", + completed: "typed/t" + }); + + // Don't autoFill this one cause it's not typed. + do_print("URL: Restrict bookmarks, bookmark, should not autoFill"); + yield check_autocomplete({ + search: "bookmarked/b", + matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ], + icon: "chrome://global/skin/icons/error-16.png" } ], + autofilled: "bookmarked/b", + completed: "bookmarked/b" + }); + + // Note we don't show this one cause it's not typed. + do_print("URL: Restrict bookmarks, typed bookmark, should autoFill"); + yield check_autocomplete({ + search: "tpbk/t", + matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill", "heuristic" ] } ], + autofilled: "tpbk/tp/", + completed: "http://tpbk/tp/" + }); + + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + + do_print("URL: Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); + yield check_autocomplete({ + search: "bookmarked/b", + matches: [ { uri: uri3, title: "bookmarked/bo/", style: [ "autofill", "heuristic" ], + icon: "chrome://global/skin/icons/error-16.png" } ], + autofilled: "bookmarked/bo/", + completed: "http://bookmarked/bo/" + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js new file mode 100644 index 000000000..54fc343ca --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_prefix_space_noautofill() { + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://moz.org/test/"), + transition: TRANSITION_TYPED + }); + + do_print("Should not try to autoFill if search string contains a space"); + yield check_autocomplete({ + search: " mo", + autofilled: " mo", + completed: " mo" + }); + + yield cleanup(); +}); + +add_task(function* test_trailing_space_noautofill() { + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://moz.org/test/"), + transition: TRANSITION_TYPED + }); + + do_print("Should not try to autoFill if search string contains a space"); + yield check_autocomplete({ + search: "mo ", + autofilled: "mo ", + completed: "mo " + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_autofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("CakeSearch", "", "", "", + "GET", "http://cake.search/"); + let engine = Services.search.getEngineByName("CakeSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + do_print("Should autoFill search engine if search string does not contains a space"); + yield check_autocomplete({ + search: "ca", + autofilled: "cake.search", + completed: "http://cake.search" + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_prefix_space_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("CupcakeSearch", "", "", "", + "GET", "http://cupcake.search/"); + let engine = Services.search.getEngineByName("CupcakeSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + do_print("Should not try to autoFill search engine if search string contains a space"); + yield check_autocomplete({ + search: " cu", + autofilled: " cu", + completed: " cu" + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_trailing_space_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("BaconSearch", "", "", "", + "GET", "http://bacon.search/"); + let engine = Services.search.getEngineByName("BaconSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + do_print("Should not try to autoFill search engine if search string contains a space"); + yield check_autocomplete({ + search: "ba ", + autofilled: "ba ", + completed: "ba " + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_www_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("HamSearch", "", "", "", + "GET", "http://ham.search/"); + let engine = Services.search.getEngineByName("HamSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + do_print("Should not autoFill search engine if search string contains www. but engine doesn't"); + yield check_autocomplete({ + search: "www.ham", + autofilled: "www.ham", + completed: "www.ham" + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_different_scheme_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("PieSearch", "", "", "", + "GET", "https://pie.search/"); + let engine = Services.search.getEngineByName("PieSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + do_print("Should not autoFill search engine if search string has a different scheme."); + yield check_autocomplete({ + search: "http://pie", + autofilled: "http://pie", + completed: "http://pie" + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_matching_prefix_autofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("BeanSearch", "", "", "", + "GET", "http://www.bean.search/"); + let engine = Services.search.getEngineByName("BeanSearch"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + + do_print("Should autoFill search engine if search string has matching prefix."); + yield check_autocomplete({ + search: "http://www.be", + autofilled: "http://www.bean.search", + completed: "http://www.bean.search" + }) + + do_print("Should autoFill search engine if search string has www prefix."); + yield check_autocomplete({ + search: "www.be", + autofilled: "www.bean.search", + completed: "http://www.bean.search" + }); + + do_print("Should autoFill search engine if search string has matching scheme."); + yield check_autocomplete({ + search: "http://be", + autofilled: "http://bean.search", + completed: "http://www.bean.search" + }); + + yield cleanup(); +}); + +add_task(function* test_prefix_autofill() { + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://moz.org/test/"), + transition: TRANSITION_TYPED + }); + + do_print("Should not try to autoFill in-the-middle if a search is canceled immediately"); + yield check_autocomplete({ + incompleteSearch: "moz", + search: "mozi", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js new file mode 100644 index 000000000..1fcfe1c75 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_protocol_trimming() { + for (let prot of ["http", "https", "ftp"]) { + let visit = { + // Include the protocol in the query string to ensure we get matches (see bug 1059395) + uri: NetUtil.newURI(prot + "://www.mozilla.org/test/?q=" + prot + encodeURIComponent("://") + "www.foo"), + title: "Test title", + transition: TRANSITION_TYPED + }; + yield PlacesTestUtils.addVisits(visit); + let matches = [{uri: visit.uri, title: visit.title}]; + + let inputs = [ + prot + "://", + prot + ":// ", + prot + ":// mo", + prot + "://mo te", + prot + "://www.", + prot + "://www. ", + prot + "://www. mo", + prot + "://www.mo te", + "www.", + "www. ", + "www. mo", + "www.mo te" + ]; + for (let input of inputs) { + do_print("Searching for: " + input); + yield check_autocomplete({ + search: input, + matches: matches + }); + } + + yield cleanup(); + } +}); + diff --git a/toolkit/components/places/tests/unifiedcomplete/test_casing.js b/toolkit/components/places/tests/unifiedcomplete/test_casing.js new file mode 100644 index 000000000..585b51be1 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_casing.js @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_casing_1() { + do_print("Searching for cased entry 1"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "MOZ", + autofilled: "MOZilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_casing_2() { + do_print("Searching for cased entry 2"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/T", + autofilled: "mozilla.org/T", + completed: "mozilla.org/T" + }); + yield cleanup(); +}); + +add_task(function* test_casing_3() { + do_print("Searching for cased entry 3"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/T", + autofilled: "mozilla.org/Test/", + completed: "http://mozilla.org/Test/" + }); + yield cleanup(); +}); + +add_task(function* test_casing_4() { + do_print("Searching for cased entry 4"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mOzilla.org/t", + autofilled: "mOzilla.org/t", + completed: "mOzilla.org/t" + }); + yield cleanup(); +}); + +add_task(function* test_casing_5() { + do_print("Searching for cased entry 5"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mOzilla.org/T", + autofilled: "mOzilla.org/Test/", + completed: "http://mozilla.org/Test/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_casing() { + do_print("Searching for untrimmed cased entry"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://mOz", + autofilled: "http://mOzilla.org/", + completed: "http://mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_www_casing() { + do_print("Searching for untrimmed cased entry with www"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://www.mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://www.mOz", + autofilled: "http://www.mOzilla.org/", + completed: "http://www.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_path_casing() { + do_print("Searching for untrimmed cased entry with path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://mOzilla.org/t", + autofilled: "http://mOzilla.org/t", + completed: "http://mOzilla.org/t" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_path_casing_2() { + do_print("Searching for untrimmed cased entry with path 2"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://mOzilla.org/T", + autofilled: "http://mOzilla.org/Test/", + completed: "http://mozilla.org/Test/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_path_www_casing() { + do_print("Searching for untrimmed cased entry with www and path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://www.mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://www.mOzilla.org/t", + autofilled: "http://www.mOzilla.org/t", + completed: "http://www.mOzilla.org/t" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_path_www_casing_2() { + do_print("Searching for untrimmed cased entry with www and path 2"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://www.mozilla.org/Test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "http://www.mOzilla.org/T", + autofilled: "http://www.mOzilla.org/Test/", + completed: "http://www.mozilla.org/Test/" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js new file mode 100644 index 000000000..014d74998 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Inline should never return matches shorter than the search string, since +// that largely confuses completeDefaultIndex + +add_task(function* test_not_autofill_ws_1() { + do_print("Do not autofill whitespaced entry 1"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org ", + autofilled: "mozilla.org ", + completed: "mozilla.org " + }); + yield cleanup(); +}); + +add_task(function* test_not_autofill_ws_2() { + do_print("Do not autofill whitespaced entry 2"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/ ", + autofilled: "mozilla.org/ ", + completed: "mozilla.org/ " + }); + yield cleanup(); +}); + +add_task(function* test_not_autofill_ws_3() { + do_print("Do not autofill whitespaced entry 3"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/link ", + autofilled: "mozilla.org/link ", + completed: "mozilla.org/link " + }); + yield cleanup(); +}); + +add_task(function* test_not_autofill_ws_4() { + do_print("Do not autofill whitespaced entry 4"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/link/ ", + autofilled: "mozilla.org/link/ ", + completed: "mozilla.org/link/ " + }); + yield cleanup(); +}); + + +add_task(function* test_not_autofill_ws_5() { + do_print("Do not autofill whitespaced entry 5"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "moz illa ", + autofilled: "moz illa ", + completed: "moz illa " + }); + yield cleanup(); +}); + +add_task(function* test_not_autofill_ws_6() { + do_print("Do not autofill whitespaced entry 6"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: " mozilla", + autofilled: " mozilla", + completed: " mozilla" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js new file mode 100644 index 000000000..72661d075 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js @@ -0,0 +1,71 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests bug 449406 to ensure that TRANSITION_DOWNLOAD, TRANSITION_EMBED and + * TRANSITION_FRAMED_LINK bookmarked uri's show up in the location bar. + */ + +add_task(function* test_download_embed_bookmarks() { + let uri1 = NetUtil.newURI("http://download/bookmarked"); + let uri2 = NetUtil.newURI("http://embed/bookmarked"); + let uri3 = NetUtil.newURI("http://framed/bookmarked"); + let uri4 = NetUtil.newURI("http://download"); + let uri5 = NetUtil.newURI("http://embed"); + let uri6 = NetUtil.newURI("http://framed"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "download-bookmark", transition: TRANSITION_DOWNLOAD }, + { uri: uri2, title: "embed-bookmark", transition: TRANSITION_EMBED }, + { uri: uri3, title: "framed-bookmark", transition: TRANSITION_FRAMED_LINK}, + { uri: uri4, title: "download2", transition: TRANSITION_DOWNLOAD }, + { uri: uri5, title: "embed2", transition: TRANSITION_EMBED }, + { uri: uri6, title: "framed2", transition: TRANSITION_FRAMED_LINK } + ]); + yield addBookmark({ uri: uri1, + title: "download-bookmark" }); + yield addBookmark({ uri: uri2, + title: "embed-bookmark" }); + yield addBookmark({ uri: uri3, + title: "framed-bookmark" }); + + do_print("Searching for bookmarked download uri matches"); + yield check_autocomplete({ + search: "download-bookmark", + matches: [ { uri: uri1, title: "download-bookmark", style: [ "bookmark" ] } ] + }); + + do_print("Searching for bookmarked embed uri matches"); + yield check_autocomplete({ + search: "embed-bookmark", + matches: [ { uri: uri2, title: "embed-bookmark", style: [ "bookmark" ] } ] + }); + + do_print("Searching for bookmarked framed uri matches"); + yield check_autocomplete({ + search: "framed-bookmark", + matches: [ { uri: uri3, title: "framed-bookmark", style: [ "bookmark" ] } ] + }); + + do_print("Searching for download uri does not match"); + yield check_autocomplete({ + search: "download2", + matches: [ ] + }); + + do_print("Searching for embed uri does not match"); + yield check_autocomplete({ + search: "embed2", + matches: [ ] + }); + + do_print("Searching for framed uri does not match"); + yield check_autocomplete({ + search: "framed2", + matches: [ ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js new file mode 100644 index 000000000..a39c15236 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure inline autocomplete doesn't return zero frecency pages. + +add_task(function* test_dupe_urls() { + do_print("Searching for urls with dupes should only show one"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/"), + transition: TRANSITION_TYPED + }, { + uri: NetUtil.newURI("http://mozilla.org/?") + }); + yield check_autocomplete({ + search: "moz", + autofilled: "mozilla.org/", + completed: "mozilla.org/", + matches: [ { uri: NetUtil.newURI("http://mozilla.org/"), + title: "mozilla.org", + style: [ "autofill", "heuristic" ] } ] + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js new file mode 100644 index 000000000..ef1159705 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 426864 that makes sure the empty search (drop down list) only + * shows typed pages from history. + */ + +add_task(function* test_javascript_match() { + let uri1 = NetUtil.newURI("http://t.foo/0"); + let uri2 = NetUtil.newURI("http://t.foo/1"); + let uri3 = NetUtil.newURI("http://t.foo/2"); + let uri4 = NetUtil.newURI("http://t.foo/3"); + let uri5 = NetUtil.newURI("http://t.foo/4"); + let uri6 = NetUtil.newURI("http://t.foo/5"); + let uri7 = NetUtil.newURI("http://t.foo/6"); + + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + { uri: uri3, title: "title", transition: TRANSITION_TYPED}, + { uri: uri4, title: "title", transition: TRANSITION_TYPED }, + { uri: uri6, title: "title", transition: TRANSITION_TYPED }, + { uri: uri7, title: "title" } + ]); + + yield addBookmark({ uri: uri2, + title: "title" }); + yield addBookmark({ uri: uri4, + title: "title" }); + yield addBookmark({ uri: uri5, + title: "title" }); + yield addBookmark({ uri: uri6, + title: "title" }); + + addOpenPages(uri7, 1); + + // Now remove page 6 from history, so it is an unvisited bookmark. + PlacesUtils.history.removePage(uri6); + + do_print("Match everything"); + yield check_autocomplete({ + search: "foo", + searchParam: "enable-actions", + matches: [ makeSearchMatch("foo", { heuristic: true }), + { uri: uri1, title: "title" }, + { uri: uri2, title: "title", style: ["bookmark"] }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "title", style: ["bookmark"] }, + { uri: uri5, title: "title", style: ["bookmark"] }, + { uri: uri6, title: "title", style: ["bookmark"] }, + makeSwitchToTabMatch("http://t.foo/6", { title: "title" }), + ] + }); + + // Note the next few tests do *not* get a search result as enable-actions + // isn't specified. + do_print("Match only typed history"); + yield check_autocomplete({ + search: "foo ^ ~", + matches: [ { uri: uri3, title: "title" }, + { uri: uri4, title: "title" } ] + }); + + do_print("Drop-down empty search matches only typed history"); + yield check_autocomplete({ + search: "", + matches: [ { uri: uri3, title: "title" }, + { uri: uri4, title: "title" } ] + }); + + do_print("Drop-down empty search matches only bookmarks"); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + yield check_autocomplete({ + search: "", + matches: [ { uri: uri2, title: "title", style: ["bookmark"] }, + { uri: uri4, title: "title", style: ["bookmark"] }, + { uri: uri5, title: "title", style: ["bookmark"] }, + { uri: uri6, title: "title", style: ["bookmark"] } ] + }); + + do_print("Drop-down empty search matches only open tabs"); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + yield check_autocomplete({ + search: "", + searchParam: "enable-actions", + matches: [ + makeSwitchToTabMatch("http://t.foo/6", { title: "title" }), + ] + }); + + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js new file mode 100644 index 000000000..dee8df8ec --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js @@ -0,0 +1,68 @@ +add_task(function* test_enabled() { + // Test for bug 471903 to make sure searching in autocomplete can be turned on + // and off. Also test bug 463535 for pref changing search. + let uri = NetUtil.newURI("http://url/0"); + yield PlacesTestUtils.addVisits([ { uri: uri, title: "title" } ]); + + do_print("plain search"); + yield check_autocomplete({ + search: "url", + matches: [ { uri: uri, title: "title" } ] + }); + + do_print("search disabled"); + Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false); + yield check_autocomplete({ + search: "url", + matches: [ ] + }); + + do_print("resume normal search"); + Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true); + yield check_autocomplete({ + search: "url", + matches: [ { uri: uri, title: "title" } ] + }); + + yield cleanup(); +}); + +add_task(function* test_sync_enabled() { + // Initialize unified complete. + Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Ci.mozIPlacesAutoComplete); + + let types = [ "history", "bookmark", "openpage", "searches" ]; + + // Test the service keeps browser.urlbar.autocomplete.enabled synchronized + // with browser.urlbar.suggest prefs. + for (let type of types) { + Services.prefs.setBoolPref("browser.urlbar.suggest." + type, true); + } + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true); + + // Disable autocomplete and check all the suggest prefs are set to false. + Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false); + for (let type of types) { + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false); + } + + // Setting even a single suggest pref to true should enable autocomplete. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + for (let type of types.filter(t => t != "history")) { + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false); + } + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true); + + // Disable autocoplete again, then re-enable it and check suggest prefs + // have been reset. + Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false); + Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true); + for (let type of types.filter(t => t != "history")) { + if (type == "searches") { + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false); + } else { + Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true); + } + } +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js new file mode 100644 index 000000000..ff6e5f929 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 422698 to make sure searches with urls from the location bar + * correctly match itself when it contains escaped characters. + */ + +add_task(function* test_escape() { + let uri1 = NetUtil.newURI("http://unescapeduri/"); + let uri2 = NetUtil.newURI("http://escapeduri/%40/"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" } + ]); + + do_print("Unescaped location matches itself"); + yield check_autocomplete({ + search: "http://unescapeduri/", + matches: [ { uri: uri1, title: "title" } ] + }); + + do_print("Escaped location matches itself"); + yield check_autocomplete({ + search: "http://escapeduri/%40/", + matches: [ { uri: uri2, title: "title" } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js new file mode 100644 index 000000000..76af20558 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js @@ -0,0 +1,384 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm"); + +let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController); + +add_task(function* test_correct_errors_are_thrown() { + let keyword = "foo"; + let anotherKeyword = "bar"; + let unregisteredKeyword = "baz"; + + // Register a keyword. + ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }); + + // Try registering the keyword again. + Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} })); + + // Register a different keyword. + ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} }); + + // Try calling handleSearch for an unregistered keyword. + Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {})); + + // Try calling handleSearch without a callback. + Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `)); + + // Try getting the description for a keyword which isn't registered. + Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword)); + + // Try getting the extension name for a keyword which isn't registered. + Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword)); + + // Try setting the default suggestion for a keyword which isn't registered. + Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion")); + + // Try calling handleInputCancelled when there is no active input session. + Assert.throws(() => ExtensionSearchHandler.handleInputCancelled()); + + // Try calling handleInputEntered when there is no active input session. + Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab")); + + // Start a session by calling handleSearch with the registered keyword. + ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}); + + // Try providing suggestions for an unregistered keyword. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, [])); + + // Try providing suggestions for an inactive keyword. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, [])); + + // Try calling handleSearch for an inactive keyword. + Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {})); + + // Try calling addSuggestions with an old callback ID. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, [])); + + // Add suggestions with a valid callback ID. + ExtensionSearchHandler.addSuggestions(keyword, 1, []); + + // Add suggestions again with a valid callback ID. + ExtensionSearchHandler.addSuggestions(keyword, 1, []); + + // Try calling addSuggestions with a future callback ID. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, [])); + + // End the input session by calling handleInputCancelled. + ExtensionSearchHandler.handleInputCancelled(); + + // Try calling handleInputCancelled after the session has ended. + Assert.throws(() => ExtensionSearchHandler.handleInputCancelled()); + + // Try calling handleSearch that doesn't have a space after the keyword. + Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {})); + + // Try calling handleSearch with text starting with the wrong keyword. + Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {})); + + // Start a new session by calling handleSearch with a different keyword + ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {}); + + // Try adding suggestions again with the same callback ID now that the input session has ended. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, [])); + + // Add suggestions with a valid callback ID. + ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []); + + // Try adding suggestions with a valid callback ID but a different keyword. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, [])); + + // Try adding suggestions with a valid callback ID but an unregistered keyword. + Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, [])); + + // Set the default suggestion. + ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"}); + + // Try ending the session using handleInputEntered with a different keyword. + Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab")); + + // Try calling handleInputEntered with invalid text. + Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab")); + + // Try calling handleInputEntered with an invalid disposition. + Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid")); + + // End the session by calling handleInputEntered. + ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"); + + // Try calling handleInputEntered after the session has ended. + Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab")); + + // Unregister the keyword. + ExtensionSearchHandler.unregisterKeyword(keyword); + + // Try setting the default suggestion for the unregistered keyword. + Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"})); + + // Try handling a search with the unregistered keyword. + Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {})); + + // Try unregistering the keyword again. + Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword)); + + // Unregister the other keyword. + ExtensionSearchHandler.unregisterKeyword(anotherKeyword); + + // Try unregistering the word which was never registered. + Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword)); + + // Try setting the default suggestion for a word that was never registered. + Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"})); + + yield cleanup(); +}); + +add_task(function* test_correct_events_are_emitted() { + let events = []; + function checkEvents(expectedEvents) { + Assert.equal(events.length, expectedEvents.length, "The correct number of events fired"); + expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`)); + events = []; + } + + let mockExtension = { emit: message => events.push(message) }; + + let keyword = "foo"; + let anotherKeyword = "bar"; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension); + + ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {}); + checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]); + + ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {}); + checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]); + + ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab"); + checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]); + + ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {}); + checkEvents([ + ExtensionSearchHandler.MSG_INPUT_STARTED, + ExtensionSearchHandler.MSG_INPUT_CHANGED + ]); + + ExtensionSearchHandler.handleInputCancelled(); + checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]); + + ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {}); + checkEvents([ + ExtensionSearchHandler.MSG_INPUT_STARTED, + ExtensionSearchHandler.MSG_INPUT_CHANGED + ]); + + ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} baz`, "tab"); + checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]); + + ExtensionSearchHandler.unregisterKeyword(keyword); +}); + +add_task(function* test_removes_suggestion_if_its_content_is_typed_in() { + let keyword = "test"; + let extensionName = "Foo Bar"; + + let mockExtension = { + name: extensionName, + emit(message, text, id) { + if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) { + ExtensionSearchHandler.addSuggestions(keyword, id, [ + {content: "foo", description: "first suggestion"}, + {content: "bar", description: "second suggestion"}, + {content: "baz", description: "third suggestion"}, + ]); + controller.stopSearch(); + } + } + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + yield check_autocomplete({ + search: `${keyword} unmatched`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}), + makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}) + ] + }); + + yield check_autocomplete({ + search: `${keyword} foo`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}), + makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}) + ] + }); + + yield check_autocomplete({ + search: `${keyword} bar`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}), + makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}) + ] + }); + + yield check_autocomplete({ + search: `${keyword} baz`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}), + makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}) + ] + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + yield cleanup(); +}); + +add_task(function* test_extension_results_should_come_first() { + let keyword = "test"; + let extensionName = "Omnibox Example"; + + let uri = NetUtil.newURI(`http://a.com/b`); + yield PlacesTestUtils.addVisits([ + { uri, title: `${keyword} -` }, + ]); + + let mockExtension = { + name: extensionName, + emit(message, text, id) { + if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) { + ExtensionSearchHandler.addSuggestions(keyword, id, [ + {content: "foo", description: "first suggestion"}, + {content: "bar", description: "second suggestion"}, + {content: "baz", description: "third suggestion"}, + ]); + } + controller.stopSearch(); + } + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + // Start an input session before testing MSG_INPUT_CHANGED. + ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {}); + + yield check_autocomplete({ + search: `${keyword} -`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}), + makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}), + { uri, title: `${keyword} -` } + ] + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + yield cleanup(); +}); + +add_task(function* test_setting_the_default_suggestion() { + let keyword = "test"; + let extensionName = "Omnibox Example"; + + let mockExtension = { + name: extensionName, + emit(message, text, id) { + if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) { + ExtensionSearchHandler.addSuggestions(keyword, id, []); + } + controller.stopSearch(); + } + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + ExtensionSearchHandler.setDefaultSuggestion(keyword, { + description: "hello world" + }); + + let searchString = `${keyword} search query`; + yield check_autocomplete({ + search: searchString, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}), + ] + }); + + ExtensionSearchHandler.setDefaultSuggestion(keyword, { + description: "foo bar" + }); + + yield check_autocomplete({ + search: searchString, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}), + ] + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + yield cleanup(); +}); + +add_task(function* test_maximum_number_of_suggestions_is_enforced() { + let keyword = "test"; + let extensionName = "Omnibox Example"; + + let mockExtension = { + name: extensionName, + emit(message, text, id) { + if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) { + ExtensionSearchHandler.addSuggestions(keyword, id, [ + {content: "a", description: "first suggestion"}, + {content: "b", description: "second suggestion"}, + {content: "c", description: "third suggestion"}, + {content: "d", description: "fourth suggestion"}, + {content: "e", description: "fifth suggestion"}, + {content: "f", description: "sixth suggestion"}, + {content: "g", description: "seventh suggestion"}, + {content: "h", description: "eigth suggestion"}, + {content: "i", description: "ninth suggestion"}, + {content: "j", description: "tenth suggestion"}, + ]); + controller.stopSearch(); + } + } + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + // Start an input session before testing MSG_INPUT_CHANGED. + ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {}); + + yield check_autocomplete({ + search: `${keyword} #`, + searchParam: "enable-actions", + matches: [ + makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}), + makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}), + makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}), + ] + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js new file mode 100644 index 000000000..92e7f601a --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 424509 to make sure searching for "h" doesn't match "http" of urls. + */ + +add_task(function* test_escape() { + let uri1 = NetUtil.newURI("http://site/"); + let uri2 = NetUtil.newURI("http://happytimes/"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" } + ]); + + do_print("Searching for h matches site and not http://"); + yield check_autocomplete({ + search: "h", + matches: [ { uri: uri2, title: "title" } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js new file mode 100644 index 000000000..12b7fea77 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 392143 that puts keyword results into the autocomplete. Makes + * sure that multiple parameter queries get spaces converted to +, + converted + * to %2B, non-ascii become escaped, and pages in history that match the + * keyword uses the page's title. + * + * Also test for bug 249468 by making sure multiple keyword bookmarks with the + * same keyword appear in the list. + */ + +add_task(function* test_keyword_searc() { + let uri1 = NetUtil.newURI("http://abc/?search=%s"); + let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "Generic page title" }, + { uri: uri2, title: "Generic page title" } + ]); + yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"}); + + do_print("Plain keyword query"); + yield check_autocomplete({ + search: "key term", + matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Plain keyword UC"); + yield check_autocomplete({ + search: "key TERM", + matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Multi-word keyword query"); + yield check_autocomplete({ + search: "key multi word", + matches: [ { uri: NetUtil.newURI("http://abc/?search=multi%20word"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Keyword query with +"); + yield check_autocomplete({ + search: "key blocking+", + matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Unescaped term in query"); + yield check_autocomplete({ + search: "key ユニコード", + matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Keyword that happens to match a page"); + yield check_autocomplete({ + search: "key ThisPageIsInHistory", + matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Keyword without query (without space)"); + yield check_autocomplete({ + search: "key", + matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Keyword without query (with space)"); + yield check_autocomplete({ + search: "key ", + matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword", "heuristic"] } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js new file mode 100644 index 000000000..61d98f72d --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 392143 that puts keyword results into the autocomplete. Makes + * sure that multiple parameter queries get spaces converted to +, + converted + * to %2B, non-ascii become escaped, and pages in history that match the + * keyword uses the page's title. + * + * Also test for bug 249468 by making sure multiple keyword bookmarks with the + * same keyword appear in the list. + */ + +add_task(function* test_keyword_search() { + let uri1 = NetUtil.newURI("http://abc/?search=%s"); + let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"); + let uri3 = NetUtil.newURI("http://abc/?search=%s&raw=%S"); + let uri4 = NetUtil.newURI("http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1"); + yield PlacesTestUtils.addVisits([{ uri: uri1 }, + { uri: uri2 }, + { uri: uri3 }]); + yield addBookmark({ uri: uri1, title: "Keyword", keyword: "key"}); + yield addBookmark({ uri: uri1, title: "Post", keyword: "post", postData: "post_search=%s"}); + yield addBookmark({ uri: uri3, title: "Encoded", keyword: "encoded"}); + yield addBookmark({ uri: uri4, title: "Charset", keyword: "charset"}); + yield addBookmark({ uri: uri2, title: "Noparam", keyword: "noparam"}); + yield addBookmark({ uri: uri2, title: "Noparam-Post", keyword: "post_noparam", postData: "noparam=1"}); + + do_print("Plain keyword query"); + yield check_autocomplete({ + search: "key term", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Plain keyword UC"); + yield check_autocomplete({ + search: "key TERM", + matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"), + title: "abc", style: ["keyword", "heuristic"] } ] + }); + + do_print("Multi-word keyword query"); + yield check_autocomplete({ + search: "key multi word", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi%20word", input: "key multi word"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Keyword query with +"); + yield check_autocomplete({ + search: "key blocking+", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Unescaped term in query"); + // ... but note that UnifiedComplete calls encodeURIComponent() on the query + // string when it builds the URL, so the expected result will have the + // ユニコード substring encoded in the URL. + yield check_autocomplete({ + search: "key ユニコード", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=" + encodeURIComponent("ユニコード"), input: "key ユニコード"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Keyword that happens to match a page"); + yield check_autocomplete({ + search: "key ThisPageIsInHistory", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Keyword without query (without space)"); + yield check_autocomplete({ + search: "key", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Keyword without query (with space)"); + yield check_autocomplete({ + search: "key ", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("POST Keyword"); + yield check_autocomplete({ + search: "post foo", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=foo", input: "post foo", postData: "post_search=foo"}), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Bug 420328: no-param keyword with a param"); + yield check_autocomplete({ + search: "noparam foo", + searchParam: "enable-actions", + matches: [ makeSearchMatch("noparam foo", { heuristic: true }) ] + }); + yield check_autocomplete({ + search: "post_noparam foo", + searchParam: "enable-actions", + matches: [ makeSearchMatch("post_noparam foo", { heuristic: true }) ] + }); + + do_print("escaping with default UTF-8 charset"); + yield check_autocomplete({ + search: "encoded foé", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%C3%A9&raw=foé", input: "encoded foé" }), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("escaping with forced ISO-8859-1 charset"); + yield check_autocomplete({ + search: "charset foé", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%E9&raw=foé", input: "charset foé" }), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Bug 359809: escaping +, / and @ with default UTF-8 charset"); + yield check_autocomplete({ + search: "encoded +/@", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "encoded +/@" }), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + do_print("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset"); + yield check_autocomplete({ + search: "charset +/@", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "charset +/@" }), + title: "abc", style: [ "action", "keyword", "heuristic" ] } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keywords.js b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js new file mode 100644 index 000000000..93e8d7a6f --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_non_keyword() { + do_print("Searching for non-keyworded entry should autoFill it"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/") }); + yield check_autocomplete({ + search: "moz", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_keyword() { + do_print("Searching for keyworded entry should not autoFill it"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); + yield check_autocomplete({ + search: "moz", + autofilled: "moz", + completed: "moz", + }); + yield cleanup(); +}); + +add_task(function* test_more_than_keyword() { + do_print("Searching for more than keyworded entry should autoFill it"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); + yield check_autocomplete({ + search: "mozi", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_less_than_keyword() { + do_print("Searching for less than keyworded entry should autoFill it"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "mozilla.org/", + }); + yield cleanup(); +}); + +add_task(function* test_keyword_casing() { + do_print("Searching for keyworded entry is case-insensitive"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); + yield check_autocomplete({ + search: "MoZ", + autofilled: "MoZ", + completed: "MoZ" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js new file mode 100644 index 000000000..57a1efaeb --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 451760 which allows matching only at the beginning of urls or + * titles to simulate Firefox 2 functionality. + */ + +add_task(function* test_match_beginning() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + + let uri1 = NetUtil.newURI("http://x.com/y"); + let uri2 = NetUtil.newURI("https://y.com/x"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "a b" }, + { uri: uri2, title: "b a" } + ]); + + do_print("Match at the beginning of titles"); + Services.prefs.setIntPref("browser.urlbar.matchBehavior", 3); + yield check_autocomplete({ + search: "a", + matches: [ { uri: uri1, title: "a b" } ] + }); + + do_print("Match at the beginning of titles"); + yield check_autocomplete({ + search: "b", + matches: [ { uri: uri2, title: "b a" } ] + }); + + do_print("Match at the beginning of urls"); + yield check_autocomplete({ + search: "x", + matches: [ { uri: uri1, title: "a b" } ] + }); + + do_print("Match at the beginning of urls"); + yield check_autocomplete({ + search: "y", + matches: [ { uri: uri2, title: "b a" } ] + }); + + do_print("Sanity check that matching anywhere finds more"); + Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1); + yield check_autocomplete({ + search: "a", + matches: [ { uri: uri1, title: "a b" }, + { uri: uri2, title: "b a" } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js new file mode 100644 index 000000000..c6c9e952e --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 401869 to allow multiple words separated by spaces to match in + * the page title, page url, or bookmark title to be considered a match. All + * terms must match but not all terms need to be in the title, etc. + * + * Test bug 424216 by making sure bookmark titles are always shown if one is + * available. Also bug 425056 makes sure matches aren't found partially in the + * page title and partially in the bookmark. + */ + +add_task(function* test_match_beginning() { + let uri1 = NetUtil.newURI("http://a.b.c/d-e_f/h/t/p"); + let uri2 = NetUtil.newURI("http://d.e.f/g-h_i/h/t/p"); + let uri3 = NetUtil.newURI("http://g.h.i/j-k_l/h/t/p"); + let uri4 = NetUtil.newURI("http://j.k.l/m-n_o/h/t/p"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "f(o)o b<a>r" }, + { uri: uri2, title: "b(a)r b<a>z" }, + { uri: uri3, title: "f(o)o b<a>r" }, + { uri: uri4, title: "f(o)o b<a>r" } + ]); + yield addBookmark({ uri: uri3, title: "f(o)o b<a>r" }); + yield addBookmark({ uri: uri4, title: "b(a)r b<a>z" }); + + do_print("Match 2 terms all in url"); + yield check_autocomplete({ + search: "c d", + matches: [ { uri: uri1, title: "f(o)o b<a>r" } ] + }); + + do_print("Match 1 term in url and 1 term in title"); + yield check_autocomplete({ + search: "b e", + matches: [ { uri: uri1, title: "f(o)o b<a>r" }, + { uri: uri2, title: "b(a)r b<a>z" } ] + }); + + do_print("Match 3 terms all in title; display bookmark title if matched"); + yield check_autocomplete({ + search: "b a z", + matches: [ { uri: uri2, title: "b(a)r b<a>z" }, + { uri: uri4, title: "b(a)r b<a>z", style: [ "bookmark" ] } ] + }); + + do_print("Match 2 terms in url and 1 in title; make sure bookmark title is used for search"); + yield check_autocomplete({ + search: "k f t", + matches: [ { uri: uri3, title: "f(o)o b<a>r", style: [ "bookmark" ] } ] + }); + + do_print("Match 3 terms in url and 1 in title"); + yield check_autocomplete({ + search: "d i g z", + matches: [ { uri: uri2, title: "b(a)r b<a>z" } ] + }); + + do_print("Match nothing"); + yield check_autocomplete({ + search: "m o z i", + matches: [ ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_query_url.js b/toolkit/components/places/tests/unifiedcomplete/test_query_url.js new file mode 100644 index 000000000..915ba770e --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_query_url.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_no_slash() { + do_print("Searching for host match without slash should match host"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://file.org/test/"), + transition: TRANSITION_TYPED + }, { + uri: NetUtil.newURI("file:///c:/test.html"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "file", + autofilled: "file.org/", + completed: "file.org/" + }); + yield cleanup(); +}); + +add_task(function* test_w_slash() { + do_print("Searching match with slash at the end should do nothing"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://file.org/test/"), + transition: TRANSITION_TYPED + }, { + uri: NetUtil.newURI("file:///c:/test.html"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "file.org/", + autofilled: "file.org/", + completed: "file.org/" + }); + yield cleanup(); +}); + +add_task(function* test_middle() { + do_print("Searching match with slash in the middle should match url"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://file.org/test/"), + transition: TRANSITION_TYPED + }, { + uri: NetUtil.newURI("file:///c:/test.html"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "file.org/t", + autofilled: "file.org/test/", + completed: "http://file.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_nonhost() { + do_print("Searching for non-host match without slash should not match url"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("file:///c:/test.html"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "file", + autofilled: "file", + completed: "file" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js new file mode 100644 index 000000000..56998d4d6 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js @@ -0,0 +1,203 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: +*/ +"use strict"; + +Cu.import("resource://services-sync/main.js"); + +Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com"); + +// A mock "Tabs" engine which autocomplete will use instead of the real +// engine. We pass a constructor that Sync creates. +function MockTabsEngine() { + this.clients = null; // We'll set this dynamically +} + +MockTabsEngine.prototype = { + name: "tabs", + + getAllClients() { + return this.clients; + }, +} + +// A clients engine that doesn't need to be a constructor. +let MockClientsEngine = { + isMobile(guid) { + Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile")); + return guid.endsWith("mobile"); + }, +} + +// Tell Sync about the mocks. +Weave.Service.engineManager.register(MockTabsEngine); +Weave.Service.clientsEngine = MockClientsEngine; + +// Tell the Sync XPCOM service it is initialized. +let weaveXPCService = Cc["@mozilla.org/weave/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject; +weaveXPCService.ready = true; + +// Configure the singleton engine for a test. +function configureEngine(clients) { + // Configure the instance Sync created. + let engine = Weave.Service.engineManager.get("tabs"); + engine.clients = clients; + // Send an observer that pretends the engine just finished a sync. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); +} + +// Make a match object suitable for passing to check_autocomplete. +function makeRemoteTabMatch(url, deviceName, extra = {}) { + return { + uri: makeActionURI("remotetab", {url, deviceName}), + title: extra.title || url, + style: [ "action", "remotetab" ], + icon: extra.icon, + } +} + +// The tests. +add_task(function* test_nomatch() { + // Nothing matches. + configureEngine({ + guid_desktop: { + clientName: "My Desktop", + tabs: [{ + urlHistory: ["http://foo.com/"], + }], + } + }); + + // No remote tabs match here, so we only expect search results. + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }) ], + }); +}); + +add_task(function* test_minimal() { + // The minimal client and tabs info we can get away with. + configureEngine({ + guid_desktop: { + clientName: "My Desktop", + tabs: [{ + urlHistory: ["http://example.com/"], + }], + } + }); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://example.com/", "My Desktop") ], + }); +}); + +add_task(function* test_maximal() { + // Every field that could possibly exist on a remote record. + configureEngine({ + guid_mobile: { + clientName: "My Phone", + tabs: [{ + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }], + } + }); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://example.com/", "My Phone", + { title: "An Example", + icon: "moz-anno:favicon:http://favicon/" + }), + ], + }); +}); + +add_task(function* test_noShowIcons() { + Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false); + configureEngine({ + guid_mobile: { + clientName: "My Phone", + tabs: [{ + urlHistory: ["http://example.com/"], + title: "An Example", + icon: "http://favicon", + }], + } + }); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://example.com/", "My Phone", + { title: "An Example", + // expecting the default favicon due to that pref. + icon: "", + }), + ], + }); + Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons"); +}); + +add_task(function* test_matches_title() { + // URL doesn't match search expression, should still match the title. + configureEngine({ + guid_mobile: { + clientName: "My Phone", + tabs: [{ + urlHistory: ["http://foo.com/"], + title: "An Example", + }], + } + }); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeRemoteTabMatch("http://foo.com/", "My Phone", + { title: "An Example" }), + ], + }); +}); + +add_task(function* test_localtab_matches_override() { + // We have an open tab to the same page on a remote device, only "switch to + // tab" should appear as duplicate detection removed the remote one. + + // First setup Sync to have the page as a remote tab. + configureEngine({ + guid_mobile: { + clientName: "My Phone", + tabs: [{ + urlHistory: ["http://foo.com/"], + title: "An Example", + }], + } + }); + + // Setup Places to think the tab is open locally. + let uri = NetUtil.newURI("http://foo.com/"); + yield PlacesTestUtils.addVisits([ + { uri: uri, title: "An Example" }, + ]); + addOpenPages(uri, 1); + + yield check_autocomplete({ + search: "ex", + searchParam: "enable-actions", + matches: [ makeSearchMatch("ex", { heuristic: true }), + makeSwitchToTabMatch("http://foo.com/", { title: "An Example" }), + ], + }); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js new file mode 100644 index 000000000..f35242e21 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + + +add_task(function*() { + // Note that head_autocomplete.js has already added a MozSearch engine. + // Here we add another engine with a search alias. + Services.search.addEngineWithDetails("AliasedGETMozSearch", "", "get", "", + "GET", "http://s.example.com/search"); + Services.search.addEngineWithDetails("AliasedPOSTMozSearch", "", "post", "", + "POST", "http://s.example.com/search"); + + for (let alias of ["get", "post"]) { + yield check_autocomplete({ + search: alias, + searchParam: "enable-actions", + matches: [ makeSearchMatch(alias, { engineName: `Aliased${alias.toUpperCase()}MozSearch`, + searchQuery: "", alias, heuristic: true }) ] + }); + + yield check_autocomplete({ + search: `${alias} `, + searchParam: "enable-actions", + matches: [ makeSearchMatch(`${alias} `, { engineName: `Aliased${alias.toUpperCase()}MozSearch`, + searchQuery: "", alias, heuristic: true }) ] + }); + + yield check_autocomplete({ + search: `${alias} mozilla`, + searchParam: "enable-actions", + matches: [ makeSearchMatch(`${alias} mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`, + searchQuery: "mozilla", alias, heuristic: true }) ] + }); + + yield check_autocomplete({ + search: `${alias} MoZiLlA`, + searchParam: "enable-actions", + matches: [ makeSearchMatch(`${alias} MoZiLlA`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`, + searchQuery: "MoZiLlA", alias, heuristic: true }) ] + }); + + yield check_autocomplete({ + search: `${alias} mozzarella mozilla`, + searchParam: "enable-actions", + matches: [ makeSearchMatch(`${alias} mozzarella mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`, + searchQuery: "mozzarella mozilla", alias, heuristic: true }) ] + }); + } + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js new file mode 100644 index 000000000..b41d9884b --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + + +add_task(function*() { + // Note that head_autocomplete.js has already added a MozSearch engine. + // Here we add another engine with a search alias. + Services.search.addEngineWithDetails("AliasedMozSearch", "", "doit", "", + "GET", "http://s.example.com/search"); + + do_print("search engine"); + yield check_autocomplete({ + search: "mozilla", + searchParam: "enable-actions", + matches: [ makeSearchMatch("mozilla", { heuristic: true }) ] + }); + + do_print("search engine, uri-like input"); + yield check_autocomplete({ + search: "http:///", + searchParam: "enable-actions", + matches: [ makeSearchMatch("http:///", { heuristic: true }) ] + }); + + do_print("search engine, multiple words"); + yield check_autocomplete({ + search: "mozzarella cheese", + searchParam: "enable-actions", + matches: [ makeSearchMatch("mozzarella cheese", { heuristic: true }) ] + }); + + do_print("search engine, after current engine has changed"); + Services.search.addEngineWithDetails("MozSearch2", "", "", "", "GET", + "http://s.example.com/search2"); + engine = Services.search.getEngineByName("MozSearch2"); + notEqual(Services.search.currentEngine, engine, "New engine shouldn't be the current engine yet"); + Services.search.currentEngine = engine; + yield check_autocomplete({ + search: "mozilla", + searchParam: "enable-actions", + matches: [ makeSearchMatch("mozilla", { engineName: "MozSearch2", heuristic: true }) ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js new file mode 100644 index 000000000..61b9826f7 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* test_searchEngine_autoFill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.search.addEngineWithDetails("MySearchEngine", "", "", "", + "GET", "http://my.search.com/"); + let engine = Services.search.getEngineByName("MySearchEngine"); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + // Add an uri that matches the search string with high frecency. + let uri = NetUtil.newURI("http://www.example.com/my/"); + let visits = []; + for (let i = 0; i < 100; ++i) { + visits.push({ uri, title: "Terms - SearchEngine Search" }); + } + yield PlacesTestUtils.addVisits(visits); + yield addBookmark({ uri: uri, title: "Example bookmark" }); + yield PlacesTestUtils.promiseAsyncUpdates(); + ok(frecencyForUrl(uri) > 10000, "Added URI should have expected high frecency"); + + do_print("Check search domain is autoFilled even if there's an higher frecency match"); + yield check_autocomplete({ + search: "my", + autofilled: "my.search.com", + completed: "http://my.search.com" + }); + + yield cleanup(); +}); + +add_task(function* test_searchEngine_noautoFill() { + let engineName = "engine-rel-searchform.xml"; + let engine = yield addTestEngine(engineName); + equal(engine.searchForm, "http://example.com/?search"); + + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://example.com/my/")); + + do_print("Check search domain is not autoFilled if it matches a visited domain"); + yield check_autocomplete({ + search: "example", + autofilled: "example.com/", + completed: "example.com/" + }); + + yield cleanup(); +}); + diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js new file mode 100644 index 000000000..2a5f2d78e --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_searchEngine() { + Services.search.addEngineWithDetails("SearchEngine", "", "", "", + "GET", "http://s.example.com/search"); + let engine = Services.search.getEngineByName("SearchEngine"); + engine.addParam("q", "{searchTerms}", null); + do_register_cleanup(() => Services.search.removeEngine(engine)); + + let uri1 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=1"); + let uri2 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=2"); + yield PlacesTestUtils.addVisits({ uri: uri1, title: "Terms - SearchEngine Search" }); + yield addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" }); + + do_print("Past search terms should be styled, unless bookmarked"); + Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true); + yield check_autocomplete({ + search: "term", + matches: [ + makeSearchMatch("Terms", { + engineName: "SearchEngine", + style: ["favicon"] + }), + { + uri: uri2, + title: "Terms - SearchEngine Search", + style: ["bookmark"] + } + ] + }); + + do_print("Past search terms should not be styled if restyling is disabled"); + Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false); + yield check_autocomplete({ + search: "term", + matches: [ { uri: uri1, title: "Terms - SearchEngine Search" }, + { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js new file mode 100644 index 000000000..63b428cd4 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js @@ -0,0 +1,651 @@ +Cu.import("resource://gre/modules/FormHistory.jsm"); + +const ENGINE_NAME = "engine-suggestions.xml"; +const SERVER_PORT = 9000; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const SUGGEST_RESTRICT_TOKEN = "$"; + +var suggestionsFn; +var previousSuggestionsFn; + +function setSuggestionsFn(fn) { + previousSuggestionsFn = suggestionsFn; + suggestionsFn = fn; +} + +function* cleanUpSuggestions() { + yield cleanup(); + if (previousSuggestionsFn) { + suggestionsFn = previousSuggestionsFn; + previousSuggestionsFn = null; + } +} + +add_task(function* setUp() { + // Set up a server that provides some suggestions by appending strings onto + // the search query. + let server = makeTestServer(SERVER_PORT); + server.registerPathHandler("/suggest", (req, resp) => { + // URL query params are x-www-form-urlencoded, which converts spaces into + // plus signs, so un-convert any plus signs back to spaces. + let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " ")); + let suggestions = suggestionsFn(searchStr); + let data = [searchStr, suggestions]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); + }); + setSuggestionsFn(searchStr => { + let suffixes = ["foo", "bar"]; + return suffixes.map(s => searchStr + " " + s); + }); + + // Install the test engine. + let oldCurrentEngine = Services.search.currentEngine; + do_register_cleanup(() => Services.search.currentEngine = oldCurrentEngine); + let engine = yield addTestEngine(ENGINE_NAME, server); + Services.search.currentEngine = engine; +}); + +add_task(function* disabled_urlbarSuggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + yield cleanUpSuggestions(); +}); + +add_task(function* disabled_allSuggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + yield cleanUpSuggestions(); +}); + +add_task(function* disabled_privateWindow() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + yield check_autocomplete({ + search: "hello", + searchParam: "private-window enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + yield cleanUpSuggestions(); +}); + +add_task(function* singleWordQuery() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + { uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello foo", + searchQuery: "hello", + searchSuggestion: "hello foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello bar", + searchQuery: "hello", + searchSuggestion: "hello bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* multiWordQuery() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + yield check_autocomplete({ + search: "hello world", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello world", { engineName: ENGINE_NAME, heuristic: true }), + { uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello world foo", + searchQuery: "hello world", + searchSuggestion: "hello world foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello world bar", + searchQuery: "hello world", + searchSuggestion: "hello world bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* suffixMatch() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + setSuggestionsFn(searchStr => { + let prefixes = ["baz", "quux"]; + return prefixes.map(p => p + " " + searchStr); + }); + + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + { uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "baz hello", + searchQuery: "hello", + searchSuggestion: "baz hello", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "quux hello", + searchQuery: "hello", + searchSuggestion: "quux hello", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* queryIsNotASubstring() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + setSuggestionsFn(searchStr => { + return ["aaa", "bbb"]; + }); + + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + { uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "aaa", + searchQuery: "hello", + searchSuggestion: "aaa", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "bbb", + searchQuery: "hello", + searchSuggestion: "bbb", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* restrictToken() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + // Add a visit and a bookmark. Actually, make the bookmark visited too so + // that it's guaranteed, with its higher frecency, to appear above the search + // suggestions. + yield PlacesTestUtils.addVisits([ + { + uri: NetUtil.newURI("http://example.com/hello-visit"), + title: "hello visit", + }, + { + uri: NetUtil.newURI("http://example.com/hello-bookmark"), + title: "hello bookmark", + }, + ]); + + yield addBookmark({ + uri: NetUtil.newURI("http://example.com/hello-bookmark"), + title: "hello bookmark", + }); + + // Do an unrestricted search to make sure everything appears in it, including + // the visit and bookmark. + yield check_autocomplete({ + search: "hello", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: NetUtil.newURI("http://example.com/hello-visit"), + title: "hello visit", + }, + { + uri: NetUtil.newURI("http://example.com/hello-bookmark"), + title: "hello bookmark", + style: ["bookmark"], + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello foo", + searchQuery: "hello", + searchSuggestion: "hello foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello bar", + searchQuery: "hello", + searchSuggestion: "hello bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + ], + }); + + // Now do a restricted search to make sure only suggestions appear. + yield check_autocomplete({ + search: SUGGEST_RESTRICT_TOKEN + " hello", + searchParam: "enable-actions", + matches: [ + // TODO (bug 1177895) This is wrong. + makeSearchMatch(SUGGEST_RESTRICT_TOKEN + " hello", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello foo", + searchQuery: "hello", + searchSuggestion: "hello foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "hello bar", + searchQuery: "hello", + searchSuggestion: "hello bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + } + ], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* mixup_frecency() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + // Add a visit and a bookmark. Actually, make the bookmark visited too so + // that it's guaranteed, with its higher frecency, to appear above the search + // suggestions. + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://example.com/lo0"), + title: "low frecency 0" }, + { uri: NetUtil.newURI("http://example.com/lo1"), + title: "low frecency 1" }, + { uri: NetUtil.newURI("http://example.com/lo2"), + title: "low frecency 2" }, + { uri: NetUtil.newURI("http://example.com/lo3"), + title: "low frecency 3" }, + { uri: NetUtil.newURI("http://example.com/lo4"), + title: "low frecency 4" }, + ]); + + for (let i = 0; i < 4; i++) { + let href = `http://example.com/lo${i}`; + let frecency = frecencyForUrl(href); + Assert.ok(frecency < FRECENCY_DEFAULT, + `frecency for ${href}: ${frecency}, should be lower than ${FRECENCY_DEFAULT}`); + } + + for (let i = 0; i < 5; i++) { + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://example.com/hi0"), + title: "high frecency 0", + transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://example.com/hi1"), + title: "high frecency 1", + transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://example.com/hi2"), + title: "high frecency 2", + transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://example.com/hi3"), + title: "high frecency 3", + transition: TRANSITION_TYPED }, + ]); + } + + for (let i = 0; i < 4; i++) { + let href = `http://example.com/hi${i}`; + yield addBookmark({ uri: href, title: `high frecency ${i}` }); + let frecency = frecencyForUrl(href); + Assert.ok(frecency > FRECENCY_DEFAULT, + `frecency for ${href}: ${frecency}, should be higher than ${FRECENCY_DEFAULT}`); + } + + // Do an unrestricted search to make sure everything appears in it, including + // the visit and bookmark. + yield check_autocomplete({ + checkSorting: true, + search: "frecency", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("frecency", { engineName: ENGINE_NAME, heuristic: true }), + { uri: NetUtil.newURI("http://example.com/hi3"), + title: "high frecency 3", + style: [ "bookmark" ] }, + { uri: NetUtil.newURI("http://example.com/hi2"), + title: "high frecency 2", + style: [ "bookmark" ] }, + { uri: NetUtil.newURI("http://example.com/hi1"), + title: "high frecency 1", + style: [ "bookmark" ] }, + { uri: NetUtil.newURI("http://example.com/hi0"), + title: "high frecency 0", + style: [ "bookmark" ] }, + { uri: NetUtil.newURI("http://example.com/lo4"), + title: "low frecency 4" }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "frecency foo", + searchQuery: "frecency", + searchSuggestion: "frecency foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "frecency bar", + searchQuery: "frecency", + searchSuggestion: "frecency bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { uri: NetUtil.newURI("http://example.com/lo3"), + title: "low frecency 3" }, + { uri: NetUtil.newURI("http://example.com/lo2"), + title: "low frecency 2" }, + { uri: NetUtil.newURI("http://example.com/lo1"), + title: "low frecency 1" }, + { uri: NetUtil.newURI("http://example.com/lo0"), + title: "low frecency 0" }, + ], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* prohibit_suggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + yield check_autocomplete({ + search: "localhost", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost foo", + searchQuery: "localhost", + searchSuggestion: "localhost foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost bar", + searchQuery: "localhost", + searchSuggestion: "localhost bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + ], + }); + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.localhost", true); + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost"); + }); + yield check_autocomplete({ + search: "localhost", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("localhost", "http://localhost/", { heuristic: true }), + makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: false }) + ], + }); + + // When using multiple words, we should still get suggestions: + yield check_autocomplete({ + search: "localhost other", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("localhost other", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost other foo", + searchQuery: "localhost other", + searchSuggestion: "localhost other foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost other bar", + searchQuery: "localhost other", + searchSuggestion: "localhost other bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + ], + }); + + // Clear the whitelist for localhost, and try preferring DNS for any single + // word instead: + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost"); + Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true); + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words"); + }); + + yield check_autocomplete({ + search: "localhost", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("localhost", "http://localhost/", { heuristic: true }), + makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: false }) + ], + }); + + yield check_autocomplete({ + search: "somethingelse", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("somethingelse", "http://somethingelse/", { heuristic: true }), + makeSearchMatch("somethingelse", { engineName: ENGINE_NAME, heuristic: false }) + ], + }); + + // When using multiple words, we should still get suggestions: + yield check_autocomplete({ + search: "localhost other", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("localhost other", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost other foo", + searchQuery: "localhost other", + searchSuggestion: "localhost other foo", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "localhost other bar", + searchQuery: "localhost other", + searchSuggestion: "localhost other bar", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + ], + }); + + Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words"); + + yield check_autocomplete({ + search: "1.2.3.4", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("1.2.3.4", "http://1.2.3.4/", { heuristic: true }), + ], + }); + yield check_autocomplete({ + search: "[2001::1]:30", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("[2001::1]:30", "http://[2001::1]:30/", { heuristic: true }), + ], + }); + yield check_autocomplete({ + search: "user:pass@test", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("user:pass@test", "http://user:pass@test/", { heuristic: true }), + ], + }); + yield check_autocomplete({ + search: "test/test", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("test/test", "http://test/test", { heuristic: true }), + ], + }); + yield check_autocomplete({ + search: "data:text/plain,Content", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("data:text/plain,Content", "data:text/plain,Content", { heuristic: true }), + ], + }); + + yield check_autocomplete({ + search: "a", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("a", { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + yield cleanUpSuggestions(); +}); + +add_task(function* avoid_url_suggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + setSuggestionsFn(searchStr => { + let suffixes = [".com", "/test", ":1]", "@test", ". com"]; + return suffixes.map(s => searchStr + s); + }); + + yield check_autocomplete({ + search: "test", + searchParam: "enable-actions", + matches: [ + makeSearchMatch("test", { engineName: ENGINE_NAME, heuristic: true }), + { + uri: makeActionURI(("searchengine"), { + engineName: ENGINE_NAME, + input: "test. com", + searchQuery: "test", + searchSuggestion: "test. com", + }), + title: ENGINE_NAME, + style: ["action", "searchengine"], + icon: "", + }, + ], + }); + + yield cleanUpSuggestions(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_special_search.js b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js new file mode 100644 index 000000000..21df7046c --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js @@ -0,0 +1,447 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 395161 that allows special searches that restrict results to + * history/bookmark/tagged items and title/url matches. + * + * Test 485122 by making sure results don't have tags when restricting result + * to just history either by default behavior or dynamic query restrict. + */ + +function setSuggestPrefsToFalse() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); +} + +add_task(function* test_special_searches() { + let uri1 = NetUtil.newURI("http://url/"); + let uri2 = NetUtil.newURI("http://url/2"); + let uri3 = NetUtil.newURI("http://foo.bar/"); + let uri4 = NetUtil.newURI("http://foo.bar/2"); + let uri5 = NetUtil.newURI("http://url/star"); + let uri6 = NetUtil.newURI("http://url/star/2"); + let uri7 = NetUtil.newURI("http://foo.bar/star"); + let uri8 = NetUtil.newURI("http://foo.bar/star/2"); + let uri9 = NetUtil.newURI("http://url/tag"); + let uri10 = NetUtil.newURI("http://url/tag/2"); + let uri11 = NetUtil.newURI("http://foo.bar/tag"); + let uri12 = NetUtil.newURI("http://foo.bar/tag/2"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title", transition: TRANSITION_TYPED }, + { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar", transition: TRANSITION_TYPED }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", transition: TRANSITION_TYPED } + ]); + yield addBookmark( { uri: uri5, title: "title" } ); + yield addBookmark( { uri: uri6, title: "foo.bar" } ); + yield addBookmark( { uri: uri7, title: "title" } ); + yield addBookmark( { uri: uri8, title: "foo.bar" } ); + yield addBookmark( { uri: uri9, title: "title", tags: [ "foo.bar" ] } ); + yield addBookmark( { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ] } ); + yield addBookmark( { uri: uri11, title: "title", tags: [ "foo.bar" ] } ); + yield addBookmark( { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ] } ); + + // Test restricting searches + do_print("History restrict"); + yield check_autocomplete({ + search: "^", + matches: [ { uri: uri1, title: "title" }, + { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("Star restrict"); + yield check_autocomplete({ + search: "*", + matches: [ { uri: uri5, title: "title", style: [ "bookmark" ] }, + { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Tag restrict"); + yield check_autocomplete({ + search: "+", + matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + // Test specials as any word position + do_print("Special as first word"); + yield check_autocomplete({ + search: "^ foo bar", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("Special as middle word"); + yield check_autocomplete({ + search: "foo ^ bar", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("Special as last word"); + yield check_autocomplete({ + search: "foo bar ^", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + // Test restricting and matching searches with a term + do_print("foo ^ -> history"); + yield check_autocomplete({ + search: "foo ^", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo | -> history (change pref)"); + changeRestrict("history", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo * -> is star"); + resetRestrict("history"); + yield check_autocomplete({ + search: "foo *", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo | -> is star (change pref)"); + changeRestrict("bookmark", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo # -> in title"); + resetRestrict("bookmark"); + yield check_autocomplete({ + search: "foo #", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo | -> in title (change pref)"); + changeRestrict("title", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo @ -> in url"); + resetRestrict("title"); + yield check_autocomplete({ + search: "foo @", + matches: [ { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo | -> in url (change pref)"); + changeRestrict("url", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo + -> is tag"); + resetRestrict("url"); + yield check_autocomplete({ + search: "foo +", + matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo | -> is tag (change pref)"); + changeRestrict("tag", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo ~ -> is typed"); + resetRestrict("tag"); + yield check_autocomplete({ + search: "foo ~", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo | -> is typed (change pref)"); + changeRestrict("typed", "|"); + yield check_autocomplete({ + search: "foo |", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + // Test various pairs of special searches + do_print("foo ^ * -> history, is star"); + resetRestrict("typed"); + yield check_autocomplete({ + search: "foo ^ *", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo ^ # -> history, in title"); + yield check_autocomplete({ + search: "foo ^ #", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo ^ @ -> history, in url"); + yield check_autocomplete({ + search: "foo ^ @", + matches: [ { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo ^ + -> history, is tag"); + yield check_autocomplete({ + search: "foo ^ +", + matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo ^ ~ -> history, is typed"); + yield check_autocomplete({ + search: "foo ^ ~", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo * # -> is star, in title"); + yield check_autocomplete({ + search: "foo * #", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo * @ -> is star, in url"); + yield check_autocomplete({ + search: "foo * @", + matches: [ { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo * + -> same as +"); + yield check_autocomplete({ + search: "foo * +", + matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo * ~ -> is star, is typed"); + yield check_autocomplete({ + search: "foo * ~", + matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo # @ -> in title, in url"); + yield check_autocomplete({ + search: "foo # @", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo # + -> in title, is tag"); + yield check_autocomplete({ + search: "foo # +", + matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo # ~ -> in title, is typed"); + yield check_autocomplete({ + search: "foo # ~", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo @ + -> in url, is tag"); + yield check_autocomplete({ + search: "foo @ +", + matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo @ ~ -> in url, is typed"); + yield check_autocomplete({ + search: "foo @ ~", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + do_print("foo + ~ -> is tag, is typed"); + yield check_autocomplete({ + search: "foo + ~", + matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] + }); + + // Disable autoFill for the next tests, see test_autoFill_default_behavior.js + // for specific tests. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + + // Test default usage by setting certain browser.urlbar.suggest.* prefs + do_print("foo -> default history"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + yield check_autocomplete({ + search: "foo", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar" }, + { uri: uri11, title: "title", tags: ["foo.bar"], style: [ "tag" ] } ] + }); + + do_print("foo -> default history, is star"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + yield check_autocomplete({ + search: "foo", + matches: [ { uri: uri2, title: "foo.bar" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "foo.bar" }, + { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo -> default history, is star, is typed"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + yield check_autocomplete({ + search: "foo", + matches: [ { uri: uri4, title: "foo.bar" }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo -> is star"); + setSuggestPrefsToFalse(); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + yield check_autocomplete({ + search: "foo", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("foo -> is star, is typed"); + setSuggestPrefsToFalse(); + // only typed should be ignored + Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + yield check_autocomplete({ + search: "foo", + matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri7, title: "title", style: [ "bookmark" ] }, + { uri: uri8, title: "foo.bar", style: [ "bookmark" ] }, + { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, + { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js new file mode 100644 index 000000000..89ccc3206 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 424717 to make sure searching with an existing location like + * http://site/ also matches https://site/ or ftp://site/. Same thing for + * ftp://site/ and https://site/. + * + * Test bug 461483 to make sure a search for "w" doesn't match the "www." from + * site subdomains. + */ + +add_task(function* test_swap_protocol() { + let uri1 = NetUtil.newURI("http://www.site/"); + let uri2 = NetUtil.newURI("http://site/"); + let uri3 = NetUtil.newURI("ftp://ftp.site/"); + let uri4 = NetUtil.newURI("ftp://site/"); + let uri5 = NetUtil.newURI("https://www.site/"); + let uri6 = NetUtil.newURI("https://site/"); + let uri7 = NetUtil.newURI("http://woohoo/"); + let uri8 = NetUtil.newURI("http://wwwwwwacko/"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "title" }, + { uri: uri5, title: "title" }, + { uri: uri6, title: "title" }, + { uri: uri7, title: "title" }, + { uri: uri8, title: "title" } + ]); + + let allMatches = [ + { uri: uri1, title: "title" }, + { uri: uri2, title: "title" }, + { uri: uri3, title: "title" }, + { uri: uri4, title: "title" }, + { uri: uri5, title: "title" }, + { uri: uri6, title: "title" } + ]; + + // Disable autoFill to avoid handling the first result. + Services.prefs.setBoolPref("browser.urlbar.autoFill", "false"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + + do_print("http://www.site matches all site"); + yield check_autocomplete({ + search: "http://www.site", + matches: allMatches + }); + + do_print("http://site matches all site"); + yield check_autocomplete({ + search: "http://site", + matches: allMatches + }); + + do_print("ftp://ftp.site matches itself"); + yield check_autocomplete({ + search: "ftp://ftp.site", + matches: [ { uri: uri3, title: "title" } ] + }); + + do_print("ftp://site matches all site"); + yield check_autocomplete({ + search: "ftp://site", + matches: allMatches + }); + + do_print("https://www.site matches all site"); + yield check_autocomplete({ + search: "https://www.site", + matches: allMatches + }); + + do_print("https://site matches all site"); + yield check_autocomplete({ + search: "https://site", + matches: allMatches + }); + + do_print("www.site matches all site"); + yield check_autocomplete({ + search: "www.site", + matches: allMatches + }); + + do_print("w matches none of www."); + yield check_autocomplete({ + search: "w", + matches: [ { uri: uri7, title: "title" }, + { uri: uri8, title: "title" } ] + }); + + do_print("http://w matches none of www."); + yield check_autocomplete({ + search: "http://w", + matches: [ { uri: uri7, title: "title" }, + { uri: uri8, title: "title" } ] + }); + + do_print("http://w matches none of www."); + yield check_autocomplete({ + search: "http://www.w", + matches: [ { uri: uri7, title: "title" }, + { uri: uri8, title: "title" } ] + }); + + do_print("ww matches none of www."); + yield check_autocomplete({ + search: "ww", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("ww matches none of www."); + yield check_autocomplete({ + search: "ww", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("http://ww matches none of www."); + yield check_autocomplete({ + search: "http://ww", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("http://www.ww matches none of www."); + yield check_autocomplete({ + search: "http://www.ww", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("www matches none of www."); + yield check_autocomplete({ + search: "www", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("http://www matches none of www."); + yield check_autocomplete({ + search: "http://www", + matches: [ { uri: uri8, title: "title" } ] + }); + + do_print("http://www.www matches none of www."); + yield check_autocomplete({ + search: "http://www.www", + matches: [ { uri: uri8, title: "title" } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js new file mode 100644 index 000000000..740b8d8ed --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gTabRestrictChar = "%"; + +add_task(function* test_tab_matches() { + let uri1 = NetUtil.newURI("http://abc.com/"); + let uri2 = NetUtil.newURI("http://xyz.net/"); + let uri3 = NetUtil.newURI("about:mozilla"); + let uri4 = NetUtil.newURI("data:text/html,test"); + let uri5 = NetUtil.newURI("http://foobar.org"); + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "ABC rocks" }, + { uri: uri2, title: "xyz.net - we're better than ABC" }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ" } + ]); + addOpenPages(uri1, 1); + // Pages that cannot be registered in history. + addOpenPages(uri3, 1); + addOpenPages(uri4, 1); + + do_print("two results, normal result is a tab match"); + yield check_autocomplete({ + search: "abc.com", + searchParam: "enable-actions", + matches: [ makeVisitMatch("abc.com", "http://abc.com/", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSearchMatch("abc.com", { heuristic: false }) ] + }); + + do_print("three results, one tab match"); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("three results, both normal results are tab matches"); + addOpenPages(uri2, 1); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }), + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("a container tab is not visible in 'switch to tab'"); + addOpenPages(uri5, 1, /* userContextId: */ 3); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }), + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("a container tab should not see 'switch to tab' for other container tabs"); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions user-context-id:3", + matches: [ makeSearchMatch("abc", { heuristic: true }), + makeSwitchToTabMatch("http://foobar.org/", { title: "foobar.org - much better than ABC, definitely better than XYZ" }), + { uri: uri1, title: "ABC rocks", style: [ "favicon" ] }, + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ] + }); + + do_print("a different container tab should not see any 'switch to tab'"); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions user-context-id:2", + matches: [ makeSearchMatch("abc", { heuristic: true }), + { uri: uri1, title: "ABC rocks", style: [ "favicon" ] }, + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("three results, both normal results are tab matches, one has multiple tabs"); + addOpenPages(uri2, 5); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }), + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("three results, no tab matches (disable-private-actions)"); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions disable-private-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + { uri: uri1, title: "ABC rocks", style: [ "favicon" ] }, + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("two results (actions disabled)"); + yield check_autocomplete({ + search: "abc", + searchParam: "", + matches: [ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] }, + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("three results, no tab matches"); + removeOpenPages(uri1, 1); + removeOpenPages(uri2, 6); + yield check_autocomplete({ + search: "abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch("abc", { heuristic: true }), + { uri: uri1, title: "ABC rocks", style: [ "favicon" ] }, + { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] }, + { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ] + }); + + do_print("tab match search with restriction character"); + addOpenPages(uri1, 1); + yield check_autocomplete({ + search: gTabRestrictChar + " abc", + searchParam: "enable-actions", + matches: [ makeSearchMatch(gTabRestrictChar + " abc", { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }) ] + }); + + do_print("tab match with not-addable pages"); + yield check_autocomplete({ + search: "mozilla", + searchParam: "enable-actions", + matches: [ makeSearchMatch("mozilla", { heuristic: true }), + makeSwitchToTabMatch("about:mozilla") ] + }); + + do_print("tab match with not-addable pages and restriction character"); + yield check_autocomplete({ + search: gTabRestrictChar + " mozilla", + searchParam: "enable-actions", + matches: [ makeSearchMatch(gTabRestrictChar + " mozilla", { heuristic: true }), + makeSwitchToTabMatch("about:mozilla") ] + }); + + do_print("tab match with not-addable pages and only restriction character"); + yield check_autocomplete({ + search: gTabRestrictChar, + searchParam: "enable-actions", + matches: [ makeSearchMatch(gTabRestrictChar, { heuristic: true }), + makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }), + makeSwitchToTabMatch("about:mozilla"), + makeSwitchToTabMatch("data:text/html,test") ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_trimming.js b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js new file mode 100644 index 000000000..e55b009ff --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js @@ -0,0 +1,313 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(function* test_untrimmed_secure_www() { + do_print("Searching for untrimmed https://www entry"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("https://www.mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "https://www.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_secure_www_path() { + do_print("Searching for untrimmed https://www entry with path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("https://www.mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/t", + autofilled: "mozilla.org/test/", + completed: "https://www.mozilla.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_secure() { + do_print("Searching for untrimmed https:// entry"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("https://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "https://mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_secure_path() { + do_print("Searching for untrimmed https:// entry with path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("https://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/t", + autofilled: "mozilla.org/test/", + completed: "https://mozilla.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_www() { + do_print("Searching for untrimmed http://www entry"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://www.mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "www.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_www_path() { + do_print("Searching for untrimmed http://www entry with path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://www.mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/t", + autofilled: "mozilla.org/test/", + completed: "http://www.mozilla.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_ftp() { + do_print("Searching for untrimmed ftp:// entry"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("ftp://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "ftp://mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untrimmed_ftp_path() { + do_print("Searching for untrimmed ftp:// entry with path"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("ftp://mozilla.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "mozilla.org/t", + autofilled: "mozilla.org/test/", + completed: "ftp://mozilla.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_priority_1() { + do_print("Ensuring correct priority 1"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("https://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_periority_2() { + do_print( "Ensuring correct priority 2"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_periority_3() { + do_print("Ensuring correct priority 3"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_periority_4() { + do_print("Ensuring correct priority 4"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_priority_5() { + do_print("Ensuring correct priority 5"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("ftp://www.mozilla.org/test/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "ftp://mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_priority_6() { + do_print("Ensuring correct priority 6"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://www.mozilla.org/test1/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://www.mozilla.org/test2/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.org/", + completed: "www.mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_longer_domain() { + do_print("Ensuring longer domain can't match"); + // The .co should be preferred, but should not get the https from the .com. + // The .co domain must be added later to activate the trigger bug. + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("https://mozilla.com/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.co/"), transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://mozilla.co/"), transition: TRANSITION_TYPED } + ]); + yield check_autocomplete({ + search: "mo", + autofilled: "mozilla.co/", + completed: "mozilla.co/" + }); + + yield cleanup(); +}); + +add_task(function* test_escaped_chars() { + do_print("Searching for URL with characters that are normally escaped"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("https://www.mozilla.org/啊-test"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "https://www.mozilla.org/啊-test", + autofilled: "https://www.mozilla.org/啊-test", + completed: "https://www.mozilla.org/啊-test" + }); + yield cleanup(); +}); + +add_task(function* test_unsecure_secure() { + do_print("Don't return unsecure URL when searching for secure ones"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://test.moz.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "https://test.moz.org/t", + autofilled: "https://test.moz.org/test/", + completed: "https://test.moz.org/test/" + }); + yield cleanup(); +}); + +add_task(function* test_unsecure_secure_domain() { + do_print("Don't return unsecure domain when searching for secure ones"); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://test.moz.org/test/"), + transition: TRANSITION_TYPED + }); + yield check_autocomplete({ + search: "https://test.moz", + autofilled: "https://test.moz.org/", + completed: "https://test.moz.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untyped_www() { + do_print("Untyped is not accounted for www"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.moz.org/test/") }); + yield check_autocomplete({ + search: "mo", + autofilled: "moz.org/", + completed: "moz.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untyped_ftp() { + do_print("Untyped is not accounted for ftp"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("ftp://moz.org/test/") }); + yield check_autocomplete({ + search: "mo", + autofilled: "moz.org/", + completed: "moz.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untyped_secure() { + do_print("Untyped is not accounted for https"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://moz.org/test/") }); + yield check_autocomplete({ + search: "mo", + autofilled: "moz.org/", + completed: "moz.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untyped_secure_www() { + do_print("Untyped is not accounted for https://www"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://www.moz.org/test/") }); + yield check_autocomplete({ + search: "mo", + autofilled: "moz.org/", + completed: "moz.org/" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_typed.js b/toolkit/components/places/tests/unifiedcomplete/test_typed.js new file mode 100644 index 000000000..72f76159c --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_typed.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// First do searches with typed behavior forced to false, so later tests will +// ensure autocomplete is able to dinamically switch behavior. + +const FAVICON_HREF = NetUtil.newURI(do_get_file("../favicons/favicon-normal16.png")).spec; + +add_task(function* test_domain() { + do_print("Searching for domain should autoFill it"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/")); + yield setFaviconForHref("http://mozilla.org/link/", FAVICON_HREF); + yield check_autocomplete({ + search: "moz", + autofilled: "mozilla.org/", + completed: "mozilla.org/", + icon: "moz-anno:favicon:" + FAVICON_HREF + }); + yield cleanup(); +}); + +add_task(function* test_url() { + do_print("Searching for url should autoFill it"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/")); + yield setFaviconForHref("http://mozilla.org/link/", FAVICON_HREF); + yield check_autocomplete({ + search: "mozilla.org/li", + autofilled: "mozilla.org/link/", + completed: "http://mozilla.org/link/", + icon: "moz-anno:favicon:" + FAVICON_HREF + }); + yield cleanup(); +}); + +// Now do searches with typed behavior forced to true. + +add_task(function* test_untyped_domain() { + do_print("Searching for non-typed domain should not autoFill it"); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/")); + yield check_autocomplete({ + search: "moz", + autofilled: "moz", + completed: "moz" + }); + yield cleanup(); +}); + +add_task(function* test_typed_domain() { + do_print("Searching for typed domain should autoFill it"); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/typed/"), + transition: TRANSITION_TYPED }); + yield check_autocomplete({ + search: "moz", + autofilled: "mozilla.org/", + completed: "mozilla.org/" + }); + yield cleanup(); +}); + +add_task(function* test_untyped_url() { + do_print("Searching for non-typed url should not autoFill it"); + yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/")); + yield check_autocomplete({ + search: "mozilla.org/li", + autofilled: "mozilla.org/li", + completed: "mozilla.org/li" + }); + yield cleanup(); +}); + +add_task(function* test_typed_url() { + do_print("Searching for typed url should autoFill it"); + yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), + transition: TRANSITION_TYPED }); + yield check_autocomplete({ + search: "mozilla.org/li", + autofilled: "mozilla.org/link/", + completed: "http://mozilla.org/link/" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js new file mode 100644 index 000000000..eaccb23e5 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js @@ -0,0 +1,186 @@ +add_task(function*() { + do_print("visit url, no protocol"); + yield check_autocomplete({ + search: "mozilla.org", + searchParam: "enable-actions", + matches: [ + { uri: makeActionURI("visiturl", {url: "http://mozilla.org/", input: "mozilla.org"}), title: "http://mozilla.org/", style: [ "action", "visiturl", "heuristic" ] }, + { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "mozilla.org", searchQuery: "mozilla.org"}), title: "MozSearch", style: ["action", "searchengine"] } + ] + }); + + do_print("visit url, no protocol but with 2 dots"); + yield check_autocomplete({ + search: "www.mozilla.org", + searchParam: "enable-actions", + matches: [ + { uri: makeActionURI("visiturl", {url: "http://www.mozilla.org/", input: "www.mozilla.org"}), title: "http://www.mozilla.org/", style: [ "action", "visiturl", "heuristic" ] }, + { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "www.mozilla.org", searchQuery: "www.mozilla.org"}), title: "MozSearch", style: ["action", "searchengine"] } + ] + }); + + do_print("visit url, no protocol but with 3 dots"); + yield check_autocomplete({ + search: "www.mozilla.org.tw", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("visiturl", {url: "http://www.mozilla.org.tw/", input: "www.mozilla.org.tw"}), title: "http://www.mozilla.org.tw/", style: [ "action", "visiturl", "heuristic" ] } ] + }); + + do_print("visit url, with protocol but with 2 dots"); + yield check_autocomplete({ + search: "https://www.mozilla.org", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("visiturl", {url: "https://www.mozilla.org/", input: "https://www.mozilla.org"}), title: "https://www.mozilla.org/", style: [ "action", "visiturl", "heuristic" ] } ] + }); + + do_print("visit url, with protocol but with 3 dots"); + yield check_autocomplete({ + search: "https://www.mozilla.org.tw", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("visiturl", {url: "https://www.mozilla.org.tw/", input: "https://www.mozilla.org.tw"}), title: "https://www.mozilla.org.tw/", style: [ "action", "visiturl", "heuristic" ] } ] + }); + + do_print("visit url, with protocol"); + yield check_autocomplete({ + search: "https://mozilla.org", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("visiturl", {url: "https://mozilla.org/", input: "https://mozilla.org"}), title: "https://mozilla.org/", style: [ "action", "visiturl", "heuristic" ] } ] + }); + + do_print("visit url, about: protocol (no host)"); + yield check_autocomplete({ + search: "about:config", + searchParam: "enable-actions", + matches: [ { uri: makeActionURI("visiturl", {url: "about:config", input: "about:config"}), title: "about:config", style: [ "action", "visiturl", "heuristic" ] } ] + }); + + // This is distinct because of how we predict being able to url autofill via + // host lookups. + do_print("visit url, host matching visited host but not visited url"); + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://mozilla.org/wine/"), title: "Mozilla Wine", transition: TRANSITION_TYPED }, + ]); + yield check_autocomplete({ + search: "mozilla.org/rum", + searchParam: "enable-actions", + matches: [ makeVisitMatch("mozilla.org/rum", "http://mozilla.org/rum", { heuristic: true }) ] + }); + + // And hosts with no dot in them are special, due to requiring whitelisting. + do_print("non-whitelisted host"); + yield check_autocomplete({ + search: "firefox", + searchParam: "enable-actions", + matches: [ makeSearchMatch("firefox", { heuristic: true }) ] + }); + + do_print("url with non-whitelisted host"); + yield check_autocomplete({ + search: "firefox/get", + searchParam: "enable-actions", + matches: [ makeVisitMatch("firefox/get", "http://firefox/get", { heuristic: true }) ] + }); + + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true); + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox"); + }); + + do_print("whitelisted host"); + yield check_autocomplete({ + search: "firefox", + searchParam: "enable-actions", + matches: [ + makeVisitMatch("firefox", "http://firefox/", { heuristic: true }), + makeSearchMatch("firefox", { heuristic: false }) + ] + }); + + do_print("url with whitelisted host"); + yield check_autocomplete({ + search: "firefox/get", + searchParam: "enable-actions", + matches: [ makeVisitMatch("firefox/get", "http://firefox/get", { heuristic: true }) ] + }); + + do_print("visit url, host matching visited host but not visited url, whitelisted host"); + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true); + do_register_cleanup(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla"); + }); + yield check_autocomplete({ + search: "mozilla/rum", + searchParam: "enable-actions", + matches: [ makeVisitMatch("mozilla/rum", "http://mozilla/rum", { heuristic: true }) ] + }); + + // ipv4 and ipv6 literal addresses should offer to visit. + do_print("visit url, ipv4 literal"); + yield check_autocomplete({ + search: "127.0.0.1", + searchParam: "enable-actions", + matches: [ makeVisitMatch("127.0.0.1", "http://127.0.0.1/", { heuristic: true }) ] + }); + + do_print("visit url, ipv6 literal"); + yield check_autocomplete({ + search: "[2001:db8::1]", + searchParam: "enable-actions", + matches: [ makeVisitMatch("[2001:db8::1]", "http://[2001:db8::1]/", { heuristic: true }) ] + }); + + // Setting keyword.enabled to false should always try to visit. + let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); + Services.prefs.setBoolPref("keyword.enabled", false); + do_register_cleanup(() => { + Services.prefs.clearUserPref("keyword.enabled"); + }); + do_print("visit url, keyword.enabled = false"); + yield check_autocomplete({ + search: "bacon", + searchParam: "enable-actions", + matches: [ makeVisitMatch("bacon", "http://bacon/", { heuristic: true }) ] + }); + do_print("visit two word query, keyword.enabled = false"); + yield check_autocomplete({ + search: "bacon lovers", + searchParam: "enable-actions", + matches: [ makeVisitMatch("bacon lovers", "bacon lovers", { heuristic: true }) ] + }); + Services.prefs.setBoolPref("keyword.enabled", keywordEnabled); + + do_print("visit url, scheme+host"); + yield check_autocomplete({ + search: "http://example", + searchParam: "enable-actions", + matches: [ makeVisitMatch("http://example", "http://example/", { heuristic: true }) ] + }); + + do_print("visit url, scheme+host"); + yield check_autocomplete({ + search: "ftp://example", + searchParam: "enable-actions", + matches: [ makeVisitMatch("ftp://example", "ftp://example/", { heuristic: true }) ] + }); + + do_print("visit url, host+port"); + yield check_autocomplete({ + search: "example:8080", + searchParam: "enable-actions", + matches: [ makeVisitMatch("example:8080", "http://example:8080/", { heuristic: true }) ] + }); + + do_print("numerical operations that look like urls should search"); + yield check_autocomplete({ + search: "123/12", + searchParam: "enable-actions", + matches: [ makeSearchMatch("123/12", { heuristic: true }) ] + }); + + do_print("numerical operations that look like urls should search"); + yield check_autocomplete({ + search: "123.12/12.1", + searchParam: "enable-actions", + matches: [ makeSearchMatch("123.12/12.1", { heuristic: true }) ] + }); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js new file mode 100644 index 000000000..f79573ae6 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 393678 to make sure matches against the url, title, tags are only + * made on word boundaries instead of in the middle of words. + * + * Make sure we don't try matching one after a CamelCase because the upper-case + * isn't really a word boundary. (bug 429498) + * + * Bug 429531 provides switching between "must match on word boundary" and "can + * match," so leverage "must match" pref for checking word boundary logic and + * make sure "can match" matches anywhere. + */ + +var katakana = ["\u30a8", "\u30c9"]; // E, Do +var ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do + +add_task(function* test_escape() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); + + let uri1 = NetUtil.newURI("http://matchme/"); + let uri2 = NetUtil.newURI("http://dontmatchme/"); + let uri3 = NetUtil.newURI("http://title/1"); + let uri4 = NetUtil.newURI("http://title/2"); + let uri5 = NetUtil.newURI("http://tag/1"); + let uri6 = NetUtil.newURI("http://tag/2"); + let uri7 = NetUtil.newURI("http://crazytitle/"); + let uri8 = NetUtil.newURI("http://katakana/"); + let uri9 = NetUtil.newURI("http://ideograph/"); + let uri10 = NetUtil.newURI("http://camel/pleaseMatchMe/"); + + yield PlacesTestUtils.addVisits([ + { uri: uri1, title: "title1" }, + { uri: uri2, title: "title1" }, + { uri: uri3, title: "matchme2" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri5, title: "title1" }, + { uri: uri6, title: "title1" }, + { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" }, + { uri: uri8, title: katakana.join("") }, + { uri: uri9, title: ideograph.join("") }, + { uri: uri10, title: "title1" } + ]); + yield addBookmark( { uri: uri5, title: "title1", tags: [ "matchme2" ] } ); + yield addBookmark( { uri: uri6, title: "title1", tags: [ "dontmatchme3" ] } ); + + // match only on word boundaries + Services.prefs.setIntPref("browser.urlbar.matchBehavior", 2); + + do_print("Match 'match' at the beginning or after / or on a CamelCase"); + yield check_autocomplete({ + search: "match", + matches: [ { uri: uri1, title: "title1" }, + { uri: uri3, title: "matchme2" }, + { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "title1" } ] + }); + + do_print("Match 'dont' at the beginning or after /"); + yield check_autocomplete({ + search: "dont", + matches: [ { uri: uri2, title: "title1" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Match 'match' at the beginning or after / or on a CamelCase"); + yield check_autocomplete({ + search: "2", + matches: [ { uri: uri3, title: "matchme2" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] }, + { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ] + }); + + do_print("Match 't' at the beginning or after /"); + yield check_autocomplete({ + search: "t", + matches: [ { uri: uri1, title: "title1" }, + { uri: uri2, title: "title1" }, + { uri: uri3, title: "matchme2" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] }, + { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "title1" } ] + }); + + do_print("Match 'word' after many consecutive word boundaries"); + yield check_autocomplete({ + search: "word", + matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ] + }); + + do_print("Match a word boundary '/' for everything"); + yield check_autocomplete({ + search: "/", + matches: [ { uri: uri1, title: "title1" }, + { uri: uri2, title: "title1" }, + { uri: uri3, title: "matchme2" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] }, + { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] }, + { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" }, + { uri: uri8, title: katakana.join("") }, + { uri: uri9, title: ideograph.join("") }, + { uri: uri10, title: "title1" } ] + }); + + do_print("Match word boundaries '()_+' that are among word boundaries"); + yield check_autocomplete({ + search: "()_+", + matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ] + }); + + do_print("Katakana characters form a string, so match the beginning"); + yield check_autocomplete({ + search: katakana[0], + matches: [ { uri: uri8, title: katakana.join("") } ] + }); + +/* + do_print("Middle of a katakana word shouldn't be matched"); + yield check_autocomplete({ + search: katakana[1], + matches: [ ] + }); +*/ + do_print("Ideographs are treated as words so 'nin' is one word"); + yield check_autocomplete({ + search: ideograph[0], + matches: [ { uri: uri9, title: ideograph.join("") } ] + }); + + do_print("Ideographs are treated as words so 'ten' is another word"); + yield check_autocomplete({ + search: ideograph[1], + matches: [ { uri: uri9, title: ideograph.join("") } ] + }); + + do_print("Ideographs are treated as words so 'do' is yet another word"); + yield check_autocomplete({ + search: ideograph[2], + matches: [ { uri: uri9, title: ideograph.join("") } ] + }); + + do_print("Extra negative assert that we don't match in the middle"); + yield check_autocomplete({ + search: "ch", + matches: [ ] + }); + + do_print("Don't match one character after a camel-case word boundary (bug 429498)"); + yield check_autocomplete({ + search: "atch", + matches: [ ] + }); + + // match against word boundaries and anywhere + Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1); + + yield check_autocomplete({ + search: "tch", + matches: [ { uri: uri1, title: "title1" }, + { uri: uri2, title: "title1" }, + { uri: uri3, title: "matchme2" }, + { uri: uri4, title: "dontmatchme3" }, + { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] }, + { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] }, + { uri: uri10, title: "title1" } ] + }); + + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js new file mode 100644 index 000000000..adf638886 --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Ensure inline autocomplete doesn't return zero frecency pages. + +add_task(function* test_zzero_frec_domain() { + do_print("Searching for zero frecency domain should not autoFill it"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/framed_link/"), + transition: TRANSITION_FRAMED_LINK + }); + yield check_autocomplete({ + search: "moz", + autofilled: "moz", + completed: "moz" + }); + yield cleanup(); +}); + +add_task(function* test_zzero_frec_url() { + do_print("Searching for zero frecency url should not autoFill it"); + Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/framed_link/"), + transition: TRANSITION_FRAMED_LINK + }); + yield check_autocomplete({ + search: "mozilla.org/f", + autofilled: "mozilla.org/f", + completed: "mozilla.org/f" + }); + yield cleanup(); +}); diff --git a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini new file mode 100644 index 000000000..60ef8c48a --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini @@ -0,0 +1,49 @@ +[DEFAULT] +head = head_autocomplete.js +tail = +skip-if = toolkit == 'android' +support-files = + data/engine-rel-searchform.xml + data/engine-suggestions.xml + !/toolkit/components/places/tests/favicons/favicon-normal16.png + +[test_416211.js] +[test_416214.js] +[test_417798.js] +[test_418257.js] +[test_422277.js] +[test_autocomplete_functional.js] +[test_autocomplete_on_value_removed_479089.js] +[test_autofill_default_behavior.js] +[test_avoid_middle_complete.js] +[test_avoid_stripping_to_empty_tokens.js] +[test_casing.js] +[test_do_not_trim.js] +[test_download_embed_bookmarks.js] +[test_dupe_urls.js] +[test_empty_search.js] +[test_enabled.js] +[test_escape_self.js] +[test_extension_matches.js] +[test_ignore_protocol.js] +[test_keyword_search.js] +[test_keyword_search_actions.js] +[test_keywords.js] +[test_match_beginning.js] +[test_multi_word_search.js] +[test_query_url.js] +[test_remote_tab_matches.js] +skip-if = !sync +[test_search_engine_alias.js] +[test_search_engine_current.js] +[test_search_engine_host.js] +[test_search_engine_restyle.js] +[test_search_suggestions.js] +[test_special_search.js] +[test_swap_protocol.js] +[test_tab_matches.js] +[test_trimming.js] +[test_typed.js] +[test_visit_url.js] +[test_word_boundary_search.js] +[test_zero_frecency.js] diff --git a/toolkit/components/places/tests/unit/.eslintrc.js b/toolkit/components/places/tests/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/places/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/places/tests/unit/bookmarks.corrupt.html b/toolkit/components/places/tests/unit/bookmarks.corrupt.html new file mode 100644 index 000000000..3cf43367f --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.corrupt.html @@ -0,0 +1,36 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1 LAST_MODIFIED="1177541029">Bookmarks</H1> + +<DL><p> + <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3> + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + <DT><A HREF="b0rked" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + </DL><p> + <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3> +<DD>folder test comment + <DL><p> + <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A> +<DD>item description + </DL> + <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3> + <DL><p> + <DT><A HREF="http://example.tld">Example.tld</A> + </DL><p> + <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3> +<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A> + <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A> + <DT><A HREF="http://bogus-icon.mozilla.com/" ICON="b0rked" ID="rdf:#$GvPhC3">Getting Started</A> +<DD>Livemark test comment + </DL><p> +</DL><p> diff --git a/toolkit/components/places/tests/unit/bookmarks.json b/toolkit/components/places/tests/unit/bookmarks.json new file mode 100644 index 000000000..afe62abae --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","id":1,"dateAdded":1361551978957783,"lastModified":1361551978957783,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1361551978957783,"lastModified":1361551979382837,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"OCyeUO5uu9FF","title":"Mozilla Firefox","id":6,"parent":2,"dateAdded":1361551979350273,"lastModified":1361551979376699,"type":"text/x-moz-place-container","children":[{"guid":"OCyeUO5uu9FG","title":"Help and Tutorials","id":7,"parent":6,"dateAdded":1361551979356436,"lastModified":1361551979362718,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/help/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FH","index":1,"title":"Customize Firefox","id":8,"parent":6,"dateAdded":1361551979365662,"lastModified":1361551979368077,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/customize/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FI","index":2,"title":"Get Involved","id":9,"parent":6,"dateAdded":1361551979371071,"lastModified":1361551979373745,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/community/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FJ","index":3,"title":"About Us","id":10,"parent":6,"dateAdded":1361551979376699,"lastModified":1361551979379060,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/about/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="}]},{"guid":"OCyeUO5uu9FK","index":1,"title":"","id":11,"parent":2,"dateAdded":1361551979380988,"lastModified":1361551979380988,"type":"text/x-moz-place-separator"},{"guid":"OCyeUO5uu9FL","index":2,"title":"test","id":12,"parent":2,"dateAdded":1177541020000000,"lastModified":1177541050000000,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"folder test comment"}],"type":"text/x-moz-place-container","children":[{"guid":"OCyeUO5uu9GX","title":"test post keyword","id":13,"parent":12,"dateAdded":1177375336000000,"lastModified":1177375423000000,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"item description"},{"name":"bookmarkProperties/POSTData","flags":0,"expires":4,"mimeType":null,"type":3,"value":"hidden1%3Dbar&text1%3D%25s"},{"name":"bookmarkProperties/loadInSidebar","flags":0,"expires":4,"mimeType":null,"type":1,"value":1}],"type":"text/x-moz-place","uri":"http://test/post","keyword":"test","charset":"ISO-8859-1"}]}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1361551978957783,"lastModified":1177541050000000,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"OCyeUO5uu9FB","title":"Getting Started","id":15,"parent":3,"dateAdded":1361551979409695,"lastModified":1361551979412080,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/central/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FR","index":1,"title":"Latest Headlines","id":16,"parent":3,"dateAdded":1361551979451584,"lastModified":1361551979457086,"livemark":1,"annos":[{"name":"placesInternal/READ_ONLY","flags":0,"expires":4,"mimeType":null,"type":1,"value":1},{"name":"livemark/feedURI","flags":0,"expires":4,"mimeType":null,"type":3,"value":"http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"},{"name":"livemark/siteURI","flags":0,"expires":4,"mimeType":null,"type":3,"value":"http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"}],"type":"text/x-moz-place-container","children":[]}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1361551978957783,"lastModified":1361551978957783,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1361551978957783,"lastModified":1177541050000000,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"OCyeUO5uu9FW","title":"Example.tld","id":14,"parent":5,"dateAdded":1361551979401846,"lastModified":1361551979402952,"type":"text/x-moz-place","uri":"http://example.tld/"}]}]} diff --git a/toolkit/components/places/tests/unit/bookmarks.preplaces.html b/toolkit/components/places/tests/unit/bookmarks.preplaces.html new file mode 100644 index 000000000..2e5a1baf0 --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks.preplaces.html @@ -0,0 +1,35 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1 LAST_MODIFIED="1177541029">Bookmarks</H1> + +<DL><p> + <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3> + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A> + </DL><p> + <HR> + <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3> +<DD>folder test comment + <DL><p> + <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A> +<DD>item description + </DL> + <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3> + <DL><p> + <DT><A HREF="http://example.tld">Example.tld</A> + </DL><p> + <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3> +<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar + <DL><p> + <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A> + <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A> +<DD>Livemark test comment + </DL><p> +</DL><p> diff --git a/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html new file mode 100644 index 000000000..9fe662f32 --- /dev/null +++ b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE NETSCAPE-Bookmark-file-1> + <HTML> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> + <Title>Bookmarks</Title> + <H1>Bookmarks</H1> + <DT><H3>Subtitle</H3> + <DL><p> + <DT><A HREF="http://www.mozilla.org/">Mozilla</A> + </DL><p> +</HTML> diff --git a/toolkit/components/places/tests/unit/bug476292.sqlite b/toolkit/components/places/tests/unit/bug476292.sqlite Binary files differnew file mode 100644 index 000000000..43130cb51 --- /dev/null +++ b/toolkit/components/places/tests/unit/bug476292.sqlite diff --git a/toolkit/components/places/tests/unit/corruptDB.sqlite b/toolkit/components/places/tests/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 000000000..b234246ca --- /dev/null +++ b/toolkit/components/places/tests/unit/corruptDB.sqlite diff --git a/toolkit/components/places/tests/unit/default.sqlite b/toolkit/components/places/tests/unit/default.sqlite Binary files differnew file mode 100644 index 000000000..8fbd3bc9a --- /dev/null +++ b/toolkit/components/places/tests/unit/default.sqlite diff --git a/toolkit/components/places/tests/unit/head_bookmarks.js b/toolkit/components/places/tests/unit/head_bookmarks.js new file mode 100644 index 000000000..842a66b31 --- /dev/null +++ b/toolkit/components/places/tests/unit/head_bookmarks.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +// Import common head. +{ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. diff --git a/toolkit/components/places/tests/unit/livemark.xml b/toolkit/components/places/tests/unit/livemark.xml new file mode 100644 index 000000000..db2ea9023 --- /dev/null +++ b/toolkit/components/places/tests/unit/livemark.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <title>Livemark Feed</title> + <link href="https://example.com/"/> + <updated>2016-08-09T19:51:45.147Z</updated> + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:e7947414-6ee0-4009-ae75-8b0ad3c6894b</id> + <entry> + <title>Some awesome article</title> + <link href="https://example.com/some-article"/> + <id>urn:uuid:d72ce019-0a56-4a0b-ac03-f66117d78141</id> + <updated>2016-08-09T19:57:22.178Z</updated> + <summary>My great article summary.</summary> + </entry> +</feed> diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json new file mode 100644 index 000000000..38762b3f1 --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"},{"guid":"XF4yRP6bTuil","title":"Mobile bookmarks query","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":5,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":9,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"KIa9iKZab2Z5","title":"Add-ons","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":10,"type":"text/x-moz-place","uri":"https://addons.mozilla.org"}]}]}
\ No newline at end of file diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json new file mode 100644 index 000000000..7319a3a52 --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":5,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":6,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":7,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":8,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"}]}
\ No newline at end of file diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json new file mode 100644 index 000000000..afe13c975 --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":3,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"},{"guid":"F_LBgd1fS_uQ","title":"Mobile bookmarks query for first folder","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"},{"guid":"oIpmQXMWsXvY","title":"Mobile bookmarks query for second folder","index":2,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":12,"type":"text/x-moz-place","uri":"place:folder=102"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":5,"type":"text/x-moz-place","uri":"https://mozilla.org/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":6,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":7,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"o4YjJpgsufU-","title":"Mobile Bookmarks","index":7,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":102,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","children":[{"guid":"sSZ86WT9WbN3","title":"DXR","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":9,"type":"text/x-moz-place","uri":"https://dxr.mozilla.org"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":10,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":11,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json new file mode 100644 index 000000000..27f5825ec --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]}]}
\ No newline at end of file diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json new file mode 100644 index 000000000..85721f2fa --- /dev/null +++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json @@ -0,0 +1 @@ +{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731955000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731961000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":7,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":8,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.js b/toolkit/components/places/tests/unit/nsDummyObserver.js new file mode 100644 index 000000000..9049d04b3 --- /dev/null +++ b/toolkit/components/places/tests/unit/nsDummyObserver.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +// Dummy boomark/history observer +function DummyObserver() { + Services.obs.notifyObservers(null, "dummy-observer-created", null); +} + +DummyObserver.prototype = { + // history observer + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, aTransitionType) { + Services.obs.notifyObservers(null, "dummy-observer-visited", null); + }, + onTitleChanged: function () {}, + onDeleteURI: function () {}, + onClearHistory: function () {}, + onPageChanged: function () {}, + onDeleteVisits: function () {}, + + // bookmark observer + // onBeginUpdateBatch: function() {}, + // onEndUpdateBatch: function() {}, + onItemAdded: function(aItemId, aParentId, aIndex, aItemType, aURI) { + Services.obs.notifyObservers(null, "dummy-observer-item-added", null); + }, + onItemChanged: function () {}, + onItemRemoved: function() {}, + onItemVisited: function() {}, + onItemMoved: function() {}, + + classID: Components.ID("62e221d3-68c3-4e1a-8943-a27beb5005fe"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + Ci.nsINavHistoryObserver, + ]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DummyObserver]); diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.manifest b/toolkit/components/places/tests/unit/nsDummyObserver.manifest new file mode 100644 index 000000000..ed4d87fff --- /dev/null +++ b/toolkit/components/places/tests/unit/nsDummyObserver.manifest @@ -0,0 +1,4 @@ +component 62e221d3-68c3-4e1a-8943-a27beb5005fe nsDummyObserver.js +contract @mozilla.org/places/test/dummy-observer;1 62e221d3-68c3-4e1a-8943-a27beb5005fe +category bookmark-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1 +category history-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1 diff --git a/toolkit/components/places/tests/unit/places.sparse.sqlite b/toolkit/components/places/tests/unit/places.sparse.sqlite Binary files differnew file mode 100644 index 000000000..915089021 --- /dev/null +++ b/toolkit/components/places/tests/unit/places.sparse.sqlite diff --git a/toolkit/components/places/tests/unit/test_000_frecency.js b/toolkit/components/places/tests/unit/test_000_frecency.js new file mode 100644 index 000000000..0a7347a02 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_000_frecency.js @@ -0,0 +1,273 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + +Autocomplete Frecency Tests + +- add a visit for each score permutation +- search +- test number of matches +- test each item's location in results + +*/ + +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); +} catch (ex) { + do_throw("Could not get services\n"); +} + +var bucketPrefs = [ + [ "firstBucketCutoff", "firstBucketWeight"], + [ "secondBucketCutoff", "secondBucketWeight"], + [ "thirdBucketCutoff", "thirdBucketWeight"], + [ "fourthBucketCutoff", "fourthBucketWeight"], + [ null, "defaultBucketWeight"] +]; + +var bonusPrefs = { + embedVisitBonus: Ci.nsINavHistoryService.TRANSITION_EMBED, + framedLinkVisitBonus: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK, + linkVisitBonus: Ci.nsINavHistoryService.TRANSITION_LINK, + typedVisitBonus: Ci.nsINavHistoryService.TRANSITION_TYPED, + bookmarkVisitBonus: Ci.nsINavHistoryService.TRANSITION_BOOKMARK, + downloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, + permRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT, + tempRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY, + reloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_RELOAD, +}; + +// create test data +var searchTerm = "frecency"; +var results = []; +var matchCount = 0; +var now = Date.now(); +var prefPrefix = "places.frecency."; + +function* task_initializeBucket(bucket) { + let [cutoffName, weightName] = bucket; + // get pref values + var weight = 0, cutoff = 0; + try { + weight = prefs.getIntPref(prefPrefix + weightName); + } catch (ex) {} + try { + cutoff = prefs.getIntPref(prefPrefix + cutoffName); + } catch (ex) {} + + if (cutoff < 1) + return; + + // generate a date within the cutoff period + var dateInPeriod = (now - ((cutoff - 1) * 86400 * 1000)) * 1000; + + for (let [bonusName, visitType] of Object.entries(bonusPrefs)) { + var frecency = -1; + var calculatedURI = null; + var matchTitle = ""; + var bonusValue = prefs.getIntPref(prefPrefix + bonusName); + // unvisited (only for first cutoff date bucket) + if (bonusName == "unvisitedBookmarkBonus" || bonusName == "unvisitedTypedBonus") { + if (cutoffName == "firstBucketCutoff") { + let points = Math.ceil(bonusValue / parseFloat(100.0) * weight); + var visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0; + frecency = Math.ceil(visitCount * points); + calculatedURI = uri("http://" + searchTerm + ".com/" + + bonusName + ":" + bonusValue + "/cutoff:" + cutoff + + "/weight:" + weight + "/frecency:" + frecency); + if (bonusName == "unvisitedBookmarkBonus") { + matchTitle = searchTerm + "UnvisitedBookmark"; + bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle); + } + else { + matchTitle = searchTerm + "UnvisitedTyped"; + yield PlacesTestUtils.addVisits({ + uri: calculatedURI, + title: matchTitle, + transition: visitType, + visitDate: now + }); + histsvc.markPageAsTyped(calculatedURI); + } + } + } + else { + // visited + // visited bookmarks get the visited bookmark bonus twice + if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) + bonusValue = bonusValue * 2; + + let points = Math.ceil(1 * ((bonusValue / parseFloat(100.000000)).toFixed(6) * weight) / 1); + if (!points) { + if (visitType == Ci.nsINavHistoryService.TRANSITION_EMBED || + visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK || + visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD || + visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD || + bonusName == "defaultVisitBonus") + frecency = 0; + else + frecency = -1; + } + else + frecency = points; + calculatedURI = uri("http://" + searchTerm + ".com/" + + bonusName + ":" + bonusValue + "/cutoff:" + cutoff + + "/weight:" + weight + "/frecency:" + frecency); + if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) { + matchTitle = searchTerm + "Bookmarked"; + bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle); + } + else + matchTitle = calculatedURI.spec.substr(calculatedURI.spec.lastIndexOf("/")+1); + yield PlacesTestUtils.addVisits({ + uri: calculatedURI, + transition: visitType, + visitDate: dateInPeriod + }); + } + + if (calculatedURI && frecency) { + results.push([calculatedURI, frecency, matchTitle]); + yield PlacesTestUtils.addVisits({ + uri: calculatedURI, + title: matchTitle, + transition: visitType, + visitDate: dateInPeriod + }); + } + } +} + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +add_task(function* test_frecency() +{ + // Disable autoFill for this test. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill")); + for (let bucket of bucketPrefs) { + yield task_initializeBucket(bucket); + } + + // sort results by frecency + results.sort((a, b) => b[1] - a[1]); + // Make sure there's enough results returned + prefs.setIntPref("browser.urlbar.maxRichResults", results.length); + + // DEBUG + // results.every(function(el) { dump("result: " + el[1] + ": " + el[0].spec + " (" + el[2] + ")\n"); return true; }) + + yield PlacesTestUtils.promiseAsyncUpdates(); + + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + // always search in history + bookmarks, no matter what the default is + prefs.setIntPref("browser.urlbar.search.sources", 3); + prefs.setIntPref("browser.urlbar.default.behavior", 0); + + var numSearchesStarted = 0; + input.onSearchBegin = function() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + let deferred = Promise.defer(); + input.onSearchComplete = function() { + do_check_eq(numSearchesStarted, 1); + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + + // test that all records with non-zero frecency were matched + do_check_eq(controller.matchCount, results.length); + + // test that matches are sorted by frecency + for (var i = 0; i < controller.matchCount; i++) { + let searchURL = controller.getValueAt(i); + let expectURL = results[i][0].spec; + if (searchURL == expectURL) { + do_check_eq(controller.getValueAt(i), results[i][0].spec); + do_check_eq(controller.getCommentAt(i), results[i][2]); + } else { + // If the results didn't match exactly, perhaps it's still the right + // frecency just in the wrong "order" (order of same frecency is + // undefined), so check if frecency matches. This is okay because we + // can still ensure the correct number of expected frecencies. + let getFrecency = aURL => aURL.match(/frecency:(-?\d+)$/)[1]; + print("### checking for same frecency between '" + searchURL + + "' and '" + expectURL + "'"); + do_check_eq(getFrecency(searchURL), getFrecency(expectURL)); + } + } + deferred.resolve(); + }; + + controller.startSearch(searchTerm); + + yield deferred.promise; +}); diff --git a/toolkit/components/places/tests/unit/test_1085291.js b/toolkit/components/places/tests/unit/test_1085291.js new file mode 100644 index 000000000..3159ff8bc --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1085291.js @@ -0,0 +1,42 @@ +add_task(function* () { + // test that nodes inserted by incremental update for bookmarks of all types + // have the extra bookmark properties (bookmarkGuid, dateAdded, lastModified). + + // getFolderContents opens the root node. + let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + + function* insertAndTest(bmInfo) { + bmInfo = yield PlacesUtils.bookmarks.insert(bmInfo); + let node = root.getChild(root.childCount - 1); + Assert.equal(node.bookmarkGuid, bmInfo.guid); + Assert.equal(node.dateAdded, bmInfo.dateAdded * 1000); + Assert.equal(node.lastModified, bmInfo.lastModified * 1000); + } + + // Normal bookmark. + yield insertAndTest({ parentGuid: root.bookmarkGuid + , type: PlacesUtils.bookmarks.TYPE_BOOKMARK + , title: "Test Bookmark" + , url: "http://test.url.tld" }); + + // place: query + yield insertAndTest({ parentGuid: root.bookmarkGuid + , type: PlacesUtils.bookmarks.TYPE_BOOKMARK + , title: "Test Query" + , url: "place:folder=BOOKMARKS_MENU" }); + + // folder + yield insertAndTest({ parentGuid: root.bookmarkGuid + , type: PlacesUtils.bookmarks.TYPE_FOLDER + , title: "Test Folder" }); + + // separator + yield insertAndTest({ parentGuid: root.bookmarkGuid + , type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + + root.containerOpen = false; +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_1105208.js b/toolkit/components/places/tests/unit/test_1105208.js new file mode 100644 index 000000000..39a27c95f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1105208.js @@ -0,0 +1,24 @@ +// Test that result node for folder shortcuts get the target folder title if +// the shortcut itself has no title set. +add_task(function* () { + let shortcutInfo = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "place:folder=TOOLBAR" + }); + + let unfiledRoot = + PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.equal(shortcutNode.bookmarkGuid, shortcutInfo.guid); + + let toolbarInfo = + yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid); + Assert.equal(shortcutNode.title, toolbarInfo.title); + + unfiledRoot.containerOpen = false; +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_1105866.js b/toolkit/components/places/tests/unit/test_1105866.js new file mode 100644 index 000000000..eb376bbe2 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_1105866.js @@ -0,0 +1,63 @@ +add_task(function* test_folder_shortcuts() { + let shortcutInfo = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "place:folder=TOOLBAR" + }); + + let unfiledRoot = + PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual(shortcutNode.itemId, + yield PlacesUtils.promiseItemId(shortcutInfo.guid)); + Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId, + PlacesUtils.toolbarFolderId); + Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid); + Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).targetFolderGuid, + PlacesUtils.bookmarks.toolbarGuid); + + // test that a node added incrementally also behaves just as well. + shortcutInfo = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "place:folder=BOOKMARKS_MENU" + }); + shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual(shortcutNode.itemId, + yield PlacesUtils.promiseItemId(shortcutInfo.guid)); + Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId, + PlacesUtils.bookmarksMenuFolderId); + Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid); + Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).targetFolderGuid, + PlacesUtils.bookmarks.menuGuid); + + unfiledRoot.containerOpen = false; +}); + +add_task(function* test_plain_folder() { + let folderInfo = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }); + + let unfiledRoot = + PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + let lastChild = unfiledRoot.getChild(unfiledRoot.childCount - 1); + Assert.strictEqual(lastChild.bookmarkGuid, folderInfo.guid); + Assert.strictEqual(PlacesUtils.asQuery(lastChild).targetFolderGuid, + folderInfo.guid); +}); + +add_task(function* test_non_item_query() { + let options = PlacesUtils.history.getNewQueryOptions(); + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), options).root; + Assert.strictEqual(root.itemId, -1); + Assert.strictEqual(PlacesUtils.asQuery(root).folderItemId, -1); + Assert.strictEqual(root.bookmarkGuid, ""); + Assert.strictEqual(PlacesUtils.asQuery(root).targetFolderGuid, ""); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_317472.js b/toolkit/components/places/tests/unit/test_317472.js new file mode 100644 index 000000000..a08651916 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_317472.js @@ -0,0 +1,65 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const charset = "UTF-8"; +const CHARSET_ANNO = "URIProperties/characterSet"; + +const TEST_URI = uri("http://foo.com"); +const TEST_BOOKMARKED_URI = uri("http://bar.com"); + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + // add pages to history + yield PlacesTestUtils.addVisits(TEST_URI); + yield PlacesTestUtils.addVisits(TEST_BOOKMARKED_URI); + + // create bookmarks on TEST_BOOKMARKED_URI + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, + TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, + TEST_BOOKMARKED_URI.spec); + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.toolbarFolderId, + TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, + TEST_BOOKMARKED_URI.spec); + + // set charset on not-bookmarked page + yield PlacesUtils.setCharsetForURI(TEST_URI, charset); + // set charset on bookmarked page + yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, charset); + + // check that we have created a page annotation + do_check_eq(PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO), charset); + + // get charset from not-bookmarked page + do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset); + + // get charset from bookmarked page + do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset); + + yield PlacesTestUtils.clearHistory(); + + // ensure that charset has gone for not-bookmarked page + do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset); + + // check that page annotation has been removed + try { + PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO); + do_throw("Charset page annotation has not been removed correctly"); + } catch (e) {} + + // ensure that charset still exists for bookmarked page + do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset); + + // remove charset from bookmark and check that has gone + yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, ""); + do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset); +}); diff --git a/toolkit/components/places/tests/unit/test_331487.js b/toolkit/components/places/tests/unit/test_331487.js new file mode 100644 index 000000000..55d41aebf --- /dev/null +++ b/toolkit/components/places/tests/unit/test_331487.js @@ -0,0 +1,95 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +// main +function run_test() { + // add a folder + var folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX); + + // add a bookmark to the folder + var b1 = bmsvc.insertBookmark(folder, uri("http://a1.com/"), + bmsvc.DEFAULT_INDEX, "1 title"); + // add a subfolder + var sf1 = bmsvc.createFolder(folder, "subfolder 1", bmsvc.DEFAULT_INDEX); + + // add a bookmark to the subfolder + var b2 = bmsvc.insertBookmark(sf1, uri("http://a2.com/"), + bmsvc.DEFAULT_INDEX, "2 title"); + + // add a subfolder to the subfolder + var sf2 = bmsvc.createFolder(sf1, "subfolder 2", bmsvc.DEFAULT_INDEX); + + // add a bookmark to the subfolder of the subfolder + var b3 = bmsvc.insertBookmark(sf2, uri("http://a3.com/"), + bmsvc.DEFAULT_INDEX, "3 title"); + + // bookmark query that should result in the "hierarchical" result + // because there is one query, one folder, + // no begin time, no end time, no domain, no uri, no search term + // and no max results. See GetSimpleBookmarksQueryFolder() + // for more details. + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + var query = histsvc.getNewQuery(); + query.setFolders([folder], 1); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).itemId, b1); + do_check_eq(root.getChild(1).itemId, sf1); + + // check the contents of the subfolder + var sf1Node = root.getChild(1); + sf1Node = sf1Node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + sf1Node.containerOpen = true; + do_check_eq(sf1Node.childCount, 2); + do_check_eq(sf1Node.getChild(0).itemId, b2); + do_check_eq(sf1Node.getChild(1).itemId, sf2); + + // check the contents of the subfolder's subfolder + var sf2Node = sf1Node.getChild(1); + sf2Node = sf2Node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + sf2Node.containerOpen = true; + do_check_eq(sf2Node.childCount, 1); + do_check_eq(sf2Node.getChild(0).itemId, b3); + + sf2Node.containerOpen = false; + sf1Node.containerOpen = false; + root.containerOpen = false; + + // bookmark query that should result in a flat list + // because we specified max results + options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.maxResults = 10; + query = histsvc.getNewQuery(); + query.setFolders([folder], 1); + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).itemId, b1); + do_check_eq(root.getChild(1).itemId, b2); + do_check_eq(root.getChild(2).itemId, b3); + root.containerOpen = false; + + // XXX TODO + // test that if we have: more than one query, + // multiple folders, a begin time, an end time, a domain, a uri + // or a search term, that we get the (correct) flat list results + // (like we do when specified maxResults) +} diff --git a/toolkit/components/places/tests/unit/test_384370.js b/toolkit/components/places/tests/unit/test_384370.js new file mode 100644 index 000000000..ec6f43683 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_384370.js @@ -0,0 +1,173 @@ +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; +const DESCRIPTION_ANNO = "bookmarkProperties/description"; + +var tagData = [ + { uri: uri("http://slint.us"), tags: ["indie", "kentucky", "music"] }, + { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), tags: ["dinosaur", "dj", "rad word"] } +]; + +var bookmarkData = [ + { uri: uri("http://slint.us"), title: "indie, kentucky, music" }, + { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), title: "dinosaur, dj, rad word" } +]; + +function run_test() { + run_next_test(); +} + +/* + HTML+FEATURES SUMMARY: + - import legacy bookmarks + - export as json, import, test (tests integrity of html > json) + - export as html, import, test (tests integrity of json > html) + + BACKUP/RESTORE SUMMARY: + - create a bookmark in each root + - tag multiple URIs with multiple tags + - export as json, import, test +*/ +add_task(function* () { + // Remove eventual bookmarks.exported.json. + let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.json"); + if ((yield OS.File.exists(jsonFile))) + yield OS.File.remove(jsonFile); + + // Test importing a pre-Places canonical bookmarks file. + // Note: we do not empty the db before this import to catch bugs like 380999 + let htmlFile = OS.Path.join(do_get_cwd().path, "bookmarks.preplaces.html"); + yield BookmarkHTMLUtils.importFromFile(htmlFile, true); + + // Populate the database. + for (let { uri, tags } of tagData) { + PlacesUtils.tagging.tagURI(uri, tags); + } + for (let { uri, title } of bookmarkData) { + yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title }); + } + for (let { uri, title } of bookmarkData) { + yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: uri, + title }); + } + + yield validate(); + + // Test exporting a Places canonical json file. + // 1. export to bookmarks.exported.json + yield BookmarkJSONUtils.exportToFile(jsonFile); + do_print("exported json"); + + // 2. empty bookmarks db + // 3. import bookmarks.exported.json + yield BookmarkJSONUtils.importFromFile(jsonFile, true); + do_print("imported json"); + + // 4. run the test-suite + yield validate(); + do_print("validated import"); +}); + +function* validate() { + yield testMenuBookmarks(); + yield testToolbarBookmarks(); + testUnfiledBookmarks(); + testTags(); + yield PlacesTestUtils.promiseAsyncUpdates(); +} + +// Tests a bookmarks datastore that has a set of bookmarks, etc +// that flex each supported field and feature. +function* testMenuBookmarks() { + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root; + Assert.equal(root.childCount, 3); + + let separatorNode = root.getChild(1); + Assert.equal(separatorNode.type, separatorNode.RESULT_TYPE_SEPARATOR); + + let folderNode = root.getChild(2); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, "test"); + let folder = yield PlacesUtils.bookmarks.fetch(folderNode.bookmarkGuid); + Assert.equal(folder.dateAdded.getTime(), 1177541020000); + + Assert.equal(PlacesUtils.asQuery(folderNode).hasChildren, true); + + Assert.equal("folder test comment", + PlacesUtils.annotations.getItemAnnotation(folderNode.itemId, + DESCRIPTION_ANNO)); + + // open test folder, and test the children + folderNode.containerOpen = true; + Assert.equal(folderNode.childCount, 1); + + let bookmarkNode = folderNode.getChild(0); + Assert.equal("http://test/post", bookmarkNode.uri); + Assert.equal("test post keyword", bookmarkNode.title); + Assert.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId, + LOAD_IN_SIDEBAR_ANNO)); + Assert.equal(bookmarkNode.dateAdded, 1177375336000000); + + let entry = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri }); + Assert.equal("test", entry.keyword); + Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData); + + Assert.equal("ISO-8859-1", + (yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri)))); + Assert.equal("item description", + PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId, + DESCRIPTION_ANNO)); + + folderNode.containerOpen = false; + root.containerOpen = false; +} + +function* testToolbarBookmarks() { + let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + + // child count (add 2 for pre-existing items) + Assert.equal(root.childCount, bookmarkData.length + 2); + + let livemarkNode = root.getChild(1); + Assert.equal("Latest Headlines", livemarkNode.title); + + let livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemarkNode.itemId }); + Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + livemark.siteURI.spec); + Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + livemark.feedURI.spec); + + // test added bookmark data + let bookmarkNode = root.getChild(2); + Assert.equal(bookmarkNode.uri, bookmarkData[0].uri.spec); + Assert.equal(bookmarkNode.title, bookmarkData[0].title); + bookmarkNode = root.getChild(3); + Assert.equal(bookmarkNode.uri, bookmarkData[1].uri.spec); + Assert.equal(bookmarkNode.title, bookmarkData[1].title); + + root.containerOpen = false; +} + +function testUnfiledBookmarks() { + let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + // child count (add 1 for pre-existing item) + Assert.equal(root.childCount, bookmarkData.length + 1); + for (let i = 1; i < root.childCount; ++i) { + let child = root.getChild(i); + Assert.equal(child.uri, bookmarkData[i - 1].uri.spec); + Assert.equal(child.title, bookmarkData[i - 1].title); + if (child.tags) + Assert.equal(child.tags, bookmarkData[i - 1].title); + } + root.containerOpen = false; +} + +function testTags() { + for (let { uri, tags } of tagData) { + do_print("Test tags for " + uri.spec + ": " + tags + "\n"); + let foundTags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(foundTags.length, tags.length); + Assert.ok(tags.every(tag => foundTags.includes(tag))); + } +} diff --git a/toolkit/components/places/tests/unit/test_385397.js b/toolkit/components/places/tests/unit/test_385397.js new file mode 100644 index 000000000..4b60d4768 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_385397.js @@ -0,0 +1,142 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TOTAL_SITES = 20; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + let now = (Date.now() - 10000) * 1000; + + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test-" + i + ".com/"; + let testURI = uri(site); + let testImageURI = uri(site + "blank.gif"); + let when = now + (i * TOTAL_SITES * 1000); + yield PlacesTestUtils.addVisits([ + { uri: testURI, visitDate: when, transition: TRANSITION_TYPED }, + { uri: testImageURI, visitDate: when + 1000, transition: TRANSITION_EMBED }, + { uri: testImageURI, visitDate: when + 2000, transition: TRANSITION_FRAMED_LINK }, + { uri: testURI, visitDate: when + 3000, transition: TRANSITION_LINK }, + ]); + } + + // verify our visits AS_VISIT, ordered by date descending + // including hidden + // we should get 80 visits: + // http://www.test-19.com/ + // http://www.test-19.com/blank.gif + // http://www.test-19.com/ + // http://www.test-19.com/ + // ... + // http://www.test-0.com/ + // http://www.test-0.com/blank.gif + // http://www.test-0.com/ + // http://www.test-0.com/ + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + options.includeHidden = true; + let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + let cc = root.childCount; + // Embed visits are not added to the database, thus they won't appear. + do_check_eq(cc, 3 * TOTAL_SITES); + for (let i = 0; i < TOTAL_SITES; i++) { + let index = i * 3; + let node = root.getChild(index); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + do_check_eq(node.uri, site + "blank.gif"); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // verify our visits AS_VISIT, ordered by date descending + // we should get 40 visits: + // http://www.test-19.com/ + // http://www.test-19.com/ + // ... + // http://www.test-0.com/ + // http://www.test-0.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + cc = root.childCount; + // 2 * TOTAL_SITES because we count the TYPED and LINK, but not EMBED or FRAMED + do_check_eq(cc, 2 * TOTAL_SITES); + for (let i=0; i < TOTAL_SITES; i++) { + let index = i * 2; + let node = root.getChild(index); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + node = root.getChild(++index); + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test our optimized query for the places menu + // place:type=0&sort=4&maxResults=10 + // verify our visits AS_URI, ordered by date descending + // we should get 10 visits: + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.maxResults = 10; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + cc = root.childCount; + do_check_eq(cc, options.maxResults); + for (let i=0; i < cc; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test without a maxResults, which executes a different query + // but the first 10 results should be the same. + // verify our visits AS_URI, ordered by date descending + // we should get 20 visits, but the first 10 should be + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + cc = root.childCount; + do_check_eq(cc, TOTAL_SITES); + for (let i=0; i < 10; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_399264_query_to_string.js b/toolkit/components/places/tests/unit/test_399264_query_to_string.js new file mode 100644 index 000000000..6e6cc279c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_399264_query_to_string.js @@ -0,0 +1,51 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Obtains the id of the folder obtained from the query. + * + * @param aFolderID + * The id of the folder we want to generate a query for. + * @returns the string representation of the query for the given folder. + */ +function query_string(aFolderID) +{ + var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + + var query = hs.getNewQuery(); + query.setFolders([aFolderID], 1); + var options = hs.getNewQueryOptions(); + return hs.queriesToQueryString([query], 1, options); +} + +function run_test() +{ + var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + + const QUERIES = [ + "folder=PLACES_ROOT" + , "folder=BOOKMARKS_MENU" + , "folder=TAGS" + , "folder=UNFILED_BOOKMARKS" + , "folder=TOOLBAR" + ]; + const FOLDER_IDS = [ + bs.placesRoot + , bs.bookmarksMenuFolder + , bs.tagsFolder + , bs.unfiledBookmarksFolder + , bs.toolbarFolder + ]; + + + for (var i = 0; i < QUERIES.length; i++) { + var result = query_string(FOLDER_IDS[i]); + dump("Looking for '" + QUERIES[i] + "' in '" + result + "'\n"); + do_check_neq(-1, result.indexOf(QUERIES[i])); + } +} diff --git a/toolkit/components/places/tests/unit/test_399264_string_to_query.js b/toolkit/components/places/tests/unit/test_399264_string_to_query.js new file mode 100644 index 000000000..bd29316d9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_399264_string_to_query.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Obtains the id of the folder obtained from the query. + * + * @param aQuery + * The query to obtain the folder id from. + * @returns the folder id of the folder of the root node of the query. + */ +function folder_id(aQuery) +{ + var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + + dump("Checking query '" + aQuery + "'\n"); + var options = { }; + var queries = { }; + var size = { }; + hs.queryStringToQueries(aQuery, queries, size, options); + var result = hs.executeQueries(queries.value, size.value, options.value); + var root = result.root; + root.containerOpen = true; + do_check_true(root.hasChildren); + var folderID = root.getChild(0).parent.itemId; + root.containerOpen = false; + return folderID; +} + +function run_test() +{ + var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + + const QUERIES = [ + "place:folder=PLACES_ROOT" + , "place:folder=BOOKMARKS_MENU" + , "place:folder=TAGS" + , "place:folder=UNFILED_BOOKMARKS" + , "place:folder=TOOLBAR" + ]; + const FOLDER_IDS = [ + bs.placesRoot + , bs.bookmarksMenuFolder + , bs.tagsFolder + , bs.unfiledBookmarksFolder + , bs.toolbarFolder + ]; + + // add something in the bookmarks menu folder so a query to it returns results + bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://example.com/bmf/"), + Ci.nsINavBookmarksService.DEFAULT_INDEX, "bmf"); + + // add something to the tags folder + var ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + ts.tagURI(uri("http://www.example.com/"), ["tag"]); + + // add something to the unfiled bookmarks folder + bs.insertBookmark(bs.unfiledBookmarksFolder, uri("http://example.com/ubf/"), + Ci.nsINavBookmarksService.DEFAULT_INDEX, "ubf"); + + // add something to the toolbar folder + bs.insertBookmark(bs.toolbarFolder, uri("http://example.com/tf/"), + Ci.nsINavBookmarksService.DEFAULT_INDEX, "tf"); + + for (var i = 0; i < QUERIES.length; i++) { + var result = folder_id(QUERIES[i]); + dump("expected " + FOLDER_IDS[i] + ", got " + result + "\n"); + do_check_eq(FOLDER_IDS[i], result); + } +} diff --git a/toolkit/components/places/tests/unit/test_399266.js b/toolkit/components/places/tests/unit/test_399266.js new file mode 100644 index 000000000..296d69414 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_399266.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TOTAL_SITES = 20; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + let places = []; + for (let i = 0; i < TOTAL_SITES; i++) { + for (let j = 0; j <= i; j++) { + places.push({ uri: uri("http://www.test-" + i + ".com/"), + transition: TRANSITION_TYPED }); + // because these are embedded visits, they should not show up on our + // query results. If they do, we have a problem. + places.push({ uri: uri("http://www.hidden.com/hidden.gif"), + transition: TRANSITION_EMBED }); + places.push({ uri: uri("http://www.alsohidden.com/hidden.gif"), + transition: TRANSITION_FRAMED_LINK }); + } + } + yield PlacesTestUtils.addVisits(places); + + // test our optimized query for the "Most Visited" item + // in the "Smart Bookmarks" folder + // place:queryType=0&sort=8&maxResults=10 + // verify our visits AS_URI, ordered by visit count descending + // we should get 10 visits: + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.maxResults = 10; + options.resultType = options.RESULTS_AS_URI; + let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + let cc = root.childCount; + do_check_eq(cc, options.maxResults); + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; + + // test without a maxResults, which executes a different query + // but the first 10 results should be the same. + // verify our visits AS_URI, ordered by visit count descending + // we should get 20 visits, but the first 10 should be + // http://www.test-19.com/ + // ... + // http://www.test-10.com/ + options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.resultType = options.RESULTS_AS_URI; + root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + cc = root.childCount; + do_check_eq(cc, TOTAL_SITES); + for (let i = 0; i < 10; i++) { + let node = root.getChild(i); + let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/"; + do_check_eq(node.uri, site); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_402799.js b/toolkit/components/places/tests/unit/test_402799.js new file mode 100644 index 000000000..263e20aa5 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_402799.js @@ -0,0 +1,62 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history services +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory); +} catch (ex) { + do_throw("Could not get history services\n"); +} + +// Get bookmark service +try { + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +} +catch (ex) { + do_throw("Could not get the nav-bookmarks-service\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + + +// main +function run_test() { + var uri1 = uri("http://foo.bar/"); + + // create 2 bookmarks on the same uri + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1, + bmsvc.DEFAULT_INDEX, "title 1"); + bmsvc.insertBookmark(bmsvc.toolbarFolder, uri1, + bmsvc.DEFAULT_INDEX, "title 2"); + // add some tags + tagssvc.tagURI(uri1, ["foo", "bar", "foobar", "foo bar"]); + + // check that a generic bookmark query returns only real bookmarks + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + var query = histsvc.getNewQuery(); + var result = histsvc.executeQuery(query, options); + var root = result.root; + + root.containerOpen = true; + var cc = root.childCount; + do_check_eq(cc, 2); + var node1 = root.getChild(0); + do_check_eq(bmsvc.getFolderIdForItem(node1.itemId), bmsvc.bookmarksMenuFolder); + var node2 = root.getChild(1); + do_check_eq(bmsvc.getFolderIdForItem(node2.itemId), bmsvc.toolbarFolder); + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_405497.js b/toolkit/components/places/tests/unit/test_405497.js new file mode 100644 index 000000000..951302b84 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_405497.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +/** + * The callback object for runInBatchMode. + * + * @param aService + * Takes a reference to the history service or the bookmark service. + * This determines which service should be called when calling the second + * runInBatchMode the second time. + */ +function callback(aService) +{ + this.callCount = 0; + this.service = aService; +} +callback.prototype = { + // nsINavHistoryBatchCallback + + runBatched: function(aUserData) + { + this.callCount++; + + if (this.callCount == 1) { + // We want to call run in batched once more. + this.service.runInBatchMode(this, null); + return; + } + + do_check_eq(this.callCount, 2); + do_test_finished(); + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryBatchCallback]) +}; + +function run_test() { + // checking the history service + do_test_pending(); + hs.runInBatchMode(new callback(hs), null); + + // checking the bookmark service + do_test_pending(); + bs.runInBatchMode(new callback(bs), null); +} diff --git a/toolkit/components/places/tests/unit/test_408221.js b/toolkit/components/places/tests/unit/test_408221.js new file mode 100644 index 000000000..2b41ce1a2 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_408221.js @@ -0,0 +1,165 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var current_test = 0; + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +function ensure_tag_results(uris, searchTerm) +{ + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + // Search is asynchronous, so don't let the test finish immediately + do_test_pending(); + + var numSearchesStarted = 0; + input.onSearchBegin = function() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + input.onSearchComplete = function() { + do_check_eq(numSearchesStarted, 1); + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + do_check_eq(controller.matchCount, uris.length); + let vals = []; + for (let i=0; i<controller.matchCount; i++) { + // Keep the URL for later because order of tag results is undefined + vals.push(controller.getValueAt(i)); + do_check_eq(controller.getStyleAt(i), "bookmark-tag"); + } + // Sort the results then check if we have the right items + vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec)) + + if (current_test < (tests.length - 1)) { + current_test++; + tests[current_test](); + } + + do_test_finished(); + }; + + controller.startSearch(searchTerm); +} + +var uri1 = uri("http://site.tld/1"); +var uri2 = uri("http://site.tld/2"); +var uri3 = uri("http://site.tld/3"); +var uri4 = uri("http://site.tld/4"); +var uri5 = uri("http://site.tld/5"); +var uri6 = uri("http://site.tld/6"); + +var tests = [function() { ensure_tag_results([uri1, uri2, uri3], "foo"); }, + function() { ensure_tag_results([uri1, uri2, uri3], "Foo"); }, + function() { ensure_tag_results([uri1, uri2, uri3], "foO"); }, + function() { ensure_tag_results([uri4, uri5, uri6], "bar mud"); }, + function() { ensure_tag_results([uri4, uri5, uri6], "BAR MUD"); }, + function() { ensure_tag_results([uri4, uri5, uri6], "Bar Mud"); }]; + +/** + * Properly tags a uri adding it to bookmarks. + * + * @param aURI + * The nsIURI to tag. + * @param aTags + * The tags to add. + */ +function tagURI(aURI, aTags) { + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + aURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A title"); + tagssvc.tagURI(aURI, aTags); +} + +/** + * Test bug #408221 + */ +function run_test() { + // always search in history + bookmarks, no matter what the default is + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.setIntPref("browser.urlbar.search.sources", 3); + prefs.setIntPref("browser.urlbar.default.behavior", 0); + + tagURI(uri1, ["Foo"]); + tagURI(uri2, ["FOO"]); + tagURI(uri3, ["foO"]); + tagURI(uri4, ["BAR"]); + tagURI(uri4, ["MUD"]); + tagURI(uri5, ["bar"]); + tagURI(uri5, ["mud"]); + tagURI(uri6, ["baR"]); + tagURI(uri6, ["muD"]); + + tests[0](); +} diff --git a/toolkit/components/places/tests/unit/test_412132.js b/toolkit/components/places/tests/unit/test_412132.js new file mode 100644 index 000000000..827391f18 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_412132.js @@ -0,0 +1,136 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * Tests patch to Bug 412132: + * https://bugzilla.mozilla.org/show_bug.cgi?id=412132 + */ + +add_task(function* changeuri_unvisited_bookmark() +{ + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should be zero if original URI is unvisited and " + + "no longer bookmarked."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2")); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Unvisited URI no longer bookmarked => frecency should = 0"); + do_check_eq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* changeuri_visited_bookmark() +{ + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is visited."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesTestUtils.addVisits(TEST_URI); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2")); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* changeuri_bookmark_still_bookmarked() +{ + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is still " + + "bookmarked."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark 1 title"); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark 2 title"); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + PlacesUtils.bookmarks.changeBookmarkURI(id1, uri("http://example.com/2")); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("URI still bookmarked => frecency should != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* changeuri_nonexistent_bookmark() +{ + do_print("Changing the URI of a nonexistent bookmark should fail."); + function tryChange(itemId) + { + try { + PlacesUtils.bookmarks.changeBookmarkURI(itemId + 1, uri("http://example.com/2")); + do_throw("Nonexistent bookmark should throw."); + } + catch (ex) {} + } + + // First try a straight-up bogus item ID, one greater than the current max + // ID. + let stmt = DBConn().createStatement("SELECT MAX(id) FROM moz_bookmarks"); + stmt.executeStep(); + let maxId = stmt.getInt32(0); + stmt.finalize(); + tryChange(maxId + 1); + + // Now add a bookmark, delete it, and check. + let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("http://example.com/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + PlacesUtils.bookmarks.removeItem(id); + tryChange(id); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +function run_test() +{ + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_413784.js b/toolkit/components/places/tests/unit/test_413784.js new file mode 100644 index 000000000..6df4dfbbb --- /dev/null +++ b/toolkit/components/places/tests/unit/test_413784.js @@ -0,0 +1,118 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + +Test autocomplete for non-English URLs + +- add a visit for a page with a non-English URL +- search +- test number of matches (should be exactly one) + +*/ + +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +// create test data +var searchTerm = "ユニコード"; +var decoded = "http://www.foobar.com/" + searchTerm + "/"; +var url = uri(decoded); + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} + +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +function run_test() +{ + do_test_pending(); + PlacesTestUtils.addVisits(url).then(continue_test); +} + +function continue_test() +{ + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + var numSearchesStarted = 0; + input.onSearchBegin = function() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + input.onSearchComplete = function() { + do_check_eq(numSearchesStarted, 1); + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + + // test that we found the entry we added + do_check_eq(controller.matchCount, 1); + + // Make sure the url is the same according to spec, so it can be deleted + do_check_eq(controller.getValueAt(0), url.spec); + + do_test_finished(); + }; + + controller.startSearch(searchTerm); +} diff --git a/toolkit/components/places/tests/unit/test_415460.js b/toolkit/components/places/tests/unit/test_415460.js new file mode 100644 index 000000000..f2e049f09 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_415460.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +/** + * Checks to see that a search has exactly one result in the database. + * + * @param aTerms + * The terms to search for. + * @returns true if the search returns one result, false otherwise. + */ +function search_has_result(aTerms) +{ + var options = hs.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = hs.getNewQuery(); + query.searchTerms = aTerms; + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return (cc == 1); +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + const SEARCH_TERM = "ユニコード"; + const TEST_URL = "http://example.com/" + SEARCH_TERM + "/"; + yield PlacesTestUtils.addVisits(uri(TEST_URL)); + do_check_true(search_has_result(SEARCH_TERM)); +}); diff --git a/toolkit/components/places/tests/unit/test_415757.js b/toolkit/components/places/tests/unit/test_415757.js new file mode 100644 index 000000000..afd396183 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_415757.js @@ -0,0 +1,102 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Checks to see that a URI is in the database. + * + * @param aURI + * The URI to check. + * @returns true if the URI is in the DB, false otherwise. + */ +function uri_in_db(aURI) { + var options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI + var query = PlacesUtils.history.getNewQuery(); + query.uri = aURI; + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return (cc == 1); +} + +const TOTAL_SITES = 20; + +// main +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + // add pages to global history + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test-" + i + ".com/"; + let testURI = uri(site); + let when = Date.now() * 1000 + (i * TOTAL_SITES); + yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when }); + } + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test.com/" + i + "/"; + let testURI = uri(site); + let when = Date.now() * 1000 + (i * TOTAL_SITES); + yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when }); + } + + // set a page annotation on one of the urls that will be removed + var testAnnoDeletedURI = uri("http://www.test.com/1/"); + var testAnnoDeletedName = "foo"; + var testAnnoDeletedValue = "bar"; + PlacesUtils.annotations.setPageAnnotation(testAnnoDeletedURI, + testAnnoDeletedName, + testAnnoDeletedValue, 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + + // set a page annotation on one of the urls that will NOT be removed + var testAnnoRetainedURI = uri("http://www.test-1.com/"); + var testAnnoRetainedName = "foo"; + var testAnnoRetainedValue = "bar"; + PlacesUtils.annotations.setPageAnnotation(testAnnoRetainedURI, + testAnnoRetainedName, + testAnnoRetainedValue, 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + + // remove pages from www.test.com + PlacesUtils.history.removePagesFromHost("www.test.com", false); + + // check that all pages in www.test.com have been removed + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test.com/" + i + "/"; + let testURI = uri(site); + do_check_false(uri_in_db(testURI)); + } + + // check that all pages in www.test-X.com have NOT been removed + for (let i = 0; i < TOTAL_SITES; i++) { + let site = "http://www.test-" + i + ".com/"; + let testURI = uri(site); + do_check_true(uri_in_db(testURI)); + } + + // check that annotation on the removed item does not exists + try { + PlacesUtils.annotations.getPageAnnotation(testAnnoDeletedURI, testAnnoName); + do_throw("fetching page-annotation that doesn't exist, should've thrown"); + } catch (ex) {} + + // check that annotation on the NOT removed item still exists + try { + var annoVal = PlacesUtils.annotations.getPageAnnotation(testAnnoRetainedURI, + testAnnoRetainedName); + } catch (ex) { + do_throw("The annotation has been removed erroneously"); + } + do_check_eq(annoVal, testAnnoRetainedValue); + +}); diff --git a/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js new file mode 100644 index 000000000..2eed02921 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js @@ -0,0 +1,143 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get services. +try { + var histSvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bmSvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + var annoSvc = Cc["@mozilla.org/browser/annotation-service;1"] + .getService(Ci.nsIAnnotationService); +} catch (ex) { + do_throw("Could not get services\n"); +} + +var validAnnoName = "validAnno"; +var validItemName = "validItem"; +var deletedAnnoName = "deletedAnno"; +var deletedItemName = "deletedItem"; +var bookmarkedURI = uri("http://www.mozilla.org/"); +// set lastModified to the past to prevent VM timing bugs +var pastDate = Date.now() * 1000 - 1; +var deletedBookmarkIds = []; + +// bookmarks observer +var observer = { + // cached ordered array of notified items + _onItemRemovedItemIds: [], + onItemRemoved: function(aItemId, aParentId, aIndex) { + // We should first get notifications for children, then for their parent + do_check_eq(this._onItemRemovedItemIds.indexOf(aParentId), -1); + // Ensure we are not wrongly removing 1 level up + do_check_neq(aParentId, bmSvc.toolbarFolder); + // Removed item must be one of those we have manually deleted + do_check_neq(deletedBookmarkIds.indexOf(aItemId), -1); + this._onItemRemovedItemIds.push(aItemId); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsINavBookmarkObserver) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +bmSvc.addObserver(observer, false); + +function add_bookmarks() { + // This is the folder we will cleanup + var validFolderId = bmSvc.createFolder(bmSvc.toolbarFolder, + validItemName, + bmSvc.DEFAULT_INDEX); + annoSvc.setItemAnnotation(validFolderId, validAnnoName, + "annotation", 0, + annoSvc.EXPIRE_NEVER); + bmSvc.setItemLastModified(validFolderId, pastDate); + + // This bookmark should not be deleted + var validItemId = bmSvc.insertBookmark(bmSvc.toolbarFolder, + bookmarkedURI, + bmSvc.DEFAULT_INDEX, + validItemName); + annoSvc.setItemAnnotation(validItemId, validAnnoName, + "annotation", 0, annoSvc.EXPIRE_NEVER); + + // The following contents should be deleted + var deletedItemId = bmSvc.insertBookmark(validFolderId, + bookmarkedURI, + bmSvc.DEFAULT_INDEX, + deletedItemName); + annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName, + "annotation", 0, annoSvc.EXPIRE_NEVER); + deletedBookmarkIds.push(deletedItemId); + + var internalFolderId = bmSvc.createFolder(validFolderId, + deletedItemName, + bmSvc.DEFAULT_INDEX); + annoSvc.setItemAnnotation(internalFolderId, deletedAnnoName, + "annotation", 0, annoSvc.EXPIRE_NEVER); + deletedBookmarkIds.push(internalFolderId); + + deletedItemId = bmSvc.insertBookmark(internalFolderId, + bookmarkedURI, + bmSvc.DEFAULT_INDEX, + deletedItemName); + annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName, + "annotation", 0, annoSvc.EXPIRE_NEVER); + deletedBookmarkIds.push(deletedItemId); + + return validFolderId; +} + +function check_bookmarks(aFolderId) { + // check that we still have valid bookmarks + var bookmarks = bmSvc.getBookmarkIdsForURI(bookmarkedURI); + for (var i = 0; i < bookmarks.length; i++) { + do_check_eq(bmSvc.getItemTitle(bookmarks[i]), validItemName); + do_check_true(annoSvc.itemHasAnnotation(bookmarks[i], validAnnoName)); + } + + // check that folder exists and has still its annotation + do_check_eq(bmSvc.getItemTitle(aFolderId), validItemName); + do_check_true(annoSvc.itemHasAnnotation(aFolderId, validAnnoName)); + + // check that folder is empty + var options = histSvc.getNewQueryOptions(); + var query = histSvc.getNewQuery(); + query.setFolders([aFolderId], 1); + var result = histSvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 0); + root.containerOpen = false; + + // test that lastModified got updated + do_check_true(pastDate < bmSvc.getItemLastModified(aFolderId)); + + // test that all children have been deleted, we use annos for that + var deletedItems = annoSvc.getItemsWithAnnotation(deletedAnnoName); + do_check_eq(deletedItems.length, 0); + + // test that observer has been called for (and only for) deleted items + do_check_eq(observer._onItemRemovedItemIds.length, deletedBookmarkIds.length); + + // Sanity check: all roots should be intact + do_check_eq(bmSvc.getFolderIdForItem(bmSvc.placesRoot), 0); + do_check_eq(bmSvc.getFolderIdForItem(bmSvc.bookmarksMenuFolder), bmSvc.placesRoot); + do_check_eq(bmSvc.getFolderIdForItem(bmSvc.tagsFolder), bmSvc.placesRoot); + do_check_eq(bmSvc.getFolderIdForItem(bmSvc.unfiledBookmarksFolder), bmSvc.placesRoot); + do_check_eq(bmSvc.getFolderIdForItem(bmSvc.toolbarFolder), bmSvc.placesRoot); +} + +// main +function run_test() { + var folderId = add_bookmarks(); + bmSvc.removeFolderChildren(folderId); + check_bookmarks(folderId); +} diff --git a/toolkit/components/places/tests/unit/test_419731.js b/toolkit/components/places/tests/unit/test_419731.js new file mode 100644 index 000000000..b1a434e12 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_419731.js @@ -0,0 +1,96 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + let uri1 = NetUtil.newURI("http://foo.bar/"); + + // create 2 bookmarks + let bookmark1id = PlacesUtils.bookmarks + .insertBookmark(PlacesUtils.bookmarksMenuFolderId, + uri1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "title 1"); + let bookmark2id = PlacesUtils.bookmarks + .insertBookmark(PlacesUtils.toolbarFolderId, + uri1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "title 2"); + // add a new tag + PlacesUtils.tagging.tagURI(uri1, ["foo"]); + + // get tag folder id + let options = PlacesUtils.history.getNewQueryOptions(); + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.tagsFolderId], 1); + let result = PlacesUtils.history.executeQuery(query, options); + let tagRoot = result.root; + tagRoot.containerOpen = true; + let tagNode = tagRoot.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + let tagItemId = tagNode.itemId; + tagRoot.containerOpen = false; + + // change bookmark 1 title + PlacesUtils.bookmarks.setItemTitle(bookmark1id, "new title 1"); + + // Workaround timers resolution and time skews. + let bookmark2LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark2id); + PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1000); + + // Query the tag. + options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.resultType = options.RESULTS_AS_TAG_QUERY; + + query = PlacesUtils.history.getNewQuery(); + result = PlacesUtils.history.executeQuery(query, options); + let root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + + let theTag = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + // Bug 524219: Check that renaming the tag shows up in the result. + do_check_eq(theTag.title, "foo") + PlacesUtils.bookmarks.setItemTitle(tagItemId, "bar"); + + // Check that the item has been replaced + do_check_neq(theTag, root.getChild(0)); + theTag = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(theTag.title, "bar"); + + // Check that tag container contains new title + theTag.containerOpen = true; + do_check_eq(theTag.childCount, 1); + let node = theTag.getChild(0); + do_check_eq(node.title, "new title 1"); + theTag.containerOpen = false; + root.containerOpen = false; + + // Change bookmark 2 title. + PlacesUtils.bookmarks.setItemTitle(bookmark2id, "new title 2"); + + // Workaround timers resolution and time skews. + let bookmark1LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark1id); + PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1000); + + // Check that tag container contains new title + options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.resultType = options.RESULTS_AS_TAG_CONTENTS; + + query = PlacesUtils.history.getNewQuery(); + query.setFolders([tagItemId], 1); + result = PlacesUtils.history.executeQuery(query, options); + root = result.root; + + root.containerOpen = true; + do_check_eq(root.childCount, 1); + node = root.getChild(0); + do_check_eq(node.title, "new title 2"); + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_419792_node_tags_property.js b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js new file mode 100644 index 000000000..4c726d667 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js @@ -0,0 +1,49 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// get services +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + +function run_test() { + // get toolbar node + var options = histsvc.getNewQueryOptions(); + var query = histsvc.getNewQuery(); + query.setFolders([bmsvc.toolbarFolder], 1); + var result = histsvc.executeQuery(query, options); + var toolbarNode = result.root; + toolbarNode.containerOpen = true; + + // add a bookmark + var bookmarkURI = uri("http://foo.com"); + var bookmarkId = bmsvc.insertBookmark(bmsvc.toolbarFolder, bookmarkURI, + bmsvc.DEFAULT_INDEX, ""); + + // get the node for the new bookmark + var node = toolbarNode.getChild(toolbarNode.childCount-1); + do_check_eq(node.itemId, bookmarkId); + + // confirm there's no tags via the .tags property + do_check_eq(node.tags, null); + + // add a tag + tagssvc.tagURI(bookmarkURI, ["foo"]); + do_check_eq(node.tags, "foo"); + + // add another tag, to test delimiter and sorting + tagssvc.tagURI(bookmarkURI, ["bar"]); + do_check_eq(node.tags, "bar, foo"); + + // remove the tags, confirming the property is cleared + tagssvc.untagURI(bookmarkURI, null); + do_check_eq(node.tags, null); + + toolbarNode.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_425563.js b/toolkit/components/places/tests/unit/test_425563.js new file mode 100644 index 000000000..bee3a4a54 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_425563.js @@ -0,0 +1,74 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + let count_visited_URIs = ["http://www.test-link.com/", + "http://www.test-typed.com/", + "http://www.test-bookmark.com/", + "http://www.test-redirect-permanent.com/", + "http://www.test-redirect-temporary.com/"]; + + let notcount_visited_URIs = ["http://www.test-embed.com/", + "http://www.test-download.com/", + "http://www.test-framed.com/", + "http://www.test-reload.com/"]; + + // add visits, one for each transition type + yield PlacesTestUtils.addVisits([ + { uri: uri("http://www.test-link.com/"), + transition: TRANSITION_LINK }, + { uri: uri("http://www.test-typed.com/"), + transition: TRANSITION_TYPED }, + { uri: uri("http://www.test-bookmark.com/"), + transition: TRANSITION_BOOKMARK }, + { uri: uri("http://www.test-embed.com/"), + transition: TRANSITION_EMBED }, + { uri: uri("http://www.test-framed.com/"), + transition: TRANSITION_FRAMED_LINK }, + { uri: uri("http://www.test-redirect-permanent.com/"), + transition: TRANSITION_REDIRECT_PERMANENT }, + { uri: uri("http://www.test-redirect-temporary.com/"), + transition: TRANSITION_REDIRECT_TEMPORARY }, + { uri: uri("http://www.test-download.com/"), + transition: TRANSITION_DOWNLOAD }, + { uri: uri("http://www.test-reload.com/"), + transition: TRANSITION_RELOAD }, + ]); + + // check that all links are marked as visited + for (let visited_uri of count_visited_URIs) { + do_check_true(yield promiseIsURIVisited(uri(visited_uri))); + } + for (let visited_uri of notcount_visited_URIs) { + do_check_true(yield promiseIsURIVisited(uri(visited_uri))); + } + + // check that visit_count does not take in count embed and downloads + // maxVisits query are directly binded to visit_count + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + options.includeHidden = true; + let query = PlacesUtils.history.getNewQuery(); + query.minVisits = 1; + let root = PlacesUtils.history.executeQuery(query, options).root; + + root.containerOpen = true; + let cc = root.childCount; + do_check_eq(cc, count_visited_URIs.length); + + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + do_check_neq(count_visited_URIs.indexOf(node.uri), -1); + } + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js new file mode 100644 index 000000000..e0b6be64c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js @@ -0,0 +1,35 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + +- add a folder +- add a folder-shortcut to the new folder +- query for the shortcut +- remove the folder-shortcut +- confirm the shortcut is removed from the query results + +*/ + +function run_test() { + const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX; + var folderId = + PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX); + + var queryId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + uri("place:folder=" + folderId), IDX, ""); + + var root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId, false, true).root; + + var oldCount = root.childCount; + + PlacesUtils.bookmarks.removeItem(queryId); + + do_check_eq(root.childCount, oldCount-1); + + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_433317_query_title_update.js b/toolkit/components/places/tests/unit/test_433317_query_title_update.js new file mode 100644 index 000000000..52558e844 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_433317_query_title_update.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + } catch (ex) { + do_throw("Unable to initialize Places services"); + } + + // create a query bookmark + var queryId = bmsvc.insertBookmark(bmsvc.toolbarFolder, uri("place:"), + 0 /* first item */, "test query"); + + // query for that query + var options = histsvc.getNewQueryOptions(); + var query = histsvc.getNewQuery(); + query.setFolders([bmsvc.toolbarFolder], 1); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var queryNode = root.getChild(0); + do_check_eq(queryNode.title, "test query"); + + // change the title + bmsvc.setItemTitle(queryId, "foo"); + + // confirm the node was updated + do_check_eq(queryNode.title, "foo"); + + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js new file mode 100644 index 000000000..92dac0b17 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + } catch (ex) { + do_throw("Unable to initialize Places services"); + } + + // add a visit + var testURI = uri("http://test"); + yield PlacesTestUtils.addVisits(testURI); + + // query for the visit + var options = histsvc.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI; + var query = histsvc.getNewQuery(); + query.uri = testURI; + var result = histsvc.executeQuery(query, options); + var root = result.root; + + // check hasChildren while the container is closed + do_check_eq(root.hasChildren, true); + + // now check via the saved search path + var queryURI = histsvc.queriesToQueryString([query], 1, options); + bmsvc.insertBookmark(bmsvc.toolbarFolder, uri(queryURI), + 0 /* first item */, "test query"); + + // query for that query + options = histsvc.getNewQueryOptions(); + query = histsvc.getNewQuery(); + query.setFolders([bmsvc.toolbarFolder], 1); + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + var queryNode = root.getChild(0); + do_check_eq(queryNode.title, "test query"); + queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(queryNode.hasChildren, true); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_452777.js b/toolkit/components/places/tests/unit/test_452777.js new file mode 100644 index 000000000..97b2852f6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_452777.js @@ -0,0 +1,36 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test ensures that when removing a folder within a transaction, undoing + * the transaction restores it with the same id (as received by the observers). + */ + +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +function run_test() +{ + const TITLE = "test folder"; + + // Create two test folders; remove the first one. This ensures that undoing + // the removal will not get the same id by chance (the insert id's can be + // reused in SQLite). + let id = bs.createFolder(bs.placesRoot, TITLE, -1); + bs.createFolder(bs.placesRoot, "test folder 2", -1); + let transaction = bs.getRemoveFolderTransaction(id); + transaction.doTransaction(); + + // Now check to make sure it gets added with the right id + bs.addObserver({ + onItemAdded: function(aItemId, aFolder, aIndex, aItemType, aURI, aTitle) + { + do_check_eq(aItemId, id); + do_check_eq(aTitle, TITLE); + } + }, false); + transaction.undoTransaction(); +} diff --git a/toolkit/components/places/tests/unit/test_454977.js b/toolkit/components/places/tests/unit/test_454977.js new file mode 100644 index 000000000..606e83048 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_454977.js @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Cache actual visit_count value, filled by add_visit, used by check_results +var visit_count = 0; + +// Returns the Place ID corresponding to an added visit. +function* task_add_visit(aURI, aVisitType) +{ + // Add the visit asynchronously, and save its visit ID. + let deferUpdatePlaces = new Promise((resolve, reject) => + { + PlacesUtils.asyncHistory.updatePlaces({ + uri: aURI, + visits: [{ transitionType: aVisitType, visitDate: Date.now() * 1000 }] + }, { + handleError: function TAV_handleError() { + reject(new Error("Unexpected error in adding visit.")); + }, + handleResult: function (aPlaceInfo) { + this.visitId = aPlaceInfo.visits[0].visitId; + }, + handleCompletion: function TAV_handleCompletion() { + resolve(this.visitId); + } + }); + }); + + let visitId = yield deferUpdatePlaces; + + // Increase visit_count if applicable + if (aVisitType != 0 && + aVisitType != TRANSITION_EMBED && + aVisitType != TRANSITION_FRAMED_LINK && + aVisitType != TRANSITION_DOWNLOAD && + aVisitType != TRANSITION_RELOAD) { + visit_count ++; + } + + // Get the place id + if (visitId > 0) { + let sql = "SELECT place_id FROM moz_historyvisits WHERE id = ?1"; + let stmt = DBConn().createStatement(sql); + stmt.bindByIndex(0, visitId); + do_check_true(stmt.executeStep()); + let placeId = stmt.getInt64(0); + stmt.finalize(); + do_check_true(placeId > 0); + return placeId; + } + return 0; +} + +/** + * Checks for results consistency, using visit_count as constraint + * @param aExpectedCount + * Number of history results we are expecting (excluded hidden ones) + * @param aExpectedCountWithHidden + * Number of history results we are expecting (included hidden ones) + */ +function check_results(aExpectedCount, aExpectedCountWithHidden) +{ + let query = PlacesUtils.history.getNewQuery(); + // used to check visit_count + query.minVisits = visit_count; + query.maxVisits = visit_count; + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + // Children without hidden ones + do_check_eq(root.childCount, aExpectedCount); + root.containerOpen = false; + + // Execute again with includeHidden = true + // This will ensure visit_count is correct + options.includeHidden = true; + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + // Children with hidden ones + do_check_eq(root.childCount, aExpectedCountWithHidden); + root.containerOpen = false; +} + +// main +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + const TEST_URI = uri("http://test.mozilla.org/"); + + // Add a visit that force hidden + yield task_add_visit(TEST_URI, TRANSITION_EMBED); + check_results(0, 0); + + let placeId = yield task_add_visit(TEST_URI, TRANSITION_FRAMED_LINK); + check_results(0, 1); + + // Add a visit that force unhide and check the place id. + // - We expect that the place gets hidden = 0 while retaining the same + // place id and a correct visit_count. + do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_TYPED)), placeId); + check_results(1, 1); + + // Add a visit that should not increase visit_count + do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_RELOAD)), placeId); + check_results(1, 1); + + // Add a visit that should not increase visit_count + do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_DOWNLOAD)), placeId); + check_results(1, 1); + + // Add a visit, check that hidden is not overwritten + // - We expect that the place has still hidden = 0, while retaining + // correct visit_count. + yield task_add_visit(TEST_URI, TRANSITION_EMBED); + check_results(1, 1); +}); diff --git a/toolkit/components/places/tests/unit/test_463863.js b/toolkit/components/places/tests/unit/test_463863.js new file mode 100644 index 000000000..2f7cece4a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_463863.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * This test checks that in a basic history query all transition types visits + * appear but TRANSITION_EMBED and TRANSITION_FRAMED_LINK ones. + */ + +var transitions = [ + TRANSITION_LINK +, TRANSITION_TYPED +, TRANSITION_BOOKMARK +, TRANSITION_EMBED +, TRANSITION_FRAMED_LINK +, TRANSITION_REDIRECT_PERMANENT +, TRANSITION_REDIRECT_TEMPORARY +, TRANSITION_DOWNLOAD +]; + +function runQuery(aResultType) { + let options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = aResultType; + let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(), + options).root; + root.containerOpen = true; + let cc = root.childCount; + do_check_eq(cc, transitions.length - 2); + + for (let i = 0; i < cc; i++) { + let node = root.getChild(i); + // Check that all transition types but EMBED and FRAMED appear in results + do_check_neq(node.uri.substr(6, 1), TRANSITION_EMBED); + do_check_neq(node.uri.substr(6, 1), TRANSITION_FRAMED_LINK); + } + root.containerOpen = false; +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + // add visits, one for each transition type + for (let transition of transitions) { + yield PlacesTestUtils.addVisits({ + uri: uri("http://" + transition + ".mozilla.org/"), + transition: transition + }); + } + + runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT); + runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_URI); +}); diff --git a/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js new file mode 100644 index 000000000..873174ffd --- /dev/null +++ b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js @@ -0,0 +1,21 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +function run_test() { + var query = hs.getNewQuery(); + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULT_TYPE_QUERY; + var result = hs.executeQuery(query, options); + result.root.containerOpen = true; + var rootNode = result.root; + rootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var queries = rootNode.getQueries(); + do_check_eq(queries[0].uri, null); // Should be null, instead of crashing the browser + rootNode.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js new file mode 100644 index 000000000..05f3f83e7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js @@ -0,0 +1,129 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * TEST DESCRIPTION: + * + * This test checks that setting a sort on a RESULTS_AS_DATE_QUERY query, + * children of inside containers are sorted accordingly. + */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +// Will be inserted in this order, so last one will be the newest visit. +var pages = [ + "http://a.mozilla.org/1/", + "http://a.mozilla.org/2/", + "http://a.mozilla.org/3/", + "http://a.mozilla.org/4/", + "http://b.mozilla.org/5/", + "http://b.mozilla.org/6/", + "http://b.mozilla.org/7/", + "http://b.mozilla.org/8/", +]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_initialize() +{ + var noon = new Date(); + noon.setHours(12); + + // Add visits. + for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) { + let page = pages[pageIndex]; + yield PlacesTestUtils.addVisits({ + uri: uri(page), + visitDate: noon - (pages.length - pageIndex) * 1000 + }); + } +}); + +/** + * Tests that sorting date query by none will sort by title asc. + */ +add_task(function() { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_NONE; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + + var cc = dayContainer.childCount; + do_check_eq(cc, pages.length); + for (var i = 0; i < cc; i++) { + var node = dayContainer.getChild(i); + do_check_eq(pages[i], node.uri); + } + + dayContainer.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Tests that sorting date query by date will sort accordingly. + */ +add_task(function() { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + + var cc = dayContainer.childCount; + do_check_eq(cc, pages.length); + for (var i = 0; i < cc; i++) { + var node = dayContainer.getChild(i); + do_check_eq(pages[pages.length - i - 1], node.uri); + } + + dayContainer.containerOpen = false; + root.containerOpen = false; +}); + +/** + * Tests that sorting date site query by date will still sort by title asc. + */ +add_task(function() { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_SITE_QUERY; + // This should sort by title asc. + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayContainer.containerOpen = true; + var siteContainer = dayContainer.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(siteContainer.title, "a.mozilla.org"); + siteContainer.containerOpen = true; + + var cc = siteContainer.childCount; + do_check_eq(cc, pages.length / 2); + for (var i = 0; i < cc / 2; i++) { + var node = siteContainer.getChild(i); + do_check_eq(pages[i], node.uri); + } + + siteContainer.containerOpen = false; + dayContainer.containerOpen = false; + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_536081.js b/toolkit/components/places/tests/unit/test_536081.js new file mode 100644 index 000000000..b61b91866 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_536081.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bh = hs.QueryInterface(Ci.nsIBrowserHistory); +var db = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; + +const URLS = [ + { u: "http://www.google.com/search?q=testing%3Bthis&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:unofficial&client=firefox-a", + s: "goog" }, +]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + for (let url of URLS) { + yield task_test_url(url); + } +}); + +function* task_test_url(aURL) { + print("Testing url: " + aURL.u); + yield PlacesTestUtils.addVisits(uri(aURL.u)); + let query = hs.getNewQuery(); + query.searchTerms = aURL.s; + let options = hs.getNewQueryOptions(); + let root = hs.executeQuery(query, options).root; + root.containerOpen = true; + let cc = root.childCount; + do_check_eq(cc, 1); + print("Checking url is in the query."); + let node = root.getChild(0); + print("Found " + node.uri); + root.containerOpen = false; + bh.removePage(uri(node.uri)); +} + +function check_empty_table(table_name) { + print("Checking url has been removed."); + let stmt = db.createStatement("SELECT count(*) FROM " + table_name); + try { + stmt.executeStep(); + do_check_eq(stmt.getInt32(0), 0); + } + finally { + stmt.finalize(); + } +} diff --git a/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js new file mode 100644 index 000000000..1280ce3e7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/PlacesSearchAutocompleteProvider.jsm"); + +function run_test() { + // Tell the search service we are running in the US. This also has the + // desired side-effect of preventing our geoip lookup. + Services.prefs.setBoolPref("browser.search.isUS", true); + Services.prefs.setCharPref("browser.search.countryCode", "US"); + Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false); + run_next_test(); +} + +add_task(function* search_engine_match() { + let engine = yield promiseDefaultSearchEngine(); + let token = engine.getResultDomain(); + let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1)); + do_check_eq(match.url, engine.searchForm); + do_check_eq(match.engineName, engine.name); + do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null); +}); + +add_task(function* no_match() { + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("test")); +}); + +add_task(function* hide_search_engine_nomatch() { + let engine = yield promiseDefaultSearchEngine(); + let token = engine.getResultDomain(); + let promiseTopic = promiseSearchTopic("engine-changed"); + Services.search.removeEngine(engine); + yield promiseTopic; + do_check_true(engine.hidden); + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1))); +}); + +add_task(function* add_search_engine_match() { + let promiseTopic = promiseSearchTopic("engine-added"); + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon")); + Services.search.addEngineWithDetails("bacon", "", "pork", "Search Bacon", + "GET", "http://www.bacon.moz/?search={searchTerms}"); + yield promiseTopic; + let match = yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"); + do_check_eq(match.url, "http://www.bacon.moz"); + do_check_eq(match.engineName, "bacon"); + do_check_eq(match.iconUrl, null); +}); + +add_task(function* test_aliased_search_engine_match() { + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByAlias("sober")); + // Lower case + let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pork"); + do_check_eq(match.engineName, "bacon"); + do_check_eq(match.alias, "pork"); + do_check_eq(match.iconUrl, null); + // Upper case + let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PORK"); + do_check_eq(match1.engineName, "bacon"); + do_check_eq(match1.alias, "pork"); + do_check_eq(match1.iconUrl, null); + // Cap case + let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pork"); + do_check_eq(match2.engineName, "bacon"); + do_check_eq(match2.alias, "pork"); + do_check_eq(match2.iconUrl, null); +}); + +add_task(function* test_aliased_search_engine_match_upper_case_alias() { + let promiseTopic = promiseSearchTopic("engine-added"); + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("patch")); + Services.search.addEngineWithDetails("patch", "", "PR", "Search Patch", + "GET", "http://www.patch.moz/?search={searchTerms}"); + yield promiseTopic; + // lower case + let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pr"); + do_check_eq(match.engineName, "patch"); + do_check_eq(match.alias, "PR"); + do_check_eq(match.iconUrl, null); + // Upper case + let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PR"); + do_check_eq(match1.engineName, "patch"); + do_check_eq(match1.alias, "PR"); + do_check_eq(match1.iconUrl, null); + // Cap case + let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pr"); + do_check_eq(match2.engineName, "patch"); + do_check_eq(match2.alias, "PR"); + do_check_eq(match2.iconUrl, null); +}); + +add_task(function* remove_search_engine_nomatch() { + let engine = Services.search.getEngineByName("bacon"); + let promiseTopic = promiseSearchTopic("engine-removed"); + Services.search.removeEngine(engine); + yield promiseTopic; + do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon")); +}); + +add_task(function* test_parseSubmissionURL_basic() { + // Most of the logic of parseSubmissionURL is tested in the search service + // itself, thus we only do a sanity check of the wrapper here. + let engine = yield promiseDefaultSearchEngine(); + let submissionURL = engine.getSubmission("terms").uri.spec; + + let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(submissionURL); + do_check_eq(result.engineName, engine.name); + do_check_eq(result.terms, "terms"); + + result = PlacesSearchAutocompleteProvider.parseSubmissionURL("http://example.org/"); + do_check_eq(result, null); +}); + +function promiseDefaultSearchEngine() { + let deferred = Promise.defer(); + Services.search.init( () => { + deferred.resolve(Services.search.defaultEngine); + }); + return deferred.promise; +} + +function promiseSearchTopic(expectedVerb) { + let deferred = Promise.defer(); + Services.obs.addObserver( function observe(subject, topic, verb) { + do_print("browser-search-engine-modified: " + verb); + if (verb == expectedVerb) { + Services.obs.removeObserver(observe, "browser-search-engine-modified"); + deferred.resolve(); + } + }, "browser-search-engine-modified", false); + return deferred.promise; +} diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js new file mode 100644 index 000000000..182f75eac --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js @@ -0,0 +1,77 @@ +/** + * This file tests PlacesUtils.asyncGetBookmarkIds method. + */ + +const TEST_URL = "http://www.example.com/"; + +var promiseAsyncGetBookmarkIds = Task.async(function* (url) { + yield PlacesTestUtils.promiseAsyncUpdates(); + return new Promise(resolve => { + PlacesUtils.asyncGetBookmarkIds(url, (itemIds, uri) => { + Assert.equal(uri, url); + resolve({ itemIds, url }); + }); + }); +}); + +add_task(function* test_no_bookmark() { + let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL); + Assert.equal(itemIds.length, 0); + Assert.equal(url, TEST_URL); +}); + +add_task(function* test_one_bookmark() { + let bookmark = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "test" + }); + let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); + { + let { itemIds, url } = yield promiseAsyncGetBookmarkIds(NetUtil.newURI(TEST_URL)); + Assert.equal(itemIds.length, 1); + Assert.equal(itemIds[0], itemId); + Assert.equal(url.spec, TEST_URL); + } + { + let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL); + Assert.equal(itemIds.length, 1); + Assert.equal(itemIds[0], itemId); + Assert.equal(url, TEST_URL); + } + yield PlacesUtils.bookmarks.remove(bookmark); +}); + +add_task(function* test_multiple_bookmarks() { + let ids = []; + let bookmark1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "test" + }); + ids.push((yield PlacesUtils.promiseItemId(bookmark1.guid))); + let bookmark2 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + title: "test" + }); + ids.push((yield PlacesUtils.promiseItemId(bookmark2.guid))); + + let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL); + Assert.deepEqual(ids, itemIds); + Assert.equal(url, TEST_URL); + + yield PlacesUtils.bookmarks.remove(bookmark1); + yield PlacesUtils.bookmarks.remove(bookmark2); +}); + +add_task(function* test_cancel() { + let pending = PlacesUtils.asyncGetBookmarkIds(TEST_URL, () => { + Assert.ok(false, "A canceled pending statement should not be invoked"); + }); + pending.cancel(); + + let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL); + Assert.equal(itemIds.length, 0); + Assert.equal(url, TEST_URL); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js new file mode 100644 index 000000000..b7906ec5c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js @@ -0,0 +1,25 @@ +add_task(function* () { + do_print("Add a bookmark."); + let bm = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let id = yield PlacesUtils.promiseItemId(bm.guid); + Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid); + + // Ensure invalidating a non-existent itemId doesn't throw. + PlacesUtils.invalidateCachedGuidFor(null); + PlacesUtils.invalidateCachedGuidFor(9999); + + do_print("Change the GUID."); + yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) { + yield db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id", + { guid: "123456789012", id}); + })); + // The cache should still point to the wrong id. + Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid); + + do_print("Invalidate the cache."); + PlacesUtils.invalidateCachedGuidFor(id); + Assert.equal((yield PlacesUtils.promiseItemGuid(id)), "123456789012"); + Assert.equal((yield PlacesUtils.promiseItemId("123456789012")), id); + yield Assert.rejects(PlacesUtils.promiseItemId(bm.guid), /no item found for the given GUID/); +}); diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js new file mode 100644 index 000000000..f0e9c5517 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + do_test_pending(); + + const TEST_URI = NetUtil.newURI("http://moz.org/") + let observer = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + ]), + + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemAdded: function (aItemId, aParentId, aIndex, aItemType, aURI) { + do_check_true(aURI.equals(TEST_URI)); + PlacesUtils.removeLazyBookmarkObserver(this); + do_test_finished(); + }, + onItemRemoved: function () {}, + onItemChanged: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {}, + }; + + // Check registration and removal with uninitialized bookmarks service. + PlacesUtils.addLazyBookmarkObserver(observer); + PlacesUtils.removeLazyBookmarkObserver(observer); + + // Add a proper lazy observer we will test. + PlacesUtils.addLazyBookmarkObserver(observer); + + // Check that we don't leak when adding and removing an observer while the + // bookmarks service is instantiated but no change happened (bug 721319). + PlacesUtils.bookmarks; + PlacesUtils.addLazyBookmarkObserver(observer); + PlacesUtils.removeLazyBookmarkObserver(observer); + try { + PlacesUtils.bookmarks.removeObserver(observer); + do_throw("Trying to remove a nonexisting observer should throw!"); + } catch (ex) {} + + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Bookmark title"); +} diff --git a/toolkit/components/places/tests/unit/test_adaptive.js b/toolkit/components/places/tests/unit/test_adaptive.js new file mode 100644 index 000000000..78ffaedb5 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_adaptive.js @@ -0,0 +1,406 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 395739 to make sure the feedback to the search results in those + * entries getting better ranks. Additionally, exact matches should be ranked + * higher. Because the interactions among adaptive rank and visit counts is not + * well defined, this test holds one of the two values constant when modifying + * the other. + * + * This also tests bug 395735 for the instrumentation feedback mechanism. + * + * Bug 411293 is tested to make sure the drop down strongly prefers previously + * typed pages that have been selected and are moved to the top with adaptive + * learning. + */ + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + get minResultsForPopup() { + return 0; + }, + get timeout() { + return 10; + }, + get searchParam() { + return ""; + }, + get textValue() { + return ""; + }, + get disableAutoComplete() { + return false; + }, + get completeDefaultIndex() { + return false; + }, + + get searchCount() { + return this.searches.length; + }, + getSearchAt: function (aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function () {}, + onSearchComplete: function() {}, + + get popupOpen() { + return false; + }, + popup: { + set selectedIndex(aIndex) {}, + invalidate: function () {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]) + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput]) +} + +/** + * Checks that autocomplete results are ordered correctly. + */ +function ensure_results(expected, searchTerm) +{ + let controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete. + let input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + input.onSearchComplete = function() { + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + do_check_eq(controller.matchCount, expected.length); + for (let i = 0; i < controller.matchCount; i++) { + print("Testing for '" + expected[i].uri.spec + "' got '" + controller.getValueAt(i) + "'"); + do_check_eq(controller.getValueAt(i), expected[i].uri.spec); + do_check_eq(controller.getStyleAt(i), expected[i].style); + } + + deferEnsureResults.resolve(); + }; + + controller.startSearch(searchTerm); +} + +/** + * Asynchronous task that bumps up the rank for an uri. + */ +function* task_setCountRank(aURI, aCount, aRank, aSearch, aBookmark) +{ + // Bump up the visit count for the uri. + let visits = []; + for (let i = 0; i < aCount; i++) { + visits.push({ uri: aURI, visitDate: d1, transition: TRANSITION_TYPED }); + } + yield PlacesTestUtils.addVisits(visits); + + // Make a nsIAutoCompleteController and friends for instrumentation feedback. + let thing = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput, + Ci.nsIAutoCompletePopup, + Ci.nsIAutoCompleteController]), + get popup() { + return thing; + }, + get controller() { + return thing; + }, + popupOpen: true, + selectedIndex: 0, + getValueAt: function() { + return aURI.spec; + }, + searchString: aSearch + }; + + // Bump up the instrumentation feedback. + for (let i = 0; i < aRank; i++) { + Services.obs.notifyObservers(thing, "autocomplete-will-enter-text", null); + } + + // If this is supposed to be a bookmark, add it. + if (aBookmark) { + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + aURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test_book"); + + // And add the tag if we need to. + if (aBookmark == "tag") { + PlacesUtils.tagging.tagURI(aURI, ["test_tag"]); + } + } +} + +/** + * Decay the adaptive entries by sending the daily idle topic. + */ +function doAdaptiveDecay() +{ + PlacesUtils.history.runInBatchMode({ + runBatched: function() { + for (let i = 0; i < 10; i++) { + PlacesUtils.history.QueryInterface(Ci.nsIObserver) + .observe(null, "idle-daily", null); + } + } + }, this); +} + +var uri1 = uri("http://site.tld/1"); +var uri2 = uri("http://site.tld/2"); + +// d1 is some date for the page visit +var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000; +// c1 is larger (should show up higher) than c2 +var c1 = 10; +var c2 = 1; +// s1 is a partial match of s2 +var s0 = ""; +var s1 = "si"; +var s2 = "site"; + +var observer = { + results: null, + search: null, + runCount: -1, + observe: function(aSubject, aTopic, aData) + { + if (--this.runCount > 0) + return; + ensure_results(this.results, this.search); + } +}; +Services.obs.addObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED, false); + +/** + * Make the result object for a given URI that will be passed to ensure_results. + */ +function makeResult(aURI, aStyle = "favicon") { + return { + uri: aURI, + style: aStyle, + }; +} + +var tests = [ + // Test things without a search term. + function*() { + print("Test 0 same count, diff rank, same term; no search"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s0; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c1, s2); + yield task_setCountRank(uri2, c1, c2, s2); + }, + function*() { + print("Test 1 same count, diff rank, same term; no search"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s0; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c2, s2); + yield task_setCountRank(uri2, c1, c1, s2); + }, + function*() { + print("Test 2 diff count, same rank, same term; no search"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s0; + observer.runCount = c1 + c1; + yield task_setCountRank(uri1, c1, c1, s2); + yield task_setCountRank(uri2, c2, c1, s2); + }, + function*() { + print("Test 3 diff count, same rank, same term; no search"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s0; + observer.runCount = c1 + c1; + yield task_setCountRank(uri1, c2, c1, s2); + yield task_setCountRank(uri2, c1, c1, s2); + }, + + // Test things with a search term (exact match one, partial other). + function*() { + print("Test 4 same count, same rank, diff term; one exact/one partial search"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s1; + observer.runCount = c1 + c1; + yield task_setCountRank(uri1, c1, c1, s1); + yield task_setCountRank(uri2, c1, c1, s2); + }, + function*() { + print("Test 5 same count, same rank, diff term; one exact/one partial search"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s1; + observer.runCount = c1 + c1; + yield task_setCountRank(uri1, c1, c1, s2); + yield task_setCountRank(uri2, c1, c1, s1); + }, + + // Test things with a search term (exact match both). + function*() { + print("Test 6 same count, diff rank, same term; both exact search"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s1; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c1, s1); + yield task_setCountRank(uri2, c1, c2, s1); + }, + function*() { + print("Test 7 same count, diff rank, same term; both exact search"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s1; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c2, s1); + yield task_setCountRank(uri2, c1, c1, s1); + }, + + // Test things with a search term (partial match both). + function*() { + print("Test 8 same count, diff rank, same term; both partial search"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s1; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c1, s2); + yield task_setCountRank(uri2, c1, c2, s2); + }, + function*() { + print("Test 9 same count, diff rank, same term; both partial search"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s1; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c2, s2); + yield task_setCountRank(uri2, c1, c1, s2); + }, + function*() { + print("Test 10 same count, same rank, same term, decay first; exact match"); + observer.results = [ + makeResult(uri2), + makeResult(uri1), + ]; + observer.search = s1; + observer.runCount = c1 + c1; + yield task_setCountRank(uri1, c1, c1, s1); + doAdaptiveDecay(); + yield task_setCountRank(uri2, c1, c1, s1); + }, + function*() { + print("Test 11 same count, same rank, same term, decay second; exact match"); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s1; + observer.runCount = c1 + c1; + yield task_setCountRank(uri2, c1, c1, s1); + doAdaptiveDecay(); + yield task_setCountRank(uri1, c1, c1, s1); + }, + // Test that bookmarks are hidden if the preferences are set right. + function*() { + print("Test 12 same count, diff rank, same term; no search; history only"); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + observer.results = [ + makeResult(uri1), + makeResult(uri2), + ]; + observer.search = s0; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c1, s2, "bookmark"); + yield task_setCountRank(uri2, c1, c2, s2); + }, + // Test that tags are shown if the preferences are set right. + function*() { + print("Test 13 same count, diff rank, same term; no search; history only with tag"); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + observer.results = [ + makeResult(uri1, "tag"), + makeResult(uri2), + ]; + observer.search = s0; + observer.runCount = c1 + c2; + yield task_setCountRank(uri1, c1, c1, s2, "tag"); + yield task_setCountRank(uri2, c1, c2, s2); + }, +]; + +/** + * This deferred object contains a promise that is resolved when the + * ensure_results function has finished its execution. + */ +var deferEnsureResults; + +/** + * Test adaptive autocomplete. + */ +add_task(function* test_adaptive() +{ + // Disable autoFill for this test. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill")); + for (let test of tests) { + // Cleanup. + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.tagsFolderId); + observer.runCount = -1; + + let types = ["history", "bookmark", "openpage"]; + for (let type of types) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } + + yield PlacesTestUtils.clearHistory(); + + deferEnsureResults = Promise.defer(); + yield test(); + yield deferEnsureResults.promise; + } + + Services.obs.removeObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED); +}); diff --git a/toolkit/components/places/tests/unit/test_adaptive_bug527311.js b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js new file mode 100644 index 000000000..024553bba --- /dev/null +++ b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js @@ -0,0 +1,141 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URL = "http://adapt.mozilla.org/"; +const SEARCH_STRING = "adapt"; +const SUGGEST_TYPES = ["history", "bookmark", "openpage"]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); +var ps = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + +const PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC = + "places-autocomplete-feedback-updated"; + +function cleanup() { + for (let type of SUGGEST_TYPES) { + ps.clearUserPref("browser.urlbar.suggest." + type); + } +} + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + searches: null, + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function ACI_getSearchAt(aIndex) { + return this.searches[aIndex]; + }, + + onSearchComplete: function ACI_onSearchComplete() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function() {}, + invalidate: function() {}, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + onSearchBegin: function() {}, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + + +function check_results() { + let controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + let input = new AutoCompleteInput(["unifiedcomplete"]); + controller.input = input; + + input.onSearchComplete = function() { + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH); + do_check_eq(controller.matchCount, 0); + + PlacesUtils.bookmarks.eraseEverything().then(() => { + cleanup(); + do_test_finished(); + }); + }; + + controller.startSearch(SEARCH_STRING); +} + + +function addAdaptiveFeedback(aUrl, aSearch, aCallback) { + let observer = { + observe: function(aSubject, aTopic, aData) { + os.removeObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC); + do_timeout(0, aCallback); + } + }; + os.addObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC, false); + + let thing = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput, + Ci.nsIAutoCompletePopup, + Ci.nsIAutoCompleteController]), + get popup() { return thing; }, + get controller() { return thing; }, + popupOpen: true, + selectedIndex: 0, + getValueAt: () => aUrl, + searchString: aSearch + }; + + os.notifyObservers(thing, "autocomplete-will-enter-text", null); +} + + +function run_test() { + do_test_pending(); + + // Add a bookmark to our url. + bs.insertBookmark(bs.unfiledBookmarksFolder, uri(TEST_URL), + bs.DEFAULT_INDEX, "test_book"); + // We want to search only history. + for (let type of SUGGEST_TYPES) { + type == "history" ? ps.setBoolPref("browser.urlbar.suggest." + type, true) + : ps.setBoolPref("browser.urlbar.suggest." + type, false); + } + + // Add an adaptive entry. + addAdaptiveFeedback(TEST_URL, SEARCH_STRING, check_results); +} diff --git a/toolkit/components/places/tests/unit/test_analyze.js b/toolkit/components/places/tests/unit/test_analyze.js new file mode 100644 index 000000000..456270101 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_analyze.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests sqlite_sta1 table exists, it should be created by analyze. +// Since the bookmark roots are created when the DB is created (bug 704855), +// the table will contain data. + +function run_test() { + do_test_pending(); + + let stmt = DBConn().createAsyncStatement( + "SELECT ROWID FROM sqlite_stat1" + ); + stmt.executeAsync({ + _gotResult: false, + handleResult: function(aResultSet) { + this._gotResult = true; + }, + handleError: function(aError) { + do_throw("Unexpected error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function(aReason) { + do_check_true(this._gotResult); + do_test_finished(); + } + }); + stmt.finalize(); +} diff --git a/toolkit/components/places/tests/unit/test_annotations.js b/toolkit/components/places/tests/unit/test_annotations.js new file mode 100644 index 000000000..a37d7e6c9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_annotations.js @@ -0,0 +1,363 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get bookmark service +try { + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService); +} catch (ex) { + do_throw("Could not get nav-bookmarks-service\n"); +} + +// Get annotation service +try { + var annosvc= Cc["@mozilla.org/browser/annotation-service;1"].getService(Ci.nsIAnnotationService); +} catch (ex) { + do_throw("Could not get annotation service\n"); +} + +var annoObserver = { + PAGE_lastSet_URI: "", + PAGE_lastSet_AnnoName: "", + + onPageAnnotationSet: function(aURI, aName) { + this.PAGE_lastSet_URI = aURI.spec; + this.PAGE_lastSet_AnnoName = aName; + }, + + ITEM_lastSet_Id: -1, + ITEM_lastSet_AnnoName: "", + onItemAnnotationSet: function(aItemId, aName) { + this.ITEM_lastSet_Id = aItemId; + this.ITEM_lastSet_AnnoName = aName; + }, + + PAGE_lastRemoved_URI: "", + PAGE_lastRemoved_AnnoName: "", + onPageAnnotationRemoved: function(aURI, aName) { + this.PAGE_lastRemoved_URI = aURI.spec; + this.PAGE_lastRemoved_AnnoName = aName; + }, + + ITEM_lastRemoved_Id: -1, + ITEM_lastRemoved_AnnoName: "", + onItemAnnotationRemoved: function(aItemId, aName) { + this.ITEM_lastRemoved_Id = aItemId; + this.ITEM_lastRemoved_AnnoName = aName; + } +}; + +// main +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + var testURI = uri("http://mozilla.com/"); + var testItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, ""); + var testAnnoName = "moz-test-places/annotations"; + var testAnnoVal = "test"; + + annosvc.addObserver(annoObserver); + // create new string annotation + try { + annosvc.setPageAnnotation(testURI, testAnnoName, testAnnoVal, 0, 0); + } catch (ex) { + do_throw("unable to add page-annotation"); + } + do_check_eq(annoObserver.PAGE_lastSet_URI, testURI.spec); + do_check_eq(annoObserver.PAGE_lastSet_AnnoName, testAnnoName); + + // get string annotation + do_check_true(annosvc.pageHasAnnotation(testURI, testAnnoName)); + var storedAnnoVal = annosvc.getPageAnnotation(testURI, testAnnoName); + do_check_true(testAnnoVal === storedAnnoVal); + // string item-annotation + try { + var lastModified = bmsvc.getItemLastModified(testItemId); + // Verify that lastModified equals dateAdded before we set the annotation. + do_check_eq(lastModified, bmsvc.getItemDateAdded(testItemId)); + // Workaround possible VM timers issues moving last modified to the past. + bmsvc.setItemLastModified(testItemId, --lastModified); + annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0); + var lastModified2 = bmsvc.getItemLastModified(testItemId); + // verify that setting the annotation updates the last modified time + do_check_true(lastModified2 > lastModified); + } catch (ex) { + do_throw("unable to add item annotation"); + } + do_check_eq(annoObserver.ITEM_lastSet_Id, testItemId); + do_check_eq(annoObserver.ITEM_lastSet_AnnoName, testAnnoName); + + try { + var annoVal = annosvc.getItemAnnotation(testItemId, testAnnoName); + // verify the anno value + do_check_true(testAnnoVal === annoVal); + } catch (ex) { + do_throw("unable to get item annotation"); + } + + // test getPagesWithAnnotation + var uri2 = uri("http://www.tests.tld"); + yield PlacesTestUtils.addVisits(uri2); + annosvc.setPageAnnotation(uri2, testAnnoName, testAnnoVal, 0, 0); + var pages = annosvc.getPagesWithAnnotation(testAnnoName); + do_check_eq(pages.length, 2); + // Don't rely on the order + do_check_false(pages[0].equals(pages[1])); + do_check_true(pages[0].equals(testURI) || pages[1].equals(testURI)); + do_check_true(pages[0].equals(uri2) || pages[1].equals(uri2)); + + // test getItemsWithAnnotation + var testItemId2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, -1, ""); + annosvc.setItemAnnotation(testItemId2, testAnnoName, testAnnoVal, 0, 0); + var items = annosvc.getItemsWithAnnotation(testAnnoName); + do_check_eq(items.length, 2); + // Don't rely on the order + do_check_true(items[0] != items[1]); + do_check_true(items[0] == testItemId || items[1] == testItemId); + do_check_true(items[0] == testItemId2 || items[1] == testItemId2); + + // get annotation that doesn't exist + try { + annosvc.getPageAnnotation(testURI, "blah"); + do_throw("fetching page-annotation that doesn't exist, should've thrown"); + } catch (ex) {} + try { + annosvc.getItemAnnotation(testURI, "blah"); + do_throw("fetching item-annotation that doesn't exist, should've thrown"); + } catch (ex) {} + + // get annotation info + var flags = {}, exp = {}, storageType = {}; + annosvc.getPageAnnotationInfo(testURI, testAnnoName, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING); + annosvc.getItemAnnotationInfo(testItemId, testAnnoName, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING); + + // get annotation names for a uri + var annoNames = annosvc.getPageAnnotationNames(testURI); + do_check_eq(annoNames.length, 1); + do_check_eq(annoNames[0], "moz-test-places/annotations"); + + // get annotation names for an item + annoNames = annosvc.getItemAnnotationNames(testItemId); + do_check_eq(annoNames.length, 1); + do_check_eq(annoNames[0], "moz-test-places/annotations"); + + // copy annotations to another uri + var newURI = uri("http://mozilla.org"); + yield PlacesTestUtils.addVisits(newURI); + annosvc.setPageAnnotation(testURI, "oldAnno", "new", 0, 0); + annosvc.setPageAnnotation(newURI, "oldAnno", "old", 0, 0); + annoNames = annosvc.getPageAnnotationNames(newURI); + do_check_eq(annoNames.length, 1); + do_check_eq(annoNames[0], "oldAnno"); + var oldAnnoNames = annosvc.getPageAnnotationNames(testURI); + do_check_eq(oldAnnoNames.length, 2); + var copiedAnno = oldAnnoNames[0]; + annosvc.copyPageAnnotations(testURI, newURI, false); + var newAnnoNames = annosvc.getPageAnnotationNames(newURI); + do_check_eq(newAnnoNames.length, 2); + do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno")); + do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno)); + do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "old"); + annosvc.setPageAnnotation(newURI, "oldAnno", "new", 0, 0); + annosvc.copyPageAnnotations(testURI, newURI, true); + newAnnoNames = annosvc.getPageAnnotationNames(newURI); + do_check_eq(newAnnoNames.length, 2); + do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno")); + do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno)); + do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "new"); + + + // copy annotations to another item + newURI = uri("http://mozilla.org"); + var newItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, newURI, -1, ""); + var itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, ""); + annosvc.setItemAnnotation(itemId, "oldAnno", "new", 0, 0); + annosvc.setItemAnnotation(itemId, "testAnno", "test", 0, 0); + annosvc.setItemAnnotation(newItemId, "oldAnno", "old", 0, 0); + annoNames = annosvc.getItemAnnotationNames(newItemId); + do_check_eq(annoNames.length, 1); + do_check_eq(annoNames[0], "oldAnno"); + oldAnnoNames = annosvc.getItemAnnotationNames(itemId); + do_check_eq(oldAnnoNames.length, 2); + copiedAnno = oldAnnoNames[0]; + annosvc.copyItemAnnotations(itemId, newItemId, false); + newAnnoNames = annosvc.getItemAnnotationNames(newItemId); + do_check_eq(newAnnoNames.length, 2); + do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno")); + do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno)); + do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "old"); + annosvc.setItemAnnotation(newItemId, "oldAnno", "new", 0, 0); + annosvc.copyItemAnnotations(itemId, newItemId, true); + newAnnoNames = annosvc.getItemAnnotationNames(newItemId); + do_check_eq(newAnnoNames.length, 2); + do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno")); + do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno)); + do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "new"); + + // test int32 anno type + var int32Key = testAnnoName + "/types/Int32"; + var int32Val = 23; + annosvc.setPageAnnotation(testURI, int32Key, int32Val, 0, 0); + do_check_true(annosvc.pageHasAnnotation(testURI, int32Key)); + flags = {}, exp = {}, storageType = {}; + annosvc.getPageAnnotationInfo(testURI, int32Key, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_INT32); + var storedVal = annosvc.getPageAnnotation(testURI, int32Key); + do_check_true(int32Val === storedVal); + annosvc.setItemAnnotation(testItemId, int32Key, int32Val, 0, 0); + do_check_true(annosvc.itemHasAnnotation(testItemId, int32Key)); + annosvc.getItemAnnotationInfo(testItemId, int32Key, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + storedVal = annosvc.getItemAnnotation(testItemId, int32Key); + do_check_true(int32Val === storedVal); + + // test int64 anno type + var int64Key = testAnnoName + "/types/Int64"; + var int64Val = 4294967296; + annosvc.setPageAnnotation(testURI, int64Key, int64Val, 0, 0); + annosvc.getPageAnnotationInfo(testURI, int64Key, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + storedVal = annosvc.getPageAnnotation(testURI, int64Key); + do_check_true(int64Val === storedVal); + annosvc.setItemAnnotation(testItemId, int64Key, int64Val, 0, 0); + do_check_true(annosvc.itemHasAnnotation(testItemId, int64Key)); + annosvc.getItemAnnotationInfo(testItemId, int64Key, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + storedVal = annosvc.getItemAnnotation(testItemId, int64Key); + do_check_true(int64Val === storedVal); + + // test double anno type + var doubleKey = testAnnoName + "/types/Double"; + var doubleVal = 0.000002342; + annosvc.setPageAnnotation(testURI, doubleKey, doubleVal, 0, 0); + annosvc.getPageAnnotationInfo(testURI, doubleKey, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + storedVal = annosvc.getPageAnnotation(testURI, doubleKey); + do_check_true(doubleVal === storedVal); + annosvc.setItemAnnotation(testItemId, doubleKey, doubleVal, 0, 0); + do_check_true(annosvc.itemHasAnnotation(testItemId, doubleKey)); + annosvc.getItemAnnotationInfo(testItemId, doubleKey, flags, exp, storageType); + do_check_eq(flags.value, 0); + do_check_eq(exp.value, 0); + do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_DOUBLE); + storedVal = annosvc.getItemAnnotation(testItemId, doubleKey); + do_check_true(doubleVal === storedVal); + + // test annotation removal + annosvc.removePageAnnotation(testURI, int32Key); + + annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0); + // verify that removing an annotation updates the last modified date + var lastModified3 = bmsvc.getItemLastModified(testItemId); + // Workaround possible VM timers issues moving last modified to the past. + bmsvc.setItemLastModified(testItemId, --lastModified3); + annosvc.removeItemAnnotation(testItemId, int32Key); + var lastModified4 = bmsvc.getItemLastModified(testItemId); + do_print("verify that removing an annotation updates the last modified date"); + do_print("lastModified3 = " + lastModified3); + do_print("lastModified4 = " + lastModified4); + do_check_true(lastModified4 > lastModified3); + + do_check_eq(annoObserver.PAGE_lastRemoved_URI, testURI.spec); + do_check_eq(annoObserver.PAGE_lastRemoved_AnnoName, int32Key); + do_check_eq(annoObserver.ITEM_lastRemoved_Id, testItemId); + do_check_eq(annoObserver.ITEM_lastRemoved_AnnoName, int32Key); + + // test that getItems/PagesWithAnnotation returns an empty array after + // removing all items/pages which had the annotation set, see bug 380317. + do_check_eq(annosvc.getItemsWithAnnotation(int32Key).length, 0); + do_check_eq(annosvc.getPagesWithAnnotation(int32Key).length, 0); + + // Setting item annotations on invalid item ids should throw + var invalidIds = [-1, 0, 37643]; + for (var id of invalidIds) { + try { + annosvc.setItemAnnotation(id, "foo", "bar", 0, 0); + do_throw("setItemAnnotation* should throw for invalid item id: " + id) + } + catch (ex) { } + } + + // setting an annotation with EXPIRE_HISTORY for an item should throw + itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, ""); + try { + annosvc.setItemAnnotation(itemId, "foo", "bar", 0, annosvc.EXPIRE_WITH_HISTORY); + do_throw("setting an item annotation with EXPIRE_HISTORY should throw"); + } + catch (ex) { + } + + annosvc.removeObserver(annoObserver); +}); + +add_test(function test_getAnnotationsHavingName() { + let uri = NetUtil.newURI("http://cat.mozilla.org"); + let id = PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, "cat"); + let fid = PlacesUtils.bookmarks.createFolder( + PlacesUtils.unfiledBookmarksFolderId, "pillow", + PlacesUtils.bookmarks.DEFAULT_INDEX); + + const ANNOS = { + "int": 7, + "double": 7.7, + "string": "seven" + }; + for (let name in ANNOS) { + PlacesUtils.annotations.setPageAnnotation( + uri, name, ANNOS[name], 0, + PlacesUtils.annotations.EXPIRE_SESSION); + PlacesUtils.annotations.setItemAnnotation( + id, name, ANNOS[name], 0, + PlacesUtils.annotations.EXPIRE_SESSION); + PlacesUtils.annotations.setItemAnnotation( + fid, name, ANNOS[name], 0, + PlacesUtils.annotations.EXPIRE_SESSION); + } + + for (let name in ANNOS) { + let results = PlacesUtils.annotations.getAnnotationsWithName(name); + do_check_eq(results.length, 3); + + for (let result of results) { + do_check_eq(result.annotationName, name); + do_check_eq(result.annotationValue, ANNOS[name]); + if (result.uri) + do_check_true(result.uri.equals(uri)); + else + do_check_true(result.itemId > 0); + + if (result.itemId != -1) { + if (result.uri) + do_check_eq(result.itemId, id); + else + do_check_eq(result.itemId, fid); + do_check_guid_for_bookmark(result.itemId, result.guid); + } + else { + do_check_guid_for_uri(result.uri, result.guid); + } + } + } + + run_next_test(); +}); diff --git a/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js new file mode 100644 index 000000000..7296fe061 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This is a test for asyncExecuteLegacyQueries API. + +var tests = [ + +function test_history_query() { + let uri = NetUtil.newURI("http://test.visit.mozilla.com/"); + let title = "Test visit"; + PlacesTestUtils.addVisits({ uri: uri, title: title }).then(function () { + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; + let query = PlacesUtils.history.getNewQuery(); + + PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .asyncExecuteLegacyQueries([query], 1, options, { + handleResult: function (aResultSet) { + for (let row; (row = aResultSet.getNextRow());) { + try { + do_check_eq(row.getResultByIndex(1), uri.spec); + do_check_eq(row.getResultByIndex(2), title); + } catch (e) { + do_throw("Error while fetching page data."); + } + } + }, + handleError: function (aError) { + do_throw("Async execution error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function (aReason) { + run_next_test(); + }, + }); + }); +}, + +function test_bookmarks_query() { + let uri = NetUtil.newURI("http://test.bookmark.mozilla.com/"); + let title = "Test bookmark"; + bookmark(uri, title); + let options = PlacesUtils.history.getNewQueryOptions(); + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_LASMODIFIED_DESCENDING; + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let query = PlacesUtils.history.getNewQuery(); + + PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .asyncExecuteLegacyQueries([query], 1, options, { + handleResult: function (aResultSet) { + for (let row; (row = aResultSet.getNextRow());) { + try { + do_check_eq(row.getResultByIndex(1), uri.spec); + do_check_eq(row.getResultByIndex(2), title); + } catch (e) { + do_throw("Error while fetching page data."); + } + } + }, + handleError: function (aError) { + do_throw("Async execution error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function (aReason) { + run_next_test(); + }, + }); +}, + +]; + +function bookmark(aURI, aTitle) +{ + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + aURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + aTitle); +} + +function run_test() +{ + do_test_pending(); + run_next_test(); +} + +function run_next_test() { + if (tests.length == 0) { + do_test_finished(); + return; + } + + Promise.all([ + PlacesTestUtils.clearHistory(), + PlacesUtils.bookmarks.eraseEverything() + ]).then(tests.shift()); +} diff --git a/toolkit/components/places/tests/unit/test_async_history_api.js b/toolkit/components/places/tests/unit/test_async_history_api.js new file mode 100644 index 000000000..a012fcda2 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_history_api.js @@ -0,0 +1,1118 @@ +/** + * This file tests the async history API exposed by mozIAsyncHistory. + */ + +// Globals + +const TEST_DOMAIN = "http://mozilla.org/"; +const URI_VISIT_SAVED = "uri-visit-saved"; +const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000; + +// Helpers +/** + * Object that represents a mozIVisitInfo object. + * + * @param [optional] aTransitionType + * The transition type of the visit. Defaults to TRANSITION_LINK if not + * provided. + * @param [optional] aVisitTime + * The time of the visit. Defaults to now if not provided. + */ +function VisitInfo(aTransitionType, + aVisitTime) { + this.transitionType = + aTransitionType === undefined ? TRANSITION_LINK : aTransitionType; + this.visitDate = aVisitTime || Date.now() * 1000; +} + +function promiseUpdatePlaces(aPlaces) { + return new Promise((resolve, reject) => { + PlacesUtils.asyncHistory.updatePlaces(aPlaces, { + _errors: [], + _results: [], + handleError(aResultCode, aPlace) { + this._errors.push({ resultCode: aResultCode, info: aPlace}); + }, + handleResult(aPlace) { + this._results.push(aPlace); + }, + handleCompletion() { + resolve({ errors: this._errors, results: this._results }); + } + }); + }); +} + +/** + * Listens for a title change notification, and calls aCallback when it gets it. + * + * @param aURI + * The URI of the page we expect a notification for. + * @param aExpectedTitle + * The expected title of the URI we expect a notification for. + * @param aCallback + * The method to call when we have gotten the proper notification about + * the title changing. + */ +function TitleChangedObserver(aURI, + aExpectedTitle, + aCallback) { + this.uri = aURI; + this.expectedTitle = aExpectedTitle; + this.callback = aCallback; +} +TitleChangedObserver.prototype = { + __proto__: NavHistoryObserver.prototype, + onTitleChanged(aURI, aTitle, aGUID) { + do_print("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); + if (!this.uri.equals(aURI)) { + return; + } + do_check_eq(aTitle, this.expectedTitle); + do_check_guid_for_uri(aURI, aGUID); + this.callback(); + }, +}; + +/** + * Listens for a visit notification, and calls aCallback when it gets it. + * + * @param aURI + * The URI of the page we expect a notification for. + * @param aCallback + * The method to call when we have gotten the proper notification about + * being visited. + */ +function VisitObserver(aURI, + aGUID, + aCallback) +{ + this.uri = aURI; + this.guid = aGUID; + this.callback = aCallback; +} +VisitObserver.prototype = { + __proto__: NavHistoryObserver.prototype, + onVisit: function(aURI, + aVisitId, + aTime, + aSessionId, + aReferringId, + aTransitionType, + aGUID) + { + do_print("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + + ", " + aSessionId + ", " + aReferringId + ", " + + aTransitionType + ", " + aGUID + ")"); + if (!this.uri.equals(aURI) || this.guid != aGUID) { + return; + } + this.callback(aTime, aTransitionType); + }, +}; + +/** + * Tests that a title was set properly in the database. + * + * @param aURI + * The uri to check. + * @param aTitle + * The expected title in the database. + */ +function do_check_title_for_uri(aURI, + aTitle) +{ + let stack = Components.stack.caller; + let stmt = DBConn().createStatement( + `SELECT title + FROM moz_places + WHERE url_hash = hash(:url) AND url = :url` + ); + stmt.params.url = aURI.spec; + do_check_true(stmt.executeStep(), stack); + do_check_eq(stmt.row.title, aTitle, stack); + stmt.finalize(); +} + +// Test Functions + +add_task(function* test_interface_exists() { + let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); + do_check_true(history instanceof Ci.mozIAsyncHistory); +}); + +add_task(function* test_invalid_uri_throws() { + // First, test passing in nothing. + let place = { + visits: [ + new VisitInfo(), + ], + }; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + // Now, test other bogus things. + const TEST_VALUES = [ + null, + undefined, + {}, + [], + TEST_DOMAIN + "test_invalid_id_throws", + ]; + for (let i = 0; i < TEST_VALUES.length; i++) { + place.uri = TEST_VALUES[i]; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + } +}); + +add_task(function* test_invalid_places_throws() { + // First, test passing in nothing. + try { + PlacesUtils.asyncHistory.updatePlaces(); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); + } + + // Now, test other bogus things. + const TEST_VALUES = [ + null, + undefined, + {}, + [], + "", + ]; + for (let i = 0; i < TEST_VALUES.length; i++) { + let value = TEST_VALUES[i]; + try { + yield promiseUpdatePlaces(value); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + } +}); + +add_task(function* test_invalid_guid_throws() { + // First check invalid length guid. + let place = { + guid: "BAD_GUID", + uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"), + visits: [ + new VisitInfo(), + ], + }; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + // Now check invalid character guid. + place.guid = "__BADGUID+__"; + do_check_eq(place.guid.length, 12); + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_no_visits_throws() { + const TEST_URI = + NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws"); + const TEST_GUID = "_RANDOMGUID_"; + + let log_test_conditions = function(aPlace) { + let str = "Testing place with " + + (aPlace.uri ? "uri" : "no uri") + ", " + + (aPlace.guid ? "guid" : "no guid") + ", " + + (aPlace.visits ? "visits array" : "no visits array"); + do_print(str); + }; + + // Loop through every possible case. Note that we don't actually care about + // the case where we have no uri, place id, or guid (covered by another test), + // but it is easier to just make sure it too throws than to exclude it. + let place = { }; + for (let uri = 1; uri >= 0; uri--) { + place.uri = uri ? TEST_URI : undefined; + + for (let guid = 1; guid >= 0; guid--) { + place.guid = guid ? TEST_GUID : undefined; + + for (let visits = 1; visits >= 0; visits--) { + place.visits = visits ? [] : undefined; + + log_test_conditions(place); + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + } + } + } +}); + +add_task(function* test_add_visit_no_date_throws() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"), + visits: [ + new VisitInfo(), + ], + }; + delete place.visits[0].visitDate; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_add_visit_no_transitionType_throws() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"), + visits: [ + new VisitInfo(), + ], + }; + delete place.visits[0].transitionType; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_add_visit_invalid_transitionType_throws() { + // First, test something that has a transition type lower than the first one. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + + "test_add_visit_invalid_transitionType_throws"), + visits: [ + new VisitInfo(TRANSITION_LINK - 1), + ], + }; + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + // Now, test something that has a transition type greater than the last one. + place.visits[0] = new VisitInfo(TRANSITION_RELOAD + 1); + try { + yield promiseUpdatePlaces(place); + do_throw("Should have thrown!"); + } + catch (e) { + do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_non_addable_uri_errors() { + // Array of protocols that nsINavHistoryService::canAddURI returns false for. + const URLS = [ + "about:config", + "imap://cyrus.andrew.cmu.edu/archive.imap", + "news://new.mozilla.org/mozilla.dev.apps.firefox", + "mailbox:Inbox", + "moz-anno:favicon:http://mozilla.org/made-up-favicon", + "view-source:http://mozilla.org", + "chrome://browser/content/browser.xul", + "resource://gre-resources/hiddenWindow.html", + "data:,Hello%2C%20World!", + "wyciwyg:/0/http://mozilla.org", + "javascript:alert('hello wolrd!');", + "blob:foo", + ]; + let places = []; + URLS.forEach(function(url) { + try { + let place = { + uri: NetUtil.newURI(url), + title: "test for " + url, + visits: [ + new VisitInfo(), + ], + }; + places.push(place); + } + catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + // NetUtil.newURI() can throw if e.g. our app knows about imap:// + // but the account is not set up and so the URL is invalid for us. + // Note this in the log but ignore as it's not the subject of this test. + do_print("Could not construct URI for '" + url + "'; ignoring"); + } + }); + + let placesResult = yield promiseUpdatePlaces(places); + if (placesResult.results.length > 0) { + do_throw("Unexpected success."); + } + for (let place of placesResult.errors) { + do_print("Checking '" + place.info.uri.spec + "'"); + do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG); + do_check_false(yield promiseIsURIVisited(place.info.uri)); + } + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_duplicate_guid_errors() { + // This test ensures that trying to add a visit, with a guid already found in + // another visit, fails. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"), + visits: [ + new VisitInfo(), + ], + }; + + do_check_false(yield promiseIsURIVisited(place.uri)); + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); + + let badPlace = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"), + visits: [ + new VisitInfo(), + ], + guid: placeInfo.guid, + }; + + do_check_false(yield promiseIsURIVisited(badPlace.uri)); + placesResult = yield promiseUpdatePlaces(badPlace); + if (placesResult.results.length > 0) { + do_throw("Unexpected success."); + } + let badPlaceInfo = placesResult.errors[0]; + do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT); + do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri)); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_invalid_referrerURI_ignored() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + + "test_invalid_referrerURI_ignored"), + visits: [ + new VisitInfo(), + ], + }; + place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI"); + do_check_false(yield promiseIsURIVisited(place.uri)); + do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); + + // Check to make sure we do not visit the invalid referrer. + do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI)); + + // Check to make sure from_visit is zero in database. + let stmt = DBConn().createStatement( + `SELECT from_visit + FROM moz_historyvisits + WHERE id = :visit_id` + ); + stmt.params.visit_id = placeInfo.visits[0].visitId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.from_visit, 0); + stmt.finalize(); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_nonnsIURI_referrerURI_ignored() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + + "test_nonnsIURI_referrerURI_ignored"), + visits: [ + new VisitInfo(), + ], + }; + place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI"; + do_check_false(yield promiseIsURIVisited(place.uri)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + do_check_true(yield promiseIsURIVisited(placeInfo.uri)); + + // Check to make sure from_visit is zero in database. + let stmt = DBConn().createStatement( + `SELECT from_visit + FROM moz_historyvisits + WHERE id = :visit_id` + ); + stmt.params.visit_id = placeInfo.visits[0].visitId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.from_visit, 0); + stmt.finalize(); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_old_referrer_ignored() { + // This tests that a referrer for a visit which is not recent (specifically, + // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by + // updatePlaces. + let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1); + let referrerPlace = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"), + visits: [ + new VisitInfo(TRANSITION_LINK, oldTime), + ], + }; + + // First we must add our referrer to the history so that it is not ignored + // as being invalid. + do_check_false(yield promiseIsURIVisited(referrerPlace.uri)); + let placesResult = yield promiseUpdatePlaces(referrerPlace); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + + // Now that the referrer is added, we can add a page with a valid + // referrer to determine if the recency of the referrer is taken into + // account. + do_check_true(yield promiseIsURIVisited(referrerPlace.uri)); + + let visitInfo = new VisitInfo(); + visitInfo.referrerURI = referrerPlace.uri; + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"), + visits: [ + visitInfo, + ], + }; + + do_check_false(yield promiseIsURIVisited(place.uri)); + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + do_check_true(yield promiseIsURIVisited(place.uri)); + + // Though the visit will not contain the referrer, we must examine the + // database to be sure. + do_check_eq(placeInfo.visits[0].referrerURI, null); + let stmt = DBConn().createStatement( + `SELECT COUNT(1) AS count + FROM moz_historyvisits + JOIN moz_places h ON h.id = place_id + WHERE url_hash = hash(:page_url) AND url = :page_url + AND from_visit = 0` + ); + stmt.params.page_url = place.uri.spec; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.count, 1); + stmt.finalize(); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_place_id_ignored() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"), + visits: [ + new VisitInfo(), + ], + }; + + do_check_false(yield promiseIsURIVisited(place.uri)); + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + do_check_true(yield promiseIsURIVisited(place.uri)); + + let placeId = placeInfo.placeId; + do_check_neq(placeId, 0); + + let badPlace = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"), + visits: [ + new VisitInfo(), + ], + placeId: placeId, + }; + + do_check_false(yield promiseIsURIVisited(badPlace.uri)); + placesResult = yield promiseUpdatePlaces(badPlace); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + placeInfo = placesResult.results[0]; + + do_check_neq(placeInfo.placeId, placeId); + do_check_true(yield promiseIsURIVisited(badPlace.uri)); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_handleCompletion_called_when_complete() { + // We test a normal visit, and embeded visit, and a uri that would fail + // the canAddURI test to make sure that the notification happens after *all* + // of them have had a callback. + let places = [ + { uri: NetUtil.newURI(TEST_DOMAIN + + "test_handleCompletion_called_when_complete"), + visits: [ + new VisitInfo(), + new VisitInfo(TRANSITION_EMBED), + ], + }, + { uri: NetUtil.newURI("data:,Hello%2C%20World!"), + visits: [ + new VisitInfo(), + ], + }, + ]; + do_check_false(yield promiseIsURIVisited(places[0].uri)); + do_check_false(yield promiseIsURIVisited(places[1].uri)); + + const EXPECTED_COUNT_SUCCESS = 2; + const EXPECTED_COUNT_FAILURE = 1; + + let {results, errors} = yield promiseUpdatePlaces(places); + + do_check_eq(results.length, EXPECTED_COUNT_SUCCESS); + do_check_eq(errors.length, EXPECTED_COUNT_FAILURE); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_add_visit() { + const VISIT_TIME = Date.now() * 1000; + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"), + title: "test_add_visit title", + visits: [], + }; + for (let t in PlacesUtils.history.TRANSITIONS) { + let transitionType = PlacesUtils.history.TRANSITIONS[t]; + place.visits.push(new VisitInfo(transitionType, VISIT_TIME)); + } + do_check_false(yield promiseIsURIVisited(place.uri)); + + let callbackCount = 0; + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + for (let placeInfo of placesResult.results) { + do_check_true(yield promiseIsURIVisited(place.uri)); + + // Check mozIPlaceInfo properties. + do_check_true(place.uri.equals(placeInfo.uri)); + do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here! + do_check_eq(placeInfo.title, place.title); + + // Check mozIVisitInfo properties. + let visits = placeInfo.visits; + do_check_eq(visits.length, 1); + let visit = visits[0]; + do_check_eq(visit.visitDate, VISIT_TIME); + do_check_true(Object.values(PlacesUtils.history.TRANSITIONS).includes(visit.transitionType)); + do_check_true(visit.referrerURI === null); + + // For TRANSITION_EMBED visits, many properties will always be zero or + // undefined. + if (visit.transitionType == TRANSITION_EMBED) { + // Check mozIPlaceInfo properties. + do_check_eq(placeInfo.placeId, 0, '//'); + do_check_eq(placeInfo.guid, null); + + // Check mozIVisitInfo properties. + do_check_eq(visit.visitId, 0); + } + // But they should be valid for non-embed visits. + else { + // Check mozIPlaceInfo properties. + do_check_true(placeInfo.placeId > 0); + do_check_valid_places_guid(placeInfo.guid); + + // Check mozIVisitInfo properties. + do_check_true(visit.visitId > 0); + } + + // If we have had all of our callbacks, continue running tests. + if (++callbackCount == place.visits.length) { + yield PlacesTestUtils.promiseAsyncUpdates(); + } + } +}); + +add_task(function* test_properties_saved() { + // Check each transition type to make sure it is saved properly. + let places = []; + for (let t in PlacesUtils.history.TRANSITIONS) { + let transitionType = PlacesUtils.history.TRANSITIONS[t]; + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" + + transitionType), + title: "test_properties_saved test", + visits: [ + new VisitInfo(transitionType), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + places.push(place); + } + + let callbackCount = 0; + let placesResult = yield promiseUpdatePlaces(places); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + for (let placeInfo of placesResult.results) { + let uri = placeInfo.uri; + do_check_true(yield promiseIsURIVisited(uri)); + let visit = placeInfo.visits[0]; + print("TEST-INFO | test_properties_saved | updatePlaces callback for " + + "transition type " + visit.transitionType); + + // Note that TRANSITION_EMBED should not be in the database. + const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1; + + // mozIVisitInfo::date + let stmt = DBConn().createStatement( + `SELECT COUNT(1) AS count + FROM moz_places h + JOIN moz_historyvisits v + ON h.id = v.place_id + WHERE h.url_hash = hash(:page_url) AND h.url = :page_url + AND v.visit_date = :visit_date` + ); + stmt.params.page_url = uri.spec; + stmt.params.visit_date = visit.visitDate; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.count, EXPECTED_COUNT); + stmt.finalize(); + + // mozIVisitInfo::transitionType + stmt = DBConn().createStatement( + `SELECT COUNT(1) AS count + FROM moz_places h + JOIN moz_historyvisits v + ON h.id = v.place_id + WHERE h.url_hash = hash(:page_url) AND h.url = :page_url + AND v.visit_type = :transition_type` + ); + stmt.params.page_url = uri.spec; + stmt.params.transition_type = visit.transitionType; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.count, EXPECTED_COUNT); + stmt.finalize(); + + // mozIPlaceInfo::title + stmt = DBConn().createStatement( + `SELECT COUNT(1) AS count + FROM moz_places h + WHERE h.url_hash = hash(:page_url) AND h.url = :page_url + AND h.title = :title` + ); + stmt.params.page_url = uri.spec; + stmt.params.title = placeInfo.title; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.count, EXPECTED_COUNT); + stmt.finalize(); + + // If we have had all of our callbacks, continue running tests. + if (++callbackCount == places.length) { + yield PlacesTestUtils.promiseAsyncUpdates(); + } + } +}); + +add_task(function* test_guid_saved() { + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"), + guid: "__TESTGUID__", + visits: [ + new VisitInfo(), + ], + }; + do_check_valid_places_guid(place.guid); + do_check_false(yield promiseIsURIVisited(place.uri)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + let placeInfo = placesResult.results[0]; + let uri = placeInfo.uri; + do_check_true(yield promiseIsURIVisited(uri)); + do_check_eq(placeInfo.guid, place.guid); + do_check_guid_for_uri(uri, place.guid); + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_referrer_saved() { + let places = [ + { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"), + visits: [ + new VisitInfo(), + ], + }, + { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"), + visits: [ + new VisitInfo(), + ], + }, + ]; + places[1].visits[0].referrerURI = places[0].uri; + do_check_false(yield promiseIsURIVisited(places[0].uri)); + do_check_false(yield promiseIsURIVisited(places[1].uri)); + + let resultCount = 0; + let placesResult = yield promiseUpdatePlaces(places); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + for (let placeInfo of placesResult.results) { + let uri = placeInfo.uri; + do_check_true(yield promiseIsURIVisited(uri)); + let visit = placeInfo.visits[0]; + + // We need to insert all of our visits before we can test conditions. + if (++resultCount == places.length) { + do_check_true(places[0].uri.equals(visit.referrerURI)); + + let stmt = DBConn().createStatement( + `SELECT COUNT(1) AS count + FROM moz_historyvisits + JOIN moz_places h ON h.id = place_id + WHERE url_hash = hash(:page_url) AND url = :page_url + AND from_visit = ( + SELECT v.id + FROM moz_historyvisits v + JOIN moz_places h ON h.id = place_id + WHERE url_hash = hash(:referrer) AND url = :referrer + )` + ); + stmt.params.page_url = uri.spec; + stmt.params.referrer = visit.referrerURI.spec; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.count, 1); + stmt.finalize(); + + yield PlacesTestUtils.promiseAsyncUpdates(); + } + } +}); + +add_task(function* test_guid_change_saved() { + // First, add a visit for it. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"), + visits: [ + new VisitInfo(), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + // Then, change the guid with visits. + place.guid = "_GUIDCHANGE_"; + place.visits = [new VisitInfo()]; + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + do_check_guid_for_uri(place.uri, place.guid); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_title_change_saved() { + // First, add a visit for it. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"), + title: "original title", + visits: [ + new VisitInfo(), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + + // Now, make sure the empty string clears the title. + place.title = ""; + place.visits = [new VisitInfo()]; + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + do_check_title_for_uri(place.uri, null); + + // Then, change the title with visits. + place.title = "title change"; + place.visits = [new VisitInfo()]; + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + do_check_title_for_uri(place.uri, place.title); + + // Lastly, check that the title is cleared if we set it to null. + place.title = null; + place.visits = [new VisitInfo()]; + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + do_check_title_for_uri(place.uri, place.title); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_no_title_does_not_clear_title() { + const TITLE = "test title"; + // First, add a visit for it. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"), + title: TITLE, + visits: [ + new VisitInfo(), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + // Now, make sure that not specifying a title does not clear it. + delete place.title; + place.visits = [new VisitInfo()]; + placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + do_check_title_for_uri(place.uri, TITLE); + + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_title_change_notifies() { + // There are three cases to test. The first case is to make sure we do not + // get notified if we do not specify a title. + let place = { + uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"), + visits: [ + new VisitInfo(), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + + let silentObserver = + new TitleChangedObserver(place.uri, "DO NOT WANT", function() { + do_throw("unexpected callback!"); + }); + + PlacesUtils.history.addObserver(silentObserver, false); + let placesResult = yield promiseUpdatePlaces(place); + if (placesResult.errors.length > 0) { + do_throw("Unexpected error."); + } + + // The second case to test is that we get the notification when we add + // it for the first time. The first case will fail before our callback if it + // is busted, so we can do this now. + place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title"); + place.title = "title 1"; + function promiseTitleChangedObserver(aPlace) { + return new Promise((resolve, reject) => { + let callbackCount = 0; + let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() { + switch (++callbackCount) { + case 1: + // The third case to test is to make sure we get a notification when + // we change an existing place. + observer.expectedTitle = place.title = "title 2"; + place.visits = [new VisitInfo()]; + PlacesUtils.asyncHistory.updatePlaces(place); + break; + case 2: + PlacesUtils.history.removeObserver(silentObserver); + PlacesUtils.history.removeObserver(observer); + resolve(); + break; + } + }); + + PlacesUtils.history.addObserver(observer, false); + PlacesUtils.asyncHistory.updatePlaces(aPlace); + }); + } + + yield promiseTitleChangedObserver(place); + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_visit_notifies() { + // There are two observers we need to see for each visit. One is an + // nsINavHistoryObserver and the other is the uri-visit-saved observer topic. + let place = { + guid: "abcdefghijkl", + uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"), + visits: [ + new VisitInfo(), + ], + }; + do_check_false(yield promiseIsURIVisited(place.uri)); + + function promiseVisitObserver(aPlace) { + return new Promise((resolve, reject) => { + let callbackCount = 0; + let finisher = function() { + if (++callbackCount == 2) { + resolve(); + } + } + let visitObserver = new VisitObserver(place.uri, place.guid, + function(aVisitDate, + aTransitionType) { + let visit = place.visits[0]; + do_check_eq(visit.visitDate, aVisitDate); + do_check_eq(visit.transitionType, aTransitionType); + + PlacesUtils.history.removeObserver(visitObserver); + finisher(); + }); + PlacesUtils.history.addObserver(visitObserver, false); + let observer = function(aSubject, aTopic, aData) { + do_print("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); + do_check_true(aSubject instanceof Ci.nsIURI); + do_check_true(aSubject.equals(place.uri)); + + Services.obs.removeObserver(observer, URI_VISIT_SAVED); + finisher(); + }; + Services.obs.addObserver(observer, URI_VISIT_SAVED, false); + PlacesUtils.asyncHistory.updatePlaces(place); + }); + } + + yield promiseVisitObserver(place); + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +// test with empty mozIVisitInfoCallback object +add_task(function* test_callbacks_not_supplied() { + const URLS = [ + "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI + "http://mozilla.org/" // valid URI + ]; + let places = []; + URLS.forEach(function(url) { + try { + let place = { + uri: NetUtil.newURI(url), + title: "test for " + url, + visits: [ + new VisitInfo(), + ], + }; + places.push(place); + } + catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + // NetUtil.newURI() can throw if e.g. our app knows about imap:// + // but the account is not set up and so the URL is invalid for us. + // Note this in the log but ignore as it's not the subject of this test. + do_print("Could not construct URI for '" + url + "'; ignoring"); + } + }); + + PlacesUtils.asyncHistory.updatePlaces(places, {}); + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +// Test that we don't wrongly overwrite typed and hidden when adding new visits. +add_task(function* test_typed_hidden_not_overwritten() { + yield PlacesTestUtils.clearHistory(); + let places = [ + { uri: NetUtil.newURI("http://mozilla.org/"), + title: "test", + visits: [ + new VisitInfo(TRANSITION_TYPED), + new VisitInfo(TRANSITION_LINK) + ] + }, + { uri: NetUtil.newURI("http://mozilla.org/"), + title: "test", + visits: [ + new VisitInfo(TRANSITION_FRAMED_LINK) + ] + }, + ]; + yield promiseUpdatePlaces(places); + + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute( + "SELECT hidden, typed FROM moz_places WHERE url_hash = hash(:url) AND url = :url", + { url: "http://mozilla.org/" }); + Assert.equal(rows[0].getResultByName("typed"), 1, + "The page should be marked as typed"); + Assert.equal(rows[0].getResultByName("hidden"), 0, + "The page should be marked as not hidden"); +}); + +function run_test() +{ + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_async_in_batchmode.js b/toolkit/components/places/tests/unit/test_async_in_batchmode.js new file mode 100644 index 000000000..b39b26519 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_in_batchmode.js @@ -0,0 +1,55 @@ +// This is testing the frankenstein situation Sync forces Places into. +// Sync does runInBatchMode() and before the callback returns the Places async +// APIs are used (either by Sync itself, or by any other code in the system) +// As seen in bug 1197856 and bug 1190131. + +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +// This function "waits" for a promise to resolve by spinning a nested event +// loop. +function waitForPromise(promise) { + let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + let finalResult, finalException; + + promise.then(result => { + finalResult = result; + }, err => { + finalException = err; + }); + + // Keep waiting until our callback is triggered (unless the app is quitting). + while (!finalResult && !finalException) { + thread.processNextEvent(true); + } + if (finalException) { + throw finalException; + } + return finalResult; +} + +add_test(function() { + let testCompleted = false; + PlacesUtils.bookmarks.runInBatchMode({ + runBatched() { + // create a bookmark. + let info = { parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/" }; + let insertPromise = PlacesUtils.bookmarks.insert(info); + let bookmark = waitForPromise(insertPromise); + // Check we got a bookmark (bookmark creation failed completely in + // bug 1190131) + equal(bookmark.url, info.url); + // Check the promiseItemGuid and promiseItemId helpers - failure in these + // was the underlying reason for the failure. + let id = waitForPromise(PlacesUtils.promiseItemId(bookmark.guid)); + let guid = waitForPromise(PlacesUtils.promiseItemGuid(id)); + equal(guid, bookmark.guid, "id and guid round-tripped correctly"); + testCompleted = true; + } + }, null); + // make sure we tested what we think we tested. + ok(testCompleted); + run_next_test(); +}); diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js new file mode 100644 index 000000000..edc9abf87 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_transactions.js @@ -0,0 +1,1739 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const bmsvc = PlacesUtils.bookmarks; +const tagssvc = PlacesUtils.tagging; +const annosvc = PlacesUtils.annotations; +const PT = PlacesTransactions; +const rootGuid = PlacesUtils.bookmarks.rootGuid; + +Components.utils.importGlobalProperties(["URL"]); + +// Create and add bookmarks observer. +var observer = { + __proto__: NavBookmarkObserver.prototype, + + tagRelatedGuids: new Set(), + + reset: function () { + this.itemsAdded = new Map(); + this.itemsRemoved = new Map(); + this.itemsChanged = new Map(); + this.itemsMoved = new Map(); + this.beginUpdateBatch = false; + this.endUpdateBatch = false; + }, + + onBeginUpdateBatch: function () { + this.beginUpdateBatch = true; + }, + + onEndUpdateBatch: function () { + this.endUpdateBatch = true; + }, + + onItemAdded: + function (aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, + aGuid, aParentGuid) { + // Ignore tag items. + if (aParentId == PlacesUtils.tagsFolderId || + (aParentId != PlacesUtils.placesRootId && + bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) { + this.tagRelatedGuids.add(aGuid); + return; + } + + this.itemsAdded.set(aGuid, { itemId: aItemId + , parentGuid: aParentGuid + , index: aIndex + , itemType: aItemType + , title: aTitle + , url: aURI }); + }, + + onItemRemoved: + function (aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) { + if (this.tagRelatedGuids.has(aGuid)) + return; + + this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid + , index: aIndex + , itemType: aItemType }); + }, + + onItemChanged: + function (aItemId, aProperty, aIsAnnoProperty, aNewValue, aLastModified, + aItemType, aParentId, aGuid, aParentGuid) { + if (this.tagRelatedGuids.has(aGuid)) + return; + + let changesForGuid = this.itemsChanged.get(aGuid); + if (changesForGuid === undefined) { + changesForGuid = new Map(); + this.itemsChanged.set(aGuid, changesForGuid); + } + + let newValue = aNewValue; + if (aIsAnnoProperty) { + if (annosvc.itemHasAnnotation(aItemId, aProperty)) + newValue = annosvc.getItemAnnotation(aItemId, aProperty); + else + newValue = null; + } + let change = { isAnnoProperty: aIsAnnoProperty + , newValue: newValue + , lastModified: aLastModified + , itemType: aItemType }; + changesForGuid.set(aProperty, change); + }, + + onItemVisited: () => {}, + + onItemMoved: + function (aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType, + aGuid, aOldParentGuid, aNewParentGuid) { + this.itemsMoved.set(aGuid, { oldParentGuid: aOldParentGuid + , oldIndex: aOldIndex + , newParentGuid: aNewParentGuid + , newIndex: aNewIndex + , itemType: aItemType }); + } +}; +observer.reset(); + +// index at which items should begin +var bmStartIndex = 0; + +function run_test() { + bmsvc.addObserver(observer, false); + do_register_cleanup(function () { + bmsvc.removeObserver(observer); + }); + + run_next_test(); +} + +function sanityCheckTransactionHistory() { + do_check_true(PT.undoPosition <= PT.length); + + let check_entry_throws = f => { + try { + f(); + do_throw("PT.entry should throw for invalid input"); + } catch (ex) {} + }; + check_entry_throws( () => PT.entry(-1) ); + check_entry_throws( () => PT.entry({}) ); + check_entry_throws( () => PT.entry(PT.length) ); + + if (PT.undoPosition < PT.length) + do_check_eq(PT.topUndoEntry, PT.entry(PT.undoPosition)); + else + do_check_null(PT.topUndoEntry); + if (PT.undoPosition > 0) + do_check_eq(PT.topRedoEntry, PT.entry(PT.undoPosition - 1)); + else + do_check_null(PT.topRedoEntry); +} + +function getTransactionsHistoryState() { + let history = []; + for (let i = 0; i < PT.length; i++) { + history.push(PT.entry(i)); + } + return [history, PT.undoPosition]; +} + +function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) { + // ensureUndoState is called in various places during this test, so it's + // a good places to sanity-check the transaction-history APIs in all + // cases. + sanityCheckTransactionHistory(); + + let [actualEntries, actualUndoPosition] = getTransactionsHistoryState(); + do_check_eq(actualEntries.length, aExpectedEntries.length); + do_check_eq(actualUndoPosition, aExpectedUndoPosition); + + function checkEqualEntries(aExpectedEntry, aActualEntry) { + do_check_eq(aExpectedEntry.length, aActualEntry.length); + aExpectedEntry.forEach( (t, i) => do_check_eq(t, aActualEntry[i]) ); + } + aExpectedEntries.forEach( (e, i) => checkEqualEntries(e, actualEntries[i]) ); +} + +function ensureItemsAdded(...items) { + Assert.equal(observer.itemsAdded.size, items.length); + for (let item of items) { + Assert.ok(observer.itemsAdded.has(item.guid)); + let info = observer.itemsAdded.get(item.guid); + Assert.equal(info.parentGuid, item.parentGuid); + for (let propName of ["title", "index", "itemType"]) { + if (propName in item) + Assert.equal(info[propName], item[propName]); + } + if ("url" in item) + Assert.ok(info.url.equals(item.url)); + } +} + +function ensureItemsRemoved(...items) { + Assert.equal(observer.itemsRemoved.size, items.length); + for (let item of items) { + // We accept both guids and full info object here. + if (typeof(item) == "string") { + Assert.ok(observer.itemsRemoved.has(item)); + } + else { + Assert.ok(observer.itemsRemoved.has(item.guid)); + let info = observer.itemsRemoved.get(item.guid); + Assert.equal(info.parentGuid, item.parentGuid); + if ("index" in item) + Assert.equal(info.index, item.index); + } + } +} + +function ensureItemsChanged(...items) { + for (let item of items) { + do_check_true(observer.itemsChanged.has(item.guid)); + let changes = observer.itemsChanged.get(item.guid); + do_check_true(changes.has(item.property)); + let info = changes.get(item.property); + do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty)); + do_check_eq(info.newValue, item.newValue); + if ("url" in item) + do_check_true(item.url.equals(info.url)); + } +} + +function ensureAnnotationsSet(aGuid, aAnnos) { + do_check_true(observer.itemsChanged.has(aGuid)); + let changes = observer.itemsChanged.get(aGuid); + for (let anno of aAnnos) { + do_check_true(changes.has(anno.name)); + let changeInfo = changes.get(anno.name); + do_check_true(changeInfo.isAnnoProperty); + do_check_eq(changeInfo.newValue, anno.value); + } +} + +function ensureItemsMoved(...items) { + do_check_true(observer.itemsMoved.size, items.length); + for (let item of items) { + do_check_true(observer.itemsMoved.has(item.guid)); + let info = observer.itemsMoved.get(item.guid); + do_check_eq(info.oldParentGuid, item.oldParentGuid); + do_check_eq(info.oldIndex, item.oldIndex); + do_check_eq(info.newParentGuid, item.newParentGuid); + do_check_eq(info.newIndex, item.newIndex); + } +} + +function ensureTimestampsUpdated(aGuid, aCheckDateAdded = false) { + do_check_true(observer.itemsChanged.has(aGuid)); + let changes = observer.itemsChanged.get(aGuid); + if (aCheckDateAdded) + do_check_true(changes.has("dateAdded")) + do_check_true(changes.has("lastModified")); +} + +function ensureTagsForURI(aURI, aTags) { + let tagsSet = tagssvc.getTagsForURI(aURI); + do_check_eq(tagsSet.length, aTags.length); + do_check_true(aTags.every( t => tagsSet.includes(t))); +} + +function createTestFolderInfo(aTitle = "Test Folder") { + return { parentGuid: rootGuid, title: "Test Folder" }; +} + +function isLivemarkTree(aTree) { + return !!aTree.annos && + aTree.annos.some( a => a.name == PlacesUtils.LMANNO_FEEDURI ); +} + +function* ensureLivemarkCreatedByAddLivemark(aLivemarkGuid) { + // This throws otherwise. + yield PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid }); +} + +// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the +// same. +// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set. +function* ensureEqualBookmarksTrees(aOriginal, + aNew, + aIsRestoredItem = true, + aCheckParentAndPosition = false) { + // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both + // ours and the one at deepEqual). This is fine for us because ids are not + // restored by Redo. + if (aIsRestoredItem) { + Assert.deepEqual(aOriginal, aNew); + if (isLivemarkTree(aNew)) + yield ensureLivemarkCreatedByAddLivemark(aNew.guid); + return; + } + + for (let property of Object.keys(aOriginal)) { + if (property == "children") { + Assert.equal(aOriginal.children.length, aNew.children.length); + for (let i = 0; i < aOriginal.children.length; i++) { + yield ensureEqualBookmarksTrees(aOriginal.children[i], + aNew.children[i], + false, + true); + } + } + else if (property == "guid") { + // guid shouldn't be copied if the item was not restored. + Assert.notEqual(aOriginal.guid, aNew.guid); + } + else if (property == "dateAdded") { + // dateAdded shouldn't be copied if the item was not restored. + Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded)); + } + else if (property == "lastModified") { + // same same, except for the never-changed case + if (!aOriginal.lastModified) + Assert.ok(!aNew.lastModified); + else + Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified)); + } + else if (aCheckParentAndPosition || + (property != "parentGuid" && property != "index")) { + Assert.deepEqual(aOriginal[property], aNew[property]); + } + } + + if (isLivemarkTree(aNew)) + yield ensureLivemarkCreatedByAddLivemark(aNew.guid); +} + +function* ensureBookmarksTreeRestoredCorrectly(...aOriginalBookmarksTrees) { + for (let originalTree of aOriginalBookmarksTrees) { + let restoredTree = + yield PlacesUtils.promiseBookmarksTree(originalTree.guid); + yield ensureEqualBookmarksTrees(originalTree, restoredTree); + } +} + +function* ensureNonExistent(...aGuids) { + for (let guid of aGuids) { + Assert.strictEqual((yield PlacesUtils.promiseBookmarksTree(guid)), null); + } +} + +add_task(function* test_recycled_transactions() { + function* ensureTransactThrowsFor(aTransaction) { + let [txns, undoPosition] = getTransactionsHistoryState(); + try { + yield aTransaction.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } + catch (ex) { } + ensureUndoState(txns, undoPosition); + } + + let txn_a = PT.NewFolder(createTestFolderInfo()); + yield txn_a.transact(); + ensureUndoState([[txn_a]], 0); + yield ensureTransactThrowsFor(txn_a); + + yield PT.undo(); + ensureUndoState([[txn_a]], 1); + ensureTransactThrowsFor(txn_a); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); + ensureTransactThrowsFor(txn_a); + + let txn_b = PT.NewFolder(createTestFolderInfo()); + yield PT.batch(function* () { + try { + yield txn_a.transact(); + do_throw("Shouldn't be able to use the same transaction twice"); + } + catch (ex) { } + ensureUndoState(); + yield txn_b.transact(); + }); + ensureUndoState([[txn_b]], 0); + + yield PT.undo(); + ensureUndoState([[txn_b]], 1); + ensureTransactThrowsFor(txn_a); + ensureTransactThrowsFor(txn_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); + observer.reset(); +}); + +add_task(function* test_new_folder_with_annotation() { + const ANNO = { name: "TestAnno", value: "TestValue" }; + let folder_info = createTestFolderInfo(); + folder_info.index = bmStartIndex; + folder_info.annotations = [ANNO]; + ensureUndoState(); + let txn = PT.NewFolder(folder_info); + folder_info.guid = yield txn.transact(); + let ensureDo = function* (aRedo = false) { + ensureUndoState([[txn]], 0); + yield ensureItemsAdded(folder_info); + ensureAnnotationsSet(folder_info.guid, [ANNO]); + if (aRedo) + ensureTimestampsUpdated(folder_info.guid, true); + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ guid: folder_info.guid + , parentGuid: folder_info.parentGuid + , index: bmStartIndex }); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + yield ensureUndo(); + yield PT.redo(); + yield ensureDo(true); + yield PT.undo(); + ensureUndo(); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_new_bookmark() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test_create_item.com") + , index: bmStartIndex + , title: "Test creating an item" }; + + ensureUndoState(); + let txn = PT.NewBookmark(bm_info); + bm_info.guid = yield txn.transact(); + + let ensureDo = function* (aRedo = false) { + ensureUndoState([[txn]], 0); + yield ensureItemsAdded(bm_info); + if (aRedo) + ensureTimestampsUpdated(bm_info.guid, true); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState([[txn]], 1); + ensureItemsRemoved({ guid: bm_info.guid + , parentGuid: bm_info.parentGuid + , index: bmStartIndex }); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(true); + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_merge_create_folder_and_item() { + let folder_info = createTestFolderInfo(); + let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com") + , title: "Test Bookmark" + , index: bmStartIndex }; + + let [folderTxnResult, bkmTxnResult] = yield PT.batch(function* () { + let folderTxn = PT.NewFolder(folder_info); + folder_info.guid = bm_info.parentGuid = yield folderTxn.transact(); + let bkmTxn = PT.NewBookmark(bm_info); + bm_info.guid = yield bkmTxn.transact(); + return [folderTxn, bkmTxn]; + }); + + let ensureDo = function* () { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 0); + yield ensureItemsAdded(folder_info, bm_info); + observer.reset(); + }; + + let ensureUndo = () => { + ensureUndoState([[bkmTxnResult, folderTxnResult]], 1); + ensureItemsRemoved(folder_info, bm_info); + observer.reset(); + }; + + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + yield ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_move_items_to_folder() { + let folder_a_info = createTestFolderInfo("Folder A"); + let bkm_a_info = { url: new URL("http://test_move_items.com") + , title: "Bookmark A" }; + let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com") + , title: "Bookmark B" }; + + // Test moving items within the same folder. + let [folder_a_txn_result, bkm_a_txn_result, bkm_b_txn_result] = yield PT.batch(function* () { + let folder_a_txn = PT.NewFolder(folder_a_info); + + folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid = + yield folder_a_txn.transact(); + let bkm_a_txn = PT.NewBookmark(bkm_a_info); + bkm_a_info.guid = yield bkm_a_txn.transact(); + let bkm_b_txn = PT.NewBookmark(bkm_b_info); + bkm_b_info.guid = yield bkm_b_txn.transact(); + return [folder_a_txn, bkm_a_txn, bkm_b_txn]; + }); + + ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + + let moveTxn = PT.Move({ guid: bkm_a_info.guid + , newParentGuid: folder_a_info.guid }); + yield moveTxn.transact(); + + let ensureDo = () => { + ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 0 + , newIndex: 1 }); + observer.reset(); + }; + let ensureUndo = () => { + ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 1); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 1 + , newIndex: 0 }); + observer.reset(); + }; + + ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + ensureDo(); + yield PT.undo(); + ensureUndo(); + + yield PT.clearTransactionsHistory(false, true); + ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0); + + // Test moving items between folders. + let folder_b_info = createTestFolderInfo("Folder B"); + let folder_b_txn = PT.NewFolder(folder_b_info); + folder_b_info.guid = yield folder_b_txn.transact(); + ensureUndoState([ [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0); + + moveTxn = PT.Move({ guid: bkm_a_info.guid + , newParentGuid: folder_b_info.guid + , newIndex: bmsvc.DEFAULT_INDEX }); + yield moveTxn.transact(); + + ensureDo = () => { + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_a_info.guid + , newParentGuid: folder_b_info.guid + , oldIndex: 0 + , newIndex: 0 }); + observer.reset(); + }; + ensureUndo = () => { + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 1); + ensureItemsMoved({ guid: bkm_a_info.guid + , oldParentGuid: folder_b_info.guid + , newParentGuid: folder_a_info.guid + , oldIndex: 0 + , newIndex: 0 }); + observer.reset(); + }; + + ensureDo(); + yield PT.undo(); + ensureUndo(); + yield PT.redo(); + ensureDo(); + yield PT.undo(); + ensureUndo(); + + // Clean up + yield PT.undo(); // folder_b_txn + yield PT.undo(); // folder_a_txn + the bookmarks; + do_check_eq(observer.itemsRemoved.size, 4); + ensureUndoState([ [moveTxn] + , [folder_b_txn] + , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 3); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_remove_folder() { + let folder_level_1_info = createTestFolderInfo("Folder Level 1"); + let folder_level_2_info = { title: "Folder Level 2" }; + let [folder_level_1_txn_result, + folder_level_2_txn_result] = yield PT.batch(function* () { + let folder_level_1_txn = PT.NewFolder(folder_level_1_info); + folder_level_1_info.guid = yield folder_level_1_txn.transact(); + folder_level_2_info.parentGuid = folder_level_1_info.guid; + let folder_level_2_txn = PT.NewFolder(folder_level_2_info); + folder_level_2_info.guid = yield folder_level_2_txn.transact(); + return [folder_level_1_txn, folder_level_2_txn]; + }); + + ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]); + yield ensureItemsAdded(folder_level_1_info, folder_level_2_info); + observer.reset(); + + let remove_folder_2_txn = PT.Remove(folder_level_2_info); + yield remove_folder_2_txn.transact(); + + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + + // Undo Remove "Folder Level 2" + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Redo Remove "Folder Level 2" + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo it again + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Undo the creation of both folders + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2); + yield ensureItemsRemoved(folder_level_2_info, folder_level_1_info); + observer.reset(); + + // Redo the creation of both folders + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_1_info, folder_level_2_info); + ensureTimestampsUpdated(folder_level_1_info.guid, true); + ensureTimestampsUpdated(folder_level_2_info.guid, true); + observer.reset(); + + // Redo Remove "Folder Level 2" + yield PT.redo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ]); + yield ensureItemsRemoved(folder_level_2_info); + observer.reset(); + + // Undo everything one last time + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1); + yield ensureItemsAdded(folder_level_2_info); + observer.reset(); + + yield PT.undo(); + ensureUndoState([ [remove_folder_2_txn] + , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2); + yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info); + observer.reset(); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_add_and_remove_bookmarks_with_additional_info() { + const testURI = NetUtil.newURI("http://add.remove.tag"); + const TAG_1 = "TestTag1"; + const TAG_2 = "TestTag2"; + const KEYWORD = "test_keyword"; + const POST_DATA = "post_data"; + const ANNO = { name: "TestAnno", value: "TestAnnoValue" }; + + let folder_info = createTestFolderInfo(); + folder_info.guid = yield PT.NewFolder(folder_info).transact(); + let ensureTags = ensureTagsForURI.bind(null, testURI); + + // Check that the NewBookmark transaction preserves tags. + observer.reset(); + let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] }; + b1_info.guid = yield PT.NewBookmark(b1_info).transact(); + ensureTags([TAG_1]); + yield PT.undo(); + ensureTags([]); + + observer.reset(); + yield PT.redo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + // Check if the Remove transaction removes and restores tags of children + // correctly. + yield PT.Remove(folder_info.guid).transact(); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + yield PT.redo(); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureTimestampsUpdated(b1_info.guid, true); + ensureTags([TAG_1]); + + // * Check that no-op tagging (the uri is already tagged with TAG_1) is + // also a no-op on undo. + // * Test the "keyword" property of the NewBookmark transaction. + observer.reset(); + let b2_info = { parentGuid: folder_info.guid + , url: testURI, tags: [TAG_1, TAG_2] + , keyword: KEYWORD + , postData: POST_DATA + , annotations: [ANNO] }; + b2_info.guid = yield PT.NewBookmark(b2_info).transact(); + let b2_post_creation_changes = [ + { guid: b2_info.guid + , isAnnoProperty: true + , property: ANNO.name + , newValue: ANNO.value }, + { guid: b2_info.guid + , property: "keyword" + , newValue: KEYWORD } ]; + ensureItemsChanged(...b2_post_creation_changes); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.undo(); + yield ensureItemsRemoved(b2_info); + ensureTags([TAG_1]); + + // Check if Remove correctly restores keywords, tags and annotations. + // Since both bookmarks share the same uri, they also share the keyword that + // is not removed along with one of the bookmarks. + observer.reset(); + yield PT.redo(); + ensureItemsChanged({ guid: b2_info.guid + , isAnnoProperty: true + , property: ANNO.name + , newValue: ANNO.value }); + ensureTags([TAG_1, TAG_2]); + + // Test Remove for multiple items. + observer.reset(); + yield PT.Remove(b1_info.guid).transact(); + yield PT.Remove(b2_info.guid).transact(); + yield PT.Remove(folder_info.guid).transact(); + yield ensureItemsRemoved(b1_info, b2_info, folder_info); + ensureTags([]); + // There is no keyword removal notification cause all bookmarks are removed + // before the keyword itself, so there's no one to notify. + let entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null, "keyword has been removed"); + + observer.reset(); + yield PT.undo(); + yield ensureItemsAdded(folder_info); + ensureTags([]); + + observer.reset(); + yield PT.undo(); + ensureItemsChanged(...b2_post_creation_changes); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.undo(); + yield ensureItemsAdded(b1_info); + ensureTags([TAG_1, TAG_2]); + + // The redo calls below cleanup everything we did. + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(b1_info); + ensureTags([TAG_1, TAG_2]); + + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(b2_info); + ensureTags([]); + + observer.reset(); + yield PT.redo(); + yield ensureItemsRemoved(folder_info); + ensureTags([]); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_creating_and_removing_a_separator() { + let folder_info = createTestFolderInfo(); + let separator_info = {}; + let undoEntries = []; + + observer.reset(); + let create_txns = yield PT.batch(function* () { + let folder_txn = PT.NewFolder(folder_info); + folder_info.guid = separator_info.parentGuid = yield folder_txn.transact(); + let separator_txn = PT.NewSeparator(separator_info); + separator_info.guid = yield separator_txn.transact(); + return [separator_txn, folder_txn]; + }); + undoEntries.unshift(create_txns); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + yield PT.redo(); + ensureUndoState(undoEntries, 0); + ensureItemsAdded(folder_info, separator_info); + + observer.reset(); + let remove_sep_txn = PT.Remove(separator_info); + yield remove_sep_txn.transact(); + undoEntries.unshift([remove_sep_txn]); + ensureUndoState(undoEntries, 0); + ensureItemsRemoved(separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(separator_info); + + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 2); + ensureItemsRemoved(folder_info, separator_info); + + observer.reset(); + yield PT.redo(); + ensureUndoState(undoEntries, 1); + ensureItemsAdded(folder_info, separator_info); + + // Clear redo entries and check that |redo| does nothing + observer.reset(); + yield PT.clearTransactionsHistory(false, true); + undoEntries.shift(); + ensureUndoState(undoEntries, 0); + yield PT.redo(); + ensureItemsAdded(); + ensureItemsRemoved(); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureUndoState(undoEntries, 1); + ensureItemsRemoved(folder_info, separator_info); + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_add_and_remove_livemark() { + let createLivemarkTxn = PT.NewLivemark( + { feedUrl: NetUtil.newURI("http://test.remove.livemark") + , parentGuid: rootGuid + , title: "Test Remove Livemark" }); + let guid = yield createLivemarkTxn.transact(); + let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid); + Assert.ok(originalInfo); + yield ensureLivemarkCreatedByAddLivemark(guid); + + let removeTxn = PT.Remove(guid); + yield removeTxn.transact(); + yield ensureNonExistent(guid); + function* undo() { + ensureUndoState([[removeTxn], [createLivemarkTxn]], 0); + yield PT.undo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 1); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.undo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 2); + yield ensureNonExistent(guid); + } + function* redo() { + ensureUndoState([[removeTxn], [createLivemarkTxn]], 2); + yield PT.redo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 1); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.redo(); + ensureUndoState([[removeTxn], [createLivemarkTxn]], 0); + yield ensureNonExistent(guid); + } + + yield undo(); + yield redo(); + yield undo(); + yield redo(); + + // Cleanup + yield undo(); + observer.reset(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_edit_title() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test_create_item.com") + , title: "Original Title" }; + + function ensureTitleChange(aCurrentTitle) { + ensureItemsChanged({ guid: bm_info.guid + , property: "title" + , newValue: aCurrentTitle}); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact(); + ensureTitleChange("New Title"); + + observer.reset(); + yield PT.undo(); + ensureTitleChange("Original Title"); + + observer.reset(); + yield PT.redo(); + ensureTitleChange("New Title"); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureTitleChange("Original Title"); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_url() { + let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/"); + let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/"); + let bm_info = { parentGuid: rootGuid, url: oldURI, tags: ["TestTag"] }; + function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) { + ensureItemsChanged({ guid: bm_info.guid + , property: "uri" + , newValue: aPostChangeURI.spec }); + ensureTagsForURI(aPostChangeURI, bm_info.tags); + ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + ensureTagsForURI(oldURI, bm_info.tags); + + // When there's a single bookmark for the same url, tags should be moved. + observer.reset(); + yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + observer.reset(); + yield PT.redo(); + ensureURIAndTags(oldURI, newURI, false); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, false); + + // When there're multiple bookmarks for the same url, tags should be copied. + let bm2_info = Object.create(bm_info); + bm2_info.guid = yield PT.NewBookmark(bm2_info).transact(); + let bm3_info = Object.create(bm_info); + bm3_info.url = newURI; + bm3_info.guid = yield PT.NewBookmark(bm3_info).transact(); + + observer.reset(); + yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact(); + ensureURIAndTags(oldURI, newURI, true); + + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + + observer.reset(); + yield PT.redo(); + ensureURIAndTags(oldURI, newURI, true); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureURIAndTags(newURI, oldURI, true); + yield PT.undo(); + yield PT.undo(); + yield PT.undo(); + ensureItemsRemoved(bm3_info, bm2_info, bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_keyword() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test.edit.keyword") }; + const KEYWORD = "test_keyword"; + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "") { + ensureItemsChanged({ guid: bm_info.guid + , property: "keyword" + , newValue: aCurrentKeyword }); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact(); + ensureKeywordChange(KEYWORD); + let entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData"); + + observer.reset(); + yield PT.undo(); + ensureKeywordChange(); + entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry, null); + + observer.reset(); + yield PT.redo(); + ensureKeywordChange(KEYWORD); + entry = yield PlacesUtils.keywords.fetch(KEYWORD); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData"); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureKeywordChange(); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_edit_specific_keyword() { + let bm_info = { parentGuid: rootGuid + , url: NetUtil.newURI("http://test.edit.keyword") }; + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") { + ensureItemsChanged({ guid: bm_info.guid + , property: "keyword" + , newValue: aCurrentKeyword + }); + } + + yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" }); + yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" }); + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact(); + ensureKeywordChange("keyword", "kw2"); + let entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + observer.reset(); + yield PT.undo(); + ensureKeywordChange("kw2", "keyword"); + entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry, null); + + observer.reset(); + yield PT.redo(); + ensureKeywordChange("keyword", "kw2"); + entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData1"); + entry = yield PlacesUtils.keywords.fetch("keyword"); + Assert.equal(entry.url.href, bm_info.url.spec); + Assert.equal(entry.postData, "postData2"); + entry = yield PlacesUtils.keywords.fetch("kw2"); + Assert.equal(entry, null); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureKeywordChange("kw2"); + yield PT.undo(); + ensureItemsRemoved(bm_info); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_tag_uri() { + // This also tests passing uri specs. + let bm_info_a = { url: "http://bookmarked.uri" + , parentGuid: rootGuid }; + let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri") + , parentGuid: rootGuid }; + let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri"); + + function* promiseIsBookmarked(aURI) { + let deferred = Promise.defer(); + PlacesUtils.asyncGetBookmarkIds(aURI, ids => { + deferred.resolve(ids.length > 0); + }); + return deferred.promise; + } + + yield PT.batch(function* () { + bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact(); + bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact(); + }); + + function* doTest(aInfo) { + let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + + let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url; + urls = urls.map(ensureURI); + + let tagWillAlsoBookmark = new Set(); + for (let url of urls) { + if (!(yield promiseIsBookmarked(url))) { + tagWillAlsoBookmark.add(url); + } + } + + function* ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, tags); + Assert.ok(yield promiseIsBookmarked(url)); + } + } + function* ensureTagsUnset() { + for (let url of urls) { + ensureTagsForURI(url, []); + if (tagWillAlsoBookmark.has(url)) + Assert.ok(!(yield promiseIsBookmarked(url))); + else + Assert.ok(yield promiseIsBookmarked(url)); + } + } + + yield PT.Tag(aInfo).transact(); + yield ensureTagsSet(); + yield PT.undo(); + yield ensureTagsUnset(); + yield PT.redo(); + yield ensureTagsSet(); + yield PT.undo(); + yield ensureTagsUnset(); + } + + yield doTest({ url: bm_info_a.url, tags: ["MyTag"] }); + yield doTest({ urls: [bm_info_a.url], tag: "MyTag" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] }); + yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" }); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_untag_uri() { + let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri") + , parentGuid: rootGuid + , tags: ["A", "B"] }; + let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri") + , parentGuid: rootGuid + , tag: "B" }; + + yield PT.batch(function* () { + bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact(); + ensureTagsForURI(bm_info_a.url, bm_info_a.tags); + bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact(); + ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]); + }); + + function* doTest(aInfo) { + let urls, tagsRemoved; + if (aInfo instanceof Ci.nsIURI) { + urls = [aInfo]; + tagsRemoved = []; + } + else if (Array.isArray(aInfo)) { + urls = aInfo; + tagsRemoved = []; + } + else { + urls = "url" in aInfo ? [aInfo.url] : aInfo.urls; + tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags; + } + + let preRemovalTags = new Map(); + for (let url of urls) { + preRemovalTags.set(url, tagssvc.getTagsForURI(url)); + } + + function ensureTagsSet() { + for (let url of urls) { + ensureTagsForURI(url, preRemovalTags.get(url)); + } + } + function ensureTagsUnset() { + for (let url of urls) { + let expectedTags = tagsRemoved.length == 0 ? + [] : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag)); + ensureTagsForURI(url, expectedTags); + } + } + + yield PT.Untag(aInfo).transact(); + yield ensureTagsUnset(); + yield PT.undo(); + yield ensureTagsSet(); + yield PT.redo(); + yield ensureTagsUnset(); + yield PT.undo(); + yield ensureTagsSet(); + } + + yield doTest(bm_info_a); + yield doTest(bm_info_b); + yield doTest(bm_info_a.url); + yield doTest(bm_info_b.url); + yield doTest([bm_info_a.url, bm_info_b.url]); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" }); + yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] }); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureItemsRemoved(bm_info_a, bm_info_b); + + yield PT.clearTransactionsHistory(); + ensureUndoState(); +}); + +add_task(function* test_annotate() { + let bm_info = { url: NetUtil.newURI("http://test.item.annotation") + , parentGuid: rootGuid }; + let anno_info = { name: "TestAnno", value: "TestValue" }; + function ensureAnnoState(aSet) { + ensureAnnotationsSet(bm_info.guid, + [{ name: anno_info.name + , value: aSet ? anno_info.value : null }]); + } + + bm_info.guid = yield PT.NewBookmark(bm_info).transact(); + + observer.reset(); + yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact(); + ensureAnnoState(true); + + observer.reset(); + yield PT.undo(); + ensureAnnoState(false); + + observer.reset(); + yield PT.redo(); + ensureAnnoState(true); + + // Test removing the annotation by not passing the |value| property. + observer.reset(); + yield PT.Annotate({ guid: bm_info.guid, + annotation: { name: anno_info.name }}).transact(); + ensureAnnoState(false); + + observer.reset(); + yield PT.undo(); + ensureAnnoState(true); + + observer.reset(); + yield PT.redo(); + ensureAnnoState(false); + + // Cleanup + yield PT.undo(); + observer.reset(); +}); + +add_task(function* test_annotate_multiple() { + let guid = yield PT.NewFolder(createTestFolderInfo()).transact(); + let itemId = yield PlacesUtils.promiseItemId(guid); + + function AnnoObj(aName, aValue) { + this.name = aName; + this.value = aValue; + this.flags = 0; + this.expires = Ci.nsIAnnotationService.EXPIRE_NEVER; + } + + function annos(a = null, b = null) { + return [new AnnoObj("A", a), new AnnoObj("B", b)]; + } + + function verifyAnnoValues(a = null, b = null) { + let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId); + let expectedAnnos = []; + if (a !== null) + expectedAnnos.push(new AnnoObj("A", a)); + if (b !== null) + expectedAnnos.push(new AnnoObj("B", b)); + + Assert.deepEqual(currentAnnos, expectedAnnos); + } + + yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact(); + verifyAnnoValues(1, 2); + yield PT.undo(); + verifyAnnoValues(); + yield PT.redo(); + verifyAnnoValues(1, 2); + + yield PT.Annotate({ guid: guid + , annotation: { name: "A" } }).transact(); + verifyAnnoValues(null, 2); + + yield PT.Annotate({ guid: guid + , annotation: { name: "B", value: 0 } }).transact(); + verifyAnnoValues(null, 0); + yield PT.undo(); + verifyAnnoValues(null, 2); + yield PT.redo(); + verifyAnnoValues(null, 0); + yield PT.undo(); + verifyAnnoValues(null, 2); + yield PT.undo(); + verifyAnnoValues(1, 2); + yield PT.undo(); + verifyAnnoValues(); + + // Cleanup + yield PT.undo(); + observer.reset(); +}); + +add_task(function* test_sort_folder_by_name() { + let folder_info = createTestFolderInfo(); + + let url = NetUtil.newURI("http://sort.by.name/"); + let preSep = ["3", "2", "1"].map(i => ({ title: i, url })); + let sep = {}; + let postSep = ["c", "b", "a"].map(l => ({ title: l, url })); + let originalOrder = [...preSep, sep, ...postSep]; + let sortedOrder = [...preSep.slice(0).reverse(), + sep, + ...postSep.slice(0).reverse()]; + yield PT.batch(function* () { + folder_info.guid = yield PT.NewFolder(folder_info).transact(); + for (let info of originalOrder) { + info.parentGuid = folder_info.guid; + info.guid = yield info == sep ? + PT.NewSeparator(info).transact() : + PT.NewBookmark(info).transact(); + } + }); + + let folderId = yield PlacesUtils.promiseItemId(folder_info.guid); + let folderContainer = PlacesUtils.getFolderContents(folderId).root; + function ensureOrder(aOrder) { + for (let i = 0; i < folderContainer.childCount; i++) { + do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid); + } + } + + ensureOrder(originalOrder); + yield PT.SortByName(folder_info.guid).transact(); + ensureOrder(sortedOrder); + yield PT.undo(); + ensureOrder(originalOrder); + yield PT.redo(); + ensureOrder(sortedOrder); + + // Cleanup + observer.reset(); + yield PT.undo(); + ensureOrder(originalOrder); + yield PT.undo(); + ensureItemsRemoved(...originalOrder, folder_info); +}); + +add_task(function* test_livemark_txns() { + let livemark_info = + { feedUrl: NetUtil.newURI("http://test.feed.uri") + , parentGuid: rootGuid + , title: "Test Livemark" }; + function ensureLivemarkAdded() { + ensureItemsAdded({ guid: livemark_info.guid + , title: livemark_info.title + , parentGuid: livemark_info.parentGuid + , itemType: bmsvc.TYPE_FOLDER }); + let annos = [{ name: PlacesUtils.LMANNO_FEEDURI + , value: livemark_info.feedUrl.spec }]; + if ("siteUrl" in livemark_info) { + annos.push({ name: PlacesUtils.LMANNO_SITEURI + , value: livemark_info.siteUrl.spec }); + } + ensureAnnotationsSet(livemark_info.guid, annos); + } + function ensureLivemarkRemoved() { + ensureItemsRemoved({ guid: livemark_info.guid + , parentGuid: livemark_info.parentGuid }); + } + + function* _testDoUndoRedoUndo() { + observer.reset(); + livemark_info.guid = yield PT.NewLivemark(livemark_info).transact(); + ensureLivemarkAdded(); + + observer.reset(); + yield PT.undo(); + ensureLivemarkRemoved(); + + observer.reset(); + yield PT.redo(); + ensureLivemarkAdded(); + + yield PT.undo(); + ensureLivemarkRemoved(); + } + + yield* _testDoUndoRedoUndo() + livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri"); + yield* _testDoUndoRedoUndo(); + + // Cleanup + observer.reset(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_copy() { + function* duplicate_and_test(aOriginalGuid) { + let txn = PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }); + yield duplicateGuid = yield txn.transact(); + let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid); + let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid); + yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false); + + function* redo() { + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(originalInfo); + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(duplicateInfo); + } + function* undo() { + yield PT.undo(); + // also undo the original item addition. + yield PT.undo(); + yield ensureNonExistent(aOriginalGuid, duplicateGuid); + } + + yield undo(); + yield redo(); + yield undo(); + yield redo(); + + // Cleanup. This also remove the original item. + yield PT.undo(); + observer.reset(); + yield PT.clearTransactionsHistory(); + } + + // Test duplicating leafs (bookmark, separator, empty folder) + PT.NewBookmark({ url: new URL("http://test.item.duplicate") + , parentGuid: rootGuid + , annos: [{ name: "Anno", value: "AnnoValue"}] }); + let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 }); + let livemarkTxn = PT.NewLivemark( + { feedUrl: new URL("http://test.feed.uri") + , parentGuid: rootGuid + , title: "Test Livemark", index: 1 }); + let emptyFolderTxn = PT.NewFolder(createTestFolderInfo()); + for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) { + let guid = yield txn.transact(); + yield duplicate_and_test(guid); + } + + // Test duplicating a folder having some contents. + let filledFolderGuid = yield PT.batch(function *() { + let folderGuid = yield PT.NewFolder(createTestFolderInfo()).transact(); + let nestedFolderGuid = + yield PT.NewFolder({ parentGuid: folderGuid + , title: "Nested Folder" }).transact(); + // Insert a bookmark under the nested folder. + yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark") + , parentGuid: nestedFolderGuid }).transact(); + // Insert a separator below the nested folder + yield PT.NewSeparator({ parentGuid: folderGuid }).transact(); + // And another bookmark. + yield PT.NewBookmark({ url: new URL("http://nested.bookmark") + , parentGuid: folderGuid }).transact(); + return folderGuid; + }); + + yield duplicate_and_test(filledFolderGuid); + + // Cleanup + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_array_input_for_batch() { + let folderTxn = PT.NewFolder(createTestFolderInfo()); + let folderGuid = yield folderTxn.transact(); + + let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid }); + let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid }); + yield PT.batch([sep1_txn, sep2_txn]); + ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0); + + let ensureChildCount = function* (count) { + let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid); + if (count == 0) + Assert.ok(!("children" in tree)); + else + Assert.equal(tree.children.length, count); + }; + + yield ensureChildCount(2); + yield PT.undo(); + yield ensureChildCount(0); + yield PT.redo() + yield ensureChildCount(2); + yield PT.undo(); + yield ensureChildCount(0); + + yield PT.undo(); + Assert.equal((yield PlacesUtils.promiseBookmarksTree(folderGuid)), null); + + // Cleanup + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_copy_excluding_annotations() { + let folderInfo = createTestFolderInfo(); + let anno = n => { return { name: n, value: 1 } }; + folderInfo.annotations = [anno("a"), anno("b"), anno("c")]; + let folderGuid = yield PT.NewFolder(folderInfo).transact(); + + let ensureAnnosSet = function* (guid, ...expectedAnnoNames) { + let tree = yield PlacesUtils.promiseBookmarksTree(guid); + let annoNames = "annos" in tree ? + tree.annos.map(a => a.name).sort() : []; + Assert.deepEqual(annoNames, expectedAnnoNames); + }; + + yield ensureAnnosSet(folderGuid, "a", "b", "c"); + + let excluding_a_dupeGuid = + yield PT.Copy({ guid: folderGuid + , newParentGuid: rootGuid + , excludingAnnotation: "a" }).transact(); + yield ensureAnnosSet(excluding_a_dupeGuid, "b", "c"); + + let excluding_ac_dupeGuid = + yield PT.Copy({ guid: folderGuid + , newParentGuid: rootGuid + , excludingAnnotations: ["a", "c"] }).transact(); + yield ensureAnnosSet(excluding_ac_dupeGuid, "b"); + + // Cleanup + yield PT.undo(); + yield PT.undo(); + yield PT.undo(); + yield PT.clearTransactionsHistory(); +}); + +add_task(function* test_invalid_uri_spec_throws() { + Assert.throws(() => + PT.NewBookmark({ parentGuid: rootGuid + , url: "invalid uri spec" + , title: "test bookmark" })); + Assert.throws(() => + PT.Tag({ tag: "TheTag" + , urls: ["invalid uri spec"] })); + Assert.throws(() => + PT.Tag({ tag: "TheTag" + , urls: ["about:blank", "invalid uri spec"] })); +}); + +add_task(function* test_annotate_multiple_items() { + let parentGuid = rootGuid; + let guids = [ + yield PT.NewBookmark({ url: "about:blank", parentGuid }).transact(), + yield PT.NewFolder({ title: "Test Folder", parentGuid }).transact()]; + + let annotation = { name: "TestAnno", value: "TestValue" }; + yield PT.Annotate({ guids, annotation }).transact(); + + function *ensureAnnoSet() { + for (let guid of guids) { + let itemId = yield PlacesUtils.promiseItemId(guid); + Assert.equal(annosvc.getItemAnnotation(itemId, annotation.name), + annotation.value); + } + } + function *ensureAnnoUnset() { + for (let guid of guids) { + let itemId = yield PlacesUtils.promiseItemId(guid); + Assert.ok(!annosvc.itemHasAnnotation(itemId, annotation.name)); + } + } + + yield ensureAnnoSet(); + yield PT.undo(); + yield ensureAnnoUnset(); + yield PT.redo(); + yield ensureAnnoSet(); + yield PT.undo(); + yield ensureAnnoUnset(); + + // Cleanup + yield PT.undo(); + yield PT.undo(); + yield ensureNonExistent(...guids); + yield PT.clearTransactionsHistory(); + observer.reset(); +}); + +add_task(function* test_remove_multiple() { + let guids = []; + yield PT.batch(function* () { + let folderGuid = yield PT.NewFolder({ title: "Test Folder" + , parentGuid: rootGuid }).transact(); + let nestedFolderGuid = + yield PT.NewFolder({ title: "Nested Test Folder" + , parentGuid: folderGuid }).transact(); + yield PT.NewSeparator(nestedFolderGuid).transact(); + + guids.push(folderGuid); + + let bmGuid = + yield PT.NewBookmark({ url: new URL("http://test.bookmark.removed") + , parentGuid: rootGuid }).transact(); + guids.push(bmGuid); + }); + + let originalInfos = []; + for (let guid of guids) { + originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid)); + } + + yield PT.Remove(guids).transact(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Undo the New* transactions batch. + yield PT.undo(); + yield ensureNonExistent(...guids); + + // Redo it. + yield PT.redo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Redo remove. + yield PT.redo(); + yield ensureNonExistent(...guids); + + // Cleanup + yield PT.clearTransactionsHistory(); + observer.reset(); +}); + +add_task(function* test_remove_bookmarks_for_urls() { + let urls = [new URL("http://test.url.1"), new URL("http://test.url.2")]; + let guids = []; + yield PT.batch(function* () { + for (let url of urls) { + for (let title of ["test title a", "test title b"]) { + let txn = PT.NewBookmark({ url, title, parentGuid: rootGuid }); + guids.push(yield txn.transact()); + } + } + }); + + let originalInfos = []; + for (let guid of guids) { + originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid)); + } + + yield PT.RemoveBookmarksForUrls(urls).transact(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.undo(); + yield ensureBookmarksTreeRestoredCorrectly(...originalInfos); + + // Cleanup. + yield PT.redo(); + yield ensureNonExistent(...guids); + yield PT.clearTransactionsHistory(); + observer.reset(); +}); diff --git a/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js new file mode 100644 index 000000000..7d5df565f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Added with bug 508102 to make sure that calling stopSearch on our + * AutoComplete implementation does not throw. + */ + +// Globals and Constants + +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]. + getService(Ci.nsIAutoCompleteSearch); + +// Test Functions + +function test_stopSearch() +{ + try { + ac.stopSearch(); + } + catch (e) { + do_throw("we should not have caught anything!"); + } +} + +// Test Runner + +var tests = [ + test_stopSearch, +]; +function run_test() +{ + tests.forEach(test => test()); +} diff --git a/toolkit/components/places/tests/unit/test_bookmark_catobs.js b/toolkit/components/places/tests/unit/test_bookmark_catobs.js new file mode 100644 index 000000000..e2b589090 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmark_catobs.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + run_next_test() +} + +add_task(function* test_observers() { + do_load_manifest("nsDummyObserver.manifest"); + + let dummyCreated = false; + let dummyReceivedOnItemAdded = false; + + Services.obs.addObserver(function created() { + Services.obs.removeObserver(created, "dummy-observer-created"); + dummyCreated = true; + }, "dummy-observer-created", false); + Services.obs.addObserver(function added() { + Services.obs.removeObserver(added, "dummy-observer-item-added"); + dummyReceivedOnItemAdded = true; + }, "dummy-observer-item-added", false); + + let initialObservers = PlacesUtils.bookmarks.getObservers(); + + // Add a common observer, it should be invoked after the category observer. + let notificationsPromised = new Promise((resolve, reject) => { + PlacesUtils.bookmarks.addObserver( { + __proto__: NavBookmarkObserver.prototype, + onItemAdded() { + let observers = PlacesUtils.bookmarks.getObservers(); + Assert.equal(observers.length, initialObservers.length + 1); + + // Check the common observer is the last one. + for (let i = 0; i < initialObservers.length; ++i) { + Assert.equal(initialObservers[i], observers[i]); + } + + PlacesUtils.bookmarks.removeObserver(this); + observers = PlacesUtils.bookmarks.getObservers(); + Assert.equal(observers.length, initialObservers.length); + + // Check the category observer has been invoked before this one. + Assert.ok(dummyCreated); + Assert.ok(dummyReceivedOnItemAdded); + resolve(); + } + }, false); + }); + + // Add a bookmark + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("http://typed.mozilla.org"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark"); + + yield notificationsPromised; +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html.js b/toolkit/components/places/tests/unit/test_bookmarks_html.js new file mode 100644 index 000000000..b10dc6185 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js @@ -0,0 +1,385 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; +const DESCRIPTION_ANNO = "bookmarkProperties/description"; + +// An object representing the contents of bookmarks.preplaces.html. +var test_bookmarks = { + menu: [ + { title: "Mozilla Firefox", + children: [ + { title: "Help and Tutorials", + url: "http://en-us.www.mozilla.com/en-US/firefox/help/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { title: "Customize Firefox", + url: "http://en-us.www.mozilla.com/en-US/firefox/customize/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { title: "Get Involved", + url: "http://en-us.www.mozilla.com/en-US/firefox/community/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { title: "About Us", + url: "http://en-us.www.mozilla.com/en-US/about/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + } + ] + }, + { + type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR + }, + { title: "test", + description: "folder test comment", + dateAdded: 1177541020000000, + lastModified: 1177541050000000, + children: [ + { title: "test post keyword", + description: "item description", + dateAdded: 1177375336000000, + lastModified: 1177375423000000, + keyword: "test", + sidebar: true, + postData: "hidden1%3Dbar&text1%3D%25s", + charset: "ISO-8859-1", + url: "http://test/post" + } + ] + } + ], + toolbar: [ + { title: "Getting Started", + url: "http://en-us.www.mozilla.com/en-US/firefox/central/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { title: "Latest Headlines", + url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + } + ], + unfiled: [ + { title: "Example.tld", + url: "http://example.tld/" + } + ] +}; + +// Pre-Places bookmarks.html file pointer. +var gBookmarksFileOld; +// Places bookmarks.html file pointer. +var gBookmarksFileNew; + +function run_test() +{ + run_next_test(); +} + +add_task(function* setup() { + // Avoid creating smart bookmarks during the test. + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1); + + // File pointer to legacy bookmarks file. + gBookmarksFileOld = do_get_file("bookmarks.preplaces.html"); + + // File pointer to a new Places-exported bookmarks file. + gBookmarksFileNew = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + gBookmarksFileNew.append("bookmarks.exported.html"); + if (gBookmarksFileNew.exists()) { + gBookmarksFileNew.remove(false); + } + + // This test must be the first one, since it setups the new bookmarks.html. + // Test importing a pre-Places canonical bookmarks file. + // 1. import bookmarks.preplaces.html + // 2. run the test-suite + // Note: we do not empty the db before this import to catch bugs like 380999 + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); + + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_new() +{ + // Test importing a Places bookmarks.html file. + // 1. import bookmarks.exported.html + // 2. run the test-suite + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + + yield testImportedBookmarks(); + yield PlacesTestUtils.promiseAsyncUpdates(); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_emptytitle_export() +{ + // Test exporting and importing with an empty-titled bookmark. + // 1. import bookmarks + // 2. create an empty-titled bookmark. + // 3. export to bookmarks.exported.html + // 4. empty bookmarks db + // 5. import bookmarks.exported.html + // 6. run the test-suite + // 7. remove the empty-titled bookmark + // 8. export to bookmarks.exported.html + // 9. empty bookmarks db and continue + + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + + const NOTITLE_URL = "http://notitle.mozilla.org/"; + let bookmark = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: NOTITLE_URL + }); + test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL }); + + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield PlacesUtils.bookmarks.eraseEverything(); + + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); + + // Cleanup. + test_bookmarks.unfiled.pop(); + // HTML imports don't restore GUIDs yet. + let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX + }); + Assert.equal(reimportedBookmark.url.href, bookmark.url.href); + yield PlacesUtils.bookmarks.remove(reimportedBookmark); + + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_chromefavicon() +{ + // Test exporting and importing with a bookmark pointing to a chrome favicon. + // 1. import bookmarks + // 2. create a bookmark pointing to a chrome favicon. + // 3. export to bookmarks.exported.html + // 4. empty bookmarks db + // 5. import bookmarks.exported.html + // 6. run the test-suite + // 7. remove the bookmark pointing to a chrome favicon. + // 8. export to bookmarks.exported.html + // 9. empty bookmarks db and continue + + const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page"); + const CHROME_FAVICON_URI = NetUtil.newURI("chrome://global/skin/icons/information-16.png"); + const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png"); + + do_print("Importing from html"); + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Insert bookmark"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: PAGE_URI, + title: "Test" + }); + + do_print("Set favicon"); + yield new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + PAGE_URI, CHROME_FAVICON_URI, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, Services.scriptSecurityManager.getSystemPrincipal()); + }); + + let data = yield new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage( + PAGE_URI, (uri, dataLen, faviconData, mimeType) => resolve(faviconData)); + }); + + let base64Icon = "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + + test_bookmarks.unfiled.push( + { title: "Test", url: PAGE_URI.spec, icon: base64Icon }); + + do_print("Export to html"); + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Set favicon"); + // Change the favicon to check it's really imported again later. + yield new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + PAGE_URI, CHROME_FAVICON_URI_2, true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + resolve, Services.scriptSecurityManager.getSystemPrincipal()); + }); + + do_print("import from html"); + yield PlacesUtils.bookmarks.eraseEverything(); + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + + do_print("Test imported bookmarks"); + yield testImportedBookmarks(); + + // Cleanup. + test_bookmarks.unfiled.pop(); + // HTML imports don't restore GUIDs yet. + let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX + }); + yield PlacesUtils.bookmarks.remove(reimportedBookmark); + + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_ontop() +{ + // Test importing the exported bookmarks.html file *on top of* the existing + // bookmarks. + // 1. empty bookmarks db + // 2. import the exported bookmarks file + // 3. export to file + // 3. import the exported bookmarks file + // 4. run the test-suite + + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); + yield PlacesTestUtils.promiseAsyncUpdates(); + + yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +function* testImportedBookmarks() +{ + for (let group in test_bookmarks) { + do_print("[testImportedBookmarks()] Checking group '" + group + "'"); + + let root; + switch (group) { + case "menu": + root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root; + break; + case "toolbar": + root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + break; + case "unfiled": + root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + break; + } + + let items = test_bookmarks[group]; + do_check_eq(root.childCount, items.length); + + for (let key in items) { + yield checkItem(items[key], root.getChild(key)); + } + + root.containerOpen = false; + } +} + +function* checkItem(aExpected, aNode) +{ + let id = aNode.itemId; + + return Task.spawn(function* () { + for (prop in aExpected) { + switch (prop) { + case "type": + do_check_eq(aNode.type, aExpected.type); + break; + case "title": + do_check_eq(aNode.title, aExpected.title); + break; + case "description": + do_check_eq(PlacesUtils.annotations + .getItemAnnotation(id, DESCRIPTION_ANNO), + aExpected.description); + break; + case "dateAdded": + do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id), + aExpected.dateAdded); + break; + case "lastModified": + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id), + aExpected.lastModified); + break; + case "url": + if (!("feedUrl" in aExpected)) + do_check_eq(aNode.uri, aExpected.url) + break; + case "icon": + let deferred = Promise.defer(); + PlacesUtils.favicons.getFaviconDataForPage( + NetUtil.newURI(aExpected.url), + function (aURI, aDataLen, aData, aMimeType) { + deferred.resolve(aData); + }); + let data = yield deferred.promise; + let base64Icon = "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + do_check_true(base64Icon == aExpected.icon); + break; + case "keyword": { + let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.keyword, aExpected.keyword); + break; + } + case "sidebar": + do_check_eq(PlacesUtils.annotations + .itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO), + aExpected.sidebar); + break; + case "postData": { + let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.postData, aExpected.postData); + break; + } + case "charset": + let testURI = NetUtil.newURI(aNode.uri); + do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset); + break; + case "feedUrl": + let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id }); + do_check_eq(livemark.siteURI.spec, aExpected.url); + do_check_eq(livemark.feedURI.spec, aExpected.feedUrl); + break; + case "children": + let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(folder.hasChildren, aExpected.children.length > 0); + folder.containerOpen = true; + do_check_eq(folder.childCount, aExpected.children.length); + + for (let index = 0; index < aExpected.children.length; index++) { + yield checkItem(aExpected.children[index], folder.getChild(index)); + } + + folder.containerOpen = false; + break; + default: + throw new Error("Unknown property"); + } + } + }); +} diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js new file mode 100644 index 000000000..845b2227b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js @@ -0,0 +1,143 @@ +/* + * This test ensures that importing/exporting to HTML does not stop + * if a malformed uri is found. + */ + +const DESCRIPTION_ANNO = "bookmarkProperties/description"; +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; + +const TEST_FAVICON_PAGE_URL = "http://en-US.www.mozilla.com/en-US/firefox/central/"; +const TEST_FAVICON_DATA_SIZE = 580; + +function run_test() { + run_next_test(); +} + +add_task(function* test_corrupt_file() { + // avoid creating the places smart folder during tests + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1); + + // Import bookmarks from the corrupt file. + let corruptHtml = OS.Path.join(do_get_cwd().path, "bookmarks.corrupt.html"); + yield BookmarkHTMLUtils.importFromFile(corruptHtml, true); + + // Check that bookmarks that are not corrupt have been imported. + yield PlacesTestUtils.promiseAsyncUpdates(); + yield database_check(); +}); + +add_task(function* test_corrupt_database() { + // Create corruption in the database, then export. + let corruptBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://test.mozilla.org", + title: "We love belugas" }); + yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) { + yield db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid", + { guid: corruptBookmark.guid }); + })); + + let bookmarksFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.html"); + if ((yield OS.File.exists(bookmarksFile))) + yield OS.File.remove(bookmarksFile); + yield BookmarkHTMLUtils.exportToFile(bookmarksFile); + + // Import again and check for correctness. + yield PlacesUtils.bookmarks.eraseEverything(); + yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield database_check(); +}); + +/* + * Check for imported bookmarks correctness + * + * @return {Promise} + * @resolves When the checks are finished. + * @rejects Never. + */ +var database_check = Task.async(function* () { + // BOOKMARKS MENU + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root; + Assert.equal(root.childCount, 2); + + let folderNode = root.getChild(1); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, "test"); + Assert.equal(PlacesUtils.bookmarks.getItemDateAdded(folderNode.itemId), 1177541020000000); + Assert.equal(PlacesUtils.bookmarks.getItemLastModified(folderNode.itemId), 1177541050000000); + Assert.equal("folder test comment", + PlacesUtils.annotations.getItemAnnotation(folderNode.itemId, + DESCRIPTION_ANNO)); + // open test folder, and test the children + PlacesUtils.asQuery(folderNode); + Assert.equal(folderNode.hasChildren, true); + folderNode.containerOpen = true; + Assert.equal(folderNode.childCount, 1); + + let bookmarkNode = folderNode.getChild(0); + Assert.equal("http://test/post", bookmarkNode.uri); + Assert.equal("test post keyword", bookmarkNode.title); + + let entry = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri }); + Assert.equal("test", entry.keyword); + Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData); + + Assert.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId, + LOAD_IN_SIDEBAR_ANNO)); + Assert.equal(bookmarkNode.dateAdded, 1177375336000000); + Assert.equal(bookmarkNode.lastModified, 1177375423000000); + + Assert.equal((yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri))), + "ISO-8859-1"); + + Assert.equal("item description", + PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId, + DESCRIPTION_ANNO)); + + // clean up + folderNode.containerOpen = false; + root.containerOpen = false; + + // BOOKMARKS TOOLBAR + root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + Assert.equal(root.childCount, 3); + + // For now some promises are resolved later, so we can't guarantee an order. + let foundLivemark = false; + for (let i = 0; i < root.childCount; ++i) { + let node = root.getChild(i); + if (node.title == "Latest Headlines") { + foundLivemark = true; + Assert.equal("Latest Headlines", node.title); + + let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: node.bookmarkGuid }); + Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + livemark.siteURI.spec); + Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + livemark.feedURI.spec); + } + } + Assert.ok(foundLivemark); + + // cleanup + root.containerOpen = false; + + // UNFILED BOOKMARKS + root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + Assert.equal(root.childCount, 1); + root.containerOpen = false; + + // favicons + yield new Promise(resolve => { + PlacesUtils.favicons.getFaviconDataForPage(uri(TEST_FAVICON_PAGE_URL), + (aURI, aDataLen, aData, aMimeType) => { + // aURI should never be null when aDataLen > 0. + Assert.notEqual(aURI, null); + // Favicon data is stored in the bookmarks file as a "data:" URI. For + // simplicity, instead of converting the data we receive to a "data:" URI + // and comparing it, we just check the data size. + Assert.equal(TEST_FAVICON_DATA_SIZE, aDataLen); + resolve(); + }); + }); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js new file mode 100644 index 000000000..e4ba433a3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js @@ -0,0 +1,57 @@ +var bookmarkData = [ + { uri: uri("http://www.toastytech.com"), + title: "Nathan's Toasty Technology Page", + tags: ["technology", "personal", "retro"] }, + { uri: uri("http://www.reddit.com"), + title: "reddit: the front page of the internet", + tags: ["social media", "news", "humour"] }, + { uri: uri("http://www.4chan.org"), + title: "4chan", + tags: ["discussion", "imageboard", "anime"] } +]; + +/* + TEST SUMMARY + - Add bookmarks with tags + - Export tagged bookmarks as HTML file + - Delete bookmarks + - Import bookmarks from HTML file + - Check that all bookmarks are successfully imported with tags +*/ + +add_task(function* test_import_tags() { + // Removes bookmarks.html if the file already exists. + let HTMLFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html"); + if ((yield OS.File.exists(HTMLFile))) + yield OS.File.remove(HTMLFile); + + // Adds bookmarks and tags to the database. + let bookmarkList = new Set(); + for (let { uri, title, tags } of bookmarkData) { + bookmarkList.add(yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title })); + PlacesUtils.tagging.tagURI(uri, tags); + } + + // Exports the bookmarks as a HTML file. + yield BookmarkHTMLUtils.exportToFile(HTMLFile); + + // Deletes bookmarks and tags from the database. + for (let bookmark of bookmarkList) { + yield PlacesUtils.bookmarks.remove(bookmark.guid); + } + + // Re-imports the bookmarks from the HTML file. + yield BookmarkHTMLUtils.importFromFile(HTMLFile, true); + + // Tests to ensure that the tags are still present for each bookmark URI. + for (let { uri, tags } of bookmarkData) { + do_print("Test tags for " + uri.spec + ": " + tags + "\n"); + let foundTags = PlacesUtils.tagging.getTagsForURI(uri); + Assert.equal(foundTags.length, tags.length); + Assert.ok(tags.every(tag => foundTags.includes(tag))); + } +}); + diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js new file mode 100644 index 000000000..02b430ff2 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test for bug #801450 + +// Get Services +Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_bookmarks_html_singleframe() +{ + let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks_html_singleframe.html"); + yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true); + + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root; + do_check_eq(root.childCount, 1); + let folder = root.getChild(0); + PlacesUtils.asContainer(folder).containerOpen = true; + do_check_eq(folder.title, "Subtitle"); + do_check_eq(folder.childCount, 1); + let bookmark = folder.getChild(0); + do_check_eq(bookmark.uri, "http://www.mozilla.org/"); + do_check_eq(bookmark.title, "Mozilla"); + folder.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json.js b/toolkit/components/places/tests/unit/test_bookmarks_json.js new file mode 100644 index 000000000..a6801540a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js @@ -0,0 +1,241 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm"); + +function run_test() { + run_next_test(); +} + +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; +const DESCRIPTION_ANNO = "bookmarkProperties/description"; + +// An object representing the contents of bookmarks.json. +var test_bookmarks = { + menu: [ + { guid: "OCyeUO5uu9FF", + title: "Mozilla Firefox", + children: [ + { guid:"OCyeUO5uu9FG", + title: "Help and Tutorials", + url: "http://en-us.www.mozilla.com/en-US/firefox/help/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { guid:"OCyeUO5uu9FH", + title: "Customize Firefox", + url: "http://en-us.www.mozilla.com/en-US/firefox/customize/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { guid:"OCyeUO5uu9FI", + title: "Get Involved", + url: "http://en-us.www.mozilla.com/en-US/firefox/community/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { guid:"OCyeUO5uu9FJ", + title: "About Us", + url: "http://en-us.www.mozilla.com/en-US/about/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + } + ] + }, + { + guid: "OCyeUO5uu9FK", + type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR + }, + { + guid:"OCyeUO5uu9FL", + title: "test", + description: "folder test comment", + dateAdded: 1177541020000000, + // lastModified: 1177541050000000, + children: [ + { guid:"OCyeUO5uu9GX", + title: "test post keyword", + description: "item description", + dateAdded: 1177375336000000, + // lastModified: 1177375423000000, + keyword: "test", + sidebar: true, + postData: "hidden1%3Dbar&text1%3D%25s", + charset: "ISO-8859-1" + } + ] + } + ], + toolbar: [ + { guid: "OCyeUO5uu9FB", + title: "Getting Started", + url: "http://en-us.www.mozilla.com/en-US/firefox/central/", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" + }, + { guid:"OCyeUO5uu9FR", + title: "Latest Headlines", + url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/", + feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + } + ], + unfiled: [ + { guid: "OCyeUO5uu9FW", + title: "Example.tld", + url: "http://example.tld/" + } + ] +}; + +// Exported bookmarks file pointer. +var bookmarksExportedFile; + +add_task(function* test_import_bookmarks() { + let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks.json"); + + yield BookmarkJSONUtils.importFromFile(bookmarksFile, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); +}); + +add_task(function* test_export_bookmarks() { + bookmarksExportedFile = OS.Path.join(OS.Constants.Path.profileDir, + "bookmarks.exported.json"); + yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + yield PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(function* test_import_exported_bookmarks() { + yield PlacesUtils.bookmarks.eraseEverything(); + yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); +}); + +add_task(function* test_import_ontop() { + yield PlacesUtils.bookmarks.eraseEverything(); + yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true); + yield PlacesTestUtils.promiseAsyncUpdates(); + yield testImportedBookmarks(); +}); + +add_task(function* test_clean() { + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +function* testImportedBookmarks() { + for (let group in test_bookmarks) { + do_print("[testImportedBookmarks()] Checking group '" + group + "'"); + + let root; + switch (group) { + case "menu": + root = + PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root; + break; + case "toolbar": + root = + PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + break; + case "unfiled": + root = + PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; + break; + } + + let items = test_bookmarks[group]; + do_check_eq(root.childCount, items.length); + + for (let key in items) { + yield checkItem(items[key], root.getChild(key)); + } + + root.containerOpen = false; + } +} + +function* checkItem(aExpected, aNode) { + let id = aNode.itemId; + + return Task.spawn(function* () { + for (prop in aExpected) { + switch (prop) { + case "type": + do_check_eq(aNode.type, aExpected.type); + break; + case "title": + do_check_eq(aNode.title, aExpected.title); + break; + case "description": + do_check_eq(PlacesUtils.annotations.getItemAnnotation( + id, DESCRIPTION_ANNO), aExpected.description); + break; + case "dateAdded": + do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id), + aExpected.dateAdded); + break; + case "lastModified": + do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id), + aExpected.lastModified); + break; + case "url": + if (!("feedUrl" in aExpected)) + do_check_eq(aNode.uri, aExpected.url); + break; + case "icon": + let deferred = Promise.defer(); + PlacesUtils.favicons.getFaviconDataForPage( + NetUtil.newURI(aExpected.url), + function (aURI, aDataLen, aData, aMimeType) { + deferred.resolve(aData); + }); + let data = yield deferred.promise; + let base64Icon = "data:image/png;base64," + + base64EncodeString(String.fromCharCode.apply(String, data)); + do_check_true(base64Icon == aExpected.icon); + break; + case "keyword": { + let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.keyword, aExpected.keyword); + break; + } + case "guid": + let guid = yield PlacesUtils.promiseItemGuid(id); + do_check_eq(guid, aExpected.guid); + break; + case "sidebar": + do_check_eq(PlacesUtils.annotations.itemHasAnnotation( + id, LOAD_IN_SIDEBAR_ANNO), aExpected.sidebar); + break; + case "postData": { + let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri }); + Assert.equal(entry.postData, aExpected.postData); + break; + } + case "charset": + let testURI = NetUtil.newURI(aNode.uri); + do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset); + break; + case "feedUrl": + let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id }); + do_check_eq(livemark.siteURI.spec, aExpected.url); + do_check_eq(livemark.feedURI.spec, aExpected.feedUrl); + break; + case "children": + let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(folder.hasChildren, aExpected.children.length > 0); + folder.containerOpen = true; + do_check_eq(folder.childCount, aExpected.children.length); + + for (let index = 0; index < aExpected.children.length; index++) { + yield checkItem(aExpected.children[index], folder.getChild(index)); + } + + folder.containerOpen = false; + break; + default: + throw new Error("Unknown property"); + } + } + }); +} diff --git a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js new file mode 100644 index 000000000..2f8022c6b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js @@ -0,0 +1,325 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); + +/** + * Tests the bookmarks-restore-* nsIObserver notifications after restoring + * bookmarks from JSON and HTML. See bug 470314. + */ + +// The topics and data passed to nsIObserver.observe() on bookmarks restore +const NSIOBSERVER_TOPIC_BEGIN = "bookmarks-restore-begin"; +const NSIOBSERVER_TOPIC_SUCCESS = "bookmarks-restore-success"; +const NSIOBSERVER_TOPIC_FAILED = "bookmarks-restore-failed"; +const NSIOBSERVER_DATA_JSON = "json"; +const NSIOBSERVER_DATA_HTML = "html"; +const NSIOBSERVER_DATA_HTML_INIT = "html-initial"; + +// Bookmarks are added for these URIs +var uris = [ + "http://example.com/1", + "http://example.com/2", + "http://example.com/3", + "http://example.com/4", + "http://example.com/5", +]; + +/** + * Adds some bookmarks for the URIs in |uris|. + */ +function* addBookmarks() { + for (let url of uris) { + yield PlacesUtils.bookmarks.insert({ + url: url, parentGuid: PlacesUtils.bookmarks.menuGuid + }) + } + checkBookmarksExist(); +} + +/** + * Checks that all of the bookmarks created for |uris| exist. It works by + * creating one query per URI and then ORing all the queries. The number of + * results returned should be uris.length. + */ +function checkBookmarksExist() { + let hs = PlacesUtils.history; + let queries = uris.map(function (u) { + let q = hs.getNewQuery(); + q.uri = uri(u); + return q; + }); + let options = hs.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = hs.executeQueries(queries, uris.length, options).root; + root.containerOpen = true; + Assert.equal(root.childCount, uris.length); + root.containerOpen = false; +} + +/** + * Creates an file in the profile directory. + * + * @param aBasename + * e.g., "foo.txt" in the path /some/long/path/foo.txt + * @return {Promise} + * @resolves to an OS.File path + */ +function promiseFile(aBasename) { + let path = OS.Path.join(OS.Constants.Path.profileDir, aBasename); + do_print("opening " + path); + return OS.File.open(path, { truncate: true }) + .then(aFile => { + aFile.close(); + return path; + }); +} + +/** + * Register observers via promiseTopicObserved helper. + * + * @param {boolean} expectSuccess pass true when expect a success notification + * @return {Promise[]} + */ +function registerObservers(expectSuccess) { + let promiseBegin = promiseTopicObserved(NSIOBSERVER_TOPIC_BEGIN); + let promiseResult; + if (expectSuccess) { + promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_SUCCESS); + } else { + promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_FAILED); + } + + return [promiseBegin, promiseResult]; +} + +/** + * Check notification results. + * + * @param {Promise[]} expectPromises array contain promiseBegin and promiseResult + * @param {object} expectedData contain data and folderId + */ +function* checkObservers(expectPromises, expectedData) { + let [promiseBegin, promiseResult] = expectPromises; + + let beginData = (yield promiseBegin)[1]; + Assert.equal(beginData, expectedData.data, + "Data for current test should be what is expected"); + + let [resultSubject, resultData] = yield promiseResult; + Assert.equal(resultData, expectedData.data, + "Data for current test should be what is expected"); + + // Make sure folder ID is what is expected. For importing HTML into a + // folder, this will be an integer, otherwise null. + if (resultSubject) { + Assert.equal(aSubject.QueryInterface(Ci.nsISupportsPRInt64).data, + expectedData.folderId); + } else { + Assert.equal(expectedData.folderId, null); + } +} + +/** + * Run after every test cases. + */ +function* teardown(file, begin, success, fail) { + // On restore failed, file may not exist, so wrap in try-catch. + try { + yield OS.File.remove(file, {ignoreAbsent: true}); + } catch (e) {} + + // clean up bookmarks + yield PlacesUtils.bookmarks.eraseEverything(); +} + +add_task(function* test_json_restore_normal() { + // data: the data passed to nsIObserver.observe() corresponding to the test + // folderId: for HTML restore into a folder, the folder ID to restore into; + // otherwise, set it to null + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("JSON restore: normal restore should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.json"); + yield addBookmarks(); + + yield BookmarkJSONUtils.exportToFile(file); + yield PlacesUtils.bookmarks.eraseEverything(); + try { + yield BookmarkJSONUtils.importFromFile(file, true); + } catch (e) { + do_throw(" Restore should not have failed" + e); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_json_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("JSON restore: empty file should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.json"); + try { + yield BookmarkJSONUtils.importFromFile(file, true); + } catch (e) { + do_throw(" Restore should not have failed" + e); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_json_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_JSON, + folderId: null + } + let expectPromises = registerObservers(false); + + do_print("JSON restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + file.append("this file doesn't exist because nobody created it 1"); + try { + yield BookmarkJSONUtils.importFromFile(file, true); + do_throw(" Restore should have failed"); + } catch (e) {} + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_restore_normal() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("HTML restore: normal restore should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.html"); + yield addBookmarks(); + yield BookmarkHTMLUtils.exportToFile(file); + yield PlacesUtils.bookmarks.eraseEverything(); + try { + BookmarkHTMLUtils.importFromFile(file, false) + .then(null, do_report_unexpected_exception); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("HTML restore: empty file should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.init.html"); + try { + BookmarkHTMLUtils.importFromFile(file, false) + .then(null, do_report_unexpected_exception); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML, + folderId: null + } + let expectPromises = registerObservers(false); + + do_print("HTML restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + file.append("this file doesn't exist because nobody created it 2"); + try { + yield BookmarkHTMLUtils.importFromFile(file, false); + do_throw("Should fail!"); + } catch (e) {} + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_init_restore_normal() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("HTML initial restore: normal restore should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.init.html"); + yield addBookmarks(); + yield BookmarkHTMLUtils.exportToFile(file); + yield PlacesUtils.bookmarks.eraseEverything(); + try { + BookmarkHTMLUtils.importFromFile(file, true) + .then(null, do_report_unexpected_exception); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_init_restore_empty() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null + } + let expectPromises = registerObservers(true); + + do_print("HTML initial restore: empty file should succeed"); + let file = yield promiseFile("bookmarks-test_restoreNotification.init.html"); + try { + BookmarkHTMLUtils.importFromFile(file, true) + .then(null, do_report_unexpected_exception); + } catch (e) { + do_throw(" Restore should not have failed"); + } + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); + +add_task(function* test_html_init_restore_nonexist() { + let expectedData = { + data: NSIOBSERVER_DATA_HTML_INIT, + folderId: null + } + let expectPromises = registerObservers(false); + + do_print("HTML initial restore: nonexistent file should fail"); + let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + file.append("this file doesn't exist because nobody created it 3"); + try { + yield BookmarkHTMLUtils.importFromFile(file, true); + do_throw("Should fail!"); + } catch (e) {} + + yield checkObservers(expectPromises, expectedData); + yield teardown(file); +}); diff --git a/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js new file mode 100644 index 000000000..959dfe85f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js @@ -0,0 +1,44 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Both SetItemtitle and insertBookmark should allow for null titles. + */ + +const bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +const TEST_URL = "http://www.mozilla.org"; + +function run_test() { + // Insert a bookmark with an empty title. + var itemId = bs.insertBookmark(bs.toolbarFolder, + uri(TEST_URL), + bs.DEFAULT_INDEX, + ""); + // Check returned title is an empty string. + do_check_eq(bs.getItemTitle(itemId), ""); + // Set title to null. + bs.setItemTitle(itemId, null); + // Check returned title is null. + do_check_eq(bs.getItemTitle(itemId), null); + // Cleanup. + bs.removeItem(itemId); + + // Insert a bookmark with a null title. + itemId = bs.insertBookmark(bs.toolbarFolder, + uri(TEST_URL), + bs.DEFAULT_INDEX, + null); + // Check returned title is null. + do_check_eq(bs.getItemTitle(itemId), null); + // Set title to an empty string. + bs.setItemTitle(itemId, ""); + // Check returned title is an empty string. + do_check_eq(bs.getItemTitle(itemId), ""); + // Cleanup. + bs.removeItem(itemId); +} diff --git a/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js new file mode 100644 index 000000000..b67e141e6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://1.moz.org/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1" + ); + let id1 = PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=1234"), + PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 1" + ); + let id2 = PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=-1"), + PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 2" + ); + PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://2.moz.org/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 2" + ); + + // Add also a simple visit. + yield PlacesTestUtils.addVisits(uri(("http://3.moz.org/"))); + + // Query containing a broken folder shortcuts among results. + let query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1); + let options = PlacesUtils.history.getNewQueryOptions(); + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + + do_check_eq(root.childCount, 4); + + let shortcut = root.getChild(1); + do_check_eq(shortcut.uri, "place:folder=1234"); + PlacesUtils.asContainer(shortcut); + shortcut.containerOpen = true; + do_check_eq(shortcut.childCount, 0); + shortcut.containerOpen = false; + // Remove the broken shortcut while the containing result is open. + PlacesUtils.bookmarks.removeItem(id1); + do_check_eq(root.childCount, 3); + + shortcut = root.getChild(1); + do_check_eq(shortcut.uri, "place:folder=-1"); + PlacesUtils.asContainer(shortcut); + shortcut.containerOpen = true; + do_check_eq(shortcut.childCount, 0); + shortcut.containerOpen = false; + // Remove the broken shortcut while the containing result is open. + PlacesUtils.bookmarks.removeItem(id2); + do_check_eq(root.childCount, 2); + + root.containerOpen = false; + + // Broken folder shortcut as root node. + query = PlacesUtils.history.getNewQuery(); + query.setFolders([1234], 1); + options = PlacesUtils.history.getNewQueryOptions(); + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + do_check_eq(root.childCount, 0); + root.containerOpen = false; + + // Broken folder shortcut as root node with folder=-1. + query = PlacesUtils.history.getNewQuery(); + query.setFolders([-1], 1); + options = PlacesUtils.history.getNewQueryOptions(); + root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + do_check_eq(root.childCount, 0); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_browserhistory.js b/toolkit/components/places/tests/unit/test_browserhistory.js new file mode 100644 index 000000000..5f88c26e3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_browserhistory.js @@ -0,0 +1,129 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_URI = NetUtil.newURI("http://mozilla.com/"); +const TEST_SUBDOMAIN_URI = NetUtil.newURI("http://foobar.mozilla.com/"); + +add_task(function* test_addPage() { + yield PlacesTestUtils.addVisits(TEST_URI); + do_check_eq(1, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_removePage() { + PlacesUtils.bhistory.removePage(TEST_URI); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_removePages() { + let pages = []; + for (let i = 0; i < 8; i++) { + pages.push(NetUtil.newURI(TEST_URI.spec + i)); + } + + yield PlacesTestUtils.addVisits(pages.map(uri => ({ uri: uri }))); + // Bookmarked item should not be removed from moz_places. + const ANNO_INDEX = 1; + const ANNO_NAME = "testAnno"; + const ANNO_VALUE = "foo"; + const BOOKMARK_INDEX = 2; + PlacesUtils.annotations.setPageAnnotation(pages[ANNO_INDEX], + ANNO_NAME, ANNO_VALUE, 0, + Ci.nsIAnnotationService.EXPIRE_NEVER); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + pages[BOOKMARK_INDEX], + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test bookmark"); + PlacesUtils.annotations.setPageAnnotation(pages[BOOKMARK_INDEX], + ANNO_NAME, ANNO_VALUE, 0, + Ci.nsIAnnotationService.EXPIRE_NEVER); + + PlacesUtils.bhistory.removePages(pages, pages.length); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); + + // Check that the bookmark and its annotation still exist. + do_check_true(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) > 0); + do_check_eq(PlacesUtils.annotations.getPageAnnotation(pages[BOOKMARK_INDEX], ANNO_NAME), + ANNO_VALUE); + + // Check the annotation on the non-bookmarked page does not exist anymore. + try { + PlacesUtils.annotations.getPageAnnotation(pages[ANNO_INDEX], ANNO_NAME); + do_throw("did not expire expire_never anno on a not bookmarked item"); + } catch (ex) {} + + // Cleanup. + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_removePagesByTimeframe() { + let visits = []; + let startDate = (Date.now() - 10000) * 1000; + for (let i = 0; i < 10; i++) { + visits.push({ + uri: NetUtil.newURI(TEST_URI.spec + i), + visitDate: startDate + i * 1000 + }); + } + + yield PlacesTestUtils.addVisits(visits); + + // Delete all pages except the first and the last. + PlacesUtils.bhistory.removePagesByTimeframe(startDate + 1000, startDate + 8000); + + // Check that we have removed the correct pages. + for (let i = 0; i < 10; i++) { + do_check_eq(page_in_database(NetUtil.newURI(TEST_URI.spec + i)) == 0, + i > 0 && i < 9); + } + + // Clear remaining items and check that all pages have been removed. + PlacesUtils.bhistory.removePagesByTimeframe(startDate, startDate + 9000); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_removePagesFromHost() { + yield PlacesTestUtils.addVisits(TEST_URI); + PlacesUtils.bhistory.removePagesFromHost("mozilla.com", true); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_removePagesFromHost_keepSubdomains() { + yield PlacesTestUtils.addVisits([{ uri: TEST_URI }, { uri: TEST_SUBDOMAIN_URI }]); + PlacesUtils.bhistory.removePagesFromHost("mozilla.com", false); + do_check_eq(1, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_history_clear() { + yield PlacesTestUtils.clearHistory(); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); +}); + +add_task(function* test_getObservers() { + // Ensure that getObservers() invalidates the hasHistoryEntries cache. + yield PlacesTestUtils.addVisits(TEST_URI); + do_check_eq(1, PlacesUtils.history.hasHistoryEntries); + // This is just for testing purposes, never do it. + return new Promise((resolve, reject) => { + DBConn().executeSimpleSQLAsync("DELETE FROM moz_historyvisits", { + handleError: function(error) { + reject(error); + }, + handleResult: function(result) { + }, + handleCompletion: function(result) { + // Just invoking getObservers should be enough to invalidate the cache. + PlacesUtils.history.getObservers(); + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); + resolve(); + } + }); + }); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js new file mode 100644 index 000000000..a7ad1257a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js @@ -0,0 +1,35 @@ +// Test that asking for a livemark in a annotationChanged notification works. +add_task(function* () { + let annoPromise = new Promise(resolve => { + let annoObserver = { + onItemAnnotationSet(id, name) { + if (name == PlacesUtils.LMANNO_FEEDURI) { + PlacesUtils.annotations.removeObserver(this); + resolve(); + } + }, + onItemAnnotationRemoved() {}, + onPageAnnotationSet() {}, + onPageAnnotationRemoved() {}, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIAnnotationObserver + ]), + }; + PlacesUtils.annotations.addObserver(annoObserver, false); + }); + + + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "livemark title" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , index: PlacesUtils.bookmarks.DEFAULT_INDEX + , siteURI: uri("http://example.com/") + , feedURI: uri("http://example.com/rdf") + }); + + yield annoPromise; + + livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid }); + Assert.ok(livemark); + yield PlacesUtils.livemarks.removeLivemark({ guid: livemark.guid }); +}); diff --git a/toolkit/components/places/tests/unit/test_childlessTags.js b/toolkit/components/places/tests/unit/test_childlessTags.js new file mode 100644 index 000000000..4c3e38fa4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_childlessTags.js @@ -0,0 +1,117 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Ensures that removal of a bookmark untags the bookmark if it's no longer + * contained in any regular, non-tag folders. See bug 444849. + */ + +// Add your tests here. Each is an object with a summary string |desc| and a +// method run() that's called to run the test. +var tests = [ + { + desc: "Removing a tagged bookmark should cause the tag to be removed.", + run: function () { + print(" Make a bookmark."); + var bmId = bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, + BOOKMARK_URI, + bmsvc.DEFAULT_INDEX, + "test bookmark"); + do_check_true(bmId > 0); + + print(" Tag it up."); + var tags = ["foo", "bar"]; + tagssvc.tagURI(BOOKMARK_URI, tags); + ensureTagsExist(tags); + + print(" Remove the bookmark. The tags should no longer exist."); + bmsvc.removeItem(bmId); + ensureTagsExist([]); + } + }, + + { + desc: "Removing a folder containing a tagged bookmark should cause the " + + "tag to be removed.", + run: function () { + print(" Make a folder."); + var folderId = bmsvc.createFolder(bmsvc.unfiledBookmarksFolder, + "test folder", + bmsvc.DEFAULT_INDEX); + do_check_true(folderId > 0); + + print(" Stick a bookmark in the folder."); + var bmId = bmsvc.insertBookmark(folderId, + BOOKMARK_URI, + bmsvc.DEFAULT_INDEX, + "test bookmark"); + do_check_true(bmId > 0); + + print(" Tag the bookmark."); + var tags = ["foo", "bar"]; + tagssvc.tagURI(BOOKMARK_URI, tags); + ensureTagsExist(tags); + + print(" Remove the folder. The tags should no longer exist."); + bmsvc.removeItem(folderId); + ensureTagsExist([]); + } + } +]; + +var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + +const BOOKMARK_URI = uri("http://example.com/"); + +/** + * Runs a tag query and ensures that the tags returned are those and only those + * in aTags. aTags may be empty, in which case this function ensures that no + * tags exist. + * + * @param aTags + * An array of tags (strings) + */ +function ensureTagsExist(aTags) { + var query = histsvc.getNewQuery(); + var opts = histsvc.getNewQueryOptions(); + opts.resultType = opts.RESULTS_AS_TAG_QUERY; + var resultRoot = histsvc.executeQuery(query, opts).root; + + // Dupe aTags. + var tags = aTags.slice(0); + + resultRoot.containerOpen = true; + + // Ensure that the number of tags returned from the query is the same as the + // number in |tags|. + do_check_eq(resultRoot.childCount, tags.length); + + // For each tag result from the query, ensure that it's contained in |tags|. + // Remove the tag from |tags| so that we ensure the sets are equal. + for (let i = 0; i < resultRoot.childCount; i++) { + var tag = resultRoot.getChild(i).title; + var indexOfTag = tags.indexOf(tag); + do_check_true(indexOfTag >= 0); + tags.splice(indexOfTag, 1); + } + + resultRoot.containerOpen = false; +} + +function run_test() +{ + tests.forEach(function (test) { + print("Running test: " + test.desc); + test.run(); + }); +} diff --git a/toolkit/components/places/tests/unit/test_corrupt_telemetry.js b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js new file mode 100644 index 000000000..cd9e9ec0c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that history initialization correctly handles a request to forcibly +// replace the current database. + +add_task(function* () { + let profileDBPath = yield OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite"); + yield OS.File.remove(profileDBPath, {ignoreAbsent: true}); + // Ensure that our database doesn't already exist. + Assert.ok(!(yield OS.File.exists(profileDBPath)), "places.sqlite shouldn't exist"); + let dir = yield OS.File.getCurrentDirectory(); + let src = OS.Path.join(dir, "corruptDB.sqlite"); + yield OS.File.copy(src, profileDBPath); + Assert.ok(yield OS.File.exists(profileDBPath), "places.sqlite should exist"); + + let count = Services.telemetry + .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE") + .snapshot() + .counts[3]; + Assert.equal(count, 0, "There should be no telemetry"); + + do_check_eq(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); + + count = Services.telemetry + .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE") + .snapshot() + .counts[3]; + Assert.equal(count, 1, "Telemetry should have been added"); +}); diff --git a/toolkit/components/places/tests/unit/test_crash_476292.js b/toolkit/components/places/tests/unit/test_crash_476292.js new file mode 100644 index 000000000..8f0862022 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_crash_476292.js @@ -0,0 +1,28 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This tests a crash during startup found in bug 476292 that was caused by + * getting the bookmarks service during nsNavHistory::Init when the bookmarks + * service was created before the history service was. + */ + +function run_test() +{ + // First, we need to move our old database file into our test profile + // directory. This will trigger DATABASE_STATUS_UPGRADED (CREATE is not + // sufficient since there will be no entries to update frecencies for, which + // causes us to get the bookmarks service in the first place). + let dbFile = do_get_file("bug476292.sqlite"); + let profD = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile); + dbFile.copyTo(profD, "places.sqlite"); + + // Now get the bookmarks service. This will crash when the bug exists. + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +} diff --git a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js new file mode 100644 index 000000000..e83d0fdae --- /dev/null +++ b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that history initialization correctly handles a request to forcibly +// replace the current database. + +function run_test() { + // Ensure that our database doesn't already exist. + let dbFile = gProfD.clone(); + dbFile.append("places.sqlite"); + do_check_false(dbFile.exists()); + + dbFile = gProfD.clone(); + dbFile.append("places.sqlite.corrupt"); + do_check_false(dbFile.exists()); + + let file = do_get_file("default.sqlite"); + file.copyToFollowingLinks(gProfD, "places.sqlite"); + file = gProfD.clone(); + file.append("places.sqlite"); + + // Create some unique stuff to check later. + let db = Services.storage.openUnsharedDatabase(file); + db.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + db.close(); + + Services.prefs.setBoolPref("places.database.replaceOnStartup", true); + do_check_eq(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); + + dbFile = gProfD.clone(); + dbFile.append("places.sqlite"); + do_check_true(dbFile.exists()); + + // Check the new database is really a new one. + db = Services.storage.openUnsharedDatabase(file); + try { + db.executeSimpleSQL("DELETE * FROM test"); + do_throw("The new database should not have our unique content"); + } catch (ex) {} + db.close(); + + dbFile = gProfD.clone(); + dbFile.append("places.sqlite.corrupt"); + do_check_true(dbFile.exists()); +} diff --git a/toolkit/components/places/tests/unit/test_download_history.js b/toolkit/components/places/tests/unit/test_download_history.js new file mode 100644 index 000000000..643360b20 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_download_history.js @@ -0,0 +1,283 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the nsIDownloadHistory Places implementation. + */ + +XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory", + "@mozilla.org/browser/download-history;1", + "nsIDownloadHistory"); + +const DOWNLOAD_URI = NetUtil.newURI("http://www.example.com/"); +const REFERRER_URI = NetUtil.newURI("http://www.example.org/"); +const PRIVATE_URI = NetUtil.newURI("http://www.example.net/"); + +/** + * Waits for the first visit notification to be received. + * + * @param aCallback + * Callback function to be called with the same arguments of onVisit. + */ +function waitForOnVisit(aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onVisit: function HO_onVisit() { + PlacesUtils.history.removeObserver(this); + aCallback.apply(null, arguments); + } + }; + PlacesUtils.history.addObserver(historyObserver, false); +} + +/** + * Waits for the first onDeleteURI notification to be received. + * + * @param aCallback + * Callback function to be called with the same arguments of onDeleteURI. + */ +function waitForOnDeleteURI(aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onDeleteURI: function HO_onDeleteURI() { + PlacesUtils.history.removeObserver(this); + aCallback.apply(null, arguments); + } + }; + PlacesUtils.history.addObserver(historyObserver, false); +} + +/** + * Waits for the first onDeleteVisits notification to be received. + * + * @param aCallback + * Callback function to be called with the same arguments of onDeleteVisits. + */ +function waitForOnDeleteVisits(aCallback) { + let historyObserver = { + __proto__: NavHistoryObserver.prototype, + onDeleteVisits: function HO_onDeleteVisits() { + PlacesUtils.history.removeObserver(this); + aCallback.apply(null, arguments); + } + }; + PlacesUtils.history.addObserver(historyObserver, false); +} + +function run_test() +{ + run_next_test(); +} + +add_test(function test_dh_is_from_places() +{ + // Test that this nsIDownloadHistory is the one places implements. + do_check_true(gDownloadHistory instanceof Ci.mozIAsyncHistory); + + run_next_test(); +}); + +add_test(function test_dh_addRemoveDownload() +{ + waitForOnVisit(function DHAD_onVisit(aURI) { + do_check_true(aURI.equals(DOWNLOAD_URI)); + + // Verify that the URI is already available in results at this time. + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + waitForOnDeleteURI(function DHRAD_onDeleteURI(aDeletedURI) { + do_check_true(aDeletedURI.equals(DOWNLOAD_URI)); + + // Verify that the URI is already available in results at this time. + do_check_false(!!page_in_database(DOWNLOAD_URI)); + + run_next_test(); + }); + gDownloadHistory.removeAllDownloads(); + }); + + gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000); +}); + +add_test(function test_dh_addMultiRemoveDownload() +{ + PlacesTestUtils.addVisits({ + uri: DOWNLOAD_URI, + transition: TRANSITION_TYPED + }).then(function () { + waitForOnVisit(function DHAD_onVisit(aURI) { + do_check_true(aURI.equals(DOWNLOAD_URI)); + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) { + do_check_true(aDeletedURI.equals(DOWNLOAD_URI)); + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + PlacesTestUtils.clearHistory().then(run_next_test); + }); + gDownloadHistory.removeAllDownloads(); + }); + + gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000); + }); +}); + +add_test(function test_dh_addBookmarkRemoveDownload() +{ + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + DOWNLOAD_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A bookmark"); + waitForOnVisit(function DHAD_onVisit(aURI) { + do_check_true(aURI.equals(DOWNLOAD_URI)); + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) { + do_check_true(aDeletedURI.equals(DOWNLOAD_URI)); + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + PlacesTestUtils.clearHistory().then(run_next_test); + }); + gDownloadHistory.removeAllDownloads(); + }); + + gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000); +}); + +add_test(function test_dh_addDownload_referrer() +{ + waitForOnVisit(function DHAD_prepareReferrer(aURI, aVisitID) { + do_check_true(aURI.equals(REFERRER_URI)); + let referrerVisitId = aVisitID; + + waitForOnVisit(function DHAD_onVisit(aVisitedURI, unused, unused2, unused3, + aReferringID) { + do_check_true(aVisitedURI.equals(DOWNLOAD_URI)); + do_check_eq(aReferringID, referrerVisitId); + + // Verify that the URI is already available in results at this time. + do_check_true(!!page_in_database(DOWNLOAD_URI)); + + PlacesTestUtils.clearHistory().then(run_next_test); + }); + + gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000); + }); + + // Note that we don't pass the optional callback argument here because we must + // ensure that we receive the onVisit notification before we call addDownload. + PlacesUtils.asyncHistory.updatePlaces({ + uri: REFERRER_URI, + visits: [{ + transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED, + visitDate: Date.now() * 1000 + }] + }); +}); + +add_test(function test_dh_addDownload_disabledHistory() +{ + waitForOnVisit(function DHAD_onVisit(aURI) { + // We should only receive the notification for the non-private URI. This + // test is based on the assumption that visit notifications are received in + // the same order of the addDownload calls, which is currently true because + // database access is serialized on the same worker thread. + do_check_true(aURI.equals(DOWNLOAD_URI)); + + do_check_true(!!page_in_database(DOWNLOAD_URI)); + do_check_false(!!page_in_database(PRIVATE_URI)); + + PlacesTestUtils.clearHistory().then(run_next_test); + }); + + Services.prefs.setBoolPref("places.history.enabled", false); + gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000); + + // The addDownload functions calls CanAddURI synchronously, thus we can set + // the preference back to true immediately (not all apps enable places by + // default). + Services.prefs.setBoolPref("places.history.enabled", true); + gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000); +}); + +/** + * Tests that nsIDownloadHistory::AddDownload saves the additional download + * details if the optional destination URL is specified. + */ +add_test(function test_dh_details() +{ + const REMOTE_URI = NetUtil.newURI("http://localhost/"); + const SOURCE_URI = NetUtil.newURI("http://example.com/test_dh_details"); + const DEST_FILE_NAME = "dest.txt"; + + // We must build a real, valid file URI for the destination. + let destFileUri = NetUtil.newURI(FileUtils.getFile("TmpD", [DEST_FILE_NAME])); + + let titleSet = false; + let destinationFileUriSet = false; + let destinationFileNameSet = false; + + function checkFinished() + { + if (titleSet && destinationFileUriSet && destinationFileNameSet) { + PlacesUtils.annotations.removeObserver(annoObserver); + PlacesUtils.history.removeObserver(historyObserver); + + PlacesTestUtils.clearHistory().then(run_next_test); + } + } + + let annoObserver = { + onPageAnnotationSet: function AO_onPageAnnotationSet(aPage, aName) + { + if (aPage.equals(SOURCE_URI)) { + let value = PlacesUtils.annotations.getPageAnnotation(aPage, aName); + switch (aName) + { + case "downloads/destinationFileURI": + destinationFileUriSet = true; + do_check_eq(value, destFileUri.spec); + break; + case "downloads/destinationFileName": + destinationFileNameSet = true; + do_check_eq(value, DEST_FILE_NAME); + break; + } + checkFinished(); + } + }, + onItemAnnotationSet: function() {}, + onPageAnnotationRemoved: function() {}, + onItemAnnotationRemoved: function() {} + } + + let historyObserver = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function() {}, + onTitleChanged: function HO_onTitleChanged(aURI, aPageTitle) + { + if (aURI.equals(SOURCE_URI)) { + titleSet = true; + do_check_eq(aPageTitle, DEST_FILE_NAME); + checkFinished(); + } + }, + onDeleteURI: function() {}, + onClearHistory: function() {}, + onPageChanged: function() {}, + onDeleteVisits: function() {} + }; + + PlacesUtils.annotations.addObserver(annoObserver, false); + PlacesUtils.history.addObserver(historyObserver, false); + + // Both null values and remote URIs should not cause errors. + gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000); + gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, null); + gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, REMOTE_URI); + + // Valid local file URIs should cause the download details to be saved. + gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, + destFileUri); +}); diff --git a/toolkit/components/places/tests/unit/test_frecency.js b/toolkit/components/places/tests/unit/test_frecency.js new file mode 100644 index 000000000..a04befe00 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency.js @@ -0,0 +1,294 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 406358 to make sure frecency works for empty input/search, but + * this also tests for non-empty inputs as well. Because the interactions among + * *DIFFERENT* visit counts and visit dates is not well defined, this test + * holds one of the two values constant when modifying the other. + * + * Also test bug 419068 to make sure tagged pages don't necessarily have to be + * first in the results. + * + * Also test bug 426166 to make sure that the results of autocomplete searches + * are stable. Note that failures of this test will be intermittent by nature + * since we are testing to make sure that the unstable sort algorithm used + * by SQLite is not changing the order of the results on us. + */ + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +function ensure_results(uris, searchTerm) +{ + PlacesTestUtils.promiseAsyncUpdates() + .then(() => ensure_results_internal(uris, searchTerm)); +} + +function ensure_results_internal(uris, searchTerm) +{ + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + var numSearchesStarted = 0; + input.onSearchBegin = function() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + input.onSearchComplete = function() { + do_check_eq(numSearchesStarted, 1); + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + do_check_eq(controller.matchCount, uris.length); + for (var i=0; i<controller.matchCount; i++) { + do_check_eq(controller.getValueAt(i), uris[i].spec); + } + + deferEnsureResults.resolve(); + }; + + controller.startSearch(searchTerm); +} + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory); + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + var bmksvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +function* task_setCountDate(aURI, aCount, aDate) +{ + // We need visits so that frecency can be computed over multiple visits + let visits = []; + for (let i = 0; i < aCount; i++) { + visits.push({ uri: aURI, visitDate: aDate, transition: TRANSITION_TYPED }); + } + yield PlacesTestUtils.addVisits(visits); +} + +function setBookmark(aURI) +{ + bmksvc.insertBookmark(bmksvc.bookmarksMenuFolder, aURI, -1, "bleh"); +} + +function tagURI(aURI, aTags) { + bmksvc.insertBookmark(bmksvc.unfiledBookmarksFolder, aURI, + bmksvc.DEFAULT_INDEX, "bleh"); + tagssvc.tagURI(aURI, aTags); +} + +var uri1 = uri("http://site.tld/1"); +var uri2 = uri("http://site.tld/2"); +var uri3 = uri("http://aaaaaaaaaa/1"); +var uri4 = uri("http://aaaaaaaaaa/2"); + +// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec) +// Make sure the dates fall into different frecency buckets +var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000; +var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000; +// c1 is larger (should show up higher) than c2 +var c1 = 10; +var c2 = 1; + +var tests = [ +// test things without a search term +function*() { + print("TEST-INFO | Test 0: same count, different date"); + yield task_setCountDate(uri1, c1, d1); + yield task_setCountDate(uri2, c1, d2); + tagURI(uri1, ["site"]); + ensure_results([uri1, uri2], ""); +}, +function*() { + print("TEST-INFO | Test 1: same count, different date"); + yield task_setCountDate(uri1, c1, d2); + yield task_setCountDate(uri2, c1, d1); + tagURI(uri1, ["site"]); + ensure_results([uri2, uri1], ""); +}, +function*() { + print("TEST-INFO | Test 2: different count, same date"); + yield task_setCountDate(uri1, c1, d1); + yield task_setCountDate(uri2, c2, d1); + tagURI(uri1, ["site"]); + ensure_results([uri1, uri2], ""); +}, +function*() { + print("TEST-INFO | Test 3: different count, same date"); + yield task_setCountDate(uri1, c2, d1); + yield task_setCountDate(uri2, c1, d1); + tagURI(uri1, ["site"]); + ensure_results([uri2, uri1], ""); +}, + +// test things with a search term +function*() { + print("TEST-INFO | Test 4: same count, different date"); + yield task_setCountDate(uri1, c1, d1); + yield task_setCountDate(uri2, c1, d2); + tagURI(uri1, ["site"]); + ensure_results([uri1, uri2], "site"); +}, +function*() { + print("TEST-INFO | Test 5: same count, different date"); + yield task_setCountDate(uri1, c1, d2); + yield task_setCountDate(uri2, c1, d1); + tagURI(uri1, ["site"]); + ensure_results([uri2, uri1], "site"); +}, +function*() { + print("TEST-INFO | Test 6: different count, same date"); + yield task_setCountDate(uri1, c1, d1); + yield task_setCountDate(uri2, c2, d1); + tagURI(uri1, ["site"]); + ensure_results([uri1, uri2], "site"); +}, +function*() { + print("TEST-INFO | Test 7: different count, same date"); + yield task_setCountDate(uri1, c2, d1); + yield task_setCountDate(uri2, c1, d1); + tagURI(uri1, ["site"]); + ensure_results([uri2, uri1], "site"); +}, +// There are multiple tests for 8, hence the multiple functions +// Bug 426166 section +function*() { + print("TEST-INFO | Test 8.1a: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "a"); +}, +function*() { + print("TEST-INFO | Test 8.1b: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "aa"); +}, +function*() { + print("TEST-INFO | Test 8.2: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "aaa"); +}, +function*() { + print("TEST-INFO | Test 8.3: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "aaaa"); +}, +function*() { + print("TEST-INFO | Test 8.4: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "aaa"); +}, +function*() { + print("TEST-INFO | Test 8.5: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "aa"); +}, +function*() { + print("TEST-INFO | Test 8.6: same count, same date"); + setBookmark(uri3); + setBookmark(uri4); + ensure_results([uri4, uri3], "a"); +} +]; + +/** + * This deferred object contains a promise that is resolved when the + * ensure_results_internal function has finished its execution. + */ +var deferEnsureResults; + +add_task(function* test_frecency() +{ + // Disable autoFill for this test. + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill")); + // always search in history + bookmarks, no matter what the default is + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("browser.urlbar.suggest.history", true); + prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + for (let test of tests) { + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); + + deferEnsureResults = Promise.defer(); + yield test(); + yield deferEnsureResults.promise; + } + for (let type of ["history", "bookmark", "openpage"]) { + prefs.clearUserPref("browser.urlbar.suggest." + type); + } +}); diff --git a/toolkit/components/places/tests/unit/test_frecency_observers.js b/toolkit/components/places/tests/unit/test_frecency_observers.js new file mode 100644 index 000000000..7fadd4ae9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_observers.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + run_next_test(); +} + +// Each of these tests a path that triggers a frecency update. Together they +// hit all sites that update a frecency. + +// InsertVisitedURIs::UpdateFrecency and History::InsertPlace +add_task(function* test_InsertVisitedURIs_UpdateFrecency_and_History_InsertPlace() { + // InsertPlace is at the end of a path that UpdateFrecency is also on, so kill + // two birds with one stone and expect two notifications. Trigger the path by + // adding a download. + let uri = NetUtil.newURI("http://example.com/a"); + Cc["@mozilla.org/browser/download-history;1"]. + getService(Ci.nsIDownloadHistory). + addDownload(uri); + yield Promise.all([onFrecencyChanged(uri), onFrecencyChanged(uri)]); +}); + +// nsNavHistory::UpdateFrecency +add_task(function* test_nsNavHistory_UpdateFrecency() { + let bm = PlacesUtils.bookmarks; + let uri = NetUtil.newURI("http://example.com/b"); + bm.insertBookmark(bm.unfiledBookmarksFolder, uri, + Ci.nsINavBookmarksService.DEFAULT_INDEX, "test"); + yield onFrecencyChanged(uri); +}); + +// nsNavHistory::invalidateFrecencies for particular pages +add_task(function* test_nsNavHistory_invalidateFrecencies_somePages() { + let uri = NetUtil.newURI("http://test-nsNavHistory-invalidateFrecencies-somePages.com/"); + // Bookmarking the URI is enough to add it to moz_places, and importantly, it + // means that removePagesFromHost doesn't remove it from moz_places, so its + // frecency is able to be changed. + let bm = PlacesUtils.bookmarks; + bm.insertBookmark(bm.unfiledBookmarksFolder, uri, + Ci.nsINavBookmarksService.DEFAULT_INDEX, "test"); + PlacesUtils.history.removePagesFromHost(uri.host, false); + yield onFrecencyChanged(uri); +}); + +// nsNavHistory::invalidateFrecencies for all pages +add_task(function* test_nsNavHistory_invalidateFrecencies_allPages() { + yield Promise.all([onManyFrecenciesChanged(), PlacesTestUtils.clearHistory()]); +}); + +// nsNavHistory::DecayFrecency and nsNavHistory::FixInvalidFrecencies +add_task(function* test_nsNavHistory_DecayFrecency_and_nsNavHistory_FixInvalidFrecencies() { + // FixInvalidFrecencies is at the end of a path that DecayFrecency is also on, + // so expect two notifications. Trigger the path by making nsNavHistory + // observe the idle-daily notification. + PlacesUtils.history.QueryInterface(Ci.nsIObserver). + observe(null, "idle-daily", ""); + yield Promise.all([onManyFrecenciesChanged(), onManyFrecenciesChanged()]); +}); + +function onFrecencyChanged(expectedURI) { + let deferred = Promise.defer(); + let obs = new NavHistoryObserver(); + obs.onFrecencyChanged = + (uri, newFrecency, guid, hidden, visitDate) => { + PlacesUtils.history.removeObserver(obs); + do_check_true(!!uri); + do_check_true(uri.equals(expectedURI)); + deferred.resolve(); + }; + PlacesUtils.history.addObserver(obs, false); + return deferred.promise; +} + +function onManyFrecenciesChanged() { + let deferred = Promise.defer(); + let obs = new NavHistoryObserver(); + obs.onManyFrecenciesChanged = () => { + PlacesUtils.history.removeObserver(obs); + do_check_true(true); + deferred.resolve(); + }; + PlacesUtils.history.addObserver(obs, false); + return deferred.promise; +} diff --git a/toolkit/components/places/tests/unit/test_frecency_zero_updated.js b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js new file mode 100644 index 000000000..e60030ca5 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests a zero frecency is correctly updated when inserting new valid visits. + +function run_test() +{ + run_next_test() +} + +add_task(function* () +{ + const TEST_URI = NetUtil.newURI("http://example.com/"); + let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + TEST_URI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A title"); + yield PlacesTestUtils.promiseAsyncUpdates(); + do_check_true(frecencyForUrl(TEST_URI) > 0); + + // Removing the bookmark should leave an orphan page with zero frecency. + // Note this would usually be expired later by expiration. + PlacesUtils.bookmarks.removeItem(id); + yield PlacesTestUtils.promiseAsyncUpdates(); + do_check_eq(frecencyForUrl(TEST_URI), 0); + + // Now add a valid visit to the page, frecency should increase. + yield PlacesTestUtils.addVisits({ uri: TEST_URI }); + do_check_true(frecencyForUrl(TEST_URI) > 0); +}); diff --git a/toolkit/components/places/tests/unit/test_getChildIndex.js b/toolkit/components/places/tests/unit/test_getChildIndex.js new file mode 100644 index 000000000..4cf164d45 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_getChildIndex.js @@ -0,0 +1,69 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests nsNavHistoryContainerResultNode::GetChildIndex(aNode) functionality. + */ + +function run_test() { + // Add a bookmark to the menu. + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, + uri("http://test.mozilla.org/bookmark/"), + Ci.nsINavBookmarksService.DEFAULT_INDEX, + "Test bookmark"); + + // Add a bookmark to unfiled folder. + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("http://test.mozilla.org/unfiled/"), + Ci.nsINavBookmarksService.DEFAULT_INDEX, + "Unfiled bookmark"); + + // Get the unfiled bookmark node. + let unfiledNode = getNodeAt(PlacesUtils.unfiledBookmarksFolderId, 0); + if (!unfiledNode) + do_throw("Unable to find bookmark in hierarchy!"); + do_check_eq(unfiledNode.title, "Unfiled bookmark"); + + let hs = PlacesUtils.history; + let query = hs.getNewQuery(); + query.setFolders([PlacesUtils.bookmarksMenuFolderId], 1); + let options = hs.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = hs.executeQuery(query, options).root; + root.containerOpen = true; + + // Check functionality for proper nodes. + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + print("Now testing: " + node.title); + do_check_eq(root.getChildIndex(node), i); + } + + // Now search for an invalid node and expect an exception. + try { + root.getChildIndex(unfiledNode); + do_throw("Searching for an invalid node should have thrown."); + } catch (ex) { + print("We correctly got an exception."); + } + + root.containerOpen = false; +} + +function getNodeAt(aFolderId, aIndex) { + let hs = PlacesUtils.history; + let query = hs.getNewQuery(); + query.setFolders([aFolderId], 1); + let options = hs.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + let root = hs.executeQuery(query, options).root; + root.containerOpen = true; + if (root.childCount < aIndex) + do_throw("Not enough children to find bookmark!"); + let node = root.getChild(aIndex); + root.containerOpen = false; + return node; +} diff --git a/toolkit/components/places/tests/unit/test_getPlacesInfo.js b/toolkit/components/places/tests/unit/test_getPlacesInfo.js new file mode 100644 index 000000000..3dfecb934 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_getPlacesInfo.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function promiseGetPlacesInfo(aPlacesIdentifiers) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.getPlacesInfo(aPlacesIdentifiers, { + _results: [], + _errors: [], + + handleResult: function handleResult(aPlaceInfo) { + this._results.push(aPlaceInfo); + }, + handleError: function handleError(aResultCode, aPlaceInfo) { + this._errors.push({ resultCode: aResultCode, info: aPlaceInfo }); + }, + handleCompletion: function handleCompletion() { + deferred.resolve({ errors: this._errors, results: this._results }); + } + }); + + return deferred.promise; +} + +function ensurePlacesInfoObjectsAreEqual(a, b) { + do_check_true(a.uri.equals(b.uri)); + do_check_eq(a.title, b.title); + do_check_eq(a.guid, b.guid); + do_check_eq(a.placeId, b.placeId); +} + +function* test_getPlacesInfoExistentPlace() { + let testURI = NetUtil.newURI("http://www.example.tld"); + yield PlacesTestUtils.addVisits(testURI); + + let getPlacesInfoResult = yield promiseGetPlacesInfo([testURI]); + do_check_eq(getPlacesInfoResult.results.length, 1); + do_check_eq(getPlacesInfoResult.errors.length, 0); + + let placeInfo = getPlacesInfoResult.results[0]; + do_check_true(placeInfo instanceof Ci.mozIPlaceInfo); + + do_check_true(placeInfo.uri.equals(testURI)); + do_check_eq(placeInfo.title, "test visit for " + testURI.spec); + do_check_true(placeInfo.guid.length > 0); + do_check_eq(placeInfo.visits, null); +} +add_task(test_getPlacesInfoExistentPlace); + +function* test_getPlacesInfoNonExistentPlace() { + let testURI = NetUtil.newURI("http://www.example_non_existent.tld"); + let getPlacesInfoResult = yield promiseGetPlacesInfo(testURI); + do_check_eq(getPlacesInfoResult.results.length, 0); + do_check_eq(getPlacesInfoResult.errors.length, 1); +} +add_task(test_getPlacesInfoNonExistentPlace); + +function* test_promisedHelper() { + let uri = NetUtil.newURI("http://www.helper_existent_example.tld"); + yield PlacesTestUtils.addVisits(uri); + let placeInfo = yield PlacesUtils.promisePlaceInfo(uri); + do_check_true(placeInfo instanceof Ci.mozIPlaceInfo); + + uri = NetUtil.newURI("http://www.helper_non_existent_example.tld"); + try { + yield PlacesUtils.promisePlaceInfo(uri); + do_throw("PlacesUtils.promisePlaceInfo should have rejected the promise"); + } + catch (ex) { } +} +add_task(test_promisedHelper); + +function* test_infoByGUID() { + let testURI = NetUtil.newURI("http://www.guid_example.tld"); + yield PlacesTestUtils.addVisits(testURI); + + let placeInfoByURI = yield PlacesUtils.promisePlaceInfo(testURI); + let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo(placeInfoByURI.guid); + ensurePlacesInfoObjectsAreEqual(placeInfoByURI, placeInfoByGUID); +} +add_task(test_infoByGUID); + +function* test_invalid_guid() { + try { + yield PlacesUtils.promisePlaceInfo("###"); + do_throw("getPlacesInfo should fail for invalid guids") + } + catch (ex) { } +} +add_task(test_invalid_guid); + +function* test_mixed_selection() { + let placeInfo1, placeInfo2; + let uri = NetUtil.newURI("http://www.mixed_selection_test_1.tld"); + yield PlacesTestUtils.addVisits(uri); + placeInfo1 = yield PlacesUtils.promisePlaceInfo(uri); + + uri = NetUtil.newURI("http://www.mixed_selection_test_2.tld"); + yield PlacesTestUtils.addVisits(uri); + placeInfo2 = yield PlacesUtils.promisePlaceInfo(uri); + + let getPlacesInfoResult = yield promiseGetPlacesInfo([placeInfo1.uri, placeInfo2.guid]); + do_check_eq(getPlacesInfoResult.results.length, 2); + do_check_eq(getPlacesInfoResult.errors.length, 0); + + do_check_eq(getPlacesInfoResult.results[0].uri.spec, placeInfo1.uri.spec); + do_check_eq(getPlacesInfoResult.results[1].guid, placeInfo2.guid); +} +add_task(test_mixed_selection); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_history.js b/toolkit/components/places/tests/unit/test_history.js new file mode 100644 index 000000000..8d194cde1 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history.js @@ -0,0 +1,184 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history services +var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + +/** + * Checks to see that a URI is in the database. + * + * @param aURI + * The URI to check. + * @returns true if the URI is in the DB, false otherwise. + */ +function uri_in_db(aURI) { + var options = histsvc.getNewQueryOptions(); + options.maxResults = 1; + options.resultType = options.RESULTS_AS_URI + var query = histsvc.getNewQuery(); + query.uri = aURI; + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + root.containerOpen = false; + return (cc == 1); +} + +// main +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + // we have a new profile, so we should have imported bookmarks + do_check_eq(histsvc.databaseStatus, histsvc.DATABASE_STATUS_CREATE); + + // add a visit + var testURI = uri("http://mozilla.com"); + yield PlacesTestUtils.addVisits(testURI); + + // now query for the visit, setting sorting and limit such that + // we should retrieve only the visit we just added + var options = histsvc.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.maxResults = 1; + // TODO: using full visit crashes in xpcshell test + // options.resultType = options.RESULTS_AS_FULL_VISIT; + options.resultType = options.RESULTS_AS_VISIT; + var query = histsvc.getNewQuery(); + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + for (var i=0; i < cc; ++i) { + var node = root.getChild(i); + // test node properties in RESULTS_AS_VISIT + do_check_eq(node.uri, testURI.spec); + do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI); + // TODO: change query type to RESULTS_AS_FULL_VISIT and test this + // do_check_eq(node.transitionType, histsvc.TRANSITION_TYPED); + } + root.containerOpen = false; + + // add another visit for the same URI, and a third visit for a different URI + var testURI2 = uri("http://google.com/"); + yield PlacesTestUtils.addVisits(testURI); + yield PlacesTestUtils.addVisits(testURI2); + + options.maxResults = 5; + options.resultType = options.RESULTS_AS_URI; + + // test minVisits + query.minVisits = 0; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 2); + result.root.containerOpen = false; + query.minVisits = 1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 2); + result.root.containerOpen = false; + query.minVisits = 2; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 1); + query.minVisits = 3; + result.root.containerOpen = false; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 0); + result.root.containerOpen = false; + + // test maxVisits + query.minVisits = -1; + query.maxVisits = -1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 2); + result.root.containerOpen = false; + query.maxVisits = 0; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 0); + result.root.containerOpen = false; + query.maxVisits = 1; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 1); + result.root.containerOpen = false; + query.maxVisits = 2; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 2); + result.root.containerOpen = false; + query.maxVisits = 3; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 2); + result.root.containerOpen = false; + + // test annotation-based queries + var annos = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + annos.setPageAnnotation(uri("http://mozilla.com/"), "testAnno", 0, 0, + Ci.nsIAnnotationService.EXPIRE_NEVER); + query.annotation = "testAnno"; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 1); + do_check_eq(result.root.getChild(0).uri, "http://mozilla.com/"); + result.root.containerOpen = false; + + // test annotationIsNot + query.annotationIsNot = true; + result = histsvc.executeQuery(query, options); + result.root.containerOpen = true; + do_check_eq(result.root.childCount, 1); + do_check_eq(result.root.getChild(0).uri, "http://google.com/"); + result.root.containerOpen = false; + + // By default history is enabled. + do_check_true(!histsvc.historyDisabled); + + // test getPageTitle + yield PlacesTestUtils.addVisits({ uri: uri("http://example.com"), title: "title" }); + let placeInfo = yield PlacesUtils.promisePlaceInfo(uri("http://example.com")); + do_check_eq(placeInfo.title, "title"); + + // query for the visit + do_check_true(uri_in_db(testURI)); + + // test for schema changes in bug 373239 + // get direct db connection + var db = histsvc.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; + var q = "SELECT id FROM moz_bookmarks"; + var statement; + try { + statement = db.createStatement(q); + } catch (ex) { + do_throw("bookmarks table does not have id field, schema is too old!"); + } + finally { + statement.finalize(); + } + + // bug 394741 - regressed history text searches + yield PlacesTestUtils.addVisits(uri("http://mozilla.com")); + options = histsvc.getNewQueryOptions(); + // options.resultType = options.RESULTS_AS_VISIT; + query = histsvc.getNewQuery(); + query.searchTerms = "moz"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_true(root.childCount > 0); + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js new file mode 100644 index 000000000..a5e0e1cb1 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js @@ -0,0 +1,185 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var current_test = 0; + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +function ensure_tag_results(uris, searchTerm) +{ + print("Searching for '" + searchTerm + "'"); + var controller = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["unifiedcomplete"]); + + controller.input = input; + + // Search is asynchronous, so don't let the test finish immediately + do_test_pending(); + + var numSearchesStarted = 0; + input.onSearchBegin = function() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + input.onSearchComplete = function() { + do_check_eq(numSearchesStarted, 1); + do_check_eq(controller.searchStatus, + uris.length ? + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH : + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH); + do_check_eq(controller.matchCount, uris.length); + let vals = []; + for (let i=0; i<controller.matchCount; i++) { + // Keep the URL for later because order of tag results is undefined + vals.push(controller.getValueAt(i)); + do_check_eq(controller.getStyleAt(i), "bookmark-tag"); + } + // Sort the results then check if we have the right items + vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec)) + + if (current_test < (tests.length - 1)) { + current_test++; + tests[current_test](); + } + + do_test_finished(); + }; + + controller.startSearch(searchTerm); +} + +var uri1 = uri("http://site.tld/1/aaa"); +var uri2 = uri("http://site.tld/2/bbb"); +var uri3 = uri("http://site.tld/3/aaa"); +var uri4 = uri("http://site.tld/4/bbb"); +var uri5 = uri("http://site.tld/5/aaa"); +var uri6 = uri("http://site.tld/6/bbb"); + +var tests = [ + () => ensure_tag_results([uri1, uri4, uri6], "foo"), + () => ensure_tag_results([uri1], "foo aaa"), + () => ensure_tag_results([uri4, uri6], "foo bbb"), + () => ensure_tag_results([uri2, uri4, uri5, uri6], "bar"), + () => ensure_tag_results([uri5], "bar aaa"), + () => ensure_tag_results([uri2, uri4, uri6], "bar bbb"), + () => ensure_tag_results([uri3, uri5, uri6], "cheese"), + () => ensure_tag_results([uri3, uri5], "chees aaa"), + () => ensure_tag_results([uri6], "chees bbb"), + () => ensure_tag_results([uri4, uri6], "fo bar"), + () => ensure_tag_results([], "fo bar aaa"), + () => ensure_tag_results([uri4, uri6], "fo bar bbb"), + () => ensure_tag_results([uri4, uri6], "ba foo"), + () => ensure_tag_results([], "ba foo aaa"), + () => ensure_tag_results([uri4, uri6], "ba foo bbb"), + () => ensure_tag_results([uri5, uri6], "ba chee"), + () => ensure_tag_results([uri5], "ba chee aaa"), + () => ensure_tag_results([uri6], "ba chee bbb"), + () => ensure_tag_results([uri5, uri6], "cheese bar"), + () => ensure_tag_results([uri5], "cheese bar aaa"), + () => ensure_tag_results([uri6], "chees bar bbb"), + () => ensure_tag_results([uri6], "cheese bar foo"), + () => ensure_tag_results([], "foo bar cheese aaa"), + () => ensure_tag_results([uri6], "foo bar cheese bbb"), +]; + +/** + * Properly tags a uri adding it to bookmarks. + * + * @param aURI + * The nsIURI to tag. + * @param aTags + * The tags to add. + */ +function tagURI(aURI, aTags) { + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + aURI, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A title"); + tagssvc.tagURI(aURI, aTags); +} + +/** + * Test history autocomplete + */ +function run_test() { + // always search in history + bookmarks, no matter what the default is + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.setIntPref("browser.urlbar.search.sources", 3); + prefs.setIntPref("browser.urlbar.default.behavior", 0); + + tagURI(uri1, ["foo"]); + tagURI(uri2, ["bar"]); + tagURI(uri3, ["cheese"]); + tagURI(uri4, ["foo bar"]); + tagURI(uri5, ["bar cheese"]); + tagURI(uri6, ["foo bar cheese"]); + + tests[0](); +} diff --git a/toolkit/components/places/tests/unit/test_history_catobs.js b/toolkit/components/places/tests/unit/test_history_catobs.js new file mode 100644 index 000000000..e0a81d67b --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_catobs.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + + +function run_test() { + run_next_test(); +} + +add_task(function* () { + do_load_manifest("nsDummyObserver.manifest"); + + let dummyCreated = false; + let dummyReceivedOnVisit = false; + + Services.obs.addObserver(function created() { + Services.obs.removeObserver(created, "dummy-observer-created"); + dummyCreated = true; + }, "dummy-observer-created", false); + Services.obs.addObserver(function visited() { + Services.obs.removeObserver(visited, "dummy-observer-visited"); + dummyReceivedOnVisit = true; + }, "dummy-observer-visited", false); + + let initialObservers = PlacesUtils.history.getObservers(); + + // Add a common observer, it should be invoked after the category observer. + let notificationsPromised = new Promise((resolve, reject) => { + PlacesUtils.history.addObserver({ + __proto__: NavHistoryObserver.prototype, + onVisit() { + let observers = PlacesUtils.history.getObservers(); + Assert.equal(observers.length, initialObservers.length + 1); + + // Check the common observer is the last one. + for (let i = 0; i < initialObservers.length; ++i) { + Assert.equal(initialObservers[i], observers[i]); + } + + PlacesUtils.history.removeObserver(this); + observers = PlacesUtils.history.getObservers(); + Assert.equal(observers.length, initialObservers.length); + + // Check the category observer has been invoked before this one. + Assert.ok(dummyCreated); + Assert.ok(dummyReceivedOnVisit); + resolve(); + } + }, false); + }); + + // Add a visit. + yield PlacesTestUtils.addVisits(uri("http://typed.mozilla.org")); + + yield notificationsPromised; +}); diff --git a/toolkit/components/places/tests/unit/test_history_clear.js b/toolkit/components/places/tests/unit/test_history_clear.js new file mode 100644 index 000000000..56d34994f --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_clear.js @@ -0,0 +1,169 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var mDBConn = DBConn(); + +function promiseOnClearHistoryObserved() { + let deferred = Promise.defer(); + + let historyObserver = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onVisit: function() {}, + onTitleChanged: function() {}, + onDeleteURI: function(aURI) {}, + onPageChanged: function() {}, + onDeleteVisits: function() {}, + + onClearHistory: function() { + PlacesUtils.history.removeObserver(this, false); + deferred.resolve(); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + ]) + } + PlacesUtils.history.addObserver(historyObserver, false); + return deferred.promise; +} + +// This global variable is a promise object, initialized in run_test and waited +// upon in the first asynchronous test. It is resolved when the +// "places-init-complete" notification is received. We cannot initialize it in +// the asynchronous test, because then it's too late to register the observer. +var promiseInit; + +function run_test() { + // places-init-complete is notified after run_test, and it will + // run a first frecency fix through async statements. + // To avoid random failures we have to run after all of this. + promiseInit = promiseTopicObserved(PlacesUtils.TOPIC_INIT_COMPLETE); + + run_next_test(); +} + +add_task(function* test_history_clear() +{ + yield promiseInit; + + yield PlacesTestUtils.addVisits([ + { uri: uri("http://typed.mozilla.org/"), + transition: TRANSITION_TYPED }, + { uri: uri("http://link.mozilla.org/"), + transition: TRANSITION_LINK }, + { uri: uri("http://download.mozilla.org/"), + transition: TRANSITION_DOWNLOAD }, + { uri: uri("http://redir_temp.mozilla.org/"), + transition: TRANSITION_REDIRECT_TEMPORARY, + referrer: "http://link.mozilla.org/"}, + { uri: uri("http://redir_perm.mozilla.org/"), + transition: TRANSITION_REDIRECT_PERMANENT, + referrer: "http://link.mozilla.org/"}, + ]); + + // add a place: bookmark + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("place:folder=4"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "shortcut"); + + // Add an expire never annotation + // Actually expire never annotations are removed as soon as a page is removed + // from the database, so this should act as a normal visit. + PlacesUtils.annotations.setPageAnnotation(uri("http://download.mozilla.org/"), + "never", "never", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + // Add a bookmark + // Bookmarked page should have history cleared and frecency = -1 + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + uri("http://typed.mozilla.org/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark"); + + yield PlacesTestUtils.addVisits([ + { uri: uri("http://typed.mozilla.org/"), + transition: TRANSITION_BOOKMARK }, + { uri: uri("http://frecency.mozilla.org/"), + transition: TRANSITION_LINK }, + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); + + // Clear history and wait for the onClearHistory notification. + let promiseWaitClearHistory = promiseOnClearHistoryObserved(); + PlacesUtils.history.clear(); + yield promiseWaitClearHistory; + + // check browserHistory returns no entries + do_check_eq(0, PlacesUtils.history.hasHistoryEntries); + + yield promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + yield PlacesTestUtils.promiseAsyncUpdates(); + + // Check that frecency for not cleared items (bookmarks) has been converted + // to -1. + stmt = mDBConn.createStatement( + "SELECT h.id FROM moz_places h WHERE h.frecency > 0 "); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h WHERE h.frecency < 0 + AND EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + // Check that all visit_counts have been brought to 0 + stmt = mDBConn.createStatement( + "SELECT id FROM moz_places WHERE visit_count <> 0 LIMIT 1"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that history tables are empty + stmt = mDBConn.createStatement( + "SELECT * FROM (SELECT id FROM moz_historyvisits LIMIT 1)"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that all moz_places entries except bookmarks and place: have been removed + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h WHERE + url_hash NOT BETWEEN hash('place', 'prefix_lo') AND hash('place', 'prefix_hi') + AND NOT EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that we only have favicons for retained places + stmt = mDBConn.createStatement( + `SELECT f.id FROM moz_favicons f WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE favicon_id = f.id) LIMIT 1`); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that we only have annotations for retained places + stmt = mDBConn.createStatement( + `SELECT a.id FROM moz_annos a WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = a.place_id) LIMIT 1`); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that we only have inputhistory for retained places + stmt = mDBConn.createStatement( + `SELECT i.place_id FROM moz_inputhistory i WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = i.place_id) LIMIT 1`); + do_check_false(stmt.executeStep()); + stmt.finalize(); + + // Check that place:uris have frecency 0 + stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h + WHERE url_hash BETWEEN hash('place', 'prefix_lo') + AND hash('place', 'prefix_hi') + AND h.frecency <> 0 LIMIT 1`); + do_check_false(stmt.executeStep()); + stmt.finalize(); +}); diff --git a/toolkit/components/places/tests/unit/test_history_notifications.js b/toolkit/components/places/tests/unit/test_history_notifications.js new file mode 100644 index 000000000..4e1e635a0 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_notifications.js @@ -0,0 +1,38 @@ +const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete"; +const NS_PLACES_DATABASE_LOCKED_TOPIC = "places-database-locked"; + +add_task(function* () { + // Create a dummy places.sqlite and open an unshared connection on it + let db = Services.dirsvc.get('ProfD', Ci.nsIFile); + db.append("places.sqlite"); + let dbConn = Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists(), "The database should have been created"); + + // We need an exclusive lock on the db + dbConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE"); + // Exclusive locking is lazy applied, we need to make a write to activate it + dbConn.executeSimpleSQL("PRAGMA USER_VERSION = 1"); + + // Try to create history service while the db is locked + let promiseLocked = promiseTopicObserved(NS_PLACES_DATABASE_LOCKED_TOPIC); + Assert.throws(() => Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsINavHistoryService), + /NS_ERROR_XPC_GS_RETURNED_FAILURE/); + yield promiseLocked; + + // Close our connection and try to cleanup the file (could fail on Windows) + dbConn.close(); + if (db.exists()) { + try { + db.remove(false); + } catch (e) { + do_print("Unable to remove dummy places.sqlite"); + } + } + + // Create history service correctly + let promiseComplete = promiseTopicObserved(NS_PLACES_INIT_COMPLETE_TOPIC); + Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsINavHistoryService); + yield promiseComplete; +}); diff --git a/toolkit/components/places/tests/unit/test_history_observer.js b/toolkit/components/places/tests/unit/test_history_observer.js new file mode 100644 index 000000000..c101cfb61 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_observer.js @@ -0,0 +1,215 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Generic nsINavHistoryObserver that doesn't implement anything, but provides + * dummy methods to prevent errors about an object not having a certain method. + */ +function NavHistoryObserver() { +} +NavHistoryObserver.prototype = { + onBeginUpdateBatch: function() { }, + onEndUpdateBatch: function() { }, + onVisit: function() { }, + onTitleChanged: function() { }, + onDeleteURI: function() { }, + onClearHistory: function() { }, + onPageChanged: function() { }, + onDeleteVisits: function() { }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]) +}; + +/** + * Registers a one-time history observer for and calls the callback + * when the specified nsINavHistoryObserver method is called. + * Returns a promise that is resolved when the callback returns. + */ +function onNotify(callback) { + return new Promise(resolve => { + let obs = new NavHistoryObserver(); + obs[callback.name] = function () { + PlacesUtils.history.removeObserver(this); + callback.apply(this, arguments); + resolve(); + }; + PlacesUtils.history.addObserver(obs, false); + }); +} + +/** + * Asynchronous task that adds a visit to the history database. + */ +function* task_add_visit(uri, timestamp, transition) { + uri = uri || NetUtil.newURI("http://firefox.com/"); + timestamp = timestamp || Date.now() * 1000; + yield PlacesTestUtils.addVisits({ + uri: uri, + transition: transition || TRANSITION_TYPED, + visitDate: timestamp + }); + return [uri, timestamp]; +} + +add_task(function* test_onVisit() { + let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime, + aSessionID, aReferringID, + aTransitionType, aGUID, + aHidden, aVisitCount, aTyped) { + Assert.ok(aURI.equals(testuri)); + Assert.ok(aVisitID > 0); + Assert.equal(aTime, testtime); + Assert.equal(aSessionID, 0); + Assert.equal(aReferringID, 0); + Assert.equal(aTransitionType, TRANSITION_TYPED); + do_check_guid_for_uri(aURI, aGUID); + Assert.ok(!aHidden); + Assert.equal(aVisitCount, 1); + Assert.equal(aTyped, 1); + }); + let testuri = NetUtil.newURI("http://firefox.com/"); + let testtime = Date.now() * 1000; + yield task_add_visit(testuri, testtime); + yield promiseNotify; +}); + +add_task(function* test_onVisit() { + let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime, + aSessionID, aReferringID, + aTransitionType, aGUID, + aHidden, aVisitCount, aTyped) { + Assert.ok(aURI.equals(testuri)); + Assert.ok(aVisitID > 0); + Assert.equal(aTime, testtime); + Assert.equal(aSessionID, 0); + Assert.equal(aReferringID, 0); + Assert.equal(aTransitionType, TRANSITION_FRAMED_LINK); + do_check_guid_for_uri(aURI, aGUID); + Assert.ok(aHidden); + Assert.equal(aVisitCount, 1); + Assert.equal(aTyped, 0); + }); + let testuri = NetUtil.newURI("http://hidden.firefox.com/"); + let testtime = Date.now() * 1000; + yield task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK); + yield promiseNotify; +}); + +add_task(function* test_multiple_onVisit() { + let testuri = NetUtil.newURI("http://self.firefox.com/"); + let promiseNotifications = new Promise(resolve => { + let observer = { + _c: 0, + __proto__: NavHistoryObserver.prototype, + onVisit(uri, id, time, unused, referrerId, transition, guid, + hidden, visitCount, typed) { + Assert.ok(testuri.equals(uri)); + Assert.ok(id > 0); + Assert.ok(time > 0); + Assert.ok(!hidden); + do_check_guid_for_uri(uri, guid); + switch (++this._c) { + case 1: + Assert.equal(referrerId, 0); + Assert.equal(transition, TRANSITION_LINK); + Assert.equal(visitCount, 1); + Assert.equal(typed, 0); + break; + case 2: + Assert.ok(referrerId > 0); + Assert.equal(transition, TRANSITION_LINK); + Assert.equal(visitCount, 2); + Assert.equal(typed, 0); + break; + case 3: + Assert.equal(referrerId, 0); + Assert.equal(transition, TRANSITION_TYPED); + Assert.equal(visitCount, 3); + Assert.equal(typed, 1); + + PlacesUtils.history.removeObserver(observer, false); + resolve(); + break; + } + } + }; + PlacesUtils.history.addObserver(observer, false); + }); + yield PlacesTestUtils.addVisits([ + { uri: testuri, transition: TRANSITION_LINK }, + { uri: testuri, referrer: testuri, transition: TRANSITION_LINK }, + { uri: testuri, transition: TRANSITION_TYPED }, + ]); + yield promiseNotifications; +}); + +add_task(function* test_onDeleteURI() { + let promiseNotify = onNotify(function onDeleteURI(aURI, aGUID, aReason) { + Assert.ok(aURI.equals(testuri)); + // Can't use do_check_guid_for_uri() here because the visit is already gone. + Assert.equal(aGUID, testguid); + Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED); + }); + let [testuri] = yield task_add_visit(); + let testguid = do_get_guid_for_uri(testuri); + PlacesUtils.bhistory.removePage(testuri); + yield promiseNotify; +}); + +add_task(function* test_onDeleteVisits() { + let promiseNotify = onNotify(function onDeleteVisits(aURI, aVisitTime, aGUID, + aReason) { + Assert.ok(aURI.equals(testuri)); + // Can't use do_check_guid_for_uri() here because the visit is already gone. + Assert.equal(aGUID, testguid); + Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED); + Assert.equal(aVisitTime, 0); // All visits have been removed. + }); + let msecs24hrsAgo = Date.now() - (86400 * 1000); + let [testuri] = yield task_add_visit(undefined, msecs24hrsAgo * 1000); + // Add a bookmark so the page is not removed. + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + testuri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + let testguid = do_get_guid_for_uri(testuri); + PlacesUtils.bhistory.removePage(testuri); + yield promiseNotify; +}); + +add_task(function* test_onTitleChanged() { + let promiseNotify = onNotify(function onTitleChanged(aURI, aTitle, aGUID) { + Assert.ok(aURI.equals(testuri)); + Assert.equal(aTitle, title); + do_check_guid_for_uri(aURI, aGUID); + }); + + let [testuri] = yield task_add_visit(); + let title = "test-title"; + yield PlacesTestUtils.addVisits({ + uri: testuri, + title: title + }); + yield promiseNotify; +}); + +add_task(function* test_onPageChanged() { + let promiseNotify = onNotify(function onPageChanged(aURI, aChangedAttribute, + aNewValue, aGUID) { + Assert.equal(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON); + Assert.ok(aURI.equals(testuri)); + Assert.equal(aNewValue, SMALLPNG_DATA_URI.spec); + do_check_guid_for_uri(aURI, aGUID); + }); + + let [testuri] = yield task_add_visit(); + + // The new favicon for the page must have data associated with it in order to + // receive the onPageChanged notification. To keep this test self-contained, + // we use an URI representing the smallest possible PNG file. + PlacesUtils.favicons.setAndFetchFaviconForPage(testuri, SMALLPNG_DATA_URI, + false, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal()); + yield promiseNotify; +}); diff --git a/toolkit/components/places/tests/unit/test_history_sidebar.js b/toolkit/components/places/tests/unit/test_history_sidebar.js new file mode 100644 index 000000000..1c03547d7 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_history_sidebar.js @@ -0,0 +1,447 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +var hs = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +var bh = hs.QueryInterface(Ci.nsIBrowserHistory); +var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +var ps = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + +/** + * Adds a test URI visit to the database. + * + * @param aURI + * The URI to add a visit for. + * @param aTime + * Reference "now" time. + * @param aDayOffset + * number of days to add, pass a negative value to subtract them. + */ +function* task_add_normalized_visit(aURI, aTime, aDayOffset) { + var dateObj = new Date(aTime); + // Normalize to midnight + dateObj.setHours(0); + dateObj.setMinutes(0); + dateObj.setSeconds(0); + dateObj.setMilliseconds(0); + // Days where DST changes should be taken in count. + var previousDateObj = new Date(dateObj.getTime() + aDayOffset * 86400000); + var DSTCorrection = (dateObj.getTimezoneOffset() - + previousDateObj.getTimezoneOffset()) * 60 * 1000; + // Substract aDayOffset + var PRTimeWithOffset = (previousDateObj.getTime() - DSTCorrection) * 1000; + var timeInMs = new Date(PRTimeWithOffset/1000); + print("Adding visit to " + aURI.spec + " at " + timeInMs); + yield PlacesTestUtils.addVisits({ + uri: aURI, + visitDate: PRTimeWithOffset + }); +} + +function days_for_x_months_ago(aNowObj, aMonths) { + var oldTime = new Date(); + // Set day before month, otherwise we could try to calculate 30 February, or + // other nonexistent days. + oldTime.setDate(1); + oldTime.setMonth(aNowObj.getMonth() - aMonths); + oldTime.setHours(0); + oldTime.setMinutes(0); + oldTime.setSeconds(0); + // Stay larger for eventual timezone issues, add 2 days. + return parseInt((aNowObj - oldTime) / (1000*60*60*24)) + 2; +} + +var nowObj = new Date(); +// This test relies on en-US locale +// Offset is number of days +/* eslint-disable comma-spacing */ +var containers = [ + { label: "Today" , offset: 0 , visible: true }, + { label: "Yesterday" , offset: -1 , visible: true }, + { label: "Last 7 days" , offset: -3 , visible: true }, + { label: "This month" , offset: -8 , visible: nowObj.getDate() > 8 }, + { label: "" , offset: -days_for_x_months_ago(nowObj, 0) , visible: true }, + { label: "" , offset: -days_for_x_months_ago(nowObj, 1) , visible: true }, + { label: "" , offset: -days_for_x_months_ago(nowObj, 2) , visible: true }, + { label: "" , offset: -days_for_x_months_ago(nowObj, 3) , visible: true }, + { label: "" , offset: -days_for_x_months_ago(nowObj, 4) , visible: true }, + { label: "Older than 6 months" , offset: -days_for_x_months_ago(nowObj, 5) , visible: true }, +]; +/* eslint-enable comma-spacing */ + +var visibleContainers = containers.filter( + function(aContainer) { return aContainer.visible }); + +/** + * Asynchronous task that fills history and checks containers' labels. + */ +function* task_fill_history() { + print("\n\n*** TEST Fill History\n"); + // We can't use "now" because our hardcoded offsets would be invalid for some + // date. So we hardcode a date. + for (let i = 0; i < containers.length; i++) { + let container = containers[i]; + var testURI = uri("http://mirror"+i+".mozilla.com/b"); + yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset); + testURI = uri("http://mirror"+i+".mozilla.com/a"); + yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset); + testURI = uri("http://mirror"+i+".google.com/b"); + yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset); + testURI = uri("http://mirror"+i+".google.com/a"); + yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset); + // Bug 485703 - Hide date containers not containing additional entries + // compared to previous ones. + // Check after every new container is added. + check_visit(container.offset); + } + + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_SITE_QUERY; + var query = hs.getNewQuery(); + + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + var cc = root.childCount; + print("Found containers:"); + var previousLabels = []; + for (let i = 0; i < cc; i++) { + let container = visibleContainers[i]; + var node = root.getChild(i); + print(node.title); + if (container.label) + do_check_eq(node.title, container.label); + // Check labels are not repeated. + do_check_eq(previousLabels.indexOf(node.title), -1); + previousLabels.push(node.title); + } + do_check_eq(cc, visibleContainers.length); + root.containerOpen = false; +} + +/** + * Bug 485703 - Hide date containers not containing additional entries compared + * to previous ones. + */ +function check_visit(aOffset) { + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_SITE_QUERY; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + var cc = root.childCount; + + var unexpected = []; + switch (aOffset) { + case 0: + unexpected = ["Yesterday", "Last 7 days", "This month"]; + break; + case -1: + unexpected = ["Last 7 days", "This month"]; + break; + case -3: + unexpected = ["This month"]; + break; + default: + // Other containers are tested later. + } + + print("Found containers:"); + for (var i = 0; i < cc; i++) { + var node = root.getChild(i); + print(node.title); + do_check_eq(unexpected.indexOf(node.title), -1); + } + + root.containerOpen = false; +} + +/** + * Queries history grouped by date and site, checking containers' labels and + * children. + */ +function test_RESULTS_AS_DATE_SITE_QUERY() { + print("\n\n*** TEST RESULTS_AS_DATE_SITE_QUERY\n"); + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_SITE_QUERY; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // Check one of the days + var dayNode = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayNode.containerOpen = true; + do_check_eq(dayNode.childCount, 2); + + // Items should be sorted by host + var site1 = dayNode.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(site1.title, "mirror0.google.com"); + + var site2 = dayNode.getChild(1) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(site2.title, "mirror0.mozilla.com"); + + site1.containerOpen = true; + do_check_eq(site1.childCount, 2); + + // Inside of host sites are sorted by title + var site1visit = site1.getChild(0); + do_check_eq(site1visit.uri, "http://mirror0.google.com/a"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = options.SORT_BY_TITLE_DESCENDING; + + // Check one of the days + dayNode = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayNode.containerOpen = true; + do_check_eq(dayNode.childCount, 2); + + // Hosts are still sorted by title + site1 = dayNode.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(site1.title, "mirror0.google.com"); + + site2 = dayNode.getChild(1) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(site2.title, "mirror0.mozilla.com"); + + site1.containerOpen = true; + do_check_eq(site1.childCount, 2); + + // But URLs are now sorted by title descending + site1visit = site1.getChild(0); + do_check_eq(site1visit.uri, "http://mirror0.google.com/b"); + + site1.containerOpen = false; + dayNode.containerOpen = false; + root.containerOpen = false; +} + +/** + * Queries history grouped by date, checking containers' labels and children. + */ +function test_RESULTS_AS_DATE_QUERY() { + print("\n\n*** TEST RESULTS_AS_DATE_QUERY\n"); + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_DATE_QUERY; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + var cc = root.childCount; + do_check_eq(cc, visibleContainers.length); + print("Found containers:"); + for (var i = 0; i < cc; i++) { + var container = visibleContainers[i]; + var node = root.getChild(i); + print(node.title); + if (container.label) + do_check_eq(node.title, container.label); + } + + // Check one of the days + var dayNode = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayNode.containerOpen = true; + do_check_eq(dayNode.childCount, 4); + + // Items should be sorted by title + var visit1 = dayNode.getChild(0); + do_check_eq(visit1.uri, "http://mirror0.google.com/a"); + + var visit2 = dayNode.getChild(3); + do_check_eq(visit2.uri, "http://mirror0.mozilla.com/b"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = options.SORT_BY_TITLE_DESCENDING; + + // Check one of the days + dayNode = root.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + dayNode.containerOpen = true; + do_check_eq(dayNode.childCount, 4); + + // But URLs are now sorted by title descending + visit1 = dayNode.getChild(0); + do_check_eq(visit1.uri, "http://mirror0.mozilla.com/b"); + + visit2 = dayNode.getChild(3); + do_check_eq(visit2.uri, "http://mirror0.google.com/a"); + + dayNode.containerOpen = false; + root.containerOpen = false; +} + +/** + * Queries history grouped by site, checking containers' labels and children. + */ +function test_RESULTS_AS_SITE_QUERY() { + print("\n\n*** TEST RESULTS_AS_SITE_QUERY\n"); + // add a bookmark with a domain not in the set of visits in the db + var itemId = bs.insertBookmark(bs.toolbarFolder, uri("http://foobar"), + bs.DEFAULT_INDEX, ""); + + var options = hs.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_SITE_QUERY; + options.sortingMode = options.SORT_BY_TITLE_ASCENDING; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, containers.length * 2); + +/* Expected results: + "mirror0.google.com", + "mirror0.mozilla.com", + "mirror1.google.com", + "mirror1.mozilla.com", + "mirror2.google.com", + "mirror2.mozilla.com", + "mirror3.google.com", <== We check for this site (index 6) + "mirror3.mozilla.com", + "mirror4.google.com", + "mirror4.mozilla.com", + "mirror5.google.com", + "mirror5.mozilla.com", + ... +*/ + + // Items should be sorted by host + var siteNode = root.getChild(6) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(siteNode.title, "mirror3.google.com"); + + siteNode.containerOpen = true; + do_check_eq(siteNode.childCount, 2); + + // Inside of host sites are sorted by title + var visitNode = siteNode.getChild(0); + do_check_eq(visitNode.uri, "http://mirror3.google.com/a"); + + // Bug 473157: changing sorting mode should not affect the containers + result.sortingMode = options.SORT_BY_TITLE_DESCENDING; + siteNode = root.getChild(6) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + do_check_eq(siteNode.title, "mirror3.google.com"); + + siteNode.containerOpen = true; + do_check_eq(siteNode.childCount, 2); + + // But URLs are now sorted by title descending + var visit = siteNode.getChild(0); + do_check_eq(visit.uri, "http://mirror3.google.com/b"); + + siteNode.containerOpen = false; + root.containerOpen = false; + + // Cleanup. + bs.removeItem(itemId); +} + +/** + * Checks that queries grouped by date do liveupdate correctly. + */ +function* task_test_date_liveupdate(aResultType) { + var midnight = nowObj; + midnight.setHours(0); + midnight.setMinutes(0); + midnight.setSeconds(0); + midnight.setMilliseconds(0); + + // TEST 1. Test that the query correctly updates when it is root. + var options = hs.getNewQueryOptions(); + options.resultType = aResultType; + var query = hs.getNewQuery(); + var result = hs.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + do_check_eq(root.childCount, visibleContainers.length); + // Remove "Today". + hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000); + do_check_eq(root.childCount, visibleContainers.length - 1); + + // Open "Last 7 days" container, this way we will have a container accepting + // the new visit, but we should still add back "Today" container. + var last7Days = root.getChild(1) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + last7Days.containerOpen = true; + + // Add a visit for "Today". This should add back the missing "Today" + // container. + yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0); + do_check_eq(root.childCount, visibleContainers.length); + + last7Days.containerOpen = false; + root.containerOpen = false; + + // TEST 2. Test that the query correctly updates even if it is not root. + var itemId = bs.insertBookmark(bs.toolbarFolder, + uri("place:type=" + aResultType), + bs.DEFAULT_INDEX, ""); + + // Query toolbar and open our query container, then check again liveupdate. + options = hs.getNewQueryOptions(); + query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + result = hs.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + var dateContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode); + dateContainer.containerOpen = true; + + do_check_eq(dateContainer.childCount, visibleContainers.length); + // Remove "Today". + hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000); + do_check_eq(dateContainer.childCount, visibleContainers.length - 1); + // Add a visit for "Today". + yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0); + do_check_eq(dateContainer.childCount, visibleContainers.length); + + dateContainer.containerOpen = false; + root.containerOpen = false; + + // Cleanup. + bs.removeItem(itemId); +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_history_sidebar() +{ + // If we're dangerously close to a date change, just bail out. + if (nowObj.getHours() == 23 && nowObj.getMinutes() >= 50) { + return; + } + + yield task_fill_history(); + test_RESULTS_AS_DATE_SITE_QUERY(); + test_RESULTS_AS_DATE_QUERY(); + test_RESULTS_AS_SITE_QUERY(); + + yield task_test_date_liveupdate(Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY); + yield task_test_date_liveupdate(Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY); + + // The remaining views are + // RESULTS_AS_URI + SORT_BY_VISITCOUNT_DESCENDING + // -> test_399266.js + // RESULTS_AS_URI + SORT_BY_DATE_DESCENDING + // -> test_385397.js +}); diff --git a/toolkit/components/places/tests/unit/test_hosts_triggers.js b/toolkit/components/places/tests/unit/test_hosts_triggers.js new file mode 100644 index 000000000..9c3359e76 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_hosts_triggers.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the validity of various triggers that add remove hosts from moz_hosts + */ + +XPCOMUtils.defineLazyServiceGetter(this, "gHistory", + "@mozilla.org/browser/history;1", + "mozIAsyncHistory"); + +// add some visits and remove them, add a bookmark, +// change its uri, then remove it, and +// for each change check that moz_hosts has correctly been updated. + +function isHostInMozPlaces(aURI) +{ + let stmt = DBConn().createStatement( + `SELECT url + FROM moz_places + WHERE url_hash = hash(:host) AND url = :host` + ); + let result = false; + stmt.params.host = aURI.spec; + while (stmt.executeStep()) { + if (stmt.row.url == aURI.spec) { + result = true; + break; + } + } + stmt.finalize(); + return result; +} + +function isHostInMozHosts(aURI, aTyped, aPrefix) +{ + let stmt = DBConn().createStatement( + `SELECT host, typed, prefix + FROM moz_hosts + WHERE host = fixup_url(:host) + AND frecency NOTNULL` + ); + let result = false; + stmt.params.host = aURI.host; + if (stmt.executeStep()) { + result = aTyped == stmt.row.typed && aPrefix == stmt.row.prefix; + } + stmt.finalize(); + return result; +} + +var urls = [{uri: NetUtil.newURI("http://visit1.mozilla.org"), + expected: "visit1.mozilla.org", + typed: 0, + prefix: null + }, + {uri: NetUtil.newURI("http://visit2.mozilla.org"), + expected: "visit2.mozilla.org", + typed: 0, + prefix: null + }, + {uri: NetUtil.newURI("http://www.foo.mozilla.org"), + expected: "foo.mozilla.org", + typed: 1, + prefix: "www." + }, + ]; + +const NEW_URL = "http://different.mozilla.org/"; + +add_task(function* test_moz_hosts_update() +{ + let places = []; + urls.forEach(function(url) { + let place = { uri: url.uri, + title: "test for " + url.url, + transition: url.typed ? TRANSITION_TYPED : undefined }; + places.push(place); + }); + + yield PlacesTestUtils.addVisits(places); + + do_check_true(isHostInMozHosts(urls[0].uri, urls[0].typed, urls[0].prefix)); + do_check_true(isHostInMozHosts(urls[1].uri, urls[1].typed, urls[1].prefix)); + do_check_true(isHostInMozHosts(urls[2].uri, urls[2].typed, urls[2].prefix)); +}); + +add_task(function* test_remove_places() +{ + for (let idx in urls) { + PlacesUtils.history.removePage(urls[idx].uri); + } + + yield PlacesTestUtils.clearHistory(); + + for (let idx in urls) { + do_check_false(isHostInMozHosts(urls[idx].uri, urls[idx].typed, urls[idx].prefix)); + } +}); + +add_task(function* test_bookmark_changes() +{ + let testUri = NetUtil.newURI("http://test.mozilla.org"); + + let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + testUri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title"); + + do_check_true(isHostInMozPlaces(testUri)); + + // Change the hostname + PlacesUtils.bookmarks.changeBookmarkURI(itemId, NetUtil.newURI(NEW_URL)); + + yield PlacesTestUtils.clearHistory(); + + let newUri = NetUtil.newURI(NEW_URL); + do_check_true(isHostInMozPlaces(newUri)); + do_check_true(isHostInMozHosts(newUri, false, null)); + do_check_false(isHostInMozHosts(NetUtil.newURI("http://test.mozilla.org"), false, null)); +}); + +add_task(function* test_bookmark_removal() +{ + let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX); + let newUri = NetUtil.newURI(NEW_URL); + PlacesUtils.bookmarks.removeItem(itemId); + yield PlacesTestUtils.clearHistory(); + + do_check_false(isHostInMozHosts(newUri, false, null)); +}); + +add_task(function* test_moz_hosts_typed_update() +{ + const TEST_URI = NetUtil.newURI("http://typed.mozilla.com"); + let places = [{ uri: TEST_URI + , title: "test for " + TEST_URI.spec + }, + { uri: TEST_URI + , title: "test for " + TEST_URI.spec + , transition: TRANSITION_TYPED + }]; + + yield PlacesTestUtils.addVisits(places); + + do_check_true(isHostInMozHosts(TEST_URI, true, null)); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_moz_hosts_www_remove() +{ + function* test_removal(aURIToRemove, aURIToKeep, aCallback) { + let places = [{ uri: aURIToRemove + , title: "test for " + aURIToRemove.spec + , transition: TRANSITION_TYPED + }, + { uri: aURIToKeep + , title: "test for " + aURIToKeep.spec + , transition: TRANSITION_TYPED + }]; + + yield PlacesTestUtils.addVisits(places); + print("removing " + aURIToRemove.spec + " keeping " + aURIToKeep); + dump_table("moz_hosts"); + dump_table("moz_places"); + PlacesUtils.history.removePage(aURIToRemove); + let prefix = /www/.test(aURIToKeep.spec) ? "www." : null; + dump_table("moz_hosts"); + dump_table("moz_places"); + do_check_true(isHostInMozHosts(aURIToKeep, true, prefix)); + } + + const TEST_URI = NetUtil.newURI("http://rem.mozilla.com"); + const TEST_WWW_URI = NetUtil.newURI("http://www.rem.mozilla.com"); + yield test_removal(TEST_URI, TEST_WWW_URI); + yield test_removal(TEST_WWW_URI, TEST_URI); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_moz_hosts_ftp_matchall() +{ + const TEST_URI_1 = NetUtil.newURI("ftp://www.mozilla.com/"); + const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/"); + + yield PlacesTestUtils.addVisits([ + { uri: TEST_URI_1, transition: TRANSITION_TYPED }, + { uri: TEST_URI_2, transition: TRANSITION_TYPED } + ]); + + do_check_true(isHostInMozHosts(TEST_URI_1, true, "ftp://")); +}); + +add_task(function* test_moz_hosts_ftp_not_matchall() +{ + const TEST_URI_1 = NetUtil.newURI("http://mozilla.com/"); + const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/"); + + yield PlacesTestUtils.addVisits([ + { uri: TEST_URI_1, transition: TRANSITION_TYPED }, + { uri: TEST_URI_2, transition: TRANSITION_TYPED } + ]); + + do_check_true(isHostInMozHosts(TEST_URI_1, true, null)); +}); + +add_task(function* test_moz_hosts_update_2() +{ + // Check that updating trigger takes into account prefixes for different + // rev_hosts. + const TEST_URI_1 = NetUtil.newURI("https://www.google.it/"); + const TEST_URI_2 = NetUtil.newURI("https://google.it/"); + let places = [{ uri: TEST_URI_1 + , transition: TRANSITION_TYPED + }, + { uri: TEST_URI_2 + }]; + yield PlacesTestUtils.addVisits(places); + + do_check_true(isHostInMozHosts(TEST_URI_1, true, "https://www.")); +}); + +function run_test() +{ + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js new file mode 100644 index 000000000..771a6ac17 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js @@ -0,0 +1,292 @@ +function* importFromFixture(fixture, replace) { + let cwd = yield OS.File.getCurrentDirectory(); + let path = OS.Path.join(cwd, fixture); + + do_print(`Importing from ${path}`); + yield BookmarkJSONUtils.importFromFile(path, replace); + yield PlacesTestUtils.promiseAsyncUpdates(); +} + +function* treeEquals(guid, expected, message) { + let root = yield PlacesUtils.promiseBookmarksTree(guid); + let bookmarks = (function nodeToEntry(node) { + let entry = { guid: node.guid, index: node.index } + if (node.children) { + entry.children = node.children.map(nodeToEntry); + } + if (node.annos) { + entry.annos = node.annos; + } + return entry; + }(root)); + + do_print(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`); + do_print(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`); + + deepEqual(bookmarks, expected, message); +} + +add_task(function* test_restore_mobile_bookmarks_root() { + yield* importFromFixture("mobile_bookmarks_root_import.json", + /* replace */ true); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + ], + }], + }, "Should restore mobile bookmarks from root"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_mobile_bookmarks_root() { + yield* importFromFixture("mobile_bookmarks_root_import.json", + /* replace */ false); + yield* importFromFixture("mobile_bookmarks_root_merge.json", + /* replace */ false); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "Utodo9b0oVws", index: 0 }, + { guid: "X6lUyOspVYwi", index: 1 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "a17yW6-nTxEJ", index: 0 }, + { guid: "xV10h9Wi3FBM", index: 1 }, + { guid: "_o8e1_zxTJFg", index: 2 }, + { guid: "QCtSqkVYUbXB", index: 3 }, + ], + }], + }, "Should merge bookmarks root contents"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_restore_mobile_bookmarks_folder() { + yield* importFromFixture("mobile_bookmarks_folder_import.json", + /* replace */ true); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "X6lUyOspVYwi", index: 0 }, + { guid: "XF4yRP6bTuil", index: 1 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "buy7711R3ZgE", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "KIa9iKZab2Z5", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "_o8e1_zxTJFg", index: 0 }, + { guid: "QCtSqkVYUbXB", index: 1 }, + ], + }], + }, "Should restore mobile bookmark folder contents into mobile root"); + + // We rewrite queries to point to the root ID instead of the name + // ("MOBILE_BOOKMARKS") so that we don't break them if the user downgrades + // to an earlier release channel. This can be removed along with the anno in + // bug 1306445. + let queryById = yield PlacesUtils.bookmarks.fetch("XF4yRP6bTuil"); + equal(queryById.url.href, "place:folder=" + PlacesUtils.mobileFolderId, + "Should rewrite mobile query to point to root ID"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_mobile_bookmarks_folder() { + yield* importFromFixture("mobile_bookmarks_folder_import.json", + /* replace */ false); + yield* importFromFixture("mobile_bookmarks_folder_merge.json", + /* replace */ false); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "Utodo9b0oVws", index: 0 }, + { guid: "X6lUyOspVYwi", index: 1 }, + { guid: "XF4yRP6bTuil", index: 2 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "buy7711R3ZgE", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "KIa9iKZab2Z5", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "a17yW6-nTxEJ", index: 0 }, + { guid: "xV10h9Wi3FBM", index: 1 }, + { guid: "_o8e1_zxTJFg", index: 2 }, + { guid: "QCtSqkVYUbXB", index: 3 }, + ], + }], + }, "Should merge bookmarks folder contents into mobile root"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_restore_multiple_bookmarks_folders() { + yield* importFromFixture("mobile_bookmarks_multiple_folders.json", + /* replace */ true); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "buy7711R3ZgE", index: 0 }, + { guid: "F_LBgd1fS_uQ", index: 1 }, + { guid: "oIpmQXMWsXvY", index: 2 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "Utodo9b0oVws", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "xV10h9Wi3FBM", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "sSZ86WT9WbN3", index: 0 }, + { guid: "a17yW6-nTxEJ", index: 1 }, + ], + }], + }, "Should restore multiple bookmarks folder contents into root"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_import_multiple_bookmarks_folders() { + yield* importFromFixture("mobile_bookmarks_root_import.json", + /* replace */ false); + yield* importFromFixture("mobile_bookmarks_multiple_folders.json", + /* replace */ false); + + yield* treeEquals(PlacesUtils.bookmarks.rootGuid, { + guid: PlacesUtils.bookmarks.rootGuid, + index: 0, + children: [{ + guid: PlacesUtils.bookmarks.menuGuid, + index: 0, + children: [ + { guid: "buy7711R3ZgE", index: 0 }, + { guid: "F_LBgd1fS_uQ", index: 1 }, + { guid: "oIpmQXMWsXvY", index: 2 }, + { guid: "X6lUyOspVYwi", index: 3 }, + ], + }, { + guid: PlacesUtils.bookmarks.toolbarGuid, + index: 1, + children: [{ guid: "Utodo9b0oVws", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.unfiledGuid, + index: 3, + children: [{ guid: "xV10h9Wi3FBM", index: 0 }], + }, { + guid: PlacesUtils.bookmarks.mobileGuid, + index: 4, + annos: [{ + name: "mobile/bookmarksRoot", + flags: 0, + expires: 4, + value: 1, + }], + children: [ + { guid: "sSZ86WT9WbN3", index: 0 }, + { guid: "a17yW6-nTxEJ", index: 1 }, + { guid: "_o8e1_zxTJFg", index: 2 }, + { guid: "QCtSqkVYUbXB", index: 3 }, + ], + }], + }, "Should merge multiple mobile folders into root"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_isPageInDB.js b/toolkit/components/places/tests/unit/test_isPageInDB.js new file mode 100644 index 000000000..249853fa9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isPageInDB.js @@ -0,0 +1,10 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +add_task(function* test_execute() { + var good_uri = uri("http://mozilla.com"); + var bad_uri = uri("http://google.com"); + yield PlacesTestUtils.addVisits({uri: good_uri}); + do_check_true(yield PlacesTestUtils.isPageInDB(good_uri)); + do_check_false(yield PlacesTestUtils.isPageInDB(bad_uri)); +}); diff --git a/toolkit/components/places/tests/unit/test_isURIVisited.js b/toolkit/components/places/tests/unit/test_isURIVisited.js new file mode 100644 index 000000000..93c010e83 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isURIVisited.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests functionality of the isURIVisited API. + +const SCHEMES = { + "http://": true, + "https://": true, + "ftp://": true, + "file:///": true, + "about:": false, +// nsIIOService.newURI() can throw if e.g. the app knows about imap:// +// but the account is not set up and so the URL is invalid for it. +// "imap://": false, + "news://": false, + "mailbox:": false, + "moz-anno:favicon:http://": false, + "view-source:http://": false, + "chrome://browser/content/browser.xul?": false, + "resource://": false, + "data:,": false, + "wyciwyg:/0/http://": false, + "javascript:": false, +}; + +var gRunner; +function run_test() +{ + do_test_pending(); + gRunner = step(); + gRunner.next(); +} + +function* step() +{ + let history = Cc["@mozilla.org/browser/history;1"] + .getService(Ci.mozIAsyncHistory); + + for (let scheme in SCHEMES) { + do_print("Testing scheme " + scheme); + for (let t in PlacesUtils.history.TRANSITIONS) { + do_print("With transition " + t); + let transition = PlacesUtils.history.TRANSITIONS[t]; + + let uri = NetUtil.newURI(scheme + "mozilla.org/"); + + history.isURIVisited(uri, function(aURI, aIsVisited) { + do_check_true(uri.equals(aURI)); + do_check_false(aIsVisited); + + let callback = { + handleError: function () {}, + handleResult: function () {}, + handleCompletion: function () { + do_print("Added visit to " + uri.spec); + + history.isURIVisited(uri, function (aURI2, aIsVisited2) { + do_check_true(uri.equals(aURI2)); + let checker = SCHEMES[scheme] ? do_check_true : do_check_false; + checker(aIsVisited2); + + PlacesTestUtils.clearHistory().then(function () { + history.isURIVisited(uri, function(aURI3, aIsVisited3) { + do_check_true(uri.equals(aURI3)); + do_check_false(aIsVisited3); + gRunner.next(); + }); + }); + }); + }, + }; + + history.updatePlaces({ uri: uri + , visits: [ { transitionType: transition + , visitDate: Date.now() * 1000 + } ] + }, callback); + }); + yield undefined; + } + } + + do_test_finished(); +} diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js new file mode 100644 index 000000000..d7bcc2851 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_isvisited.js @@ -0,0 +1,75 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + var referrer = uri("about:blank"); + + // add a http:// uri + var uri1 = uri("http://mozilla.com"); + yield PlacesTestUtils.addVisits({uri: uri1, referrer: referrer}); + do_check_guid_for_uri(uri1); + do_check_true(yield promiseIsURIVisited(uri1)); + + // add a https:// uri + var uri2 = uri("https://etrade.com"); + yield PlacesTestUtils.addVisits({uri: uri2, referrer: referrer}); + do_check_guid_for_uri(uri2); + do_check_true(yield promiseIsURIVisited(uri2)); + + // add a ftp:// uri + var uri3 = uri("ftp://ftp.mozilla.org"); + yield PlacesTestUtils.addVisits({uri: uri3, referrer: referrer}); + do_check_guid_for_uri(uri3); + do_check_true(yield promiseIsURIVisited(uri3)); + + // check if a nonexistent uri is visited + var uri4 = uri("http://foobarcheese.com"); + do_check_false(yield promiseIsURIVisited(uri4)); + + // check that certain schemes never show up as visited + // even if we attempt to add them to history + // see CanAddURI() in nsNavHistory.cpp + const URLS = [ + "about:config", + "imap://cyrus.andrew.cmu.edu/archive.imap", + "news://new.mozilla.org/mozilla.dev.apps.firefox", + "mailbox:Inbox", + "moz-anno:favicon:http://mozilla.org/made-up-favicon", + "view-source:http://mozilla.org", + "chrome://browser/content/browser.xul", + "resource://gre-resources/hiddenWindow.html", + "data:,Hello%2C%20World!", + "wyciwyg:/0/http://mozilla.org", + "javascript:alert('hello wolrd!');", + "http://localhost/" + "a".repeat(1984), + ]; + for (let currentURL of URLS) { + try { + var cantAddUri = uri(currentURL); + } + catch (e) { + // nsIIOService.newURI() can throw if e.g. our app knows about imap:// + // but the account is not set up and so the URL is invalid for us. + // Note this in the log but ignore as it's not the subject of this test. + do_print("Could not construct URI for '" + currentURL + "'; ignoring"); + } + if (cantAddUri) { + PlacesTestUtils.addVisits({uri: cantAddUri, referrer: referrer}).then(() => { + do_throw("Should not have added history for invalid URI."); + }, error => { + do_check_true(error.message.includes("No items were added to history")); + }); + do_check_false(yield promiseIsURIVisited(cantAddUri)); + } + } +}); + diff --git a/toolkit/components/places/tests/unit/test_keywords.js b/toolkit/components/places/tests/unit/test_keywords.js new file mode 100644 index 000000000..57b734c5d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_keywords.js @@ -0,0 +1,548 @@ +"use strict" + +function* check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) { + // Check case-insensitivity. + aKeyword = aKeyword.toUpperCase(); + + let entry = yield PlacesUtils.keywords.fetch(aKeyword); + + Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword })); + + if (aExpectExists) { + Assert.ok(!!entry, "A keyword should exist"); + Assert.equal(entry.url.href, aHref); + Assert.equal(entry.postData, aPostData); + Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })); + let entries = []; + yield PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e)); + Assert.ok(entries.some(e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase())); + } else { + Assert.ok(!entry || entry.url.href != aHref, + "The given keyword entry should not exist"); + Assert.equal(null, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })); + } +} + +/** + * Polls the keywords cache waiting for the given keyword entry. + */ +function* promiseKeyword(keyword, expectedHref) { + let href = null; + do { + yield new Promise(resolve => do_timeout(100, resolve)); + let entry = yield PlacesUtils.keywords.fetch(keyword); + if (entry) + href = entry.url.href; + } while (href != expectedHref); +} + +function* check_no_orphans() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT id FROM moz_keywords k + WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) + `); + Assert.equal(rows.length, 0); +} + +function expectBookmarkNotifications() { + let notifications = []; + let observer = new Proxy(NavBookmarkObserver, { + get(target, name) { + if (name == "check") { + PlacesUtils.bookmarks.removeObserver(observer); + return expectedNotifications => + Assert.deepEqual(notifications, expectedNotifications); + } + + if (name.startsWith("onItemChanged")) { + return function(itemId, property) { + if (property != "keyword") + return; + let args = Array.from(arguments, arg => { + if (arg && arg instanceof Ci.nsIURI) + return new URL(arg.spec); + if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) + return new Date(parseInt(arg/1000)); + return arg; + }); + notifications.push({ name: name, arguments: args }); + } + } + + if (name in target) + return target[name]; + return undefined; + } + }); + PlacesUtils.bookmarks.addObserver(observer, false); + return observer; +} + +add_task(function* test_invalid_input() { + Assert.throws(() => PlacesUtils.keywords.fetch(null), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(5), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(undefined), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: null }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: {} }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: 5 }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch({}), + /At least keyword or url must be provided/); + Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"), + /onResult callback must be a valid function/); + Assert.throws(() => PlacesUtils.keywords.fetch({ url: "test" }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.fetch({ url: {} }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.fetch({ url: null }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.fetch({ url: "" }), + /is not a valid URL/); + + Assert.throws(() => PlacesUtils.keywords.insert(null), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert("test"), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert(undefined), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert({ }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: null }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: 5 }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "" }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }), + /Invalid POST data/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }), + /Invalid POST data/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test" }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "" }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: null }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }), + /is not a valid URL/); + + Assert.throws(() => PlacesUtils.keywords.remove(null), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(""), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(5), + /Invalid keyword/); +}); + +add_task(function* test_addKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword + + // Check using URL. + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/") }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_addBookmarkAndKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword + + // Add again the keyword, then remove the bookmark. + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + while ((yield foreign_count("http://example.com/"))); + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_addKeywordToURIHavingKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 keyword + let entries = []; + let entry = yield PlacesUtils.keywords.fetch({ url: "http://example.com/" }, e => entries.push(e)); + Assert.equal(entries.length, 2); + Assert.deepEqual(entries[0], entry); + + // Now remove the keywords. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield PlacesUtils.keywords.remove("keyword2"); + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(false, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword + + yield check_no_orphans(); +}); + +add_task(function* test_addBookmarkToURIHavingKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + observer = expectBookmarkNotifications(); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark + observer.check([]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + while ((yield foreign_count("http://example.com/"))); + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_sameKeywordDifferentURL() { + let fc1 = yield foreign_count("http://example1.com/"); + let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let fc2 = yield foreign_count("http://example2.com/"); + let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example2.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/" }); + + yield check_keyword(true, "http://example1.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword + yield check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // +1 bookmark + + // Assign the same keyword to another url. + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" }); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + yield check_keyword(false, "http://example1.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword + yield check_keyword(true, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + yield check_keyword(false, "http://example1.com/", "keyword"); + yield check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword + + yield PlacesUtils.bookmarks.remove(bookmark1); + yield PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark + while ((yield foreign_count("http://example2.com/"))); // -1 keyword + + yield check_no_orphans(); +}); + +add_task(function* test_sameURIDifferentKeyword() { + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({keyword: "keyword", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword2", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + // Add a third keyword. + yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + yield check_keyword(true, "http://example.com/", "keyword3"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword + + // Remove one of the keywords. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + yield check_keyword(true, "http://example.com/", "keyword3"); + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword + + // Now remove the bookmark. + yield PlacesUtils.bookmarks.remove(bookmark); + while ((yield foreign_count("http://example.com/"))); + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(false, "http://example.com/", "keyword2"); + yield check_keyword(false, "http://example.com/", "keyword3"); + + check_no_orphans(); +}); + +add_task(function* test_deleteKeywordMultipleBookmarks() { + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "keyword", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid, "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); + + // Now remove the bookmarks. + yield PlacesUtils.bookmarks.remove(bookmark1); + yield PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks + + check_no_orphans(); +}); + +add_task(function* test_multipleKeywordsSamePostData() { + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" }); + yield check_keyword(true, "http://example.com/", "keyword", "postData1"); + // Add another keyword with same postData, should fail. + yield Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }), + /constraint failed/); + yield check_keyword(false, "http://example.com/", "keyword2", "postData1"); + + yield PlacesUtils.keywords.remove("keyword"); + + check_no_orphans(); +}); + +add_task(function* test_oldPostDataAPI() { + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); + yield PlacesUtils.setPostDataForBookmark(itemId, "postData"); + yield check_keyword(true, "http://example.com/", "keyword", "postData"); + Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData"); + + yield PlacesUtils.keywords.remove("keyword"); + yield PlacesUtils.bookmarks.remove(bookmark); + + check_no_orphans(); +}); + +add_task(function* test_oldKeywordsAPI() { + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield check_keyword(false, "http://example.com/", "keyword"); + let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); + yield promiseKeyword("keyword", "http://example.com/"); + + // Remove the keyword. + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, ""); + yield promiseKeyword("keyword", null); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" }); + Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword"); + Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/"); + yield PlacesUtils.bookmarks.remove(bookmark); + + check_no_orphans(); +}); + +add_task(function* test_bookmarkURLChange() { + let fc1 = yield foreign_count("http://example1.com/"); + let fc2 = yield foreign_count("http://example2.com/"); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", + url: "http://example1.com/" }); + + yield check_keyword(true, "http://example1.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword + + yield PlacesUtils.bookmarks.update({ guid: bookmark.guid, + url: "http://example2.com/"}); + yield promiseKeyword("keyword", "http://example2.com/"); + + yield check_keyword(false, "http://example1.com/", "keyword"); + yield check_keyword(true, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark -1 keyword + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 bookmark +1 keyword +}); diff --git a/toolkit/components/places/tests/unit/test_lastModified.js b/toolkit/components/places/tests/unit/test_lastModified.js new file mode 100644 index 000000000..c75494932 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_lastModified.js @@ -0,0 +1,34 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Test that inserting a new bookmark will set lastModified to the same + * values as dateAdded. + */ +// main +function run_test() { + var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + var itemId = bs.insertBookmark(bs.bookmarksMenuFolder, + uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, + "itemTitle"); + var dateAdded = bs.getItemDateAdded(itemId); + do_check_eq(dateAdded, bs.getItemLastModified(itemId)); + + // Change lastModified, then change dateAdded. LastModified should be set + // to the new dateAdded. + // This could randomly fail on virtual machines due to timing issues, so + // we manually increase the time value. See bug 500640 for details. + bs.setItemLastModified(itemId, dateAdded + 1000); + do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1000); + do_check_true(bs.getItemDateAdded(itemId) < bs.getItemLastModified(itemId)); + bs.setItemDateAdded(itemId, dateAdded + 2000); + do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2000); + do_check_eq(bs.getItemDateAdded(itemId), bs.getItemLastModified(itemId)); + + bs.removeItem(itemId); +} diff --git a/toolkit/components/places/tests/unit/test_markpageas.js b/toolkit/components/places/tests/unit/test_markpageas.js new file mode 100644 index 000000000..ba4f740c6 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_markpageas.js @@ -0,0 +1,61 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gVisits = [{url: "http://www.mozilla.com/", + transition: TRANSITION_TYPED}, + {url: "http://www.google.com/", + transition: TRANSITION_BOOKMARK}, + {url: "http://www.espn.com/", + transition: TRANSITION_LINK}]; + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + let observer; + let completionPromise = new Promise(resolveCompletionPromise => { + observer = { + __proto__: NavHistoryObserver.prototype, + _visitCount: 0, + onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, + aTransitionType, aAdded) + { + do_check_eq(aURI.spec, gVisits[this._visitCount].url); + do_check_eq(aTransitionType, gVisits[this._visitCount].transition); + this._visitCount++; + + if (this._visitCount == gVisits.length) { + resolveCompletionPromise(); + } + }, + }; + }); + + PlacesUtils.history.addObserver(observer, false); + + for (var visit of gVisits) { + if (visit.transition == TRANSITION_TYPED) + PlacesUtils.history.markPageAsTyped(uri(visit.url)); + else if (visit.transition == TRANSITION_BOOKMARK) + PlacesUtils.history.markPageAsFollowedBookmark(uri(visit.url)) + else { + // because it is a top level visit with no referrer, + // it will result in TRANSITION_LINK + } + yield PlacesTestUtils.addVisits({ + uri: uri(visit.url), + transition: visit.transition + }); + } + + yield completionPromise; + + PlacesUtils.history.removeObserver(observer); +}); + diff --git a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js new file mode 100644 index 000000000..5136591ba --- /dev/null +++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js @@ -0,0 +1,514 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests functionality of the mozIAsyncLivemarks interface. + +const FEED_URI = NetUtil.newURI("http://feed.rss/"); +const SITE_URI = NetUtil.newURI("http://site.org/"); + +// This test must be the first one, since it's testing the cache. +add_task(function* test_livemark_cache() { + // Add a livemark through other APIs. + let folder = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "test", + parentGuid: PlacesUtils.bookmarks.unfiledGuid + }); + let id = yield PlacesUtils.promiseItemId(folder.guid); + PlacesUtils.annotations + .setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI, + "http://example.com/feed", + 0, PlacesUtils.annotations.EXPIRE_NEVER); + PlacesUtils.annotations + .setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI, + "http://example.com/site", + 0, PlacesUtils.annotations.EXPIRE_NEVER); + + let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: folder.guid }); + Assert.equal(folder.guid, livemark.guid); + Assert.equal(folder.dateAdded * 1000, livemark.dateAdded); + Assert.equal(folder.parentGuid, livemark.parentGuid); + Assert.equal(folder.index, livemark.index); + Assert.equal(folder.title, livemark.title); + Assert.equal(id, livemark.id); + Assert.equal(PlacesUtils.unfiledBookmarksFolderId, livemark.parentId); + Assert.equal("http://example.com/feed", livemark.feedURI.spec); + Assert.equal("http://example.com/site", livemark.siteURI.spec); + + yield PlacesUtils.livemarks.removeLivemark(livemark); +}); + +add_task(function* test_addLivemark_noArguments_throws() { + try { + yield PlacesUtils.livemarks.addLivemark(); + do_throw("Invoking addLivemark with no arguments should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS); + } +}); + +add_task(function* test_addLivemark_emptyObject_throws() { + try { + yield PlacesUtils.livemarks.addLivemark({}); + do_throw("Invoking addLivemark with empty object should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_badParentId_throws() { + try { + yield PlacesUtils.livemarks.addLivemark({ parentId: "test" }); + do_throw("Invoking addLivemark with a bad parent id should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_invalidParentId_throws() { + try { + yield PlacesUtils.livemarks.addLivemark({ parentId: -2 }); + do_throw("Invoking addLivemark with an invalid parent id should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_noIndex_throws() { + try { + yield PlacesUtils.livemarks.addLivemark({ + parentId: PlacesUtils.unfiledBookmarksFolderId }); + do_throw("Invoking addLivemark with no index should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_badIndex_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentId: PlacesUtils.unfiledBookmarksFolderId + , index: "test" }); + do_throw("Invoking addLivemark with a bad index should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_invalidIndex_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentId: PlacesUtils.unfiledBookmarksFolderId + , index: -2 + }); + do_throw("Invoking addLivemark with an invalid index should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_noFeedURI_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + do_throw("Invoking addLivemark with no feedURI should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_badFeedURI_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: "test" }); + do_throw("Invoking addLivemark with a bad feedURI should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_badSiteURI_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , siteURI: "test" }); + do_throw("Invoking addLivemark with a bad siteURI should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_badGuid_throws() { + try { + yield PlacesUtils.livemarks.addLivemark( + { parentGuid: PlacesUtils.bookmarks.unfileGuid + , feedURI: FEED_URI + , guid: "123456" }); + do_throw("Invoking addLivemark with a bad guid should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_parentId_succeeds() { + let onItemAddedCalled = false; + PlacesUtils.bookmarks.addObserver({ + __proto__: NavBookmarkObserver.prototype, + onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType, + aURI, aTitle) + { + onItemAddedCalled = true; + PlacesUtils.bookmarks.removeObserver(this); + do_check_eq(aParentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_eq(aIndex, 0); + do_check_eq(aItemType, Ci.nsINavBookmarksService.TYPE_FOLDER); + do_check_eq(aTitle, "test"); + } + }, false); + + yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentId: PlacesUtils.unfiledBookmarksFolderId + , feedURI: FEED_URI }); + do_check_true(onItemAddedCalled); +}); + + +add_task(function* test_addLivemark_noSiteURI_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + do_check_true(livemark.id > 0); + do_check_valid_places_guid(livemark.guid); + do_check_eq(livemark.title, "test"); + do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + do_check_true(livemark.feedURI.equals(FEED_URI)); + do_check_eq(livemark.siteURI, null); + do_check_true(livemark.lastModified > 0); + do_check_true(is_time_ordered(livemark.dateAdded, livemark.lastModified)); + + let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid); + do_check_eq(livemark.index, bookmark.index); + do_check_eq(livemark.dateAdded, bookmark.dateAdded * 1000); +}); + +add_task(function* test_addLivemark_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , siteURI: SITE_URI + }); + + do_check_true(livemark.id > 0); + do_check_valid_places_guid(livemark.guid); + do_check_eq(livemark.title, "test"); + do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + do_check_true(livemark.feedURI.equals(FEED_URI)); + do_check_true(livemark.siteURI.equals(SITE_URI)); + do_check_true(PlacesUtils.annotations + .itemHasAnnotation(livemark.id, + PlacesUtils.LMANNO_FEEDURI)); + do_check_true(PlacesUtils.annotations + .itemHasAnnotation(livemark.id, + PlacesUtils.LMANNO_SITEURI)); +}); + +add_task(function* test_addLivemark_bogusid_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { id: 100 // Should be ignored. + , title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , siteURI: SITE_URI + }); + do_check_true(livemark.id > 0); + do_check_neq(livemark.id, 100); +}); + +add_task(function* test_addLivemark_bogusParentId_fails() { + try { + yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentId: 187 + , feedURI: FEED_URI + }); + do_throw("Adding a livemark with a bogus parent should fail"); + } catch (ex) {} +}); + +add_task(function* test_addLivemark_bogusParentGuid_fails() { + try { + yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: "123456789012" + , feedURI: FEED_URI + }); + do_throw("Adding a livemark with a bogus parent should fail"); + } catch (ex) {} +}) + +add_task(function* test_addLivemark_intoLivemark_fails() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + + try { + yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: livemark.guid + , feedURI: FEED_URI + }); + do_throw("Adding a livemark into a livemark should fail"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_addLivemark_forceGuid_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , guid: "1234567890AB" + }); + do_check_eq(livemark.guid, "1234567890AB"); + do_check_guid_for_bookmark(livemark.id, "1234567890AB"); +}); + +add_task(function* test_addLivemark_dateAdded_succeeds() { + let dateAdded = new Date("2013-03-01T01:10:00") * 1000; + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , dateAdded + }); + do_check_eq(livemark.dateAdded, dateAdded); +}); + +add_task(function* test_addLivemark_lastModified_succeeds() { + let now = Date.now() * 1000; + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , lastModified: now + }); + do_check_eq(livemark.dateAdded, now); + do_check_eq(livemark.lastModified, now); +}); + +add_task(function* test_removeLivemark_emptyObject_throws() { + try { + yield PlacesUtils.livemarks.removeLivemark({}); + do_throw("Invoking removeLivemark with empty object should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_removeLivemark_noValidId_throws() { + try { + yield PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"}); + do_throw("Invoking removeLivemark with no valid id should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_removeLivemark_nonExistent_fails() { + try { + yield PlacesUtils.livemarks.removeLivemark({ id: 1337 }); + do_throw("Removing a non-existent livemark should fail"); + } + catch (ex) { + } +}); + +add_task(function* test_removeLivemark_guid_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , guid: "234567890ABC" + }); + + do_check_eq(livemark.guid, "234567890ABC"); + + yield PlacesUtils.livemarks.removeLivemark({ + id: 789, guid: "234567890ABC" + }); + + do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null); +}); + +add_task(function* test_removeLivemark_id_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + + yield PlacesUtils.livemarks.removeLivemark({ id: livemark.id }); + + do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null); +}); + +add_task(function* test_getLivemark_emptyObject_throws() { + try { + yield PlacesUtils.livemarks.getLivemark({}); + do_throw("Invoking getLivemark with empty object should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_getLivemark_noValidId_throws() { + try { + yield PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"}); + do_throw("Invoking getLivemark with no valid id should throw"); + } catch (ex) { + do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG); + } +}); + +add_task(function* test_getLivemark_nonExistentId_fails() { + try { + yield PlacesUtils.livemarks.getLivemark({ id: 1234 }); + do_throw("getLivemark for a non existent id should fail"); + } catch (ex) {} +}); + +add_task(function* test_getLivemark_nonExistentGUID_fails() { + try { + yield PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" }); + do_throw("getLivemark for a non-existent guid should fail"); + } catch (ex) {} +}); + +add_task(function* test_getLivemark_guid_succeeds() { + yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + , guid: "34567890ABCD" }); + + // invalid id to check the guid wins. + let livemark = + yield PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" }); + + do_check_eq(livemark.title, "test"); + do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + do_check_true(livemark.feedURI.equals(FEED_URI)); + do_check_eq(livemark.siteURI, null); + do_check_eq(livemark.guid, "34567890ABCD"); + + let bookmark = yield PlacesUtils.bookmarks.fetch("34567890ABCD"); + do_check_eq(livemark.index, bookmark.index); +}); + +add_task(function* test_getLivemark_id_succeeds() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + + livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.id }); + + do_check_eq(livemark.title, "test"); + do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + do_check_true(livemark.feedURI.equals(FEED_URI)); + do_check_eq(livemark.siteURI, null); + do_check_guid_for_bookmark(livemark.id, livemark.guid); + + let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid); + do_check_eq(livemark.index, bookmark.index); +}); + +add_task(function* test_getLivemark_removeItem_contention() { + // do not yield. + PlacesUtils.livemarks.addLivemark({ title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }).catch(() => { /* swallow errors*/ }); + yield PlacesUtils.bookmarks.eraseEverything(); + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + + livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid }); + + do_check_eq(livemark.title, "test"); + do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId); + do_check_true(livemark.feedURI.equals(FEED_URI)); + do_check_eq(livemark.siteURI, null); + do_check_guid_for_bookmark(livemark.id, livemark.guid); +}); + +add_task(function* test_title_change() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI + }); + + yield PlacesUtils.bookmarks.update({ guid: livemark.guid, + title: "test2" }); + // Poll for the title change. + while (true) { + let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid }); + if (lm.title == "test2") + break; + yield new Promise(resolve => do_timeout(resolve, 100)); + } +}); + +add_task(function* test_livemark_move() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI } ); + + yield PlacesUtils.bookmarks.update({ guid: livemark.guid, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX }); + // Poll for the parent change. + while (true) { + let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid }); + if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid) + break; + yield new Promise(resolve => do_timeout(resolve, 100)); + } +}); + +add_task(function* test_livemark_removed() { + let livemark = yield PlacesUtils.livemarks.addLivemark( + { title: "test" + , parentGuid: PlacesUtils.bookmarks.unfiledGuid + , feedURI: FEED_URI } ); + + yield PlacesUtils.bookmarks.remove(livemark.guid); + // Poll for the livemark removal. + while (true) { + try { + yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid }); + } catch (ex) { + break; + } + yield new Promise(resolve => do_timeout(resolve, 100)); + } +}); diff --git a/toolkit/components/places/tests/unit/test_multi_queries.js b/toolkit/components/places/tests/unit/test_multi_queries.js new file mode 100644 index 000000000..d485355a5 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_multi_queries.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Adds a test URI visit to history. + * + * @param aURI + * The URI to add a visit for. + * @param aReferrer + * The referring URI for the given URI. This can be null. + */ +function* add_visit(aURI, aDayOffset, aTransition) { + yield PlacesTestUtils.addVisits({ + uri: aURI, + transition: aTransition, + visitDate: (Date.now() + aDayOffset*86400000) * 1000 + }); +} + +function run_test() +{ + run_next_test(); +} + +add_task(function* test_execute() +{ + yield add_visit(uri("http://mirror1.mozilla.com/a"), -1, TRANSITION_LINK); + yield add_visit(uri("http://mirror2.mozilla.com/b"), -2, TRANSITION_LINK); + yield add_visit(uri("http://mirror3.mozilla.com/c"), -4, TRANSITION_FRAMED_LINK); + yield add_visit(uri("http://mirror1.google.com/b"), -1, TRANSITION_EMBED); + yield add_visit(uri("http://mirror2.google.com/a"), -2, TRANSITION_LINK); + yield add_visit(uri("http://mirror1.apache.org/b"), -3, TRANSITION_LINK); + yield add_visit(uri("http://mirror2.apache.org/a"), -4, TRANSITION_FRAMED_LINK); + + let queries = [ + PlacesUtils.history.getNewQuery(), + PlacesUtils.history.getNewQuery() + ]; + queries[0].domain = "mozilla.com"; + queries[1].domain = "google.com"; + + let root = PlacesUtils.history.executeQueries( + queries, queries.length, PlacesUtils.history.getNewQueryOptions() + ).root; + root.containerOpen = true; + let childCount = root.childCount; + root.containerOpen = false; + + do_check_eq(childCount, 3); +}); diff --git a/toolkit/components/places/tests/unit/test_multi_word_tags.js b/toolkit/components/places/tests/unit/test_multi_word_tags.js new file mode 100644 index 000000000..6a0e5f130 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +// Get bookmark service +try { + var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +} +catch (ex) { + do_throw("Could not get the nav-bookmarks-service\n"); +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +// main +function run_test() { + var uri1 = uri("http://site.tld/1"); + var uri2 = uri("http://site.tld/2"); + var uri3 = uri("http://site.tld/3"); + var uri4 = uri("http://site.tld/4"); + var uri5 = uri("http://site.tld/5"); + var uri6 = uri("http://site.tld/6"); + + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1, bmsvc.DEFAULT_INDEX, null); + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, bmsvc.DEFAULT_INDEX, null); + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri3, bmsvc.DEFAULT_INDEX, null); + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri4, bmsvc.DEFAULT_INDEX, null); + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri5, bmsvc.DEFAULT_INDEX, null); + bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri6, bmsvc.DEFAULT_INDEX, null); + + tagssvc.tagURI(uri1, ["foo"]); + tagssvc.tagURI(uri2, ["bar"]); + tagssvc.tagURI(uri3, ["cheese"]); + tagssvc.tagURI(uri4, ["foo bar"]); + tagssvc.tagURI(uri5, ["bar cheese"]); + tagssvc.tagURI(uri6, ["foo bar cheese"]); + + // exclude livemark items, search for "item", should get one result + var options = histsvc.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + + var query = histsvc.getNewQuery(); + query.searchTerms = "foo"; + var result = histsvc.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).uri, "http://site.tld/1"); + do_check_eq(root.getChild(1).uri, "http://site.tld/4"); + do_check_eq(root.getChild(2).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 4); + do_check_eq(root.getChild(0).uri, "http://site.tld/2"); + do_check_eq(root.getChild(1).uri, "http://site.tld/4"); + do_check_eq(root.getChild(2).uri, "http://site.tld/5"); + do_check_eq(root.getChild(3).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 3); + do_check_eq(root.getChild(0).uri, "http://site.tld/3"); + do_check_eq(root.getChild(1).uri, "http://site.tld/5"); + do_check_eq(root.getChild(2).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/4"); + do_check_eq(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/4"); + do_check_eq(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/5"); + do_check_eq(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + do_check_eq(root.getChild(0).uri, "http://site.tld/5"); + do_check_eq(root.getChild(1).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "foo bar cheese"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese foo bar"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; + + query.searchTerms = "cheese bar foo"; + result = histsvc.executeQuery(query, options); + root = result.root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + do_check_eq(root.getChild(0).uri, "http://site.tld/6"); + root.containerOpen = false; +} diff --git a/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js new file mode 100644 index 000000000..037ab7d08 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js @@ -0,0 +1,256 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Get history service +var histsvc = PlacesUtils.history; +var bhist = PlacesUtils.bhistory; +var bmsvc = PlacesUtils.bookmarks; + +var resultObserver = { + insertedNode: null, + nodeInserted: function(parent, node, newIndex) { + this.insertedNode = node; + }, + removedNode: null, + nodeRemoved: function(parent, node, oldIndex) { + this.removedNode = node; + }, + + nodeAnnotationChanged: function() {}, + + newTitle: "", + nodeChangedByTitle: null, + nodeTitleChanged: function(node, newTitle) { + this.nodeChangedByTitle = node; + this.newTitle = newTitle; + }, + + newAccessCount: 0, + newTime: 0, + nodeChangedByHistoryDetails: null, + nodeHistoryDetailsChanged: function(node, + updatedVisitDate, + updatedVisitCount) { + this.nodeChangedByHistoryDetails = node + this.newTime = updatedVisitDate; + this.newAccessCount = updatedVisitCount; + }, + + movedNode: null, + nodeMoved: function(node, oldParent, oldIndex, newParent, newIndex) { + this.movedNode = node; + }, + openedContainer: null, + closedContainer: null, + containerStateChanged: function (aNode, aOldState, aNewState) { + if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) { + this.openedContainer = aNode; + } + else if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) { + this.closedContainer = aNode; + } + }, + invalidatedContainer: null, + invalidateContainer: function(node) { + this.invalidatedContainer = node; + }, + sortingMode: null, + sortingChanged: function(sortingMode) { + this.sortingMode = sortingMode; + }, + inBatchMode: false, + batching: function(aToggleMode) { + do_check_neq(this.inBatchMode, aToggleMode); + this.inBatchMode = aToggleMode; + }, + result: null, + reset: function() { + this.insertedNode = null; + this.removedNode = null; + this.nodeChangedByTitle = null; + this.nodeChangedByHistoryDetails = null; + this.replacedNode = null; + this.movedNode = null; + this.openedContainer = null; + this.closedContainer = null; + this.invalidatedContainer = null; + this.sortingMode = null; + } +}; + +var testURI = uri("http://mozilla.com"); + +function run_test() { + run_next_test(); +} + +add_test(function check_history_query() { + var options = histsvc.getNewQueryOptions(); + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + var query = histsvc.getNewQuery(); + var result = histsvc.executeQuery(query, options); + result.addObserver(resultObserver, false); + var root = result.root; + root.containerOpen = true; + + do_check_neq(resultObserver.openedContainer, null); + + // nsINavHistoryResultObserver.nodeInserted + // add a visit + PlacesTestUtils.addVisits(testURI).then(function() { + do_check_eq(testURI.spec, resultObserver.insertedNode.uri); + + // nsINavHistoryResultObserver.nodeHistoryDetailsChanged + // adding a visit causes nodeHistoryDetailsChanged for the folder + do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri); + + // nsINavHistoryResultObserver.itemTitleChanged for a leaf node + PlacesTestUtils.addVisits({ uri: testURI, title: "baz" }).then(function () { + do_check_eq(resultObserver.nodeChangedByTitle.title, "baz"); + + // nsINavHistoryResultObserver.nodeRemoved + var removedURI = uri("http://google.com"); + PlacesTestUtils.addVisits(removedURI).then(function() { + bhist.removePage(removedURI); + do_check_eq(removedURI.spec, resultObserver.removedNode.uri); + + // nsINavHistoryResultObserver.invalidateContainer + bhist.removePagesFromHost("mozilla.com", false); + do_check_eq(root.uri, resultObserver.invalidatedContainer.uri); + + // nsINavHistoryResultObserver.sortingChanged + resultObserver.invalidatedContainer = null; + result.sortingMode = options.SORT_BY_TITLE_ASCENDING; + do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING); + do_check_eq(resultObserver.invalidatedContainer, result.root); + + // nsINavHistoryResultObserver.invalidateContainer + PlacesTestUtils.clearHistoryEnabled().then(() => { + do_check_eq(root.uri, resultObserver.invalidatedContainer.uri); + + // nsINavHistoryResultObserver.batching + do_check_false(resultObserver.inBatchMode); + histsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + bmsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + + root.containerOpen = false; + do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + PlacesTestUtils.promiseAsyncUpdates().then(run_next_test); + }); + }); + }); + }); +}); + +add_test(function check_bookmarks_query() { + var options = histsvc.getNewQueryOptions(); + var query = histsvc.getNewQuery(); + query.setFolders([bmsvc.bookmarksMenuFolder], 1); + var result = histsvc.executeQuery(query, options); + result.addObserver(resultObserver, false); + var root = result.root; + root.containerOpen = true; + + do_check_neq(resultObserver.openedContainer, null); + + // nsINavHistoryResultObserver.nodeInserted + // add a bookmark + var testBookmark = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, bmsvc.DEFAULT_INDEX, "foo"); + do_check_eq("foo", resultObserver.insertedNode.title); + do_check_eq(testURI.spec, resultObserver.insertedNode.uri); + + // nsINavHistoryResultObserver.nodeHistoryDetailsChanged + // adding a visit causes nodeHistoryDetailsChanged for the folder + do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri); + + // nsINavHistoryResultObserver.nodeTitleChanged for a leaf node + bmsvc.setItemTitle(testBookmark, "baz"); + do_check_eq(resultObserver.nodeChangedByTitle.title, "baz"); + do_check_eq(resultObserver.newTitle, "baz"); + + var testBookmark2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri("http://google.com"), bmsvc.DEFAULT_INDEX, "foo"); + bmsvc.moveItem(testBookmark2, bmsvc.bookmarksMenuFolder, 0); + do_check_eq(resultObserver.movedNode.itemId, testBookmark2); + + // nsINavHistoryResultObserver.nodeRemoved + bmsvc.removeItem(testBookmark2); + do_check_eq(testBookmark2, resultObserver.removedNode.itemId); + + // XXX nsINavHistoryResultObserver.invalidateContainer + + // nsINavHistoryResultObserver.sortingChanged + resultObserver.invalidatedContainer = null; + result.sortingMode = options.SORT_BY_TITLE_ASCENDING; + do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING); + do_check_eq(resultObserver.invalidatedContainer, result.root); + + // nsINavHistoryResultObserver.batching + do_check_false(resultObserver.inBatchMode); + histsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + bmsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + + root.containerOpen = false; + do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + PlacesTestUtils.promiseAsyncUpdates().then(run_next_test); +}); + +add_test(function check_mixed_query() { + var options = histsvc.getNewQueryOptions(); + var query = histsvc.getNewQuery(); + query.onlyBookmarked = true; + var result = histsvc.executeQuery(query, options); + result.addObserver(resultObserver, false); + var root = result.root; + root.containerOpen = true; + + do_check_neq(resultObserver.openedContainer, null); + + // nsINavHistoryResultObserver.batching + do_check_false(resultObserver.inBatchMode); + histsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + bmsvc.runInBatchMode({ + runBatched: function (aUserData) { + do_check_true(resultObserver.inBatchMode); + } + }, null); + do_check_false(resultObserver.inBatchMode); + + root.containerOpen = false; + do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer); + result.removeObserver(resultObserver); + resultObserver.reset(); + PlacesTestUtils.promiseAsyncUpdates().then(run_next_test); +}); diff --git a/toolkit/components/places/tests/unit/test_null_interfaces.js b/toolkit/components/places/tests/unit/test_null_interfaces.js new file mode 100644 index 000000000..524837ca3 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_null_interfaces.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test bug 489872 to make sure passing nulls to nsNavHistory doesn't crash. + */ + +// Make an array of services to test, each specifying a class id, interface +// and an array of function names that don't throw when passed nulls +var testServices = [ + ["browser/nav-history-service;1", + ["nsINavHistoryService"], + ["queryStringToQueries", "removePagesByTimeframe", "removePagesFromHost", "getObservers"] + ], + ["browser/nav-bookmarks-service;1", + ["nsINavBookmarksService", "nsINavHistoryObserver", "nsIAnnotationObserver"], + ["createFolder", "getObservers", "onFrecencyChanged", "onTitleChanged", + "onPageAnnotationSet", "onPageAnnotationRemoved", "onDeleteURI"] + ], + ["browser/livemark-service;2", ["mozIAsyncLivemarks"], ["reloadLivemarks"]], + ["browser/annotation-service;1", ["nsIAnnotationService"], []], + ["browser/favicon-service;1", ["nsIFaviconService"], []], + ["browser/tagging-service;1", ["nsITaggingService"], []], +]; +do_print(testServices.join("\n")); + +function run_test() +{ + for (let [cid, ifaces, nothrow] of testServices) { + do_print(`Running test with ${cid} ${ifaces.join(", ")} ${nothrow}`); + let s = Cc["@mozilla.org/" + cid].getService(Ci.nsISupports); + for (let iface of ifaces) { + s.QueryInterface(Ci[iface]); + } + + let okName = function(name) { + do_print(`Checking if function is okay to test: ${name}`); + let func = s[name]; + + let mesg = ""; + if (typeof func != "function") + mesg = "Not a function!"; + else if (func.length == 0) + mesg = "No args needed!"; + else if (name == "QueryInterface") + mesg = "Ignore QI!"; + + if (mesg) { + do_print(`${mesg} Skipping: ${name}`); + return false; + } + + return true; + } + + do_print(`Generating an array of functions to test service: ${s}`); + for (let n of Object.keys(s).filter(i => okName(i)).sort()) { + do_print(`\nTesting ${ifaces.join(", ")} function with null args: ${n}`); + + let func = s[n]; + let num = func.length; + do_print(`Generating array of nulls for #args: ${num}`); + let args = Array(num).fill(null); + + let tryAgain = true; + while (tryAgain == true) { + try { + do_print(`Calling with args: ${JSON.stringify(args)}`); + func.apply(s, args); + + do_print(`The function did not throw! Is it one of the nothrow? ${nothrow}`); + Assert.notEqual(nothrow.indexOf(n), -1); + + do_print("Must have been an expected nothrow, so no need to try again"); + tryAgain = false; + } + catch (ex) { + if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) { + do_print(`Caught an expected exception: ${ex.name}`); + do_print("Moving on to the next test.."); + tryAgain = false; + } else if (ex.result == Cr.NS_ERROR_XPC_NEED_OUT_OBJECT) { + let pos = Number(ex.message.match(/object arg (\d+)/)[1]); + do_print(`Function call expects an out object at ${pos}`); + args[pos] = {}; + } else if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) { + do_print(`Method not implemented exception: ${ex.name}`); + do_print("Moving on to the next test.."); + tryAgain = false; + } else { + throw ex; + } + } + } + } + } +} diff --git a/toolkit/components/places/tests/unit/test_onItemChanged_tags.js b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js new file mode 100644 index 000000000..7a0eb354d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that changing a tag for a bookmark with multiple tags +// notifies OnItemChanged("tags") only once, and not once per tag. + +function run_test() { + do_test_pending(); + + let tags = ["a", "b", "c"]; + let uri = NetUtil.newURI("http://1.moz.org/"); + + let id = PlacesUtils.bookmarks.insertBookmark( + PlacesUtils.unfiledBookmarksFolderId, uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1" + ); + PlacesUtils.tagging.tagURI(uri, tags); + + let bookmarksObserver = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver + ]), + + _changedCount: 0, + onItemChanged: function (aItemId, aProperty, aIsAnnotationProperty, aValue, + aLastModified, aItemType) { + if (aProperty == "tags") { + do_check_eq(aItemId, id); + this._changedCount++; + } + }, + + onItemRemoved: function (aItemId, aParentId, aIndex, aItemType) { + if (aItemId == id) { + PlacesUtils.bookmarks.removeObserver(this); + do_check_eq(this._changedCount, 2); + do_test_finished(); + } + }, + + onItemAdded: function () {}, + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {}, + }; + PlacesUtils.bookmarks.addObserver(bookmarksObserver, false); + + PlacesUtils.tagging.tagURI(uri, ["d"]); + PlacesUtils.tagging.tagURI(uri, ["e"]); + PlacesUtils.bookmarks.removeItem(id); +} diff --git a/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js new file mode 100644 index 000000000..f6131b211 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js @@ -0,0 +1,179 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const bmsvc = PlacesUtils.bookmarks; +const histsvc = PlacesUtils.history; + +function run_test() { + run_next_test(); +} + +add_task(function* test_addBookmarksAndCheckGuids() { + let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX); + bmsvc.insertBookmark(folder, uri("http://test1.com/"), + bmsvc.DEFAULT_INDEX, "1 title"); + bmsvc.insertBookmark(folder, uri("http://test2.com/"), + bmsvc.DEFAULT_INDEX, "2 title"); + bmsvc.insertBookmark(folder, uri("http://test3.com/"), + bmsvc.DEFAULT_INDEX, "3 title"); + bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX); + bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX); + + let root = PlacesUtils.getFolderContents(folder).root; + do_check_eq(root.childCount, 5); + + // check bookmark guids + let bookmarkGuidZero = root.getChild(0).bookmarkGuid; + do_check_eq(bookmarkGuidZero.length, 12); + // bookmarks have bookmark guids + do_check_eq(root.getChild(1).bookmarkGuid.length, 12); + do_check_eq(root.getChild(2).bookmarkGuid.length, 12); + // separator has bookmark guid + do_check_eq(root.getChild(3).bookmarkGuid.length, 12); + // folder has bookmark guid + do_check_eq(root.getChild(4).bookmarkGuid.length, 12); + // all bookmark guids are different. + do_check_neq(bookmarkGuidZero, root.getChild(1).bookmarkGuid); + do_check_neq(root.getChild(1).bookmarkGuid, root.getChild(2).bookmarkGuid); + do_check_neq(root.getChild(2).bookmarkGuid, root.getChild(3).bookmarkGuid); + do_check_neq(root.getChild(3).bookmarkGuid, root.getChild(4).bookmarkGuid); + + // check page guids + let pageGuidZero = root.getChild(0).pageGuid; + do_check_eq(pageGuidZero.length, 12); + // bookmarks have page guids + do_check_eq(root.getChild(1).pageGuid.length, 12); + do_check_eq(root.getChild(2).pageGuid.length, 12); + // folder and separator don't have page guids + do_check_eq(root.getChild(3).pageGuid, ""); + do_check_eq(root.getChild(4).pageGuid, ""); + + do_check_neq(pageGuidZero, root.getChild(1).pageGuid); + do_check_neq(root.getChild(1).pageGuid, root.getChild(2).pageGuid); + + root.containerOpen = false; + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_updateBookmarksAndCheckGuids() { + let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX); + let b1 = bmsvc.insertBookmark(folder, uri("http://test1.com/"), + bmsvc.DEFAULT_INDEX, "1 title"); + let f1 = bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX); + + let root = PlacesUtils.getFolderContents(folder).root; + do_check_eq(root.childCount, 2); + + // ensure the bookmark and page guids remain the same after modifing other property. + let bookmarkGuidZero = root.getChild(0).bookmarkGuid; + let pageGuidZero = root.getChild(0).pageGuid; + bmsvc.setItemTitle(b1, "1 title mod"); + do_check_eq(root.getChild(0).title, "1 title mod"); + do_check_eq(root.getChild(0).bookmarkGuid, bookmarkGuidZero); + do_check_eq(root.getChild(0).pageGuid, pageGuidZero); + + let bookmarkGuidOne = root.getChild(1).bookmarkGuid; + let pageGuidOne = root.getChild(1).pageGuid; + bmsvc.setItemTitle(f1, "test foolder 234"); + do_check_eq(root.getChild(1).title, "test foolder 234"); + do_check_eq(root.getChild(1).bookmarkGuid, bookmarkGuidOne); + do_check_eq(root.getChild(1).pageGuid, pageGuidOne); + + root.containerOpen = false; + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_addVisitAndCheckGuid() { + // add a visit and test page guid and non-existing bookmark guids. + let sourceURI = uri("http://test4.com/"); + yield PlacesTestUtils.addVisits({ uri: sourceURI }); + do_check_eq(bmsvc.getBookmarkedURIFor(sourceURI), null); + + let options = histsvc.getNewQueryOptions(); + let query = histsvc.getNewQuery(); + query.uri = sourceURI; + let root = histsvc.executeQuery(query, options).root; + root.containerOpen = true; + do_check_eq(root.childCount, 1); + + do_check_valid_places_guid(root.getChild(0).pageGuid); + do_check_eq(root.getChild(0).bookmarkGuid, ""); + root.containerOpen = false; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_addItemsWithInvalidGUIDsFails() { + const INVALID_GUID = "XYZ"; + try { + bmsvc.createFolder(bmsvc.placesRoot, "XYZ folder", + bmsvc.DEFAULT_INDEX, INVALID_GUID); + do_throw("Adding a folder with an invalid guid should fail"); + } + catch (ex) { } + + let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", + bmsvc.DEFAULT_INDEX); + try { + bmsvc.insertBookmark(folder, uri("http://test.tld"), bmsvc.DEFAULT_INDEX, + "title", INVALID_GUID); + do_throw("Adding a bookmark with an invalid guid should fail"); + } + catch (ex) { } + + try { + bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, INVALID_GUID); + do_throw("Adding a separator with an invalid guid should fail"); + } + catch (ex) { } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_addItemsWithGUIDs() { + const FOLDER_GUID = "FOLDER--GUID"; + const BOOKMARK_GUID = "BM------GUID"; + const SEPARATOR_GUID = "SEP-----GUID"; + + let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", + bmsvc.DEFAULT_INDEX, FOLDER_GUID); + bmsvc.insertBookmark(folder, uri("http://test1.com/"), bmsvc.DEFAULT_INDEX, + "1 title", BOOKMARK_GUID); + bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, SEPARATOR_GUID); + + let root = PlacesUtils.getFolderContents(folder).root; + do_check_eq(root.childCount, 2); + do_check_eq(root.bookmarkGuid, FOLDER_GUID); + do_check_eq(root.getChild(0).bookmarkGuid, BOOKMARK_GUID); + do_check_eq(root.getChild(1).bookmarkGuid, SEPARATOR_GUID); + + root.containerOpen = false; + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_emptyGUIDIgnored() { + let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", + bmsvc.DEFAULT_INDEX, ""); + do_check_valid_places_guid(PlacesUtils.getFolderContents(folder) + .root.bookmarkGuid); + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_usingSameGUIDFails() { + const GUID = "XYZXYZXYZXYZ"; + bmsvc.createFolder(bmsvc.placesRoot, "test folder", + bmsvc.DEFAULT_INDEX, GUID); + try { + bmsvc.createFolder(bmsvc.placesRoot, "test folder 2", + bmsvc.DEFAULT_INDEX, GUID); + do_throw("Using the same guid twice should fail"); + } + catch (ex) { } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_placeURIs.js b/toolkit/components/places/tests/unit/test_placeURIs.js new file mode 100644 index 000000000..0f585ca51 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_placeURIs.js @@ -0,0 +1,42 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +// Get history service +try { + var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService); +} catch (ex) { + do_throw("Could not get history service\n"); +} + +// main +function run_test() { + // XXX Full testing coverage for QueriesToQueryString and + // QueryStringToQueries + + var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + const NHQO = Ci.nsINavHistoryQueryOptions; + // Bug 376798 + var query = histsvc.getNewQuery(); + query.setFolders([bs.placesRoot], 1); + do_check_eq(histsvc.queriesToQueryString([query], 1, histsvc.getNewQueryOptions()), + "place:folder=PLACES_ROOT"); + + // Bug 378828 + var options = histsvc.getNewQueryOptions(); + options.sortingAnnotation = "test anno"; + options.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING; + var placeURI = + "place:folder=PLACES_ROOT&sort=" + NHQO.SORT_BY_ANNOTATION_DESCENDING + + "&sortingAnnotation=test%20anno"; + do_check_eq(histsvc.queriesToQueryString([query], 1, options), + placeURI); + options = {}; + histsvc.queryStringToQueries(placeURI, { }, {}, options); + do_check_eq(options.value.sortingAnnotation, "test anno"); + do_check_eq(options.value.sortingMode, NHQO.SORT_BY_ANNOTATION_DESCENDING); +} diff --git a/toolkit/components/places/tests/unit/test_placesTxn.js b/toolkit/components/places/tests/unit/test_placesTxn.js new file mode 100644 index 000000000..3cc9809bb --- /dev/null +++ b/toolkit/components/places/tests/unit/test_placesTxn.js @@ -0,0 +1,937 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var bmsvc = PlacesUtils.bookmarks; +var tagssvc = PlacesUtils.tagging; +var annosvc = PlacesUtils.annotations; +var txnManager = PlacesUtils.transactionManager; +const DESCRIPTION_ANNO = "bookmarkProperties/description"; + +function* promiseKeyword(keyword, href, postData) { + while (true) { + let entry = yield PlacesUtils.keywords.fetch(keyword); + if (href == null && !entry) + break; + if (entry && entry.url.href == href && entry.postData == postData) { + break; + } + + yield new Promise(resolve => do_timeout(100, resolve)); + } +} + +// create and add bookmarks observer +var observer = { + + onBeginUpdateBatch: function() { + this._beginUpdateBatch = true; + }, + _beginUpdateBatch: false, + + onEndUpdateBatch: function() { + this._endUpdateBatch = true; + }, + _endUpdateBatch: false, + + onItemAdded: function(id, folder, index, itemType, uri) { + this._itemAddedId = id; + this._itemAddedParent = folder; + this._itemAddedIndex = index; + this._itemAddedType = itemType; + }, + _itemAddedId: null, + _itemAddedParent: null, + _itemAddedIndex: null, + _itemAddedType: null, + + onItemRemoved: function(id, folder, index, itemType) { + this._itemRemovedId = id; + this._itemRemovedFolder = folder; + this._itemRemovedIndex = index; + }, + _itemRemovedId: null, + _itemRemovedFolder: null, + _itemRemovedIndex: null, + + onItemChanged: function(id, property, isAnnotationProperty, newValue, + lastModified, itemType) { + // The transaction manager is being rewritten in bug 891303, so just + // skip checking this for now. + if (property == "tags") + return; + this._itemChangedId = id; + this._itemChangedProperty = property; + this._itemChanged_isAnnotationProperty = isAnnotationProperty; + this._itemChangedValue = newValue; + }, + _itemChangedId: null, + _itemChangedProperty: null, + _itemChanged_isAnnotationProperty: null, + _itemChangedValue: null, + + onItemVisited: function(id, visitID, time) { + this._itemVisitedId = id; + this._itemVisitedVistId = visitID; + this._itemVisitedTime = time; + }, + _itemVisitedId: null, + _itemVisitedVistId: null, + _itemVisitedTime: null, + + onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex, + itemType) { + this._itemMovedId = id; + this._itemMovedOldParent = oldParent; + this._itemMovedOldIndex = oldIndex; + this._itemMovedNewParent = newParent; + this._itemMovedNewIndex = newIndex; + }, + _itemMovedId: null, + _itemMovedOldParent: null, + _itemMovedOldIndex: null, + _itemMovedNewParent: null, + _itemMovedNewIndex: null, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsINavBookmarkObserver) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +// index at which items should begin +var bmStartIndex = 0; + +// get bookmarks root id +var root = PlacesUtils.bookmarksMenuFolderId; + +add_task(function* init() { + bmsvc.addObserver(observer, false); + do_register_cleanup(function () { + bmsvc.removeObserver(observer); + }); +}); + +add_task(function* test_create_folder_with_description() { + const TEST_FOLDERNAME = "Test creating a folder with a description"; + const TEST_DESCRIPTION = "this is my test description"; + + let annos = [{ name: DESCRIPTION_ANNO, + type: annosvc.TYPE_STRING, + flags: 0, + value: TEST_DESCRIPTION, + expires: annosvc.EXPIRE_NEVER }]; + let txn = new PlacesCreateFolderTransaction(TEST_FOLDERNAME, root, bmStartIndex, annos); + txnManager.doTransaction(txn); + + // This checks that calling undoTransaction on an "empty batch" doesn't + // undo the previous transaction (getItemTitle will fail) + txnManager.beginBatch(null); + txnManager.endBatch(false); + txnManager.undoTransaction(); + + let folderId = observer._itemAddedId; + do_check_eq(bmsvc.getItemTitle(folderId), TEST_FOLDERNAME); + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_eq(observer._itemAddedParent, root); + do_check_eq(observer._itemAddedId, folderId); + do_check_eq(TEST_DESCRIPTION, annosvc.getItemAnnotation(folderId, DESCRIPTION_ANNO)); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, folderId); + do_check_eq(observer._itemRemovedFolder, root); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); + + txn.redoTransaction(); + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_eq(observer._itemAddedParent, root); + do_check_eq(observer._itemAddedId, folderId); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, folderId); + do_check_eq(observer._itemRemovedFolder, root); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); +}); + +add_task(function* test_create_item() { + let testURI = NetUtil.newURI("http://test_create_item.com"); + + let txn = new PlacesCreateBookmarkTransaction(testURI, root, bmStartIndex, + "Test creating an item"); + + txnManager.doTransaction(txn); + let id = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(observer._itemAddedId, id); + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_true(bmsvc.isBookmarked(testURI)); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, id); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); + do_check_false(bmsvc.isBookmarked(testURI)); + + txn.redoTransaction(); + do_check_true(bmsvc.isBookmarked(testURI)); + let newId = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_eq(observer._itemAddedParent, root); + do_check_eq(observer._itemAddedId, newId); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, newId); + do_check_eq(observer._itemRemovedFolder, root); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); +}); + +add_task(function* test_create_item_to_folder() { + const TEST_FOLDERNAME = "Test creating item to a folder"; + let testURI = NetUtil.newURI("http://test_create_item_to_folder.com"); + let folderId = bmsvc.createFolder(root, TEST_FOLDERNAME, bmsvc.DEFAULT_INDEX); + + let txn = new PlacesCreateBookmarkTransaction(testURI, folderId, bmStartIndex, + "Test creating item"); + txnManager.doTransaction(txn); + let bkmId = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(observer._itemAddedId, bkmId); + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_true(bmsvc.isBookmarked(testURI)); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, bkmId); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); + + txn.redoTransaction(); + let newBkmId = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(observer._itemAddedIndex, bmStartIndex); + do_check_eq(observer._itemAddedParent, folderId); + do_check_eq(observer._itemAddedId, newBkmId); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, newBkmId); + do_check_eq(observer._itemRemovedFolder, folderId); + do_check_eq(observer._itemRemovedIndex, bmStartIndex); +}); + +add_task(function* test_move_items_to_folder() { + let testFolderId = bmsvc.createFolder(root, "Test move items", bmsvc.DEFAULT_INDEX); + let testURI = NetUtil.newURI("http://test_move_items.com"); + let testBkmId = bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "1: Test move items"); + bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "2: Test move items"); + + // Moving items between the same folder + let sameTxn = new PlacesMoveItemTransaction(testBkmId, testFolderId, bmsvc.DEFAULT_INDEX); + + sameTxn.doTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 1); + + sameTxn.undoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 1); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 0); + + sameTxn.redoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 1); + + sameTxn.undoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 1); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 0); + + // Moving items between different folders + let folderId = bmsvc.createFolder(testFolderId, + "Test move items between different folders", + bmsvc.DEFAULT_INDEX); + let diffTxn = new PlacesMoveItemTransaction(testBkmId, folderId, bmsvc.DEFAULT_INDEX); + + diffTxn.doTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, folderId); + do_check_eq(observer._itemMovedNewIndex, 0); + + sameTxn.undoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, folderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 0); + + diffTxn.redoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, testFolderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, folderId); + do_check_eq(observer._itemMovedNewIndex, 0); + + sameTxn.undoTransaction(); + do_check_eq(observer._itemMovedId, testBkmId); + do_check_eq(observer._itemMovedOldParent, folderId); + do_check_eq(observer._itemMovedOldIndex, 0); + do_check_eq(observer._itemMovedNewParent, testFolderId); + do_check_eq(observer._itemMovedNewIndex, 0); +}); + +add_task(function* test_remove_folder() { + let testFolder = bmsvc.createFolder(root, "Test Removing a Folder", bmsvc.DEFAULT_INDEX); + let folderId = bmsvc.createFolder(testFolder, "Removed Folder", bmsvc.DEFAULT_INDEX); + + let txn = new PlacesRemoveItemTransaction(folderId); + + txn.doTransaction(); + do_check_eq(observer._itemRemovedId, folderId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedId, folderId); + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); + + txn.redoTransaction(); + do_check_eq(observer._itemRemovedId, folderId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedId, folderId); + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); +}); + +add_task(function* test_remove_item_with_tag() { + // Notice in this case the tag persists since other bookmarks have same uri. + let testFolder = bmsvc.createFolder(root, "Test removing an item with a tag", + bmsvc.DEFAULT_INDEX); + + const TAG_NAME = "tag-test_remove_item_with_tag"; + let testURI = NetUtil.newURI("http://test_remove_item_with_tag.com"); + let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1"); + + // create bookmark for not removing tag. + bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item2"); + + // set tag + tagssvc.tagURI(testURI, [TAG_NAME]); + + let txn = new PlacesRemoveItemTransaction(testBkmId); + + txn.doTransaction(); + do_check_eq(observer._itemRemovedId, testBkmId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + do_check_eq(tagssvc.getTagsForURI(testURI), TAG_NAME); + + txn.undoTransaction(); + let newbkmk2Id = observer._itemAddedId; + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); + do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME); + + txn.redoTransaction(); + do_check_eq(observer._itemRemovedId, newbkmk2Id); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); + do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME); +}); + +add_task(function* test_remove_item_with_keyword() { + // Notice in this case the tag persists since other bookmarks have same uri. + let testFolder = bmsvc.createFolder(root, "Test removing an item with a keyword", + bmsvc.DEFAULT_INDEX); + + const KEYWORD = "test: test removing an item with a keyword"; + let testURI = NetUtil.newURI("http://test_remove_item_with_keyword.com"); + let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1"); + + // set keyword + yield PlacesUtils.keywords.insert({ url: testURI.spec, keyword: KEYWORD}); + + let txn = new PlacesRemoveItemTransaction(testBkmId); + + txn.doTransaction(); + do_check_eq(observer._itemRemovedId, testBkmId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + yield promiseKeyword(KEYWORD, null); + + txn.undoTransaction(); + let newbkmk2Id = observer._itemAddedId; + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); + yield promiseKeyword(KEYWORD, testURI.spec); + + txn.redoTransaction(); + do_check_eq(observer._itemRemovedId, newbkmk2Id); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + yield promiseKeyword(KEYWORD, null); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); +}); + +add_task(function* test_creating_separator() { + let testFolder = bmsvc.createFolder(root, "Test creating a separator", bmsvc.DEFAULT_INDEX); + + let txn = new PlacesCreateSeparatorTransaction(testFolder, 0); + txn.doTransaction(); + + let sepId = observer._itemAddedId; + do_check_eq(observer._itemAddedIndex, 0); + do_check_eq(observer._itemAddedParent, testFolder); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, sepId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + + txn.redoTransaction(); + let newSepId = observer._itemAddedId; + do_check_eq(observer._itemAddedIndex, 0); + do_check_eq(observer._itemAddedParent, testFolder); + + txn.undoTransaction(); + do_check_eq(observer._itemRemovedId, newSepId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); +}); + +add_task(function* test_removing_separator() { + let testFolder = bmsvc.createFolder(root, "Test removing a separator", bmsvc.DEFAULT_INDEX); + + let sepId = bmsvc.insertSeparator(testFolder, 0); + let txn = new PlacesRemoveItemTransaction(sepId); + + txn.doTransaction(); + do_check_eq(observer._itemRemovedId, sepId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedId, sepId); // New separator created + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); + + txn.redoTransaction(); + do_check_eq(observer._itemRemovedId, sepId); + do_check_eq(observer._itemRemovedFolder, testFolder); + do_check_eq(observer._itemRemovedIndex, 0); + + txn.undoTransaction(); + do_check_eq(observer._itemAddedId, sepId); // New separator created + do_check_eq(observer._itemAddedParent, testFolder); + do_check_eq(observer._itemAddedIndex, 0); +}); + +add_task(function* test_editing_item_title() { + const TITLE = "Test editing item title"; + const MOD_TITLE = "Mod: Test editing item title"; + let testURI = NetUtil.newURI("http://www.test_editing_item_title.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, TITLE); + + let txn = new PlacesEditItemTitleTransaction(testBkmId, MOD_TITLE); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "title"); + do_check_eq(observer._itemChangedValue, MOD_TITLE); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "title"); + do_check_eq(observer._itemChangedValue, TITLE); + + txn.redoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "title"); + do_check_eq(observer._itemChangedValue, MOD_TITLE); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "title"); + do_check_eq(observer._itemChangedValue, TITLE); +}); + +add_task(function* test_editing_item_uri() { + const OLD_TEST_URI = NetUtil.newURI("http://old.test_editing_item_uri.com/"); + const NEW_TEST_URI = NetUtil.newURI("http://new.test_editing_item_uri.com/"); + let testBkmId = bmsvc.insertBookmark(root, OLD_TEST_URI, bmsvc.DEFAULT_INDEX, + "Test editing item title"); + tagssvc.tagURI(OLD_TEST_URI, ["tag"]); + + let txn = new PlacesEditBookmarkURITransaction(testBkmId, NEW_TEST_URI); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "uri"); + do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"])); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([])); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "uri"); + do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"])); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([])); + + txn.redoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "uri"); + do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"])); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([])); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "uri"); + do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"])); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([])); +}); + +add_task(function* test_edit_description_transaction() { + let testURI = NetUtil.newURI("http://test_edit_description_transaction.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit description transaction"); + + let anno = { + name: DESCRIPTION_ANNO, + type: Ci.nsIAnnotationService.TYPE_STRING, + flags: 0, + value: "Test edit Description", + expires: Ci.nsIAnnotationService.EXPIRE_NEVER, + }; + let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, DESCRIPTION_ANNO); +}); + +add_task(function* test_edit_keyword() { + const KEYWORD = "keyword-test_edit_keyword"; + + let testURI = NetUtil.newURI("http://test_edit_keyword.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword"); + + let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData"); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "keyword"); + do_check_eq(observer._itemChangedValue, KEYWORD); + do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData"); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "keyword"); + do_check_eq(observer._itemChangedValue, ""); + do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null); +}); + +add_task(function* test_edit_specific_keyword() { + const KEYWORD = "keyword-test_edit_keyword2"; + + let testURI = NetUtil.newURI("http://test_edit_keyword2.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword"); + // Add multiple keyword to this uri. + yield PlacesUtils.keywords.insert({ keyword: "kw1", url: testURI.spec, postData: "postData1" }); + yield PlacesUtils.keywords.insert({keyword: "kw2", url: testURI.spec, postData: "postData2" }); + + // Try to change only kw2. + let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData2", "kw2"); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "keyword"); + do_check_eq(observer._itemChangedValue, KEYWORD); + let entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, testURI.spec); + Assert.equal(entry.postData, "postData1"); + yield promiseKeyword(KEYWORD, testURI.spec, "postData2"); + yield promiseKeyword("kw2", null); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "keyword"); + do_check_eq(observer._itemChangedValue, "kw2"); + do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData1"); + entry = yield PlacesUtils.keywords.fetch("kw1"); + Assert.equal(entry.url.href, testURI.spec); + Assert.equal(entry.postData, "postData1"); + yield promiseKeyword("kw2", testURI.spec, "postData2"); + yield promiseKeyword("keyword", null); +}); + +add_task(function* test_LoadInSidebar_transaction() { + let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction"); + + const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; + let anno = { name: LOAD_IN_SIDEBAR_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: true, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); +}); + +add_task(function* test_generic_item_annotation() { + let testURI = NetUtil.newURI("http://test_generic_item_annotation.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test generic item annotation"); + + let itemAnnoObj = { name: "testAnno/testInt", + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: 123, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let txn = new PlacesSetItemAnnotationTransaction(testBkmId, itemAnnoObj); + + txn.doTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "testAnno/testInt"); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); + + txn.undoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "testAnno/testInt"); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); + + txn.redoTransaction(); + do_check_eq(observer._itemChangedId, testBkmId); + do_check_eq(observer._itemChangedProperty, "testAnno/testInt"); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); +}); + +add_task(function* test_editing_item_date_added() { + let testURI = NetUtil.newURI("http://test_editing_item_date_added.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, + "Test editing item date added"); + + let oldAdded = bmsvc.getItemDateAdded(testBkmId); + let newAdded = Date.now() * 1000 + 1000; + let txn = new PlacesEditItemDateAddedTransaction(testBkmId, newAdded); + + txn.doTransaction(); + do_check_eq(newAdded, bmsvc.getItemDateAdded(testBkmId)); + + txn.undoTransaction(); + do_check_eq(oldAdded, bmsvc.getItemDateAdded(testBkmId)); +}); + +add_task(function* test_edit_item_last_modified() { + let testURI = NetUtil.newURI("http://test_edit_item_last_modified.com"); + let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, + "Test editing item last modified"); + + let oldModified = bmsvc.getItemLastModified(testBkmId); + let newModified = Date.now() * 1000 + 1000; + let txn = new PlacesEditItemLastModifiedTransaction(testBkmId, newModified); + + txn.doTransaction(); + do_check_eq(newModified, bmsvc.getItemLastModified(testBkmId)); + + txn.undoTransaction(); + do_check_eq(oldModified, bmsvc.getItemLastModified(testBkmId)); +}); + +add_task(function* test_generic_page_annotation() { + const TEST_ANNO = "testAnno/testInt"; + let testURI = NetUtil.newURI("http://www.mozilla.org/"); + PlacesTestUtils.addVisits(testURI).then(function () { + let pageAnnoObj = { name: TEST_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: 123, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let txn = new PlacesSetPageAnnotationTransaction(testURI, pageAnnoObj); + + txn.doTransaction(); + do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO)); + + txn.undoTransaction(); + do_check_false(annosvc.pageHasAnnotation(testURI, TEST_ANNO)); + + txn.redoTransaction(); + do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO)); + }); +}); + +add_task(function* test_sort_folder_by_name() { + let testFolder = bmsvc.createFolder(root, "Test PlacesSortFolderByNameTransaction", + bmsvc.DEFAULT_INDEX); + let testURI = NetUtil.newURI("http://test_sort_folder_by_name.com"); + + bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark3"); + bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark2"); + bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark1"); + + let bkmIds = bmsvc.getBookmarkIdsForURI(testURI); + bkmIds.sort(); + + let b1 = bkmIds[0]; + let b2 = bkmIds[1]; + let b3 = bkmIds[2]; + + do_check_eq(0, bmsvc.getItemIndex(b1)); + do_check_eq(1, bmsvc.getItemIndex(b2)); + do_check_eq(2, bmsvc.getItemIndex(b3)); + + let txn = new PlacesSortFolderByNameTransaction(testFolder); + + txn.doTransaction(); + do_check_eq(2, bmsvc.getItemIndex(b1)); + do_check_eq(1, bmsvc.getItemIndex(b2)); + do_check_eq(0, bmsvc.getItemIndex(b3)); + + txn.undoTransaction(); + do_check_eq(0, bmsvc.getItemIndex(b1)); + do_check_eq(1, bmsvc.getItemIndex(b2)); + do_check_eq(2, bmsvc.getItemIndex(b3)); + + txn.redoTransaction(); + do_check_eq(2, bmsvc.getItemIndex(b1)); + do_check_eq(1, bmsvc.getItemIndex(b2)); + do_check_eq(0, bmsvc.getItemIndex(b3)); + + txn.undoTransaction(); + do_check_eq(0, bmsvc.getItemIndex(b1)); + do_check_eq(1, bmsvc.getItemIndex(b2)); + do_check_eq(2, bmsvc.getItemIndex(b3)); +}); + +add_task(function* test_tagURI_untagURI() { + const TAG_1 = "tag-test_tagURI_untagURI-bar"; + const TAG_2 = "tag-test_tagURI_untagURI-foo"; + let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com"); + + // Test tagURI + let tagTxn = new PlacesTagURITransaction(tagURI, [TAG_1, TAG_2]); + + tagTxn.doTransaction(); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2])); + + tagTxn.undoTransaction(); + do_check_eq(tagssvc.getTagsForURI(tagURI).length, 0); + + tagTxn.redoTransaction(); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2])); + + // Test untagURI + let untagTxn = new PlacesUntagURITransaction(tagURI, [TAG_1]); + + untagTxn.doTransaction(); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2])); + + untagTxn.undoTransaction(); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2])); + + untagTxn.redoTransaction(); + do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2])); +}); + +add_task(function* test_aggregate_removeItem_Txn() { + let testFolder = bmsvc.createFolder(root, "Test aggregate removeItem transaction", bmsvc.DEFAULT_INDEX); + + const TEST_URL = "http://test_aggregate_removeitem_txn.com/"; + const FOLDERNAME = "Folder"; + let testURI = NetUtil.newURI(TEST_URL); + + let bkmk1Id = bmsvc.insertBookmark(testFolder, testURI, 0, "Mozilla"); + let bkmk2Id = bmsvc.insertSeparator(testFolder, 1); + let bkmk3Id = bmsvc.createFolder(testFolder, FOLDERNAME, 2); + + let bkmk3_1Id = bmsvc.insertBookmark(bkmk3Id, testURI, 0, "Mozilla"); + let bkmk3_2Id = bmsvc.insertSeparator(bkmk3Id, 1); + let bkmk3_3Id = bmsvc.createFolder(bkmk3Id, FOLDERNAME, 2); + + let childTxn1 = new PlacesRemoveItemTransaction(bkmk1Id); + let childTxn2 = new PlacesRemoveItemTransaction(bkmk2Id); + let childTxn3 = new PlacesRemoveItemTransaction(bkmk3Id); + let transactions = [childTxn1, childTxn2, childTxn3]; + let txn = new PlacesAggregatedTransaction("RemoveItems", transactions); + + txn.doTransaction(); + do_check_eq(bmsvc.getItemIndex(bkmk1Id), -1); + do_check_eq(bmsvc.getItemIndex(bkmk2Id), -1); + do_check_eq(bmsvc.getItemIndex(bkmk3Id), -1); + do_check_eq(bmsvc.getItemIndex(bkmk3_1Id), -1); + do_check_eq(bmsvc.getItemIndex(bkmk3_2Id), -1); + do_check_eq(bmsvc.getItemIndex(bkmk3_3Id), -1); + // Check last removed item id. + do_check_eq(observer._itemRemovedId, bkmk3Id); + + txn.undoTransaction(); + let newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0); + let newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1); + let newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2); + let newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0); + let newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1); + let newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2); + do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK); + do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL); + do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR); + do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER); + do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK); + do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER); + do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME); + // Check last added back item id. + // Notice items are restored in reverse order. + do_check_eq(observer._itemAddedId, newBkmk1Id); + + txn.redoTransaction(); + do_check_eq(bmsvc.getItemIndex(newBkmk1Id), -1); + do_check_eq(bmsvc.getItemIndex(newBkmk2Id), -1); + do_check_eq(bmsvc.getItemIndex(newBkmk3Id), -1); + do_check_eq(bmsvc.getItemIndex(newBkmk3_1Id), -1); + do_check_eq(bmsvc.getItemIndex(newBkmk3_2Id), -1); + do_check_eq(bmsvc.getItemIndex(newBkmk3_3Id), -1); + // Check last removed item id. + do_check_eq(observer._itemRemovedId, newBkmk3Id); + + txn.undoTransaction(); + newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0); + newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1); + newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2); + newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0); + newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1); + newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2); + do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK); + do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL); + do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR); + do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER); + do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK); + do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR); + do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id); + do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER); + do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME); + // Check last added back item id. + // Notice items are restored in reverse order. + do_check_eq(observer._itemAddedId, newBkmk1Id); +}); + +add_task(function* test_create_item_with_childTxn() { + let testFolder = bmsvc.createFolder(root, "Test creating an item with childTxns", bmsvc.DEFAULT_INDEX); + + const BOOKMARK_TITLE = "parent item"; + let testURI = NetUtil.newURI("http://test_create_item_with_childTxn.com"); + let childTxns = []; + let newDateAdded = Date.now() * 1000 - 20000; + let editDateAdddedTxn = new PlacesEditItemDateAddedTransaction(null, newDateAdded); + childTxns.push(editDateAdddedTxn); + + let itemChildAnnoObj = { name: "testAnno/testInt", + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: 123, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let annoTxn = new PlacesSetItemAnnotationTransaction(null, itemChildAnnoObj); + childTxns.push(annoTxn); + + let itemWChildTxn = new PlacesCreateBookmarkTransaction(testURI, testFolder, bmStartIndex, + BOOKMARK_TITLE, null, null, + childTxns); + try { + txnManager.doTransaction(itemWChildTxn); + let itemId = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(observer._itemAddedId, itemId); + do_check_eq(newDateAdded, bmsvc.getItemDateAdded(itemId)); + do_check_eq(observer._itemChangedProperty, "testAnno/testInt"); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); + do_check_true(annosvc.itemHasAnnotation(itemId, itemChildAnnoObj.name)) + do_check_eq(annosvc.getItemAnnotation(itemId, itemChildAnnoObj.name), itemChildAnnoObj.value); + + itemWChildTxn.undoTransaction(); + do_check_eq(observer._itemRemovedId, itemId); + + itemWChildTxn.redoTransaction(); + do_check_true(bmsvc.isBookmarked(testURI)); + let newId = bmsvc.getBookmarkIdsForURI(testURI)[0]; + do_check_eq(newDateAdded, bmsvc.getItemDateAdded(newId)); + do_check_eq(observer._itemAddedId, newId); + do_check_eq(observer._itemChangedProperty, "testAnno/testInt"); + do_check_eq(observer._itemChanged_isAnnotationProperty, true); + do_check_true(annosvc.itemHasAnnotation(newId, itemChildAnnoObj.name)) + do_check_eq(annosvc.getItemAnnotation(newId, itemChildAnnoObj.name), itemChildAnnoObj.value); + + itemWChildTxn.undoTransaction(); + do_check_eq(observer._itemRemovedId, newId); + } + catch (ex) { + do_throw("Setting a child transaction in a createItem transaction did throw: " + ex); + } +}); + +add_task(function* test_create_folder_with_child_itemTxn() { + let childURI = NetUtil.newURI("http://test_create_folder_with_child_itemTxn.com"); + let childItemTxn = new PlacesCreateBookmarkTransaction(childURI, root, + bmStartIndex, "childItem"); + let txn = new PlacesCreateFolderTransaction("Test creating a folder with child itemTxns", + root, bmStartIndex, null, [childItemTxn]); + try { + txnManager.doTransaction(txn); + let childItemId = bmsvc.getBookmarkIdsForURI(childURI)[0]; + do_check_eq(observer._itemAddedId, childItemId); + do_check_eq(observer._itemAddedIndex, 0); + do_check_true(bmsvc.isBookmarked(childURI)); + + txn.undoTransaction(); + do_check_false(bmsvc.isBookmarked(childURI)); + + txn.redoTransaction(); + let newchildItemId = bmsvc.getBookmarkIdsForURI(childURI)[0]; + do_check_eq(observer._itemAddedIndex, 0); + do_check_eq(observer._itemAddedId, newchildItemId); + do_check_true(bmsvc.isBookmarked(childURI)); + + txn.undoTransaction(); + do_check_false(bmsvc.isBookmarked(childURI)); + } + catch (ex) { + do_throw("Setting a child item transaction in a createFolder transaction did throw: " + ex); + } +}); diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js new file mode 100644 index 000000000..a533c8295 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js @@ -0,0 +1,1356 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Test preventive maintenance + * For every maintenance query create an uncoherent db and check that we take + * correct fix steps, without polluting valid data. + */ + +// Include PlacesDBUtils module +Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + +const FINISHED_MAINTENANCE_NOTIFICATION_TOPIC = "places-maintenance-finished"; + +// Get services and database connection +var hs = PlacesUtils.history; +var bs = PlacesUtils.bookmarks; +var ts = PlacesUtils.tagging; +var as = PlacesUtils.annotations; +var fs = PlacesUtils.favicons; + +var mDBConn = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; + +// ------------------------------------------------------------------------------ +// Helpers + +var defaultBookmarksMaxId = 0; +function cleanDatabase() { + mDBConn.executeSimpleSQL("DELETE FROM moz_places"); + mDBConn.executeSimpleSQL("DELETE FROM moz_historyvisits"); + mDBConn.executeSimpleSQL("DELETE FROM moz_anno_attributes"); + mDBConn.executeSimpleSQL("DELETE FROM moz_annos"); + mDBConn.executeSimpleSQL("DELETE FROM moz_items_annos"); + mDBConn.executeSimpleSQL("DELETE FROM moz_inputhistory"); + mDBConn.executeSimpleSQL("DELETE FROM moz_keywords"); + mDBConn.executeSimpleSQL("DELETE FROM moz_favicons"); + mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId); +} + +function addPlace(aUrl, aFavicon) { + let stmt = mDBConn.createStatement( + "INSERT INTO moz_places (url, url_hash, favicon_id) VALUES (:url, hash(:url), :favicon)"); + stmt.params["url"] = aUrl || "http://www.mozilla.org"; + stmt.params["favicon"] = aFavicon || null; + stmt.execute(); + stmt.finalize(); + return mDBConn.lastInsertRowID; +} + +function addBookmark(aPlaceId, aType, aParent, aKeywordId, aFolderType, aTitle) { + let stmt = mDBConn.createStatement( + `INSERT INTO moz_bookmarks (fk, type, parent, keyword_id, folder_type, + title, guid) + VALUES (:place_id, :type, :parent, :keyword_id, :folder_type, :title, + GENERATE_GUID())`); + stmt.params["place_id"] = aPlaceId || null; + stmt.params["type"] = aType || bs.TYPE_BOOKMARK; + stmt.params["parent"] = aParent || bs.unfiledBookmarksFolder; + stmt.params["keyword_id"] = aKeywordId || null; + stmt.params["folder_type"] = aFolderType || null; + stmt.params["title"] = typeof(aTitle) == "string" ? aTitle : null; + stmt.execute(); + stmt.finalize(); + return mDBConn.lastInsertRowID; +} + +// ------------------------------------------------------------------------------ +// Tests + +var tests = []; + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "A.1", + desc: "Remove obsolete annotations from moz_annos", + + _obsoleteWeaveAttribute: "weave/test", + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid. + this._placeId = addPlace(); + // Add an obsolete attribute. + let stmt = mDBConn.createStatement( + "INSERT INTO moz_anno_attributes (name) VALUES (:anno)" + ); + stmt.params['anno'] = this._obsoleteWeaveAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement( + `INSERT INTO moz_annos (place_id, anno_attribute_id) + VALUES (:place_id, + (SELECT id FROM moz_anno_attributes WHERE name = :anno) + )` + ); + stmt.params['place_id'] = this._placeId; + stmt.params['anno'] = this._obsoleteWeaveAttribute; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that the obsolete annotation has been removed. + let stmt = mDBConn.createStatement( + "SELECT id FROM moz_anno_attributes WHERE name = :anno" + ); + stmt.params['anno'] = this._obsoleteWeaveAttribute; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +tests.push({ + name: "A.2", + desc: "Remove obsolete annotations from moz_items_annos", + + _obsoleteSyncAttribute: "sync/children", + _obsoleteGuidAttribute: "placesInternal/GUID", + _obsoleteWeaveAttribute: "weave/test", + _placeId: null, + _bookmarkId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid. + this._placeId = addPlace(); + // Add a bookmark. + this._bookmarkId = addBookmark(this._placeId); + // Add an obsolete attribute. + let stmt = mDBConn.createStatement( + `INSERT INTO moz_anno_attributes (name) + VALUES (:anno1), (:anno2), (:anno3)` + ); + stmt.params['anno1'] = this._obsoleteSyncAttribute; + stmt.params['anno2'] = this._obsoleteGuidAttribute; + stmt.params['anno3'] = this._obsoleteWeaveAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement( + `INSERT INTO moz_items_annos (item_id, anno_attribute_id) + SELECT :item_id, id + FROM moz_anno_attributes + WHERE name IN (:anno1, :anno2, :anno3)` + ); + stmt.params['item_id'] = this._bookmarkId; + stmt.params['anno1'] = this._obsoleteSyncAttribute; + stmt.params['anno2'] = this._obsoleteGuidAttribute; + stmt.params['anno3'] = this._obsoleteWeaveAttribute; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that the obsolete annotations have been removed. + let stmt = mDBConn.createStatement( + `SELECT id FROM moz_anno_attributes + WHERE name IN (:anno1, :anno2, :anno3)` + ); + stmt.params['anno1'] = this._obsoleteSyncAttribute; + stmt.params['anno2'] = this._obsoleteGuidAttribute; + stmt.params['anno3'] = this._obsoleteWeaveAttribute; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +tests.push({ + name: "A.3", + desc: "Remove unused attributes", + + _usedPageAttribute: "usedPage", + _usedItemAttribute: "usedItem", + _unusedAttribute: "unused", + _placeId: null, + _bookmarkId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // add a bookmark + this._bookmarkId = addBookmark(this._placeId); + // Add a used attribute and an unused one. + let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)"); + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.reset(); + stmt.params['anno'] = this._usedItemAttribute; + stmt.execute(); + stmt.reset(); + stmt.params['anno'] = this._unusedAttribute; + stmt.execute(); + stmt.finalize(); + + stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params['place_id'] = this._placeId; + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params['item_id'] = this._bookmarkId; + stmt.params['anno'] = this._usedItemAttribute; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that used attributes are still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno"); + stmt.params['anno'] = this._usedPageAttribute; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params['anno'] = this._usedItemAttribute; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that unused attribute has been removed + stmt.params['anno'] = this._unusedAttribute; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "B.1", + desc: "Remove annotations with an invalid attribute", + + _usedPageAttribute: "usedPage", + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add a used attribute. + let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)"); + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params['place_id'] = this._placeId; + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.finalize(); + // Add an annotation with a nonexistent attribute + stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, 1337)"); + stmt.params['place_id'] = this._placeId; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that used attribute is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno"); + stmt.params['anno'] = this._usedPageAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // check that annotation with valid attribute is still there + stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)"); + stmt.params['anno'] = this._usedPageAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // Check that annotation with bogus attribute has been removed + stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = 1337"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "B.2", + desc: "Remove orphan page annotations", + + _usedPageAttribute: "usedPage", + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add a used attribute. + let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)"); + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params['place_id'] = this._placeId; + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.reset(); + // Add an annotation to a nonexistent page + stmt.params['place_id'] = 1337; + stmt.params['anno'] = this._usedPageAttribute; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that used attribute is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno"); + stmt.params['anno'] = this._usedPageAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // check that annotation with valid attribute is still there + stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)"); + stmt.params['anno'] = this._usedPageAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // Check that an annotation to a nonexistent page has been removed + stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE place_id = 1337"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ +tests.push({ + name: "C.1", + desc: "fix missing Places root", + + setup: function() { + // Sanity check: ensure that roots are intact. + do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0); + do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot); + + // Remove the root. + mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE parent = 0"); + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE parent = 0"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + }, + + check: function() { + // Ensure the roots have been correctly restored. + do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0); + do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot); + } +}); + +// ------------------------------------------------------------------------------ +tests.push({ + name: "C.2", + desc: "Fix roots titles", + + setup: function() { + // Sanity check: ensure that roots titles are correct. We can use our check. + this.check(); + // Change some roots' titles. + bs.setItemTitle(bs.placesRoot, "bad title"); + do_check_eq(bs.getItemTitle(bs.placesRoot), "bad title"); + bs.setItemTitle(bs.unfiledBookmarksFolder, "bad title"); + do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), "bad title"); + }, + + check: function() { + // Ensure all roots titles are correct. + do_check_eq(bs.getItemTitle(bs.placesRoot), ""); + do_check_eq(bs.getItemTitle(bs.bookmarksMenuFolder), + PlacesUtils.getString("BookmarksMenuFolderTitle")); + do_check_eq(bs.getItemTitle(bs.tagsFolder), + PlacesUtils.getString("TagsFolderTitle")); + do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), + PlacesUtils.getString("OtherBookmarksFolderTitle")); + do_check_eq(bs.getItemTitle(bs.toolbarFolder), + PlacesUtils.getString("BookmarksToolbarFolderTitle")); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.1", + desc: "Remove items without a valid place", + + _validItemId: null, + _invalidItemId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this.placeId = addPlace(); + // Insert a valid bookmark + this._validItemId = addBookmark(this.placeId); + // Insert a bookmark with an invalid place + this._invalidItemId = addBookmark(1337); + }, + + check: function() { + // Check that valid bookmark is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id"); + stmt.params["item_id"] = this._validItemId; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that invalid bookmark has been removed + stmt.params["item_id"] = this._invalidItemId; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.2", + desc: "Remove items that are not uri bookmarks from tag containers", + + _tagId: null, + _bookmarkId: null, + _separatorId: null, + _folderId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Create a tag + this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder); + // Insert a bookmark in the tag + this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId); + // Insert a separator in the tag + this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR, this._tagId); + // Insert a folder in the tag + this._folderId = addBookmark(null, bs.TYPE_FOLDER, this._tagId); + }, + + check: function() { + // Check that valid bookmark is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE type = :type AND parent = :parent"); + stmt.params["type"] = bs.TYPE_BOOKMARK; + stmt.params["parent"] = this._tagId; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that separator is no more there + stmt.params["type"] = bs.TYPE_SEPARATOR; + stmt.params["parent"] = this._tagId; + do_check_false(stmt.executeStep()); + stmt.reset(); + // Check that folder is no more there + stmt.params["type"] = bs.TYPE_FOLDER; + stmt.params["parent"] = this._tagId; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.3", + desc: "Remove empty tags", + + _tagId: null, + _bookmarkId: null, + _emptyTagId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Create a tag + this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder); + // Insert a bookmark in the tag + this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId); + // Create another tag (empty) + this._emptyTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder); + }, + + check: function() { + // Check that valid bookmark is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :id AND type = :type AND parent = :parent"); + stmt.params["id"] = this._bookmarkId; + stmt.params["type"] = bs.TYPE_BOOKMARK; + stmt.params["parent"] = this._tagId; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["id"] = this._tagId; + stmt.params["type"] = bs.TYPE_FOLDER; + stmt.params["parent"] = bs.tagsFolder; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["id"] = this._emptyTagId; + stmt.params["type"] = bs.TYPE_FOLDER; + stmt.params["parent"] = bs.tagsFolder; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.4", + desc: "Move orphan items to unsorted folder", + + _orphanBookmarkId: null, + _orphanSeparatorId: null, + _orphanFolderId: null, + _bookmarkId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Insert an orphan bookmark + this._orphanBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, 8888); + // Insert an orphan separator + this._orphanSeparatorId = addBookmark(null, bs.TYPE_SEPARATOR, 8888); + // Insert a orphan folder + this._orphanFolderId = addBookmark(null, bs.TYPE_FOLDER, 8888); + // Create a child of the last created folder + this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._orphanFolderId); + }, + + check: function() { + // Check that bookmarks are now children of a real folder (unsorted) + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent"); + stmt.params["item_id"] = this._orphanBookmarkId; + stmt.params["parent"] = bs.unfiledBookmarksFolder; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["item_id"] = this._orphanSeparatorId; + stmt.params["parent"] = bs.unfiledBookmarksFolder; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["item_id"] = this._orphanFolderId; + stmt.params["parent"] = bs.unfiledBookmarksFolder; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["item_id"] = this._bookmarkId; + stmt.params["parent"] = this._orphanFolderId; + do_check_true(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.6", + desc: "Fix wrong item types | bookmarks", + + _separatorId: null, + _folderId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add a separator with a fk + this._separatorId = addBookmark(this._placeId, bs.TYPE_SEPARATOR); + // Add a folder with a fk + this._folderId = addBookmark(this._placeId, bs.TYPE_FOLDER); + }, + + check: function() { + // Check that items with an fk have been converted to bookmarks + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type"); + stmt.params["item_id"] = this._separatorId; + stmt.params["type"] = bs.TYPE_BOOKMARK; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["item_id"] = this._folderId; + stmt.params["type"] = bs.TYPE_BOOKMARK; + do_check_true(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.7", + desc: "Fix wrong item types | bookmarks", + + _validBookmarkId: null, + _invalidBookmarkId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add a bookmark with a valid place id + this._validBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK); + // Add a bookmark with a null place id + this._invalidBookmarkId = addBookmark(null, bs.TYPE_BOOKMARK); + }, + + check: function() { + // Check valid bookmark + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type"); + stmt.params["item_id"] = this._validBookmarkId; + stmt.params["type"] = bs.TYPE_BOOKMARK; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check invalid bookmark has been converted to a folder + stmt.params["item_id"] = this._invalidBookmarkId; + stmt.params["type"] = bs.TYPE_FOLDER; + do_check_true(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.9", + desc: "Fix wrong parents", + + _bookmarkId: null, + _separatorId: null, + _bookmarkId1: null, + _bookmarkId2: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Insert a bookmark + this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK); + // Insert a separator + this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR); + // Create 3 children of these items + this._bookmarkId1 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._bookmarkId); + this._bookmarkId2 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._separatorId); + }, + + check: function() { + // Check that bookmarks are now children of a real folder (unsorted) + let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent"); + stmt.params["item_id"] = this._bookmarkId1; + stmt.params["parent"] = bs.unfiledBookmarksFolder; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["item_id"] = this._bookmarkId2; + stmt.params["parent"] = bs.unfiledBookmarksFolder; + do_check_true(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.10", + desc: "Recalculate positions", + + _unfiledBookmarks: [], + _toolbarBookmarks: [], + + setup: function() { + const NUM_BOOKMARKS = 20; + bs.runInBatchMode({ + runBatched: function (aUserData) { + // Add bookmarks to two folders to better perturbate the table. + for (let i = 0; i < NUM_BOOKMARKS; i++) { + bs.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + NetUtil.newURI("http://example.com/"), + bs.DEFAULT_INDEX, "testbookmark"); + } + for (let i = 0; i < NUM_BOOKMARKS; i++) { + bs.insertBookmark(PlacesUtils.toolbarFolderId, + NetUtil.newURI("http://example.com/"), + bs.DEFAULT_INDEX, "testbookmark"); + } + } + }, null); + + function randomize_positions(aParent, aResultArray) { + let stmt = mDBConn.createStatement( + `UPDATE moz_bookmarks SET position = :rand + WHERE id IN ( + SELECT id FROM moz_bookmarks WHERE parent = :parent + ORDER BY RANDOM() LIMIT 1 + )` + ); + for (let i = 0; i < (NUM_BOOKMARKS / 2); i++) { + stmt.params["parent"] = aParent; + stmt.params["rand"] = Math.round(Math.random() * (NUM_BOOKMARKS - 1)); + stmt.execute(); + stmt.reset(); + } + stmt.finalize(); + + // Build the expected ordered list of bookmarks. + stmt = mDBConn.createStatement( + `SELECT id, position + FROM moz_bookmarks WHERE parent = :parent + ORDER BY position ASC, ROWID ASC` + ); + stmt.params["parent"] = aParent; + while (stmt.executeStep()) { + aResultArray.push(stmt.row.id); + print(stmt.row.id + "\t" + stmt.row.position + "\t" + + (aResultArray.length - 1)); + } + stmt.finalize(); + } + + // Set random positions for the added bookmarks. + randomize_positions(PlacesUtils.unfiledBookmarksFolderId, + this._unfiledBookmarks); + randomize_positions(PlacesUtils.toolbarFolderId, this._toolbarBookmarks); + }, + + check: function() { + function check_order(aParent, aResultArray) { + // Build the expected ordered list of bookmarks. + let stmt = mDBConn.createStatement( + `SELECT id, position FROM moz_bookmarks WHERE parent = :parent + ORDER BY position ASC` + ); + stmt.params["parent"] = aParent; + let pass = true; + while (stmt.executeStep()) { + print(stmt.row.id + "\t" + stmt.row.position); + if (aResultArray.indexOf(stmt.row.id) != stmt.row.position) { + pass = false; + } + } + stmt.finalize(); + if (!pass) { + dump_table("moz_bookmarks"); + do_throw("Unexpected unfiled bookmarks order."); + } + } + + check_order(PlacesUtils.unfiledBookmarksFolderId, this._unfiledBookmarks); + check_order(PlacesUtils.toolbarFolderId, this._toolbarBookmarks); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "D.12", + desc: "Fix empty-named tags", + + setup: function() { + // Add a place to ensure place_id = 1 is valid + let placeId = addPlace(); + // Create a empty-named tag. + this._untitledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, ""); + // Insert a bookmark in the tag, otherwise it will be removed. + addBookmark(placeId, bs.TYPE_BOOKMARK, this._untitledTagId); + // Create a empty-named folder. + this._untitledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, ""); + // Create a titled tag. + this._titledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, "titledTag"); + // Insert a bookmark in the tag, otherwise it will be removed. + addBookmark(placeId, bs.TYPE_BOOKMARK, this._titledTagId); + // Create a titled folder. + this._titledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, "titledFolder"); + }, + + check: function() { + // Check that valid bookmark is still there + let stmt = mDBConn.createStatement( + "SELECT title FROM moz_bookmarks WHERE id = :id" + ); + stmt.params["id"] = this._untitledTagId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.title, "(notitle)"); + stmt.reset(); + stmt.params["id"] = this._untitledFolderId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.title, ""); + stmt.reset(); + stmt.params["id"] = this._titledTagId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.title, "titledTag"); + stmt.reset(); + stmt.params["id"] = this._titledFolderId; + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.title, "titledFolder"); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "E.1", + desc: "Remove orphan icons", + + _placeId: null, + + setup: function() { + // Insert favicon entries + let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(:favicon_id, :url)"); + stmt.params["favicon_id"] = 1; + stmt.params["url"] = "http://www1.mozilla.org/favicon.ico"; + stmt.execute(); + stmt.reset(); + stmt.params["favicon_id"] = 2; + stmt.params["url"] = "http://www2.mozilla.org/favicon.ico"; + stmt.execute(); + stmt.finalize(); + // Insert a place using the existing favicon entry + this._placeId = addPlace("http://www.mozilla.org", 1); + }, + + check: function() { + // Check that used icon is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_favicons WHERE id = :favicon_id"); + stmt.params["favicon_id"] = 1; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that unused icon has been removed + stmt.params["favicon_id"] = 2; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "F.1", + desc: "Remove orphan visits", + + _placeId: null, + _invalidPlaceId: 1337, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add a valid visit and an invalid one + stmt = mDBConn.createStatement("INSERT INTO moz_historyvisits(place_id) VALUES (:place_id)"); + stmt.params["place_id"] = this._placeId; + stmt.execute(); + stmt.reset(); + stmt.params["place_id"] = this._invalidPlaceId; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that valid visit is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_historyvisits WHERE place_id = :place_id"); + stmt.params["place_id"] = this._placeId; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that invalid visit has been removed + stmt.params["place_id"] = this._invalidPlaceId; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "G.1", + desc: "Remove orphan input history", + + _placeId: null, + _invalidPlaceId: 1337, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Add input history entries + let stmt = mDBConn.createStatement("INSERT INTO moz_inputhistory (place_id, input) VALUES (:place_id, :input)"); + stmt.params["place_id"] = this._placeId; + stmt.params["input"] = "moz"; + stmt.execute(); + stmt.reset(); + stmt.params["place_id"] = this._invalidPlaceId; + stmt.params["input"] = "moz"; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that inputhistory on valid place is still there + let stmt = mDBConn.createStatement("SELECT place_id FROM moz_inputhistory WHERE place_id = :place_id"); + stmt.params["place_id"] = this._placeId; + do_check_true(stmt.executeStep()); + stmt.reset(); + // Check that inputhistory on invalid place has gone + stmt.params["place_id"] = this._invalidPlaceId; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "H.1", + desc: "Remove item annos with an invalid attribute", + + _usedItemAttribute: "usedItem", + _bookmarkId: null, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Insert a bookmark + this._bookmarkId = addBookmark(this._placeId); + // Add a used attribute. + let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)"); + stmt.params['anno'] = this._usedItemAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params['item_id'] = this._bookmarkId; + stmt.params['anno'] = this._usedItemAttribute; + stmt.execute(); + stmt.finalize(); + // Add an annotation with a nonexistent attribute + stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, 1337)"); + stmt.params['item_id'] = this._bookmarkId; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that used attribute is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno"); + stmt.params['anno'] = this._usedItemAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // check that annotation with valid attribute is still there + stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)"); + stmt.params['anno'] = this._usedItemAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // Check that annotation with bogus attribute has been removed + stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = 1337"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "H.2", + desc: "Remove orphan item annotations", + + _usedItemAttribute: "usedItem", + _bookmarkId: null, + _invalidBookmarkId: 8888, + _placeId: null, + + setup: function() { + // Add a place to ensure place_id = 1 is valid + this._placeId = addPlace(); + // Insert a bookmark + this._bookmarkId = addBookmark(this._placeId); + // Add a used attribute. + stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)"); + stmt.params['anno'] = this._usedItemAttribute; + stmt.execute(); + stmt.finalize(); + stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES (:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))"); + stmt.params["item_id"] = this._bookmarkId; + stmt.params["anno"] = this._usedItemAttribute; + stmt.execute(); + stmt.reset(); + // Add an annotation to a nonexistent item + stmt.params["item_id"] = this._invalidBookmarkId; + stmt.params["anno"] = this._usedItemAttribute; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that used attribute is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno"); + stmt.params['anno'] = this._usedItemAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // check that annotation with valid attribute is still there + stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)"); + stmt.params['anno'] = this._usedItemAttribute; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // Check that an annotation to a nonexistent page has been removed + stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE item_id = 8888"); + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "I.1", + desc: "Remove unused keywords", + + _bookmarkId: null, + _placeId: null, + + setup: function() { + // Insert 2 keywords + let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword, place_id) VALUES(:id, :keyword, :place_id)"); + stmt.params["id"] = 1; + stmt.params["keyword"] = "unused"; + stmt.params["place_id"] = 100; + stmt.execute(); + stmt.finalize(); + }, + + check: function() { + // Check that "used" keyword is still there + let stmt = mDBConn.createStatement("SELECT id FROM moz_keywords WHERE keyword = :keyword"); + // Check that "unused" keyword has gone + stmt.params["keyword"] = "unused"; + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "L.1", + desc: "Fix wrong favicon ids", + + _validIconPlaceId: null, + _invalidIconPlaceId: null, + + setup: function() { + // Insert a favicon entry + let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(1, :url)"); + stmt.params["url"] = "http://www.mozilla.org/favicon.ico"; + stmt.execute(); + stmt.finalize(); + // Insert a place using the existing favicon entry + this._validIconPlaceId = addPlace("http://www1.mozilla.org", 1); + + // Insert a place using a nonexistent favicon entry + this._invalidIconPlaceId = addPlace("http://www2.mozilla.org", 1337); + }, + + check: function() { + // Check that bogus favicon is not there + let stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE favicon_id = :favicon_id"); + stmt.params["favicon_id"] = 1337; + do_check_false(stmt.executeStep()); + stmt.reset(); + // Check that valid favicon is still there + stmt.params["favicon_id"] = 1; + do_check_true(stmt.executeStep()); + stmt.finalize(); + // Check that place entries are there + stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE id = :place_id"); + stmt.params["place_id"] = this._validIconPlaceId; + do_check_true(stmt.executeStep()); + stmt.reset(); + stmt.params["place_id"] = this._invalidIconPlaceId; + do_check_true(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "L.2", + desc: "Recalculate visit_count and last_visit_date", + + setup: function* () { + function setVisitCount(aURL, aValue) { + let stmt = mDBConn.createStatement( + `UPDATE moz_places SET visit_count = :count WHERE url_hash = hash(:url) + AND url = :url` + ); + stmt.params.count = aValue; + stmt.params.url = aURL; + stmt.execute(); + stmt.finalize(); + } + function setLastVisitDate(aURL, aValue) { + let stmt = mDBConn.createStatement( + `UPDATE moz_places SET last_visit_date = :date WHERE url_hash = hash(:url) + AND url = :url` + ); + stmt.params.date = aValue; + stmt.params.url = aURL; + stmt.execute(); + stmt.finalize(); + } + + let now = Date.now() * 1000; + // Add a page with 1 visit. + let url = "http://1.moz.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ }); + // Add a page with 1 visit and set wrong visit_count. + url = "http://2.moz.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ }); + setVisitCount(url, 10); + // Add a page with 1 visit and set wrong last_visit_date. + url = "http://3.moz.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ }); + setLastVisitDate(url, now++); + // Add a page with 1 visit and set wrong stats. + url = "http://4.moz.org/"; + yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ }); + setVisitCount(url, 10); + setLastVisitDate(url, now++); + + // Add a page without visits. + url = "http://5.moz.org/"; + addPlace(url); + // Add a page without visits and set wrong visit_count. + url = "http://6.moz.org/"; + addPlace(url); + setVisitCount(url, 10); + // Add a page without visits and set wrong last_visit_date. + url = "http://7.moz.org/"; + addPlace(url); + setLastVisitDate(url, now++); + // Add a page without visits and set wrong stats. + url = "http://8.moz.org/"; + addPlace(url); + setVisitCount(url, 10); + setLastVisitDate(url, now++); + }, + + check: function() { + let stmt = mDBConn.createStatement( + `SELECT h.id FROM moz_places h + JOIN moz_historyvisits v ON v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9) + GROUP BY h.id HAVING h.visit_count <> count(*) + UNION ALL + SELECT h.id FROM moz_places h + JOIN moz_historyvisits v ON v.place_id = h.id + GROUP BY h.id HAVING h.last_visit_date <> MAX(v.visit_date)` + ); + do_check_false(stmt.executeStep()); + stmt.finalize(); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "L.3", + desc: "recalculate hidden for redirects.", + + *setup() { + yield PlacesTestUtils.addVisits([ + { uri: NetUtil.newURI("http://l3.moz.org/"), + transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://l3.moz.org/redirecting/"), + transition: TRANSITION_TYPED }, + { uri: NetUtil.newURI("http://l3.moz.org/redirecting2/"), + transition: TRANSITION_REDIRECT_TEMPORARY, + referrer: NetUtil.newURI("http://l3.moz.org/redirecting/") }, + { uri: NetUtil.newURI("http://l3.moz.org/target/"), + transition: TRANSITION_REDIRECT_PERMANENT, + referrer: NetUtil.newURI("http://l3.moz.org/redirecting2/") }, + ]); + }, + + check: function () { + return new Promise(resolve => { + let stmt = mDBConn.createAsyncStatement( + "SELECT h.url FROM moz_places h WHERE h.hidden = 1" + ); + stmt.executeAsync({ + _count: 0, + handleResult: function(aResultSet) { + for (let row; (row = aResultSet.getNextRow());) { + let url = row.getResultByIndex(0); + do_check_true(/redirecting/.test(url)); + this._count++; + } + }, + handleError: function(aError) { + }, + handleCompletion: function(aReason) { + dump_table("moz_places"); + dump_table("moz_historyvisits"); + do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED); + do_check_eq(this._count, 2); + resolve(); + } + }); + stmt.finalize(); + }); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "L.4", + desc: "recalculate foreign_count.", + + *setup() { + this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l4.moz.org/", + visits: [{ date: new Date() }] })).guid; + yield PlacesUtils.bookmarks.insert({ url: "http://l4.moz.org/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid}); + yield PlacesUtils.keywords.insert({ url: "http://l4.moz.org/", keyword: "kw" }); + Assert.equal((yield this._getForeignCount()), 2); + }, + + *_getForeignCount() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute(`SELECT foreign_count FROM moz_places + WHERE guid = :guid`, { guid: this._pageGuid }); + return rows[0].getResultByName("foreign_count"); + }, + + *check() { + Assert.equal((yield this._getForeignCount()), 2); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "L.5", + desc: "recalculate hashes when missing.", + + *setup() { + this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l5.moz.org/", + visits: [{ date: new Date() }] })).guid; + Assert.ok((yield this._getHash()) > 0); + yield PlacesUtils.withConnectionWrapper("change url hash", Task.async(function* (db) { + yield db.execute(`UPDATE moz_places SET url_hash = 0`); + })); + Assert.equal((yield this._getHash()), 0); + }, + + *_getHash() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.execute(`SELECT url_hash FROM moz_places + WHERE guid = :guid`, { guid: this._pageGuid }); + return rows[0].getResultByName("url_hash"); + }, + + *check() { + Assert.ok((yield this._getHash()) > 0); + } +}); + +// ------------------------------------------------------------------------------ + +tests.push({ + name: "Z", + desc: "Sanity: Preventive maintenance does not touch valid items", + + _uri1: uri("http://www1.mozilla.org"), + _uri2: uri("http://www2.mozilla.org"), + _folderId: null, + _bookmarkId: null, + _separatorId: null, + + setup: function* () { + // use valid api calls to create a bunch of items + yield PlacesTestUtils.addVisits([ + { uri: this._uri1 }, + { uri: this._uri2 }, + ]); + + this._folderId = bs.createFolder(bs.toolbarFolder, "testfolder", + bs.DEFAULT_INDEX); + do_check_true(this._folderId > 0); + this._bookmarkId = bs.insertBookmark(this._folderId, this._uri1, + bs.DEFAULT_INDEX, "testbookmark"); + do_check_true(this._bookmarkId > 0); + this._separatorId = bs.insertSeparator(bs.unfiledBookmarksFolder, + bs.DEFAULT_INDEX); + do_check_true(this._separatorId > 0); + ts.tagURI(this._uri1, ["testtag"]); + fs.setAndFetchFaviconForPage(this._uri2, SMALLPNG_DATA_URI, false, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal()); + yield PlacesUtils.keywords.insert({ url: this._uri1.spec, keyword: "testkeyword" }); + as.setPageAnnotation(this._uri2, "anno", "anno", 0, as.EXPIRE_NEVER); + as.setItemAnnotation(this._bookmarkId, "anno", "anno", 0, as.EXPIRE_NEVER); + }, + + check: Task.async(function* () { + // Check that all items are correct + let isVisited = yield promiseIsURIVisited(this._uri1); + do_check_true(isVisited); + isVisited = yield promiseIsURIVisited(this._uri2); + do_check_true(isVisited); + + do_check_eq(bs.getBookmarkURI(this._bookmarkId).spec, this._uri1.spec); + do_check_eq(bs.getItemIndex(this._folderId), 0); + do_check_eq(bs.getItemType(this._folderId), bs.TYPE_FOLDER); + do_check_eq(bs.getItemType(this._separatorId), bs.TYPE_SEPARATOR); + + do_check_eq(ts.getTagsForURI(this._uri1).length, 1); + do_check_eq((yield PlacesUtils.keywords.fetch({ url: this._uri1.spec })).keyword, "testkeyword"); + do_check_eq(as.getPageAnnotation(this._uri2, "anno"), "anno"); + do_check_eq(as.getItemAnnotation(this._bookmarkId, "anno"), "anno"); + + yield new Promise(resolve => { + fs.getFaviconURLForPage(this._uri2, aFaviconURI => { + do_check_true(aFaviconURI.equals(SMALLPNG_DATA_URI)); + resolve(); + }); + }); + }) +}); + +// ------------------------------------------------------------------------------ + +add_task(function* test_preventive_maintenance() +{ + // Get current bookmarks max ID for cleanup + let stmt = mDBConn.createStatement("SELECT MAX(id) FROM moz_bookmarks"); + stmt.executeStep(); + defaultBookmarksMaxId = stmt.getInt32(0); + stmt.finalize(); + do_check_true(defaultBookmarksMaxId > 0); + + for (let test of tests) { + dump("\nExecuting test: " + test.name + "\n" + "*** " + test.desc + "\n"); + yield test.setup(); + + let promiseMaintenanceFinished = + promiseTopicObserved(FINISHED_MAINTENANCE_NOTIFICATION_TOPIC); + Services.prefs.clearUserPref("places.database.lastMaintenance"); + let callbackInvoked = false; + PlacesDBUtils.maintenanceOnIdle(() => callbackInvoked = true); + yield promiseMaintenanceFinished; + do_check_true(callbackInvoked); + + // Check the lastMaintenance time has been saved. + do_check_neq(Services.prefs.getIntPref("places.database.lastMaintenance"), null); + + yield test.check(); + + cleanDatabase(); + } + + // Sanity check: all roots should be intact + do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0); + do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot); + do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot); +}); diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js new file mode 100644 index 000000000..a8acb4be0 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Test preventive maintenance checkAndFixDatabase. + */ + +// Include PlacesDBUtils module. +Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + +function run_test() { + do_test_pending(); + PlacesDBUtils.checkAndFixDatabase(function(aLog) { + let sections = []; + let positives = []; + let negatives = []; + let infos = []; + + aLog.forEach(function (aMsg) { + print (aMsg); + switch (aMsg.substr(0, 1)) { + case "+": + positives.push(aMsg); + break; + case "-": + negatives.push(aMsg); + break; + case ">": + sections.push(aMsg); + break; + default: + infos.push(aMsg); + } + }); + + print("Check that we have run all sections."); + do_check_eq(sections.length, 5); + print("Check that we have no negatives."); + do_check_false(!!negatives.length); + print("Check that we have positives."); + do_check_true(!!positives.length); + print("Check that we have info."); + do_check_true(!!infos.length); + + do_test_finished(); + }); +} diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js new file mode 100644 index 000000000..ebe308f03 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Test preventive maintenance runTasks. + */ + +// Include PlacesDBUtils module. +Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + +function run_test() { + do_test_pending(); + PlacesDBUtils.runTasks([PlacesDBUtils.reindex], function(aLog) { + let sections = []; + let positives = []; + let negatives = []; + let infos = []; + + aLog.forEach(function (aMsg) { + print (aMsg); + switch (aMsg.substr(0, 1)) { + case "+": + positives.push(aMsg); + break; + case "-": + negatives.push(aMsg); + break; + case ">": + sections.push(aMsg); + break; + default: + infos.push(aMsg); + } + }); + + print("Check that we have run all sections."); + do_check_eq(sections.length, 1); + print("Check that we have no negatives."); + do_check_false(!!negatives.length); + print("Check that we have positives."); + do_check_true(!!positives.length); + + do_test_finished(); + }); +} diff --git a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js new file mode 100644 index 000000000..0719a0cd4 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js @@ -0,0 +1,256 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function* check_has_child(aParentGuid, aChildGuid) { + let parentTree = yield PlacesUtils.promiseBookmarksTree(aParentGuid); + do_check_true("children" in parentTree); + do_check_true(parentTree.children.find( e => e.guid == aChildGuid ) != null); +} + +function* compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) { + // itemId==-1 indicates a non-bookmark node, which is unexpected. + do_check_neq(aNode.itemId, -1); + + function check_unset(...aProps) { + aProps.forEach( p => { do_check_false(p in aItem); } ); + } + function strict_eq_check(v1, v2) { + dump("v1: " + v1 + " v2: " + v2 + "\n"); + do_check_eq(typeof v1, typeof v2); + do_check_eq(v1, v2); + } + function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) { + if (aOptional && aNode[aNodeProp] === null) + check_unset(aItemProp); + else + strict_eq_check(aItem[aItemProp], aNode[aNodeProp]); + } + function compare_prop_to_value(aItemProp, aValue, aOptional = true) { + if (aOptional && aValue === null) + check_unset(aItemProp); + else + strict_eq_check(aItem[aItemProp], aValue); + } + + // Bug 1013053 - bookmarkIndex is unavailable for the query's root + if (aNode.bookmarkIndex == -1) { + compare_prop_to_value("index", + PlacesUtils.bookmarks.getItemIndex(aNode.itemId), + false); + } + else { + compare_prop("index", "bookmarkIndex"); + } + + compare_prop("dateAdded"); + compare_prop("lastModified"); + + if (aIsRootItem && aNode.itemId != PlacesUtils.placesRootId) { + do_check_true("parentGuid" in aItem); + yield check_has_child(aItem.parentGuid, aItem.guid) + } + else { + check_unset("parentGuid"); + } + + let expectedAnnos = PlacesUtils.getAnnotationsForItem(aItem.id); + if (expectedAnnos.length > 0) { + let annosToString = annos => { + return annos.map(a => a.name + ":" + a.value).sort().join(","); + }; + do_check_true(Array.isArray(aItem.annos)) + do_check_eq(annosToString(aItem.annos), annosToString(expectedAnnos)); + } + else { + check_unset("annos"); + } + const BOOKMARK_ONLY_PROPS = ["uri", "iconuri", "tags", "charset", "keyword"]; + const FOLDER_ONLY_PROPS = ["children", "root"]; + + let nodesCount = 1; + + switch (aNode.type) { + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER: + do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER); + compare_prop("title", "title", true); + check_unset(...BOOKMARK_ONLY_PROPS); + + let expectedChildrenNodes = []; + + PlacesUtils.asContainer(aNode); + if (!aNode.containerOpen) + aNode.containerOpen = true; + + for (let i = 0; i < aNode.childCount; i++) { + let childNode = aNode.getChild(i); + if (childNode.itemId == PlacesUtils.tagsFolderId || + aExcludedGuids.includes(childNode.bookmarkGuid)) { + continue; + } + expectedChildrenNodes.push(childNode); + } + + if (expectedChildrenNodes.length > 0) { + do_check_true(Array.isArray(aItem.children)); + do_check_eq(aItem.children.length, expectedChildrenNodes.length); + for (let i = 0; i < aItem.children.length; i++) { + nodesCount += + yield compareToNode(aItem.children[i], expectedChildrenNodes[i], + false, aExcludedGuids); + } + } + else { + check_unset("children"); + } + + switch (aItem.id) { + case PlacesUtils.placesRootId: + compare_prop_to_value("root", "placesRoot"); + break; + case PlacesUtils.bookmarksMenuFolderId: + compare_prop_to_value("root", "bookmarksMenuFolder"); + break; + case PlacesUtils.toolbarFolderId: + compare_prop_to_value("root", "toolbarFolder"); + break; + case PlacesUtils.unfiledBookmarksFolderId: + compare_prop_to_value("root", "unfiledBookmarksFolder"); + break; + case PlacesUtils.mobileFolderId: + compare_prop_to_value("root", "mobileFolder"); + break; + default: + check_unset("root"); + } + break; + case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR: + do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR); + check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS); + break; + default: + do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE); + compare_prop("uri"); + // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b" + if (aNode.tags === null) + check_unset("tags"); + else + compare_prop_to_value("tags", aNode.tags.replace(/, /g, ","), false); + + if (aNode.icon) { + let nodeIconData = aNode.icon.replace("moz-anno:favicon:", ""); + compare_prop_to_value("iconuri", nodeIconData); + } + else { + check_unset(aItem.iconuri); + } + + check_unset(...FOLDER_ONLY_PROPS); + + let itemURI = uri(aNode.uri); + compare_prop_to_value("charset", + yield PlacesUtils.getCharsetForURI(itemURI)); + + let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri }); + compare_prop_to_value("keyword", entry ? entry.keyword : null); + + if ("title" in aItem) + compare_prop("title"); + else + do_check_null(aNode.title); + } + + if (aIsRootItem) + do_check_eq(aItem.itemsCount, nodesCount); + + return nodesCount; +} + +var itemsCount = 0; +function* new_bookmark(aInfo) { + ++itemsCount; + if (!("url" in aInfo)) + aInfo.url = uri("http://test.item." + itemsCount); + + if (!("title" in aInfo)) + aInfo.title = "Test Item (bookmark) " + itemsCount; + + yield PlacesTransactions.NewBookmark(aInfo).transact(); +} + +function* new_folder(aInfo) { + if (!("title" in aInfo)) + aInfo.title = "Test Item (folder) " + itemsCount; + return yield PlacesTransactions.NewFolder(aInfo).transact(); +} + +// Walks a result nodes tree and test promiseBookmarksTree for each node. +// DO NOT COPY THIS LOGIC: It is done here to accomplish a more comprehensive +// test of the API (the entire hierarchy data is available in the very test). +function* test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGuids) { + do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0); + let item = yield PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions); + yield* compareToNode(item, aNode, true, aExcludedGuids); + + for (let i = 0; i < aNode.childCount; i++) { + let child = aNode.getChild(i); + if (child.itemId != PlacesUtils.tagsFolderId) + yield test_promiseBookmarksTreeForEachNode(child, + { includeItemIds: true }, + aExcludedGuids); + } + return item; +} + +function* test_promiseBookmarksTreeAgainstResult(aItemGuid = "", + aOptions = { includeItemIds: true }, + aExcludedGuids) { + let itemId = aItemGuid ? + yield PlacesUtils.promiseItemId(aItemGuid) : PlacesUtils.placesRootId; + let node = PlacesUtils.getFolderContents(itemId).root; + return yield test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids); +} + +add_task(function* () { + // Add some bookmarks to cover various use cases. + yield new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid }); + yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid, + annotations: [{ name: "TestAnnoA", value: "TestVal" }, + { name: "TestAnnoB", value: 0 }]}); + let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid }; + yield PlacesTransactions.NewSeparator(sepInfo).transact(); + let folderGuid = yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid }); + yield new_bookmark({ title: null, + parentGuid: folderGuid, + keyword: "test_keyword", + tags: ["TestTagA", "TestTagB"], + annotations: [{ name: "TestAnnoA", value: "TestVal2"}]}); + let urlWithCharsetAndFavicon = uri("http://charset.and.favicon"); + yield new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon }); + yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8"); + yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI); + // Test the default places root without specifying it. + yield test_promiseBookmarksTreeAgainstResult(); + + // Do specify it + yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid); + + // Exclude the bookmarks menu. + // The calllback should be four times - once for the toolbar, once for + // the bookmark we inserted under, and once for the menu (and not + // at all for any of its descendants) and once for the unsorted bookmarks + // folder. However, promiseBookmarksTree is called multiple times, so + // rather than counting the calls, we count the number of unique items + // passed in. + let guidsPassedToExcludeCallback = new Set(); + let placesRootWithoutTheMenu = + yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, { + excludeItemsCallback: aItem => { + guidsPassedToExcludeCallback.add(aItem.guid); + return aItem.root == "bookmarksMenuFolder"; + }, + includeItemIds: true + }, [PlacesUtils.bookmarks.menuGuid]); + do_check_eq(guidsPassedToExcludeCallback.size, 5); + do_check_eq(placesRootWithoutTheMenu.children.length, 3); +}); diff --git a/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js new file mode 100644 index 000000000..01fb3eef9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js @@ -0,0 +1,49 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + run_next_test(); +} + +add_test(function test_resolveNullBookmarkTitles() { + let uri1 = uri("http://foo.tld/"); + let uri2 = uri("https://bar.tld/"); + + PlacesTestUtils.addVisits([ + { uri: uri1, title: "foo title" }, + { uri: uri2, title: "bar title" } + ]).then(function () { + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, + uri1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + null); + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, + uri2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + null); + + PlacesUtils.tagging.tagURI(uri1, ["tag 1"]); + PlacesUtils.tagging.tagURI(uri2, ["tag 2"]); + + let options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; + options.resultType = options.RESULTS_AS_TAG_CONTENTS; + + let query = PlacesUtils.history.getNewQuery(); + // if we don't set a tag folder, RESULTS_AS_TAG_CONTENTS will return all + // tagged URIs + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + do_check_eq(root.childCount, 2); + // actually RESULTS_AS_TAG_CONTENTS return results ordered by place_id DESC + // so they are reversed + do_check_eq(root.getChild(0).title, "bar title"); + do_check_eq(root.getChild(1).title, "foo title"); + root.containerOpen = false; + + run_next_test(); + }); +}); diff --git a/toolkit/components/places/tests/unit/test_result_sort.js b/toolkit/components/places/tests/unit/test_result_sort.js new file mode 100644 index 000000000..35405ac50 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_result_sort.js @@ -0,0 +1,139 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const NHQO = Ci.nsINavHistoryQueryOptions; + +/** + * Waits for onItemVisited notifications to be received. + */ +function promiseOnItemVisited() { + let defer = Promise.defer(); + let bookmarksObserver = { + __proto__: NavBookmarkObserver.prototype, + onItemVisited: function BO_onItemVisited() { + PlacesUtils.bookmarks.removeObserver(this); + // Enqueue to be sure that all onItemVisited notifications ran. + do_execute_soon(defer.resolve); + } + }; + PlacesUtils.bookmarks.addObserver(bookmarksObserver, false); + return defer.promise; +} + +function run_test() { + run_next_test(); +} + +add_task(function* test() { + let testFolder = PlacesUtils.bookmarks.createFolder( + PlacesUtils.bookmarks.placesRoot, + "Result-sort functionality tests root", + PlacesUtils.bookmarks.DEFAULT_INDEX); + + let uri1 = NetUtil.newURI("http://foo.tld/a"); + let uri2 = NetUtil.newURI("http://foo.tld/b"); + + let id1 = PlacesUtils.bookmarks.insertBookmark( + testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "b"); + let id2 = PlacesUtils.bookmarks.insertBookmark( + testFolder, uri2, PlacesUtils.bookmarks.DEFAULT_INDEX, "a"); + // url of id1, title of id2 + let id3 = PlacesUtils.bookmarks.insertBookmark( + testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "a"); + + // query with natural order + let result = PlacesUtils.getFolderContents(testFolder); + let root = result.root; + + do_check_eq(root.childCount, 3); + + function checkOrder(a, b, c) { + do_check_eq(root.getChild(0).itemId, a); + do_check_eq(root.getChild(1).itemId, b); + do_check_eq(root.getChild(2).itemId, c); + } + + // natural order + do_print("Natural order"); + checkOrder(id1, id2, id3); + + // title: id3 should precede id2 since we fall-back to URI-based sorting + do_print("Sort by title asc"); + result.sortingMode = NHQO.SORT_BY_TITLE_ASCENDING; + checkOrder(id3, id2, id1); + + // In reverse + do_print("Sort by title desc"); + result.sortingMode = NHQO.SORT_BY_TITLE_DESCENDING; + checkOrder(id1, id2, id3); + + // uri sort: id1 should precede id3 since we fall-back to natural order + do_print("Sort by uri asc"); + result.sortingMode = NHQO.SORT_BY_URI_ASCENDING; + checkOrder(id1, id3, id2); + + // test live update + do_print("Change bookmark uri liveupdate"); + PlacesUtils.bookmarks.changeBookmarkURI(id1, uri2); + checkOrder(id3, id1, id2); + PlacesUtils.bookmarks.changeBookmarkURI(id1, uri1); + checkOrder(id1, id3, id2); + + // keyword sort + do_print("Sort by keyword asc"); + result.sortingMode = NHQO.SORT_BY_KEYWORD_ASCENDING; + checkOrder(id3, id2, id1); // no keywords set - falling back to title sort + yield PlacesUtils.keywords.insert({ url: uri1.spec, keyword: "a" }); + yield PlacesUtils.keywords.insert({ url: uri2.spec, keyword: "z" }); + checkOrder(id3, id1, id2); + + // XXXtodo: test history sortings (visit count, visit date) + // XXXtodo: test different item types once folderId and bookmarkId are merged. + // XXXtodo: test sortingAnnotation functionality with non-bookmark nodes + + do_print("Sort by annotation desc"); + PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "a", 0, 0); + PlacesUtils.annotations.setItemAnnotation(id3, "testAnno", "b", 0, 0); + result.sortingAnnotation = "testAnno"; + result.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING; + + // id1 precedes id2 per title-descending fallback + checkOrder(id3, id1, id2); + + // XXXtodo: test dateAdded sort + // XXXtodo: test lastModified sort + + // test live update + do_print("Annotation liveupdate"); + PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "c", 0, 0); + checkOrder(id1, id3, id2); + + // Add a visit, then check frecency ordering. + + // When the bookmarks service gets onVisit, it asynchronously fetches all + // items for that visit, and then notifies onItemVisited. Thus we must + // explicitly wait for that. + let waitForVisited = promiseOnItemVisited(); + yield PlacesTestUtils.addVisits({ uri: uri2, transition: TRANSITION_TYPED }); + yield waitForVisited; + + do_print("Sort by frecency desc"); + result.sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING; + for (let i = 0; i < root.childCount; ++i) { + print(root.getChild(i).uri + " " + root.getChild(i).title); + } + // For id1 and id3, since they have same frecency and no visits, fallback + // to sort by the newest bookmark. + checkOrder(id2, id3, id1); + do_print("Sort by frecency asc"); + result.sortingMode = NHQO.SORT_BY_FRECENCY_ASCENDING; + for (let i = 0; i < root.childCount; ++i) { + print(root.getChild(i).uri + " " + root.getChild(i).title); + } + checkOrder(id1, id3, id2); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js new file mode 100644 index 000000000..8e71ffd0d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js @@ -0,0 +1,85 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +const {bookmarks, history} = PlacesUtils + +add_task(function* test_addVisitCheckFields() { + let uri = NetUtil.newURI("http://test4.com/"); + yield PlacesTestUtils.addVisits([ + { uri }, + { uri, referrer: uri }, + { uri, transition: history.TRANSITION_TYPED }, + ]); + + + let options = history.getNewQueryOptions(); + let query = history.getNewQuery(); + + query.uri = uri; + + + // Check RESULTS_AS_VISIT node. + options.resultType = options.RESULTS_AS_VISIT; + + let root = history.executeQuery(query, options).root; + root.containerOpen = true; + + equal(root.childCount, 3); + + let child = root.getChild(0); + equal(child.visitType, history.TRANSITION_LINK, "Visit type should be TRANSITION_LINK"); + equal(child.visitId, 1, "Visit ID should be 1"); + equal(child.fromVisitId, -1, "Should have no referrer visit ID"); + + child = root.getChild(1); + equal(child.visitType, history.TRANSITION_LINK, "Visit type should be TRANSITION_LINK"); + equal(child.visitId, 2, "Visit ID should be 2"); + equal(child.fromVisitId, 1, "First visit should be the referring visit"); + + child = root.getChild(2); + equal(child.visitType, history.TRANSITION_TYPED, "Visit type should be TRANSITION_TYPED"); + equal(child.visitId, 3, "Visit ID should be 3"); + equal(child.fromVisitId, -1, "Should have no referrer visit ID"); + + root.containerOpen = false; + + + // Check RESULTS_AS_URI node. + options.resultType = options.RESULTS_AS_URI; + + root = history.executeQuery(query, options).root; + root.containerOpen = true; + + equal(root.childCount, 1); + + child = root.getChild(0); + equal(child.visitType, 0, "Visit type should be 0"); + equal(child.visitId, -1, "Visit ID should be -1"); + equal(child.fromVisitId, -1, "Referrer visit id should be -1"); + + root.containerOpen = false; + + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* test_bookmarkFields() { + let folder = bookmarks.createFolder(bookmarks.placesRoot, "test folder", bookmarks.DEFAULT_INDEX); + bookmarks.insertBookmark(folder, uri("http://test4.com/"), + bookmarks.DEFAULT_INDEX, "test4 title"); + + let root = PlacesUtils.getFolderContents(folder).root; + equal(root.childCount, 1); + + equal(root.visitType, 0, "Visit type should be 0"); + equal(root.visitId, -1, "Visit ID should be -1"); + equal(root.fromVisitId, -1, "Referrer visit id should be -1"); + + let child = root.getChild(0); + equal(child.visitType, 0, "Visit type should be 0"); + equal(child.visitId, -1, "Visit ID should be -1"); + equal(child.fromVisitId, -1, "Referrer visit id should be -1"); + + root.containerOpen = false; + + yield bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_sql_guid_functions.js b/toolkit/components/places/tests/unit/test_sql_guid_functions.js new file mode 100644 index 000000000..41e6bab9e --- /dev/null +++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests that the guid function generates a guid of the proper length, + * with no invalid characters. + */ + +/** + * Checks all our invariants about our guids for a given result. + * + * @param aGuid + * The guid to check. + */ +function check_invariants(aGuid) +{ + do_print("Checking guid '" + aGuid + "'"); + + do_check_valid_places_guid(aGuid); +} + +// Test Functions + +function test_guid_invariants() +{ + const kExpectedChars = 64; + const kAllowedChars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + do_check_eq(kAllowedChars.length, kExpectedChars); + const kGuidLength = 12; + + let checkedChars = []; + for (let i = 0; i < kGuidLength; i++) { + checkedChars[i] = {}; + for (let j = 0; j < kAllowedChars; j++) { + checkedChars[i][kAllowedChars[j]] = false; + } + } + + // We run this until we've seen every character that we expect to see in every + // position. + let seenChars = 0; + let stmt = DBConn().createStatement("SELECT GENERATE_GUID()"); + while (seenChars != (kExpectedChars * kGuidLength)) { + do_check_true(stmt.executeStep()); + let guid = stmt.getString(0); + check_invariants(guid); + + for (let i = 0; i < guid.length; i++) { + let character = guid[i]; + if (!checkedChars[i][character]) { + checkedChars[i][character] = true; + seenChars++; + } + } + stmt.reset(); + } + stmt.finalize(); + + // One last reality check - make sure all of our characters were seen. + for (let i = 0; i < kGuidLength; i++) { + for (let j = 0; j < kAllowedChars; j++) { + do_check_true(checkedChars[i][kAllowedChars[j]]); + } + } + + run_next_test(); +} + +function test_guid_on_background() +{ + // We should not assert if we execute this asynchronously. + let stmt = DBConn().createAsyncStatement("SELECT GENERATE_GUID()"); + let checked = false; + stmt.executeAsync({ + handleResult: function(aResult) { + try { + let row = aResult.getNextRow(); + check_invariants(row.getResultByIndex(0)); + do_check_eq(aResult.getNextRow(), null); + checked = true; + } + catch (e) { + do_throw(e); + } + }, + handleCompletion: function(aReason) { + do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED); + do_check_true(checked); + run_next_test(); + } + }); + stmt.finalize(); +} + +// Test Runner + +[ + test_guid_invariants, + test_guid_on_background, +].forEach(add_test); + +function run_test() +{ + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_svg_favicon.js b/toolkit/components/places/tests/unit/test_svg_favicon.js new file mode 100644 index 000000000..cec40ddef --- /dev/null +++ b/toolkit/components/places/tests/unit/test_svg_favicon.js @@ -0,0 +1,31 @@ +const PAGEURI = NetUtil.newURI("http://deliciousbacon.com/"); + +add_task(function* () { + // First, add a history entry or else Places can't save a favicon. + yield PlacesTestUtils.addVisits({ + uri: PAGEURI, + transition: TRANSITION_LINK, + visitDate: Date.now() * 1000 + }); + + yield new Promise(resolve => { + function onSetComplete(aURI, aDataLen, aData, aMimeType) { + equal(aURI.spec, SMALLSVG_DATA_URI.spec, "setFavicon aURI check"); + equal(aDataLen, 263, "setFavicon aDataLen check"); + equal(aMimeType, "image/svg+xml", "setFavicon aMimeType check"); + resolve(); + } + + PlacesUtils.favicons.setAndFetchFaviconForPage(PAGEURI, SMALLSVG_DATA_URI, + false, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + onSetComplete, + Services.scriptSecurityManager.getSystemPrincipal()); + }); + + let data = yield PlacesUtils.promiseFaviconData(PAGEURI.spec); + equal(data.uri.spec, SMALLSVG_DATA_URI.spec, "getFavicon aURI check"); + equal(data.dataLen, 263, "getFavicon aDataLen check"); + equal(data.mimeType, "image/svg+xml", "getFavicon aMimeType check"); +}); + diff --git a/toolkit/components/places/tests/unit/test_sync_utils.js b/toolkit/components/places/tests/unit/test_sync_utils.js new file mode 100644 index 000000000..f8c7e6b58 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_sync_utils.js @@ -0,0 +1,1150 @@ +Cu.import("resource://gre/modules/PlacesSyncUtils.jsm"); +Cu.import("resource://testing-common/httpd.js"); +Cu.importGlobalProperties(["crypto", "URLSearchParams"]); + +const DESCRIPTION_ANNO = "bookmarkProperties/description"; +const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; +const SYNC_PARENT_ANNO = "sync/parent"; + +function makeGuid() { + return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), { + pad: false, + }); +} + +function makeLivemarkServer() { + let server = new HttpServer(); + server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml")); + server.start(-1); + return { + server, + get site() { + let { identity } = server; + let host = identity.primaryHost.includes(":") ? + `[${identity.primaryHost}]` : identity.primaryHost; + return `${identity.primaryScheme}://${host}:${identity.primaryPort}`; + }, + stopServer() { + return new Promise(resolve => server.stop(resolve)); + }, + }; +} + +function shuffle(array) { + let results = []; + for (let i = 0; i < array.length; ++i) { + let randomIndex = Math.floor(Math.random() * (i + 1)); + results[i] = results[randomIndex]; + results[randomIndex] = array[i]; + } + return results; +} + +function compareAscending(a, b) { + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; +} + +function assertTagForURLs(tag, urls, message) { + let taggedURLs = PlacesUtils.tagging.getURIsForTag(tag).map(uri => uri.spec); + deepEqual(taggedURLs.sort(compareAscending), urls.sort(compareAscending), message); +} + +function assertURLHasTags(url, tags, message) { + let actualTags = PlacesUtils.tagging.getTagsForURI(uri(url)); + deepEqual(actualTags.sort(compareAscending), tags, message); +} + +var populateTree = Task.async(function* populate(parentGuid, ...items) { + let guids = {}; + + for (let index = 0; index < items.length; index++) { + let item = items[index]; + let guid = makeGuid(); + + switch (item.kind) { + case "bookmark": + case "query": + yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: item.url, + title: item.title, + parentGuid, guid, index, + }); + break; + + case "separator": + yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid, guid, + }); + break; + + case "folder": + yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: item.title, + parentGuid, guid, + }); + if (item.children) { + Object.assign(guids, yield* populate(guid, ...item.children)); + } + break; + + default: + throw new Error(`Unsupported item type: ${item.type}`); + } + + if (item.exclude) { + let itemId = yield PlacesUtils.promiseItemId(guid); + PlacesUtils.annotations.setItemAnnotation( + itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, "Don't back this up", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + } + + guids[item.title] = guid; + } + + return guids; +}); + +var syncIdToId = Task.async(function* syncIdToId(syncId) { + let guid = yield PlacesSyncUtils.bookmarks.syncIdToGuid(syncId); + return PlacesUtils.promiseItemId(guid); +}); + +add_task(function* test_order() { + do_print("Insert some bookmarks"); + let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, { + kind: "bookmark", + title: "childBmk", + url: "http://getfirefox.com", + }, { + kind: "bookmark", + title: "siblingBmk", + url: "http://getthunderbird.com", + }, { + kind: "folder", + title: "siblingFolder", + }, { + kind: "separator", + title: "siblingSep", + }); + + do_print("Reorder inserted bookmarks"); + { + let order = [guids.siblingFolder, guids.siblingSep, guids.childBmk, + guids.siblingBmk]; + yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, order); + let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(PlacesUtils.bookmarks.menuGuid); + deepEqual(childSyncIds, order, "New bookmarks should be reordered according to array"); + } + + do_print("Reorder with unspecified children"); + { + yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [ + guids.siblingSep, guids.siblingBmk, + ]); + let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds( + PlacesUtils.bookmarks.menuGuid); + deepEqual(childSyncIds, [guids.siblingSep, guids.siblingBmk, + guids.siblingFolder, guids.childBmk], + "Unordered children should be moved to end"); + } + + do_print("Reorder with nonexistent children"); + { + yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [ + guids.childBmk, makeGuid(), guids.siblingBmk, guids.siblingSep, + makeGuid(), guids.siblingFolder, makeGuid()]); + let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds( + PlacesUtils.bookmarks.menuGuid); + deepEqual(childSyncIds, [guids.childBmk, guids.siblingBmk, guids.siblingSep, + guids.siblingFolder], "Nonexistent children should be ignored"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_changeGuid_invalid() { + yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid()), + "Should require a new GUID"); + yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), "!@#$"), + "Should reject invalid GUIDs"); + yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), makeGuid()), + "Should reject nonexistent item GUIDs"); + yield rejects( + PlacesSyncUtils.bookmarks.changeGuid(PlacesUtils.bookmarks.menuGuid, + makeGuid()), + "Should reject roots"); +}); + +add_task(function* test_changeGuid() { + let item = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "https://mozilla.org", + }); + let id = yield PlacesUtils.promiseItemId(item.guid); + + let newGuid = makeGuid(); + let result = yield PlacesSyncUtils.bookmarks.changeGuid(item.guid, newGuid); + equal(result, newGuid, "Should return new GUID"); + + equal(yield PlacesUtils.promiseItemId(newGuid), id, "Should map ID to new GUID"); + yield rejects(PlacesUtils.promiseItemId(item.guid), "Should not map ID to old GUID"); + equal(yield PlacesUtils.promiseItemGuid(id), newGuid, "Should map new GUID to ID"); +}); + +add_task(function* test_order_roots() { + let oldOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds( + PlacesUtils.bookmarks.rootGuid); + yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.rootGuid, + shuffle(oldOrder)); + let newOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds( + PlacesUtils.bookmarks.rootGuid); + deepEqual(oldOrder, newOrder, "Should ignore attempts to reorder roots"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_update_tags() { + do_print("Insert item without tags"); + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://mozilla.org", + syncId: makeGuid(), + parentSyncId: "menu", + }); + + do_print("Add tags"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + tags: ["foo", "bar"], + }); + deepEqual(updatedItem.tags, ["foo", "bar"], "Should return new tags"); + assertURLHasTags("https://mozilla.org", ["bar", "foo"], + "Should set new tags for URL"); + } + + do_print("Add new tag, remove existing tag"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + tags: ["foo", "baz"], + }); + deepEqual(updatedItem.tags, ["foo", "baz"], "Should return updated tags"); + assertURLHasTags("https://mozilla.org", ["baz", "foo"], + "Should update tags for URL"); + assertTagForURLs("bar", [], "Should remove existing tag"); + } + + do_print("Tags with whitespace"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + tags: [" leading", "trailing ", " baz ", " "], + }); + deepEqual(updatedItem.tags, ["leading", "trailing", "baz"], + "Should return filtered tags"); + assertURLHasTags("https://mozilla.org", ["baz", "leading", "trailing"], + "Should trim whitespace and filter blank tags"); + } + + do_print("Remove all tags"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + tags: null, + }); + deepEqual(updatedItem.tags, [], "Should return empty tag array"); + assertURLHasTags("https://mozilla.org", [], + "Should remove all existing tags"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_update_keyword() { + do_print("Insert item without keyword"); + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + parentSyncId: "menu", + url: "https://mozilla.org", + syncId: makeGuid(), + }); + + do_print("Add item keyword"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + keyword: "moz", + }); + equal(updatedItem.keyword, "moz", "Should return new keyword"); + let entryByKeyword = yield PlacesUtils.keywords.fetch("moz"); + equal(entryByKeyword.url.href, "https://mozilla.org/", + "Should set new keyword for URL"); + let entryByURL = yield PlacesUtils.keywords.fetch({ + url: "https://mozilla.org", + }); + equal(entryByURL.keyword, "moz", "Looking up URL should return new keyword"); + } + + do_print("Change item keyword"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + keyword: "m", + }); + equal(updatedItem.keyword, "m", "Should return updated keyword"); + let newEntry = yield PlacesUtils.keywords.fetch("m"); + equal(newEntry.url.href, "https://mozilla.org/", "Should update keyword for URL"); + let oldEntry = yield PlacesUtils.keywords.fetch("moz"); + ok(!oldEntry, "Should remove old keyword"); + } + + do_print("Remove existing keyword"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + keyword: null, + }); + ok(!updatedItem.keyword, + "Should not include removed keyword in properties"); + let entry = yield PlacesUtils.keywords.fetch({ + url: "https://mozilla.org", + }); + ok(!entry, "Should remove new keyword from URL"); + } + + do_print("Remove keyword for item without keyword"); + { + yield PlacesSyncUtils.bookmarks.update({ + syncId: item.syncId, + keyword: null, + }); + let entry = yield PlacesUtils.keywords.fetch({ + url: "https://mozilla.org", + }); + ok(!entry, + "Removing keyword for URL without existing keyword should succeed"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_update_annos() { + let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, { + kind: "folder", + title: "folder", + }, { + kind: "bookmark", + title: "bmk", + url: "https://example.com", + }); + + do_print("Add folder description"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: guids.folder, + description: "Folder description", + }); + equal(updatedItem.description, "Folder description", + "Should return new description"); + let id = yield syncIdToId(updatedItem.syncId); + equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO), + "Folder description", "Should set description anno"); + } + + do_print("Clear folder description"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: guids.folder, + description: null, + }); + ok(!updatedItem.description, "Should not return cleared description"); + let id = yield syncIdToId(updatedItem.syncId); + ok(!PlacesUtils.annotations.itemHasAnnotation(id, DESCRIPTION_ANNO), + "Should remove description anno"); + } + + do_print("Add bookmark sidebar anno"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: guids.bmk, + loadInSidebar: true, + }); + ok(updatedItem.loadInSidebar, "Should return sidebar anno"); + let id = yield syncIdToId(updatedItem.syncId); + ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO), + "Should set sidebar anno for existing bookmark"); + } + + do_print("Clear bookmark sidebar anno"); + { + let updatedItem = yield PlacesSyncUtils.bookmarks.update({ + syncId: guids.bmk, + loadInSidebar: false, + }); + ok(!updatedItem.loadInSidebar, "Should not return cleared sidebar anno"); + let id = yield syncIdToId(updatedItem.syncId); + ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO), + "Should clear sidebar anno for existing bookmark"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_update_move_root() { + do_print("Move root to same parent"); + { + // This should be a no-op. + let sameRoot = yield PlacesSyncUtils.bookmarks.update({ + syncId: "menu", + parentSyncId: "places", + }); + equal(sameRoot.syncId, "menu", + "Menu root GUID should not change"); + equal(sameRoot.parentSyncId, "places", + "Parent Places root GUID should not change"); + } + + do_print("Try reparenting root"); + yield rejects(PlacesSyncUtils.bookmarks.update({ + syncId: "menu", + parentSyncId: "toolbar", + })); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert() { + do_print("Insert bookmark"); + { + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + syncId: makeGuid(), + parentSyncId: "menu", + url: "https://example.org", + }); + let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId }); + equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK, + "Bookmark should have correct type"); + } + + do_print("Insert query"); + { + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "menu", + url: "place:terms=term&folder=TOOLBAR&queryType=1", + folder: "Saved search", + }); + let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId }); + equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK, + "Queries should be stored as bookmarks"); + } + + do_print("Insert folder"); + { + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "folder", + syncId: makeGuid(), + parentSyncId: "menu", + title: "New folder", + }); + let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId }); + equal(type, PlacesUtils.bookmarks.TYPE_FOLDER, + "Folder should have correct type"); + } + + do_print("Insert separator"); + { + let item = yield PlacesSyncUtils.bookmarks.insert({ + kind: "separator", + syncId: makeGuid(), + parentSyncId: "menu", + }); + let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId }); + equal(type, PlacesUtils.bookmarks.TYPE_SEPARATOR, + "Separator should have correct type"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_livemark() { + let { site, stopServer } = makeLivemarkServer(); + + try { + do_print("Insert livemark with feed URL"); + { + let livemark = yield PlacesSyncUtils.bookmarks.insert({ + kind: "livemark", + syncId: makeGuid(), + feed: site + "/feed/1", + parentSyncId: "menu", + }); + let bmk = yield PlacesUtils.bookmarks.fetch({ + guid: yield PlacesSyncUtils.bookmarks.syncIdToGuid(livemark.syncId), + }) + equal(bmk.type, PlacesUtils.bookmarks.TYPE_FOLDER, + "Livemarks should be stored as folders"); + } + + let livemarkSyncId; + do_print("Insert livemark with site and feed URLs"); + { + let livemark = yield PlacesSyncUtils.bookmarks.insert({ + kind: "livemark", + syncId: makeGuid(), + site, + feed: site + "/feed/1", + parentSyncId: "menu", + }); + livemarkSyncId = livemark.syncId; + } + + do_print("Try inserting livemark into livemark"); + { + let livemark = yield PlacesSyncUtils.bookmarks.insert({ + kind: "livemark", + syncId: makeGuid(), + site, + feed: site + "/feed/1", + parentSyncId: livemarkSyncId, + }); + ok(!livemark, "Should not insert livemark as child of livemark"); + } + } finally { + yield stopServer(); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_update_livemark() { + let { site, stopServer } = makeLivemarkServer(); + let feedURI = uri(site + "/feed/1"); + + try { + // We shouldn't reinsert the livemark if the URLs are the same. + do_print("Update livemark with same URLs"); + { + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI, + siteURI: uri(site), + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + + yield PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + feed: feedURI, + }); + // `nsLivemarkService` returns references to `Livemark` instances, so we + // can compare them with `==` to make sure they haven't been replaced. + equal(yield PlacesUtils.livemarks.getLivemark({ + guid: livemark.guid, + }), livemark, "Livemark with same feed URL should not be replaced"); + + yield PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + site, + }); + equal(yield PlacesUtils.livemarks.getLivemark({ + guid: livemark.guid, + }), livemark, "Livemark with same site URL should not be replaced"); + + yield PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + feed: feedURI, + site, + }); + equal(yield PlacesUtils.livemarks.getLivemark({ + guid: livemark.guid, + }), livemark, "Livemark with same feed and site URLs should not be replaced"); + } + + do_print("Change livemark feed URL"); + { + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + + // Since we're reinserting, we need to pass all properties required + // for a new livemark. `update` won't merge the old and new ones. + yield rejects(PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + feed: site + "/feed/2", + }), "Reinserting livemark with changed feed URL requires full record"); + + let newLivemark = yield PlacesSyncUtils.bookmarks.update({ + kind: "livemark", + parentSyncId: "menu", + syncId: livemark.guid, + feed: site + "/feed/2", + }); + equal(newLivemark.syncId, livemark.guid, + "GUIDs should match for reinserted livemark with changed feed URL"); + equal(newLivemark.feed.href, site + "/feed/2", + "Reinserted livemark should have changed feed URI"); + } + + do_print("Add livemark site URL"); + { + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI, + }); + ok(livemark.feedURI.equals(feedURI), "Livemark feed URI should match"); + ok(!livemark.siteURI, "Livemark should not have site URI"); + + yield rejects(PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + site, + }), "Reinserting livemark with new site URL requires full record"); + + let newLivemark = yield PlacesSyncUtils.bookmarks.update({ + kind: "livemark", + parentSyncId: "menu", + syncId: livemark.guid, + feed: feedURI, + site, + }); + notEqual(newLivemark, livemark, + "Livemark with new site URL should replace old livemark"); + equal(newLivemark.syncId, livemark.guid, + "GUIDs should match for reinserted livemark with new site URL"); + equal(newLivemark.site.href, site + "/", + "Reinserted livemark should have new site URI"); + equal(newLivemark.feed.href, feedURI.spec, + "Reinserted livemark with new site URL should have same feed URI"); + } + + do_print("Remove livemark site URL"); + { + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI, + siteURI: uri(site), + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + + yield rejects(PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + site: null, + }), "Reinserting livemark witout site URL requires full record"); + + let newLivemark = yield PlacesSyncUtils.bookmarks.update({ + kind: "livemark", + parentSyncId: "menu", + syncId: livemark.guid, + feed: feedURI, + site: null, + }); + notEqual(newLivemark, livemark, + "Livemark without site URL should replace old livemark"); + equal(newLivemark.syncId, livemark.guid, + "GUIDs should match for reinserted livemark without site URL"); + ok(!newLivemark.site, "Reinserted livemark should not have site URI"); + } + + do_print("Change livemark site URL"); + { + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI, + siteURI: uri(site), + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + + yield rejects(PlacesSyncUtils.bookmarks.update({ + syncId: livemark.guid, + site: site + "/new", + }), "Reinserting livemark with changed site URL requires full record"); + + let newLivemark = yield PlacesSyncUtils.bookmarks.update({ + kind: "livemark", + parentSyncId: "menu", + syncId: livemark.guid, + feed:feedURI, + site: site + "/new", + }); + notEqual(newLivemark, livemark, + "Livemark with changed site URL should replace old livemark"); + equal(newLivemark.syncId, livemark.guid, + "GUIDs should match for reinserted livemark with changed site URL"); + equal(newLivemark.site.href, site + "/new", + "Reinserted livemark should have changed site URI"); + } + + // Livemarks are stored as folders, but have different kinds. We should + // remove the folder and insert a livemark with the same GUID instead of + // trying to update the folder in-place. + do_print("Replace folder with livemark"); + { + let folder = yield PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Plain folder", + }); + let livemark = yield PlacesSyncUtils.bookmarks.update({ + kind: "livemark", + parentSyncId: "menu", + syncId: folder.guid, + feed: feedURI, + }); + equal(livemark.guid, folder.syncId, + "Livemark should have same GUID as replaced folder"); + } + } finally { + yield stopServer(); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_tags() { + yield Promise.all([{ + kind: "bookmark", + url: "https://example.com", + syncId: makeGuid(), + parentSyncId: "menu", + tags: ["foo", "bar"], + }, { + kind: "bookmark", + url: "https://example.org", + syncId: makeGuid(), + parentSyncId: "toolbar", + tags: ["foo", "baz"], + }, { + kind: "query", + url: "place:queryType=1&sort=12&maxResults=10", + syncId: makeGuid(), + parentSyncId: "toolbar", + folder: "bar", + tags: ["baz", "qux"], + title: "bar", + }].map(info => PlacesSyncUtils.bookmarks.insert(info))); + + assertTagForURLs("foo", ["https://example.com/", "https://example.org/"], + "2 URLs with new tag"); + assertTagForURLs("bar", ["https://example.com/"], "1 URL with existing tag"); + assertTagForURLs("baz", ["https://example.org/", + "place:queryType=1&sort=12&maxResults=10"], + "Should support tagging URLs and tag queries"); + assertTagForURLs("qux", ["place:queryType=1&sort=12&maxResults=10"], + "Should support tagging tag queries"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_tags_whitespace() { + do_print("Untrimmed and blank tags"); + let taggedBlanks = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://example.org", + syncId: makeGuid(), + parentSyncId: "menu", + tags: [" untrimmed ", " ", "taggy"], + }); + deepEqual(taggedBlanks.tags, ["untrimmed", "taggy"], + "Should not return empty tags"); + assertURLHasTags("https://example.org/", ["taggy", "untrimmed"], + "Should set trimmed tags and ignore dupes"); + + do_print("Dupe tags"); + let taggedDupes = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://example.net", + syncId: makeGuid(), + parentSyncId: "toolbar", + tags: [" taggy", "taggy ", " taggy ", "taggy"], + }); + deepEqual(taggedDupes.tags, ["taggy", "taggy", "taggy", "taggy"], + "Should return trimmed and dupe tags"); + assertURLHasTags("https://example.net/", ["taggy"], + "Should ignore dupes when setting tags"); + + assertTagForURLs("taggy", ["https://example.net/", "https://example.org/"], + "Should exclude falsy tags"); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_keyword() { + do_print("Insert item with new keyword"); + { + yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + parentSyncId: "menu", + url: "https://example.com", + keyword: "moz", + syncId: makeGuid(), + }); + let entry = yield PlacesUtils.keywords.fetch("moz"); + equal(entry.url.href, "https://example.com/", + "Should add keyword for item"); + } + + do_print("Insert item with existing keyword"); + { + yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + parentSyncId: "menu", + url: "https://mozilla.org", + keyword: "moz", + syncId: makeGuid(), + }); + let entry = yield PlacesUtils.keywords.fetch("moz"); + equal(entry.url.href, "https://mozilla.org/", + "Should reassign keyword to new item"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_annos() { + do_print("Bookmark with description"); + let descBmk = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://example.com", + syncId: makeGuid(), + parentSyncId: "menu", + description: "Bookmark description", + }); + { + equal(descBmk.description, "Bookmark description", + "Should return new bookmark description"); + let id = yield syncIdToId(descBmk.syncId); + equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO), + "Bookmark description", "Should set new bookmark description"); + } + + do_print("Folder with description"); + let descFolder = yield PlacesSyncUtils.bookmarks.insert({ + kind: "folder", + syncId: makeGuid(), + parentSyncId: "menu", + description: "Folder description", + }); + { + equal(descFolder.description, "Folder description", + "Should return new folder description"); + let id = yield syncIdToId(descFolder.syncId); + equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO), + "Folder description", "Should set new folder description"); + } + + do_print("Bookmark with sidebar anno"); + let sidebarBmk = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://example.com", + syncId: makeGuid(), + parentSyncId: "menu", + loadInSidebar: true, + }); + { + ok(sidebarBmk.loadInSidebar, "Should return sidebar anno for new bookmark"); + let id = yield syncIdToId(sidebarBmk.syncId); + ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO), + "Should set sidebar anno for new bookmark"); + } + + do_print("Bookmark without sidebar anno"); + let noSidebarBmk = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + url: "https://example.org", + syncId: makeGuid(), + parentSyncId: "toolbar", + loadInSidebar: false, + }); + { + ok(!noSidebarBmk.loadInSidebar, + "Should not return sidebar anno for new bookmark"); + let id = yield syncIdToId(noSidebarBmk.syncId); + ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO), + "Should not set sidebar anno for new bookmark"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_tag_query() { + let tagFolder = -1; + + do_print("Insert tag query for new tag"); + { + deepEqual(PlacesUtils.tagging.allTags, [], "New tag should not exist yet"); + let query = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "toolbar", + url: "place:type=7&folder=90", + folder: "taggy", + title: "Tagged stuff", + }); + notEqual(query.url.href, "place:type=7&folder=90", + "Tag query URL for new tag should differ"); + + [, tagFolder] = /\bfolder=(\d+)\b/.exec(query.url.pathname); + ok(tagFolder > 0, "New tag query URL should contain valid folder"); + deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "New tag should exist"); + } + + do_print("Insert tag query for existing tag"); + { + let url = "place:type=7&folder=90&maxResults=15"; + let query = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + url, + folder: "taggy", + title: "Sorted and tagged", + syncId: makeGuid(), + parentSyncId: "menu", + }); + notEqual(query.url.href, url, "Tag query URL for existing tag should differ"); + let params = new URLSearchParams(query.url.pathname); + equal(params.get("type"), "7", "Should preserve query type"); + equal(params.get("maxResults"), "15", "Should preserve additional params"); + equal(params.get("folder"), tagFolder, "Should update tag folder"); + deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "Should not duplicate existing tags"); + } + + do_print("Use the public tagging API to ensure we added the tag correctly"); + { + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "https://mozilla.org", + title: "Mozilla", + }); + PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["taggy"]); + assertURLHasTags("https://mozilla.org/", ["taggy"], + "Should set tags using the tagging API"); + } + + do_print("Removing the tag should clean up the tag folder"); + { + PlacesUtils.tagging.untagURI(uri("https://mozilla.org"), null); + deepEqual(PlacesUtils.tagging.allTags, [], + "Should remove tag folder once last item is untagged"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_insert_orphans() { + let grandParentGuid = makeGuid(); + let parentGuid = makeGuid(); + let childGuid = makeGuid(); + let childId; + + do_print("Insert an orphaned child"); + { + let child = yield PlacesSyncUtils.bookmarks.insert({ + kind: "bookmark", + parentSyncId: parentGuid, + syncId: childGuid, + url: "https://mozilla.org", + }); + equal(child.syncId, childGuid, + "Should insert orphan with requested GUID"); + equal(child.parentSyncId, "unfiled", + "Should reparent orphan to unfiled"); + + childId = yield PlacesUtils.promiseItemId(childGuid); + equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO), + parentGuid, "Should set anno to missing parent GUID"); + } + + do_print("Insert the grandparent"); + { + yield PlacesSyncUtils.bookmarks.insert({ + kind: "folder", + parentSyncId: "menu", + syncId: grandParentGuid, + }); + equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO), + parentGuid, "Child should still have orphan anno"); + } + + // Note that only `PlacesSyncUtils` reparents orphans, though Sync adds an + // observer that removes the orphan anno if the orphan is manually moved. + do_print("Insert the missing parent"); + { + let parent = yield PlacesSyncUtils.bookmarks.insert({ + kind: "folder", + parentSyncId: grandParentGuid, + syncId: parentGuid, + }); + equal(parent.syncId, parentGuid, "Should insert parent with requested GUID"); + equal(parent.parentSyncId, grandParentGuid, + "Parent should be child of grandparent"); + ok(!PlacesUtils.annotations.itemHasAnnotation(childId, SYNC_PARENT_ANNO), + "Orphan anno should be removed after reparenting"); + + let child = yield PlacesUtils.bookmarks.fetch({ guid: childGuid }); + equal(child.parentGuid, parentGuid, + "Should reparent child after inserting missing parent"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_fetch() { + let folder = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: "menu", + kind: "folder", + description: "Folder description", + }); + let bmk = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: "menu", + kind: "bookmark", + url: "https://example.com", + description: "Bookmark description", + loadInSidebar: true, + tags: ["taggy"], + }); + let folderBmk = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: folder.syncId, + kind: "bookmark", + url: "https://example.org", + keyword: "kw", + }); + let folderSep = yield PlacesSyncUtils.bookmarks.insert({ + syncId: makeGuid(), + parentSyncId: folder.syncId, + kind: "separator", + }); + let tagQuery = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "toolbar", + url: "place:type=7&folder=90", + folder: "taggy", + title: "Tagged stuff", + }); + let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname); + let smartBmk = yield PlacesSyncUtils.bookmarks.insert({ + kind: "query", + syncId: makeGuid(), + parentSyncId: "toolbar", + url: "place:folder=TOOLBAR", + query: "BookmarksToolbar", + title: "Bookmarks toolbar query", + }); + + do_print("Fetch empty folder with description"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folder.syncId); + deepEqual(item, { + syncId: folder.syncId, + kind: "folder", + parentSyncId: "menu", + description: "Folder description", + childSyncIds: [folderBmk.syncId, folderSep.syncId], + parentTitle: "Bookmarks Menu", + title: "", + }, "Should include description, children, title, and parent title in folder"); + } + + do_print("Fetch bookmark with description, sidebar anno, and tags"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(bmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "tags", "description", "loadInSidebar", "parentTitle", "title"].sort(), + "Should include bookmark-specific properties"); + equal(item.syncId, bmk.syncId, "Sync ID should match"); + equal(item.url.href, "https://example.com/", "Should return URL"); + equal(item.parentSyncId, "menu", "Should return parent sync ID"); + deepEqual(item.tags, ["taggy"], "Should return tags"); + equal(item.description, "Bookmark description", "Should return bookmark description"); + strictEqual(item.loadInSidebar, true, "Should return sidebar anno"); + equal(item.parentTitle, "Bookmarks Menu", "Should return parent title"); + strictEqual(item.title, "", "Should return empty title"); + } + + do_print("Fetch bookmark with keyword; without parent title or annos"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folderBmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "keyword", "tags", "loadInSidebar", "parentTitle", "title"].sort(), + "Should omit blank bookmark-specific properties"); + strictEqual(item.loadInSidebar, false, "Should not load bookmark in sidebar"); + deepEqual(item.tags, [], "Tags should be empty"); + equal(item.keyword, "kw", "Should return keyword"); + strictEqual(item.parentTitle, "", "Should include parent title even if empty"); + strictEqual(item.title, "", "Should include bookmark title even if empty"); + } + + do_print("Fetch separator"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(folderSep.syncId); + strictEqual(item.index, 1, "Should return separator position"); + } + + do_print("Fetch tag query"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(tagQuery.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "title", "folder", "parentTitle"].sort(), + "Should include query-specific properties"); + equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries"); + equal(item.folder, "taggy", "Should return tag name for tag queries"); + } + + do_print("Fetch smart bookmark"); + { + let item = yield PlacesSyncUtils.bookmarks.fetch(smartBmk.syncId); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "url", "title", "query", "parentTitle"].sort(), + "Should include smart bookmark-specific properties"); + equal(item.query, "BookmarksToolbar", "Should return query name for smart bookmarks"); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_fetch_livemark() { + let { site, stopServer } = makeLivemarkServer(); + + try { + do_print("Create livemark"); + let livemark = yield PlacesUtils.livemarks.addLivemark({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + feedURI: uri(site + "/feed/1"), + siteURI: uri(site), + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + PlacesUtils.annotations.setItemAnnotation(livemark.id, DESCRIPTION_ANNO, + "Livemark description", 0, PlacesUtils.annotations.EXPIRE_NEVER); + + do_print("Fetch livemark"); + let item = yield PlacesSyncUtils.bookmarks.fetch(livemark.guid); + deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId", + "description", "feed", "site", "parentTitle", "title"].sort(), + "Should include livemark-specific properties"); + equal(item.description, "Livemark description", "Should return description"); + equal(item.feed.href, site + "/feed/1", "Should return feed URL"); + equal(item.site.href, site + "/", "Should return site URL"); + strictEqual(item.title, "", "Should include livemark title even if empty"); + } finally { + yield stopServer(); + } + + yield PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js new file mode 100644 index 000000000..92930e329 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var current_test = 0; + +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + constructor: AutoCompleteInput, + + searches: null, + + minResultsForPopup: 0, + timeout: 10, + searchParam: "", + textValue: "", + disableAutoComplete: false, + completeDefaultIndex: false, + + get searchCount() { + return this.searches.length; + }, + + getSearchAt: function(aIndex) { + return this.searches[aIndex]; + }, + + onSearchBegin: function() {}, + onSearchComplete: function() {}, + + popupOpen: false, + + popup: { + setSelectedIndex: function(aIndex) {}, + invalidate: function() {}, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompletePopup)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } + }, + + // nsISupports implementation + QueryInterface: function(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIAutoCompleteInput)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +// Get tagging service +try { + var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +} catch (ex) { + do_throw("Could not get tagging service\n"); +} + +function ensure_tag_results(results, searchTerm) +{ + var controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + + // Make an AutoCompleteInput that uses our searches + // and confirms results on search complete + var input = new AutoCompleteInput(["places-tag-autocomplete"]); + + controller.input = input; + + var numSearchesStarted = 0; + input.onSearchBegin = function input_onSearchBegin() { + numSearchesStarted++; + do_check_eq(numSearchesStarted, 1); + }; + + input.onSearchComplete = function input_onSearchComplete() { + do_check_eq(numSearchesStarted, 1); + if (results.length) + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH); + else + do_check_eq(controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH); + + do_check_eq(controller.matchCount, results.length); + for (var i=0; i<controller.matchCount; i++) { + do_check_eq(controller.getValueAt(i), results[i]); + } + + if (current_test < (tests.length - 1)) { + current_test++; + tests[current_test](); + } + else { + // finish once all tests have run + do_test_finished(); + } + }; + + controller.startSearch(searchTerm); +} + +var uri1 = uri("http://site.tld/1"); + +var tests = [ + function test1() { ensure_tag_results(["bar", "Baz", "boo"], "b"); }, + function test2() { ensure_tag_results(["bar", "Baz"], "ba"); }, + function test3() { ensure_tag_results(["bar", "Baz"], "Ba"); }, + function test4() { ensure_tag_results(["bar"], "bar"); }, + function test5() { ensure_tag_results(["Baz"], "Baz"); }, + function test6() { ensure_tag_results([], "barb"); }, + function test7() { ensure_tag_results([], "foo"); }, + function test8() { ensure_tag_results(["first tag, bar", "first tag, Baz"], "first tag, ba"); }, + function test9() { ensure_tag_results(["first tag; bar", "first tag; Baz"], "first tag; ba"); } +]; + +/** + * Test tag autocomplete + */ +function run_test() { + // Search is asynchronous, so don't let the test finish immediately + do_test_pending(); + + tagssvc.tagURI(uri1, ["bar", "Baz", "boo", "*nix"]); + + tests[0](); +} diff --git a/toolkit/components/places/tests/unit/test_tagging.js b/toolkit/components/places/tests/unit/test_tagging.js new file mode 100644 index 000000000..ccb287050 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_tagging.js @@ -0,0 +1,189 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Notice we use createInstance because later we will have to terminate the +// service and restart it. +var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + createInstance().QueryInterface(Ci.nsITaggingService); + +function run_test() { + var options = PlacesUtils.history.getNewQueryOptions(); + var query = PlacesUtils.history.getNewQuery(); + + query.setFolders([PlacesUtils.tagsFolderId], 1); + var result = PlacesUtils.history.executeQuery(query, options); + var tagRoot = result.root; + tagRoot.containerOpen = true; + + do_check_eq(tagRoot.childCount, 0); + + var uri1 = uri("http://foo.tld/"); + var uri2 = uri("https://bar.tld/"); + + // this also tests that the multiple folders are not created for the same tag + tagssvc.tagURI(uri1, ["tag 1"]); + tagssvc.tagURI(uri2, ["tag 1"]); + do_check_eq(tagRoot.childCount, 1); + + var tag1node = tagRoot.getChild(0) + .QueryInterface(Ci.nsINavHistoryContainerResultNode); + var tag1itemId = tag1node.itemId; + + do_check_eq(tag1node.title, "tag 1"); + tag1node.containerOpen = true; + do_check_eq(tag1node.childCount, 2); + + // Tagging the same url twice (or even thrice!) with the same tag should be a + // no-op + tagssvc.tagURI(uri1, ["tag 1"]); + do_check_eq(tag1node.childCount, 2); + tagssvc.tagURI(uri1, [tag1itemId]); + do_check_eq(tag1node.childCount, 2); + do_check_eq(tagRoot.childCount, 1); + + // also tests bug 407575 + tagssvc.tagURI(uri1, [tag1itemId, "tag 1", "tag 2", "Tag 1", "Tag 2"]); + do_check_eq(tagRoot.childCount, 2); + do_check_eq(tag1node.childCount, 2); + + // test getTagsForURI + var uri1tags = tagssvc.getTagsForURI(uri1); + do_check_eq(uri1tags.length, 2); + do_check_eq(uri1tags[0], "Tag 1"); + do_check_eq(uri1tags[1], "Tag 2"); + var uri2tags = tagssvc.getTagsForURI(uri2); + do_check_eq(uri2tags.length, 1); + do_check_eq(uri2tags[0], "Tag 1"); + + // test getURIsForTag + var tag1uris = tagssvc.getURIsForTag("tag 1"); + do_check_eq(tag1uris.length, 2); + do_check_true(tag1uris[0].equals(uri1)); + do_check_true(tag1uris[1].equals(uri2)); + + // test allTags attribute + var allTags = tagssvc.allTags; + do_check_eq(allTags.length, 2); + do_check_eq(allTags[0], "Tag 1"); + do_check_eq(allTags[1], "Tag 2"); + + // test untagging + tagssvc.untagURI(uri1, ["tag 1"]); + do_check_eq(tag1node.childCount, 1); + + // removing the last uri from a tag should remove the tag-container + tagssvc.untagURI(uri2, ["tag 1"]); + do_check_eq(tagRoot.childCount, 1); + + // cleanup + tag1node.containerOpen = false; + + // get array of tag folder ids => title + // for testing tagging with mixed folder ids and tags + var child = tagRoot.getChild(0); + var tagId = child.itemId; + var tagTitle = child.title; + + // test mixed id/name tagging + // as well as non-id numeric tags + var uri3 = uri("http://testuri/3"); + tagssvc.tagURI(uri3, [tagId, "tag 3", "456"]); + var tags = tagssvc.getTagsForURI(uri3); + do_check_true(tags.includes(tagTitle)); + do_check_true(tags.includes("tag 3")); + do_check_true(tags.includes("456")); + + // test mixed id/name tagging + tagssvc.untagURI(uri3, [tagId, "tag 3", "456"]); + tags = tagssvc.getTagsForURI(uri3); + do_check_eq(tags.length, 0); + + // Terminate tagging service, fire up a new instance and check that existing + // tags are there. This will ensure that any internal caching system is + // correctly filled at startup and we are not losing previously existing tags. + var uri4 = uri("http://testuri/4"); + tagssvc.tagURI(uri4, [tagId, "tag 3", "456"]); + tagssvc = null; + tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + var uri4Tags = tagssvc.getTagsForURI(uri4); + do_check_eq(uri4Tags.length, 3); + do_check_true(uri4Tags.includes(tagTitle)); + do_check_true(uri4Tags.includes("tag 3")); + do_check_true(uri4Tags.includes("456")); + + // Test sparse arrays. + let curChildCount = tagRoot.childCount; + + try { + tagssvc.tagURI(uri1, [, "tagSparse"]); + do_check_eq(tagRoot.childCount, curChildCount + 1); + } catch (ex) { + do_throw("Passing a sparse array should not throw"); + } + try { + tagssvc.untagURI(uri1, [, "tagSparse"]); + do_check_eq(tagRoot.childCount, curChildCount); + } catch (ex) { + do_throw("Passing a sparse array should not throw"); + } + + // Test that the API throws for bad arguments. + try { + tagssvc.tagURI(uri1, ["", "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.untagURI(uri1, ["", "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.tagURI(uri1, [0, "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + try { + tagssvc.tagURI(uri1, [0, "test"]); + do_throw("Passing a bad tags array should throw"); + } catch (ex) { + do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + + // Tag name length should be limited to nsITaggingService.MAX_TAG_LENGTH (bug407821) + try { + + // generate a long tag name. i.e. looooo...oong_tag + var n = Ci.nsITaggingService.MAX_TAG_LENGTH; + var someOos = new Array(n).join('o'); + var longTagName = "l" + someOos + "ng_tag"; + + tagssvc.tagURI(uri1, ["short_tag", longTagName]); + do_throw("Passing a bad tags array should throw"); + + } catch (ex) { + do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE"); + } + + // cleanup + tagRoot.containerOpen = false; + + // Tagging service should trim tags (Bug967196) + let exampleURI = uri("http://www.example.com/"); + PlacesUtils.tagging.tagURI(exampleURI, [ " test " ]); + + let exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI); + do_check_eq(exampleTags.length, 1); + do_check_eq(exampleTags[0], "test"); + + PlacesUtils.tagging.untagURI(exampleURI, [ "test" ]); + exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI); + do_check_eq(exampleTags.length, 0); +} diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js new file mode 100644 index 000000000..99f36d78c --- /dev/null +++ b/toolkit/components/places/tests/unit/test_telemetry.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests common Places telemetry probes by faking the telemetry service. + +Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + +var histograms = { + PLACES_PAGES_COUNT: val => do_check_eq(val, 1), + PLACES_BOOKMARKS_COUNT: val => do_check_eq(val, 1), + PLACES_TAGS_COUNT: val => do_check_eq(val, 1), + PLACES_KEYWORDS_COUNT: val => do_check_eq(val, 1), + PLACES_SORTED_BOOKMARKS_PERC: val => do_check_eq(val, 100), + PLACES_TAGGED_BOOKMARKS_PERC: val => do_check_eq(val, 100), + PLACES_DATABASE_FILESIZE_MB: val => do_check_true(val > 0), + PLACES_DATABASE_PAGESIZE_B: val => do_check_eq(val, 32768), + PLACES_DATABASE_SIZE_PER_PAGE_B: val => do_check_true(val > 0), + PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => do_check_true(val > 1), + // PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS: val => do_check_true(val > 1), + PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val >= 0), + PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0), + // One from the `setItemAnnotation` call; the other from the mobile root. + // This can be removed along with the anno in bug 1306445. + PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 2), + PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1), + PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0), +} + +/** + * Forces an expiration run. + * + * @param [optional] aLimit + * Limit for the expiration. Pass -1 for unlimited. + * Any other non-positive value will just expire orphans. + * + * @return {Promise} + * @resolves When expiration finishes. + * @rejects Never. + */ +function promiseForceExpirationStep(aLimit) { + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); + let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver); + expire.observe(null, "places-debug-start-expiration", aLimit); + return promise; +} + +/** + * Returns a PRTime in the past usable to add expirable visits. + * + * param [optional] daysAgo + * Expiration ignores any visit added in the last 7 days, so by default + * this will be set to 7. + * @note to be safe against DST issues we go back one day more. + */ +function getExpirablePRTime(daysAgo = 7) { + let dateObj = new Date(); + // Normalize to midnight + dateObj.setHours(0); + dateObj.setMinutes(0); + dateObj.setSeconds(0); + dateObj.setMilliseconds(0); + dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000); + return dateObj.getTime() * 1000; +} + +add_task(function* test_execute() +{ + // Put some trash in the database. + let uri = NetUtil.newURI("http://moz.org/"); + + let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, + "moz test", + PlacesUtils.bookmarks.DEFAULT_INDEX); + let itemId = PlacesUtils.bookmarks.insertBookmark(folderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "moz test"); + PlacesUtils.tagging.tagURI(uri, ["tag"]); + yield PlacesUtils.keywords.insert({ url: uri.spec, keyword: "keyword"}); + + // Set a large annotation. + let content = ""; + while (content.length < 1024) { + content += "0"; + } + PlacesUtils.annotations.setItemAnnotation(itemId, "test-anno", content, 0, + PlacesUtils.annotations.EXPIRE_NEVER); + PlacesUtils.annotations.setPageAnnotation(uri, "test-anno", content, 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + // Request to gather telemetry data. + Cc["@mozilla.org/places/categoriesStarter;1"] + .getService(Ci.nsIObserver) + .observe(null, "gather-telemetry", null); + + yield PlacesTestUtils.promiseAsyncUpdates(); + + // Test expiration probes. + let timeInMicroseconds = getExpirablePRTime(8); + + function newTimeInMicroseconds() { + timeInMicroseconds = timeInMicroseconds + 1000; + return timeInMicroseconds; + } + + for (let i = 0; i < 3; i++) { + yield PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://" + i + ".moz.org/"), + visitDate: newTimeInMicroseconds() + }); + } + Services.prefs.setIntPref("places.history.expiration.max_pages", 0); + yield promiseForceExpirationStep(2); + yield promiseForceExpirationStep(2); + + // Test autocomplete probes. + /* + // This is useful for manual testing by changing the minimum time for + // autocomplete telemetry to 0, but there is no way to artificially delay + // autocomplete by more than 50ms in a realiable way. + Services.prefs.setIntPref("browser.urlbar.search.sources", 3); + Services.prefs.setIntPref("browser.urlbar.default.behavior", 0); + function AutoCompleteInput(aSearches) { + this.searches = aSearches; + } + AutoCompleteInput.prototype = { + timeout: 10, + textValue: "", + searchParam: "", + popupOpen: false, + minResultsForPopup: 0, + invalidate: function() {}, + disableAutoComplete: false, + completeDefaultIndex: false, + get popup() { return this; }, + onSearchBegin: function() {}, + onSearchComplete: function() {}, + setSelectedIndex: function() {}, + get searchCount() { return this.searches.length; }, + getSearchAt: function(aIndex) { return this.searches[aIndex]; }, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIAutoCompleteInput, + Ci.nsIAutoCompletePopup, + ]) + }; + let controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + controller.input = new AutoCompleteInput(["unifiedcomplete"]); + controller.startSearch("moz"); + */ + + // Test idle probes. + PlacesUtils.history.QueryInterface(Ci.nsIObserver) + .observe(null, "idle-daily", null); + PlacesDBUtils.maintenanceOnIdle(); + + yield promiseTopicObserved("places-maintenance-finished"); + + for (let histogramId in histograms) { + do_print("checking histogram " + histogramId); + let validate = histograms[histogramId]; + let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot(); + validate(snapshot.sum); + do_check_true(snapshot.counts.reduce((a, b) => a + b) > 0); + } +}); diff --git a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js new file mode 100644 index 000000000..662ea0841 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js @@ -0,0 +1,151 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 455315 + * https://bugzilla.mozilla.org/show_bug.cgi?id=412132 + * + * Ensures that the frecency of a bookmark's URI is what it should be after the + * bookmark is deleted. + */ + +add_task(function* removed_bookmark() { + do_print("After removing bookmark, frecency of bookmark's URI should be " + + "zero if URI is unvisited and no longer bookmarked."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI + }); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.remove(bm); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Unvisited URI no longer bookmarked => frecency should = 0"); + do_check_eq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* removed_but_visited_bookmark() { + do_print("After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is visited."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let bm = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI + }); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesTestUtils.addVisits(TEST_URI); + yield PlacesUtils.bookmarks.remove(bm); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* remove_bookmark_still_bookmarked() { + do_print("After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is still bookmarked."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + let bm1 = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 1 title", + url: TEST_URI + }); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark 2 title", + url: TEST_URI + }); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.remove(bm1); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("URI still bookmarked => frecency should != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* cleared_parent_of_visited_bookmark() { + do_print("After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is visited."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark title", + url: TEST_URI + }); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesTestUtils.addVisits(TEST_URI); + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); + +add_task(function* cleared_parent_of_bookmark_still_bookmarked() { + do_print("After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is still " + + "bookmarked."); + const TEST_URI = NetUtil.newURI("http://example.com/1"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "bookmark 1 title", + url: TEST_URI + }); + + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bookmark 2 folder" + }); + yield PlacesUtils.bookmarks.insert({ + title: "bookmark 2 title", + parentGuid: folder.guid, + url: TEST_URI + }); + + yield PlacesTestUtils.promiseAsyncUpdates(); + do_print("Bookmarked => frecency of URI should be != 0"); + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.remove(folder); + yield PlacesTestUtils.promiseAsyncUpdates(); + // URI still bookmarked => frecency should != 0. + do_check_neq(frecencyForUrl(TEST_URI), 0); + + yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesTestUtils.clearHistory(); +}); diff --git a/toolkit/components/places/tests/unit/test_utils_backups_create.js b/toolkit/components/places/tests/unit/test_utils_backups_create.js new file mode 100644 index 000000000..a30589c44 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js @@ -0,0 +1,90 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Check for correct functionality of bookmarks backups + */ + +const NUMBER_OF_BACKUPS = 10; + +function run_test() { + run_next_test(); +} + +add_task(function* () { + // Generate random dates. + let dateObj = new Date(); + let dates = []; + while (dates.length < NUMBER_OF_BACKUPS) { + // Use last year to ensure today's backup is the newest. + let randomDate = new Date(dateObj.getFullYear() - 1, + Math.floor(12 * Math.random()), + Math.floor(28 * Math.random())); + if (!dates.includes(randomDate.getTime())) + dates.push(randomDate.getTime()); + } + // Sort dates from oldest to newest. + dates.sort(); + + // Get and cleanup the backups folder. + let backupFolderPath = yield PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolderPath); + + // Fake backups are created backwards to ensure we won't consider file + // creation time. + // Create fake backups for the newest dates. + for (let i = dates.length - 1; i >= 0; i--) { + let backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i])); + let backupFile = bookmarksBackupDir.clone(); + backupFile.append(backupFilename); + backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); + do_print("Creating fake backup " + backupFile.leafName); + if (!backupFile.exists()) + do_throw("Unable to create fake backup " + backupFile.leafName); + } + + yield PlacesBackups.create(NUMBER_OF_BACKUPS); + // Add today's backup. + dates.push(dateObj.getTime()); + + // Check backups. We have 11 dates but we the max number is 10 so the + // oldest backup should have been removed. + for (let i = 0; i < dates.length; i++) { + let backupFilename; + let shouldExist; + let backupFile; + if (i > 0) { + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.getNext().QueryInterface(Ci.nsIFile); + if (PlacesBackups.filenamesRegex.test(entry.leafName)) { + backupFilename = entry.leafName; + backupFile = entry; + break; + } + } + shouldExist = true; + } + else { + backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i])); + backupFile = bookmarksBackupDir.clone(); + backupFile.append(backupFilename); + shouldExist = false; + } + if (backupFile.exists() != shouldExist) + do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename); + } + + // Cleanup backups folder. + // XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens + // on WIN XP. + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.getNext().QueryInterface(Ci.nsIFile); + entry.remove(false); + } + do_check_false(bookmarksBackupDir.directoryEntries.hasMoreElements()); +}); diff --git a/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js new file mode 100644 index 000000000..ecebce94a --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js @@ -0,0 +1,180 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Check for correct functionality of PlacesUtils.getURLsForContainerNode and + * PlacesUtils.hasChildURIs (those helpers share almost all of their code) + */ + +var PU = PlacesUtils; +var hs = PU.history; +var bs = PU.bookmarks; + +var tests = [ + +function() { + dump("\n\n*** TEST: folder\n"); + // This is the folder we will check for children. + var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX); + bs.insertBookmark(folderId, uri("place:sort=1"), + bs.DEFAULT_INDEX, "inside query"); + + var query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + var options = hs.getNewQueryOptions(); + + dump("Check folder without uri nodes\n"); + check_uri_nodes(query, options, 0); + + dump("Check folder with uri nodes\n"); + // Add an uri node, this should be considered. + bs.insertBookmark(folderId, uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, "bookmark"); + check_uri_nodes(query, options, 1); +}, + +function() { + dump("\n\n*** TEST: folder in an excludeItems root\n"); + // This is the folder we will check for children. + var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX); + bs.insertBookmark(folderId, uri("place:sort=1"), bs.DEFAULT_INDEX, "inside query"); + + var query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + var options = hs.getNewQueryOptions(); + options.excludeItems = true; + + dump("Check folder without uri nodes\n"); + check_uri_nodes(query, options, 0); + + dump("Check folder with uri nodes\n"); + // Add an uri node, this should be considered. + bs.insertBookmark(folderId, uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, "bookmark"); + check_uri_nodes(query, options, 1); +}, + +function() { + dump("\n\n*** TEST: query\n"); + // This is the query we will check for children. + bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=1"), + bs.DEFAULT_INDEX, "inside query"); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX); + bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"), + bs.DEFAULT_INDEX, "inside query"); + + var query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + var options = hs.getNewQueryOptions(); + + dump("Check query without uri nodes\n"); + check_uri_nodes(query, options, 0); + + dump("Check query with uri nodes\n"); + // Add an uri node, this should be considered. + bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, "bookmark"); + check_uri_nodes(query, options, 1); +}, + +function() { + dump("\n\n*** TEST: excludeItems Query\n"); + // This is the query we will check for children. + bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"), + bs.DEFAULT_INDEX, "inside query"); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX); + bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"), + bs.DEFAULT_INDEX, "inside query"); + + var query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + var options = hs.getNewQueryOptions(); + options.excludeItems = true; + + dump("Check folder without uri nodes\n"); + check_uri_nodes(query, options, 0); + + dump("Check folder with uri nodes\n"); + // Add an uri node, this should be considered. + bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, "bookmark"); + check_uri_nodes(query, options, 1); +}, + +function() { + dump("\n\n*** TEST: !expandQueries Query\n"); + // This is the query we will check for children. + bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"), + bs.DEFAULT_INDEX, "inside query"); + + // Create a folder and a query node inside it, these should not be considered + // uri nodes. + bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX); + bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"), + bs.DEFAULT_INDEX, "inside query"); + + var query = hs.getNewQuery(); + query.setFolders([bs.toolbarFolder], 1); + var options = hs.getNewQueryOptions(); + options.expandQueries = false; + + dump("Check folder without uri nodes\n"); + check_uri_nodes(query, options, 0); + + dump("Check folder with uri nodes\n"); + // Add an uri node, this should be considered. + bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"), + bs.DEFAULT_INDEX, "bookmark"); + check_uri_nodes(query, options, 1); +} + +]; + +/** + * Executes a query and checks number of uri nodes in the first container in + * query's results. To correctly test a container ensure that the query will + * return only your container in the first level. + * + * @param aQuery + * nsINavHistoryQuery object defining the query + * @param aOptions + * nsINavHistoryQueryOptions object defining the query's options + * @param aExpectedURINodes + * number of expected uri nodes + */ +function check_uri_nodes(aQuery, aOptions, aExpectedURINodes) { + var result = hs.executeQuery(aQuery, aOptions); + var root = result.root; + root.containerOpen = true; + var node = root.getChild(0); + do_check_eq(PU.hasChildURIs(node), aExpectedURINodes > 0); + do_check_eq(PU.getURLsForContainerNode(node).length, aExpectedURINodes); + root.containerOpen = false; +} + +add_task(function* () { + for (let test of tests) { + yield PlacesUtils.bookmarks.eraseEverything(); + test(); + } + + // Cleanup. + yield PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js new file mode 100644 index 000000000..62947620d --- /dev/null +++ b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js @@ -0,0 +1,79 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * Check for correct functionality of PlacesUtils.setAnnotationsForItem/URI + */ + +var hs = PlacesUtils.history; +var bs = PlacesUtils.bookmarks; +var as = PlacesUtils.annotations; + +const TEST_URL = "http://test.mozilla.org/"; + +function run_test() { + var testURI = uri(TEST_URL); + // add a bookmark + var itemId = bs.insertBookmark(bs.unfiledBookmarksFolder, testURI, + bs.DEFAULT_INDEX, "test"); + + // create annotations array + var testAnnos = [{ name: "testAnno/test0", + flags: 0, + value: "test0", + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }, + { name: "testAnno/test1", + flags: 0, + value: "test1", + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }, + { name: "testAnno/test2", + flags: 0, + value: "test2", + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }, + { name: "testAnno/test3", + flags: 0, + value: 0, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }]; + + // Add item annotations + PlacesUtils.setAnnotationsForItem(itemId, testAnnos); + // Check for correct addition + testAnnos.forEach(function(anno) { + do_check_true(as.itemHasAnnotation(itemId, anno.name)); + do_check_eq(as.getItemAnnotation(itemId, anno.name), anno.value); + }); + + // Add page annotations + PlacesUtils.setAnnotationsForURI(testURI, testAnnos); + // Check for correct addition + testAnnos.forEach(function(anno) { + do_check_true(as.pageHasAnnotation(testURI, anno.name)); + do_check_eq(as.getPageAnnotation(testURI, anno.name), anno.value); + }); + + // To unset annotations we unset their values or set them to + // null/undefined + testAnnos[0].value = null; + testAnnos[1].value = undefined; + delete testAnnos[2].value; + delete testAnnos[3].value; + + // Unset all item annotations + PlacesUtils.setAnnotationsForItem(itemId, testAnnos); + // Check for correct removal + testAnnos.forEach(function(anno) { + do_check_false(as.itemHasAnnotation(itemId, anno.name)); + // sanity: page annotations should not be removed here + do_check_true(as.pageHasAnnotation(testURI, anno.name)); + }); + + // Unset all page annotations + PlacesUtils.setAnnotationsForURI(testURI, testAnnos); + // Check for correct removal + testAnnos.forEach(function(anno) { + do_check_false(as.pageHasAnnotation(testURI, anno.name)); + }); +} diff --git a/toolkit/components/places/tests/unit/test_visitsInDB.js b/toolkit/components/places/tests/unit/test_visitsInDB.js new file mode 100644 index 000000000..3cab39ed9 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_visitsInDB.js @@ -0,0 +1,12 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +add_task(function* test_execute() { + const TEST_URI = uri("http://mozilla.com"); + + do_check_eq(0, yield PlacesTestUtils.visitsInDB(TEST_URI)); + yield PlacesTestUtils.addVisits({uri: TEST_URI}); + do_check_eq(1, yield PlacesTestUtils.visitsInDB(TEST_URI)); + yield PlacesTestUtils.addVisits({uri: TEST_URI}); + do_check_eq(2, yield PlacesTestUtils.visitsInDB(TEST_URI)); +}); diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini new file mode 100644 index 000000000..60bba4758 --- /dev/null +++ b/toolkit/components/places/tests/unit/xpcshell.ini @@ -0,0 +1,163 @@ +[DEFAULT] +head = head_bookmarks.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' +support-files = + bookmarks.corrupt.html + bookmarks.json + bookmarks.preplaces.html + bookmarks_html_singleframe.html + bug476292.sqlite + corruptDB.sqlite + default.sqlite + livemark.xml + mobile_bookmarks_folder_import.json + mobile_bookmarks_folder_merge.json + mobile_bookmarks_multiple_folders.json + mobile_bookmarks_root_import.json + mobile_bookmarks_root_merge.json + nsDummyObserver.js + nsDummyObserver.manifest + places.sparse.sqlite + +[test_000_frecency.js] +[test_317472.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_331487.js] +[test_384370.js] +[test_385397.js] +# Bug 676989: test fails consistently on Android +fail-if = os == "android" +[test_399264_query_to_string.js] +[test_399264_string_to_query.js] +[test_399266.js] +# Bug 676989: test fails consistently on Android +fail-if = os == "android" +# Bug 821781: test fails intermittently on Linux +skip-if = os == "linux" +[test_402799.js] +[test_405497.js] +[test_408221.js] +[test_412132.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_413784.js] +[test_415460.js] +[test_415757.js] +[test_418643_removeFolderChildren.js] +[test_419731.js] +[test_419792_node_tags_property.js] +[test_425563.js] +[test_429505_remove_shortcuts.js] +[test_433317_query_title_update.js] +[test_433525_hasChildren_crash.js] +[test_452777.js] +[test_454977.js] +[test_463863.js] +[test_485442_crash_bug_nsNavHistoryQuery_GetUri.js] +[test_486978_sort_by_date_queries.js] +[test_536081.js] +[test_1085291.js] +[test_1105208.js] +[test_1105866.js] +[test_adaptive.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_adaptive_bug527311.js] +[test_analyze.js] +[test_annotations.js] +[test_asyncExecuteLegacyQueries.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_async_history_api.js] +[test_async_in_batchmode.js] +[test_async_transactions.js] +skip-if = (os == "win" && os_version == "5.1") # Bug 1158887 +[test_autocomplete_stopSearch_no_throw.js] +[test_bookmark_catobs.js] +[test_bookmarks_json.js] +[test_bookmarks_html.js] +[test_bookmarks_html_corrupt.js] +[test_bookmarks_html_import_tags.js] +[test_bookmarks_html_singleframe.js] +[test_bookmarks_restore_notification.js] +[test_bookmarks_setNullTitle.js] +[test_broken_folderShortcut_result.js] +[test_browserhistory.js] +[test_bug636917_isLivemark.js] +[test_childlessTags.js] +[test_corrupt_telemetry.js] +[test_crash_476292.js] +[test_database_replaceOnStartup.js] +[test_download_history.js] +# Bug 676989: test fails consistently on Android +fail-if = os == "android" +[test_frecency.js] +[test_frecency_zero_updated.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_getChildIndex.js] +[test_getPlacesInfo.js] +[test_history.js] +[test_history_autocomplete_tags.js] +[test_history_catobs.js] +[test_history_clear.js] +[test_history_notifications.js] +[test_history_observer.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_history_sidebar.js] +[test_hosts_triggers.js] +[test_import_mobile_bookmarks.js] +[test_isPageInDB.js] +[test_isURIVisited.js] +[test_isvisited.js] +[test_keywords.js] +[test_lastModified.js] +[test_markpageas.js] +[test_mozIAsyncLivemarks.js] +[test_multi_queries.js] +# Bug 676989: test fails consistently on Android +fail-if = os == "android" +[test_multi_word_tags.js] +[test_nsINavHistoryViewer.js] +# Bug 902248: intermittent timeouts on all platforms +skip-if = true +[test_null_interfaces.js] +[test_onItemChanged_tags.js] +[test_pageGuid_bookmarkGuid.js] +[test_frecency_observers.js] +[test_placeURIs.js] +[test_PlacesSearchAutocompleteProvider.js] +[test_PlacesUtils_asyncGetBookmarkIds.js] +[test_PlacesUtils_invalidateCachedGuidFor.js] +[test_PlacesUtils_lazyobservers.js] +[test_placesTxn.js] +[test_preventive_maintenance.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_preventive_maintenance_checkAndFixDatabase.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_preventive_maintenance_runTasks.js] +[test_promiseBookmarksTree.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_resolveNullBookmarkTitles.js] +[test_result_sort.js] +[test_resultsAsVisit_details.js] +[test_sql_guid_functions.js] +[test_svg_favicon.js] +[test_sync_utils.js] +[test_tag_autocomplete_search.js] +[test_tagging.js] +[test_telemetry.js] +[test_update_frecency_after_delete.js] +# Bug 676989: test hangs consistently on Android +skip-if = os == "android" +[test_utils_backups_create.js] +[test_utils_getURLsForContainerNode.js] +[test_utils_setAnnotationsFor.js] +[test_visitsInDB.js] |