summaryrefslogtreecommitdiffstats
path: root/browser/components/places/tests/browser/browser_views_liveupdate.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/tests/browser/browser_views_liveupdate.js')
-rw-r--r--browser/components/places/tests/browser/browser_views_liveupdate.js493
1 files changed, 493 insertions, 0 deletions
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 0000000000..cce35941f3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -0,0 +1,493 @@
+/* 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;
+
+/**
+ * Simulates popup opening causing it to populate.
+ * We cannot just use menu.open, since it would not work on Mac due to native menubar.
+ *
+ * @param {object} aPopup
+ * The popup element
+ */
+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);
+}
+
+async function testInFolder(folderGuid, prefix) {
+ let addedBookmarks = [];
+ let item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}1`,
+ url: `http://${prefix}1.mozilla.org/`,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}1_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}2`,
+ url: "place:",
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}2_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}f`,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}f_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: item.guid,
+ title: `${prefix}f1`,
+ url: `http://${prefix}f1.mozilla.org/`,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item.index = 0;
+ item.parentGuid = folderGuid;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ return addedBookmarks;
+}
+
+add_task(async function test() {
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ // Open bookmarks menu.
+ var popup = document.getElementById("bookmarksMenuPopup");
+ ok(popup, "Menu popup element exists");
+ fakeOpenPopup(popup);
+
+ // Open bookmarks sidebar.
+ await withSidebarTree("bookmarks", async () => {
+ // Add observers.
+ bookmarksObserver.handlePlacesEvents =
+ bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver);
+ PlacesUtils.observers.addListener(
+ ["bookmark-added", "bookmark-removed"],
+ bookmarksObserver.handlePlacesEvents
+ );
+ var addedBookmarks = [];
+
+ // MENU
+ info("*** Acting on menu bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm")
+ );
+
+ // TOOLBAR
+ info("*** Acting on toolbar bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.toolbarGuid, "tb")
+ );
+
+ // UNSORTED
+ info("*** Acting on unsorted bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.unfiledGuid, "ub")
+ );
+
+ // Remove all added bookmarks.
+ for (let bm of addedBookmarks) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ await PlacesUtils.bookmarks.remove(bm);
+ } catch (ex) {}
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ }
+
+ // Remove observers.
+ PlacesUtils.observers.removeListener(
+ ["bookmark-added", "bookmark-removed"],
+ bookmarksObserver.handlePlacesEvents
+ );
+ });
+
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+});
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ _notifications: [],
+
+ handlePlacesEvents(events) {
+ for (let { type, parentGuid, guid, index } of events) {
+ switch (type) {
+ case "bookmark-added":
+ this._notifications.push([
+ "assertItemAdded",
+ parentGuid,
+ guid,
+ index,
+ ]);
+ break;
+ case "bookmark-removed":
+ this._notifications.push(["assertItemRemoved", parentGuid, guid]);
+ break;
+ }
+ }
+ },
+
+ async assertViewsUpdatedCorrectly() {
+ for (let notification of this._notifications) {
+ let assertFunction = notification.shift();
+
+ let views = await getViewsForFolder(notification.shift());
+ Assert.greater(
+ views.length,
+ 0,
+ "Should have found one or more views for the parent folder."
+ );
+
+ await this[assertFunction](views, ...notification);
+ }
+
+ this._notifications = [];
+ },
+
+ async assertItemAdded(views, guid, expectedIndex) {
+ for (let i = 0; i < views.length; i++) {
+ let [node, index] = searchItemInView(guid, views[i]);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ index,
+ expectedIndex,
+ "Should have found the node at the expected index"
+ );
+ }
+ },
+
+ async assertItemRemoved(views, guid) {
+ for (let i = 0; i < views.length; i++) {
+ let [node] = searchItemInView(guid, views[i]);
+ Assert.equal(node, null, "Should not have found the node");
+ }
+ },
+
+ async assertItemMoved(views, guid, newIndex) {
+ // Check that item has been moved in the correct position.
+ for (let i = 0; i < views.length; i++) {
+ let [node, index] = searchItemInView(guid, views[i]);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ index,
+ newIndex,
+ "Should have found the node at the expected index"
+ );
+ }
+ },
+
+ async assertItemChanged(views, guid, newValue) {
+ let validator = function (aElementOrTreeIndex) {
+ if (typeof aElementOrTreeIndex == "number") {
+ let sidebar = document.getElementById("sidebar");
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let cellText = tree.view.getCellText(
+ aElementOrTreeIndex,
+ tree.columns.getColumnAt(0)
+ );
+ if (!newValue) {
+ return (
+ cellText ==
+ PlacesUIUtils.getBestTitle(
+ tree.view.nodeForTreeIndex(aElementOrTreeIndex),
+ true
+ )
+ );
+ }
+ return cellText == newValue;
+ }
+ if (!newValue && aElementOrTreeIndex.localName != "toolbarbutton") {
+ return (
+ aElementOrTreeIndex.getAttribute("label") ==
+ PlacesUIUtils.getBestTitle(aElementOrTreeIndex._placesNode)
+ );
+ }
+ return aElementOrTreeIndex.getAttribute("label") == newValue;
+ };
+
+ for (let i = 0; i < views.length; i++) {
+ let [node, , valid] = searchItemInView(guid, views[i], validator);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ node.title,
+ newValue,
+ "Node should have the correct new title"
+ );
+ Assert.ok(valid, "Node element should have the correct label");
+ }
+ },
+};
+
+/**
+ * Search an item guid in a view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {string} view
+ * either "toolbar", "menu" or "sidebar"
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index, valid] or [null, null, false] if not found.
+ */
+function searchItemInView(itemGuid, view, validator) {
+ switch (view) {
+ case "toolbar":
+ return getNodeForToolbarItem(itemGuid, validator);
+ case "menu":
+ return getNodeForMenuItem(itemGuid, validator);
+ case "sidebar":
+ return getNodeForSidebarItem(itemGuid, validator);
+ }
+
+ return [null, null, false];
+}
+
+/**
+ * Get places node and index for an itemGuid in bookmarks toolbar view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForToolbarItem(itemGuid, validator) {
+ var placesToolbarItems = document.getElementById("PlacesToolbarItems");
+
+ function findNode(aContainer) {
+ var children = aContainer.children;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.bookmarkGuid == itemGuid) {
+ let valid = validator ? validator(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.menupopup;
+ popup.openPopup();
+ var foundNode = findNode(popup);
+ popup.hidePopup();
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+ }
+ return [null, null];
+ }
+
+ return findNode(placesToolbarItems);
+}
+
+/**
+ * Get places node and index for an itemGuid in bookmarks menu view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForMenuItem(itemGuid, validator) {
+ var menu = document.getElementById("bookmarksMenu");
+
+ function findNode(aContainer) {
+ var children = aContainer.children;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.bookmarkGuid == itemGuid) {
+ let valid = validator ? validator(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.lastElementChild;
+ fakeOpenPopup(popup);
+ var foundNode = findNode(popup);
+
+ child.open = false;
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+ }
+ return [null, null, false];
+ }
+
+ return findNode(menu.lastElementChild);
+}
+
+/**
+ * Get places node and index for an itemGuid in sidebar tree view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForSidebarItem(itemGuid, validator) {
+ 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.bookmarkGuid == itemGuid) {
+ // Minus one because we want relative index inside the container.
+ let valid = validator ? validator(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 {string} folderGuid
+ * item guid of the folder we have changed.
+ * @returns {Array<"toolbar" | "menu" | "sidebar">}
+ * subset of views: ["toolbar", "menu", "sidebar"]
+ */
+async function getViewsForFolder(folderGuid) {
+ let rootGuid = folderGuid;
+ while (!PlacesUtils.isRootItem(rootGuid)) {
+ let itemData = await PlacesUtils.bookmarks.fetch(rootGuid);
+ rootGuid = itemData.parentGuid;
+ }
+
+ switch (rootGuid) {
+ case PlacesUtils.bookmarks.toolbarGuid:
+ return ["toolbar", "sidebar"];
+ case PlacesUtils.bookmarks.menuGuid:
+ return ["menu", "sidebar"];
+ case PlacesUtils.bookmarks.unfiledGuid:
+ return ["sidebar"];
+ }
+ return [];
+}