author | Wes Kocher <wkocher@mozilla.com> |
Wed, 04 Feb 2015 17:48:54 -0800 | |
changeset 227537 | 2037891bcdc84fdacd7e9b93f1b9cedc5f73b804 |
parent 227536 | 4f1b69be79ddda8398d640e6bbd30f05cd5a43c7 (current diff) |
parent 227534 | 34a66aaaca81826f0b838016aa15a077b50fa2fc (diff) |
child 227538 | a2b89c1b4c47b4c8518ed0f16c8c578a21070bdc |
push id | 28233 |
push user | cbook@mozilla.com |
push date | Thu, 05 Feb 2015 13:22:30 +0000 |
treeherder | mozilla-central@d2df44094059 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 38.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser-fxaccounts.js +++ b/browser/base/content/browser-fxaccounts.js @@ -290,35 +290,36 @@ let gFxAccounts = { } let note = null; switch (this._migrationInfo.state) { case this.fxaMigrator.STATE_USER_FXA: { // There are 2 cases here - no email address means it is an offer on // the first device (so the user is prompted to create an account). // If there is an email address it is the "join the party" flow, so the // user is prompted to sign in with the address they previously used. - let msg, upgradeLabel, upgradeAccessKey; + let msg, upgradeLabel, upgradeAccessKey, learnMoreLink; if (this._migrationInfo.email) { msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description", [this._migrationInfo.email], 1); upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label"); upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey"); } else { msg = this.strings.GetStringFromName("needUserLong"); upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label"); upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey"); + learnMoreLink = this.fxaMigrator.learnMoreLink; } note = new Weave.Notification( undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [ new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => { this._expectingNotifyClose = true; this.fxaMigrator.createFxAccount(window); }), - ] + ], learnMoreLink ); break; } case this.fxaMigrator.STATE_USER_FXA_VERIFIED: { let msg = this.strings.formatStringFromName("needVerifiedUserLong", [this._migrationInfo.email], 1); let resendLabel =
--- a/browser/base/content/sync/notification.xml +++ b/browser/base/content/sync/notification.xml @@ -68,16 +68,28 @@ <parameter name="notification"/> <body><![CDATA[ var node = this.appendNotification(notification.description, notification.title, notification.iconURL, notification.priority, notification.buttons); node.notification = notification; + + if (notification.link) { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let link = node.ownerDocument.createElementNS(XULNS, "label"); + link.className = "text-link"; + link.setAttribute("value", notification.link.text); + link.href = notification.link.href; + let desc = node.ownerDocument.getAnonymousElementByAttribute( + node, "anonid", "messageText" + ); + desc.appendChild(link); + } ]]></body> </method> </implementation> </binding> <binding id="notification" extends="chrome://global/content/bindings/notification.xml#notification"> <content>
--- a/browser/components/places/tests/unit/head_bookmarks.js +++ b/browser/components/places/tests/unit/head_bookmarks.js @@ -7,66 +7,102 @@ const Ci = Components.interfaces; const Cc = Components.classes; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/LoadContextInfo.jsm"); // Import common head. -let (commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false)) { +let commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false); +if (commonFile) { let uri = Services.io.newFileURI(commonFile); Services.scriptloader.loadSubScript(uri.spec, this); } // Put any other stuff relative to this test folder below. - XPCOMUtils.defineLazyGetter(this, "PlacesUIUtils", function() { Cu.import("resource:///modules/PlacesUIUtils.jsm"); return PlacesUIUtils; }); - const ORGANIZER_FOLDER_ANNO = "PlacesOrganizer/OrganizerFolder"; const ORGANIZER_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery"; - // Needed by some test that relies on having an app registered. -let (XULAppInfo = { +let XULAppInfo = { vendor: "Mozilla", name: "PlacesTest", ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", version: "1", appBuildID: "2007010101", platformVersion: "", platformBuildID: "2007010101", inSafeMode: false, logConsoleErrors: true, OS: "XPCShell", XPCOMABI: "noarch-spidermonkey", QueryInterface: XPCOMUtils.generateQI([ Ci.nsIXULAppInfo, Ci.nsIXULRuntime, ]) -}) { - let XULAppInfoFactory = { - createInstance: function (outer, iid) { - if (outer != null) - throw Cr.NS_ERROR_NO_AGGREGATION; - return XULAppInfo.QueryInterface(iid); - } - }; - let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); - registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"), - "XULAppInfo", "@mozilla.org/xre/app-info;1", - XULAppInfoFactory); -} +}; + +let XULAppInfoFactory = { + createInstance: function (outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return XULAppInfo.QueryInterface(iid); + } +}; +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"), + "XULAppInfo", "@mozilla.org/xre/app-info;1", + XULAppInfoFactory); // Smart bookmarks constants. const SMART_BOOKMARKS_VERSION = 7; const SMART_BOOKMARKS_ON_TOOLBAR = 1; const SMART_BOOKMARKS_ON_MENU = 3; // Takes into account the additional separator. // Default bookmarks constants. const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1; const DEFAULT_BOOKMARKS_ON_MENU = 1; + +const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; + +function checkItemHasAnnotation(guid, name) { + return PlacesUtils.promiseItemId(guid).then(id => { + let hasAnnotation = PlacesUtils.annotations.itemHasAnnotation(id, name); + Assert.ok(hasAnnotation, `Expected annotation ${name}`); + }); +} + +function waitForImportAndSmartBookmarks() { + return Promise.all([ + promiseTopicObserved("bookmarks-restore-success"), + PlacesTestUtils.promiseAsyncUpdates() + ]); +} + +function promiseEndUpdateBatch() { + return new Promise(resolve => { + PlacesUtils.bookmarks.addObserver({ + __proto__: NavBookmarkObserver.prototype, + onEndUpdateBatch: resolve + }, false); + }); +} + +let createCorruptDB = Task.async(function* () { + let dbPath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite"); + yield OS.File.remove(dbPath); + + // Create a corrupt database. + let dir = yield OS.File.getCurrentDirectory(); + let src = OS.Path.join(dir, "corruptDB.sqlite"); + yield OS.File.copy(src, dbPath); + + // Check there's a DB now. + Assert.ok((yield OS.File.exists(dbPath)), "should have a DB now"); +});
--- a/browser/components/places/tests/unit/test_421483.js +++ b/browser/components/places/tests/unit/test_421483.js @@ -1,81 +1,104 @@ /* -*- 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 SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion"; let gluesvc = Cc["@mozilla.org/browser/browserglue;1"]. getService(Ci.nsIBrowserGlue). QueryInterface(Ci.nsIObserver); // Avoid default bookmarks import. gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", ""); function run_test() { run_next_test(); } -add_task(function smart_bookmarks_disabled() { +add_task(function* smart_bookmarks_disabled() { Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1); gluesvc.ensurePlacesDefaultQueriesInitialized(); + let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_eq(smartBookmarkItemIds.length, 0); + Assert.equal(smartBookmarkItemIds.length, 0); + do_print("check that pref has not been bumped up"); - do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1); + Assert.equal(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1); }); -add_task(function create_smart_bookmarks() { +add_task(function* create_smart_bookmarks() { Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); gluesvc.ensurePlacesDefaultQueriesInitialized(); + let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_neq(smartBookmarkItemIds.length, 0); + Assert.notEqual(smartBookmarkItemIds.length, 0); + do_print("check that pref has been bumped up"); - do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); + Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); }); -add_task(function remove_smart_bookmark_and_restore() { +add_task(function* remove_smart_bookmark_and_restore() { let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); let smartBookmarksCount = smartBookmarkItemIds.length; do_print("remove one smart bookmark and restore"); - PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]); + + let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]); + yield PlacesUtils.bookmarks.remove(guid); Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); + gluesvc.ensurePlacesDefaultQueriesInitialized(); smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount); + Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount); + do_print("check that pref has been bumped up"); - do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); + Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); }); -add_task(function move_smart_bookmark_rename_and_restore() { +add_task(function* move_smart_bookmark_rename_and_restore() { let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); let smartBookmarksCount = smartBookmarkItemIds.length; do_print("smart bookmark should be restored in place"); - let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]); - let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]); + + let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]); + let bm = yield PlacesUtils.bookmarks.fetch(guid); + let oldTitle = bm.title; + // create a subfolder and move inside it - let newParent = - PlacesUtils.bookmarks.createFolder(parent, "test", - PlacesUtils.bookmarks.DEFAULT_INDEX); - PlacesUtils.bookmarks.moveItem(smartBookmarkItemIds[0], newParent, - PlacesUtils.bookmarks.DEFAULT_INDEX); - // change title - PlacesUtils.bookmarks.setItemTitle(smartBookmarkItemIds[0], "new title"); + let subfolder = yield PlacesUtils.bookmarks.insert({ + parentGuid: bm.parentGuid, + title: "test", + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_FOLDER + }); + + // change title and move into new subfolder + yield PlacesUtils.bookmarks.update({ + guid: guid, + parentGuid: subfolder.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + title: "new title" + }); + // restore Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); gluesvc.ensurePlacesDefaultQueriesInitialized(); + smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); - do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount); - do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle); + Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount); + + guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]); + bm = yield PlacesUtils.bookmarks.fetch(guid); + Assert.equal(bm.parentGuid, subfolder.guid); + Assert.equal(bm.title, oldTitle); + do_print("check that pref has been bumped up"); - do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); + Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0); });
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js @@ -4,83 +4,56 @@ * 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 that nsBrowserGlue correctly restores bookmarks from a JSON backup if * database is corrupt and one backup is available. */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "bs", - "@mozilla.org/browser/nav-bookmarks-service;1", - "nsINavBookmarksService"); -XPCOMUtils.defineLazyServiceGetter(this, "anno", - "@mozilla.org/browser/annotation-service;1", - "nsIAnnotationService"); - -let bookmarksObserver = { - onBeginUpdateBatch: function() {}, - onEndUpdateBatch: function() { - let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0); - do_check_neq(itemId, -1); - if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark")) - continue_test(); - }, - onItemAdded: function() {}, - onItemRemoved: function(id, folder, index, itemType) {}, - onItemChanged: function() {}, - onItemVisited: function(id, visitID, time) {}, - onItemMoved: function() {}, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) -}; - function run_test() { - do_test_pending(); - - // Create our bookmarks.html copying bookmarks.glue.html to the profile - // folder. It should be ignored. + // Create our bookmarks.html from bookmarks.glue.html. create_bookmarks_html("bookmarks.glue.html"); - // Create our JSON backup copying bookmarks.glue.json to the profile folder. remove_all_JSON_backups(); + + // Create our JSON backup from bookmarks.glue.json. create_JSON_backup("bookmarks.glue.json"); - // Remove current database file. - let db = gProfD.clone(); - db.append("places.sqlite"); - if (db.exists()) { - db.remove(false); - do_check_false(db.exists()); - } + run_next_test(); +} + +do_register_cleanup(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + return PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_main() { // Create a corrupt database. - let corruptDB = gTestDir.clone(); - corruptDB.append("corruptDB.sqlite"); - corruptDB.copyTo(gProfD, "places.sqlite"); - do_check_true(db.exists()); + yield createCorruptDB(); // Initialize nsBrowserGlue before Places. Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue); - // Initialize Places through the History Service. - let hs = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); // Check the database was corrupt. // nsBrowserGlue uses databaseStatus to manage initialization. - do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT); + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); // The test will continue once restore has finished and smart bookmarks // have been created. - bs.addObserver(bookmarksObserver, false); -} + yield promiseEndUpdateBatch(); -function continue_test() { + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + // Check that JSON backup has been restored. // Notice restore from JSON notification is fired before smart bookmarks creation. - let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR); - do_check_eq(bs.getItemTitle(itemId), "examplejson"); - - remove_bookmarks_html(); - remove_all_JSON_backups(); - - do_test_finished(); -} + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(bm.title, "examplejson"); +});
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js @@ -4,78 +4,49 @@ * 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 that nsBrowserGlue correctly imports from bookmarks.html if database * is corrupt but a JSON backup is not available. */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "bs", - "@mozilla.org/browser/nav-bookmarks-service;1", - "nsINavBookmarksService"); -XPCOMUtils.defineLazyServiceGetter(this, "anno", - "@mozilla.org/browser/annotation-service;1", - "nsIAnnotationService"); +function run_test() { + // Create our bookmarks.html from bookmarks.glue.html. + create_bookmarks_html("bookmarks.glue.html"); -let bookmarksObserver = { - onBeginUpdateBatch: function() {}, - onEndUpdateBatch: function() { - let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0); - do_check_neq(itemId, -1); - if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark")) - continue_test(); - }, - onItemAdded: function() {}, - onItemRemoved: function(id, folder, index, itemType) {}, - onItemChanged: function() {}, - onItemVisited: function(id, visitID, time) {}, - onItemMoved: function() {}, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) -}; - -function run_test() { - do_test_pending(); - - // Create bookmarks.html in the profile. - create_bookmarks_html("bookmarks.glue.html"); // Remove JSON backup from profile. remove_all_JSON_backups(); - // Remove current database file. - let db = gProfD.clone(); - db.append("places.sqlite"); - if (db.exists()) { - db.remove(false); - do_check_false(db.exists()); - } + run_next_test(); +} + +do_register_cleanup(remove_bookmarks_html); + +add_task(function* () { // Create a corrupt database. - let corruptDB = gTestDir.clone(); - corruptDB.append("corruptDB.sqlite"); - corruptDB.copyTo(gProfD, "places.sqlite"); - do_check_true(db.exists()); + yield createCorruptDB(); // Initialize nsBrowserGlue before Places. Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue); - // Initialize Places through the History Service. - let hs = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); // Check the database was corrupt. // nsBrowserGlue uses databaseStatus to manage initialization. - do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT); + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); // The test will continue once import has finished and smart bookmarks // have been created. - bs.addObserver(bookmarksObserver, false); -} + yield promiseEndUpdateBatch(); -function continue_test() { + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + // Check that bookmarks html has been restored. - let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR); - do_check_eq(bs.getItemTitle(itemId), "example"); - - remove_bookmarks_html(); - - do_test_finished(); -} + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(bm.title, "example"); +});
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js @@ -4,77 +4,47 @@ * 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 that nsBrowserGlue correctly restores default bookmarks if database is * corrupt, nor a JSON backup nor bookmarks.html are available. */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "bs", - "@mozilla.org/browser/nav-bookmarks-service;1", - "nsINavBookmarksService"); -XPCOMUtils.defineLazyServiceGetter(this, "anno", - "@mozilla.org/browser/annotation-service;1", - "nsIAnnotationService"); - -let bookmarksObserver = { - onBeginUpdateBatch: function() {}, - onEndUpdateBatch: function() { - let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0); - do_check_neq(itemId, -1); - if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark")) - continue_test(); - }, - onItemAdded: function() {}, - onItemRemoved: function(id, folder, index, itemType) {}, - onItemChanged: function() {}, - onItemVisited: function(id, visitID, time) {}, - onItemMoved: function() {}, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) -}; - function run_test() { - do_test_pending(); - // Remove bookmarks.html from profile. remove_bookmarks_html(); + // Remove JSON backup from profile. remove_all_JSON_backups(); - // Remove current database file. - let db = gProfD.clone(); - db.append("places.sqlite"); - if (db.exists()) { - db.remove(false); - do_check_false(db.exists()); - } + run_next_test(); +} + +add_task(function* () { // Create a corrupt database. - let corruptDB = gTestDir.clone(); - corruptDB.append("corruptDB.sqlite"); - corruptDB.copyTo(gProfD, "places.sqlite"); - do_check_true(db.exists()); + yield createCorruptDB(); // Initialize nsBrowserGlue before Places. Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue); - // Initialize Places through the History Service. - let hs = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); // Check the database was corrupt. // nsBrowserGlue uses databaseStatus to manage initialization. - do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT); + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CORRUPT); // The test will continue once import has finished and smart bookmarks // have been created. - bs.addObserver(bookmarksObserver, false); -} + yield promiseEndUpdateBatch(); -function continue_test() { + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + // Check that default bookmarks have been restored. - let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR); - do_check_true(itemId > 0); - do_check_eq(bs.getItemTitle(itemId), "Getting Started"); - - do_test_finished(); -} + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + do_check_eq(bm.title, "Getting Started"); +});
--- a/browser/components/places/tests/unit/test_browserGlue_distribution.js +++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js @@ -8,98 +8,90 @@ const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion"; const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed"; const PREF_DISTRIBUTION_ID = "distribution.id"; const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization"; const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete"; const TOPIC_BROWSERGLUE_TEST = "browser-glue-test"; -function run_test() -{ - do_test_pending(); - +function run_test() { // Set special pref to load distribution.ini from the profile folder. Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true); + // Copy distribution.ini file to the profile dir. let distroDir = gProfD.clone(); distroDir.leafName = "distribution"; let iniFile = distroDir.clone(); iniFile.append("distribution.ini"); if (iniFile.exists()) { iniFile.remove(false); print("distribution.ini already exists, did some test forget to cleanup?"); } + let testDistributionFile = gTestDir.clone(); testDistributionFile.append("distribution.ini"); testDistributionFile.copyTo(distroDir, "distribution.ini"); - do_check_true(testDistributionFile.exists()); + Assert.ok(testDistributionFile.exists()); + + run_next_test(); +} +do_register_cleanup(function () { + // Remove the distribution file, even if the test failed, otherwise all + // next tests will import it. + let iniFile = gProfD.clone(); + iniFile.leafName = "distribution"; + iniFile.append("distribution.ini"); + if (iniFile.exists()) { + iniFile.remove(false); + } + Assert.ok(!iniFile.exists()); +}); + +add_task(function* () { // Disable Smart Bookmarks creation. Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1); // Initialize Places through the History Service and check that a new // database has been created. - do_check_eq(PlacesUtils.history.databaseStatus, - PlacesUtils.history.DATABASE_STATUS_CREATE); + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE); // Force distribution. - Cc["@mozilla.org/browser/browserglue;1"]. - getService(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_DISTRIBUTION_CUSTOMIZATION); + let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver) + glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION); // Test will continue on customization complete notification. - Services.obs.addObserver(function(aSubject, aTopic, aData) { - Services.obs.removeObserver(arguments.callee, - TOPIC_CUSTOMIZATION_COMPLETE, - false); - do_execute_soon(onCustomizationComplete); - }, TOPIC_CUSTOMIZATION_COMPLETE, false); -} + yield promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE); -function onCustomizationComplete() -{ // Check the custom bookmarks exist on menu. - let menuItemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0); - do_check_neq(menuItemId, -1); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(menuItemId), - "Menu Link Before"); - menuItemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, - 1 + DEFAULT_BOOKMARKS_ON_MENU); - do_check_neq(menuItemId, -1); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(menuItemId), - "Menu Link After"); + let menuItem = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + }); + Assert.equal(menuItem.title, "Menu Link Before"); + + menuItem = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1 + DEFAULT_BOOKMARKS_ON_MENU + }); + Assert.equal(menuItem.title, "Menu Link After"); // Check the custom bookmarks exist on toolbar. - let toolbarItemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_neq(toolbarItemId, -1); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(toolbarItemId), - "Toolbar Link Before"); - toolbarItemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_neq(toolbarItemId, -1); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(toolbarItemId), - "Toolbar Link After"); + let toolbarItem = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + Assert.equal(toolbarItem.title, "Toolbar Link Before"); + + toolbarItem = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(toolbarItem.title, "Toolbar Link After"); // Check the bmprocessed pref has been created. - do_check_true(Services.prefs.getBoolPref(PREF_BMPROCESSED)); + Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED)); // Check distribution prefs have been created. - do_check_eq(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444"); - - do_test_finished(); -} - -do_register_cleanup(function() { - // Remove the distribution file, even if the test failed, otherwise all - // next tests will import it. - let iniFile = gProfD.clone(); - iniFile.leafName = "distribution"; - iniFile.append("distribution.ini"); - if (iniFile.exists()) - iniFile.remove(false); - do_check_false(iniFile.exists()); + Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444"); });
--- a/browser/components/places/tests/unit/test_browserGlue_migrate.js +++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js @@ -5,84 +5,66 @@ * Tests that nsBrowserGlue does not overwrite bookmarks imported from the * migrators. They usually run before nsBrowserGlue, so if we find any * bookmark on init, we should not try to import. */ const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion"; function run_test() { - do_test_pending(); - - // Create our bookmarks.html copying bookmarks.glue.html to the profile - // folder. It should be ignored. + // Create our bookmarks.html from bookmarks.glue.html. create_bookmarks_html("bookmarks.glue.html"); // Remove current database file. - let db = gProfD.clone(); - db.append("places.sqlite"); - if (db.exists()) { - db.remove(false); - do_check_false(db.exists()); - } + clearDB(); + run_next_test(); +} + +do_register_cleanup(remove_bookmarks_html); + +add_task(function* test_migrate_bookmarks() { // Initialize Places through the History Service and check that a new // database has been created. - do_check_eq(PlacesUtils.history.databaseStatus, - PlacesUtils.history.DATABASE_STATUS_CREATE); + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE); // A migrator would run before nsBrowserGlue Places initialization, so mimic // that behavior adding a bookmark and notifying the migration. - let bg = Cc["@mozilla.org/browser/browserglue;1"]. - getService(Ci.nsIObserver); - + let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver); bg.observe(null, "initial-migration-will-import-default-bookmarks", null); - PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder, uri("http://mozilla.org/"), - PlacesUtils.bookmarks.DEFAULT_INDEX, "migrated"); + yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/", + title: "migrated" + }); + + let promise = promiseEndUpdateBatch(); + bg.observe(null, "initial-migration-did-import-default-bookmarks", null); + yield promise; - let bookmarksObserver = { - onBeginUpdateBatch: function() {}, - onEndUpdateBatch: function() { - // Check if the currently finished batch created the smart bookmarks. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_neq(itemId, -1); - if (PlacesUtils.annotations - .itemHasAnnotation(itemId, "Places/SmartBookmark")) { - do_execute_soon(onSmartBookmarksCreation); - } - }, - onItemAdded: function() {}, - onItemRemoved: function(id, folder, index, itemType) {}, - onItemChanged: function() {}, - onItemVisited: function(id, visitID, time) {}, - onItemMoved: function() {}, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) - }; - // The test will continue once import has finished and smart bookmarks - // have been created. - PlacesUtils.bookmarks.addObserver(bookmarksObserver, false); + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); - bg.observe(null, "initial-migration-did-import-default-bookmarks", null); -} - -function onSmartBookmarksCreation() { - // Check the created bookmarks still exist. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, - SMART_BOOKMARKS_ON_MENU); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "migrated"); + // Check the created bookmark still exists. + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: SMART_BOOKMARKS_ON_MENU + }); + Assert.equal(bm.title, "migrated"); // Check that we have not imported any new bookmark. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, - SMART_BOOKMARKS_ON_MENU + 1) - do_check_eq(itemId, -1); - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - SMART_BOOKMARKS_ON_MENU) - do_check_eq(itemId, -1); + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: SMART_BOOKMARKS_ON_MENU + 1 + }))); - remove_bookmarks_html(); - - do_test_finished(); -} + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_MENU + }))); +});
--- a/browser/components/places/tests/unit/test_browserGlue_prefs.js +++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js @@ -10,274 +10,233 @@ const PREF_IMPORT_BOOKMARKS_HTML = "brow const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks"; const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion"; const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML"; const TOPIC_BROWSERGLUE_TEST = "browser-glue-test"; const TOPICDATA_FORCE_PLACES_INIT = "force-places-init"; let bg = Cc["@mozilla.org/browser/browserglue;1"]. - getService(Ci.nsIBrowserGlue); - -function waitForImportAndSmartBookmarks(aCallback) { - Services.obs.addObserver(function waitImport() { - Services.obs.removeObserver(waitImport, "bookmarks-restore-success"); - // Delay to test eventual smart bookmarks creation. - do_execute_soon(function () { - PlacesTestUtils.promiseAsyncUpdates().then(aCallback); - }); - }, "bookmarks-restore-success", false); -} - -[ - - // This test must be the first one. - function test_checkPreferences() { - // Initialize Places through the History Service and check that a new - // database has been created. - do_check_eq(PlacesUtils.history.databaseStatus, - PlacesUtils.history.DATABASE_STATUS_CREATE); - - // Wait for Places init notification. - Services.obs.addObserver(function(aSubject, aTopic, aData) { - Services.obs.removeObserver(arguments.callee, - "places-browser-init-complete"); - do_execute_soon(function () { - // Ensure preferences status. - do_check_false(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); - - try { - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - do_throw("importBookmarksHTML pref should not exist"); - } - catch(ex) {} - - try { - do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); - do_throw("importBookmarksHTML pref should not exist"); - } - catch(ex) {} - - run_next_test(); - }); - }, "places-browser-init-complete", false); - }, - - function test_import() - { - do_print("Import from bookmarks.html if importBookmarksHTML is true."); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); - - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been imported, and a smart bookmark has been - // created. - itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - SMART_BOOKMARKS_ON_TOOLBAR); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example"); - // Check preferences have been reverted. - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces(). - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - }, - - function test_import_noSmartBookmarks() - { - do_print("import from bookmarks.html, but don't create smart bookmarks \ - if they are disabled"); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1); - Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); - - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been imported, but smart bookmarks have not - // been created. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example"); - // Check preferences have been reverted. - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces(). - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - }, - - function test_import_autoExport_updatedSmartBookmarks() - { - do_print("Import from bookmarks.html, but don't create smart bookmarks \ - if autoExportHTML is true and they are at latest version"); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999); - Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true); - Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + getService(Ci.nsIObserver); - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been imported, but smart bookmarks have not - // been created. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example"); - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - // Check preferences have been reverted. - Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces() - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - }, - - function test_import_autoExport_oldSmartBookmarks() - { - do_print("Import from bookmarks.html, and create smart bookmarks if \ - autoExportHTML is true and they are not at latest version."); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); - Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true); - Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); - - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been imported, but smart bookmarks have not - // been created. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - SMART_BOOKMARKS_ON_TOOLBAR); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example"); - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - // Check preferences have been reverted. - Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces() - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - }, - - function test_restore() - { - do_print("restore from default bookmarks.html if \ - restore_default_bookmarks is true."); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); - - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been restored. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - SMART_BOOKMARKS_ON_TOOLBAR); - do_check_true(itemId > 0); - // Check preferences have been reverted. - do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces() - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - - }, - - function test_restore_import() - { - do_print("setting both importBookmarksHTML and \ - restore_default_bookmarks should restore defaults."); - - remove_all_bookmarks(); - // Sanity check: we should not have any bookmark on the toolbar. - let itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_eq(itemId, -1); - - // Set preferences. - Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); - Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); - - waitForImportAndSmartBookmarks(function () { - // Check bookmarks.html has been restored. - itemId = - PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, - SMART_BOOKMARKS_ON_TOOLBAR); - do_check_true(itemId > 0); - // Check preferences have been reverted. - do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - - run_next_test(); - }); - // Force nsBrowserGlue::_initPlaces() - do_print("Simulate Places init"); - bg.QueryInterface(Ci.nsIObserver).observe(null, - TOPIC_BROWSERGLUE_TEST, - TOPICDATA_FORCE_PLACES_INIT); - } - -].forEach(add_test); - -do_register_cleanup(function () { - remove_all_bookmarks(); - remove_bookmarks_html(); - remove_all_JSON_backups(); -}); - -function run_test() -{ +function run_test() { // Create our bookmarks.html from bookmarks.glue.html. create_bookmarks_html("bookmarks.glue.html"); + remove_all_JSON_backups(); + // Create our JSON backup from bookmarks.glue.json. create_JSON_backup("bookmarks.glue.json"); run_next_test(); } + +do_register_cleanup(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + + return PlacesUtils.bookmarks.eraseEverything(); +}); + +function simulatePlacesInit() { + do_print("Simulate Places init"); + let promise = waitForImportAndSmartBookmarks(); + + // Force nsBrowserGlue::_initPlaces(). + bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT); + return promise; +} + +add_task(function* test_checkPreferences() { + // Initialize Places through the History Service and check that a new + // database has been created. + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_CREATE); + + // Wait for Places init notification. + yield promiseTopicObserved("places-browser-init-complete"); + + // Ensure preferences status. + Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); + + Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); + Assert.throws(() => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); +}); + +add_task(function* test_import() { + do_print("Import from bookmarks.html if importBookmarksHTML is true."); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been imported, and a smart bookmark has been + // created. + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(bm.title, "example"); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); +}); + +add_task(function* test_import_noSmartBookmarks() { + do_print("import from bookmarks.html, but don't create smart bookmarks " + + "if they are disabled"); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1); + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been imported, but smart bookmarks have not + // been created. + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + Assert.equal(bm.title, "example"); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); +}); + +add_task(function* test_import_autoExport_updatedSmartBookmarks() { + do_print("Import from bookmarks.html, but don't create smart bookmarks " + + "if autoExportHTML is true and they are at latest version"); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999); + Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true); + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been imported, but smart bookmarks have not + // been created. + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + Assert.equal(bm.title, "example"); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); + + Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false); +}); + +add_task(function* test_import_autoExport_oldSmartBookmarks() { + do_print("Import from bookmarks.html, and create smart bookmarks if " + + "autoExportHTML is true and they are not at latest version."); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); + Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true); + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been imported, but smart bookmarks have not + // been created. + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(bm.title, "example"); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); + + Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false); +}); + +add_task(function* test_restore() { + do_print("restore from default bookmarks.html if " + + "restore_default_bookmarks is true."); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been restored. + Assert.ok(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + })); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); +}); + +add_task(function* test_restore_import() { + do_print("setting both importBookmarksHTML and " + + "restore_default_bookmarks should restore defaults."); + + yield PlacesUtils.bookmarks.eraseEverything(); + + // Sanity check: we should not have any bookmark on the toolbar. + Assert.ok(!(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }))); + + // Set preferences. + Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true); + Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true); + + yield simulatePlacesInit(); + + // Check bookmarks.html has been restored. + Assert.ok(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + })); + + // Check preferences have been reverted. + Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); + Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); +});
--- a/browser/components/places/tests/unit/test_browserGlue_restore.js +++ b/browser/components/places/tests/unit/test_browserGlue_restore.js @@ -4,79 +4,59 @@ * 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 that nsBrowserGlue correctly restores bookmarks from a JSON backup if * database has been created and one backup is available. */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "bs", - "@mozilla.org/browser/nav-bookmarks-service;1", - "nsINavBookmarksService"); -XPCOMUtils.defineLazyServiceGetter(this, "anno", - "@mozilla.org/browser/annotation-service;1", - "nsIAnnotationService"); - -let bookmarksObserver = { - onBeginUpdateBatch: function() {}, - onEndUpdateBatch: function() { - let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0); - do_check_neq(itemId, -1); - if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark")) - continue_test(); - }, - onItemAdded: function() {}, - onItemRemoved: function(id, folder, index, itemType) {}, - onItemChanged: function() {}, - onItemVisited: function(id, visitID, time) {}, - onItemMoved: function() {}, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) -}; - function run_test() { - do_test_pending(); - - // Create our bookmarks.html copying bookmarks.glue.html to the profile - // folder. It will be ignored. + // Create our bookmarks.html from bookmarks.glue.html. create_bookmarks_html("bookmarks.glue.html"); - // Create our JSON backup copying bookmarks.glue.json to the profile - // folder. It will be ignored. remove_all_JSON_backups(); + + // Create our JSON backup from bookmarks.glue.json. create_JSON_backup("bookmarks.glue.json"); // Remove current database file. - let db = gProfD.clone(); - db.append("places.sqlite"); - if (db.exists()) { - db.remove(false); - do_check_false(db.exists()); - } + clearDB(); + + run_next_test(); +} +do_register_cleanup(function () { + remove_bookmarks_html(); + remove_all_JSON_backups(); + return PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* test_main() { // Initialize nsBrowserGlue before Places. Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue); // Initialize Places through the History Service. let hs = Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService); + // Check a new database has been created. // nsBrowserGlue uses databaseStatus to manage initialization. - do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CREATE); + Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE); // The test will continue once restore has finished and smart bookmarks // have been created. - bs.addObserver(bookmarksObserver, false); -} + yield promiseEndUpdateBatch(); -function continue_test() { + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + // Check that JSON backup has been restored. // Notice restore from JSON notification is fired before smart bookmarks creation. - let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR); - do_check_eq(bs.getItemTitle(itemId), "examplejson"); - - remove_bookmarks_html(); - remove_all_JSON_backups(); - - do_test_finished(); -} + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: SMART_BOOKMARKS_ON_TOOLBAR + }); + Assert.equal(bm.title, "examplejson"); +});
--- a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js +++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js @@ -9,343 +9,329 @@ * by the user or by other components. */ const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion"; const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML"; const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML"; const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks"; -const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; - -/** - * Rebuilds smart bookmarks listening to console output to report any message or - * exception generated when calling ensurePlacesDefaultQueriesInitialized(). - */ -function rebuildSmartBookmarks() { - let consoleListener = { - observe: function(aMsg) { - print("Got console message: " + aMsg.message); - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIConsoleListener - ]), - }; - Services.console.reset(); - Services.console.registerListener(consoleListener); - Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue) - .ensurePlacesDefaultQueriesInitialized(); - Services.console.unregisterListener(consoleListener); +function run_test() { + remove_bookmarks_html(); + remove_all_JSON_backups(); + run_next_test(); } - -let tests = []; -//------------------------------------------------------------------------------ - -tests.push({ - description: "All smart bookmarks are created if smart bookmarks version is 0.", - exec: function() { - // Sanity check: we should have default bookmark. - do_check_neq(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0), -1); - do_check_neq(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0), -1); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); - - rebuildSmartBookmarks(); - - // Count items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "An existing smart bookmark is replaced when version changes.", - exec: function() { - // Sanity check: we have a smart bookmark on the toolbar. - let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_neq(itemId, -1); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - // Change its title. - PlacesUtils.bookmarks.setItemTitle(itemId, "new title"); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "new title"); - - // Sanity check items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); - - rebuildSmartBookmarks(); - - // Count items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check smart bookmark has been replaced, itemId has changed. - itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0); - do_check_neq(itemId, -1); - do_check_neq(PlacesUtils.bookmarks.getItemTitle(itemId), "new title"); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "bookmarks position is retained when version changes.", - exec: function() { - // Sanity check items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId); - - itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); - - rebuildSmartBookmarks(); - - // Count items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check smart bookmarks are still in correct position. - itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), firstItemTitle); - - itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO)); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), secondItemTitle); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "moved bookmarks position is retained when version changes.", - exec: function() { - // Sanity check items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - let itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO)); - let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId1); - - let itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO)); - let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId2); - - // Move the first smart bookmark to the end of the menu. - PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId, - PlacesUtils.bookmarks.DEFAULT_INDEX); - - do_check_eq(itemId1, PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, - PlacesUtils.bookmarks.DEFAULT_INDEX)); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); - - rebuildSmartBookmarks(); - - // Count items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check smart bookmarks are still in correct position. - itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO)); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId2), secondItemTitle); - - itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, - PlacesUtils.bookmarks.DEFAULT_INDEX); - do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO)); - do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId1), firstItemTitle); - - // Move back the smart bookmark to the original position. - PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId, 1); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "An explicitly removed smart bookmark should not be recreated.", - exec: function() { - // Remove toolbar's smart bookmarks - PlacesUtils.bookmarks.removeItem(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0)); - - // Sanity check items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); - - rebuildSmartBookmarks(); - - // Count items. - // We should not have recreated the smart bookmark on toolbar. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); - -//------------------------------------------------------------------------------ - -tests.push({ - description: "Even if a smart bookmark has been removed recreate it if version is 0.", - exec: function() { - // Sanity check items. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Set preferences. - Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); - - rebuildSmartBookmarks(); - - // Count items. - // We should not have recreated the smart bookmark on toolbar. - do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId), - SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); - do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), - SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); - - // Check version has been updated. - do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), - SMART_BOOKMARKS_VERSION); - - next_test(); - } -}); -//------------------------------------------------------------------------------ +do_register_cleanup(() => PlacesUtils.bookmarks.eraseEverything()); function countFolderChildren(aFolderItemId) { let rootNode = PlacesUtils.getFolderContents(aFolderItemId).root; let cc = rootNode.childCount; // Dump contents. for (let i = 0; i < cc ; i++) { let node = rootNode.getChild(i); let title = PlacesUtils.nodeIsSeparator(node) ? "---" : node.title; print("Found child(" + i + "): " + title); } rootNode.containerOpen = false; return cc; } -function next_test() { - if (tests.length) { - // Execute next test. - let test = tests.shift(); - print("\nTEST: " + test.description); - test.exec(); - } - else { - // Clean up database from all bookmarks. - remove_all_bookmarks(); - do_test_finished(); - } +/** + * Rebuilds smart bookmarks listening to console output to report any message or + * exception generated when calling ensurePlacesDefaultQueriesInitialized(). + */ +function rebuildSmartBookmarks() { + let consoleListener = { + observe: function(aMsg) { + do_throw("Got console message: " + aMsg.message); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIConsoleListener + ]), + }; + Services.console.reset(); + Services.console.registerListener(consoleListener); + Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue) + .ensurePlacesDefaultQueriesInitialized(); + Services.console.unregisterListener(consoleListener); } -function run_test() { - do_test_pending(); - - remove_bookmarks_html(); - remove_all_JSON_backups(); - +add_task(function* setup() { // Initialize browserGlue, but remove it's listener to places-init-complete. let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver); + // Initialize Places. PlacesUtils.history; - // Observes Places initialisation complete. - Services.obs.addObserver(function waitPlaceInitComplete() { - Services.obs.removeObserver(waitPlaceInitComplete, "places-browser-init-complete"); + + // Wait for Places init notification. + yield promiseTopicObserved("places-browser-init-complete"); + + // Ensure preferences status. + Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); + Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); + Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); + + yield waitForImportAndSmartBookmarks(); +}); + +add_task(function* test_version_0() { + do_print("All smart bookmarks are created if smart bookmarks version is 0."); + + // Sanity check: we should have default bookmark. + Assert.ok(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + })); + + Assert.ok(yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + })); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); + + rebuildSmartBookmarks(); + + // Count items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +}); + +add_task(function* test_version_change() { + do_print("An existing smart bookmark is replaced when version changes."); + + // Sanity check: we have a smart bookmark on the toolbar. + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + + // Change its title. + yield PlacesUtils.bookmarks.update({guid: bm.guid, title: "new title"}); + bm = yield PlacesUtils.bookmarks.fetch({guid: bm.guid}); + Assert.equal(bm.title, "new title"); + + // Sanity check items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); + + rebuildSmartBookmarks(); - // Ensure preferences status. - do_check_false(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); - do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); - try { - do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - do_throw("importBookmarksHTML pref should not exist"); - } - catch(ex) {} + // Count items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check smart bookmark has been replaced, itemId has changed. + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + Assert.notEqual(bm.title, "new title"); + + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +}); + +add_task(function* test_version_change_pos() { + do_print("bookmarks position is retained when version changes."); + + // Sanity check items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + let firstItemTitle = bm.title; + + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + let secondItemTitle = bm.title; + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); + + rebuildSmartBookmarks(); + + // Count items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check smart bookmarks are still in correct position. + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + Assert.equal(bm.title, firstItemTitle); + + bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1 + }); + yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO); + Assert.equal(bm.title, secondItemTitle); - waitForImportAndSmartBookmarks(next_test); - }, "places-browser-init-complete", false); + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +}); + +add_task(function* test_version_change_pos_moved() { + do_print("moved bookmarks position is retained when version changes."); + + // Sanity check items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + let bm1 = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO); + let firstItemTitle = bm1.title; + + let bm2 = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 1 + }); + yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO); + let secondItemTitle = bm2.title; - // Usually places init would async notify to glue, but we want to avoid - // randomness here, thus we fire the notification synchronously. - bg.observe(null, "places-init-complete", null); -} + // Move the first smart bookmark to the end of the menu. + yield PlacesUtils.bookmarks.update({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: bm1.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX + }); + + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX + }); + Assert.equal(bm.guid, bm1.guid); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); + + rebuildSmartBookmarks(); + + // Count items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check smart bookmarks are still in correct position. + bm2 = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: 0 + }); + yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO); + Assert.equal(bm2.title, secondItemTitle); + + bm1 = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX + }); + yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO); + Assert.equal(bm1.title, firstItemTitle); -function waitForImportAndSmartBookmarks(aCallback) { - Services.obs.addObserver(function waitImport() { - Services.obs.removeObserver(waitImport, "bookmarks-restore-success"); - // Delay to test eventual smart bookmarks creation. - do_execute_soon(function () { - PlacesTestUtils.promiseAsyncUpdates().then(aCallback); - }); - }, "bookmarks-restore-success", false); -} + // Move back the smart bookmark to the original position. + yield PlacesUtils.bookmarks.update({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + guid: bm1.guid, + index: 1 + }); + + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +}); + +add_task(function* test_recreation() { + do_print("An explicitly removed smart bookmark should not be recreated."); + + // Remove toolbar's smart bookmarks + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0 + }); + yield PlacesUtils.bookmarks.remove(bm.guid); + + // Sanity check items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1); + + rebuildSmartBookmarks(); + + // Count items. + // We should not have recreated the smart bookmark on toolbar. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +}); + +add_task(function* test_recreation_version_0() { + do_print("Even if a smart bookmark has been removed recreate it if version is 0."); + + // Sanity check items. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Set preferences. + Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0); + + rebuildSmartBookmarks(); + + // Count items. + // We should not have recreated the smart bookmark on toolbar. + Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId), + SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR); + Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId), + SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU); + + // Check version has been updated. + Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION), + SMART_BOOKMARKS_VERSION); +});
--- a/browser/components/places/tests/unit/test_leftpane_corruption_handling.js +++ b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js @@ -3,144 +3,154 @@ /* 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 that we build a working leftpane in various corruption situations. */ +function run_test() { + // We want empty roots. + remove_all_bookmarks(); + + // Sanity check. + Assert.ok(!!PlacesUIUtils); + + // Check getters. + gLeftPaneFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId"); + Assert.equal(typeof(gLeftPaneFolderIdGetter.get), "function"); + gAllBookmarksFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "allBookmarksFolderId"); + Assert.equal(typeof(gAllBookmarksFolderIdGetter.get), "function"); + + run_next_test(); +} + +do_register_cleanup(remove_all_bookmarks); + // Used to store the original leftPaneFolderId getter. let gLeftPaneFolderIdGetter; let gAllBookmarksFolderIdGetter; // Used to store the original left Pane status as a JSON string. let gReferenceHierarchy; let gLeftPaneFolderId; -// Third party annotated folder. -let gFolderId; - -// Corruption cases. -let gTests = [ - - function test1() { - print("1. Do nothing, checks test calibration."); - }, - - function test2() { - print("2. Delete the left pane folder."); - PlacesUtils.bookmarks.removeItem(gLeftPaneFolderId); - }, - - function test3() { - print("3. Delete a child of the left pane folder."); - let id = PlacesUtils.bookmarks.getIdForItemAt(gLeftPaneFolderId, 0); - PlacesUtils.bookmarks.removeItem(id); - }, - - function test4() { - print("4. Delete AllBookmarks."); - PlacesUtils.bookmarks.removeItem(PlacesUIUtils.allBookmarksFolderId); - }, - - function test5() { - print("5. Create a duplicated left pane folder."); - let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, - "PlacesRoot", - PlacesUtils.bookmarks.DEFAULT_INDEX); - PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_FOLDER_ANNO, - "PlacesRoot", 0, - PlacesUtils.annotations.EXPIRE_NEVER); - }, - function test6() { - print("6. Create a duplicated left pane query."); - let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, - "AllBookmarks", - PlacesUtils.bookmarks.DEFAULT_INDEX); - PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_QUERY_ANNO, - "AllBookmarks", 0, - PlacesUtils.annotations.EXPIRE_NEVER); - }, - - function test7() { - print("7. Remove the left pane folder annotation."); - PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId, - ORGANIZER_FOLDER_ANNO); - }, - - function test8() { - print("8. Remove a left pane query annotation."); - PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId, - ORGANIZER_QUERY_ANNO); - }, +add_task(function* () { + // Add a third party bogus annotated item. Should not be removed. + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test", + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_FOLDER + }); - function test9() { - print("9. Remove a child of AllBookmarks."); - let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUIUtils.allBookmarksFolderId, 0); - PlacesUtils.bookmarks.removeItem(id); - }, - -]; - -function run_test() { - // We want empty roots. - remove_all_bookmarks(); - - // Sanity check. - do_check_true(!!PlacesUIUtils); - - // Check getters. - gLeftPaneFolderIdGetter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId"); - do_check_eq(typeof(gLeftPaneFolderIdGetter), "function"); - gAllBookmarksFolderIdGetter = PlacesUIUtils.__lookupGetter__("allBookmarksFolderId"); - do_check_eq(typeof(gAllBookmarksFolderIdGetter), "function"); - - // Add a third party bogus annotated item. Should not be removed. - gFolderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, - "test", - PlacesUtils.bookmarks.DEFAULT_INDEX); - PlacesUtils.annotations.setItemAnnotation(gFolderId, ORGANIZER_QUERY_ANNO, + let folderId = yield PlacesUtils.promiseItemId(folder.guid); + PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO, "test", 0, PlacesUtils.annotations.EXPIRE_NEVER); // Create the left pane, and store its current status, it will be used // as reference value. gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId; gReferenceHierarchy = folderIdToHierarchy(gLeftPaneFolderId); - do_test_pending(); - run_next_test(); -} + + while (gTests.length) { + // Run current test. + yield Task.spawn(gTests.shift()); -function run_next_test() { - if (gTests.length) { - // Create corruption. - let test = gTests.shift(); - test(); // Regenerate getters. - PlacesUIUtils.__defineGetter__("leftPaneFolderId", gLeftPaneFolderIdGetter); + Object.defineProperty(PlacesUIUtils, "leftPaneFolderId", gLeftPaneFolderIdGetter); gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId; - PlacesUIUtils.__defineGetter__("allBookmarksFolderId", gAllBookmarksFolderIdGetter); + Object.defineProperty(PlacesUIUtils, "allBookmarksFolderId", gAllBookmarksFolderIdGetter); + // Check the new left pane folder. - Task.spawn(function() { - let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId) - if (gReferenceHierarchy != leftPaneHierarchy) { - do_throw("hierarchies differ!\n" + gReferenceHierarchy + - "\n" + leftPaneHierarchy); - } - do_check_eq(PlacesUtils.bookmarks.getItemTitle(gFolderId), "test"); - // Go to next test. - run_next_test(); + let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId) + Assert.equal(gReferenceHierarchy, leftPaneHierarchy); + + folder = yield PlacesUtils.bookmarks.fetch({guid: folder.guid}); + Assert.equal(folder.title, "test"); + } +}); + +// Corruption cases. +let gTests = [ + + function* test1() { + print("1. Do nothing, checks test calibration."); + }, + + function* test2() { + print("2. Delete the left pane folder."); + let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId); + yield PlacesUtils.bookmarks.remove(guid); + }, + + function* test3() { + print("3. Delete a child of the left pane folder."); + let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId); + let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0}); + yield PlacesUtils.bookmarks.remove(bm.guid); + }, + + function* test4() { + print("4. Delete AllBookmarks."); + let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId); + yield PlacesUtils.bookmarks.remove(guid); + }, + + function* test5() { + print("5. Create a duplicated left pane folder."); + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "PlacesRoot", + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_FOLDER }); + + let folderId = yield PlacesUtils.promiseItemId(folder.guid); + PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_FOLDER_ANNO, + "PlacesRoot", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + }, + + function* test6() { + print("6. Create a duplicated left pane query."); + let folder = yield PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "AllBookmarks", + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + type: PlacesUtils.bookmarks.TYPE_FOLDER + }); + + let folderId = yield PlacesUtils.promiseItemId(folder.guid); + PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO, + "AllBookmarks", 0, + PlacesUtils.annotations.EXPIRE_NEVER); + }, + + function* test7() { + print("7. Remove the left pane folder annotation."); + PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId, + ORGANIZER_FOLDER_ANNO); + }, + + function* test8() { + print("8. Remove a left pane query annotation."); + PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId, + ORGANIZER_QUERY_ANNO); + }, + + function* test9() { + print("9. Remove a child of AllBookmarks."); + let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId); + let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0}); + yield PlacesUtils.bookmarks.remove(bm.guid); } - else { - // All tests finished. - remove_all_bookmarks(); - do_test_finished(); - } -} + +]; /** * Convert a folder item id to a JSON representation of it and its contents. */ function folderIdToHierarchy(aFolderId) { let root = PlacesUtils.getFolderContents(aFolderId).root; let hier = JSON.stringify(hierarchyToObj(root)); root.containerOpen = false;
--- a/browser/components/preferences/in-content/sync.js +++ b/browser/components/preferences/in-content/sync.js @@ -308,16 +308,26 @@ let gSyncPane = { // user is prompted to sign in with the address they previously used. let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null; let elt = document.getElementById("sync-migrate-upgrade-description"); elt.textContent = email ? sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description", [email], 1) : sb.GetStringFromName("needUserLong"); + // The "Learn more" link. + if (!email) { + let learnMoreLink = document.createElement("label"); + learnMoreLink.className = "text-link"; + let { text, href } = fxaMigrator.learnMoreLink; + learnMoreLink.setAttribute("value", text); + learnMoreLink.href = href; + elt.appendChild(learnMoreLink); + } + // The "upgrade" button. let button = document.getElementById("sync-migrate-upgrade"); button.setAttribute("label", sb.GetStringFromName(email ? "signInAfterUpgradeOnOtherDevice.label" : "upgradeToFxA.label")); button.setAttribute("accesskey", sb.GetStringFromName(email
--- a/browser/components/preferences/sync.js +++ b/browser/components/preferences/sync.js @@ -208,16 +208,26 @@ let gSyncPane = { // user is prompted to sign in with the address they previously used. let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null; let elt = document.getElementById("sync-migrate-upgrade-description"); elt.textContent = email ? sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description", [email], 1) : sb.GetStringFromName("needUserLong"); + // The "Learn more" link. + if (!email) { + let learnMoreLink = document.createElement("label"); + learnMoreLink.className = "text-link"; + let { text, href } = fxaMigrator.learnMoreLink; + learnMoreLink.setAttribute("value", text); + learnMoreLink.href = href; + elt.appendChild(learnMoreLink); + } + // The "upgrade" button. let button = document.getElementById("sync-migrate-upgrade"); button.setAttribute("label", sb.GetStringFromName(email ? "signInAfterUpgradeOnOtherDevice.label" : "upgradeToFxA.label")); button.setAttribute("accesskey", sb.GetStringFromName(email
--- a/browser/devtools/animationinspector/animation-panel.js +++ b/browser/devtools/animationinspector/animation-panel.js @@ -133,16 +133,18 @@ EventEmitter.decorate(AnimationsPanel); function PlayerWidget(player, containerEl) { EventEmitter.decorate(this); this.player = player; this.containerEl = containerEl; this.onStateChanged = this.onStateChanged.bind(this); this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this); + + this.metaDataComponent = new PlayerMetaDataHeader(); } PlayerWidget.prototype = { initialize: Task.async(function*() { if (this.initialized) { return; } this.initialized = true; @@ -154,16 +156,17 @@ PlayerWidget.prototype = { destroy: Task.async(function*() { if (this.destroyed) { return; } this.destroyed = true; this.stopTimelineAnimation(); this.stopListeners(); + this.metaDataComponent.destroy(); this.el.remove(); this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null; this.containerEl = this.el = this.player = null; }), startListeners: function() { this.player.on(this.player.AUTO_REFRESH_EVENT, this.onStateChanged); @@ -179,55 +182,18 @@ PlayerWidget.prototype = { let state = this.player.state; this.el = createNode({ attributes: { "class": "player-widget " + state.playState } }); - // Animation header - let titleEl = createNode({ - parent: this.el, - attributes: { - "class": "animation-title" - } - }); - let titleHTML = ""; - - // Name. - if (state.name) { - // Css animations have names. - titleHTML += L10N.getStr("player.animationNameLabel"); - titleHTML += "<strong>" + state.name + "</strong>"; - } else { - // Css transitions don't. - titleHTML += L10N.getStr("player.transitionNameLabel"); - } - - // Duration, delay and iteration count. - titleHTML += "<span class='meta-data'>"; - titleHTML += L10N.getStr("player.animationDurationLabel"); - titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel", - this.getFormattedTime(state.duration)) + "</strong>"; - - if (state.delay) { - titleHTML += L10N.getStr("player.animationDelayLabel"); - titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel", - this.getFormattedTime(state.delay)) + "</strong>"; - } - - if (state.iterationCount !== 1) { - titleHTML += L10N.getStr("player.animationIterationCountLabel"); - let count = state.iterationCount || L10N.getStr("player.infiniteIterationCount"); - titleHTML += "<strong>" + count + "</strong>"; - } - - titleHTML += "</span>"; - titleEl.innerHTML = titleHTML; + this.metaDataComponent.createMarkup(this.el); + this.metaDataComponent.render(state); // Timeline widget. let timelineEl = createNode({ parent: this.el, attributes: { "class": "timeline" } }); @@ -292,28 +258,16 @@ PlayerWidget.prototype = { this.containerEl.appendChild(this.el); // Show the initial time. this.displayTime(state.currentTime); }, /** - * Format time as a string. - * @param {Number} time Defaults to the player's currentTime. - * @return {String} The formatted time, e.g. "10.55" - */ - getFormattedTime: function(time) { - return (time/1000).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }); - }, - - /** * Executed when the playPause button is clicked. * Note that tests may want to call this callback directly rather than * simulating a click on the button since it returns the promise returned by * play and paused. * @return {Promise} */ onPlayPauseBtnClick: function() { if (this.player.state.playState === "running") { @@ -323,17 +277,18 @@ PlayerWidget.prototype = { } }, /** * Whenever a player state update is received. */ onStateChanged: function() { let state = this.player.state; - this.updateWidgetState(state.playState); + this.updateWidgetState(state); + this.metaDataComponent.render(state); switch (state.playState) { case "finished": this.stopTimelineAnimation(); this.displayTime(this.player.state.duration); this.stopListeners(); break; case "running": @@ -349,36 +304,36 @@ PlayerWidget.prototype = { /** * Pause the animation player via this widget. * @return {Promise} Resolves when the player is paused, the button is * switched to the right state, and the timeline animation is stopped. */ pause: function() { // Switch to the right className on the element right away to avoid waiting // for the next state update to change the playPause icon. - this.updateWidgetState("paused"); + this.updateWidgetState({playState: "paused"}); return this.player.pause().then(() => { this.stopTimelineAnimation(); }); }, /** * Play the animation player via this widget. * @return {Promise} Resolves when the player is playing, the button is * switched to the right state, and the timeline animation is started. */ play: function() { // Switch to the right className on the element right away to avoid waiting // for the next state update to change the playPause icon. - this.updateWidgetState("running"); + this.updateWidgetState({playState: "running"}); this.startTimelineAnimation(); return this.player.play(); }, - updateWidgetState: function(playState) { + updateWidgetState: function({playState}) { this.el.className = "player-widget " + playState; }, /** * Make the timeline progress smoothly, even though the currentTime is only * updated at some intervals. This uses a local animation loop. */ startTimelineAnimation: function() { @@ -412,17 +367,17 @@ PlayerWidget.prototype = { // the animation total duration (this may happen due to the local // requestAnimationFrame loop). if (state.iterationCount) { time = Math.min(time, state.iterationCount * state.duration); } // Set the time label value. this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel", - this.getFormattedTime(time)); + L10N.numberWithDecimals(time / 1000, 2)); // Set the timeline slider value. if (!state.iterationCount && time !== state.duration) { time = time % state.duration; } this.currentTimeEl.value = time; }, @@ -433,16 +388,172 @@ PlayerWidget.prototype = { if (this.rafID) { cancelAnimationFrame(this.rafID); this.rafID = null; } } }; /** + * UI component responsible for displaying and updating the player meta-data: + * name, duration, iterations, delay. + * The parent UI component for this should drive its updates by calling + * render(state) whenever it wants the component to update. + */ +function PlayerMetaDataHeader() { + // Store the various state pieces we need to only refresh the UI when things + // change. + this.state = {}; +} + +PlayerMetaDataHeader.prototype = { + createMarkup: function(containerEl) { + // The main title element. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-title" + } + }); + + // Animation name (value hidden by default since transitions don't have names). + this.nameLabel = createNode({ + parent: this.el, + nodeType: "span" + }); + + this.nameValue = createNode({ + parent: this.el, + nodeType: "strong", + attributes: { + "style": "display:none;" + } + }); + + // Animation duration, delay and iteration container. + let metaData = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "meta-data" + } + }); + + // Animation duration. + this.durationLabel = createNode({ + parent: metaData, + nodeType: "span" + }); + this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel"); + + this.durationValue = createNode({ + parent: metaData, + nodeType: "strong" + }); + + // Animation delay (hidden by default since there may not be a delay). + this.delayLabel = createNode({ + parent: metaData, + nodeType: "span", + attributes: { + "style": "display:none;" + } + }); + this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel"); + + this.delayValue = createNode({ + parent: metaData, + nodeType: "strong" + }); + + // Animation iteration count (also hidden by default since we don't display + // single iterations). + this.iterationLabel = createNode({ + parent: metaData, + nodeType: "span", + attributes: { + "style": "display:none;" + } + }); + this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel"); + + this.iterationValue = createNode({ + parent: metaData, + nodeType: "strong", + attributes: { + "style": "display:none;" + } + }); + }, + + destroy: function() { + this.state = null; + this.el.remove(); + this.el = null; + this.nameLabel = this.nameValue = null; + this.durationLabel = this.durationValue = null; + this.delayLabel = this.delayValue = null; + this.iterationLabel = this.iterationValue = null; + }, + + render: function(state) { + // Update the name if needed. + if (state.name !== this.state.name) { + if (state.name) { + // Css animations have names. + this.nameLabel.textContent = L10N.getStr("player.animationNameLabel"); + this.nameValue.style.display = "inline"; + this.nameValue.textContent = state.name; + } else { + // Css transitions don't. + this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel"); + this.nameValue.style.display = "none"; + } + } + + // update the duration value if needed. + if (state.duration !== this.state.duration) { + this.durationValue.textContent = L10N.getFormatStr("player.timeLabel", + L10N.numberWithDecimals(state.duration / 1000, 2)); + } + + // Update the delay if needed. + if (state.delay !== this.state.delay) { + if (state.delay) { + this.delayLabel.style.display = "inline"; + this.delayValue.style.display = "inline"; + this.delayValue.textContent = L10N.getFormatStr("player.timeLabel", + L10N.numberWithDecimals(state.delay / 1000, 2)); + } else { + // Hide the delay elements if there is no delay defined. + this.delayLabel.style.display = "none"; + this.delayValue.style.display = "none"; + } + } + + // Update the iterationCount if needed. + if (state.iterationCount !== this.state.iterationCount) { + if (state.iterationCount !== 1) { + this.iterationLabel.style.display = "inline"; + this.iterationValue.style.display = "inline"; + let count = state.iterationCount || + L10N.getStr("player.infiniteIterationCount"); + this.iterationValue.innerHTML = count; + } else { + // Hide the iteration elements if iteration is 1. + this.iterationLabel.style.display = "none"; + this.iterationValue.style.display = "none"; + } + } + + this.state = state; + } +}; + +/** * DOM node creation helper function. * @param {Object} Options to customize the node to be created. * @return {DOMNode} The newly created node. */ function createNode(options) { let type = options.nodeType || "div"; let node = document.createElement(type);
--- a/browser/devtools/animationinspector/test/browser.ini +++ b/browser/devtools/animationinspector/test/browser.ini @@ -17,8 +17,9 @@ support-files = [browser_animation_playerWidgets_meta_data.js] [browser_animation_playerWidgets_state_after_pause.js] [browser_animation_refresh_when_active.js] [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js] [browser_animation_shows_player_on_valid_node.js] [browser_animation_timeline_animates.js] [browser_animation_timeline_waits_for_delay.js] [browser_animation_ui_updates_when_animation_changes.js] +[browser_animation_ui_updates_when_animation_data_changes.js]
--- a/browser/devtools/animationinspector/test/browser_animation_iterationCount_hidden_by_default.js +++ b/browser/devtools/animationinspector/test/browser_animation_iterationCount_hidden_by_default.js @@ -8,17 +8,27 @@ add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); let {inspector, panel} = yield openAnimationInspector(); info("Selecting a node with an animation that doesn't repeat"); yield selectNode(".long", inspector); let widget = panel.playerWidgets[0]; - let metaDataLabels = widget.el.querySelectorAll(".animation-title .meta-data strong"); - is(metaDataLabels.length, 1, "Only the duration is shown"); + + ok(isNodeVisible(widget.metaDataComponent.durationValue), + "The duration value is shown"); + ok(!isNodeVisible(widget.metaDataComponent.delayValue), + "The delay value is hidden"); + ok(!isNodeVisible(widget.metaDataComponent.iterationValue), + "The iteration count is hidden"); info("Selecting a node with an animation that repeats several times"); yield selectNode(".delayed", inspector); widget = panel.playerWidgets[0]; - let iterationLabel = widget.el.querySelectorAll(".animation-title .meta-data strong")[2]; - is(iterationLabel.textContent, "10", "The iteration is shown"); + + ok(isNodeVisible(widget.metaDataComponent.durationValue), + "The duration value is shown"); + ok(isNodeVisible(widget.metaDataComponent.delayValue), + "The delay value is shown"); + ok(isNodeVisible(widget.metaDataComponent.iterationValue), + "The iteration count is shown"); });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_dont_show_time_after_duration.js +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_dont_show_time_after_duration.js @@ -3,16 +3,18 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test that after the animation has ended, the current time label and timeline // slider don't show values bigger than the animation duration (which would // happen if the local requestAnimationFrame loop didn't stop correctly). +let L10N = new ViewHelpers.L10N(); + add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); let {inspector, panel} = yield openAnimationInspector(); info("Start an animation on the test node"); getNode(".still").classList.add("short"); info("Select the node"); @@ -30,11 +32,11 @@ add_task(function*() { } }; front.on(front.AUTO_REFRESH_EVENT, onStateChanged); yield def.promise; is(widget.currentTimeEl.value, front.state.duration, "The timeline slider has the right value"); is(widget.timeDisplayEl.textContent, - widget.getFormattedTime(front.state.duration) + "s", + L10N.numberWithDecimals(front.state.duration / 1000, 2) + "s", "The timeline slider has the right value"); });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_meta_data.js +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_meta_data.js @@ -21,29 +21,34 @@ add_task(function*() { let nameEl = titleEl.querySelector("strong"); ok(nameEl, "The first <strong> tag was retrieved, it should contain the name"); is(nameEl.textContent, "simple-animation", "The animation name is correct"); let metaDataEl = titleEl.querySelector(".meta-data"); ok(metaDataEl, "The meta-data element exists"); let metaDataEls = metaDataEl.querySelectorAll("strong"); - is(metaDataEls.length, 2, "2 meta-data elements were found"); - is(metaDataEls[0].textContent, "2.00s", + is(metaDataEls.length, 3, "3 meta-data elements were found"); + is(metaDataEls[0].textContent, "2s", "The first meta-data is the duration, and is correct"); + ok(!isNodeVisible(metaDataEls[1]), + "The second meta-data is hidden, since there's no delay on the animation"); info("Select the node with the delayed animation"); yield selectNode(".delayed", inspector); titleEl = panel.playerWidgets[0].el.querySelector(".animation-title"); nameEl = titleEl.querySelector("strong"); is(nameEl.textContent, "simple-animation", "The animation name is correct"); metaDataEls = titleEl.querySelectorAll(".meta-data strong"); is(metaDataEls.length, 3, "3 meta-data elements were found for the delayed animation"); - is(metaDataEls[0].textContent, "3.00s", + is(metaDataEls[0].textContent, "3s", "The first meta-data is the duration, and is correct"); - is(metaDataEls[1].textContent, "60.00s", + ok(isNodeVisible(metaDataEls[0]), "The duration is shown"); + is(metaDataEls[1].textContent, "60s", "The second meta-data is the delay, and is correct"); + ok(isNodeVisible(metaDataEls[1]), "The delay is shown"); is(metaDataEls[2].textContent, "10", "The third meta-data is the iteration count, and is correct"); + ok(isNodeVisible(metaDataEls[2]), "The iteration count is shown"); });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_state_after_pause.js +++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_state_after_pause.js @@ -2,16 +2,18 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test that once an animation is paused and its widget is refreshed, the right // initial time is displayed. +let L10N = new ViewHelpers.L10N(); + add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); let {inspector, panel} = yield openAnimationInspector(); info("Selecting the test node"); yield selectNode(".animated", inspector); info("Pausing the animation by using the widget"); @@ -20,11 +22,12 @@ add_task(function*() { info("Selecting another node and then the same node again to refresh the widget"); yield selectNode(".still", inspector); yield selectNode(".animated", inspector); widget = panel.playerWidgets[0]; ok(widget.el.classList.contains("paused"), "The widget is still in paused mode"); is(widget.timeDisplayEl.textContent, - widget.getFormattedTime(widget.player.state.currentTime) + "s", + L10N.numberWithDecimals(widget.player.state.currentTime / 1000, 2) + "s", "The initial time has been set to the player's"); }); +
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js +++ b/browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js @@ -15,10 +15,10 @@ add_task(function*() { yield selectNode(".delayed", inspector); let widget = panel.playerWidgets[0]; let timeline = widget.currentTimeEl; is(timeline.value, 0, "The timeline is at 0 since the animation hasn't started"); let timeLabel = widget.timeDisplayEl; - is(timeLabel.textContent, "0.00s", "The current time is 0"); + is(timeLabel.textContent, "0s", "The current time is 0"); });
new file mode 100644 --- /dev/null +++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js @@ -0,0 +1,45 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Verify that if the animation's duration, iterations or delay change in +// content, then the widget reflects the changes. + +add_task(function*() { + yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); + let {panel, inspector} = yield openAnimationInspector(); + + info("Select the test node"); + yield selectNode(".animated", inspector); + + info("Get the player widget"); + let widget = panel.playerWidgets[0]; + + yield setStyle(widget, "animationDuration", "5.5s"); + is(widget.metaDataComponent.durationValue.textContent, "5.50s", + "The widget shows the new duration"); + + yield setStyle(widget, "animationIterationCount", "300"); + is(widget.metaDataComponent.iterationValue.textContent, "300", + "The widget shows the new iteration count"); + + yield setStyle(widget, "animationDelay", "45s"); + is(widget.metaDataComponent.delayValue.textContent, "45s", + "The widget shows the new delay"); +}); + +function* setStyle(widget, name, value) { + info("Change the animation style via the content DOM. Setting " + + name + " to " + value); + yield executeInContent("Test:SetNodeStyle", { + propertyName: name, + propertyValue: value + }, { + node: getNode(".animated") + }); + + info("Wait for the next state update"); + yield widget.player.once(widget.player.AUTO_REFRESH_EVENT); +}
--- a/browser/devtools/animationinspector/test/doc_frame_script.js +++ b/browser/devtools/animationinspector/test/doc_frame_script.js @@ -21,8 +21,26 @@ addMessageListener("Test:ToggleAnimation if (pause) { player.pause(); } else { player.play(); } sendAsyncMessage("Test:ToggleAnimationPlayer"); }); + +/** + * Set a given style property value on a node. This is useful to dynamically + * change an animation's duration or delay for instance. + * @param {Object} data + * - {String} propertyName The name of the property to set. + * - {String} propertyValue The value for the property. + * @param {Object} objects + * - {DOMNode} node The node to use + */ +addMessageListener("Test:SetNodeStyle", function(msg) { + let {propertyName, propertyValue} = msg.data; + let {node} = msg.objects; + + node.style[propertyName] = propertyValue; + + sendAsyncMessage("Test:SetNodeStyle"); +});
--- a/browser/devtools/animationinspector/test/doc_simple_animation.html +++ b/browser/devtools/animationinspector/test/doc_simple_animation.html @@ -49,25 +49,25 @@ other-animation 4s; } .short { top: 500px; left: 10px; background: red; - animation: simple-animation 2s + animation: simple-animation 2s; } .long { top: 600px; left: 10px; background: blue; - animation: simple-animation 120s + animation: simple-animation 120s; } @keyframes simple-animation { 100% { transform: translateX(300px); } }
--- a/browser/devtools/animationinspector/test/head.js +++ b/browser/devtools/animationinspector/test/head.js @@ -1,20 +1,21 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const Cu = Components.utils; -let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); -let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); -let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); -let TargetFactory = devtools.TargetFactory; -let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const TargetFactory = devtools.TargetFactory; +const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); +const {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {}); // All tests are asynchronous waitForExplicitFinish(); const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/"; const ROOT_TEST_DIR = getRootDirectory(gTestPath); const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js"; @@ -258,17 +259,35 @@ function executeInContent(name, data={}, return promise.resolve(); } } /** * Simulate a click on the playPause button of a playerWidget. */ let togglePlayPauseButton = Task.async(function*(widget) { + let nextState = widget.player.state.playState === "running" ? "paused" : "running"; + // Note that instead of simulating a real event here, the callback is just // called. This is better because the callback returns a promise, so we know // when the player is paused, and we don't really care to test that simulating // a DOM event actually works. - yield widget.onPlayPauseBtnClick(); + let onClicked = widget.onPlayPauseBtnClick(); + + // Verify that the button's state is changed immediately, even if it will be + // changed anyway with the next auto-refresh. + ok(widget.el.classList.contains(nextState), + "The button's state was changed in the UI before the request was sent"); + + yield onClicked; // Wait for the next sate change event to make sure the state is updated yield widget.player.once(widget.player.AUTO_REFRESH_EVENT); }); + +/** + * Is the given node visible in the page (rendered in the frame tree). + * @param {DOMNode} + * @return {Boolean} + */ +function isNodeVisible(node) { + return !!node.getClientRects().length; +}
--- a/browser/devtools/performance/performance-view.js +++ b/browser/devtools/performance/performance-view.js @@ -2,77 +2,143 @@ * 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"; /** * Master view handler for the performance tool. */ let PerformanceView = { + + _state: null, + + // Mapping of state to selectors for different panes + // of the main profiler view. Used in `PerformanceView.setState()` + states: { + empty: [ + { deck: "#performance-view", pane: "#empty-notice" } + ], + recording: [ + { deck: "#performance-view", pane: "#performance-view-content" }, + { deck: "#details-pane-container", pane: "#recording-notice" } + ], + recorded: [ + { deck: "#performance-view", pane: "#performance-view-content" }, + { deck: "#details-pane-container", pane: "#details-pane" } + ] + }, + /** * Sets up the view with event binding and main subviews. */ initialize: function () { this._recordButton = $("#record-button"); this._importButton = $("#import-button"); this._onRecordButtonClick = this._onRecordButtonClick.bind(this); this._onImportButtonClick = this._onImportButtonClick.bind(this); this._lockRecordButton = this._lockRecordButton.bind(this); this._unlockRecordButton = this._unlockRecordButton.bind(this); + this._onRecordingSelected = this._onRecordingSelected.bind(this); + this._onRecordingStopped = this._onRecordingStopped.bind(this); - this._recordButton.addEventListener("click", this._onRecordButtonClick); + for (let button of $$(".record-button")) { + button.addEventListener("click", this._onRecordButtonClick); + } this._importButton.addEventListener("click", this._onImportButtonClick); // Bind to controller events to unlock the record button PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton); - PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton); + PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped); + PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected); + + this.setState("empty"); return promise.all([ RecordingsView.initialize(), OverviewView.initialize(), ToolbarView.initialize(), DetailsView.initialize() ]); }, /** * Unbinds events and destroys subviews. */ destroy: function () { - this._recordButton.removeEventListener("click", this._onRecordButtonClick); + for (let button of $$(".record-button")) { + button.removeEventListener("click", this._onRecordButtonClick); + } this._importButton.removeEventListener("click", this._onImportButtonClick); PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton); - PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton); + PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped); + PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected); return promise.all([ RecordingsView.destroy(), OverviewView.destroy(), ToolbarView.destroy(), DetailsView.destroy() ]); }, /** + * Sets the state of the profiler view. Possible options are "empty", + * "recording", "recorded". + */ + setState: function (state) { + let viewConfig = this.states[state]; + if (!viewConfig) { + throw new Error(`Invalid state for PerformanceView: ${state}`); + } + for (let { deck, pane } of viewConfig) { + $(deck).selectedPanel = $(pane); + } + + this._state = state; + }, + + /** + * Returns the state of the PerformanceView. + */ + getState: function () { + return this._state; + }, + + /** * Adds the `locked` attribute on the record button. This prevents it * from being clicked while recording is started or stopped. */ _lockRecordButton: function () { this._recordButton.setAttribute("locked", "true"); }, /** * Removes the `locked` attribute on the record button. */ _unlockRecordButton: function () { this._recordButton.removeAttribute("locked"); }, /** + * When a recording is complete. + */ + _onRecordingStopped: function (_, recording) { + this._unlockRecordButton(); + + // If this recording stopped is the current recording, set the + // state to "recorded". A stopped recording doesn't necessarily + // have to be the current recording (console.profileEnd, for example) + if (recording === PerformanceController.getCurrentRecording()) { + this.setState("recorded"); + } + }, + + /** * Handler for clicking the record button. */ _onRecordButtonClick: function (e) { if (this._recordButton.hasAttribute("checked")) { this._recordButton.removeAttribute("checked"); this._lockRecordButton(); this.emit(EVENTS.UI_STOP_RECORDING); } else { @@ -89,15 +155,26 @@ let PerformanceView = { let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen); fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json"); fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*"); if (fp.show() == Ci.nsIFilePicker.returnOK) { this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file); } + }, + + /** + * Fired when a recording is selected. Used to toggle the profiler view state. + */ + _onRecordingSelected: function (_, recording) { + if (recording.isRecording()) { + this.setState("recording"); + } else { + this.setState("recorded"); + } } }; /** * Convenient way of emitting events from the view. */ EventEmitter.decorate(PerformanceView);
--- a/browser/devtools/performance/performance.xul +++ b/browser/devtools/performance/performance.xul @@ -60,17 +60,17 @@ <hbox class="theme-body" flex="1"> <vbox id="recordings-pane"> <toolbar id="recordings-toolbar" class="devtools-toolbar"> <hbox id="recordings-controls" class="devtools-toolbarbutton-group"> <toolbarbutton id="record-button" - class="devtools-toolbarbutton" + class="devtools-toolbarbutton record-button" tooltiptext="&profilerUI.recordButton.tooltip;"/> <toolbarbutton id="import-button" class="devtools-toolbarbutton" label="&profilerUI.importButton;"/> <toolbarbutton id="clear-button" class="devtools-toolbarbutton" label="&profilerUI.clearButton;"/> </hbox> @@ -106,103 +106,127 @@ <hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group"> <toolbarbutton id="performance-options-button" class="devtools-toolbarbutton devtools-option-toolbarbutton" popup="performance-options-menupopup" tooltiptext="&profilerUI.options.tooltiptext;"/> </hbox> </toolbar> - <vbox id="overview-pane"> - <hbox id="markers-overview"/> - <hbox id="memory-overview"/> - <hbox id="time-framerate"/> - </vbox> - - <deck id="details-pane" flex="1"> - <hbox id="waterfall-view" flex="1"> - <vbox id="waterfall-breakdown" flex="1" /> - <splitter class="devtools-side-splitter"/> - <vbox id="waterfall-details" - class="theme-sidebar" - width="150" - height="150"/> + <deck id="performance-view" flex="1"> + <hbox id="empty-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <label value="&profilerUI.emptyNotice1;"/> + <button class="devtools-toolbarbutton record-button" + standalone="true" /> + <label value="&profilerUI.emptyNotice2;"/> </hbox> - - <vbox id="js-calltree-view" flex="1"> - <hbox class="call-tree-headers-container"> - <label class="plain call-tree-header" - type="duration" - crop="end" - value="&profilerUI.table.totalDuration2;"/> - <label class="plain call-tree-header" - type="percentage" - crop="end" - value="&profilerUI.table.totalPercentage;"/> - <label class="plain call-tree-header" - type="self-duration" - crop="end" - value="&profilerUI.table.selfDuration2;"/> - <label class="plain call-tree-header" - type="self-percentage" - crop="end" - value="&profilerUI.table.selfPercentage;"/> - <label class="plain call-tree-header" - type="samples" - crop="end" - value="&profilerUI.table.samples;"/> - <label class="plain call-tree-header" - type="function" - crop="end" - value="&profilerUI.table.function;"/> - </hbox> - <vbox class="call-tree-cells-container" flex="1"/> - </vbox> + <vbox id="performance-view-content" flex="1"> + <vbox id="overview-pane"> + <hbox id="markers-overview"/> + <hbox id="memory-overview"/> + <hbox id="time-framerate"/> + </vbox> + <deck id="details-pane-container" flex="1"> + <hbox id="recording-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <label value="&profilerUI.stopNotice1;"/> + <button class="devtools-toolbarbutton record-button" + standalone="true" + checked="true" /> + <label value="&profilerUI.stopNotice2;"/> + </hbox> + <deck id="details-pane" flex="1"> + <hbox id="waterfall-view" flex="1"> + <vbox id="waterfall-breakdown" flex="1" /> + <splitter class="devtools-side-splitter"/> + <vbox id="waterfall-details" + class="theme-sidebar" + width="150" + height="150"/> + </hbox> - <hbox id="js-flamegraph-view" flex="1"> - </hbox> + <vbox id="js-calltree-view" flex="1"> + <hbox class="call-tree-headers-container"> + <label class="plain call-tree-header" + type="duration" + crop="end" + value="&profilerUI.table.totalDuration2;"/> + <label class="plain call-tree-header" + type="percentage" + crop="end" + value="&profilerUI.table.totalPercentage;"/> + <label class="plain call-tree-header" + type="self-duration" + crop="end" + value="&profilerUI.table.selfDuration2;"/> + <label class="plain call-tree-header" + type="self-percentage" + crop="end" + value="&profilerUI.table.selfPercentage;"/> + <label class="plain call-tree-header" + type="samples" + crop="end" + value="&profilerUI.table.samples;"/> + <label class="plain call-tree-header" + type="function" + crop="end" + value="&profilerUI.table.function;"/> + </hbox> + <vbox class="call-tree-cells-container" flex="1"/> + </vbox> + + <hbox id="js-flamegraph-view" flex="1"> + </hbox> - <vbox id="memory-calltree-view" flex="1"> - <hbox class="call-tree-headers-container"> - <label class="plain call-tree-header" - type="duration" - crop="end" - value="&profilerUI.table.totalDuration2;"/> - <label class="plain call-tree-header" - type="percentage" - crop="end" - value="&profilerUI.table.totalPercentage;"/> - <label class="plain call-tree-header" - type="allocations" - crop="end" - value="&profilerUI.table.totalAlloc;"/> - <label class="plain call-tree-header" - type="self-duration" - crop="end" - value="&profilerUI.table.selfDuration2;"/> - <label class="plain call-tree-header" - type="self-percentage" - crop="end" - value="&profilerUI.table.selfPercentage;"/> - <label class="plain call-tree-header" - type="self-allocations" - crop="end" - value="&profilerUI.table.selfAlloc;"/> - <label class="plain call-tree-header" - type="samples" - crop="end" - value="&profilerUI.table.samples;"/> - <label class="plain call-tree-header" - type="function" - crop="end" - value="&profilerUI.table.function;"/> - </hbox> - <vbox class="call-tree-cells-container" flex="1"/> + <vbox id="memory-calltree-view" flex="1"> + <hbox class="call-tree-headers-container"> + <label class="plain call-tree-header" + type="duration" + crop="end" + value="&profilerUI.table.totalDuration2;"/> + <label class="plain call-tree-header" + type="percentage" + crop="end" + value="&profilerUI.table.totalPercentage;"/> + <label class="plain call-tree-header" + type="allocations" + crop="end" + value="&profilerUI.table.totalAlloc;"/> + <label class="plain call-tree-header" + type="self-duration" + crop="end" + value="&profilerUI.table.selfDuration2;"/> + <label class="plain call-tree-header" + type="self-percentage" + crop="end" + value="&profilerUI.table.selfPercentage;"/> + <label class="plain call-tree-header" + type="self-allocations" + crop="end" + value="&profilerUI.table.selfAlloc;"/> + <label class="plain call-tree-header" + type="samples" + crop="end" + value="&profilerUI.table.samples;"/> + <label class="plain call-tree-header" + type="function" + crop="end" + value="&profilerUI.table.function;"/> + </hbox> + <vbox class="call-tree-cells-container" flex="1"/> + </vbox> + + <hbox id="memory-flamegraph-view" flex="1"> + </hbox> + </deck> + </deck> </vbox> - - <hbox id="memory-flamegraph-view" flex="1"> - <!-- TODO: bug 1077461 --> - </hbox> </deck> - </vbox> </hbox> </window>
--- a/browser/devtools/performance/test/browser.ini +++ b/browser/devtools/performance/test/browser.ini @@ -47,16 +47,18 @@ support-files = [browser_perf-overview-render-03.js] [browser_perf-overview-selection-01.js] [browser_perf-overview-selection-02.js] [browser_perf-overview-selection-03.js] [browser_perf-overview-time-interval.js] [browser_perf-shared-connection-02.js] [browser_perf-shared-connection-03.js] [browser_perf-ui-recording.js] +[browser_perf-recording-notices-01.js] +[browser_perf-recording-notices-02.js] [browser_perf_recordings-io-01.js] [browser_perf_recordings-io-02.js] [browser_perf_recordings-io-03.js] [browser_perf_recordings-io-04.js] [browser_perf-range-changed-render.js] [browser_perf-recording-selected-01.js] [browser_perf-recording-selected-02.js] [browser_perf-recording-selected-03.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/performance/test/browser_perf-recording-notices-01.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the recording notice panes are toggled in correct scenarios + * for initialization and a single recording. + */ +function spawnTest () { + let { panel } = yield initPerformance(SIMPLE_URL); + let { EVENTS, $, PerformanceView, RecordingsView } = panel.panelWin; + + let MAIN_CONTAINER = $("#performance-view"); + let EMPTY = $("#empty-notice"); + let CONTENT = $("#performance-view-content"); + let CONTENT_CONTAINER = $("#details-pane-container"); + let RECORDING = $("#recording-notice"); + let DETAILS = $("#details-pane"); + + is(PerformanceView.getState(), "empty", "correct default state"); + is(MAIN_CONTAINER.selectedPanel, EMPTY, "showing empty panel on load"); + + yield startRecording(panel); + + is(PerformanceView.getState(), "recording", "correct state during recording"); + is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline"); + is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel"); + + yield stopRecording(panel); + + is(PerformanceView.getState(), "recorded", "correct state after recording"); + is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline"); + is(CONTENT_CONTAINER.selectedPanel, DETAILS, "showing rendered graphs"); + + yield teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/performance/test/browser_perf-recording-notices-02.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the recording notice panes are toggled when going between + * a completed recording and an in-progress recording. + */ +function spawnTest () { + let { panel } = yield initPerformance(SIMPLE_URL); + let { EVENTS, $, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin; + + let MAIN_CONTAINER = $("#performance-view"); + let CONTENT = $("#performance-view-content"); + let CONTENT_CONTAINER = $("#details-pane-container"); + let RECORDING = $("#recording-notice"); + let DETAILS = $("#details-pane"); + + yield startRecording(panel); + yield stopRecording(panel); + + yield startRecording(panel); + + is(PerformanceView.getState(), "recording", "correct state during recording"); + is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline"); + is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel"); + + let select = once(PerformanceController, EVENTS.RECORDING_SELECTED); + RecordingsView.selectedIndex = 0; + yield select; + + is(PerformanceView.getState(), "recorded", "correct state during recording but selecting a completed recording"); + is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline"); + is(CONTENT_CONTAINER.selectedPanel, DETAILS, "showing recorded panel"); + + select = once(PerformanceController, EVENTS.RECORDING_SELECTED); + RecordingsView.selectedIndex = 1; + yield select; + + is(PerformanceView.getState(), "recording", "correct state when switching back to recording in progress"); + is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline"); + is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel"); + + yield stopRecording(panel); + + yield teardown(panel); + finish(); +}
--- a/browser/themes/shared/devtools/performance.inc.css +++ b/browser/themes/shared/devtools/performance.inc.css @@ -29,20 +29,39 @@ min-width: 0; } #performance-toolbar-controls-detail-views .toolbarbutton-text { -moz-padding-start: 4px; -moz-padding-end: 8px; } +/* Recording Notice */ + +#performance-view .notice-container { + font-size: 120%; + background-color: var(--theme-toolbar-background); + color: var(--theme-body-color); + padding-bottom: 20vh; +} + +#performance-view .notice-container button { + min-width: 30px; + min-height: 28px; + margin: 0; +} + +/* Overview Panel */ + +.notice-container button, #record-button { list-style-image: url(profiler-stopwatch.svg); } +.notice-container button[checked], #record-button[checked] { list-style-image: url(profiler-stopwatch-checked.svg); } #record-button[locked] { pointer-events: none; }
--- a/browser/themes/shared/devtools/toolbars.inc.css +++ b/browser/themes/shared/devtools/toolbars.inc.css @@ -886,32 +886,32 @@ .theme-light .command-button-invertable:active > image, .theme-light .devtools-closebutton > image, .theme-light .devtools-toolbarbutton > image, .theme-light .devtools-option-toolbarbutton > image, .theme-light #breadcrumb-separator-normal, .theme-light .scrollbutton-up > .toolbarbutton-icon, .theme-light .scrollbutton-down > .toolbarbutton-icon, .theme-light #black-boxed-message-button .button-icon, -.theme-light #profiling-notice-button .button-icon, -.theme-light #canvas-debugging-empty-notice-button .button-icon, +.theme-light .notice-container button .button-icon, .theme-light #requests-menu-perf-notice-button .button-icon, .theme-light #requests-menu-network-summary-button .button-icon, .theme-light .event-tooltip-debugger-icon, .theme-light .devtools-button::before { filter: url(filters.svg#invert); } /* Since selected backgrounds are blue, we want to use the normal * (light) icons. */ .theme-light .command-button-invertable[checked=true]:not(:active) > image, .theme-light .devtools-tab[icon-invertable][selected] > image, .theme-light .devtools-tab[icon-invertable][highlighted] > image, .theme-light #record-snapshot[checked] > image, -.theme-light #profiler-start[checked] > image { +.theme-light #profiler-start[checked] > image, +.theme-light .notice-container button[checked] .button-icon { filter: none !important; } .theme-light .command-button:hover { background-color: inherit; } .theme-light .command-button:hover:active, @@ -953,9 +953,9 @@ @keyframes throbber-spin { from { transform: none; } to { transform: rotate(360deg); } -} \ No newline at end of file +}
--- a/mobile/android/base/ReadingListHelper.java +++ b/mobile/android/base/ReadingListHelper.java @@ -5,139 +5,126 @@ package org.mozilla.gecko; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.db.BrowserContract.ReadingListItems; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeJSObject; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UIAsyncTask; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.util.Log; import android.widget.Toast; -public final class ReadingListHelper implements GeckoEventListener, NativeEventListener { +public final class ReadingListHelper implements NativeEventListener { private static final String LOGTAG = "ReadingListHelper"; protected final Context context; public ReadingListHelper(Context context) { this.context = context; - EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this, - "Reader:AddToList", "Reader:FaviconRequest"); EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this, - "Reader:ListStatusRequest", "Reader:RemoveFromList"); + "Reader:AddToList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList"); } public void uninit() { - EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this, - "Reader:AddToList", "Reader:FaviconRequest"); EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this, - "Reader:ListStatusRequest", "Reader:RemoveFromList"); - } - - @Override - public void handleMessage(String event, JSONObject message) { - switch(event) { - case "Reader:AddToList": { - handleAddToList(message); - break; - } - - case "Reader:FaviconRequest": { - handleReaderModeFaviconRequest(message.optString("url")); - break; - } - } + "Reader:AddToList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList"); } @Override public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { switch(event) { + case "Reader:AddToList": { + handleAddToList(callback, message); + break; + } + case "Reader:FaviconRequest": { + handleReaderModeFaviconRequest(callback, message.getString("url")); + break; + } case "Reader:RemoveFromList": { handleRemoveFromList(message.getString("url")); break; } - case "Reader:ListStatusRequest": { handleReadingListStatusRequest(callback, message.getString("url")); break; } } } /** * A page can be added to the ReadingList by long-tap of the page-action * icon, or by tapping the readinglist-add icon in the ReaderMode banner. + * + * This method will only add new items, not update existing items. */ - private void handleAddToList(final JSONObject message) { + private void handleAddToList(final EventCallback callback, final NativeJSObject message) { final ContentResolver cr = context.getContentResolver(); - final String url = message.optString("url"); + final String url = message.getString("url"); + + // We can't access a NativeJSObject from the background thread, so we need to get the + // values here, even if we may not use them to insert an item into the DB. + final ContentValues values = new ContentValues(); + values.put(ReadingListItems.URL, url); + values.put(ReadingListItems.TITLE, message.getString("title")); + values.put(ReadingListItems.LENGTH, message.getInt("length")); + values.put(ReadingListItems.EXCERPT, message.getString("excerpt")); + values.put(ReadingListItems.CONTENT_STATUS, message.getInt("status")); final BrowserDB db = GeckoProfile.get(context).getDB(); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { if (db.isReadingListItem(cr, url)) { showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT); - + callback.sendError("URL already in reading list: " + url); } else { - final ContentValues values = new ContentValues(); - values.put(ReadingListItems.URL, url); - values.put(ReadingListItems.TITLE, message.optString("title")); - values.put(ReadingListItems.LENGTH, message.optInt("length")); - values.put(ReadingListItems.EXCERPT, message.optString("excerpt")); - values.put(ReadingListItems.CONTENT_STATUS, message.optInt("status")); db.addReadingListItem(cr, values); - showToast(R.string.reading_list_added, Toast.LENGTH_SHORT); + callback.sendSuccess(url); } } }); - - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Added", url)); } /** * Gecko (ReaderMode) requests the page favicon to append to the * document head for display. */ - private void handleReaderModeFaviconRequest(final String url) { + private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) { final BrowserDB db = GeckoProfile.get(context).getDB(); (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) { @Override public String doInBackground() { return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url); } @Override public void onPostExecute(String faviconUrl) { JSONObject args = new JSONObject(); - if (faviconUrl != null) { try { args.put("url", url); args.put("faviconUrl", faviconUrl); } catch (JSONException e) { Log.w(LOGTAG, "Error building JSON favicon arguments.", e); } } - - GeckoAppShell.sendEventToGecko( - GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString())); + callback.sendSuccess(args.toString()); } }).execute(); } /** * A page can be removed from the ReadingList by panel context menu, * or by tapping the readinglist-remove icon in the ReaderMode banner. */
--- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -765,27 +765,29 @@ sync_thirdparty_java_files = [ sync_java_files = [ 'background/BackgroundService.java', 'background/bagheera/BagheeraClient.java', 'background/bagheera/BagheeraRequestDelegate.java', 'background/bagheera/BoundedByteArrayEntity.java', 'background/bagheera/DeflateHelper.java', 'background/common/DateUtils.java', + 'background/common/EditorBranch.java', 'background/common/GlobalConstants.java', 'background/common/log/Logger.java', 'background/common/log/writers/AndroidLevelCachingLogWriter.java', 'background/common/log/writers/AndroidLogWriter.java', 'background/common/log/writers/LevelFilteringLogWriter.java', 'background/common/log/writers/LogWriter.java', 'background/common/log/writers/PrintLogWriter.java', 'background/common/log/writers/SimpleTagLogWriter.java', 'background/common/log/writers/StringLogWriter.java', 'background/common/log/writers/TagLogWriter.java', 'background/common/log/writers/ThreadLocalTagLogWriter.java', + 'background/common/PrefsBranch.java', 'background/common/telemetry/TelemetryWrapper.java', 'background/datareporting/TelemetryRecorder.java', 'background/db/CursorDumper.java', 'background/db/Tab.java', 'background/fxa/FxAccount10AuthDelegate.java', 'background/fxa/FxAccount10CreateDelegate.java', 'background/fxa/FxAccount20CreateDelegate.java', 'background/fxa/FxAccount20LoginDelegate.java',
new file mode 100644 --- /dev/null +++ b/mobile/android/base/background/common/EditorBranch.java @@ -0,0 +1,82 @@ +/* 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/. */ + +package org.mozilla.gecko.background.common; + +import java.util.Set; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +public class EditorBranch implements Editor { + + private final String prefix; + private Editor editor; + + public EditorBranch(final SharedPreferences prefs, final String prefix) { + if (!prefix.endsWith(".")) { + throw new IllegalArgumentException("No trailing period in prefix."); + } + this.prefix = prefix; + this.editor = prefs.edit(); + } + + public void apply() { + // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding. + this.editor.commit(); + } + + @Override + public Editor clear() { + this.editor = this.editor.clear(); + return this; + } + + @Override + public boolean commit() { + return this.editor.commit(); + } + + @Override + public Editor putBoolean(String key, boolean value) { + this.editor = this.editor.putBoolean(prefix + key, value); + return this; + } + + @Override + public Editor putFloat(String key, float value) { + this.editor = this.editor.putFloat(prefix + key, value); + return this; + } + + @Override + public Editor putInt(String key, int value) { + this.editor = this.editor.putInt(prefix + key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + this.editor = this.editor.putLong(prefix + key, value); + return this; + } + + @Override + public Editor putString(String key, String value) { + this.editor = this.editor.putString(prefix + key, value); + return this; + } + + // Not marking as Override, because Android <= 10 doesn't have + // putStringSet. Neither can we implement it. + public Editor putStringSet(String key, Set<String> value) { + throw new RuntimeException("putStringSet not available."); + } + + @Override + public Editor remove(String key) { + this.editor = this.editor.remove(prefix + key); + return this; + } +} \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/mobile/android/base/background/common/PrefsBranch.java @@ -0,0 +1,83 @@ +/* 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/. */ + +package org.mozilla.gecko.background.common; + +import java.util.Map; +import java.util.Set; + +import android.content.SharedPreferences; + +/** + * A wrapper around a portion of the SharedPreferences space. + */ +public class PrefsBranch implements SharedPreferences { + private final SharedPreferences prefs; + private final String prefix; // Including trailing period. + + public PrefsBranch(SharedPreferences prefs, String prefix) { + if (!prefix.endsWith(".")) { + throw new IllegalArgumentException("No trailing period in prefix."); + } + this.prefs = prefs; + this.prefix = prefix; + } + + @Override + public boolean contains(String key) { + return prefs.contains(prefix + key); + } + + @Override + public Editor edit() { + return new EditorBranch(prefs, prefix); + } + + @Override + public Map<String, ?> getAll() { + // Not implemented. TODO + return null; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return prefs.getBoolean(prefix + key, defValue); + } + + @Override + public float getFloat(String key, float defValue) { + return prefs.getFloat(prefix + key, defValue); + } + + @Override + public int getInt(String key, int defValue) { + return prefs.getInt(prefix + key, defValue); + } + + @Override + public long getLong(String key, long defValue) { + return prefs.getLong(prefix + key, defValue); + } + + @Override + public String getString(String key, String defValue) { + return prefs.getString(prefix + key, defValue); + } + + // Not marking as Override, because Android <= 10 doesn't have + // getStringSet. Neither can we implement it. + public Set<String> getStringSet(String key, Set<String> defValue) { + throw new RuntimeException("getStringSet not available."); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + prefs.registerOnSharedPreferenceChangeListener(listener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + prefs.unregisterOnSharedPreferenceChangeListener(listener); + } +} \ No newline at end of file
--- a/mobile/android/base/sync/SyncConfiguration.java +++ b/mobile/android/base/sync/SyncConfiguration.java @@ -8,178 +8,27 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.mozilla.gecko.background.common.PrefsBranch; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; import org.mozilla.gecko.sync.net.AuthHeaderProvider; import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; public class SyncConfiguration { - - public class EditorBranch implements Editor { - - private final String prefix; - private Editor editor; - - public EditorBranch(SyncConfiguration config, String prefix) { - if (!prefix.endsWith(".")) { - throw new IllegalArgumentException("No trailing period in prefix."); - } - this.prefix = prefix; - this.editor = config.getEditor(); - } - - public void apply() { - // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding. - this.editor.commit(); - } - - @Override - public Editor clear() { - this.editor = this.editor.clear(); - return this; - } - - @Override - public boolean commit() { - return this.editor.commit(); - } - - @Override - public Editor putBoolean(String key, boolean value) { - this.editor = this.editor.putBoolean(prefix + key, value); - return this; - } - - @Override - public Editor putFloat(String key, float value) { - this.editor = this.editor.putFloat(prefix + key, value); - return this; - } - - @Override - public Editor putInt(String key, int value) { - this.editor = this.editor.putInt(prefix + key, value); - return this; - } - - @Override - public Editor putLong(String key, long value) { - this.editor = this.editor.putLong(prefix + key, value); - return this; - } - - @Override - public Editor putString(String key, String value) { - this.editor = this.editor.putString(prefix + key, value); - return this; - } - - // Not marking as Override, because Android <= 10 doesn't have - // putStringSet. Neither can we implement it. - public Editor putStringSet(String key, Set<String> value) { - throw new RuntimeException("putStringSet not available."); - } - - @Override - public Editor remove(String key) { - this.editor = this.editor.remove(prefix + key); - return this; - } - - } - - /** - * A wrapper around a portion of the SharedPreferences space. - * - * @author rnewman - * - */ - public class ConfigurationBranch implements SharedPreferences { - - private final SyncConfiguration config; - private final String prefix; // Including trailing period. - - public ConfigurationBranch(SyncConfiguration syncConfiguration, - String prefix) { - if (!prefix.endsWith(".")) { - throw new IllegalArgumentException("No trailing period in prefix."); - } - this.config = syncConfiguration; - this.prefix = prefix; - } - - @Override - public boolean contains(String key) { - return config.getPrefs().contains(prefix + key); - } - - @Override - public Editor edit() { - return new EditorBranch(config, prefix); - } - - @Override - public Map<String, ?> getAll() { - // Not implemented. TODO - return null; - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - return config.getPrefs().getBoolean(prefix + key, defValue); - } - - @Override - public float getFloat(String key, float defValue) { - return config.getPrefs().getFloat(prefix + key, defValue); - } - - @Override - public int getInt(String key, int defValue) { - return config.getPrefs().getInt(prefix + key, defValue); - } - - @Override - public long getLong(String key, long defValue) { - return config.getPrefs().getLong(prefix + key, defValue); - } - - @Override - public String getString(String key, String defValue) { - return config.getPrefs().getString(prefix + key, defValue); - } - - // Not marking as Override, because Android <= 10 doesn't have - // getStringSet. Neither can we implement it. - public Set<String> getStringSet(String key, Set<String> defValue) { - throw new RuntimeException("getStringSet not available."); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - config.getPrefs().registerOnSharedPreferenceChangeListener(listener); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener); - } - } - private static final String LOG_TAG = "SyncConfiguration"; // These must be set in GlobalSession's constructor. public URI clusterURL; public KeyBundle syncKeyBundle; public CollectionKeys collectionKeys; public InfoCollections infoCollections; @@ -294,21 +143,21 @@ public class SyncConfiguration { engineNames.add(stage.getRepositoryName()); } return engineNames; } /** * Return a convenient accessor for part of prefs. * @return - * A ConfigurationBranch object representing this + * A PrefsBranch object representing this * section of the preferences space. */ - public ConfigurationBranch getBranch(String prefix) { - return new ConfigurationBranch(this, prefix); + public PrefsBranch getBranch(String prefix) { + return new PrefsBranch(this.getPrefs(), prefix); } /** * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global. * * @param prefs * SharedPreferences that the engines are associated with. * @param pref
--- a/mobile/android/base/sync/SynchronizerConfiguration.java +++ b/mobile/android/base/sync/SynchronizerConfiguration.java @@ -2,41 +2,41 @@ * 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/. */ package org.mozilla.gecko.sync; import java.io.IOException; import org.json.simple.parser.ParseException; +import org.mozilla.gecko.background.common.PrefsBranch; import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.SyncConfiguration.ConfigurationBranch; import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; import android.content.SharedPreferences.Editor; public class SynchronizerConfiguration { private static final String LOG_TAG = "SynczrConfiguration"; public String syncID; public RepositorySessionBundle remoteBundle; public RepositorySessionBundle localBundle; - public SynchronizerConfiguration(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException { + public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException { this.load(config); } public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) { this.syncID = syncID; this.remoteBundle = remoteBundle; this.localBundle = localBundle; } // This should get partly shuffled back into SyncConfiguration, I think. - public void load(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException { + public void load(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException { if (config == null) { throw new IllegalArgumentException("config cannot be null."); } String remoteJSON = config.getString("remote", null); String localJSON = config.getString("local", null); RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON); RepositorySessionBundle lB = new RepositorySessionBundle(localJSON); if (remoteJSON == null) { @@ -46,17 +46,17 @@ public class SynchronizerConfiguration { lB.setTimestamp(0); } syncID = config.getString("syncID", null); remoteBundle = rB; localBundle = lB; Logger.debug(LOG_TAG, "Loaded SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle); } - public void persist(ConfigurationBranch config) { + public void persist(PrefsBranch config) { if (config == null) { throw new IllegalArgumentException("config cannot be null."); } String jsonRemote = remoteBundle.toJSONString(); String jsonLocal = localBundle.toJSONString(); Editor editor = config.edit(); editor.putString("remote", jsonRemote); editor.putString("local", jsonLocal);
--- a/mobile/android/base/tests/BaseRobocopTest.java +++ b/mobile/android/base/tests/BaseRobocopTest.java @@ -66,17 +66,16 @@ public abstract class BaseRobocopTest ex protected Map<String, String> mConfig; protected String mRootPath; protected Solo mSolo; protected Driver mDriver; protected Actions mActions; - protected Activity mActivity; protected String mProfile; protected abstract Intent createActivityIntent(); /** * The browser is started at the beginning of this test. A single test is a * class inheriting from <code>BaseRobocopTest</code> that contains test * methods. @@ -135,22 +134,22 @@ public abstract class BaseRobocopTest ex mAsserter = new FennecMochitestAssert(); } mAsserter.setLogFile(mLogFile); mAsserter.setTestName(getClass().getName()); // Start the activity. final Intent intent = createActivityIntent(); setActivityIntent(intent); - mActivity = getActivity(); // Set up Robotium.solo and Driver objects - mSolo = new Solo(getInstrumentation(), mActivity); - mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath); - mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter); + Activity tempActivity = getActivity(); + mSolo = new Solo(getInstrumentation(), tempActivity); + mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath); + mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter); } /** * Function to early abort if we can't reach the given HTTP server. Provides local testers * with diagnostic information. Not currently available for TALOS tests, which are rarely run * locally in any case. */ public void throwIfHttpGetFails() {
--- a/mobile/android/base/tests/BaseTest.java +++ b/mobile/android/base/tests/BaseTest.java @@ -98,17 +98,17 @@ abstract class BaseTest extends BaseRobo } } @Override public void setUp() throws Exception { super.setUp(); mDevice = new Device(); - mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter); + mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter); // Ensure Robocop tests have access to network, and are run with Display powered on. throwIfHttpGetFails(); throwIfScreenNotOn(); } protected GeckoProfile getTestProfile() { if (mProfile.startsWith("/")) { @@ -727,17 +727,17 @@ abstract class BaseTest extends BaseRobo */ public void closeTabAt(final int index) { View closeButton = getTabViewAt(index).findViewById(R.id.close); mSolo.clickOnView(closeButton); } public final void runOnUiThreadSync(Runnable runnable) { - RobocopUtils.runOnUiThreadSync(mActivity, runnable); + RobocopUtils.runOnUiThreadSync(getActivity(), runnable); } /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */ public void toggleBookmark() { mActions.sendSpecialKey(Actions.SpecialKey.MENU); waitForText("Settings"); // On ICS+ phones, there is no button labeled "Bookmarks"
--- a/mobile/android/chrome/content/Reader.js +++ b/mobile/android/chrome/content/Reader.js @@ -12,21 +12,16 @@ let Reader = { STATUS_UNFETCHED: 0, STATUS_FETCH_FAILED_TEMPORARY: 1, STATUS_FETCH_FAILED_PERMANENT: 2, STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3, STATUS_FETCHED_ARTICLE: 4, observe: function Reader_observe(aMessage, aTopic, aData) { switch (aTopic) { - case "Reader:Added": { - let mm = window.getGroupMessageManager("browsers"); - mm.broadcastAsyncMessage("Reader:Added", { url: aData }); - break; - } case "Reader:Removed": { let uri = Services.io.newURI(aData, null, null); ReaderMode.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e)); let mm = window.getGroupMessageManager("browsers"); mm.broadcastAsyncMessage("Reader:Removed", { url: aData }); break; } @@ -51,35 +46,35 @@ let Reader = { ZoomHelper.zoomToRect(newRect, -1); break; } } }, receiveMessage: function(message) { switch (message.name) { - case "Reader:AddToList": - this.addArticleToReadingList(message.data.article); + case "Reader:AddToList": { + // If the article is coming from reader mode, we must have fetched it already. + let article = message.data.article; + article.status = this.STATUS_FETCHED_ARTICLE; + this._addArticleToReadingList(article); break; - + } case "Reader:ArticleGet": this._getArticle(message.data.url, message.target).then((article) => { message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article }); }); break; case "Reader:FaviconRequest": { - let observer = (s, t, d) => { - Services.obs.removeObserver(observer, "Reader:FaviconReturn", false); - message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(d)); - }; - Services.obs.addObserver(observer, "Reader:FaviconReturn", false); - Messaging.sendRequest({ + Messaging.sendRequestForResult({ type: "Reader:FaviconRequest", url: message.data.url + }).then(data => { + message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data)); }); break; } case "Reader:ListStatusRequest": Messaging.sendRequestForResult({ type: "Reader:ListStatusRequest", url: message.data.url @@ -195,41 +190,40 @@ let Reader = { return null; }); if (!article) { // If there was a problem getting the article, just store the // URL and title from the tab. article = { url: urlWithoutRef, title: tab.browser.contentDocument.title, + length: 0, + excerpt: "", status: this.STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT, }; } else { article.status = this.STATUS_FETCHED_ARTICLE; } - this.addArticleToReadingList(article); + this._addArticleToReadingList(article); }), - addArticleToReadingList: function(article) { - if (!article || !article.url) { - Cu.reportError("addArticleToReadingList requires article with valid URL"); - return; - } - - Messaging.sendRequest({ + _addArticleToReadingList: function(article) { + Messaging.sendRequestForResult({ type: "Reader:AddToList", url: truncate(article.url, MAX_URI_LENGTH), - title: truncate(article.title || "", MAX_TITLE_LENGTH), - length: article.length || 0, - excerpt: article.excerpt || "", + title: truncate(article.title, MAX_TITLE_LENGTH), + length: article.length, + excerpt: article.excerpt, status: article.status, - }); - - ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e)); + }).then((url) => { + let mm = window.getGroupMessageManager("browsers"); + mm.broadcastAsyncMessage("Reader:Added", { url: url }); + ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e)); + }).catch(Cu.reportError); }, /** * Gets an article for a given URL. This method will download and parse a document * if it does not find the article in the tab data or the cache. * * @param url The article URL. * @param browser The browser where the article is currently loaded.
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -141,17 +141,17 @@ let lazilyLoadedObserverScripts = [ ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"], ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"], ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"], ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"], ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"], ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"], - ["Reader", ["Reader:Added", "Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"], + ["Reader", ["Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"], ]; if (AppConstants.MOZ_WEBRTC) { lazilyLoadedObserverScripts.push( ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"]) } lazilyLoadedObserverScripts.forEach(function (aScript) { let [name, notifications, script] = aScript;
--- a/mobile/android/locales/filter.py +++ b/mobile/android/locales/filter.py @@ -29,15 +29,16 @@ def test(mod, path, entity = None): return "error" # we're in mod == "mobile" if re.match(r"searchplugins\/.+\.xml", path): return "ignore" if path == "chrome/region.properties": # only region.properties exceptions remain if (re.match(r"browser\.search\.order\.[1-9]", entity) or + re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or re.match(r"gecko\.handlerService\.schemes\.", entity) or re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or re.match(r"browser\.suggestedsites\.", entity)): return "ignore" return "error"
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java @@ -336,17 +336,17 @@ public class SearchActivity extends Loca set.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { // Don't do anything if the activity is destroyed before the animation ends. - if (SearchActivity.this.isDestroyed()) { + if (searchEngineManager == null) { return; } setEditState(EditState.WAITING); setSearchState(SearchState.POSTSEARCH); // We need to manually clear the animation for the views to be hidden on gingerbread. animationCard.clearAnimation();
--- a/mobile/locales/filter.py +++ b/mobile/locales/filter.py @@ -29,15 +29,16 @@ def test(mod, path, entity = None): return "error" # we're in mod == "mobile" if re.match(r"searchplugins\/.+\.xml", path): return "ignore" if path == "chrome/region.properties": # only region.properties exceptions remain if (re.match(r"browser\.search\.order\.[1-9]", entity) or + re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or re.match(r"gecko\.handlerService\.schemes\.", entity) or re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or re.match(r"browser\.suggestedsites\.", entity)): return "ignore" return "error"
--- a/services/sync/modules/FxaMigrator.jsm +++ b/services/sync/modules/FxaMigrator.jsm @@ -519,14 +519,28 @@ Migrator.prototype = { case this.TELEMETRY_ACCEPTED: case this.TELEMETRY_UNLINKED: case this.TELEMETRY_DECLINED: Services.obs.notifyObservers(null, OBSERVER_INTERNAL_TELEMETRY_TOPIC, flag); break; default: throw new Error("Unexpected telemetry flag: " + flag); } - } -} + }, + + get learnMoreLink() { + try { + var url = Services.prefs.getCharPref("app.support.baseURL"); + } catch (err) { + return null; + } + url += "sync-upgrade"; + let sb = Services.strings.createBundle("chrome://weave/locale/services/sync.properties"); + return { + text: sb.GetStringFromName("sync.eol.learnMore.label"), + href: Services.urlFormatter.formatURL(url), + }; + }, +}; // We expose a singleton this.EXPORTED_SYMBOLS = ["fxaMigrator"]; let fxaMigrator = new Migrator();
--- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -79,28 +79,31 @@ this.Notifications = { } }; /** * A basic notification. Subclass this to create more complex notifications. */ this.Notification = - function Notification(title, description, iconURL, priority, buttons) { +function Notification(title, description, iconURL, priority, buttons, link) { this.title = title; this.description = description; if (iconURL) this.iconURL = iconURL; if (priority) this.priority = priority; if (buttons) this.buttons = buttons; + + if (link) + this.link = link; } // We set each prototype property individually instead of redefining // the entire prototype to avoid blowing away existing properties // of the prototype like the the "constructor" property, which we use // to bind notification objects to their XBL representations. Notification.prototype.priority = Notifications.PRIORITY_INFO; Notification.prototype.iconURL = null;
--- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -103,20 +103,17 @@ function DBConn(aForceNewConnection) { // 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); + promiseTopicObserved("profile-before-change").then(() => dbConn.asyncClose()); } return gDBConn.connectionReady ? gDBConn : null; }; /** * Reads data from the provided inputstream. * @@ -374,25 +371,22 @@ function check_no_bookmarks() { * Notification topic to observe. * * @return {Promise} * @resolves The array [aSubject, aData] from the observed notification. * @rejects Never. */ function promiseTopicObserved(aTopic) { - let deferred = Promise.defer(); - - Services.obs.addObserver( - function PTO_observe(aSubject, aTopic, aData) { - Services.obs.removeObserver(PTO_observe, aTopic); - deferred.resolve([aSubject, aData]); + return new Promise(resolve => { + Services.obs.addObserver(function observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(observe, aTopic); + resolve([aSubject, aData]); }, aTopic, false); - - return deferred.promise; + }); } /** * Simulates a Places shutdown. */ function shutdownPlaces(aKeepAliveConnection) { let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver); @@ -601,20 +595,17 @@ function is_time_ordered(before, after) /** * Shutdowns Places, invoking the callback when the connection has been closed. * * @param aCallback * Function to be called when done. */ function waitForConnectionClosed(aCallback) { - Services.obs.addObserver(function WFCCCallback() { - Services.obs.removeObserver(WFCCCallback, "places-connection-closed"); - aCallback(); - }, "places-connection-closed", false); + 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.
--- a/toolkit/modules/NewTabUtils.jsm +++ b/toolkit/modules/NewTabUtils.jsm @@ -715,19 +715,20 @@ let Links = { maxNumLinks: LINKS_GET_LINKS_LIMIT, /** * The link providers. */ _providers: new Set(), /** - * A mapping from each provider to an object { sortedLinks, linkMap }. - * sortedLinks is the cached, sorted array of links for the provider. linkMap - * is a Map from link URLs to link objects. + * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }. + * sortedLinks is the cached, sorted array of links for the provider. + * siteMap is a mapping from base domains to URL count associated with the domain. + * linkMap is a Map from link URLs to link objects. */ _providerLinks: new Map(), /** * The properties of link objects used to sort them. */ _sortProperties: [ "frecency", @@ -857,16 +858,31 @@ let Links = { if (!(prop in aLink1) || !(prop in aLink2)) throw new Error("Comparable link missing required property: " + prop); } return aLink2.frecency - aLink1.frecency || aLink2.lastVisitDate - aLink1.lastVisitDate || aLink1.url.localeCompare(aLink2.url); }, + _incrementSiteMap: function(map, link) { + let site = NewTabUtils.extractSite(link.url); + map.set(site, (map.get(site) || 0) + 1); + }, + + _decrementSiteMap: function(map, link) { + let site = NewTabUtils.extractSite(link.url); + let previousURLCount = map.get(site); + if (previousURLCount === 1) { + map.delete(site); + } else { + map.set(site, previousURLCount - 1); + } + }, + /** * Calls getLinks on the given provider and populates our cache for it. * @param aProvider The provider whose cache will be populated. * @param aCallback The callback to call when finished. * @param aForce When true, populates the provider's cache even when it's * already filled. */ _populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) { @@ -874,16 +890,20 @@ let Links = { aCallback(); } else { aProvider.getLinks(links => { // Filter out null and undefined links so we don't have to deal with // them in getLinks when merging links from providers. links = links.filter((link) => !!link); this._providerLinks.set(aProvider, { sortedLinks: links, + siteMap: links.reduce((map, link) => { + this._incrementSiteMap(map, link); + return map; + }, new Map()), linkMap: links.reduce((map, link) => { map.set(link.url, link); return map; }, new Map()), }); aCallback(); }); } @@ -933,17 +953,17 @@ let Links = { let links = this._providerLinks.get(aProvider); if (!links) // This is not an error, it just means that between the time the provider // was added and the future time we call getLinks on it, it notified us of // a change. return; - let { sortedLinks, linkMap } = links; + let { sortedLinks, siteMap, linkMap } = links; let existingLink = linkMap.get(aLink.url); let insertionLink = null; let updatePages = false; if (existingLink) { // Update our copy's position in O(lg n) by first removing it from its // list. It's important to do this before modifying its properties. if (this._sortProperties.some(prop => prop in aLink)) { @@ -978,24 +998,26 @@ let Links = { } // Copy the link object so that changes later made to it by the caller // don't affect our copy. insertionLink = {}; for (let prop in aLink) { insertionLink[prop] = aLink[prop]; } linkMap.set(aLink.url, insertionLink); + this._incrementSiteMap(siteMap, aLink); } if (insertionLink) { let idx = this._insertionIndexOf(sortedLinks, insertionLink); sortedLinks.splice(idx, 0, insertionLink); if (sortedLinks.length > aProvider.maxNumLinks) { let lastLink = sortedLinks.pop(); linkMap.delete(lastLink.url); + this._decrementSiteMap(siteMap, lastLink); } updatePages = true; } if (updatePages) { AllPages.update(null, "links-changed"); } }, @@ -1182,16 +1204,24 @@ this.NewTabUtils = { this._initialized = true; ExpirationFilter.init(); Telemetry.init(); return true; } return false; }, + isTopSiteGivenProvider: function(aSite, aProvider) { + return Links._providerLinks.get(aProvider).siteMap.has(aSite); + }, + + isTopPlacesSite: function(aSite) { + return this.isTopSiteGivenProvider(aSite, PlacesProvider); + }, + /** * Restores all sites that have been removed from the grid. */ restore: function NewTabUtils_restore() { Storage.clear(); Links.resetCache(); PinnedLinks.resetCache(); BlockedLinks.resetCache();
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js +++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js @@ -6,17 +6,56 @@ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; Cu.import("resource://gre/modules/NewTabUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); function run_test() { run_next_test(); } -add_test(function multipleProviders() { +add_task(function isTopSiteGivenProvider() { + let expectedLinks = makeLinks(0, 10, 2); + + // The lowest 2 frecencies have the same base domain. + expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test"; + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + NewTabUtils.links.populateCache(function () {}, false); + + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example1.com", provider), false); + + // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3 + let newLink = makeLink(3); + provider.notifyLinkChanged(newLink); + + // There is still a frecent url with example2 domain, so it's still frecent. + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example3.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + + // Push out frecency 3 + newLink = makeLink(5); + provider.notifyLinkChanged(newLink); + + // Push out frecency 4 + newLink = makeLink(9); + provider.notifyLinkChanged(newLink); + + // Our count reached 0 for the example2.com domain so it's no longer a frecent site. + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example5.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), false); + + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function multipleProviders() { // Make each provider generate NewTabUtils.links.maxNumLinks links to check // that no more than maxNumLinks are actually returned in the merged list. let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2); let evenProvider = new TestProvider(done => done(evenLinks)); let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2); let oddProvider = new TestProvider(done => done(oddLinks)); NewTabUtils.initWithoutProviders(); @@ -30,20 +69,19 @@ add_test(function multipleProviders() { let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks, 2 * NewTabUtils.links.maxNumLinks, 1); do_check_eq(links.length, NewTabUtils.links.maxNumLinks); do_check_links(links, expectedLinks); NewTabUtils.links.removeProvider(evenProvider); NewTabUtils.links.removeProvider(oddProvider); - run_next_test(); }); -add_test(function changeLinks() { +add_task(function changeLinks() { let expectedLinks = makeLinks(0, 20, 2); let provider = new TestProvider(done => done(expectedLinks)); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider); // This is sync since the provider's getLinks is sync. NewTabUtils.links.populateCache(function () {}, false); @@ -86,17 +124,16 @@ add_test(function changeLinks() { // Notify of many links changed. expectedLinks = makeLinks(0, 3, 1); provider.notifyManyLinksChanged(); // NewTabUtils.links will now repopulate its cache, which is sync since // the provider's getLinks is sync. do_check_links(NewTabUtils.links.getLinks(), expectedLinks); NewTabUtils.links.removeProvider(provider); - run_next_test(); }); add_task(function oneProviderAlreadyCached() { let links1 = makeLinks(0, 10, 1); let provider1 = new TestProvider(done => done(links1)); NewTabUtils.initWithoutProviders(); NewTabUtils.links.addProvider(provider1);
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini @@ -20,9 +20,10 @@ run-if = appname == "firefox" [test_provider_markSafe.js] [test_provider_shutdown.js] [test_provider_unsafe_access_shutdown.js] [test_provider_unsafe_access_startup.js] [test_shutdown.js] [test_XPIcancel.js] [test_XPIStates.js] + [include:xpcshell-shared.ini]
--- a/toolkit/themes/linux/global/notification.css +++ b/toolkit/themes/linux/global/notification.css @@ -1,28 +1,44 @@ /* 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/. */ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +notification, +.messageText > .text-link { + color: InfoText !important; +} + notification { background-color: InfoBackground; - color: InfoText; text-shadow: none; } +notification[type="info"], +notification[type="info"] .messageText > .text-link { + color: -moz-DialogText !important; +} + notification[type="info"] { background-color: -moz-Dialog; - color: -moz-DialogText; +} + +notification[type="critical"], +notification[type="critical"] .messageText > .text-link { + color: white !important; } notification[type="critical"] { background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0)); - color: white; +} + +.messageText > .text-link { + text-decoration: underline; } .notification-inner { padding-top: 1px; padding-bottom: 1px; } .messageText {
--- a/toolkit/themes/osx/global/notification.css +++ b/toolkit/themes/osx/global/notification.css @@ -5,37 +5,53 @@ %include shared.inc @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); notification { padding: 3px 3px 4px; text-shadow: none; } +notification[type="info"], +notification[type="info"] .messageText > .text-link { + color: rgba(255,255,255,0.95) !important; +} + notification[type="info"] { - color: rgba(255,255,255,0.95); background: url("chrome://global/skin/notification/info-bar-background.png") #404040 repeat-x top left; border-top: 1px solid #707070; border-bottom: 1px solid #2a2a2a; } +notification[type="warning"], +notification[type="warning"] .messageText > .text-link { + color: rgba(0,0,0,0.95) !important; +} + notification[type="warning"] { - color: rgba(0,0,0,0.95); background: url("chrome://global/skin/notification/warning-bar-background.png") #ffc703 repeat-x top left; border-top: 1px solid #ffe970; border-bottom: 1px solid #bf8a01; } +notification[type="critical"], +notification[type="critical"] .messageText > .text-link { + color: rgba(255,255,255,0.95) !important; +} + notification[type="critical"] { - color: rgba(255,255,255,0.95); background: url("chrome://global/skin/notification/critical-bar-background.png") #980000 repeat-x top left; border-top: 1px solid #e35959; border-bottom: 1px solid #5d0000; } +.messageText > .text-link { + text-decoration: underline; +} + .messageImage { width: 16px; height: 16px; margin: 0 4px; } /* Default icons for notifications */
--- a/toolkit/themes/windows/global/notification.css +++ b/toolkit/themes/windows/global/notification.css @@ -1,28 +1,44 @@ /* 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/. */ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +notification, +.messageText > .text-link { + color: InfoText !important; +} + notification { background-color: InfoBackground; - color: InfoText; text-shadow: none; } +notification[type="info"], +notification[type="info"] .messageText > .text-link { + color: -moz-DialogText !important; +} + notification[type="info"] { background-color: -moz-Dialog; - color: -moz-DialogText; +} + +notification[type="critical"], +notification[type="critical"] .messageText > .text-link { + color: white !important; } notification[type="critical"] { background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0)); - color: white; +} + +.messageText > .text-link { + text-decoration: underline; } .messageImage { width: 16px; height: 16px; -moz-margin-start: 6px; -moz-margin-end: 1px; }