summaryrefslogtreecommitdiffstats
path: root/browser/components/places/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/tests')
-rw-r--r--browser/components/places/tests/browser/.eslintrc.js7
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_1.html9
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_2.html9
-rw-r--r--browser/components/places/tests/browser/browser.ini58
-rw-r--r--browser/components/places/tests/browser/browser_0_library_left_pane_migration.js90
-rw-r--r--browser/components/places/tests/browser/browser_410196_paste_into_tags.js114
-rw-r--r--browser/components/places/tests/browser/browser_416459_cut.js83
-rw-r--r--browser/components/places/tests/browser/browser_423515.js173
-rw-r--r--browser/components/places/tests/browser/browser_425884.js127
-rw-r--r--browser/components/places/tests/browser/browser_435851_copy_query.js59
-rw-r--r--browser/components/places/tests/browser/browser_475045.js65
-rw-r--r--browser/components/places/tests/browser/browser_555547.js66
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js53
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js110
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js39
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js71
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js42
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_all_tabs.js37
-rw-r--r--browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js61
-rw-r--r--browser/components/places/tests/browser/browser_bookmarksProperties.js450
-rw-r--r--browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js256
-rw-r--r--browser/components/places/tests/browser/browser_forgetthissite_single.js78
-rw-r--r--browser/components/places/tests/browser/browser_history_sidebar_search.js64
-rw-r--r--browser/components/places/tests/browser/browser_library_batch_delete.js114
-rw-r--r--browser/components/places/tests/browser/browser_library_commands.js235
-rw-r--r--browser/components/places/tests/browser/browser_library_downloads.js70
-rw-r--r--browser/components/places/tests/browser/browser_library_infoBox.js197
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_fixnames.js94
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js41
-rw-r--r--browser/components/places/tests/browser/browser_library_middleclick.js279
-rw-r--r--browser/components/places/tests/browser/browser_library_openFlatContainer.js42
-rw-r--r--browser/components/places/tests/browser/browser_library_open_leak.js23
-rw-r--r--browser/components/places/tests/browser/browser_library_panel_leak.js54
-rw-r--r--browser/components/places/tests/browser/browser_library_search.js182
-rw-r--r--browser/components/places/tests/browser/browser_library_views_liveupdate.js300
-rw-r--r--browser/components/places/tests/browser/browser_markPageAsFollowedLink.js68
-rw-r--r--browser/components/places/tests/browser/browser_sidebarpanels_click.js157
-rw-r--r--browser/components/places/tests/browser/browser_sort_in_library.js249
-rw-r--r--browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js53
-rw-r--r--browser/components/places/tests/browser/browser_views_liveupdate.js475
-rw-r--r--browser/components/places/tests/browser/frameLeft.html8
-rw-r--r--browser/components/places/tests/browser/frameRight.html8
-rw-r--r--browser/components/places/tests/browser/framedPage.html9
-rw-r--r--browser/components/places/tests/browser/head.js460
-rw-r--r--browser/components/places/tests/browser/keyword_form.html17
-rw-r--r--browser/components/places/tests/browser/pageopeningwindow.html9
-rw-r--r--browser/components/places/tests/browser/sidebarpanels_click_test_page.html7
-rw-r--r--browser/components/places/tests/chrome/.eslintrc.js7
-rw-r--r--browser/components/places/tests/chrome/chrome.ini15
-rw-r--r--browser/components/places/tests/chrome/head.js7
-rw-r--r--browser/components/places/tests/chrome/test_0_bug510634.xul99
-rw-r--r--browser/components/places/tests/chrome/test_0_multiple_left_pane.xul85
-rw-r--r--browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul89
-rw-r--r--browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul91
-rw-r--r--browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul83
-rw-r--r--browser/components/places/tests/chrome/test_bug549192.xul120
-rw-r--r--browser/components/places/tests/chrome/test_bug549491.xul78
-rw-r--r--browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul170
-rw-r--r--browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul99
-rw-r--r--browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul204
-rw-r--r--browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul86
-rw-r--r--browser/components/places/tests/chrome/test_treeview_date.xul159
-rw-r--r--browser/components/places/tests/unit/.eslintrc.js7
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.html16
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.json1
-rw-r--r--browser/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--browser/components/places/tests/unit/distribution.ini27
-rw-r--r--browser/components/places/tests/unit/head_bookmarks.js133
-rw-r--r--browser/components/places/tests/unit/test_421483.js103
-rw-r--r--browser/components/places/tests/unit/test_PUIU_makeTransaction.js361
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js33
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt.js59
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js52
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js55
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_distribution.js125
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_migrate.js70
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_prefs.js240
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_restore.js62
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js285
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js150
-rw-r--r--browser/components/places/tests/unit/test_clearHistory_shutdown.js181
-rw-r--r--browser/components/places/tests/unit/test_leftpane_corruption_handling.js174
-rw-r--r--browser/components/places/tests/unit/xpcshell.ini25
83 files changed, 8823 insertions, 0 deletions
diff --git a/browser/components/places/tests/browser/.eslintrc.js b/browser/components/places/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/components/places/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/browser/bookmark_dummy_1.html b/browser/components/places/tests/browser/bookmark_dummy_1.html
new file mode 100644
index 000000000..c03e0c18c
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_1.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 1</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 1</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/bookmark_dummy_2.html b/browser/components/places/tests/browser/bookmark_dummy_2.html
new file mode 100644
index 000000000..229a730b3
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_2.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 2</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 2</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/browser.ini b/browser/components/places/tests/browser/browser.ini
new file mode 100644
index 000000000..5dce31653
--- /dev/null
+++ b/browser/components/places/tests/browser/browser.ini
@@ -0,0 +1,58 @@
+# 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/.
+
+[DEFAULT]
+support-files =
+ head.js
+ framedPage.html
+ frameLeft.html
+ frameRight.html
+ sidebarpanels_click_test_page.html
+ keyword_form.html
+
+[browser_0_library_left_pane_migration.js]
+[browser_410196_paste_into_tags.js]
+subsuite = clipboard
+[browser_416459_cut.js]
+subsuite = clipboard
+[browser_423515.js]
+[browser_425884.js]
+[browser_435851_copy_query.js]
+subsuite = clipboard
+[browser_475045.js]
+[browser_555547.js]
+[browser_bookmarklet_windowOpen.js]
+support-files =
+ pageopeningwindow.html
+[browser_bookmarkProperties_addFolderDefaultButton.js]
+[browser_bookmarkProperties_addKeywordForThisSearch.js]
+[browser_bookmarkProperties_addLivemark.js]
+[browser_bookmarkProperties_editTagContainer.js]
+[browser_bookmarkProperties_readOnlyRoot.js]
+[browser_bookmarksProperties.js]
+[browser_drag_bookmarks_on_toolbar.js]
+[browser_forgetthissite_single.js]
+[browser_history_sidebar_search.js]
+[browser_library_batch_delete.js]
+[browser_library_commands.js]
+[browser_library_downloads.js]
+[browser_library_infoBox.js]
+[browser_library_left_pane_fixnames.js]
+[browser_library_left_pane_select_hierarchy.js]
+[browser_library_middleclick.js]
+[browser_library_open_leak.js]
+[browser_library_openFlatContainer.js]
+[browser_library_panel_leak.js]
+[browser_library_search.js]
+[browser_library_views_liveupdate.js]
+[browser_markPageAsFollowedLink.js]
+[browser_sidebarpanels_click.js]
+skip-if = true # temporarily disabled for breaking the treeview - bug 658744
+[browser_sort_in_library.js]
+[browser_toolbarbutton_menu_context.js]
+[browser_views_liveupdate.js]
+[browser_bookmark_all_tabs.js]
+support-files =
+ bookmark_dummy_1.html
+ bookmark_dummy_2.html
diff --git a/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js b/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
new file mode 100644
index 000000000..a7089b497
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_0_library_left_pane_migration.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test we correctly migrate Library left pane to the latest version.
+ * Note: this test MUST be the first between browser chrome tests, or results
+ * of next tests could be unexpected due to PlacesUIUtils getters.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+add_task(function* () {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Check if we have any left pane folder already set, remove it eventually.
+ let leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ if (leftPaneItems.length > 0) {
+ // The left pane has already been created, touching it now would cause
+ // next tests to rely on wrong values (and possibly crash)
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ let version = PlacesUtils.annotations.getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+ ok(true, "left pane has already been created, skipping test");
+ return;
+ }
+
+ // Create a fake left pane folder with an old version (current version - 1).
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.rootGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: ""
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(folderId,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Check fake left pane root has been correctly created.
+ leftPaneItems =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ is(leftPaneItems[0], folderId, "left pane root itemId is correct");
+
+ // Check version.
+ let version = PlacesUtils.annotations.getItemAnnotation(folderId,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION - 1, "Left pane version correctly set");
+
+ // Open Library, this will upgrade our left pane version.
+ let organizer = yield promiseLibrary();
+
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0, "Left pane folder correctly created");
+ leftPaneItems =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ let leftPaneRoot = leftPaneItems[0];
+ is(leftPaneRoot, PlacesUIUtils.leftPaneFolderId,
+ "leftPaneFolderId getter has correct value");
+
+ // Check version has been upgraded.
+ version = PlacesUtils.annotations.getItemAnnotation(leftPaneRoot,
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
+ "Left pane version has been correctly upgraded");
+
+ // Check left pane is populated.
+ organizer.PlacesOrganizer.selectLeftPaneQuery("History");
+ is(organizer.PlacesOrganizer._places.selectedNode.itemId,
+ PlacesUIUtils.leftPaneQueries["History"],
+ "Library left pane is populated and working");
+
+ yield promiseLibraryClosed(organizer);
+});
diff --git a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
new file mode 100644
index 000000000..2acb1d9b7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const TEST_URL = Services.io.newURI("http://example.com/", null, null);
+const MOZURISPEC = Services.io.newURI("http://mozilla.com/", null, null);
+
+add_task(function* () {
+ let organizer = yield promiseLibrary();
+
+ ok(PlacesUtils, "PlacesUtils in scope");
+ ok(PlacesUIUtils, "PlacesUIUtils in scope");
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ ok(PlacesOrganizer, "Places organizer in scope");
+
+ let ContentTree = organizer.ContentTree;
+ ok(ContentTree, "ContentTree is in scope");
+
+ let visits = {uri: MOZURISPEC, transition: PlacesUtils.history.TRANSITION_TYPED};
+ yield PlacesTestUtils.addVisits(visits);
+
+ // create an initial tag to work with
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "bookmark/" + TEST_URL.spec,
+ url: TEST_URL
+ });
+
+ ok(bm, "A bookmark was added");
+ PlacesUtils.tagging.tagURI(TEST_URL, ["foo"]);
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags[0], "foo", "tag is foo");
+
+ // focus the new tag
+ focusTag(PlacesOrganizer);
+
+ let populate = () => copyHistNode(PlacesOrganizer, ContentTree);
+ yield promiseClipboard(populate, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ focusTag(PlacesOrganizer);
+ PlacesOrganizer._places.controller.paste();
+
+ // re-focus the history again
+ PlacesOrganizer.selectLeftPaneQuery("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ok(histNode, "histNode exists: " + histNode.title);
+
+ // check to see if the history node is tagged!
+ tags = PlacesUtils.tagging.getTagsForURI(MOZURISPEC);
+ ok(tags.length == 1, "history node is tagged: " + tags.length);
+
+ // check if a bookmark was created
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({url: MOZURISPEC}, bm => {
+ bookmarks.push(bm);
+ });
+ ok(bookmarks.length > 0, "bookmark exists for the tagged history item");
+
+ // is the bookmark visible in the UI?
+ // get the Unsorted Bookmarks node
+ PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+
+ // now we can see what is in the ContentTree tree
+ let unsortedNode = ContentTree.view.view.nodeForTreeIndex(1);
+ ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
+ is(unsortedNode.uri, MOZURISPEC.spec, "node uri's are the same");
+
+ yield promiseLibraryClosed(organizer);
+
+ // Remove new Places data we created.
+ PlacesUtils.tagging.untagURI(MOZURISPEC, ["foo"]);
+ PlacesUtils.tagging.untagURI(TEST_URL, ["foo"]);
+ tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags.length, 0, "tags are gone");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+function focusTag(PlacesOrganizer) {
+ PlacesOrganizer.selectLeftPaneQuery("Tags");
+ let tags = PlacesOrganizer._places.selectedNode;
+ tags.containerOpen = true;
+ let fooTag = tags.getChild(0);
+ let tagNode = fooTag;
+ PlacesOrganizer._places.selectNode(fooTag);
+ is(tagNode.title, 'foo', "tagNode title is foo");
+ let ip = PlacesOrganizer._places.insertionPoint;
+ ok(ip.isTag, "IP is a tag");
+}
+
+function copyHistNode(PlacesOrganizer, ContentTree) {
+ // focus the history object
+ PlacesOrganizer.selectLeftPaneQuery("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ContentTree.view.selectNode(histNode);
+ is(histNode.uri, MOZURISPEC.spec,
+ "historyNode exists: " + histNode.uri);
+ // copy the history node
+ ContentTree.view.controller.copy();
+}
diff --git a/browser/components/places/tests/browser/browser_416459_cut.js b/browser/components/places/tests/browser/browser_416459_cut.js
new file mode 100644
index 000000000..6f3f8cdd5
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_416459_cut.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/";
+
+add_task(function* () {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ let organizer = yield promiseLibrary();
+
+ registerCleanupFunction(function* () {
+ yield promiseLibraryClosed(organizer);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in scope");
+ ok(PlacesUIUtils, "PlacesUIUtils in scope");
+ ok(PlacesOrganizer, "PlacesOrganizer in scope");
+ ok(ContentTree, "ContentTree is in scope");
+
+ // Test with multiple entries to ensure they retain their order.
+ let bookmarks = [];
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "0"
+ }));
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "1"
+ }));
+ bookmarks.push(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "2"
+ }));
+
+ yield selectBookmarksIn(organizer, bookmarks, "BookmarksToolbar");
+
+ yield promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ info("Pasting clipboard");
+ ContentTree.view.controller.paste();
+
+ yield selectBookmarksIn(organizer, bookmarks, "UnfiledBookmarks");
+});
+
+var selectBookmarksIn = Task.async(function* (organizer, bookmarks, aLeftPaneQuery) {
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+ info("Selecting " + aLeftPaneQuery + " in the left pane");
+ PlacesOrganizer.selectLeftPaneQuery(aLeftPaneQuery);
+
+ let ids = [];
+ for (let {guid} of bookmarks) {
+ let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
+ is (bookmark.parentGuid, PlacesOrganizer._places.selectedNode.targetFolderGuid,
+ "Bookmark has the right parent");
+ ids.push(yield PlacesUtils.promiseItemId(bookmark.guid));
+ }
+
+ info("Selecting the bookmarks in the right pane");
+ ContentTree.view.selectItems(ids);
+
+ for (let node of ContentTree.view.selectedNodes) {
+ is(node.bookmarkIndex, node.title,
+ "Found the expected bookmark in the expected position");
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_423515.js b/browser/components/places/tests/browser/browser_423515.js
new file mode 100644
index 000000000..4d3da8fc1
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_423515.js
@@ -0,0 +1,173 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ // sanity check
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?");
+ ok(PlacesControllerDragHelper, "checking PlacesControllerDragHelper, running in chrome context?");
+
+ const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+
+ // setup
+ var rootId = PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX);
+ var rootNode = PlacesUtils.getFolderContents(rootId, false, true).root;
+ is(rootNode.childCount, 0, "confirm test root is empty");
+
+ var tests = [];
+
+ // add a regular folder, should be moveable
+ tests.push({
+ populate: function() {
+ this.id =
+ PlacesUtils.bookmarks.createFolder(rootId, "", IDX);
+ },
+ validate: function() {
+ is(rootNode.childCount, 1,
+ "populate added data to the test root");
+ is(PlacesControllerDragHelper.canMoveNode(rootNode.getChild(0)),
+ true, "can move regular folder node");
+ }
+ });
+
+ // add a regular folder shortcut, should be moveable
+ tests.push({
+ populate: function() {
+ this.folderId =
+ PlacesUtils.bookmarks.createFolder(rootId, "foo", IDX);
+ this.shortcutId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder="+this.folderId), IDX, "bar");
+ },
+ validate: function() {
+ is(rootNode.childCount, 2,
+ "populated data to the test root");
+
+ var folderNode = rootNode.getChild(0);
+ is(folderNode.type, 6, "node is folder");
+ is(this.folderId, folderNode.itemId, "folder id and folder node item id match");
+
+ var shortcutNode = rootNode.getChild(1);
+ is(shortcutNode.type, 9, "node is folder shortcut");
+ is(this.shortcutId, shortcutNode.itemId, "shortcut id and shortcut node item id match");
+
+ var concreteId = PlacesUtils.getConcreteItemId(shortcutNode);
+ is(concreteId, folderNode.itemId, "shortcut node id and concrete id match");
+
+ is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
+ true, "can move folder shortcut node");
+ }
+ });
+
+ // add a regular query, should be moveable
+ tests.push({
+ populate: function() {
+ this.bookmarkId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("http://foo.com"), IDX, "foo");
+ this.queryId =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:terms=foo"), IDX, "bar");
+ },
+ validate: function() {
+ is(rootNode.childCount, 2,
+ "populated data to the test root");
+
+ var bmNode = rootNode.getChild(0);
+ is(bmNode.itemId, this.bookmarkId, "bookmark id and bookmark node item id match");
+
+ var queryNode = rootNode.getChild(1);
+ is(queryNode.itemId, this.queryId, "query id and query node item id match");
+
+ is(PlacesControllerDragHelper.canMoveNode(queryNode),
+ true, "can move query node");
+ }
+ });
+
+ // test that special folders cannot be moved
+ // test that special folders shortcuts can be moved
+ tests.push({
+ folders: [PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.toolbarFolderId],
+ shortcuts: {},
+ populate: function() {
+ for (var i = 0; i < this.folders.length; i++) {
+ var id = this.folders[i];
+ this.shortcuts[id] =
+ PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder=" + id), IDX, "");
+ }
+ },
+ validate: function() {
+ // test toolbar shortcut node
+ is(rootNode.childCount, this.folders.length,
+ "populated data to the test root");
+
+ function getRootChildNode(aId) {
+ var node = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false, true).root;
+ for (var i = 0; i < node.childCount; i++) {
+ var child = node.getChild(i);
+ if (child.itemId == aId) {
+ node.containerOpen = false;
+ return child;
+ }
+ }
+ node.containerOpen = false;
+ ok(false, "Unable to find child node");
+ return null;
+ }
+
+ for (var i = 0; i < this.folders.length; i++) {
+ var id = this.folders[i];
+
+ var node = getRootChildNode(id);
+ isnot(node, null, "Node found");
+ is(PlacesControllerDragHelper.canMoveNode(node),
+ false, "shouldn't be able to move special folder node");
+
+ var shortcutId = this.shortcuts[id];
+ var shortcutNode = rootNode.getChild(i);
+
+ is(shortcutNode.itemId, shortcutId, "shortcut id and shortcut node item id match");
+
+ dump("can move shortcut node?\n");
+ is(PlacesControllerDragHelper.canMoveNode(shortcutNode),
+ true, "should be able to move special folder shortcut node");
+ }
+ }
+ });
+
+ // test that a tag container cannot be moved
+ tests.push({
+ populate: function() {
+ // tag a uri
+ this.uri = makeURI("http://foo.com");
+ PlacesUtils.tagging.tagURI(this.uri, ["bar"]);
+ registerCleanupFunction(() => PlacesUtils.tagging.untagURI(this.uri, ["bar"]));
+ },
+ validate: function() {
+ // get tag root
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY;
+ var tagsNode = PlacesUtils.history.executeQuery(query, options).root;
+
+ tagsNode.containerOpen = true;
+ is(tagsNode.childCount, 1, "has new tag");
+
+ var tagNode = tagsNode.getChild(0);
+
+ is(PlacesControllerDragHelper.canMoveNode(tagNode),
+ false, "should not be able to move tag container node");
+ tagsNode.containerOpen = false;
+ }
+ });
+
+ tests.forEach(function(aTest) {
+ PlacesUtils.bookmarks.removeFolderChildren(rootId);
+ aTest.populate();
+ aTest.validate();
+ });
+
+ rootNode.containerOpen = false;
+ PlacesUtils.bookmarks.removeItem(rootId);
+}
diff --git a/browser/components/places/tests/browser/browser_425884.js b/browser/components/places/tests/browser/browser_425884.js
new file mode 100644
index 000000000..655eb1ffd
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_425884.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ Deep copy of bookmark data, using the front-end codepath:
+
+ - create test folder A
+ - add a subfolder to folder A, and add items to it
+ - validate folder A (sanity check)
+ - copy folder A, creating new folder B, using the front-end path
+ - validate folder B
+ - undo copy transaction
+ - validate folder B (empty)
+ - redo copy transaction
+ - validate folder B's contents
+*/
+
+add_task(function* () {
+ // sanity check
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?");
+
+ let toolbarId = PlacesUtils.toolbarFolderId;
+ let toolbarNode = PlacesUtils.getFolderContents(toolbarId).root;
+
+ let oldCount = toolbarNode.childCount;
+ let testRoot = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "test root"
+ });
+ is(toolbarNode.childCount, oldCount+1, "confirm test root node is a container, and is empty");
+
+ let testRootNode = toolbarNode.getChild(toolbarNode.childCount-1);
+ testRootNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ testRootNode.containerOpen = true;
+ is(testRootNode.childCount, 0, "confirm test root node is a container, and is empty");
+
+ // create folder A, fill it, validate its contents
+ let folderA = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: testRoot.guid,
+ title: "A"
+ });
+
+ yield populate(folderA);
+
+ let folderAId = yield PlacesUtils.promiseItemId(folderA.guid);
+ let folderANode = PlacesUtils.getFolderContents(folderAId).root;
+ validate(folderANode);
+ is(testRootNode.childCount, 1, "create test folder");
+
+ // copy it, using the front-end helper functions
+ let serializedNode = PlacesUtils.wrapNode(folderANode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ let rawNode = PlacesUtils.unwrapNodes(serializedNode, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER).shift();
+ // confirm serialization
+ ok(rawNode.type, "confirm json node");
+ folderANode.containerOpen = false;
+
+ let testRootId = yield PlacesUtils.promiseItemId(testRoot.guid);
+ let transaction = PlacesUIUtils.makeTransaction(rawNode,
+ PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+ testRootId,
+ -1,
+ true);
+ ok(transaction, "create transaction");
+ PlacesUtils.transactionManager.doTransaction(transaction);
+ // confirm copy
+ is(testRootNode.childCount, 2, "create test folder via copy");
+
+ // validate the copy
+ let folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // undo the transaction, confirm the removal
+ PlacesUtils.transactionManager.undoTransaction();
+ is(testRootNode.childCount, 1, "confirm undo removed the copied folder");
+
+ // redo the transaction
+ PlacesUtils.transactionManager.redoTransaction();
+ is(testRootNode.childCount, 2, "confirm redo re-copied the folder");
+ folderBNode = testRootNode.getChild(1);
+ validate(folderBNode);
+
+ // Close containers, cleaning up their observers.
+ testRootNode.containerOpen = false;
+ toolbarNode.containerOpen = false;
+
+ // clean up
+ PlacesUtils.transactionManager.undoTransaction();
+ yield PlacesUtils.bookmarks.remove(folderA.guid);
+});
+
+var populate = Task.async(function* (parentFolder) {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: parentFolder.guid,
+ title: "test folder"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder.guid,
+ title: "test bookmark",
+ url: "http://foo"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: folder.guid
+ });
+});
+
+function validate(aNode) {
+ PlacesUtils.asContainer(aNode);
+ aNode.containerOpen = true;
+ is(aNode.childCount, 1, "confirm child count match");
+ var folderNode = aNode.getChild(0);
+ is(folderNode.title, "test folder", "confirm folder title");
+ PlacesUtils.asContainer(folderNode);
+ folderNode.containerOpen = true;
+ is(folderNode.childCount, 2, "confirm child count match");
+ folderNode.containerOpen = false;
+ aNode.containerOpen = false;
+}
diff --git a/browser/components/places/tests/browser/browser_435851_copy_query.js b/browser/components/places/tests/browser/browser_435851_copy_query.js
new file mode 100644
index 000000000..92f818b41
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_435851_copy_query.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* test that copying a non movable query or folder shortcut makes a new query with the same url, not a deep copy */
+
+const SHORTCUT_URL = "place:folder=2";
+const QUERY_URL = "place:sort=8&maxResults=10";
+
+add_task(function* copy_toolbar_shortcut() {
+ let library = yield promiseLibrary();
+
+ registerCleanupFunction(function () {
+ library.close();
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ library.PlacesOrganizer.selectLeftPaneQuery("BookmarksToolbar");
+
+ yield promiseClipboard(function () { library.PlacesOrganizer._places.controller.copy(); },
+ PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ library.ContentTree.view.controller.paste();
+
+ let toolbarCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(toolbarCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "copy is still a folder shortcut");
+
+ PlacesUtils.bookmarks.removeItem(toolbarCopyNode.itemId);
+ library.PlacesOrganizer.selectLeftPaneQuery("BookmarksToolbar");
+ is(library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "original is still a folder shortcut");
+});
+
+add_task(function* copy_history_query() {
+ let library = yield promiseLibrary();
+
+ library.PlacesOrganizer.selectLeftPaneQuery("History");
+
+ yield promiseClipboard(function () { library.PlacesOrganizer._places.controller.copy(); },
+ PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ library.ContentTree.view.controller.paste();
+
+ let historyCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(historyCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "copy is still a query");
+
+ PlacesUtils.bookmarks.removeItem(historyCopyNode.itemId);
+ library.PlacesOrganizer.selectLeftPaneQuery("History");
+ is(library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "original is still a query");
+});
diff --git a/browser/components/places/tests/browser/browser_475045.js b/browser/components/places/tests/browser/browser_475045.js
new file mode 100644
index 000000000..7d562349b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_475045.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
+// we only need EventUtils.js for a few files which is why we are using loadSubScript.
+var EventUtils = {};
+this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+add_task(function* test() {
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ if (toolbar.collapsed) {
+ yield promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function() {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+
+ // Setup the node we will use to be dropped. The actual node used does not
+ // matter because we will set its data, effect, and mimeType manually.
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ ok(placesItems, "PlacesToolbarItems should not be null");
+ ok(placesItems.localName == "scrollbox", "PlacesToolbarItems should not be null");
+ ok(placesItems.childNodes[0], "PlacesToolbarItems must have at least one child");
+
+ /**
+ * Simulates a drop of a URI onto the bookmarks bar.
+ *
+ * @param aEffect
+ * The effect to use for the drop operation: move, copy, or link.
+ * @param aMimeType
+ * The mime type to use for the drop operation.
+ */
+ let simulateDragDrop = function(aEffect, aMimeType) {
+ const uriSpec = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7";
+ let uri = makeURI(uriSpec);
+ EventUtils.synthesizeDrop(placesItems.childNodes[0],
+ placesItems,
+ [[{type: aMimeType,
+ data: uriSpec}]],
+ aEffect, window);
+
+ // Verify that the drop produces exactly one bookmark.
+ let bookmarkIds = PlacesUtils.bookmarks
+ .getBookmarkIdsForURI(uri);
+ ok(bookmarkIds.length == 1, "There should be exactly one bookmark");
+
+ PlacesUtils.bookmarks.removeItem(bookmarkIds[0]);
+
+ // Verify that we removed the bookmark successfully.
+ ok(!PlacesUtils.bookmarks.isBookmarked(uri), "URI should be removed");
+ }
+
+ // Simulate a bookmark drop for all of the mime types and effects.
+ let mimeTypes = ["text/plain", "text/unicode", "text/x-moz-url"];
+ let effects = ["move", "copy", "link"];
+ effects.forEach(function (effect) {
+ mimeTypes.forEach(function (mimeType) {
+ simulateDragDrop(effect, mimeType);
+ });
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_555547.js b/browser/components/places/tests/browser/browser_555547.js
new file mode 100644
index 000000000..0654139cb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_555547.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(function* test() {
+ let sidebarBox = document.getElementById("sidebar-box");
+ is(sidebarBox.hidden, true, "The sidebar should be hidden");
+
+ // Uncollapse the personal toolbar if needed.
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ yield promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ let sidebar = yield promiseLoadedSidebar("viewBookmarksSidebar");
+ registerCleanupFunction(() => {
+ SidebarUI.hide();
+ });
+
+ // Focus the tree and check if its controller is returned.
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ tree.focus();
+
+ let controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ let treeController = tree.controllers
+ .getControllerForCommand("placesCmd_copy");
+ ok(controller == treeController, "tree controller was returned");
+
+ // Open the context menu for a toolbar item, and check if the toolbar's
+ // controller is returned.
+ let toolbarItems = document.getElementById("PlacesToolbarItems");
+ EventUtils.synthesizeMouse(toolbarItems.childNodes[0],
+ 4, 4, { type: "contextmenu", button: 2 },
+ window);
+ controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ let toolbarController = document.getElementById("PlacesToolbar")
+ .controllers
+ .getControllerForCommand("placesCmd_copy");
+ ok(controller == toolbarController, "the toolbar controller was returned");
+
+ document.getElementById("placesContext").hidePopup();
+
+ // Now that the context menu is closed, try to get the tree controller again.
+ tree.focus();
+ controller = doGetPlacesControllerForCommand("placesCmd_copy");
+ ok(controller == treeController, "tree controller was returned");
+
+ if (wasCollapsed) {
+ yield promiseSetToolbarVisibility(toolbar, false);
+ }
+});
+
+function promiseLoadedSidebar(cmd) {
+ return new Promise(resolve => {
+ let sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function onLoad() {
+ sidebar.removeEventListener("load", onLoad, true);
+ resolve(sidebar);
+ }, true);
+
+ SidebarUI.show(cmd);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
new file mode 100644
index 000000000..a1f091ebe
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
@@ -0,0 +1,53 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 475529 - Add is the default button for the new folder dialog + " +
+ "Bug 1206376 - Changing properties of a new bookmark while adding it " +
+ "acts on the last bookmark in the current container");
+
+ // Add a new bookmark at index 0 in the unfiled folder.
+ let insertionIndex = 0;
+ let newBookmark = yield PlacesUtils.bookmarks.insert({
+ index: insertionIndex,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ });
+ let newBookmarkId = yield PlacesUtils.promiseItemId(newBookmark.guid);
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ // Select the new bookmark in the sidebar.
+ tree.selectItems([newBookmarkId]);
+ ok(tree.controller.isCommandEnabled("placesCmd_new:folder"),
+ "'placesCmd_new:folder' on current selected node is enabled");
+
+ // Create a new folder. Since the new bookmark is selected, and new items
+ // are inserted at the index of the currently selected item, the new folder
+ // will be inserted at index 0.
+ yield withBookmarksDialog(
+ false,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_new:folder");
+ },
+ function* test(dialogWin) {
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val =="n");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "n", dialogWin, false);
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ yield promiseTitleChangeNotification;
+
+ let newFolder = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: insertionIndex,
+ });
+
+ is(newFolder.title, "n", "folder name has been edited");
+ yield PlacesUtils.bookmarks.remove(newFolder);
+ yield PlacesUtils.bookmarks.remove(newBookmark);
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
new file mode 100644
index 000000000..5283a1610
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
@@ -0,0 +1,110 @@
+"use strict"
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser/keyword_form.html";
+
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form1 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let promiseKeywordNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "keyword" && val =="kw");
+
+ fillBookmarkTextField("editBMPanel_keywordField", "kw", dialogWin);
+
+ ok(!acceptBtn.disabled, "Accept button is enabled");
+
+ // The dialog is instant apply.
+ yield promiseKeywordNotification;
+
+ // After the notification, the keywords cache will update asynchronously.
+ info("Check the keyword entry has been created");
+ let entry;
+ yield waitForCondition(function* () {
+ entry = yield PlacesUtils.keywords.fetch("kw");
+ return !!entry;
+ }, "Unable to find the expected keyword");
+ is(entry.keyword, "kw", "keyword is correct");
+ is(entry.url.href, TEST_URL, "URL is correct");
+ is(entry.postData, "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s", "POST data is correct");
+
+ info("Check the charset has been saved");
+ let charset = yield PlacesUtils.getCharsetForURI(NetUtil.newURI(TEST_URL));
+ is(charset, "windows-1252", "charset is correct");
+
+ // Now check getShortcutOrURI.
+ let data = yield getShortcutOrURIAndPostData("kw test");
+ is(getPostDataString(data.postData), "accenti=\u00E0\u00E8\u00EC\u00F2\u00F9&search=test", "getShortcutOrURI POST data is correct");
+ is(data.url, TEST_URL, "getShortcutOrURI URL is correct");
+ });
+ });
+});
+
+add_task(function* reopen_same_field() {
+ yield PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw",
+ postData: "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s"
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.keywords.remove("kw");
+ });
+ // Reopening on the same input field should show the existing keyword.
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form1 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let elt = dialogWin.document.getElementById("editBMPanel_keywordField");
+ is(elt.value, "kw");
+ });
+ });
+});
+
+add_task(function* open_other_field() {
+ yield PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw2",
+ postData: "search%3D%25s"
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.keywords.remove("kw2");
+ });
+ // Reopening on another field of the same page that has different postData
+ // should not show the existing keyword.
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function* (browser) {
+ // We must wait for the context menu code to build metadata.
+ yield openContextMenuForContentSelector(browser, '#form2 > input[name="search"]');
+
+ yield withBookmarksDialog(true, AddKeywordForSearchField, function* (dialogWin) {
+ let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let elt = dialogWin.document.getElementById("editBMPanel_keywordField");
+ is(elt.value, "");
+ });
+ });
+});
+
+function getPostDataString(stream) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ sis.init(stream);
+ return sis.read(stream.available()).split("\n").pop();
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
new file mode 100644
index 000000000..d9f4c07d7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addLivemark.js
@@ -0,0 +1,39 @@
+"use strict"
+
+add_task(function* () {
+ info("Add a live bookmark editing its data");
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+ tree.selectItems([itemId]);
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ PlacesCommandHook.addLiveBookmark("http://livemark.com/",
+ "livemark", "description");
+ },
+ function* test(dialogWin) {
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val == "modified");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "modified", dialogWin);
+
+ yield promiseTitleChangeNotification;
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+
+ is(bookmark.title, "modified", "folder name has been edited");
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({
+ guid: bookmark.guid
+ });
+ is(livemark.feedURI.spec, "http://livemark.com/", "livemark has the correct url");
+ is(livemark.title, "modified", "livemark has the correct title");
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
new file mode 100644
index 000000000..fde9ea272
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
@@ -0,0 +1,71 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 479348 - Properties on a root should be read-only.");
+ let uri = NetUtil.newURI("http://example.com/");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ url: uri.spec,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ });
+
+ PlacesUtils.tagging.tagURI(uri, ["tag1"]);
+
+ let library = yield promiseLibrary();
+ let PlacesOrganizer = library.PlacesOrganizer;
+ registerCleanupFunction(function* () {
+ yield promiseLibraryClosed(library);
+ });
+
+ PlacesOrganizer.selectLeftPaneQuery("Tags");
+ let tree = PlacesOrganizer._places;
+ let tagsContainer = tree.selectedNode;
+ tagsContainer.containerOpen = true;
+ let fooTag = tagsContainer.getChild(0);
+ let tagNode = fooTag;
+ tree.selectNode(fooTag);
+ is(tagNode.title, 'tag1', "tagNode title is correct");
+
+ ok(tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is enabled");
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ function* test(dialogWin) {
+ // Check that the dialog is not read-only.
+ ok(!dialogWin.gEditItemOverlay.readOnly, "Dialog should not be read-only");
+
+ // Check that name picker is not read only
+ let namepicker = dialogWin.document.getElementById("editBMPanel_namePicker");
+ ok(!namepicker.readOnly, "Name field should not be read-only");
+ is(namepicker.value, "tag1", "Node title is correct");
+
+ let promiseTitleChangeNotification = promiseBookmarksNotification(
+ "onItemChanged", (itemId, prop, isAnno, val) => prop == "title" && val == "tag2");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin);
+
+ yield promiseTitleChangeNotification;
+
+ is(namepicker.value, "tag2", "Node title has been properly edited");
+
+ // Check the shortcut's title.
+ is(tree.selectedNode.title, "tag2", "The node has the correct title");
+
+ // Check the tags have been edited.
+ let tags = PlacesUtils.tagging.getTagsForURI(uri);
+ is(tags.length, 1, "Found the right number of tags");
+ ok(tags.includes("tag2"), "Found the expected tag");
+ }
+ );
+
+ // Check the tag change has been reverted.
+ let tags = PlacesUtils.tagging.getTagsForURI(uri);
+ is(tags.length, 1, "Found the right number of tags");
+ ok(tags.includes("tag1"), "Found the expected tag");
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
new file mode 100644
index 000000000..6f499888c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
@@ -0,0 +1,42 @@
+"use strict"
+
+add_task(function* () {
+ info("Bug 479348 - Properties on a root should be read-only.");
+
+ yield withSidebarTree("bookmarks", function* (tree) {
+ let itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
+ tree.selectItems([itemId]);
+ ok(tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is enabled");
+
+ yield withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ function* test(dialogWin) {
+ // Check that the dialog is read-only.
+ ok(dialogWin.gEditItemOverlay.readOnly, "Dialog is read-only");
+ // Check that accept button is disabled
+ let acceptButton = dialogWin.document.documentElement.getButton("accept");
+ ok(acceptButton.disabled, "Accept button is disabled");
+
+ // Check that name picker is read only
+ let namepicker = dialogWin.document.getElementById("editBMPanel_namePicker");
+ ok(namepicker.readOnly, "Name field is read-only");
+ is(namepicker.value,
+ PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
+ "Node title is correct");
+ // Blur the field and ensure root's name has not been changed.
+ namepicker.blur();
+ is(namepicker.value,
+ PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
+ "Root title is correct");
+ // Check the shortcut's title.
+ let bookmark = yield PlacesUtils.bookmarks.fetch(tree.selectedNode.bookmarkGuid);
+ is(bookmark.title, null,
+ "Shortcut title is null");
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_all_tabs.js b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
new file mode 100644
index 000000000..afd32b78a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
@@ -0,0 +1,37 @@
+/**
+ * Test for Bug 446171 - Name field of bookmarks saved via 'Bookmark All Tabs'
+ * has '(null)' value if history is disabled or just in private browsing mode
+ */
+"use strict"
+
+add_task(function* () {
+ const BASE_URL = "http://example.org/browser/browser/components/places/tests/browser/";
+ const TEST_PAGES = [
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html",
+ BASE_URL + "bookmark_dummy_1.html"
+ ];
+
+ function promiseAddTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ }
+
+ let tabs = yield Promise.all(TEST_PAGES.map(promiseAddTab));
+
+ let URIs = PlacesCommandHook.uniqueCurrentPages;
+ is(URIs.length, 3, "Only unique pages are returned");
+
+ Assert.deepEqual(URIs.map(URI => URI.uri.spec), [
+ "about:blank",
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html"
+ ], "Correct URIs are returned");
+
+ Assert.deepEqual(URIs.map(URI => URI.title), [
+ "New Tab", "Bookmark Dummy 1", "Bookmark Dummy 2"
+ ], "Correct titles are returned");
+
+ registerCleanupFunction(function* () {
+ yield Promise.all(tabs.map(BrowserTestUtils.removeTab));
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
new file mode 100644
index 000000000..85ce25311
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
@@ -0,0 +1,61 @@
+"use strict";
+
+const TEST_URL = 'http://example.com/browser/browser/components/places/tests/browser/pageopeningwindow.html';
+
+function makeBookmarkFor(url, keyword) {
+ return Promise.all([
+ PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url: url }),
+ PlacesUtils.keywords.insert({url: url,
+ keyword: keyword})
+ ]);
+
+}
+
+add_task(function* openKeywordBookmarkWithWindowOpen() {
+ // This is the current default, but let's not assume that...
+ yield new Promise((resolve, reject) => {
+ SpecialPowers.pushPrefEnv({ 'set': [[ 'browser.link.open_newwindow', 3 ],
+ [ 'dom.disable_open_during_load', true ]] },
+ resolve);
+ });
+
+ let moztab;
+ let tabOpened = BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla")
+ .then((tab) => { moztab = tab; });
+ let keywordForBM = "openmeatab";
+
+ let bookmarkInfo;
+ let bookmarkCreated =
+ makeBookmarkFor("javascript:void open('" + TEST_URL + "')", keywordForBM)
+ .then((values) => {
+ bookmarkInfo = values[0];
+ });
+ yield Promise.all([tabOpened, bookmarkCreated]);
+
+ registerCleanupFunction(function() {
+ return Promise.all([
+ PlacesUtils.bookmarks.remove(bookmarkInfo),
+ PlacesUtils.keywords.remove(keywordForBM)
+ ]);
+ });
+ gURLBar.value = keywordForBM;
+ gURLBar.focus();
+
+ let tabCreatedPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("Waiting for tab being created");
+ let {target: tab} = yield tabCreatedPromise;
+ info("Got tab");
+ let browser = tab.linkedBrowser;
+ if (!browser.currentURI || browser.currentURI.spec != TEST_URL) {
+ info("Waiting for browser load");
+ yield BrowserTestUtils.browserLoaded(browser);
+ }
+ is(browser.currentURI && browser.currentURI.spec, TEST_URL, "Tab with expected URL loaded.");
+ info("Waiting to remove tab");
+ yield Promise.all([ BrowserTestUtils.removeTab(tab),
+ BrowserTestUtils.removeTab(moztab) ]);
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarksProperties.js b/browser/components/places/tests/browser/browser_bookmarksProperties.js
new file mode 100644
index 000000000..f7f9f4762
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the bookmarks Properties dialog.
+ */
+
+// DOM ids of Places sidebar trees.
+const SIDEBAR_HISTORY_TREE_ID = "historyTree";
+const SIDEBAR_BOOKMARKS_TREE_ID = "bookmarks-view";
+
+const SIDEBAR_HISTORY_ID = "viewHistorySidebar";
+const SIDEBAR_BOOKMARKS_ID = "viewBookmarksSidebar";
+
+// For history sidebar.
+const SIDEBAR_HISTORY_BYLASTVISITED_VIEW = "bylastvisited";
+const SIDEBAR_HISTORY_BYMOSTVISITED_VIEW = "byvisited";
+const SIDEBAR_HISTORY_BYDATE_VIEW = "byday";
+const SIDEBAR_HISTORY_BYSITE_VIEW = "bysite";
+const SIDEBAR_HISTORY_BYDATEANDSITE_VIEW = "bydateandsite";
+
+// Action to execute on the current node.
+const ACTION_EDIT = 0;
+const ACTION_ADD = 1;
+
+// If action is ACTION_ADD, set type to one of those, to define what do you
+// want to create.
+const TYPE_FOLDER = 0;
+const TYPE_BOOKMARK = 1;
+
+const TEST_URL = "http://www.example.com/";
+
+const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xul";
+const DIALOG_URL_MINIMAL_UI = "chrome://browser/content/places/bookmarkProperties2.xul";
+
+Cu.import("resource:///modules/RecentWindow.jsm");
+var win = RecentWindow.getMostRecentBrowserWindow();
+var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+
+function add_bookmark(aURI) {
+ var bId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark/" + aURI.spec);
+ return bId;
+}
+
+// Each test is an obj w/ a desc property and run method.
+var gTests = [];
+var gCurrentTest = null;
+
+// ------------------------------------------------------------------------------
+// Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog
+gTests.push({
+ desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _itemId: null,
+ _cleanShutdown: false,
+
+ setup: function(aCallback) {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._itemId = add_bookmark(PlacesUtils._uri(TEST_URL));
+ ok(this._itemId > 0, "Correctly added a bookmark");
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Correctly added a tag");
+ aCallback();
+ },
+
+ selectNode: function(tree) {
+ tree.selectItems([PlacesUtils.unfiledBookmarksFolderId]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._itemId]);
+ is(tree.selectedNode.itemId, this._itemId, "Bookmark has been selected");
+ },
+
+ run: function() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById("editBMPanel_tagsField");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ tagsField.popup.removeEventListener("popuphidden", popupListener, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing Enter on the autocomplete popup");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ var popupListener = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine, we can stop observing the window.
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ var tree = tagsField.popup.tree;
+ // Focus and select first result.
+ isnot(tree, null, "Autocomplete results tree exists");
+ is(tree.view.rowCount, 1, "We have 1 autocomplete result");
+ tagsField.popup.selectedIndex = 0;
+ is(tree.view.selection.count, 1,
+ "We have selected a tag from the autocomplete popup");
+ info("About to focus the autocomplete results tree");
+ tree.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, self.window);
+ break;
+ default:
+ ok(false, "unknown event: " + aEvent.type);
+ return;
+ }
+ }
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ executeSoon(() => {
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ });
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["testTag"]);
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel
+
+gTests.push({
+ desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _itemId: null,
+ _cleanShutdown: false,
+
+ setup: function(aCallback) {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._itemId = add_bookmark(PlacesUtils._uri(TEST_URL));
+ ok(this._itemId > 0, "Correctly added a bookmark");
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Correctly added a tag");
+ aCallback();
+ },
+
+ selectNode: function(tree) {
+ tree.selectItems([PlacesUtils.unfiledBookmarksFolderId]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._itemId]);
+ is(tree.selectedNode.itemId, this._itemId, "Bookmark has been selected");
+ },
+
+ run: function() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById("editBMPanel_tagsField");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ tagsField.popup.removeEventListener("popuphidden", popupListener, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing Escape on the autocomplete popup");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ var popupListener = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine.
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ var tree = tagsField.popup.tree;
+ // Focus and select first result.
+ isnot(tree, null, "Autocomplete results tree exists");
+ is(tree.view.rowCount, 1, "We have 1 autocomplete result");
+ tagsField.popup.selectedIndex = 0;
+ is(tree.view.selection.count, 1,
+ "We have selected a tag from the autocomplete popup");
+ info("About to focus the autocomplete results tree");
+ tree.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ break;
+ default:
+ ok(false, "unknown event: " + aEvent.type);
+ return;
+ }
+ }
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(TEST_URL));
+ is(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL),
+ ["testTag"]);
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog
+
+gTests.push({
+ desc: " Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog",
+ sidebar: SIDEBAR_HISTORY_ID,
+ action: ACTION_ADD,
+ historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
+ window: null,
+
+ setup: function(aCallback) {
+ // Add a visit.
+ PlacesTestUtils.addVisits(
+ {uri: PlacesUtils._uri(TEST_URL),
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ).then(aCallback);
+ },
+
+ selectNode: function(tree) {
+ var visitNode = tree.view.nodeForTreeIndex(0);
+ tree.selectNode(visitNode);
+ is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
+ is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
+ },
+
+ run: function() {
+ // Open folder selector.
+ var foldersExpander = this.window.document.getElementById("editBMPanel_foldersExpander");
+ var folderTree = this.window.document.getElementById("editBMPanel_folderTree");
+ var self = this;
+
+ this.window.addEventListener("unload", function(event) {
+ self.window.removeEventListener("unload", arguments.callee, true);
+ ok(self._cleanShutdown, "Dialog window should not be closed by pressing ESC in folder name textbox");
+ executeSoon(function () {
+ self.finish();
+ });
+ }, true);
+
+ folderTree.addEventListener("DOMAttrModified", function onDOMAttrModified(event) {
+ if (event.attrName != "place")
+ return;
+ folderTree.removeEventListener("DOMAttrModified", arguments.callee, false);
+ executeSoon(function () {
+ // Create a new folder.
+ var newFolderButton = self.window.document.getElementById("editBMPanel_newFolderButton");
+ newFolderButton.doCommand();
+ ok(folderTree.hasAttribute("editing"),
+ "We are editing new folder name in folder tree");
+
+ // Press Escape to discard editing new folder name.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ ok(!folderTree.hasAttribute("editing"),
+ "We have finished editing folder name in folder tree");
+ self._cleanShutdown = true;
+ self.window.document.documentElement.cancelDialog();
+ });
+ }, false);
+ foldersExpander.doCommand();
+ },
+
+ finish: function() {
+ SidebarUI.hide();
+ runNextTest();
+ },
+
+ cleanup: function() {
+ return PlacesTestUtils.clearHistory();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ // This test can take some time, if we timeout too early it could run
+ // in the middle of other tests, or hang them.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // kick off tests
+ runNextTest();
+}
+
+function runNextTest() {
+ // Cleanup from previous test.
+ if (gCurrentTest) {
+ Promise.resolve(gCurrentTest.cleanup()).then(() => {
+ info("End of test: " + gCurrentTest.desc);
+ gCurrentTest = null;
+ waitForAsyncUpdates(runNextTest);
+ });
+ return;
+ }
+
+ if (gTests.length > 0) {
+ // Goto next tests.
+ gCurrentTest = gTests.shift();
+ info("Start of test: " + gCurrentTest.desc);
+ gCurrentTest.setup(function() {
+ execute_test_in_sidebar();
+ });
+ }
+ else {
+ // Finished all tests.
+ finish();
+ }
+}
+
+/**
+ * Global functions to run a test in Properties dialog context.
+ */
+
+function execute_test_in_sidebar() {
+ var sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ executeSoon(open_properties_dialog);
+ }, true);
+ SidebarUI.show(gCurrentTest.sidebar);
+}
+
+function open_properties_dialog() {
+ var sidebar = document.getElementById("sidebar");
+
+ // If this is history sidebar, set the required view.
+ if (gCurrentTest.sidebar == SIDEBAR_HISTORY_ID)
+ sidebar.contentDocument.getElementById(gCurrentTest.historyView).doCommand();
+
+ // Get sidebar's Places tree.
+ var sidebarTreeID = gCurrentTest.sidebar == SIDEBAR_BOOKMARKS_ID ?
+ SIDEBAR_BOOKMARKS_TREE_ID :
+ SIDEBAR_HISTORY_TREE_ID;
+ var tree = sidebar.contentDocument.getElementById(sidebarTreeID);
+ ok(tree, "Sidebar tree has been loaded");
+
+ // Ask current test to select the node to edit.
+ gCurrentTest.selectNode(tree);
+ ok(tree.selectedNode,
+ "We have a places node selected: " + tree.selectedNode.title);
+
+ // Wait for the Properties dialog.
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened")
+ return;
+ ww.unregisterNotification(windowObserver);
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ waitForFocus(() => {
+ // Windows has been loaded, execute our test now.
+ executeSoon(function () {
+ // Ensure overlay is loaded
+ ok(win.gEditItemOverlay.initialized, "EditItemOverlay is initialized");
+ gCurrentTest.window = win;
+ try {
+ gCurrentTest.run();
+ } catch (ex) {
+ ok(false, "An error occured during test run: " + ex.message);
+ }
+ });
+ }, win);
+ }
+ ww.registerNotification(windowObserver);
+
+ var command = null;
+ switch (gCurrentTest.action) {
+ case ACTION_EDIT:
+ command = "placesCmd_show:info";
+ break;
+ case ACTION_ADD:
+ if (gCurrentTest.sidebar == SIDEBAR_BOOKMARKS_ID) {
+ if (gCurrentTest.itemType == TYPE_FOLDER)
+ command = "placesCmd_new:folder";
+ else if (gCurrentTest.itemType == TYPE_BOOKMARK)
+ command = "placesCmd_new:bookmark";
+ else
+ ok(false, "You didn't set a valid itemType for adding an item");
+ }
+ else
+ command = "placesCmd_createBookmark";
+ break;
+ default:
+ ok(false, "You didn't set a valid action for this test");
+ }
+ // Ensure command is enabled for this node.
+ ok(tree.controller.isCommandEnabled(command),
+ " command '" + command + "' on current selected node is enabled");
+
+ // This will open the dialog.
+ tree.controller.doCommand(command);
+}
diff --git a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
new file mode 100644
index 000000000..1ab9411f3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const TEST_URL = "http://www.mozilla.org";
+const TEST_TITLE = "example_title";
+
+var gBookmarksToolbar = window.document.getElementById("PlacesToolbar");
+var dragDirections = { LEFT: 0, UP: 1, RIGHT: 2, DOWN: 3 };
+
+/**
+ * Tests dragging on toolbar.
+ *
+ * We must test these 2 cases:
+ * - Dragging toward left, top, right should start a drag.
+ * - Dragging toward down should should open the container if the item is a
+ * container, drag the item otherwise.
+ *
+ * @param aElement
+ * DOM node element we will drag
+ * @param aExpectedDragData
+ * Array of flavors and values in the form:
+ * [ ["text/plain: sometext", "text/html: <b>sometext</b>"], [...] ]
+ * Pass an empty array to check that drag even has been canceled.
+ * @param aDirection
+ * Direction for the dragging gesture, see dragDirections helper object.
+ */
+function synthesizeDragWithDirection(aElement, aExpectedDragData, aDirection, aCallback) {
+ // Dragstart listener function.
+ gBookmarksToolbar.addEventListener("dragstart", function(event)
+ {
+ info("A dragstart event has been trapped.");
+ var dataTransfer = event.dataTransfer;
+ is(dataTransfer.mozItemCount, aExpectedDragData.length,
+ "Number of dragged items should be the same.");
+
+ for (var t = 0; t < dataTransfer.mozItemCount; t++) {
+ var types = dataTransfer.mozTypesAt(t);
+ var expecteditem = aExpectedDragData[t];
+ is(types.length, expecteditem.length,
+ "Number of flavors for item " + t + " should be the same.");
+
+ for (var f = 0; f < types.length; f++) {
+ is(types[f], expecteditem[f].substring(0, types[f].length),
+ "Flavor " + types[f] + " for item " + t + " should be the same.");
+ is(dataTransfer.mozGetDataAt(types[f], t),
+ expecteditem[f].substring(types[f].length + 2),
+ "Contents for item " + t + " with flavor " + types[f] + " should be the same.");
+ }
+ }
+
+ if (!aExpectedDragData.length)
+ ok(event.defaultPrevented, "Drag has been canceled.");
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ gBookmarksToolbar.removeEventListener("dragstart", arguments.callee, false);
+
+ // This is likely to cause a click event, and, in case we are dragging a
+ // bookmark, an unwanted page visit. Prevent the click event.
+ aElement.addEventListener("click", prevent, false);
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mouseup" });
+ aElement.removeEventListener("click", prevent, false);
+
+ // Cleanup eventually opened menus.
+ if (aElement.localName == "menu" && aElement.open)
+ aElement.open = false;
+ aCallback()
+ }, false);
+
+ var prevent = function(aEvent) { aEvent.preventDefault(); }
+
+ var xIncrement = 0;
+ var yIncrement = 0;
+
+ switch (aDirection) {
+ case dragDirections.LEFT:
+ xIncrement = -1;
+ break;
+ case dragDirections.RIGHT:
+ xIncrement = +1;
+ break;
+ case dragDirections.UP:
+ yIncrement = -1;
+ break;
+ case dragDirections.DOWN:
+ yIncrement = +1;
+ break;
+ }
+
+ var rect = aElement.getBoundingClientRect();
+ var startingPoint = { x: (rect.right - rect.left)/2,
+ y: (rect.bottom - rect.top)/2 };
+
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x,
+ startingPoint.y,
+ { type: "mousedown" });
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 1,
+ startingPoint.y + yIncrement * 1,
+ { type: "mousemove" });
+ EventUtils.synthesizeMouse(aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mousemove" });
+}
+
+function getToolbarNodeForItemId(aItemId) {
+ var children = document.getElementById("PlacesToolbarItems").childNodes;
+ var node = null;
+ for (var i = 0; i < children.length; i++) {
+ if (aItemId == children[i]._placesNode.itemId) {
+ node = children[i];
+ break;
+ }
+ }
+ return node;
+}
+
+function getExpectedDataForPlacesNode(aNode) {
+ var wrappedNode = [];
+ var flavors = ["text/x-moz-place",
+ "text/x-moz-url",
+ "text/plain",
+ "text/html"];
+
+ flavors.forEach(function(aFlavor) {
+ var wrappedFlavor = aFlavor + ": " +
+ PlacesUtils.wrapNode(aNode, aFlavor);
+ wrappedNode.push(wrappedFlavor);
+ });
+
+ return [wrappedNode];
+}
+
+var gTests = [
+
+// ------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a folder on toolbar",
+ run: function() {
+ // Create a test folder to be dragged.
+ var folderId = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.toolbarFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ var element = getToolbarNodeForItemId(folderId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT,
+ function ()
+ {
+ info("Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT,
+ function ()
+ {
+ info("Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP,
+ function ()
+ {
+ info("Dragging down");
+ synthesizeDragWithDirection(element, new Array(), dragDirections.DOWN,
+ function () {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(folderId);
+ nextTest();
+ });
+ });
+ });
+ });
+ }
+ },
+
+// ------------------------------------------------------------------------------
+
+ {
+ desc: "Drag a bookmark on toolbar",
+ run: function() {
+ // Create a test bookmark to be dragged.
+ var itemId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ var element = getToolbarNodeForItemId(itemId);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(element._placesNode, null, "Toolbar node has an associated Places node.");
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT,
+ function ()
+ {
+ info("Dragging right");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.RIGHT,
+ function ()
+ {
+ info("Dragging up");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.UP,
+ function ()
+ {
+ info("Dragging down");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.DOWN,
+ function () {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeItem(itemId);
+ nextTest();
+ });
+ });
+ });
+ });
+ }
+ },
+];
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ waitForFocus(function() {
+ info("Start of test: " + test.desc);
+ test.run();
+ });
+ }
+ else if (wasCollapsed) {
+ // Collapse the personal toolbar if needed.
+ promiseSetToolbarVisibility(toolbar, false).then(finish);
+ } else {
+ finish();
+ }
+}
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, true).then(nextTest);
+ } else {
+ nextTest();
+ }
+}
+
diff --git a/browser/components/places/tests/browser/browser_forgetthissite_single.js b/browser/components/places/tests/browser/browser_forgetthissite_single.js
new file mode 100644
index 000000000..b1d7936e9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_forgetthissite_single.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_URIs = [
+ "http://www.mozilla.org/test1",
+ "http://www.mozilla.org/test2"
+];
+
+// This test makes sure that the Forget This Site command is hidden for multiple
+// selections.
+add_task(function* () {
+ // Add a history entry.
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+
+ let places = [];
+ let transition = PlacesUtils.history.TRANSITION_TYPED;
+ TEST_URIs.forEach(uri => places.push({uri: PlacesUtils._uri(uri), transition}));
+
+ yield PlacesTestUtils.addVisits(places);
+ yield testForgetThisSiteVisibility(1);
+ yield testForgetThisSiteVisibility(2);
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+});
+
+var testForgetThisSiteVisibility = Task.async(function* (selectionCount) {
+ let organizer = yield promiseLibrary();
+
+ // Select History in the left pane.
+ organizer.PlacesOrganizer.selectLeftPaneQuery("History");
+ let PO = organizer.PlacesOrganizer;
+ let histContainer = PO._places.selectedNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ histContainer.containerOpen = true;
+ PO._places.selectNode(histContainer.getChild(0));
+
+ // Select the first history entry.
+ let doc = organizer.document;
+ let tree = doc.getElementById("placeContent");
+ let selection = tree.view.selection;
+ selection.clearSelection();
+ selection.rangedSelect(0, selectionCount - 1, true);
+ is(selection.count, selectionCount, "The selected range is as big as expected");
+
+ // Open the context menu.
+ let contextmenu = doc.getElementById("placesContext");
+ let popupShown = promisePopupShown(contextmenu);
+
+ // Get cell coordinates.
+ let rect = tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "text");
+ // Initiate a context menu for the selected cell.
+ EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {type: "contextmenu", button: 2}, organizer);
+ yield popupShown;
+
+ let forgetThisSite = doc.getElementById("placesContext_deleteHost");
+ let hideForgetThisSite = (selectionCount != 1);
+ is(forgetThisSite.hidden, hideForgetThisSite,
+ `The Forget this site menu item should ${hideForgetThisSite ? "" : "not "}` +
+ ` be hidden with ${selectionCount} items selected`);
+
+ // Close the context menu.
+ contextmenu.hidePopup();
+
+ // Close the library window.
+ yield promiseLibraryClosed(organizer);
+});
+
+function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ popup.addEventListener("popupshown", function onShown() {
+ popup.removeEventListener("popupshown", onShown, true);
+ resolve();
+ }, true);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_history_sidebar_search.js b/browser/components/places/tests/browser/browser_history_sidebar_search.js
new file mode 100644
index 000000000..89472c4ab
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_history_sidebar_search.js
@@ -0,0 +1,64 @@
+add_task(function* test () {
+ let sidebar = document.getElementById("sidebar");
+
+ // Visited pages listed by descending visit date.
+ let pages = [
+ "http://sidebar.mozilla.org/a",
+ "http://sidebar.mozilla.org/b",
+ "http://sidebar.mozilla.org/c",
+ "http://www.mozilla.org/d",
+ ];
+
+ // Number of pages that will be filtered out by the search.
+ const FILTERED_COUNT = 1;
+
+ yield PlacesTestUtils.clearHistory();
+
+ // Add some visited page.
+ let time = Date.now();
+ let places = [];
+ for (let i = 0; i < pages.length; i++) {
+ places.push({ uri: NetUtil.newURI(pages[i]),
+ visitDate: (time - i) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(places);
+
+ yield withSidebarTree("history", function* () {
+ info("Set 'by last visited' view");
+ sidebar.contentDocument.getElementById("bylastvisited").doCommand();
+ let tree = sidebar.contentDocument.getElementById("historyTree");
+ check_tree_order(tree, pages);
+
+ // Set a search value.
+ let searchBox = sidebar.contentDocument.getElementById("search-box");
+ ok(searchBox, "search box is in context");
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+ check_tree_order(tree, pages, -FILTERED_COUNT);
+
+ info("Reset the search");
+ searchBox.value = "";
+ searchBox.doCommand();
+ check_tree_order(tree, pages);
+ });
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+function check_tree_order(tree, pages, aNumberOfRowsDelta = 0) {
+ let treeView = tree.view;
+ let columns = tree.columns;
+ is(columns.count, 1, "There should be only 1 column in the sidebar");
+
+ let found = 0;
+ for (let i = 0; i < treeView.rowCount; i++) {
+ let node = treeView.nodeForTreeIndex(i);
+ // We could inherit delayed visits from previous tests, skip them.
+ if (!pages.includes(node.uri))
+ continue;
+ is(node.uri, pages[i], "Node is in correct position based on its visit date");
+ found++;
+ }
+ ok(found, pages.length + aNumberOfRowsDelta, "Found all expected results");
+}
diff --git a/browser/components/places/tests/browser/browser_library_batch_delete.js b/browser/components/places/tests/browser/browser_library_batch_delete.js
new file mode 100644
index 000000000..6a907c70f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_batch_delete.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Library handles correctly batch deletes.
+ */
+
+const TEST_URL = "http://www.batch.delete.me/";
+
+var gTests = [];
+var gLibrary;
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Create and batch remove bookmarks",
+ run: function() {
+ let testURI = makeURI(TEST_URL);
+ PlacesUtils.history.runInBatchMode({
+ runBatched: function (aUserData) {
+ // Create a folder in unserted and populate it with bookmarks.
+ let folder = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "deleteme",
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "keepme",
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ for (let i = 0; i < 10; i++) {
+ PlacesUtils.bookmarks.insertBookmark(folder,
+ testURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bm" + i);
+ }
+ }
+ }, null);
+
+ // Select and open the left pane "History" query.
+ let PO = gLibrary.PlacesOrganizer;
+ PO.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(PO._places.selectedNode, null, "Selected unsorted bookmarks");
+
+ let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ unsortedNode.containerOpen = true;
+ is(unsortedNode.childCount, 2, "Unsorted node has 2 children");
+ let folderNode = unsortedNode.getChild(0);
+ is(folderNode.title, "deleteme", "Folder found in unsorted bookmarks");
+ // Check delete command is available.
+ PO._places.selectNode(folderNode);
+ is(PO._places.selectedNode.title, "deleteme", "Folder node selected");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+ // Execute the delete command and check bookmark has been removed.
+ PO._places.controller.doCommand("cmd_delete");
+ ok(!PlacesUtils.bookmarks.isBookmarked(testURI),
+ "Bookmark has been correctly removed");
+ // Test live update.
+ is(unsortedNode.childCount, 1, "Unsorted node has 1 child");
+ is(PO._places.selectedNode.title, "keepme", "Folder node selected");
+ unsortedNode.containerOpen = false;
+ nextTest();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Ensure correct selection and functionality in Library",
+ run: function() {
+ let PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
+ // Move selection forth and back.
+ PO.selectLeftPaneQuery("History");
+ PO.selectLeftPaneQuery("UnfiledBookmarks");
+ // Now select the "keepme" folder in the right pane and delete it.
+ ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0));
+ is(ContentTree.view.selectedNode.title, "keepme",
+ "Found folder in content pane");
+ // Test live update.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ makeURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bm");
+ is(ContentTree.view.result.root.childCount, 2,
+ "Right pane was correctly updated");
+ nextTest();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(function () {
+ PlacesUtils.bookmarks
+ .removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ gLibrary = openLibrary(nextTest);
+}
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ info("Start of test: " + test.desc);
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ finish();
+ }
+}
diff --git a/browser/components/places/tests/browser/browser_library_commands.js b/browser/components/places/tests/browser/browser_library_commands.js
new file mode 100644
index 000000000..e3bb75a34
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -0,0 +1,235 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test enabled commands in the left pane folder of the Library.
+ */
+
+const TEST_URI = NetUtil.newURI("http://www.mozilla.org/");
+
+registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_date_container() {
+ let library = yield promiseLibrary();
+ info("Ensure date containers under History cannot be cut but can be deleted");
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+
+ // Select and open the left pane "History" query.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('History');
+ isnot(PO._places.selectedNode, null, "We correctly selected History");
+
+ // Check that both delete and cut commands are disabled, cause this is
+ // a child of the left pane folder.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+ let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ historyNode.containerOpen = true;
+
+ // Check that we have a child container. It is "Today" container.
+ is(historyNode.childCount, 1, "History node has one child");
+ let todayNode = historyNode.getChild(0);
+ let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
+ is(todayNode.title, todayNodeExpectedTitle,
+ "History child is the expected container");
+
+ // Select "Today" container.
+ PO._places.selectNode(todayNode);
+ is(PO._places.selectedNode, todayNode,
+ "We correctly selected Today container");
+ // Check that delete command is enabled but cut command is disabled, cause
+ // this is an history item.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ // Execute the delete command and check visit has been removed.
+ let promiseURIRemoved = promiseHistoryNotification("onDeleteURI",
+ v => TEST_URI.equals(v));
+ PO._places.controller.doCommand("cmd_delete");
+ yield promiseURIRemoved;
+
+ // Test live update of "History" query.
+ is(historyNode.childCount, 0, "History node has no more children");
+
+ historyNode.containerOpen = false;
+
+ ok(!(yield promiseIsURIVisited(TEST_URI)), "Visit has been removed");
+
+ library.close();
+});
+
+add_task(function* test_query_on_toolbar() {
+ let library = yield promiseLibrary();
+ info("Ensure queries can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('BookmarksToolbar');
+ isnot(PO._places.selectedNode, null, "We have a valid selection");
+ is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+ PlacesUtils.toolbarFolderId,
+ "We have correctly selected bookmarks toolbar node.");
+
+ // Check that both cut and delete commands are disabled, cause this is a child
+ // of AllBookmarksFolderId.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+
+ let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ toolbarNode.containerOpen = true;
+
+ // Add an History query to the toolbar.
+ let query = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:sort=4",
+ title: "special_query",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0 });
+
+ // Get first child and check it is the just inserted query.
+ ok(toolbarNode.childCount > 0, "Toolbar node has children");
+ let queryNode = toolbarNode.getChild(0);
+ is(queryNode.title, "special_query", "Query node is correctly selected");
+
+ // Select query node.
+ PO._places.selectNode(queryNode);
+ is(PO._places.selectedNode, queryNode, "We correctly selected query node");
+
+ // Check that both cut and delete commands are enabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ // Execute the delete command and check bookmark has been removed.
+ let promiseItemRemoved = promiseBookmarksNotification("onItemRemoved",
+ (...args) => query.guid == args[5]);
+ PO._places.controller.doCommand("cmd_delete");
+ yield promiseItemRemoved;
+
+ is((yield PlacesUtils.bookmarks.fetch(query.guid)), null,
+ "Query node bookmark has been correctly removed");
+
+ toolbarNode.containerOpen = false;
+
+ library.close();
+});
+
+add_task(function* test_search_contents() {
+ yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0 });
+
+ let library = yield promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('BookmarksToolbar');
+ isnot(PO._places.selectedNode, null, "We have a valid selection");
+ is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+ PlacesUtils.toolbarFolderId,
+ "We have correctly selected bookmarks toolbar node.");
+
+ let searchBox = library.document.getElementById("searchFilter");
+ searchBox.value = "example";
+ library.PlacesSearchBox.search(searchBox.value);
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+ // Check that both cut and delete commands are enabled.
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ library.close();
+});
+
+add_task(function* test_tags() {
+ yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0 });
+ PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]);
+
+ let library = yield promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneQuery('Tags');
+ let tagsNode = PO._places.selectedNode;
+ isnot(tagsNode, null, "We have a valid selection");
+ let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+ is(tagsNode.title, tagsTitle,
+ "Tags has been properly selected");
+
+ // Check that both cut and delete commands are disabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled");
+
+ // Now select the tag.
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ let tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+ is(PO._places.selectedNode.title, "test",
+ "The created tag has been properly selected");
+
+ // Check that cut is disabled but delete is enabled.
+ ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+
+ // Check that both cut and delete commands are enabled.
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled");
+ ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled");
+ ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled");
+
+ tagsNode.containerOpen = false;
+
+ library.close();
+});
diff --git a/browser/components/places/tests/browser/browser_library_downloads.js b/browser/components/places/tests/browser/browser_library_downloads.js
new file mode 100644
index 000000000..81daadd71
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_downloads.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests bug 564900: Add folder specifically for downloads to Library left pane.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=564900
+ * This test visits various pages then opens the Library and ensures
+ * that both the Downloads folder shows up and that the correct visits
+ * are shown in it.
+ */
+
+var now = Date.now();
+
+function test() {
+ waitForExplicitFinish();
+
+ let onLibraryReady = function(win) {
+ // Add visits to compare contents with.
+ let places = [
+ { uri: NetUtil.newURI("http://mozilla.com"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_TYPED) ]
+ },
+ { uri: NetUtil.newURI("http://google.com"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_DOWNLOAD) ]
+ },
+ { uri: NetUtil.newURI("http://en.wikipedia.org"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_TYPED) ]
+ },
+ { uri: NetUtil.newURI("http://ubuntu.org"),
+ visits: [ new VisitInfo(PlacesUtils.history.TRANSITION_DOWNLOAD) ]
+ },
+ ]
+ PlacesUtils.asyncHistory.updatePlaces(places, {
+ handleResult: function () {},
+ handleError: function () {
+ ok(false, "gHistory.updatePlaces() failed");
+ },
+ handleCompletion: function () {
+ // Make sure Downloads is present.
+ isnot(win.PlacesOrganizer._places.selectedNode, null,
+ "Downloads is present and selected");
+
+
+ // Check results.
+ let contentRoot = win.ContentArea.currentView.result.root;
+ let len = contentRoot.childCount;
+ const TEST_URIS = ["http://ubuntu.org/", "http://google.com/"];
+ for (let i = 0; i < len; i++) {
+ is(contentRoot.getChild(i).uri, TEST_URIS[i],
+ "Comparing downloads shown at index " + i);
+ }
+
+ win.close();
+ PlacesTestUtils.clearHistory().then(finish);
+ }
+ })
+ }
+
+ openLibrary(onLibraryReady, "Downloads");
+}
+
+function VisitInfo(aTransitionType)
+{
+ this.transitionType =
+ aTransitionType === undefined ?
+ PlacesUtils.history.TRANSITION_LINK : aTransitionType;
+ this.visitDate = now++ * 1000;
+}
+VisitInfo.prototype = {}
diff --git a/browser/components/places/tests/browser/browser_library_infoBox.js b/browser/components/places/tests/browser/browser_library_infoBox.js
new file mode 100644
index 000000000..17cd78f8c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test appropriate visibility of infoBoxExpanderWrapper and
+ * additionalInfoFields in infoBox section of library
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+var gTests = [];
+var gLibrary;
+
+// ------------------------------------------------------------------------------
+
+gTests.push({
+ desc: "Bug 430148 - Remove or hide the more/less button in details pane...",
+ run: function() {
+ var PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
+ var infoBoxExpanderWrapper = getAndCheckElmtById("infoBoxExpanderWrapper");
+
+ function addVisitsCallback() {
+ // open all bookmarks node
+ PO.selectLeftPaneQuery("AllBookmarks");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected all bookmarks node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for all bookmarks node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history node
+ PO.selectLeftPaneQuery("History");
+ isnot(PO._places.selectedNode, null, "Correctly selected history node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history child node
+ var historyNode = PO._places.selectedNode.
+ QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ historyNode.containerOpen = true;
+ var childNode = historyNode.getChild(0);
+ isnot(childNode, null, "History node first child is not null.");
+ PO._places.selectNode(childNode);
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history child node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open history item
+ var view = ContentTree.view.view;
+ ok(view.rowCount > 0, "History item exists.");
+ view.selection.select(0);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for history item.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ historyNode.containerOpen = false;
+
+ // open bookmarks menu node
+ PO.selectLeftPaneQuery("BookmarksMenu");
+ isnot(PO._places.selectedNode, null,
+ "Correctly selected bookmarks menu node.");
+ checkInfoBoxSelected(PO);
+ ok(infoBoxExpanderWrapper.hidden,
+ "Expander button is hidden for bookmarks menu node.");
+ checkAddInfoFieldsCollapsed(PO);
+
+ // open recently bookmarked node
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ NetUtil.newURI("place:folder=BOOKMARKS_MENU" +
+ "&folder=UNFILED_BOOKMARKS" +
+ "&folder=TOOLBAR" +
+ "&queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
+ "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
+ "&maxResults=10" +
+ "&excludeQueries=1"),
+ 0, "Recent Bookmarks");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ NetUtil.newURI("http://mozilla.org/"),
+ 1, "Mozilla");
+ var menuNode = PO._places.selectedNode.
+ QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ menuNode.containerOpen = true;
+ childNode = menuNode.getChild(0);
+ isnot(childNode, null, "Bookmarks menu child node exists.");
+ is(childNode.title, "Recent Bookmarks",
+ "Correctly selected recently bookmarked node.");
+ PO._places.selectNode(childNode);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for recently bookmarked node.");
+ checkAddInfoFieldsNotCollapsed(PO);
+
+ // open first bookmark
+ view = ContentTree.view.view;
+ ok(view.rowCount > 0, "Bookmark item exists.");
+ view.selection.select(0);
+ checkInfoBoxSelected(PO);
+ ok(!infoBoxExpanderWrapper.hidden,
+ "Expander button is not hidden for bookmark item.");
+ checkAddInfoFieldsNotCollapsed(PO);
+ checkAddInfoFields(PO, "bookmark item");
+
+ menuNode.containerOpen = false;
+
+ PlacesTestUtils.clearHistory().then(nextTest);
+ }
+ // add a visit to browser history
+ PlacesTestUtils.addVisits(
+ { uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED }
+ ).then(addVisitsCallback);
+ }
+});
+
+function checkInfoBoxSelected(PO) {
+ is(getAndCheckElmtById("detailsDeck").selectedIndex, 1,
+ "Selected element in detailsDeck is infoBox.");
+}
+
+function checkAddInfoFieldsCollapsed(PO) {
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).collapsed,
+ "Additional info field correctly collapsed: #" + id);
+ });
+}
+
+function checkAddInfoFieldsNotCollapsed(PO) {
+ ok(PO._additionalInfoFields.some(function (id) {
+ return !getAndCheckElmtById(id).collapsed;
+ }), "Some additional info field correctly not collapsed");
+}
+
+function checkAddInfoFields(PO, nodeName) {
+ ok(true, "Checking additional info fields visibiity for node: " + nodeName);
+ var expanderButton = getAndCheckElmtById("infoBoxExpander");
+
+ // make sure additional fields are hidden by default
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden by default: #" + id);
+ });
+
+ // toggle fields and make sure they are hidden/unhidden as expected
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(!getAndCheckElmtById(id).hidden,
+ "Additional info field correctly unhidden after toggle: #" + id);
+ });
+ expanderButton.click();
+ PO._additionalInfoFields.forEach(function (id) {
+ ok(getAndCheckElmtById(id).hidden,
+ "Additional info field correctly hidden after toggle: #" + id);
+ });
+}
+
+function getAndCheckElmtById(id) {
+ var elmt = gLibrary.document.getElementById(id);
+ isnot(elmt, null, "Correctly got element: #" + id);
+ return elmt;
+}
+
+// ------------------------------------------------------------------------------
+
+function nextTest() {
+ if (gTests.length) {
+ var test = gTests.shift();
+ ok(true, "TEST: " + test.desc);
+ dump("TEST: " + test.desc + "\n");
+ test.run();
+ }
+ else {
+ // Close Library window.
+ gLibrary.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+
+ // Open Library.
+ openLibrary(function (library) {
+ gLibrary = library;
+ gLibrary.PlacesOrganizer._places.focus();
+ nextTest(gLibrary);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
new file mode 100644
index 000000000..7cea38f20
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_fixnames.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test we correctly fix broken Library left pane queries names.
+ */
+
+// Array of left pane queries objects, each one has the following properties:
+// name: query's identifier got from annotations,
+// itemId: query's itemId,
+// correctTitle: original and correct query's title.
+var leftPaneQueries = [];
+
+function onLibraryReady(organizer) {
+ // Check titles have been fixed.
+ for (var i = 0; i < leftPaneQueries.length; i++) {
+ var query = leftPaneQueries[i];
+ is(PlacesUtils.bookmarks.getItemTitle(query.itemId),
+ query.correctTitle, "Title is correct for query " + query.name);
+ if ("concreteId" in query) {
+ is(PlacesUtils.bookmarks.getItemTitle(query.concreteId),
+ query.concreteTitle, "Concrete title is correct for query " + query.name);
+ }
+ }
+
+ // Close Library window.
+ organizer.close();
+ // No need to cleanup anything, we have a correct left pane now.
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ // Ensure left pane is initialized.
+ ok(PlacesUIUtils.leftPaneFolderId > 0, "left pane folder is initialized");
+
+ // Get the left pane folder.
+ var leftPaneItems = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+
+ is(leftPaneItems.length, 1, "We correctly have only 1 left pane folder");
+ // Check version.
+ var version = PlacesUtils.annotations
+ .getItemAnnotation(leftPaneItems[0],
+ PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(version, PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, "Left pane version is actual");
+
+ // Get all left pane queries.
+ var items = PlacesUtils.annotations
+ .getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ // Get current queries names.
+ for (var i = 0; i < items.length; i++) {
+ var itemId = items[i];
+ var queryName = PlacesUtils.annotations
+ .getItemAnnotation(items[i],
+ PlacesUIUtils.ORGANIZER_QUERY_ANNO);
+ var query = { name: queryName,
+ itemId: itemId,
+ correctTitle: PlacesUtils.bookmarks.getItemTitle(itemId) }
+ switch (queryName) {
+ case "BookmarksToolbar":
+ query.concreteId = PlacesUtils.toolbarFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "BookmarksMenu":
+ query.concreteId = PlacesUtils.bookmarksMenuFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ case "UnfiledBookmarks":
+ query.concreteId = PlacesUtils.unfiledBookmarksFolderId;
+ query.concreteTitle = PlacesUtils.bookmarks.getItemTitle(query.concreteId);
+ break;
+ }
+ leftPaneQueries.push(query);
+ // Rename to a bad title.
+ PlacesUtils.bookmarks.setItemTitle(query.itemId, "badName");
+ if ("concreteId" in query)
+ PlacesUtils.bookmarks.setItemTitle(query.concreteId, "badName");
+ }
+
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Open Library, this will kick-off left pane code.
+ openLibrary(onLibraryReady);
+}
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
new file mode 100644
index 000000000..b90df120c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ waitForExplicitFinish();
+ openLibrary(onLibraryReady);
+}
+
+function onLibraryReady(aLibrary) {
+ let hierarchy = [ "AllBookmarks", "BookmarksMenu" ];
+
+ let folder1 = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.bookmarksMenuFolderId,
+ "Folder 1",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ hierarchy.push(folder1);
+ let folder2 = PlacesUtils.bookmarks
+ .createFolder(folder1, "Folder 2",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ hierarchy.push(folder2);
+ let bookmark = PlacesUtils.bookmarks
+ .insertBookmark(folder2, NetUtil.newURI("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark");
+
+ registerCleanupFunction(function() {
+ PlacesUtils.bookmarks.removeItem(folder1);
+ aLibrary.close();
+ });
+
+ aLibrary.PlacesOrganizer.selectLeftPaneContainerByHierarchy(hierarchy);
+
+ is(aLibrary.PlacesOrganizer._places.selectedNode.itemId, folder2,
+ "Found the expected left pane selected node");
+
+ is(aLibrary.ContentTree.view.view.nodeForTreeIndex(0).itemId, bookmark,
+ "Found the expected right pane contents");
+
+ finish();
+}
diff --git a/browser/components/places/tests/browser/browser_library_middleclick.js b/browser/components/places/tests/browser/browser_library_middleclick.js
new file mode 100644
index 000000000..894f89446
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_middleclick.js
@@ -0,0 +1,279 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /**
+ * Tests middle-clicking items in the Library.
+ */
+
+const ENABLE_HISTORY_PREF = "places.history.enabled";
+
+var gLibrary = null;
+var gTests = [];
+var gCurrentTest = null;
+
+// Listener for TabOpen and tabs progress.
+var gTabsListener = {
+ _loadedURIs: [],
+ _openTabsCount: 0,
+
+ handleEvent: function(aEvent) {
+ if (aEvent.type != "TabOpen")
+ return;
+
+ if (++this._openTabsCount == gCurrentTest.URIs.length) {
+ is(gBrowser.tabs.length, gCurrentTest.URIs.length + 1,
+ "We have opened " + gCurrentTest.URIs.length + " new tab(s)");
+ }
+
+ var tab = aEvent.target;
+ is(tab.ownerGlobal, window,
+ "Tab has been opened in current browser window");
+ },
+
+ onLocationChange: function(aBrowser, aWebProgress, aRequest, aLocationURI,
+ aFlags) {
+ var spec = aLocationURI.spec;
+ ok(true, spec);
+ // When a new tab is opened, location is first set to "about:blank", so
+ // we can ignore those calls.
+ // Ignore multiple notifications for the same URI too.
+ if (spec == "about:blank" || this._loadedURIs.includes(spec))
+ return;
+
+ ok(gCurrentTest.URIs.includes(spec),
+ "Opened URI found in list: " + spec);
+
+ if (gCurrentTest.URIs.includes(spec))
+ this._loadedURIs.push(spec);
+
+ if (this._loadedURIs.length == gCurrentTest.URIs.length) {
+ // We have correctly opened all URIs.
+
+ // Reset arrays.
+ this._loadedURIs.length = 0;
+
+ this._openTabsCount = 0;
+
+ executeSoon(function () {
+ // Close all tabs.
+ while (gBrowser.tabs.length > 1)
+ gBrowser.removeCurrentTab();
+
+ // Test finished. This will move to the next one.
+ waitForFocus(gCurrentTest.finish, gBrowser.ownerGlobal);
+ });
+ }
+ }
+}
+
+// ------------------------------------------------------------------------------
+// Open bookmark in a new tab.
+
+gTests.push({
+ desc: "Open bookmark in a new tab.",
+ URIs: ["about:buildconfig"],
+ _itemId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Add a new unsorted bookmark.
+ this._itemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri(this.URIs[0]),
+ bs.DEFAULT_INDEX,
+ "Title");
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var bookmarkNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(bookmarkNode.uri, this.URIs[0], "Found bookmark in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Open a folder in tabs.
+
+gTests.push({
+ desc: "Open a folder in tabs.",
+ URIs: ["about:buildconfig", "about:"],
+ _folderId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Create a new folder.
+ var folderId = bs.createFolder(bs.unfiledBookmarksFolder,
+ "Folder",
+ bs.DEFAULT_INDEX);
+ this._folderId = folderId;
+
+ // Add bookmarks in folder.
+ this.URIs.forEach(function(aURI) {
+ bs.insertBookmark(folderId,
+ PlacesUtils._uri(aURI),
+ bs.DEFAULT_INDEX,
+ "Title");
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Folder", "Found folder in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._folderId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+// Open a query in tabs.
+
+gTests.push({
+ desc: "Open a query in tabs.",
+ URIs: ["about:buildconfig", "about:"],
+ _folderId: -1,
+ _queryId: -1,
+
+ setup: function() {
+ var bs = PlacesUtils.bookmarks;
+ // Create a new folder.
+ var folderId = bs.createFolder(bs.unfiledBookmarksFolder,
+ "Folder",
+ bs.DEFAULT_INDEX);
+ this._folderId = folderId;
+
+ // Add bookmarks in folder.
+ this.URIs.forEach(function(aURI) {
+ bs.insertBookmark(folderId,
+ PlacesUtils._uri(aURI),
+ bs.DEFAULT_INDEX,
+ "Title");
+ });
+
+ // Create a bookmarks query containing our bookmarks.
+ var hs = PlacesUtils.history;
+ var options = hs.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ var query = hs.getNewQuery();
+ // The colon included in the terms selects only about: URIs. If not included
+ // we also may get pages like about.html included in the query result.
+ query.searchTerms = "about:";
+ var queryString = hs.queriesToQueryString([query], 1, options);
+ this._queryId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri(queryString),
+ 0, // It must be the first.
+ "Query");
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ isnot(gLibrary.PlacesOrganizer._places.selectedNode, null,
+ "We correctly have selection in the Library left pane");
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Query", "Found query in the right pane");
+ },
+
+ finish: function() {
+ setTimeout(runNextTest, 0);
+ },
+
+ cleanup: function() {
+ PlacesUtils.bookmarks.removeItem(this._folderId);
+ PlacesUtils.bookmarks.removeItem(this._queryId);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+function test() {
+ waitForExplicitFinish();
+ // Increase timeout, this test can be quite slow due to waitForFocus calls.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Add tabs listeners.
+ gBrowser.tabContainer.addEventListener("TabOpen", gTabsListener, false);
+ gBrowser.addTabsProgressListener(gTabsListener);
+
+ // Temporary disable history, so we won't record pages navigation.
+ gPrefService.setBoolPref(ENABLE_HISTORY_PREF, false);
+
+ // Open Library window.
+ openLibrary(function (library) {
+ gLibrary = library;
+ // Kick off tests.
+ runNextTest();
+ });
+}
+
+function runNextTest() {
+ // Cleanup from previous test.
+ if (gCurrentTest)
+ gCurrentTest.cleanup();
+
+ if (gTests.length > 0) {
+ // Goto next test.
+ gCurrentTest = gTests.shift();
+ info("Start of test: " + gCurrentTest.desc);
+ // Test setup will set Library so that the bookmark to be opened is the
+ // first node in the content (right pane) tree.
+ gCurrentTest.setup();
+
+ // Middle click on first node in the content tree of the Library.
+ gLibrary.focus();
+ waitForFocus(function() {
+ mouseEventOnCell(gLibrary.ContentTree.view, 0, 0, { button: 1 });
+ }, gLibrary);
+ }
+ else {
+ // No more tests.
+
+ // Close Library window.
+ gLibrary.close();
+
+ // Remove tabs listeners.
+ gBrowser.tabContainer.removeEventListener("TabOpen", gTabsListener, false);
+ gBrowser.removeTabsProgressListener(gTabsListener);
+
+ // Restore history.
+ try {
+ gPrefService.clearUserPref(ENABLE_HISTORY_PREF);
+ } catch (ex) {}
+
+ finish();
+ }
+}
+
+function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
+ var selection = aTree.view.selection;
+ selection.select(aRowIndex);
+ aTree.treeBoxObject.ensureRowIsVisible(aRowIndex);
+ var column = aTree.columns[aColumnIndex];
+
+ // get cell coordinates
+ var rect = aTree.treeBoxObject.getCoordsForCellItem(aRowIndex, column, "text");
+
+ EventUtils.synthesizeMouse(aTree.body, rect.x, rect.y,
+ aEventDetails, gLibrary);
+}
diff --git a/browser/components/places/tests/browser/browser_library_openFlatContainer.js b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
new file mode 100644
index 000000000..167b33031
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test opening a flat container in the right pane even if its parent in the
+ * left pane is closed.
+ */
+
+add_task(function* () {
+ let folder = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "Folder",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let bookmark = PlacesUtils.bookmarks
+ .insertBookmark(folder, NetUtil.newURI("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark");
+
+ let library = yield promiseLibrary("AllBookmarks");
+ registerCleanupFunction(function () {
+ library.close();
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ // Select unfiled later, to ensure it's closed.
+ library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+ ok(!library.PlacesOrganizer._places.selectedNode.containerOpen,
+ "Unfiled container is closed");
+
+ let folderNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.itemId, folder,
+ "Found the expected folder in the right pane");
+ // Select the folder node in the right pane.
+ library.ContentTree.view.selectNode(folderNode);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view,
+ { clickCount: 2 });
+
+ is(library.ContentTree.view.view.nodeForTreeIndex(0).itemId, bookmark,
+ "Found the expected bookmark in the right pane");
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_leak.js b/browser/components/places/tests/browser/browser_library_open_leak.js
new file mode 100644
index 000000000..f002236a9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_leak.js
@@ -0,0 +1,23 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 474831
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=474831
+ *
+ * Tests for leaks caused by simply opening and closing the Places Library
+ * window. Opens the Places Library window, waits for it to load, closes it,
+ * and finishes.
+ */
+
+function test() {
+ waitForExplicitFinish();
+ openLibrary(function (win) {
+ ok(true, "Library has been correctly opened");
+ win.close();
+ finish();
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_panel_leak.js b/browser/components/places/tests/browser/browser_library_panel_leak.js
new file mode 100644
index 000000000..643a261fb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_panel_leak.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 433231 - Places Library leaks the nsGlobalWindow when closed with a
+ * history entry selected.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=433231
+ *
+ * STRs: Open Library, select an history entry in History, close Library.
+ * ISSUE: We were adding a bookmarks observer when editing a bookmark, when
+ * selecting an history entry the panel was not un-initialized, and
+ * since an histroy entry does not have an itemId, the observer was
+ * never removed.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+function test() {
+ function onLibraryReady(organizer) {
+ let contentTree = organizer.document.getElementById("placeContent");
+ isnot(contentTree, null, "Sanity check: placeContent tree should exist");
+ isnot(organizer.PlacesOrganizer, null, "Sanity check: PlacesOrganizer should exist");
+ isnot(organizer.gEditItemOverlay, null, "Sanity check: gEditItemOverlay should exist");
+
+ ok(organizer.gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+ isnot(organizer.gEditItemOverlay.itemId, -1, "Editing a bookmark");
+
+ // Select History in the left pane.
+ organizer.PlacesOrganizer.selectLeftPaneQuery('History');
+ // Select the first history entry.
+ let selection = contentTree.view.selection;
+ selection.clearSelection();
+ selection.rangedSelect(0, 0, true);
+ // Check the panel is editing the history entry.
+ is(organizer.gEditItemOverlay.itemId, -1, "Editing an history entry");
+ // Close Library window.
+ organizer.close();
+ // Clean up history.
+ PlacesTestUtils.clearHistory().then(finish);
+ }
+
+ waitForExplicitFinish();
+ // Add an history entry.
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ PlacesTestUtils.addVisits(
+ {uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ).then(() => {
+ openLibrary(onLibraryReady);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_search.js b/browser/components/places/tests/browser/browser_library_search.js
new file mode 100644
index 000000000..93af22363
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -0,0 +1,182 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 451151
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=451151
+ *
+ * Summary:
+ * Tests frontend Places Library searching -- search, search reset, search scope
+ * consistency.
+ *
+ * Details:
+ * Each test below
+ * 1. selects a folder in the left pane and ensures that the content tree is
+ * appropriately updated,
+ * 2. performs a search and ensures that the content tree is correct for the
+ * folder and search and that the search UI is visible and appropriate to
+ * folder,
+ * 5. resets the search and ensures that the content tree is correct and that
+ * the search UI is hidden, and
+ * 6. if folder scope was clicked, searches again and ensures folder scope
+ * remains selected.
+ */
+
+const TEST_URL = "http://dummy.mozilla.org/";
+const TEST_DOWNLOAD_URL = "http://dummy.mozilla.org/dummy.pdf";
+
+var gLibrary;
+
+var testCases = [
+ function allBookmarksScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.allBookmarksFolderId);
+ search(PlacesUIUtils.allBookmarksFolderId, "dummy", defScope);
+ },
+
+ function historyScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["History"]);
+ search(PlacesUIUtils.leftPaneQueries["History"], "dummy", defScope);
+ },
+
+ function downloadsScope() {
+ let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["Downloads"]);
+ search(PlacesUIUtils.leftPaneQueries["Downloads"], "dummy", defScope);
+ },
+];
+
+/**
+ * Returns the default search scope for a given folder.
+ *
+ * @param aFolderId
+ * the item ID of a node in the left pane's tree
+ * @return the default scope when the folder is newly selected
+ */
+function getDefaultScope(aFolderId) {
+ switch (aFolderId) {
+ case PlacesUIUtils.leftPaneQueries["History"]:
+ return "scopeBarHistory"
+ case PlacesUIUtils.leftPaneQueries["Downloads"]:
+ return "scopeBarDownloads";
+ default:
+ return "scopeBarAll";
+ }
+}
+
+/**
+ * Returns the single nsINavHistoryQuery represented by a given place URI.
+ *
+ * @param aPlaceURI
+ * a URI that represents a single query
+ * @return an nsINavHistoryQuery object
+ */
+function queryStringToQuery(aPlaceURI) {
+ let queries = {};
+ PlacesUtils.history.queryStringToQueries(aPlaceURI, queries, {}, {});
+ return queries.value[0];
+}
+
+/**
+ * Resets the search by clearing the search box's text and ensures that the
+ * search scope remains as expected.
+ *
+ * @param aExpectedScopeButtonId
+ * this button should be selected after the reset
+ */
+function resetSearch(aExpectedScopeButtonId) {
+ search(null, "", aExpectedScopeButtonId);
+}
+
+/**
+ * Performs a search for a given folder and search string and ensures that the
+ * URI of the right pane's content tree is as expected for the folder and search
+ * string. Also ensures that the search scope button is as expected after the
+ * search.
+ *
+ * @param aFolderId
+ * the item ID of a node in the left pane's tree
+ * @param aSearchStr
+ * the search text; may be empty to reset the search
+ * @param aExpectedScopeButtonId
+ * after searching the selected scope button should be this
+ */
+function search(aFolderId, aSearchStr, aExpectedScopeButtonId) {
+ let doc = gLibrary.document;
+ let folderTree = doc.getElementById("placesList");
+ let contentTree = doc.getElementById("placeContent");
+
+ // First, ensure that selecting the folder in the left pane updates the
+ // content tree properly.
+ if (aFolderId) {
+ folderTree.selectItems([aFolderId]);
+ isnot(folderTree.selectedNode, null,
+ "Sanity check: left pane tree should have selection after selecting!");
+
+ // getFolders() on a History query returns an empty array, so no use
+ // comparing against aFolderId in that case.
+ if (aFolderId !== PlacesUIUtils.leftPaneQueries["History"] &&
+ aFolderId !== PlacesUIUtils.leftPaneQueries["Downloads"]) {
+ // contentTree.place should be equal to contentTree.result.root.uri,
+ // but it's not until bug 476952 is fixed.
+ let query = queryStringToQuery(contentTree.result.root.uri);
+ is(query.getFolders()[0], aFolderId,
+ "Content tree's folder should be what was selected in the left pane");
+ }
+ }
+
+ // Second, ensure that searching updates the content tree and search UI
+ // properly.
+ let searchBox = doc.getElementById("searchFilter");
+ searchBox.value = aSearchStr;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+ let query = queryStringToQuery(contentTree.result.root.uri);
+ if (aSearchStr) {
+ is(query.searchTerms, aSearchStr,
+ "Content tree's searchTerms should be text in search box");
+ }
+ else {
+ is(query.hasSearchTerms, false,
+ "Content tree's searchTerms should not exist after search reset");
+ }
+}
+
+/**
+ * test() contains window-launching boilerplate that calls this to really kick
+ * things off. Add functions to the testCases array, and this will call them.
+ */
+function onLibraryAvailable() {
+ testCases.forEach(aTest => aTest());
+
+ gLibrary.close();
+ gLibrary = null;
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ PlacesTestUtils.clearHistory().then(finish);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ // Sanity:
+ ok(PlacesUtils, "PlacesUtils in context");
+
+ // Add visits, a bookmark and a tag.
+ PlacesTestUtils.addVisits(
+ [{ uri: PlacesUtils._uri(TEST_URL), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED },
+ { uri: PlacesUtils._uri(TEST_DOWNLOAD_URL), visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD }]
+ ).then(() => {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "dummy");
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
+
+ gLibrary = openLibrary(onLibraryAvailable);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_library_views_liveupdate.js b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
new file mode 100644
index 000000000..c78ed641a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
@@ -0,0 +1,300 @@
+/* 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 Library Left pane view for liveupdate.
+ */
+
+var gLibrary = null;
+
+function test() {
+ waitForExplicitFinish();
+ // This test takes quite some time, and timeouts frequently, so we require
+ // more time to run.
+ // See Bug 525610.
+ requestLongerTimeout(2);
+
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Open Library, we will check the left pane.
+ openLibrary(function (library) {
+ gLibrary = library;
+ startTest();
+ });
+}
+
+/**
+ * Adds bookmarks observer, and executes a bunch of bookmarks operations.
+ */
+function startTest() {
+ var bs = PlacesUtils.bookmarks;
+ // Add observers.
+ bs.addObserver(bookmarksObserver, false);
+ PlacesUtils.annotations.addObserver(bookmarksObserver, false);
+ var addedBookmarks = [];
+
+ // MENU
+ ok(true, "*** Acting on menu bookmarks");
+ var id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("http://bm1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bm1");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "bm2");
+ bs.setItemTitle(id, "bm2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.bookmarksMenuFolder,
+ "bmf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "bmf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://bmf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.bookmarksMenuFolder, 0);
+
+ // TOOLBAR
+ ok(true, "*** Acting on toolbar bookmarks");
+ bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("http://tb1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tb1");
+ bs.setItemTitle(id, "tb1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "tb2");
+ bs.setItemTitle(id, "tb2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.toolbarFolder,
+ "tbf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "tbf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://tbf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.toolbarFolder, 0);
+
+ // UNSORTED
+ ok(true, "*** Acting on unsorted bookmarks");
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("http://ub1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ub1");
+ bs.setItemTitle(id, "ub1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "ub2");
+ bs.setItemTitle(id, "ub2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "ubf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "ubf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://ubf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ubf1");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.unfiledBookmarksFolder, 0);
+
+ // Remove all added bookmarks.
+ addedBookmarks.forEach(function (aItem) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ bs.removeItem(aItem);
+ } catch (ex) {}
+ });
+
+ // Remove observers.
+ bs.removeObserver(bookmarksObserver);
+ PlacesUtils.annotations.removeObserver(bookmarksObserver);
+ finishTest();
+}
+
+/**
+ * Restores browser state and calls finish.
+ */
+function finishTest() {
+ // Close Library window.
+ gLibrary.close();
+ finish();
+}
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ , Ci.nsIAnnotationObserver
+ ]),
+
+ // nsIAnnotationObserver
+ onItemAnnotationSet: function() {},
+ onItemAnnotationRemoved: function() {},
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+ // nsINavBookmarkObserver
+ onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
+ aURI) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ switch (aItemType) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = aURI.spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onItemRemoved: function PSB_onItemRemoved(aItemId, aFolder, aIndex) {
+ var node = null;
+ [node, ] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ is(node, null, "Places node not found in left pane");
+ },
+
+ onItemMoved: function(aItemId,
+ aOldFolderId, aOldIndex,
+ aNewFolderId, aNewIndex, aItemType) {
+ var node = null;
+ var index = null;
+ [node, index] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places);
+ // Left pane should not be updated for normal bookmarks or separators.
+ switch (aItemType) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ var uriString = PlacesUtils.bookmarks.getBookmarkURI(aItemId).spec;
+ var isQuery = uriString.substr(0, 6) == "place:";
+ if (isQuery) {
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ break;
+ }
+ // Fallback to separator case if this is not a query.
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ is(node, null, "New Places node not added in left pane");
+ break;
+ default:
+ isnot(node, null, "Found new Places node in left pane");
+ ok(index >= 0, "Node is at index " + index);
+ }
+ },
+
+ onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PSB_onEndUpdateBatch() {},
+ onItemVisited: function() {},
+ onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "title") {
+ let validator = function(aTreeRowIndex) {
+ let tree = gLibrary.PlacesOrganizer._places;
+ let cellText = tree.view.getCellText(aTreeRowIndex,
+ tree.columns.getColumnAt(0));
+ return cellText == aNewValue;
+ }
+ let [node, , valid] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places, validator);
+ if (node) // Only visible nodes.
+ ok(valid, "Title cell value has been correctly updated");
+ }
+ }
+};
+
+
+/**
+ * Get places node and index for an itemId in a tree view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @param aTree
+ * Tree to search in.
+ * @param aValidator [optional]
+ * function to check row validity if found. Defaults to {return true;}.
+ * @returns [node, index, valid] or [null, null, false] if not found.
+ */
+function getNodeForTreeItem(aItemId, aTree, aValidator) {
+
+ function findNode(aContainerIndex) {
+ if (aTree.view.isContainerEmpty(aContainerIndex))
+ return [null, null, false];
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < aTree.view.rowCount; i++) {
+ var node = aTree.view.nodeForTreeIndex(i);
+
+ if (node.itemId == aItemId) {
+ // Minus one because we want relative index inside the container.
+ let valid = aValidator ? aValidator(i) : true;
+ return [node, i - aTree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+
+ // We have finished walking this container.
+ if (!aTree.view.hasNextSibling(aContainerIndex + 1, i))
+ break;
+ }
+ return [null, null, false]
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < aTree.view.rowCount; i++) {
+ // Open container.
+ aTree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ aTree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ return [null, null, false];
+}
diff --git a/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
new file mode 100644
index 000000000..02d564e28
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
@@ -0,0 +1,68 @@
+/**
+ * Tests that visits across frames are correctly represented in the database.
+ */
+
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser";
+const PAGE_URL = BASE_URL + "/framedPage.html";
+const LEFT_URL = BASE_URL + "/frameLeft.html";
+const RIGHT_URL = BASE_URL + "/frameRight.html";
+
+add_task(function* test() {
+ // We must wait for both frames to be loaded and the visits to be registered.
+ let deferredLeftFrameVisit = PromiseUtils.defer();
+ let deferredRightFrameVisit = PromiseUtils.defer();
+
+ Services.obs.addObserver(function observe(subject) {
+ Task.spawn(function* () {
+ let url = subject.QueryInterface(Ci.nsIURI).spec;
+ if (url == LEFT_URL ) {
+ is((yield getTransitionForUrl(url)), null,
+ "Embed visits should not get a database entry.");
+ deferredLeftFrameVisit.resolve();
+ }
+ else if (url == RIGHT_URL ) {
+ is((yield getTransitionForUrl(url)),
+ PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ "User activated visits should get a FRAMED_LINK transition.");
+ Services.obs.removeObserver(observe, "uri-visit-saved");
+ deferredRightFrameVisit.resolve();
+ }
+ });
+ }, "uri-visit-saved", false);
+
+ // Open a tab and wait for all the subframes to load.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+
+ // Wait for the left frame visit to be registered.
+ info("Waiting left frame visit");
+ yield deferredLeftFrameVisit.promise;
+
+ // Click on the link in the left frame to cause a page load in the
+ // right frame.
+ info("Clicking link");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+ content.frames[0].document.getElementById("clickme").click();
+ });
+
+ // Wait for the right frame visit to be registered.
+ info("Waiting right frame visit");
+ yield deferredRightFrameVisit.promise;
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+function* getTransitionForUrl(url) {
+ // Ensure all the transactions completed.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`
+ SELECT visit_type
+ FROM moz_historyvisits
+ JOIN moz_places h ON place_id = h.id
+ WHERE url_hash = hash(:url) AND url = :url`,
+ { url });
+ if (rows.length) {
+ return rows[0].getResultByName("visit_type");
+ }
+ return null;
+}
diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
new file mode 100644
index 000000000..80ed2eb2b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test makes sure that the items in the bookmarks and history sidebar
+// panels are clickable in both LTR and RTL modes.
+
+function test() {
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ const BOOKMARKS_SIDEBAR_ID = "viewBookmarksSidebar";
+ const BOOKMARKS_SIDEBAR_TREE_ID = "bookmarks-view";
+ const HISTORY_SIDEBAR_ID = "viewHistorySidebar";
+ const HISTORY_SIDEBAR_TREE_ID = "historyTree";
+ const TEST_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser/sidebarpanels_click_test_page.html";
+
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ info("Unexpected sidebar found - a previous test failed to cleanup correctly");
+ SidebarUI.hide();
+ }
+
+ let sidebar = document.getElementById("sidebar");
+ let tests = [];
+ let currentTest;
+
+ tests.push({
+ _itemID: null,
+ init: function(aCallback) {
+ // Add a bookmark to the Unfiled Bookmarks folder.
+ this._itemID = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, PlacesUtils._uri(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "test"
+ );
+ aCallback();
+ },
+ prepare: function() {
+ },
+ selectNode: function(tree) {
+ tree.selectItems([this._itemID]);
+ },
+ cleanup: function(aCallback) {
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ executeSoon(aCallback);
+ },
+ sidebarName: BOOKMARKS_SIDEBAR_ID,
+ treeName: BOOKMARKS_SIDEBAR_TREE_ID,
+ desc: "Bookmarks sidebar test"
+ });
+
+ tests.push({
+ init: function(aCallback) {
+ // Add a history entry.
+ let uri = PlacesUtils._uri(TEST_URL);
+ PlacesTestUtils.addVisits({
+ uri: uri, visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ }).then(aCallback);
+ },
+ prepare: function() {
+ sidebar.contentDocument.getElementById("byvisited").doCommand();
+ },
+ selectNode: function(tree) {
+ tree.selectNode(tree.view.nodeForTreeIndex(0));
+ is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
+ is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
+ },
+ cleanup: function(aCallback) {
+ PlacesTestUtils.clearHistory().then(aCallback);
+ },
+ sidebarName: HISTORY_SIDEBAR_ID,
+ treeName: HISTORY_SIDEBAR_TREE_ID,
+ desc: "History sidebar test"
+ });
+
+ function testPlacesPanel(preFunc, postFunc) {
+ currentTest.init(function() {
+ SidebarUI.show(currentTest.sidebarName);
+ });
+
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ executeSoon(function() {
+ currentTest.prepare();
+
+ if (preFunc)
+ preFunc();
+
+ function observer(aSubject, aTopic, aData) {
+ info("alert dialog observed as expected");
+ Services.obs.removeObserver(observer, "common-dialog-loaded");
+ Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
+
+ aSubject.Dialog.ui.button0.click();
+
+ executeSoon(function () {
+ SidebarUI.hide();
+ currentTest.cleanup(postFunc);
+ });
+ }
+ Services.obs.addObserver(observer, "common-dialog-loaded", false);
+ Services.obs.addObserver(observer, "tabmodal-dialog-loaded", false);
+
+ let tree = sidebar.contentDocument.getElementById(currentTest.treeName);
+
+ // Select the inserted places item.
+ currentTest.selectNode(tree);
+
+ synthesizeClickOnSelectedTreeCell(tree);
+ // Now, wait for the observer to catch the alert dialog.
+ // If something goes wrong, the test will time out at this stage.
+ // Note that for the history sidebar, the URL itself is not opened,
+ // and Places will show the load-js-data-url-error prompt as an alert
+ // box, which means that the click actually worked, so it's good enough
+ // for the purpose of this test.
+ });
+ }, true);
+ }
+
+ function changeSidebarDirection(aDirection) {
+ sidebar.contentDocument.documentElement.style.direction = aDirection;
+ }
+
+ function runNextTest() {
+ // Remove eventual tabs created by previous sub-tests.
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabContainer.lastChild);
+ }
+
+ if (tests.length == 0) {
+ finish();
+ }
+ else {
+ // Create a new tab and run the test.
+ gBrowser.selectedTab = gBrowser.addTab();
+ currentTest = tests.shift();
+ testPlacesPanel(function() {
+ changeSidebarDirection("ltr");
+ info("Running " + currentTest.desc + " in LTR mode");
+ },
+ function() {
+ testPlacesPanel(function() {
+ // Run the test in RTL mode.
+ changeSidebarDirection("rtl");
+ info("Running " + currentTest.desc + " in RTL mode");
+ },
+ function() {
+ runNextTest();
+ });
+ });
+ }
+ }
+
+ // Ensure history is clean before starting the test.
+ PlacesTestUtils.clearHistory().then(runNextTest);
+}
diff --git a/browser/components/places/tests/browser/browser_sort_in_library.js b/browser/components/places/tests/browser/browser_sort_in_library.js
new file mode 100644
index 000000000..af9c35e59
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sort_in_library.js
@@ -0,0 +1,249 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the following bugs:
+ *
+ * Bug 443745 - View>Sort>of "alpha" sort items is default to Z>A instead of A>Z
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=443745
+ *
+ * Bug 444179 - Library>Views>Sort>Sort by Tags does nothing
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=444179
+ *
+ * Basically, fully tests sorting the placeContent tree in the Places Library
+ * window. Sorting is verified by comparing the nsINavHistoryResult returned by
+ * placeContent.result to the expected sort values.
+ */
+
+// Two properties of nsINavHistoryResult control the sort of the tree:
+// sortingMode and sortingAnnotation. sortingMode's value is one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants. sortingAnnotation is the
+// annotation used to sort for SORT_BY_ANNOTATION_* mode.
+//
+// This lookup table maps the possible values of anonid's of the treecols to
+// objects that represent the treecols' correct state after the user sorts the
+// previously unsorted tree by selecting a column from the Views > Sort menu.
+// sortingMode is constructed from the key and dir properties (i.e.,
+// SORT_BY_<key>_<dir>) and sortingAnnotation is checked against anno. anno
+// may be undefined if key is not "ANNOTATION".
+const SORT_LOOKUP_TABLE = {
+ title: { key: "TITLE", dir: "ASCENDING" },
+ tags: { key: "TAGS", dir: "ASCENDING" },
+ url: { key: "URI", dir: "ASCENDING" },
+ date: { key: "DATE", dir: "DESCENDING" },
+ visitCount: { key: "VISITCOUNT", dir: "DESCENDING" },
+ dateAdded: { key: "DATEADDED", dir: "DESCENDING" },
+ lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" },
+ description: { key: "ANNOTATION",
+ dir: "ASCENDING",
+ anno: "bookmarkProperties/description" }
+};
+
+// This is the column that's sorted if one is not specified and the tree is
+// currently unsorted. Set it to a key substring in the name of one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants, e.g., "TITLE", "URI".
+// Method ViewMenu.setSortColumn in browser/components/places/content/places.js
+// determines this value.
+const DEFAULT_SORT_KEY = "TITLE";
+
+// Part of the test is checking that sorts stick, so each time we sort we need
+// to remember it.
+var prevSortDir = null;
+var prevSortKey = null;
+
+/**
+ * Ensures that the sort of aTree is aSortingMode and aSortingAnno.
+ *
+ * @param aTree
+ * the tree to check
+ * @param aSortingMode
+ * one of the Ci.nsINavHistoryQueryOptions.SORT_BY_* constants
+ * @param aSortingAnno
+ * checked only if sorting mode is one of the
+ * Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_* constants
+ */
+function checkSort(aTree, aSortingMode, aSortingAnno) {
+ // The placeContent tree's sort is determined by the nsINavHistoryResult it
+ // stores. Get it and check that the sort is what the caller expects.
+ let res = aTree.result;
+ isnot(res, null,
+ "sanity check: placeContent.result should not return null");
+
+ // Check sortingMode.
+ is(res.sortingMode, aSortingMode,
+ "column should now have sortingMode " + aSortingMode);
+
+ // Check sortingAnnotation, but only if sortingMode is ANNOTATION.
+ if ([Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING].
+ indexOf(aSortingMode) >= 0) {
+ is(res.sortingAnnotation, aSortingAnno,
+ "column should now have sorting annotation " + aSortingAnno);
+ }
+}
+
+/**
+ * Sets the sort of aTree.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aTree
+ * the tree to sort
+ * @param aUnsortFirst
+ * true if the sort should be set to SORT_BY_NONE before sorting by aCol
+ * and aDir
+ * @param aShouldFail
+ * true if setSortColumn should fail on aCol or aDir
+ * @param aCol
+ * the column of aTree by which to sort
+ * @param aDir
+ * either "ascending" or "descending"
+ */
+function setSort(aOrganizerWin, aTree, aUnsortFirst, aShouldFail, aCol, aDir) {
+ if (aUnsortFirst) {
+ aOrganizerWin.ViewMenu.setSortColumn();
+ checkSort(aTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+
+ // Remember the sort key and direction.
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+
+ let failed = false;
+ try {
+ aOrganizerWin.ViewMenu.setSortColumn(aCol, aDir);
+
+ // Remember the sort key and direction.
+ if (!aCol && !aDir) {
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+ else {
+ if (aCol)
+ prevSortKey = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].key;
+ else if (prevSortKey === null)
+ prevSortKey = DEFAULT_SORT_KEY;
+
+ if (aDir)
+ prevSortDir = aDir.toUpperCase();
+ else if (prevSortDir === null)
+ prevSortDir = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].dir;
+ }
+ } catch (exc) {
+ failed = true;
+ }
+
+ is(failed, !!aShouldFail,
+ "setSortColumn on column " +
+ (aCol ? aCol.getAttribute("anonid") : "(no column)") +
+ " with direction " + (aDir || "(no direction)") +
+ " and table previously " + (aUnsortFirst ? "unsorted" : "sorted") +
+ " should " + (aShouldFail ? "" : "not ") + "fail");
+}
+
+/**
+ * Tries sorting by an invalid column and sort direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ */
+function testInvalid(aOrganizerWin, aPlaceContentTree) {
+ // Invalid column should fail by throwing an exception.
+ let bogusCol = document.createElement("treecol");
+ bogusCol.setAttribute("anonid", "bogusColumn");
+ setSort(aOrganizerWin, aPlaceContentTree, true, true, bogusCol, "ascending");
+
+ // Invalid direction reverts to SORT_BY_NONE.
+ setSort(aOrganizerWin, aPlaceContentTree, false, false, null, "bogus dir");
+ checkSort(aPlaceContentTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+}
+
+/**
+ * Tests sorting aPlaceContentTree by column only and then by both column
+ * and direction.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByColAndDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ let cols = aPlaceContentTree.getElementsByTagName("treecol");
+ ok(cols.length > 0, "sanity check: placeContent should contain columns");
+
+ for (let i = 0; i < cols.length; i++) {
+ let col = cols.item(i);
+ ok(col.hasAttribute("anonid"),
+ "sanity check: column " + col.id + " should have anonid");
+
+ let colId = col.getAttribute("anonid");
+ ok(colId in SORT_LOOKUP_TABLE,
+ "sanity check: unexpected placeContent column anonid");
+
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" +
+ (aUnsortFirst ? SORT_LOOKUP_TABLE[colId].dir : prevSortDir);
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ let expectedAnno = SORT_LOOKUP_TABLE[colId].anno || "";
+
+ // Test sorting by only a column.
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+
+ // Test sorting by both a column and a direction.
+ ["ascending", "descending"].forEach(function (dir) {
+ let sortConst =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, expectedAnno);
+ });
+ }
+}
+
+/**
+ * Tests sorting aPlaceContentTree by direction only.
+ *
+ * @param aOrganizerWin
+ * the Places window
+ * @param aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ ["ascending", "descending"].forEach(function (dir) {
+ let key = (aUnsortFirst ? DEFAULT_SORT_KEY : prevSortKey);
+ let sortConst = "SORT_BY_" + key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortConst];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, null, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, "");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ openLibrary(function (win) {
+ let tree = win.document.getElementById("placeContent");
+ isnot(tree, null, "sanity check: placeContent tree should exist");
+ // Run the tests.
+ testSortByColAndDir(win, tree, true);
+ testSortByColAndDir(win, tree, false);
+ testSortByDir(win, tree, true);
+ testSortByDir(win, tree, false);
+ testInvalid(win, tree);
+ // Reset the sort to SORT_BY_NONE.
+ setSort(win, tree, false, false);
+ // Close the window and finish.
+ win.close();
+ finish();
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
new file mode 100644
index 000000000..7a0eec22f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
@@ -0,0 +1,53 @@
+var bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
+var BMB_menuPopup = document.getElementById("BMB_bookmarksPopup");
+var BMB_showAllBookmarks = document.getElementById("BMB_bookmarksShowAll");
+var contextMenu = document.getElementById("placesContext");
+var newBookmarkItem = document.getElementById("placesContext_new:bookmark");
+
+waitForExplicitFinish();
+add_task(function* testPopup() {
+ info("Checking popup context menu before moving the bookmarks button");
+ yield checkPopupContextMenu();
+ let pos = CustomizableUI.getPlacementOfWidget("bookmarks-menu-button").position;
+ CustomizableUI.addWidgetToArea("bookmarks-menu-button", CustomizableUI.AREA_PANEL);
+ CustomizableUI.addWidgetToArea("bookmarks-menu-button", CustomizableUI.AREA_NAVBAR, pos);
+ info("Checking popup context menu after moving the bookmarks button");
+ yield checkPopupContextMenu();
+});
+
+function* checkPopupContextMenu() {
+ let dropmarker = document.getAnonymousElementByAttribute(bookmarksMenuButton, "anonid", "dropmarker");
+ BMB_menuPopup.setAttribute("style", "transition: none;");
+ let popupShownPromise = onPopupEvent(BMB_menuPopup, "shown");
+ EventUtils.synthesizeMouseAtCenter(dropmarker, {});
+ info("Waiting for bookmarks menu to be shown.");
+ yield popupShownPromise;
+ let contextMenuShownPromise = onPopupEvent(contextMenu, "shown");
+ EventUtils.synthesizeMouseAtCenter(BMB_showAllBookmarks, {type: "contextmenu", button: 2 });
+ info("Waiting for context menu on bookmarks menu to be shown.");
+ yield contextMenuShownPromise;
+ ok(!newBookmarkItem.hasAttribute("disabled"), "New bookmark item shouldn't be disabled");
+ let contextMenuHiddenPromise = onPopupEvent(contextMenu, "hidden");
+ contextMenu.hidePopup();
+ BMB_menuPopup.removeAttribute("style");
+ info("Waiting for context menu on bookmarks menu to be hidden.");
+ yield contextMenuHiddenPromise;
+ let popupHiddenPromise = onPopupEvent(BMB_menuPopup, "hidden");
+ // Can't use synthesizeMouseAtCenter because the dropdown panel is in the way
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ info("Waiting for bookmarks menu to be hidden.");
+ yield popupHiddenPromise;
+}
+
+function onPopupEvent(popup, evt) {
+ let fullEvent = "popup" + evt;
+ let deferred = new Promise.defer();
+ let onPopupHandler = (e) => {
+ if (e.target == popup) {
+ popup.removeEventListener(fullEvent, onPopupHandler);
+ deferred.resolve();
+ }
+ };
+ popup.addEventListener(fullEvent, onPopupHandler);
+ return deferred.promise;
+}
diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js
new file mode 100644
index 000000000..735d6b168
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -0,0 +1,475 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests Places views (menu, toolbar, tree) for liveupdate.
+ */
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, true).then(openBookmarksSidebar);
+ } else {
+ openBookmarksSidebar();
+ }
+}
+
+function openBookmarksSidebar() {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils in context");
+ ok(PlacesUIUtils, "PlacesUIUtils in context");
+
+ // Open bookmarks menu.
+ var popup = document.getElementById("bookmarksMenuPopup");
+ ok(popup, "Menu popup element exists");
+ fakeOpenPopup(popup);
+
+ // Open bookmarks sidebar.
+ var sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener("load", function() {
+ sidebar.removeEventListener("load", arguments.callee, true);
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ executeSoon(startTest);
+ }, true);
+ SidebarUI.show("viewBookmarksSidebar");
+}
+
+/**
+ * Simulates popup opening causing it to populate.
+ * We cannot just use menu.open, since it would not work on Mac due to native menubar.
+ */
+function fakeOpenPopup(aPopup) {
+ var popupEvent = document.createEvent("MouseEvent");
+ popupEvent.initMouseEvent("popupshowing", true, true, window, 0,
+ 0, 0, 0, 0, false, false, false, false,
+ 0, null);
+ aPopup.dispatchEvent(popupEvent);
+}
+
+/**
+ * Adds bookmarks observer, and executes a bunch of bookmarks operations.
+ */
+function startTest() {
+ var bs = PlacesUtils.bookmarks;
+ // Add observers.
+ bs.addObserver(bookmarksObserver, false);
+ PlacesUtils.annotations.addObserver(bookmarksObserver, false);
+ var addedBookmarks = [];
+
+ // MENU
+ info("*** Acting on menu bookmarks");
+ var id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("http://bm1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bm1");
+ bs.setItemTitle(id, "bm1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.bookmarksMenuFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "bm2");
+ bs.setItemTitle(id, "");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.bookmarksMenuFolder,
+ "bmf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "bmf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://bmf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bmf1");
+ bs.setItemTitle(id, "bmf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.bookmarksMenuFolder, 0);
+
+ // TOOLBAR
+ info("*** Acting on toolbar bookmarks");
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("http://tb1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tb1");
+ bs.setItemTitle(id, "tb1_edited");
+ addedBookmarks.push(id);
+ // Test live update of title.
+ bs.setItemTitle(id, "tb1_edited");
+ id = bs.insertBookmark(bs.toolbarFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "tb2");
+ bs.setItemTitle(id, "");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.toolbarFolder,
+ "tbf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "tbf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://tbf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "tbf1");
+ bs.setItemTitle(id, "tbf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.toolbarFolder, 0);
+
+ // UNSORTED
+ info("*** Acting on unsorted bookmarks");
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("http://ub1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "ub1");
+ bs.setItemTitle(id, "ub1_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ PlacesUtils._uri("place:"),
+ bs.DEFAULT_INDEX,
+ "ub2");
+ bs.setItemTitle(id, "ub2_edited");
+ addedBookmarks.push(id);
+ id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX);
+ addedBookmarks.push(id);
+ id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "ubf",
+ bs.DEFAULT_INDEX);
+ bs.setItemTitle(id, "ubf_edited");
+ addedBookmarks.push(id);
+ id = bs.insertBookmark(id,
+ PlacesUtils._uri("http://ubf1.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "bubf1");
+ bs.setItemTitle(id, "bubf1_edited");
+ addedBookmarks.push(id);
+ bs.moveItem(id, bs.unfiledBookmarksFolder, 0);
+
+ // Remove all added bookmarks.
+ addedBookmarks.forEach(function (aItem) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ bs.removeItem(aItem);
+ } catch (ex) {}
+ });
+
+ // Remove observers.
+ bs.removeObserver(bookmarksObserver);
+ PlacesUtils.annotations.removeObserver(bookmarksObserver);
+ finishTest();
+}
+
+/**
+ * Restores browser state and calls finish.
+ */
+function finishTest() {
+ // Close bookmarks sidebar.
+ SidebarUI.hide();
+
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ promiseSetToolbarVisibility(toolbar, false).then(finish);
+ } else {
+ finish();
+ }
+}
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ , Ci.nsIAnnotationObserver
+ ]),
+
+ // nsIAnnotationObserver
+ onItemAnnotationSet: function() {},
+ onItemAnnotationRemoved: function() {},
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+ // nsINavBookmarkObserver
+ onItemAdded: function PSB_onItemAdded(aItemId, aFolderId, aIndex,
+ aItemType, aURI) {
+ var views = getViewsForFolder(aFolderId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+
+ // Check that item has been added in the correct position.
+ for (var i = 0; i < views.length; i++) {
+ var [node, index] = searchItemInView(aItemId, views[i]);
+ isnot(node, null, "Found new Places node in " + views[i]);
+ is(index, aIndex, "Node is at index " + index);
+ }
+ },
+
+ onItemRemoved: function PSB_onItemRemoved(aItemId, aFolderId, aIndex,
+ aItemType) {
+ var views = getViewsForFolder(aFolderId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+ // Check that item has been removed.
+ for (var i = 0; i < views.length; i++) {
+ var node = null;
+ [node, ] = searchItemInView(aItemId, views[i]);
+ is(node, null, "Places node not found in " + views[i]);
+ }
+ },
+
+ onItemMoved: function(aItemId,
+ aOldFolderId, aOldIndex,
+ aNewFolderId, aNewIndex,
+ aItemType) {
+ var views = getViewsForFolder(aNewFolderId);
+ ok(views.length > 0, "Found affected views: " + views);
+
+ // Check that item has been moved in the correct position.
+ for (var i = 0; i < views.length; i++) {
+ var node = null;
+ var index = null;
+ [node, index] = searchItemInView(aItemId, views[i]);
+ isnot(node, null, "Found new Places node in " + views[i]);
+ is(index, aNewIndex, "Node is at index " + index);
+ }
+ },
+
+ onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PSB_onEndUpdateBatch() {},
+ onItemVisited: function() {},
+
+ onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue,
+ aLastModified, aItemType,
+ aParentId) {
+ if (aProperty !== "title")
+ return;
+
+ var views = getViewsForFolder(aParentId);
+ ok(views.length > 0, "Found affected views (" + views.length + "): " + views);
+
+ // Check that item has been moved in the correct position.
+ let validator = function(aElementOrTreeIndex) {
+ if (typeof(aElementOrTreeIndex) == "number") {
+ var sidebar = document.getElementById("sidebar");
+ var tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let cellText = tree.view.getCellText(aElementOrTreeIndex,
+ tree.columns.getColumnAt(0));
+ if (!aNewValue)
+ return cellText == PlacesUIUtils.getBestTitle(tree.view.nodeForTreeIndex(aElementOrTreeIndex), true);
+ return cellText == aNewValue;
+ }
+ if (!aNewValue && aElementOrTreeIndex.localName != "toolbarbutton") {
+ return aElementOrTreeIndex.getAttribute("label") == PlacesUIUtils.getBestTitle(aElementOrTreeIndex._placesNode);
+ }
+ return aElementOrTreeIndex.getAttribute("label") == aNewValue;
+ };
+
+ for (var i = 0; i < views.length; i++) {
+ var [node, , valid] = searchItemInView(aItemId, views[i], validator);
+ isnot(node, null, "Found changed Places node in " + views[i]);
+ is(node.title, aNewValue, "Node has correct title: " + aNewValue);
+ ok(valid, "Node element has correct label: " + aNewValue);
+ }
+ }
+};
+
+/**
+ * Search an item id in a view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @param aView
+ * either "toolbar", "menu" or "sidebar"
+ * @param aValidator
+ * function to check validity of the found node element.
+ * @returns [node, index, valid] or [null, null, false] if not found.
+ */
+function searchItemInView(aItemId, aView, aValidator) {
+ switch (aView) {
+ case "toolbar":
+ return getNodeForToolbarItem(aItemId, aValidator);
+ case "menu":
+ return getNodeForMenuItem(aItemId, aValidator);
+ case "sidebar":
+ return getNodeForSidebarItem(aItemId, aValidator);
+ }
+
+ return [null, null, false];
+}
+
+/**
+ * Get places node and index for an itemId in bookmarks toolbar view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForToolbarItem(aItemId, aValidator) {
+ var toolbar = document.getElementById("PlacesToolbarItems");
+
+ function findNode(aContainer) {
+ var children = aContainer.childNodes;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.itemId == aItemId) {
+ let valid = aValidator ? aValidator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.lastChild;
+ popup.showPopup(popup);
+ var foundNode = findNode(popup);
+ popup.hidePopup();
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ }
+ return [null, null];
+ }
+
+ return findNode(toolbar);
+}
+
+/**
+ * Get places node and index for an itemId in bookmarks menu view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForMenuItem(aItemId, aValidator) {
+ var menu = document.getElementById("bookmarksMenu");
+
+ function findNode(aContainer) {
+ var children = aContainer.childNodes;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.itemId == aItemId) {
+ let valid = aValidator ? aValidator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.lastChild;
+ fakeOpenPopup(popup);
+ var foundNode = findNode(popup);
+
+ child.open = false;
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ }
+ return [null, null, false];
+ }
+
+ return findNode(menu.lastChild);
+}
+
+/**
+ * Get places node and index for an itemId in sidebar tree view.
+ *
+ * @param aItemId
+ * item id of the item to search.
+ * @returns [node, index] or [null, null] if not found.
+ */
+function getNodeForSidebarItem(aItemId, aValidator) {
+ var sidebar = document.getElementById("sidebar");
+ var tree = sidebar.contentDocument.getElementById("bookmarks-view");
+
+ function findNode(aContainerIndex) {
+ if (tree.view.isContainerEmpty(aContainerIndex))
+ return [null, null, false];
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < tree.view.rowCount; i++) {
+ var node = tree.view.nodeForTreeIndex(i);
+
+ if (node.itemId == aItemId) {
+ // Minus one because we want relative index inside the container.
+ let valid = aValidator ? aValidator(i) : true;
+ return [node, i - tree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+
+ // We have finished walking this container.
+ if (!tree.view.hasNextSibling(aContainerIndex + 1, i))
+ break;
+ }
+ return [null, null, false]
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < tree.view.rowCount; i++) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null)
+ return foundNode;
+ }
+ return [null, null, false];
+}
+
+/**
+ * Get views affected by changes to a folder.
+ *
+ * @param aFolderId:
+ * item id of the folder we have changed.
+ * @returns a subset of views: ["toolbar", "menu", "sidebar"]
+ */
+function getViewsForFolder(aFolderId) {
+ var rootId = aFolderId;
+ while (!PlacesUtils.isRootItem(rootId))
+ rootId = PlacesUtils.bookmarks.getFolderIdForItem(rootId);
+
+ switch (rootId) {
+ case PlacesUtils.toolbarFolderId:
+ return ["toolbar", "sidebar"]
+ case PlacesUtils.bookmarksMenuFolderId:
+ return ["menu", "sidebar"]
+ case PlacesUtils.unfiledBookmarksFolderId:
+ return ["sidebar"]
+ }
+ return new Array();
+}
diff --git a/browser/components/places/tests/browser/frameLeft.html b/browser/components/places/tests/browser/frameLeft.html
new file mode 100644
index 000000000..5a54fe353
--- /dev/null
+++ b/browser/components/places/tests/browser/frameLeft.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Left frame</title>
+ </head>
+ <body>
+ <a id="clickme" href="frameRight.html" target="right">Open page in the right frame.</a>
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/frameRight.html b/browser/components/places/tests/browser/frameRight.html
new file mode 100644
index 000000000..226accc34
--- /dev/null
+++ b/browser/components/places/tests/browser/frameRight.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Right Frame</title>
+ </head>
+ <body>
+ This is the right frame.
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/framedPage.html b/browser/components/places/tests/browser/framedPage.html
new file mode 100644
index 000000000..d388562e6
--- /dev/null
+++ b/browser/components/places/tests/browser/framedPage.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>Framed page</title>
+ </head>
+ <frameset cols="*,*">
+ <frame name="left" src="frameLeft.html">
+ <frame name="right" src="about:mozilla">
+ </frameset>
+</html>
diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
new file mode 100644
index 000000000..aaf78332e
--- /dev/null
+++ b/browser/components/places/tests/browser/head.js
@@ -0,0 +1,460 @@
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+// We need to cache this before test runs...
+var cachedLeftPaneFolderIdGetter;
+var getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function") {
+ cachedLeftPaneFolderIdGetter = getter;
+}
+
+// ...And restore it when test ends.
+registerCleanupFunction(function() {
+ let getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ if (cachedLeftPaneFolderIdGetter && typeof(getter) != "function") {
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+ }
+});
+
+function openLibrary(callback, aLeftPaneRoot) {
+ let library = window.openDialog("chrome://browser/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable",
+ aLeftPaneRoot);
+ waitForFocus(function () {
+ callback(library);
+ }, library);
+
+ return library;
+}
+
+/**
+ * Returns a handle to a Library window.
+ * If one is opens returns itm otherwise it opens a new one.
+ *
+ * @param aLeftPaneRoot
+ * Hierarchy to open and select in the left pane.
+ */
+function promiseLibrary(aLeftPaneRoot) {
+ return new Promise(resolve => {
+ let library = Services.wm.getMostRecentWindow("Places:Organizer");
+ if (library && !library.closed) {
+ if (aLeftPaneRoot) {
+ library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+ }
+ resolve(library);
+ }
+ else {
+ openLibrary(resolve, aLeftPaneRoot);
+ }
+ });
+}
+
+function promiseLibraryClosed(organizer) {
+ return new Promise(resolve => {
+ // Wait for the Organizer window to actually be closed
+ organizer.addEventListener("unload", function onUnload() {
+ organizer.removeEventListener("unload", onUnload);
+ resolve();
+ });
+
+ // Close Library window.
+ organizer.close();
+ });
+}
+
+/**
+ * Waits for a clipboard operation to complete, looking for the expected type.
+ *
+ * @see waitForClipboard
+ *
+ * @param aPopulateClipboardFn
+ * Function to populate the clipboard.
+ * @param aFlavor
+ * Data flavor to expect.
+ */
+function promiseClipboard(aPopulateClipboardFn, aFlavor) {
+ return new Promise(resolve => {
+ waitForClipboard(data => !!data, aPopulateClipboardFn, resolve, aFlavor);
+ });
+}
+
+/**
+ * Waits for all pending async statements on the default connection, before
+ * proceeding with aCallback.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ * @param aScope
+ * Scope for the callback.
+ * @param aArguments
+ * Arguments array for the callback.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ * a write lock. Since all statements on the same connection are
+ * serialized, the end of this write operation means that all writes are
+ * complete. Note that WAL makes so that writers don't block readers, but
+ * this is a problem only across different connections.
+ */
+function waitForAsyncUpdates(aCallback, aScope, aArguments)
+{
+ let scope = aScope || this;
+ let args = aArguments || [];
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
+ begin.executeAsync();
+ begin.finalize();
+
+ let commit = db.createAsyncStatement("COMMIT");
+ commit.executeAsync({
+ handleResult: function() {},
+ handleError: function() {},
+ handleCompletion: function(aReason)
+ {
+ aCallback.apply(scope, args);
+ }
+ });
+ commit.finalize();
+}
+
+function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
+ let tbo = aTree.treeBoxObject;
+ if (tbo.view.selection.count != 1)
+ throw new Error("The test node should be successfully selected");
+ // Get selection rowID.
+ let min = {}, max = {};
+ tbo.view.selection.getRangeAt(0, min, max);
+ let rowID = min.value;
+ tbo.ensureRowIsVisible(rowID);
+ // Calculate the click coordinates.
+ var rect = tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text");
+ var x = rect.x + rect.width / 2;
+ var y = rect.y + rect.height / 2;
+ // Simulate the click.
+ EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
+ aTree.ownerGlobal);
+}
+
+/**
+ * Asynchronously check a url is visited.
+ *
+ * @param aURI The URI.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+function promiseBookmarksNotification(notification, conditionFn) {
+ info(`promiseBookmarksNotification: waiting for ${notification}`);
+ return new Promise((resolve) => {
+ let proxifiedObserver = new Proxy({}, {
+ get: (target, name) => {
+ if (name == "QueryInterface")
+ return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
+ info(`promiseBookmarksNotification: got ${name} notification`);
+ if (name == notification)
+ return (...args) => {
+ if (conditionFn.apply(this, args)) {
+ PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
+ executeSoon(resolve);
+ } else {
+ info(`promiseBookmarksNotification: skip cause condition doesn't apply to ${JSON.stringify(args)}`);
+ }
+ }
+ return () => {};
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(proxifiedObserver, false);
+ });
+}
+
+function promiseHistoryNotification(notification, conditionFn) {
+ info(`Waiting for ${notification}`);
+ return new Promise((resolve) => {
+ let proxifiedObserver = new Proxy({}, {
+ get: (target, name) => {
+ if (name == "QueryInterface")
+ return XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver ]);
+ if (name == notification)
+ return (...args) => {
+ if (conditionFn.apply(this, args)) {
+ PlacesUtils.history.removeObserver(proxifiedObserver, false);
+ executeSoon(resolve);
+ }
+ }
+ return () => {};
+ }
+ });
+ PlacesUtils.history.addObserver(proxifiedObserver, false);
+ });
+}
+
+/**
+ * Makes the specified toolbar visible or invisible and returns a Promise object
+ * that is resolved when the toolbar has completed any animations associated
+ * with hiding or showing the toolbar.
+ *
+ * Note that this code assumes that changes to a toolbar's visibility trigger
+ * a transition on the max-height property of the toolbar element.
+ * Changes to this styling could cause the returned Promise object to be
+ * resolved too early or not at all.
+ *
+ * @param aToolbar
+ * The toolbar to update.
+ * @param aVisible
+ * True to make the toolbar visible, false to make it hidden.
+ *
+ * @return {Promise}
+ * @resolves Any animation associated with updating the toolbar's visibility has
+ * finished.
+ * @rejects Never.
+ */
+function promiseSetToolbarVisibility(aToolbar, aVisible, aCallback) {
+ return new Promise((resolve, reject) => {
+ function listener(event) {
+ if (event.propertyName == "max-height") {
+ aToolbar.removeEventListener("transitionend", listener);
+ resolve();
+ }
+ }
+
+ let transitionProperties =
+ window.getComputedStyle(aToolbar).transitionProperty.split(", ");
+ if (isToolbarVisible(aToolbar) != aVisible &&
+ transitionProperties.some(
+ prop => prop == "max-height" || prop == "all"
+ )) {
+ // Just because max-height is a transitionable property doesn't mean
+ // a transition will be triggered, but it's more likely.
+ aToolbar.addEventListener("transitionend", listener);
+ setToolbarVisibility(aToolbar, aVisible);
+ return;
+ }
+
+ // No animation to wait for
+ setToolbarVisibility(aToolbar, aVisible);
+ resolve();
+ });
+}
+
+/**
+ * Helper function to determine if the given toolbar is in the visible
+ * state according to its autohide/collapsed attribute.
+ *
+ * @aToolbar The toolbar to query.
+ *
+ * @returns True if the relevant attribute on |aToolbar| indicates it is
+ * visible, false otherwise.
+ */
+function isToolbarVisible(aToolbar) {
+ let hidingAttribute = aToolbar.getAttribute("type") == "menubar"
+ ? "autohide"
+ : "collapsed";
+ let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase();
+ // Check for both collapsed="true" and collapsed="collapsed"
+ return hidingValue !== "true" && hidingValue !== hidingAttribute;
+}
+
+/**
+ * Executes a task after opening the bookmarks dialog, then cancels the dialog.
+ *
+ * @param autoCancel
+ * whether to automatically cancel the dialog at the end of the task
+ * @param openFn
+ * generator function causing the dialog to open
+ * @param task
+ * the task to execute once the dialog is open
+ */
+var withBookmarksDialog = Task.async(function* (autoCancel, openFn, taskFn) {
+ let closed = false;
+ let dialogPromise = new Promise(resolve => {
+ Services.ww.registerNotification(function winObserver(subject, topic, data) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function load() {
+ win.removeEventListener("load", load);
+ ok(win.location.href.startsWith("chrome://browser/content/places/bookmarkProperties"),
+ "The bookmark properties dialog is open");
+ // This is needed for the overlay.
+ waitForFocus(() => {
+ resolve(win);
+ }, win);
+ });
+ } else if (topic == "domwindowclosed") {
+ Services.ww.unregisterNotification(winObserver);
+ closed = true;
+ }
+ });
+ });
+
+ info("withBookmarksDialog: opening the dialog");
+ // The dialog might be modal and could block our events loop, so executeSoon.
+ executeSoon(openFn);
+
+ info("withBookmarksDialog: waiting for the dialog");
+ let dialogWin = yield dialogPromise;
+
+ // Ensure overlay is loaded
+ info("waiting for the overlay to be loaded");
+ yield waitForCondition(() => dialogWin.gEditItemOverlay.initialized,
+ "EditItemOverlay should be initialized");
+
+ // Check the first textbox is focused.
+ let doc = dialogWin.document;
+ let elt = doc.querySelector("textbox:not([collapsed=true])");
+ if (elt) {
+ info("waiting for focus on the first textfield");
+ yield waitForCondition(() => doc.activeElement == elt.inputField,
+ "The first non collapsed textbox should have been focused");
+ }
+
+ info("withBookmarksDialog: executing the task");
+ try {
+ yield taskFn(dialogWin);
+ } finally {
+ if (!closed) {
+ if (!autoCancel) {
+ ok(false, "The test should have closed the dialog!");
+ }
+ info("withBookmarksDialog: canceling the dialog");
+ doc.documentElement.cancelDialog();
+ }
+ }
+});
+
+/**
+ * Opens the contextual menu on the element pointed by the given selector.
+ *
+ * @param selector
+ * Valid selector syntax
+ * @return Promise
+ * Returns a Promise that resolves once the context menu has been
+ * opened.
+ */
+var openContextMenuForContentSelector = Task.async(function* (browser, selector) {
+ info("wait for the context menu");
+ let contextPromise = BrowserTestUtils.waitForEvent(document.getElementById("contentAreaContextMenu"),
+ "popupshown");
+ yield ContentTask.spawn(browser, { selector }, function* (args) {
+ let doc = content.document;
+ let elt = doc.querySelector(args.selector)
+ dump(`openContextMenuForContentSelector: found ${elt}\n`);
+
+ /* Open context menu so chrome can access the element */
+ const domWindowUtils =
+ content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ let rect = elt.getBoundingClientRect();
+ let left = rect.left + rect.width / 2;
+ let top = rect.top + rect.height / 2;
+ domWindowUtils.sendMouseEvent("contextmenu", left, top, 2,
+ 1, 0, false, 0, 0, true);
+ });
+ yield contextPromise;
+});
+
+/**
+ * Waits for a specified condition to happen.
+ *
+ * @param conditionFn
+ * a Function or a generator function, returning a boolean for whether
+ * the condition is fulfilled.
+ * @param errorMsg
+ * Error message to use if the condition has not been satisfied after a
+ * meaningful amount of tries.
+ */
+var waitForCondition = Task.async(function* (conditionFn, errorMsg) {
+ for (let tries = 0; tries < 100; ++tries) {
+ if ((yield conditionFn()))
+ return;
+ yield new Promise(resolve => {
+ if (!waitForCondition._timers) {
+ waitForCondition._timers = new Set();
+ registerCleanupFunction(() => {
+ is(waitForCondition._timers.size, 0, "All the wait timers have been removed");
+ delete waitForCondition._timers;
+ });
+ }
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ waitForCondition._timers.add(timer);
+ timer.init(() => {
+ waitForCondition._timers.delete(timer);
+ resolve();
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ });
+ }
+ ok(false, errorMsg);
+});
+
+/**
+ * Fills a bookmarks dialog text field ensuring to cause expected edit events.
+ *
+ * @param id
+ * id of the text field
+ * @param text
+ * text to fill in
+ * @param win
+ * dialog window
+ * @param [optional] blur
+ * whether to blur at the end.
+ */
+function fillBookmarkTextField(id, text, win, blur = true) {
+ let elt = win.document.getElementById(id);
+ elt.focus();
+ elt.select();
+ for (let c of text.split("")) {
+ EventUtils.synthesizeKey(c, {}, win);
+ }
+ if (blur)
+ elt.blur();
+}
+
+/**
+ * Executes a task after opening the bookmarks or history sidebar. Takes care
+ * of closing the sidebar once done.
+ *
+ * @param type
+ * either "bookmarks" or "history".
+ * @param taskFn
+ * The task to execute once the sidebar is ready. Will get the Places
+ * tree view as input.
+ */
+var withSidebarTree = Task.async(function* (type, taskFn) {
+ let sidebar = document.getElementById("sidebar");
+ info("withSidebarTree: waiting sidebar load");
+ let sidebarLoadedPromise = new Promise(resolve => {
+ sidebar.addEventListener("load", function load() {
+ sidebar.removeEventListener("load", load, true);
+ resolve();
+ }, true);
+ });
+ let sidebarId = type == "bookmarks" ? "viewBookmarksSidebar"
+ : "viewHistorySidebar";
+ SidebarUI.show(sidebarId);
+ yield sidebarLoadedPromise;
+
+ let treeId = type == "bookmarks" ? "bookmarks-view"
+ : "historyTree";
+ let tree = sidebar.contentDocument.getElementById(treeId);
+
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ info("withSidebarTree: executing the task");
+ try {
+ yield taskFn(tree);
+ } finally {
+ SidebarUI.hide();
+ }
+});
diff --git a/browser/components/places/tests/browser/keyword_form.html b/browser/components/places/tests/browser/keyword_form.html
new file mode 100644
index 000000000..a881c0d5a
--- /dev/null
+++ b/browser/components/places/tests/browser/keyword_form.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+</head>
+<body>
+ <form id="form1" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="àèìòù">
+ <input type="text" name="search">
+ </form>
+ <form id="form2" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="ùòìèà">
+ <input type="text" name="search">
+ </form>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/pageopeningwindow.html b/browser/components/places/tests/browser/pageopeningwindow.html
new file mode 100644
index 000000000..282f9c593
--- /dev/null
+++ b/browser/components/places/tests/browser/pageopeningwindow.html
@@ -0,0 +1,9 @@
+<meta charset="UTF-8">
+Hi, I was opened via a <script>document.write(location.search ?
+ "popup call from the opened window... uh oh, that shouldn't happen!" :
+ "bookmarklet, and I will open a new window myself.")</script><br>
+<script>
+ if (!location.search) {
+ open(location.href + "?donotopen=true", '_blank');
+ }
+</script>
diff --git a/browser/components/places/tests/browser/sidebarpanels_click_test_page.html b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
new file mode 100644
index 000000000..c73eaa540
--- /dev/null
+++ b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+ <title>browser_sidebarpanels_click.js test page</title>
+</head>
+<body onload="alert('test');">
+</body>
+</html>
diff --git a/browser/components/places/tests/chrome/.eslintrc.js b/browser/components/places/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/browser/components/places/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/chrome/chrome.ini b/browser/components/places/tests/chrome/chrome.ini
new file mode 100644
index 000000000..d7b4a55c8
--- /dev/null
+++ b/browser/components/places/tests/chrome/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files = head.js
+
+[test_0_bug510634.xul]
+[test_0_multiple_left_pane.xul]
+[test_bug1163447_selectItems_through_shortcut.xul]
+[test_bug427633_no_newfolder_if_noip.xul]
+[test_bug485100-change-case-loses-tag.xul]
+[test_bug549192.xul]
+[test_bug549491.xul]
+[test_bug631374_tags_selector_scroll.xul]
+[test_editBookmarkOverlay_keywords.xul]
+[test_editBookmarkOverlay_tags_liveUpdate.xul]
+[test_selectItems_on_nested_tree.xul]
+[test_treeview_date.xul]
diff --git a/browser/components/places/tests/chrome/head.js b/browser/components/places/tests/chrome/head.js
new file mode 100644
index 000000000..26b97f6d7
--- /dev/null
+++ b/browser/components/places/tests/chrome/head.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
diff --git a/browser/components/places/tests/chrome/test_0_bug510634.xul b/browser/components/places/tests/chrome/test_0_bug510634.xul
new file mode 100644
index 000000000..86e102180
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_0_bug510634.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="510634: Wrong icons on bookmarks sidebar"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 510634 - Wrong icons on bookmarks sidebar
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=510634
+ *
+ * Ensures that properties for special queries are set on their tree nodes,
+ * even if PlacesUIUtils.leftPaneFolderId was not initialized.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // We need to cache and restore this getter in order to simulate
+ // Bug 510634
+ let cachedLeftPaneFolderIdGetter =
+ PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
+ // Must also cache and restore this getter as it is affected by
+ // leftPaneFolderId, from bug 564900.
+ let cachedAllBookmarksFolderIdGetter =
+ PlacesUIUtils.__lookupGetter__("allBookmarksFolderId");
+
+ let leftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+
+ // restore the getter
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:queryType=1&folder=" + leftPaneFolderId;
+
+ // The query-property is set on the title column for each row.
+ let titleColumn = tree.treeBoxObject.columns.getColumnAt(0);
+
+ // Open All Bookmarks
+ tree.selectItems([PlacesUIUtils.leftPaneQueries["AllBookmarks"]]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ is(PlacesUIUtils.allBookmarksFolderId, tree.selectedNode.itemId,
+ "Opened All Bookmarks");
+
+ ["History", "Downloads", "Tags", "AllBookmarks", "BookmarksToolbar",
+ "BookmarksMenu", "UnfiledBookmarks"].forEach(
+ function(aQueryName, aRow) {
+ let found = false;
+ for (let i = 0; i < tree.view.rowCount && !found; i++) {
+ rowProperties = tree.view.getCellProperties(i, titleColumn).split(" ");
+ found = rowProperties.includes("OrganizerQuery_" + aQueryName);
+ }
+ ok(found, "OrganizerQuery_" + aQueryName + " is set");
+ }
+ );
+
+ // Close the root node
+ tree.result.root.containerOpen = false;
+
+ // Restore the getters for the next test.
+ PlacesUIUtils.__defineGetter__("leftPaneFolderId", cachedLeftPaneFolderIdGetter);
+ PlacesUIUtils.__defineGetter__("allBookmarksFolderId",
+ cachedAllBookmarksFolderIdGetter);
+
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul b/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul
new file mode 100644
index 000000000..09a4d2054
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_0_multiple_left_pane.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Bug 466422:
+ - Check that we replace the left pane with a correct one if it gets corrupted
+ - and we end up having more than one. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Test handling of multiple left pane folders"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ // Sanity checks.
+ ok(PlacesUtils, "PlacesUtils is running in chrome context");
+ ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
+ ok(PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION > 0,
+ "Left pane version in chrome context, " +
+ "current version is: " + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION );
+
+ let fakeLeftPanes = [];
+ // We need 2 left pane folders to simulate a corrupt profile.
+ do {
+ let leftPaneItems = PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+
+ // Create a fake left pane folder.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.rootGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ let fakeLeftPaneRoot = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations.setItemAnnotation(fakeLeftPaneRoot, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+ PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ fakeLeftPanes.push(folder.guid);
+ } while (fakeLeftPanes.length < 2);
+
+ // Initialize the left pane queries.
+ PlacesUIUtils.leftPaneFolderId;
+
+ // Check left pane.
+ ok(PlacesUIUtils.leftPaneFolderId > 0,
+ "Left pane folder correctly created");
+ let leftPaneItems = PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
+ is(leftPaneItems.length, 1,
+ "We correctly have only 1 left pane folder");
+
+ // Check that all old left pane items have been removed.
+ for (let guid of fakeLeftPanes) {
+ ok(!(yield PlacesUtils.bookmarks.fetch({guid})), "This folder should have been removed");
+ }
+ }).then(() => SimpleTest.finish());
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul
new file mode 100644
index 000000000..8e3a99533
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xul
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="1163447: selectItems in Places no longer selects items within Toolbar or Sidebar folders"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+
+ /**
+ * Bug 1163447: places-tree should be able to select an item within the toolbar, and
+ * unfiled bookmarks. Yet not follow recursive folder-shortcuts infinitely.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let bmu = PlacesUtils.bookmarks;
+
+ yield bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: "place:folder=TOOLBAR",
+ title: "shortcut to self - causing infinite recursion if not handled properly"
+ });
+
+ yield bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS",
+ title: "shortcut to unfiled, within toolbar"
+ });
+
+ let folder = yield bmu.insert({
+ parentGuid: bmu.unfiledGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_FOLDER,
+ title: "folder within unfiled"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:folder=TOOLBAR";
+
+ // Select the folder via the selectItems(itemId) API being tested
+ let itemId = yield PlacesUtils.promiseItemId(folder.guid);
+ tree.selectItems([itemId]);
+
+ is(tree.selectedNode && tree.selectedNode.itemId, itemId, "The node was selected through the shortcut");
+
+ // Cleanup
+ yield bmu.eraseEverything();
+
+ }).catch(err => {
+ ok(false, `Uncaught error: ${err}`);
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul b/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
new file mode 100644
index 000000000..b659b2b46
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if insertionPoint is invalid"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if
+ * insertionPoint is invalid.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://www.example.com/",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "mozilla"
+ });
+
+ // Init panel.
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+ ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ let tree = gEditItemOverlay._element("folderTree");
+ yield openFolderTree(tree);
+
+ tree.view.selection.clearSelection();
+ ok(document.getElementById("editBMPanel_newFolderButton").disabled,
+ "New folder button is disabled if there's no selection");
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ }).then(() => SimpleTest.finish());
+ }
+
+ function openFolderTree(tree) {
+ return new Promise(resolve => {
+ tree.addEventListener("DOMAttrModified", function onAttrModified(event) {
+ if (event.attrName == "place") {
+ tree.removeEventListener("DOMAttrModified", onAttrModified);
+ resolve();
+ }
+ });
+
+ // Open the folder tree.
+ document.getElementById("editBMPanel_foldersExpander").doCommand();
+ });
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul b/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
new file mode 100644
index 000000000..afad950cb
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="485100: Exchanging a letter of a tag name with its big/small equivalent removes tag from bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let testTag = "foo";
+ let testTagUpper = "Foo";
+ let testURI = Services.io.newURI("http://www.example.com/", null, null);
+
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: testURI
+ });
+
+ // Init panel
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+
+ // add a tag
+ document.getElementById("editBMPanel_tagsField").value = testTag;
+ gEditItemOverlay.onTagsFieldChange();
+
+ // test that the tag has been added in the backend
+ is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match");
+
+ // change the tag
+ document.getElementById("editBMPanel_tagsField").value = testTagUpper;
+ gEditItemOverlay.onTagsFieldChange();
+
+ // test that the tag has been added in the backend
+ is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTagUpper, "tags match");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(testURI, [testTag]);
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ }).then(() => SimpleTest.finish());
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549192.xul b/browser/components/places/tests/chrome/test_bug549192.xul
new file mode 100644
index 000000000..4e6a89bb1
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549192.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 874407
+ * Ensures that history views are updated properly after visits.
+ * Bug 549192
+ * Ensures that history views are updated after deleting entries.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ // Add some visits.
+ let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ let vtime = Date.now() * 1000;
+ const ttype = PlacesUtils.history.TRANSITION_TYPED;
+ let places =
+ [{ uri: Services.io.newURI("http://example.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype },
+ { uri: Services.io.newURI("http://example2.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype },
+ { uri: Services.io.newURI("http://example3.tld/", null, null),
+ visitDate: newTimeInMicroseconds(), transition: ttype }];
+
+ yield PlacesTestUtils.addVisits(places);
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ var tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check them.
+ let treeView = tree.view;
+ let selection = treeView.selection;
+ let rc = treeView.rowCount;
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ is(rc, 3, "Found expected number of rows.");
+
+ // First check live-update of the view when adding visits.
+ places.forEach(place => place.visitDate = newTimeInMicroseconds());
+ yield PlacesTestUtils.addVisits(places);
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ // Now remove the pages and verify live-update again.
+ for (let i = 0; i < rc; i++) {
+ selection.select(0);
+ let node = tree.selectedNode;
+ tree.controller.remove("Removing page");
+ ok(treeView.treeIndexForNode(node) == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE,
+ node.uri + " removed.");
+ ok(treeView.rowCount == rc - i - 1, "Rows count decreased");
+ }
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+ }).then(() => SimpleTest.finish());
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549491.xul b/browser/components/places/tests/chrome/test_bug549491.xul
new file mode 100644
index 000000000..5ec7a765a
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549491.xul
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549491: 'The root node is never visible' exception when details of the root node are modified "
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Date" anonid="date" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 549491
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=549491
+ *
+ * Ensures that changing the details of places tree's root-node doesn't
+ * throw.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ yield PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://example.tld/", null, null),
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ let rootNode = tree.result.root;
+ let obs = tree.view.QueryInterface(Ci.nsINavHistoryResultObserver);
+ obs.nodeHistoryDetailsChanged(rootNode, rootNode.time, rootNode.accessCount);
+ obs.nodeTitleChanged(rootNode, rootNode.title);
+ ok(true, "No exceptions thrown");
+
+ // Cleanup.
+ yield PlacesTestUtils.clearHistory();
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul b/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
new file mode 100644
index 000000000..b1d73017f
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
@@ -0,0 +1,170 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 631374 - Editing tags in the selector scrolls up the listbox"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * This test checks that editing tags doesn't scroll the tags selector
+ * listbox to wrong positions.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let bs = PlacesUtils.bookmarks;
+
+ let tags = ["a", "b", "c", "d", "e", "f", "g",
+ "h", "i", "l", "m", "n", "o", "p"];
+
+ // Add a bookmark and tag it.
+ let uri1 = Services.io.newURI("http://www1.mozilla.org/", null, null);
+ let bm1 = yield bs.insert({
+ parentGuid: bs.toolbarGuid,
+ index: bs.DEFAULT_INDEX,
+ type: bs.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: uri1.spec
+ });
+ PlacesUtils.tagging.tagURI(uri1, tags);
+
+ // Add a second bookmark so that tags won't disappear when unchecked.
+ let uri2 = Services.io.newURI("http://www2.mozilla.org/", null, null);
+ let bm2 = yield bs.insert({
+ parentGuid: bs.toolbarGuid,
+ index: bs.DEFAULT_INDEX,
+ type: bs.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: uri2.spec
+ });
+ PlacesUtils.tagging.tagURI(uri2, tags);
+
+ // Init panel.
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ let node1 = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm1);
+ gEditItemOverlay.initPanel({ node: node1 });
+ ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ yield openTagSelector();
+ let tagsSelector = document.getElementById("editBMPanel_tagsSelector");
+
+ // Go by two so there is some untouched tag in the middle.
+ for (let i = 8; i < tags.length; i += 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+ let visibleIndex = tagsSelector.getIndexOfFirstVisibleRow();
+
+ ok(listItem.checked, "Item is checked " + i);
+ let selectedTag = listItem.label;
+
+ // Uncheck the tag.
+ listItem.checked = false;
+ is(visibleIndex, tagsSelector.getIndexOfFirstVisibleRow(),
+ "Scroll position did not change");
+
+ // The listbox is rebuilt, so we have to get the new element.
+ let newItem = tagsSelector.selectedItem;
+ isnot(newItem, null, "Valid new listItem found");
+ ok(!newItem.checked, "New listItem is unchecked " + i);
+ is(newItem.label, selectedTag, "Correct tag is still selected");
+
+ // Check the tag.
+ newItem.checked = true;
+ is(visibleIndex, tagsSelector.getIndexOfFirstVisibleRow(),
+ "Scroll position did not change");
+ }
+
+ // Remove the second bookmark, then nuke some of the tags.
+ yield bs.remove(bm2.guid);
+
+ // Doing this backwords tests more interesting paths.
+ for (let i = tags.length - 1; i >= 0 ; i -= 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+ let firstVisibleTag = tags[tagsSelector.getIndexOfFirstVisibleRow()];
+
+ ok(listItem.checked, "Item is checked " + i);
+ let selectedTag = listItem.label;
+
+ // Uncheck the tag.
+ listItem.checked = false;
+
+ // Ensure the first visible tag is still visible in the list.
+ let firstVisibleIndex = tagsSelector.getIndexOfFirstVisibleRow();
+ let lastVisibleIndex = firstVisibleIndex + tagsSelector.getNumberOfVisibleRows() -1;
+ let expectedTagIndex = tags.indexOf(firstVisibleTag);
+ ok(expectedTagIndex >= firstVisibleIndex &&
+ expectedTagIndex <= lastVisibleIndex,
+ "Scroll position is correct");
+
+ // The listbox is rebuilt, so we have to get the new element.
+ let newItem = tagsSelector.selectedItem;
+ isnot(newItem, null, "Valid new listItem found");
+ ok(newItem.checked, "New listItem is checked " + i);
+ is(tagsSelector.selectedItem.label,
+ tags[Math.min(i + 1, tags.length - 2)],
+ "The next tag is now selected");
+ }
+
+ // Cleanup.
+ yield bs.remove(bm1.guid);
+ }).then(SimpleTest.finish).catch(alert);
+ }
+
+ function openTagSelector() {
+ // Wait for the tags selector to be open.
+ let promise = new Promise(resolve => {
+ let row = document.getElementById("editBMPanel_tagsSelectorRow");
+ row.addEventListener("DOMAttrModified", function onAttrModified() {
+ row.removeEventListener("DOMAttrModified", onAttrModified);
+ resolve();
+ });
+ });
+
+ // Open the tags selector.
+ document.getElementById("editBMPanel_tagsSelectorExpander").doCommand();
+
+ return promise;
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul b/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul
new file mode 100644
index 000000000..f553d018b
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_editBookmarkOverlay_keywords.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 1343256 - Bookmark keywords disappear from one bookmark when adding a keyword to another bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+ Task.spawn(test.bind(this))
+ .catch(ex => ok(false, ex))
+ .then(() => PlacesUtils.bookmarks.eraseEverything())
+ .then(SimpleTest.finish);
+ }
+
+ function promiseOnItemChanged() {
+ return new Promise(resolve => {
+ PlacesUtils.bookmarks.addObserver({
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemAdded() {},
+ onItemRemoved() {},
+ onItemVisited() {},
+ onItemMoved() {},
+ onItemChanged(id, property, isAnno, value) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ resolve({ property, value });
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+ }, false);
+ });
+ }
+
+ function* test() {
+ ok(gEditItemOverlay, "Sanity check: gEditItemOverlay is in context");
+ let keywordField = document.getElementById("editBMPanel_keywordField");
+
+ for (let i = 0; i < 2; ++i) {
+ let bm = yield PlacesUtils.bookmarks.insert({
+ url: `http://www.test${i}.me/`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ info(`Init panel on bookmark #${i+1}`);
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+ is(document.getElementById("editBMPanel_keywordField").value, "",
+ "The keyword field should be empty");
+ info("Add a keyword to the bookmark");
+ let promise = promiseOnItemChanged();
+ keywordField.focus();
+ keywordField.value = "kw";
+ synthesizeKey(i.toString(), {});
+ synthesizeKey("VK_RETURN", {});
+ keywordField.blur();
+ let {property, value} = yield promise;
+ is(property, "keyword", "The keyword should have been changed");
+ is(value, `kw${i}`, "The new keyword value is correct");
+ }
+
+ for (let i = 0; i < 2; ++i) {
+ let entry = yield PlacesUtils.keywords.fetch({ url: `http://www.test${i}.me/` });
+ is(entry.keyword, `kw${i}`, `The keyword for http://www.test${i}.me/ is correct`);
+ }
+ };
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul b/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
new file mode 100644
index 000000000..1b1cc6473
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
@@ -0,0 +1,204 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?>
+
+<!DOCTYPE window [
+ <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
+ %editBookmarkOverlayDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="485100: Exchanging a letter of a tag name with its big/small equivalent removes tag from bookmark"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <vbox id="editBookmarkPanelContent"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function checkTagsSelector(aAvailableTags, aCheckedTags) {
+ is(PlacesUtils.tagging.allTags.length, aAvailableTags.length,
+ "tagging service is in sync.");
+ let tagsSelector = document.getElementById("editBMPanel_tagsSelector");
+ let children = tagsSelector.childNodes;
+ is(children.length, aAvailableTags.length,
+ "Found expected number of tags in the tags selector");
+
+ Array.prototype.forEach.call(children, function (aChild) {
+ let tag = aChild.getAttribute("label");
+ ok(true, "Found tag '" + tag + "' in the selector");
+ ok(aAvailableTags.includes(tag), "Found expected tag");
+ let checked = aChild.getAttribute("checked") == "true";
+ is(checked, aCheckedTags.includes(tag),
+ "Tag is correctly marked");
+ });
+ }
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ const TEST_URI = Services.io.newURI("http://www.test.me/", null, null);
+ const TEST_URI2 = Services.io.newURI("http://www.test.again.me/", null, null);
+ const TEST_TAG = "test-tag";
+
+ ok(gEditItemOverlay, "Sanity check: gEditItemOverlay is in context");
+
+ // Open the tags selector.
+ document.getElementById("editBMPanel_tagsSelectorRow").collapsed = false;
+
+ // Add a bookmark.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URI.spec,
+ title: "test.me"
+ });
+
+ // Init panel.
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+ gEditItemOverlay.initPanel({ node });
+
+ // Add a tag.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added tag to a single bookmark");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing a single bookmark shows the added tag");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "The tag has been removed");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing a single bookmark should not show any tag");
+ checkTagsSelector([], []);
+
+ // Add a second bookmark.
+ let bm2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "test.again.me",
+ url: TEST_URI2.spec
+ });
+
+ // Init panel with multiple uris.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
+
+ // Add a tag to the first uri.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added a tag to the first bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Add a tag to the second uri.
+ PlacesUtils.tagging.tagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], TEST_TAG,
+ "Correctly added a tag to the second bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing multiple bookmarks should show matching tags.");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag from the first bookmark.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the first bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Remove tag from the second bookmark.
+ PlacesUtils.tagging.untagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], undefined,
+ "Correctly removed tag from the second bookmark.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple bookmarks without matching tags should not show any tag.");
+ checkTagsSelector([], []);
+
+ // Init panel with a nsIURI entry.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI] });
+
+ // Add a tag.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Correctly added tag to the first entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing a single nsIURI entry shows the added tag");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the nsIURI entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing a single nsIURI entry should not show any tag");
+ checkTagsSelector([], []);
+
+ // Init panel with multiple nsIURI entries.
+ gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
+
+ // Add a tag to the first entry.
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
+ "Tag correctly added.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Add a tag to the second entry.
+ PlacesUtils.tagging.tagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], TEST_TAG,
+ "Tag correctly added.");
+ is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
+ "Editing multiple nsIURIs should show matching tags");
+ checkTagsSelector([TEST_TAG], [TEST_TAG]);
+
+ // Remove tag from the first entry.
+ PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
+ "Correctly removed tag from the first entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([TEST_TAG], []);
+
+ // Remove tag from the second entry.
+ PlacesUtils.tagging.untagURI(TEST_URI2, [TEST_TAG]);
+ is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], undefined,
+ "Correctly removed tag from the second entry.");
+ is(document.getElementById("editBMPanel_tagsField").value, "",
+ "Editing multiple nsIURIs without matching tags should not show any tag.");
+ checkTagsSelector([], []);
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ yield PlacesUtils.bookmarks.remove(bm2.guid);
+ }).then(SimpleTest.finish);
+ }
+ ]]>
+ </script>
+
+</window>
diff --git a/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul
new file mode 100644
index 000000000..032c7a258
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xul
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Ensure that selectItems doesn't recurse infinitely in nested trees.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS",
+ title: "shortcut"
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:folder=UNFILED_BOOKMARKS&maxResults=10",
+ title: "query"
+ });
+
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "folder"
+ });
+
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://www.mozilla.org/",
+ title: "bookmark"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = "place:folder=UNFILED_BOOKMARKS";
+
+ // Select the last bookmark.
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ tree.selectItems([itemId]);
+ is (tree.selectedNode.itemId, itemId, "The right node was selected");
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_treeview_date.xul b/browser/components/places/tests/chrome/test_treeview_date.xul
new file mode 100644
index 000000000..559232611
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_treeview_date.xul
@@ -0,0 +1,159 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="435322: Places tree view's formatting"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ type="places"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" ordinal="1" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Tags" id="tags" anonid="tags" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Url" id="url" anonid="url" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Date" id="date" anonid="date" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Count" id="visitCount" anonid="visitCount" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 435322
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=435322
+ *
+ * Ensures that date in places treeviews is correctly formatted.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ function uri(spec) {
+ return Services.io.newURI(spec, null, null);
+ }
+
+ Task.spawn(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ let midnight = new Date();
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ // Add a visit 1ms before midnight, a visit at midnight, and
+ // a visit 1ms after midnight.
+ yield PlacesTestUtils.addVisits([
+ {uri: uri("http://before.midnight.com/"),
+ visitDate: (midnight.getTime() - 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://at.midnight.com/"),
+ visitDate: (midnight.getTime()) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://after.midnight.com/"),
+ visitDate: (midnight.getTime() + 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ]);
+
+ // add a bookmark to the midnight visit
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ url: "http://at.midnight.com/",
+ title: "A bookmark at midnight",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check formatting
+ let treeView = tree.view;
+ let rc = treeView.rowCount;
+ ok(rc >= 3, "Rows found");
+ let columns = tree.columns;
+ ok(columns.count > 0, "Columns found");
+ const locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ for (let r = 0; r < rc; r++) {
+ let node = treeView.nodeForTreeIndex(r);
+ ok(node, "Places node found");
+ for (let ci = 0; ci < columns.count; ci++) {
+ let c = columns.getColumnAt(ci);
+ let text = treeView.getCellText(r, c);
+ switch (c.element.getAttribute("anonid")) {
+ case "title":
+ // The title can differ, we did not set any title so we would
+ // expect null, but in such a case the view will generate a title
+ // through PlacesUIUtils.getBestTitle.
+ if (node.title)
+ is(text, node.title, "Title is correct");
+ break;
+ case "url":
+ is(text, node.uri, "Uri is correct");
+ break;
+ case "date":
+ let timeObj = new Date(node.time / 1000);
+ // Default is short date format.
+ let dtOptions = { year: 'numeric', month: 'numeric', day: 'numeric',
+ hour: 'numeric', minute: 'numeric' };
+ // For today's visits we don't show date portion.
+ if (node.uri == "http://at.midnight.com/" ||
+ node.uri == "http://after.midnight.com/") {
+ dtOptions = { hour: 'numeric', minute: 'numeric' };
+ } else if (node.uri != "http://before.midnight.com/") {
+ // Avoid to test spurious uris, due to how the test works
+ // a redirecting uri could be put in the tree while we test.
+ break;
+ }
+ let timeStr = timeObj.toLocaleString(locale, dtOptions);
+
+ is(text, timeStr, "Date format is correct");
+ break;
+ case "visitCount":
+ is(text, 1, "Visit count is correct");
+ break;
+ }
+ }
+ }
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.remove(bm.guid);
+ yield PlacesTestUtils.clearHistory();
+ }).then(SimpleTest.finish);
+ }
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/unit/.eslintrc.js b/browser/components/places/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/browser/components/places/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/places/tests/unit/bookmarks.glue.html b/browser/components/places/tests/unit/bookmarks.glue.html
new file mode 100644
index 000000000..07b22e9b3
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.html
@@ -0,0 +1,16 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ <DT><H3 ADD_DATE="1233157910" LAST_MODIFIED="1233157972" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ </DL><p>
+</DL><p>
diff --git a/browser/components/places/tests/unit/bookmarks.glue.json b/browser/components/places/tests/unit/bookmarks.glue.json
new file mode 100644
index 000000000..95900e176
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.json
@@ -0,0 +1 @@
+{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Other Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]}
diff --git a/browser/components/places/tests/unit/corruptDB.sqlite b/browser/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /dev/null
+++ b/browser/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/browser/components/places/tests/unit/distribution.ini b/browser/components/places/tests/unit/distribution.ini
new file mode 100644
index 000000000..93e73cb5c
--- /dev/null
+++ b/browser/components/places/tests/unit/distribution.ini
@@ -0,0 +1,27 @@
+# Distribution Configuration File
+# Bug 516444 demo
+
+[Global]
+id=516444
+version=1.0
+about=Test distribution file
+
+[BookmarksToolbar]
+item.1.title=Toolbar Link Before
+item.1.link=https://example.org/toolbar/before/
+item.1.keyword=e:t:b
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.title=Toolbar Link After
+item.3.link=https://example.org/toolbar/after/
+item.3.keyword=e:t:a
+
+[BookmarksMenu]
+item.1.title=Menu Link Before
+item.1.link=https://example.org/menu/before/
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.title=Menu Link After
+item.3.link=https://example.org/menu/after/
diff --git a/browser/components/places/tests/unit/head_bookmarks.js b/browser/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 000000000..460295f96
--- /dev/null
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,133 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoadContextInfo.jsm");
+
+// Import common head.
+var 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.
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "PlacesTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+// Smart bookmarks constants.
+const SMART_BOOKMARKS_VERSION = 8;
+const SMART_BOOKMARKS_ON_TOOLBAR = 1;
+const SMART_BOOKMARKS_ON_MENU = 2; // 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}`);
+ });
+}
+
+var 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");
+});
+
+/**
+ * Rebuilds smart bookmarks listening to console output to report any message or
+ * exception generated.
+ *
+ * @return {Promise}
+ * Resolved when done.
+ */
+function rebuildSmartBookmarks() {
+ let consoleListener = {
+ observe(aMsg) {
+ if (aMsg.message.startsWith("[JavaScript Warning:")) {
+ // TODO (Bug 1300416): Ignore spurious strict warnings.
+ return;
+ }
+ do_throw("Got console message: " + aMsg.message);
+ },
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIConsoleListener ]),
+ };
+ Services.console.reset();
+ Services.console.registerListener(consoleListener);
+ do_register_cleanup(() => {
+ try {
+ Services.console.unregisterListener(consoleListener);
+ } catch (ex) { /* will likely fail */ }
+ });
+ Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "browser-glue-test", "smart-bookmarks-init");
+ return promiseTopicObserved("test-smart-bookmarks-done").then(() => {
+ Services.console.unregisterListener(consoleListener);
+ });
+}
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+/**
+ * Similar to waitForConditionPromise, but poll for an asynchronous value
+ * every SINGLE_TRY_TIMEOUT ms, for no more than tryCount times.
+ *
+ * @param promiseFn
+ * A function to generate a promise, which resolves to the expected
+ * asynchronous value.
+ * @param timeoutMsg
+ * The reason to reject the returned promise with.
+ * @param [optional] tryCount
+ * Maximum times to try before rejecting the returned promise with
+ * timeoutMsg, defaults to NUMBER_OF_TRIES.
+ * @return {Promise}
+ * @resolves to the asynchronous value being polled.
+ * @rejects if the asynchronous value is not available after tryCount attempts.
+ */
+var waitForResolvedPromise = Task.async(function* (promiseFn, timeoutMsg, tryCount=NUMBER_OF_TRIES) {
+ let tries = 0;
+ do {
+ try {
+ let value = yield promiseFn();
+ return value;
+ } catch (ex) {}
+ yield new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve));
+ } while (++tries <= tryCount);
+ throw new Error(timeoutMsg);
+});
diff --git a/browser/components/places/tests/unit/test_421483.js b/browser/components/places/tests/unit/test_421483.js
new file mode 100644
index 000000000..a0d138372
--- /dev/null
+++ b/browser/components/places/tests/unit/test_421483.js
@@ -0,0 +1,103 @@
+/* -*- 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_PREF = "browser.places.smartBookmarksVersion";
+
+var gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(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() {
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+ yield rebuildSmartBookmarks();
+
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, 0);
+
+ do_print("check that pref has not been bumped up");
+ Assert.equal(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
+});
+
+add_task(function* create_smart_bookmarks() {
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+ yield rebuildSmartBookmarks();
+
+ let smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.notEqual(smartBookmarkItemIds.length, 0);
+
+ do_print("check that pref has been bumped up");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
+
+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");
+
+ let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+ yield PlacesUtils.bookmarks.remove(guid);
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+
+ yield rebuildSmartBookmarks();
+ smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+
+ do_print("check that pref has been bumped up");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
+
+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 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 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);
+ yield rebuildSmartBookmarks();
+
+ smartBookmarkItemIds =
+ PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+ 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");
+ Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
diff --git a/browser/components/places/tests/unit/test_PUIU_makeTransaction.js b/browser/components/places/tests/unit/test_PUIU_makeTransaction.js
new file mode 100644
index 000000000..c0626f53b
--- /dev/null
+++ b/browser/components/places/tests/unit/test_PUIU_makeTransaction.js
@@ -0,0 +1,361 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function waitForBookmarkNotification(aNotification, aCallback, aProperty)
+{
+ PlacesUtils.bookmarks.addObserver({
+ validate: function (aMethodName, aData)
+ {
+ if (aMethodName == aNotification &&
+ (!aProperty || aProperty == aData.property)) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ aCallback(aData);
+ }
+ },
+
+ // nsINavBookmarkObserver
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+ onBeginUpdateBatch: function onBeginUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onEndUpdateBatch: function onEndUpdateBatch() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI, aTitle)
+ {
+ return this.validate(arguments.callee.name, { id: aItemId,
+ index: aIndex,
+ type: aItemType,
+ url: aURI ? aURI.spec : null,
+ title: aTitle });
+ },
+ onItemRemoved: function onItemRemoved() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemChanged: function onItemChanged(id, property, aIsAnno,
+ aNewValue, aLastModified, type)
+ {
+ return this.validate(arguments.callee.name,
+ { id,
+ get index() {
+ return PlacesUtils.bookmarks.getItemIndex(this.id);
+ },
+ type,
+ property,
+ get url() {
+ return type == PlacesUtils.bookmarks.TYPE_BOOKMARK ?
+ PlacesUtils.bookmarks.getBookmarkURI(this.id).spec :
+ null;
+ },
+ get title() {
+ return PlacesUtils.bookmarks.getItemTitle(this.id);
+ },
+ });
+ },
+ onItemVisited: function onItemVisited() {
+ return this.validate(arguments.callee.name, arguments);
+ },
+ onItemMoved: function onItemMoved(aItemId, aOldParentId, aOldIndex,
+ aNewParentId, aNewIndex, aItemType)
+ {
+ this.validate(arguments.callee.name, { id: aItemId,
+ index: aNewIndex,
+ type: aItemType });
+ }
+ }, false);
+}
+
+function wrapNodeByIdAndParent(aItemId, aParentId)
+{
+ let wrappedNode;
+ let root = PlacesUtils.getFolderContents(aParentId, false, false).root;
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ if (node.itemId == aItemId) {
+ let type;
+ if (PlacesUtils.nodeIsContainer(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+ }
+ else if (PlacesUtils.nodeIsURI(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ }
+ else if (PlacesUtils.nodeIsSeparator(node)) {
+ type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ }
+ else {
+ do_throw("Unknown node type");
+ }
+ wrappedNode = PlacesUtils.wrapNode(node, type);
+ }
+ }
+ root.containerOpen = false;
+ return JSON.parse(wrappedNode);
+}
+
+add_test(function test_text_paste()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aData)
+ {
+ do_check_eq(aData.title, TEST_TITLE);
+ do_check_eq(aData.url, TEST_URL);
+ do_check_eq(aData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aData.index, 0);
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ { title: TEST_TITLE, uri: TEST_URL },
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true // Unused for text.
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+});
+
+add_test(function test_container()
+{
+ const TEST_TITLE = "Places folder"
+
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.title, TEST_TITLE);
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aChangedData.index, 1);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aAddedData.index, 2);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+
+add_test(function test_separator()
+{
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aChangedData.index, 3);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aAddedData.index, 4);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_bookmark()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemChanged", function(aChangedData)
+ {
+ do_check_eq(aChangedData.title, TEST_TITLE);
+ do_check_eq(aChangedData.url, TEST_URL);
+ do_check_eq(aChangedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aChangedData.index, 5);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.url, TEST_URL);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData.index, 6);
+ let id = aAddedData.id;
+
+ waitForBookmarkNotification("onItemMoved", function(aMovedData)
+ {
+ do_check_eq(aMovedData.id, id);
+ do_check_eq(aMovedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aMovedData.index, 1);
+
+ run_next_test();
+ });
+
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ 1, // Move to position 1.
+ false
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ });
+
+ try {
+ let txn = PlacesUIUtils.makeTransaction(
+ wrapNodeByIdAndParent(aChangedData.id, PlacesUtils.unfiledBookmarksFolderId),
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }, "random-anno");
+
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+ PlacesUtils.annotations.setItemAnnotation(id, PlacesUIUtils.DESCRIPTION_ANNO,
+ "description", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setItemAnnotation(id, "random-anno",
+ "random-value", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+});
+
+add_test(function test_visit()
+{
+ const TEST_URL = "http://places.moz.org/"
+ const TEST_TITLE = "Places bookmark"
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData)
+ {
+ do_check_eq(aAddedData.title, TEST_TITLE);
+ do_check_eq(aAddedData.url, TEST_URL);
+ do_check_eq(aAddedData.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData.index, 7);
+
+ waitForBookmarkNotification("onItemAdded", function(aAddedData2)
+ {
+ do_check_eq(aAddedData2.title, TEST_TITLE);
+ do_check_eq(aAddedData2.url, TEST_URL);
+ do_check_eq(aAddedData2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ do_check_eq(aAddedData2.index, 8);
+ run_next_test();
+ });
+
+ try {
+ let node = wrapNodeByIdAndParent(aAddedData.id, PlacesUtils.unfiledBookmarksFolderId);
+ // Simulate a not-bookmarked node, will copy it to a new bookmark.
+ node.id = -1;
+ let txn = PlacesUIUtils.makeTransaction(
+ node,
+ 0, // Unused for real nodes.
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ true
+ );
+ PlacesUtils.transactionManager.doTransaction(txn);
+ } catch (ex) {
+ do_throw(ex);
+ }
+ });
+
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI(TEST_URL),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_TITLE);
+});
+
+add_test(function check_annotations() {
+ // As last step check how many items for each annotation exist.
+
+ // Copies should retain the description annotation.
+ let descriptions =
+ PlacesUtils.annotations.getItemsWithAnnotation(PlacesUIUtils.DESCRIPTION_ANNO, {});
+ do_check_eq(descriptions.length, 4);
+
+ // Only the original bookmarks should have this annotation.
+ let others = PlacesUtils.annotations.getItemsWithAnnotation("random-anno", {});
+ do_check_eq(others.length, 3);
+ run_next_test();
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
new file mode 100644
index 000000000..4db21555f
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsBrowserGlue correctly exports bookmarks.html at shutdown if
+ * browser.bookmarks.autoExportHTML is set to true.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ remove_bookmarks_html();
+
+ Services.prefs.setBoolPref("browser.bookmarks.autoExportHTML", true);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.bookmarks.autoExportHTML"));
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Initialize Places through the History Service.
+ Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "profile-before-change");
+ check_bookmarks_html();
+ }, "profile-before-change", false);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt.js b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
new file mode 100644
index 000000000..5b2a09068
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
@@ -0,0 +1,59 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database is corrupt and one backup is available.
+ */
+
+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();
+});
+
+add_task(function* test_main() {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ 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.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
new file mode 100644
index 000000000..7cb4e5e4c
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsBrowserGlue correctly imports from bookmarks.html if database
+ * is corrupt but a JSON backup is not available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+do_register_cleanup(remove_bookmarks_html);
+
+add_task(function* () {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ 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.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "example");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
new file mode 100644
index 000000000..480420091
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests 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/AppConstants.jsm");
+
+function run_test() {
+ // Remove bookmarks.html from profile.
+ remove_bookmarks_html();
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+add_task(function* () {
+ // Create a corrupt database.
+ yield createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ // The test will continue once import has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ 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.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+
+ // Bug 1283076: Nightly bookmark points to Get Involved page, not Getting Started one
+ let chanTitle = AppConstants.NIGHTLY_BUILD ? "Get Involved" : "Getting Started";
+ do_check_eq(bm.title, chanTitle);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_distribution.js b/browser/components/places/tests/unit/test_browserGlue_distribution.js
new file mode 100644
index 000000000..c3d6e1d9e
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue correctly imports bookmarks from distribution.ini.
+ */
+
+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() {
+ // 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");
+ 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.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE);
+
+ // Force distribution.
+ 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.
+ yield promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE);
+
+ // Check the custom bookmarks exist on menu.
+ 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 no favicon or keyword exists for this bookmark
+ yield Assert.rejects(waitForResolvedPromise(() => {
+ return PlacesUtils.promiseFaviconData(menuItem.url.href);
+ }, "Favicon not found", 10), /Favicon\snot\sfound/, "Favicon not found");
+
+ let keywordItem = yield PlacesUtils.keywords.fetch({
+ url: menuItem.url.href
+ });
+ Assert.strictEqual(keywordItem, null);
+
+ // Check the custom bookmarks exist on toolbar.
+ let toolbarItem = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ Assert.equal(toolbarItem.title, "Toolbar Link Before");
+
+ // Check the custom favicon and keyword exist for this bookmark
+ let faviconItem = yield waitForResolvedPromise(() => {
+ return PlacesUtils.promiseFaviconData(toolbarItem.url.href);
+ }, "Favicon not found", 10);
+ Assert.equal(faviconItem.uri.spec, "https://example.org/favicon.png");
+ Assert.greater(faviconItem.dataLen, 0);
+ Assert.equal(faviconItem.mimeType, "image/png");
+
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, faviconItem.data));
+ Assert.equal(base64Icon, SMALLPNG_DATA_URI.spec);
+
+ keywordItem = yield PlacesUtils.keywords.fetch({
+ url: toolbarItem.url.href
+ });
+ Assert.notStrictEqual(keywordItem, null);
+ Assert.equal(keywordItem.keyword, "e:t:b");
+
+ 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.
+ Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED));
+
+ // Check distribution prefs have been created.
+ Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_migrate.js b/browser/components/places/tests/unit/test_browserGlue_migrate.js
new file mode 100644
index 000000000..817f10c81
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * 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() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove current database file.
+ 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.
+ 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);
+ bg.observe(null, "initial-migration-will-import-default-bookmarks", null);
+
+ 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 = promiseTopicObserved("places-browser-init-complete");
+ bg.observe(null, "initial-migration-did-import-default-bookmarks", null);
+ yield promise;
+
+ let bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0
+ });
+ yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+ // 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.
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: SMART_BOOKMARKS_ON_MENU + 1
+ })));
+
+ Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_MENU
+ })));
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js
new file mode 100644
index 000000000..9f3504636
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+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";
+
+var bg = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+
+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");
+ // Force nsBrowserGlue::_initPlaces().
+ bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT);
+ return promiseTopicObserved("places-browser-init-complete");
+}
+
+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));
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_restore.js b/browser/components/places/tests/unit/test_browserGlue_restore.js
new file mode 100644
index 000000000..9d7ac5ac1
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_restore.js
@@ -0,0 +1,62 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database has been created and one backup is available.
+ */
+
+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");
+
+ // Remove current database file.
+ 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.nsISupports);
+
+ // 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.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // The test will continue once restore has finished and smart bookmarks
+ // have been created.
+ yield promiseTopicObserved("places-browser-init-complete");
+
+ 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.
+ bm = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: SMART_BOOKMARKS_ON_TOOLBAR
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
new file mode 100644
index 000000000..6ecaec4fe
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -0,0 +1,285 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that nsBrowserGlue is correctly interpreting the preferences settable
+ * 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";
+
+function run_test() {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ run_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;
+}
+
+add_task(function* setup() {
+ // Initialize browserGlue, but remove it's listener to places-init-complete.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+
+ // Initialize Places.
+ PlacesUtils.history;
+
+ // 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));
+});
+
+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);
+
+ yield 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);
+
+ yield 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 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;
+
+ // Set preferences.
+ Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+ yield 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);
+
+ // 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;
+
+ // 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);
+
+ yield 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);
+
+ 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);
+
+ // 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);
+
+ yield 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);
+
+ yield 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);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
new file mode 100644
index 000000000..072056b3f
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
@@ -0,0 +1,150 @@
+/* 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 UI_VERSION = 26;
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const DEFAULT_BEHAVIOR_PREF = "browser.urlbar.default.behavior";
+const AUTOCOMPLETE_PREF = "browser.urlbar.autocomplete.enabled";
+
+var gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver);
+var gGetBoolPref = Services.prefs.getBoolPref;
+
+function run_test() {
+ run_next_test();
+}
+
+do_register_cleanup(cleanup);
+
+function cleanup() {
+ let prefix = "browser.urlbar.suggest.";
+ for (let type of ["history", "bookmark", "openpage", "history.onlyTyped"]) {
+ Services.prefs.clearUserPref(prefix + type);
+ }
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.clearUserPref(AUTOCOMPLETE_PREF);
+}
+
+function setupBehaviorAndMigrate(aDefaultBehavior, aAutocompleteEnabled = true) {
+ cleanup();
+ // Migrate browser.urlbar.default.behavior preference.
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setIntPref(DEFAULT_BEHAVIOR_PREF, aDefaultBehavior);
+ Services.prefs.setBoolPref(AUTOCOMPLETE_PREF, aAutocompleteEnabled);
+ // Simulate a migration.
+ gBrowserGlue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_BROWSERGLUE_TEST);
+}
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 0");
+ setupBehaviorAndMigrate(0);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.openpage"),
+ "Openpage preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false.");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 1");
+ setupBehaviorAndMigrate(1);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 2");
+ setupBehaviorAndMigrate(2);
+
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
+ "History preference should be false.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 3");
+ setupBehaviorAndMigrate(3);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 19");
+ setupBehaviorAndMigrate(19);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.bookmark"),
+ "Bookmark preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 33");
+ setupBehaviorAndMigrate(33);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"),
+ "Typed preference should be true");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 129");
+ setupBehaviorAndMigrate(129);
+
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
+ "History preference should be true.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.ok(gGetBoolPref("browser.urlbar.suggest.openpage"),
+ "Openpage preference should be true");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
+
+add_task(function*() {
+ do_print("Migrate default.behavior = 0, autocomplete.enabled = false");
+ setupBehaviorAndMigrate(0, false);
+
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
+ "History preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.bookmark"), false,
+ "Bookmark preference should be false.");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.openpage"), false,
+ "Openpage preference should be false");
+ Assert.equal(gGetBoolPref("browser.urlbar.suggest.history.onlyTyped"), false,
+ "Typed preference should be false");
+});
diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
new file mode 100644
index 000000000..0c1d78801
--- /dev/null
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that requesting clear history at shutdown will really clear history.
+ */
+
+const URIS = [
+ "http://a.example1.com/"
+, "http://b.example1.com/"
+, "http://b.example2.com/"
+, "http://c.example3.com/"
+];
+
+const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
+
+var EXPECTED_NOTIFICATIONS = [
+ "places-shutdown"
+, "places-will-close-connection"
+, "places-expiration-finished"
+, "places-connection-closed"
+];
+
+const UNEXPECTED_NOTIFICATIONS = [
+ "xpcom-shutdown"
+];
+
+const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
+
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+ getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+var timeInMicroseconds = Date.now() * 1000;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_execute() {
+ do_print("Initialize browserglue before Places");
+
+ // Avoid default bookmarks import.
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+ glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
+ glue.observe(null, "test-initialize-sanitizer", null);
+
+
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.formData", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.siteSettings", true);
+
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+
+ do_print("Add visits.");
+ for (let aUrl of URIS) {
+ yield PlacesTestUtils.addVisits({
+ uri: uri(aUrl), visitDate: timeInMicroseconds++,
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ });
+ }
+ do_print("Add cache.");
+ yield storeCache(FTP_URL, "testData");
+ do_print("Add form history.");
+ yield addFormHistory();
+ Assert.equal((yield getFormHistoryCount()), 1, "Added form history");
+
+ do_print("Simulate and wait shutdown.");
+ yield shutdownPlaces();
+
+ Assert.equal((yield getFormHistoryCount()), 0, "Form history cleared");
+
+ let stmt = DBConn(true).createStatement(
+ "SELECT id FROM moz_places WHERE url = :page_url "
+ );
+
+ try {
+ URIS.forEach(function(aUrl) {
+ stmt.params.page_url = aUrl;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ });
+ } finally {
+ stmt.finalize();
+ }
+
+ do_print("Check cache");
+ // Check cache.
+ yield checkCache(FTP_URL);
+});
+
+function addFormHistory() {
+ return new Promise(resolve => {
+ let now = Date.now() * 1000;
+ FormHistory.update({ op: "add",
+ fieldname: "testfield",
+ value: "test",
+ timesUsed: 1,
+ firstUsed: now,
+ lastUsed: now
+ },
+ { handleCompletion(reason) { resolve(); } });
+ });
+}
+
+function getFormHistoryCount() {
+ return new Promise((resolve, reject) => {
+ let count = -1;
+ FormHistory.count({ fieldname: "testfield" },
+ { handleResult(result) { count = result; },
+ handleCompletion(reason) { resolve(count); }
+ });
+ });
+}
+
+function storeCache(aURL, aContent) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+
+ return new Promise(resolve => {
+ let storeCacheListener = {
+ onCacheEntryCheck: function (entry, appcache) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+ do_check_eq(status, Cr.NS_OK);
+
+ entry.setMetaDataElement("servertype", "0");
+ var os = entry.openOutputStream(0);
+
+ var written = os.write(aContent, aContent.length);
+ if (written != aContent.length) {
+ do_throw("os.write has not written all data!\n" +
+ " Expected: " + written + "\n" +
+ " Actual: " + aContent.length + "\n");
+ }
+ os.close();
+ entry.close();
+ resolve();
+ }
+ };
+
+ storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ storeCacheListener);
+ });
+}
+
+
+function checkCache(aURL) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+
+ return new Promise(resolve => {
+ let checkCacheListener = {
+ onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+ do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ resolve();
+ }
+ };
+
+ storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ checkCacheListener);
+ });
+}
diff --git a/browser/components/places/tests/unit/test_leftpane_corruption_handling.js b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
new file mode 100644
index 000000000..0af6f4e95
--- /dev/null
+++ b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
@@ -0,0 +1,174 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that we build a working leftpane in various corruption situations.
+ */
+
+// Used to store the original leftPaneFolderId getter.
+var gLeftPaneFolderIdGetter;
+var gAllBookmarksFolderIdGetter;
+// Used to store the original left Pane status as a JSON string.
+var gReferenceHierarchy;
+var gLeftPaneFolderId;
+
+add_task(function* () {
+ // We want empty roots.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // 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");
+
+ do_register_cleanup(() => PlacesUtils.bookmarks.eraseEverything());
+});
+
+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
+ });
+
+ 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);
+
+ while (gTests.length) {
+ // Run current test.
+ yield Task.spawn(gTests.shift());
+
+ // Regenerate getters.
+ Object.defineProperty(PlacesUIUtils, "leftPaneFolderId", gLeftPaneFolderIdGetter);
+ gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
+ Object.defineProperty(PlacesUIUtils, "allBookmarksFolderId", gAllBookmarksFolderIdGetter);
+
+ // Check the new left pane folder.
+ let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId)
+ Assert.equal(gReferenceHierarchy, leftPaneHierarchy);
+
+ folder = yield PlacesUtils.bookmarks.fetch({guid: folder.guid});
+ Assert.equal(folder.title, "test");
+ }
+});
+
+// Corruption cases.
+var 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);
+ }
+
+];
+
+/**
+ * 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;
+ return hier;
+}
+
+function hierarchyToObj(aNode) {
+ let o = {}
+ o.title = aNode.title;
+ o.annos = PlacesUtils.getAnnotationsForItem(aNode.itemId)
+ if (PlacesUtils.nodeIsURI(aNode)) {
+ o.uri = aNode.uri;
+ }
+ else if (PlacesUtils.nodeIsFolder(aNode)) {
+ o.children = [];
+ PlacesUtils.asContainer(aNode).containerOpen = true;
+ for (let i = 0; i < aNode.childCount; ++i) {
+ o.children.push(hierarchyToObj(aNode.getChild(i)));
+ }
+ aNode.containerOpen = false;
+ }
+ return o;
+}
diff --git a/browser/components/places/tests/unit/xpcshell.ini b/browser/components/places/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..1c40e1c53
--- /dev/null
+++ b/browser/components/places/tests/unit/xpcshell.ini
@@ -0,0 +1,25 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ bookmarks.glue.html
+ bookmarks.glue.json
+ corruptDB.sqlite
+ distribution.ini
+
+[test_421483.js]
+[test_browserGlue_bookmarkshtml.js]
+[test_browserGlue_corrupt.js]
+[test_browserGlue_corrupt_nobackup.js]
+[test_browserGlue_corrupt_nobackup_default.js]
+[test_browserGlue_distribution.js]
+[test_browserGlue_migrate.js]
+[test_browserGlue_prefs.js]
+[test_browserGlue_restore.js]
+[test_browserGlue_smartBookmarks.js]
+[test_browserGlue_urlbar_defaultbehavior_migration.js]
+[test_clearHistory_shutdown.js]
+[test_leftpane_corruption_handling.js]
+[test_PUIU_makeTransaction.js]