Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* 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 [];
}