Bug 1221764 - Implement simple chrome.bookmarks events, r=aswan,mak

MozReview-Commit-ID: LWbhYf8CpZD

--HG--
extra : rebase_source : 74e11b9787ef151640aa5ff13024e62c40ddc476
This commit is contained in:
Tom Schuster 2016-08-17 10:22:10 -04:00
parent bb74527cff
commit cdf4b653b0
4 changed files with 314 additions and 23 deletions

View File

@ -5,12 +5,19 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
SingletonEventManager,
} = ExtensionUtils;
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
let listenerCount = 0;
function getTree(rootGuid, onlyChildren) {
function convert(node, parent) {
let treenode = {
@ -77,6 +84,103 @@ function convert(result) {
return node;
}
let observer = {
skipTags: true,
skipDescendantsOnItemRemoval: true,
onBeginUpdateBatch() {},
onEndUpdateBatch() {},
onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let bookmark = {
id: guid,
parentId: parentGuid,
index,
title,
dateAdded: dateAdded / 1000,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
bookmark.url = uri.spec;
} else {
bookmark.dateGroupModified = bookmark.dateAdded;
}
this.emit("created", bookmark);
},
onItemVisited() {},
onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {
parentId: newParentGuid,
index: newIndex,
oldParentId: oldParentGuid,
oldIndex,
};
this.emit("moved", {guid, info});
},
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let node = {
id: guid,
parentId: parentGuid,
index,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
node.url = uri.spec;
}
this.emit("removed", {guid, info: {parentId: parentGuid, index, node}});
},
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {};
if (prop == "title") {
info.title = val;
} else if (prop == "uri") {
info.url = val;
} else {
// Not defined yet.
return;
}
this.emit("changed", {guid, info});
},
};
EventEmitter.decorate(observer);
function decrementListeners() {
listenerCount -= 1;
if (!listenerCount) {
PlacesUtils.bookmarks.removeObserver(observer);
}
}
function incrementListeners() {
listenerCount++;
if (listenerCount == 1) {
PlacesUtils.bookmarks.addObserver(observer, false);
}
}
extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
return {
bookmarks: {
@ -213,6 +317,58 @@ extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
let listener = (event, bookmark) => {
context.runSafe(fire, bookmark.id, bookmark);
};
observer.on("created", listener);
incrementListeners();
return () => {
observer.off("created", listener);
decrementListeners();
};
}).api(),
onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("removed", listener);
incrementListeners();
return () => {
observer.off("removed", listener);
decrementListeners();
};
}).api(),
onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("changed", listener);
incrementListeners();
return () => {
observer.off("changed", listener);
decrementListeners();
};
}).api(),
onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
let listener = (event, data) => {
context.runSafe(fire, data.guid, data.info);
};
observer.on("moved", listener);
incrementListeners();
return () => {
observer.off("moved", listener);
decrementListeners();
};
}).api(),
},
};
});

View File

@ -451,7 +451,6 @@
"events": [
{
"name": "onCreated",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark or folder is created.",
"parameters": [
@ -467,7 +466,6 @@
},
{
"name": "onRemoved",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark or folder is removed. When a folder is removed recursively, a single notification is fired for the folder, and none for its contents.",
"parameters": [
@ -488,7 +486,6 @@
},
{
"name": "onChanged",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark or folder changes. <b>Note:</b> Currently, only title and url changes trigger this.",
"parameters": [
@ -511,7 +508,6 @@
},
{
"name": "onMoved",
"unsupported": true,
"type": "function",
"description": "Fired when a bookmark or folder is moved to a different parent folder.",
"parameters": [

View File

@ -6,6 +6,8 @@ function backgroundScript() {
let unsortedId, ourId;
let initialBookmarkCount = 0;
let createdBookmarks = new Set();
let createdFolderId;
let collectedEvents = [];
const nonExistentId = "000000000000";
const bookmarkGuids = {
menuGuid: "menu________",
@ -37,6 +39,73 @@ function backgroundScript() {
browser.test.fail("Did not get expected error");
}
function checkOnCreated(id, parentId, index, title, url, dateAdded) {
let createdData = collectedEvents.pop();
browser.test.assertEq("onCreated", createdData.event, "onCreated was the last event received");
browser.test.assertEq(id, createdData.id, "onCreated event received the expected id");
let bookmark = createdData.bookmark;
browser.test.assertEq(id, bookmark.id, "onCreated event received the expected bookmark id");
browser.test.assertEq(parentId, bookmark.parentId, "onCreated event received the expected bookmark parentId");
browser.test.assertEq(index, bookmark.index, "onCreated event received the expected bookmark index");
browser.test.assertEq(title, bookmark.title, "onCreated event received the expected bookmark title");
browser.test.assertEq(url, bookmark.url, "onCreated event received the expected bookmark url");
browser.test.assertEq(dateAdded, bookmark.dateAdded, "onCreated event received the expected bookmark dateAdded");
}
function checkOnChanged(id, url, title) {
// If both url and title are changed, then url is fired last.
let changedData = collectedEvents.pop();
browser.test.assertEq("onChanged", changedData.event, "onChanged was the last event received");
browser.test.assertEq(id, changedData.id, "onChanged event received the expected id");
browser.test.assertEq(url, changedData.info.url, "onChanged event received the expected url");
// title is fired first.
changedData = collectedEvents.pop();
browser.test.assertEq("onChanged", changedData.event, "onChanged was the last event received");
browser.test.assertEq(id, changedData.id, "onChanged event received the expected id");
browser.test.assertEq(title, changedData.info.title, "onChanged event received the expected title");
}
function checkOnMoved(id, parentId, oldParentId, index, oldIndex) {
let movedData = collectedEvents.pop();
browser.test.assertEq("onMoved", movedData.event, "onMoved was the last event received");
browser.test.assertEq(id, movedData.id, "onMoved event received the expected id");
let info = movedData.info;
browser.test.assertEq(parentId, info.parentId, "onMoved event received the expected parentId");
browser.test.assertEq(oldParentId, info.oldParentId, "onMoved event received the expected oldParentId");
browser.test.assertEq(index, info.index, "onMoved event received the expected index");
browser.test.assertEq(oldIndex, info.oldIndex, "onMoved event received the expected oldIndex");
}
function checkOnRemoved(id, parentId, index, url) {
let removedData = collectedEvents.pop();
browser.test.assertEq("onRemoved", removedData.event, "onRemoved was the last event received");
browser.test.assertEq(id, removedData.id, "onRemoved event received the expected id");
let info = removedData.info;
browser.test.assertEq(parentId, removedData.info.parentId, "onRemoved event received the expected parentId");
browser.test.assertEq(index, removedData.info.index, "onRemoved event received the expected index");
let node = info.node;
browser.test.assertEq(id, node.id, "onRemoved event received the expected node id");
browser.test.assertEq(parentId, node.parentId, "onRemoved event received the expected node parentId");
browser.test.assertEq(index, node.index, "onRemoved event received the expected node index");
browser.test.assertEq(url, node.url, "onRemoved event received the expected node url");
}
browser.bookmarks.onChanged.addListener((id, info) => {
collectedEvents.push({event: "onChanged", id, info});
});
browser.bookmarks.onCreated.addListener((id, bookmark) => {
collectedEvents.push({event: "onCreated", id, bookmark});
});
browser.bookmarks.onMoved.addListener((id, info) => {
collectedEvents.push({event: "onMoved", id, info});
});
browser.bookmarks.onRemoved.addListener((id, info) => {
collectedEvents.push({event: "onRemoved", id, info});
});
browser.bookmarks.get(["not-a-bookmark-guid"]).then(expectedError, error => {
browser.test.assertTrue(
error.message.includes("Invalid value for property 'guid': not-a-bookmark-guid"),
@ -57,6 +126,8 @@ function backgroundScript() {
}).then(result => {
ourId = result.id;
checkOurBookmark(result);
browser.test.assertEq(1, collectedEvents.length, "1 expected event received");
checkOnCreated(ourId, bookmarkGuids.unfiledGuid, 0, "test bookmark", "http://example.org/", result.dateAdded);
return browser.bookmarks.get(ourId);
}).then(results => {
@ -96,6 +167,9 @@ function backgroundScript() {
browser.test.assertEq("http://example.com/", result.url, "Updated bookmark has the expected URL");
browser.test.assertEq(ourId, result.id, "Updated bookmark has the expected id");
browser.test.assertEq(2, collectedEvents.length, "2 expected events received");
checkOnChanged(ourId, "http://example.com/", "new test title");
return Promise.resolve().then(() => {
return browser.bookmarks.update(ourId, {url: "this is not a valid url"});
}).then(expectedError, error => {
@ -129,6 +203,9 @@ function backgroundScript() {
}).then(result => {
browser.test.assertEq(undefined, result, "Removing a bookmark returns undefined");
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnRemoved(ourId, bookmarkGuids.unfiledGuid, 0, "http://example.com/");
return browser.bookmarks.get(ourId).then(expectedError, error => {
browser.test.assertTrue(
error.message.includes("Bookmark not found"),
@ -145,32 +222,49 @@ function backgroundScript() {
}).then(() => {
// test bookmarks.search
return Promise.all([
browser.bookmarks.create({title: "MØzillä", url: "http://møzîllä.örg"}),
browser.bookmarks.create({title: "Example", url: "http://example.org"}),
browser.bookmarks.create({title: "MØzillä", url: "http://møzîllä.örg/"}),
browser.bookmarks.create({title: "Example", url: "http://example.org/"}),
browser.bookmarks.create({title: "Mozilla Folder"}),
browser.bookmarks.create({title: "EFF", url: "http://eff.org"}),
browser.bookmarks.create({title: "Menu Item", url: "http://menu.org", parentId: bookmarkGuids.menuGuid}),
browser.bookmarks.create({title: "Toolbar Item", url: "http://toolbar.org", parentId: bookmarkGuids.toolbarGuid}),
browser.bookmarks.create({title: "EFF", url: "http://eff.org/"}),
browser.bookmarks.create({title: "Menu Item", url: "http://menu.org/", parentId: bookmarkGuids.menuGuid}),
browser.bookmarks.create({title: "Toolbar Item", url: "http://toolbar.org/", parentId: bookmarkGuids.toolbarGuid}),
]);
}).then(results => {
browser.test.assertEq(6, collectedEvents.length, "6 expected events received");
checkOnCreated(results[5].id, bookmarkGuids.toolbarGuid, 0, "Toolbar Item", "http://toolbar.org/", results[5].dateAdded);
checkOnCreated(results[4].id, bookmarkGuids.menuGuid, 0, "Menu Item", "http://menu.org/", results[4].dateAdded);
checkOnCreated(results[3].id, bookmarkGuids.unfiledGuid, 0, "EFF", "http://eff.org/", results[3].dateAdded);
checkOnCreated(results[2].id, bookmarkGuids.unfiledGuid, 0, "Mozilla Folder", undefined, results[2].dateAdded);
checkOnCreated(results[1].id, bookmarkGuids.unfiledGuid, 0, "Example", "http://example.org/", results[1].dateAdded);
checkOnCreated(results[0].id, bookmarkGuids.unfiledGuid, 0, "MØzillä", "http://møzîllä.örg/", results[0].dateAdded);
for (let result of results) {
if (result.title !== "Mozilla Folder") {
createdBookmarks.add(result.id);
}
}
let createdFolderId = results[2].id;
let folderResult = results[2];
createdFolderId = folderResult.id;
return Promise.all([
browser.bookmarks.create({title: "Mozilla", url: "http://allizom.org", parentId: createdFolderId}),
browser.bookmarks.create({title: "Mozilla Corporation", url: "http://allizom.com", parentId: createdFolderId}),
browser.bookmarks.create({title: "Firefox", url: "http://allizom.org/firefox", parentId: createdFolderId}),
browser.bookmarks.create({title: "Mozilla", url: "http://allizom.org/", parentId: createdFolderId}),
browser.bookmarks.create({title: "Mozilla Corporation", url: "http://allizom.com/", parentId: createdFolderId}),
browser.bookmarks.create({title: "Firefox", url: "http://allizom.org/firefox/", parentId: createdFolderId}),
]).then(results => {
browser.test.assertEq(3, collectedEvents.length, "3 expected events received");
checkOnCreated(results[2].id, createdFolderId, 0, "Firefox", "http://allizom.org/firefox/", results[2].dateAdded);
checkOnCreated(results[1].id, createdFolderId, 0, "Mozilla Corporation", "http://allizom.com/", results[1].dateAdded);
checkOnCreated(results[0].id, createdFolderId, 0, "Mozilla", "http://allizom.org/", results[0].dateAdded);
return browser.bookmarks.create({
title: "About Mozilla",
url: "http://allizom.org/about",
url: "http://allizom.org/about/",
parentId: createdFolderId,
index: 1,
});
}).then(() => {
}).then(result => {
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnCreated(result.id, createdFolderId, 1, "About Mozilla", "http://allizom.org/about/", result.dateAdded);
// returns all items on empty object
return browser.bookmarks.search({});
}).then(results => {
@ -375,20 +469,33 @@ function backgroundScript() {
return browser.bookmarks.move(corporationBookmark.id, {index: 0}).then(result => {
browser.test.assertEq(0, result.index, "Bookmark has the expected index");
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnMoved(corporationBookmark.id, createdFolderId, createdFolderId, 0, 2);
return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.menuGuid});
}).then(result => {
browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent");
browser.test.assertEq(childCount, result.index, "Bookmark has the expected index");
return browser.bookmarks.move(corporationBookmark.id, {index: 1});
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnMoved(corporationBookmark.id, bookmarkGuids.menuGuid, createdFolderId, 1, 0);
return browser.bookmarks.move(corporationBookmark.id, {index: 0});
}).then(result => {
browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent");
browser.test.assertEq(1, result.index, "Bookmark has the expected index");
browser.test.assertEq(0, result.index, "Bookmark has the expected index");
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnMoved(corporationBookmark.id, bookmarkGuids.menuGuid, bookmarkGuids.menuGuid, 0, 1);
return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.toolbarGuid, index: 1});
}).then(result => {
browser.test.assertEq(bookmarkGuids.toolbarGuid, result.parentId, "Bookmark has the expected parent");
browser.test.assertEq(1, result.index, "Bookmark has the expected index");
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnMoved(corporationBookmark.id, bookmarkGuids.toolbarGuid, bookmarkGuids.menuGuid, 1, 0);
createdBookmarks.add(corporationBookmark.id);
});
}).then(() => {
@ -415,6 +522,9 @@ function backgroundScript() {
return browser.bookmarks.search({title: "Mozilla Folder"}).then(result => {
return browser.bookmarks.removeTree(result[0].id);
}).then(() => {
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnRemoved(createdFolderId, bookmarkGuids.unfiledGuid, 1);
return browser.bookmarks.search({}).then(results => {
browser.test.assertEq(
startBookmarkCount - 4,
@ -426,8 +536,15 @@ function backgroundScript() {
return browser.bookmarks.create({title: "Empty Folder"});
}).then(result => {
let emptyFolderId = result.id;
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnCreated(emptyFolderId, bookmarkGuids.unfiledGuid, 3, "Empty Folder", undefined, result.dateAdded);
browser.test.assertEq("Empty Folder", result.title, "Folder has the expected title");
return browser.bookmarks.remove(emptyFolderId).then(() => {
browser.test.assertEq(1, collectedEvents.length, "1 expected events received");
checkOnRemoved(emptyFolderId, bookmarkGuids.unfiledGuid, 3);
return browser.bookmarks.get(emptyFolderId).then(expectedError, error => {
browser.test.assertTrue(
error.message.includes("Bookmark not found"),
@ -456,9 +573,12 @@ function backgroundScript() {
let promises = Array.from(createdBookmarks, guid => browser.bookmarks.remove(guid));
return Promise.all(promises);
}).then(() => {
browser.test.assertEq(createdBookmarks.size, collectedEvents.length, "expected number of events received");
return browser.bookmarks.search({});
}).then(results => {
browser.test.assertEq(initialBookmarkCount, results.length, "All created bookmarks have been removed");
return browser.test.notifyPass("bookmarks");
}).catch(error => {
browser.test.fail(`Error: ${String(error)} :: ${error.stack}`);

View File

@ -189,13 +189,17 @@ var Bookmarks = Object.freeze({
// complete we may stop using it.
let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
let itemId = yield PlacesUtils.promiseItemId(item.guid);
// Pass tagging information for the observers to skip over these notifications when needed.
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
item.type, uri, item.title || null,
PlacesUtils.toPRTime(item.dateAdded), item.guid,
item.parentGuid, item.source ]);
item.parentGuid, item.source ],
{ isTagging: isTagging || isTagsFolder });
// If it's a tag, notify OnItemChanged to all bookmarks for this URL.
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
if (isTagging) {
for (let entry of (yield fetchBookmarksByURL(item))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
@ -430,12 +434,13 @@ var Bookmarks = Object.freeze({
let { source = Bookmarks.SOURCES.DEFAULT } = options;
let observers = PlacesUtils.bookmarks.getObservers();
let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
item.type, uri, item.guid,
item.parentGuid,
source ]);
source ],
{ isTagging: isUntagging });
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
if (isUntagging) {
for (let entry of (yield fetchBookmarksByURL(item))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
@ -790,9 +795,20 @@ var Bookmarks = Object.freeze({
* the notification name.
* @param args
* array of arguments to pass to the notification.
* @param information
* Information about the notification, so we can filter based
* based on the observer's preferences.
*/
function notify(observers, notification, args) {
function notify(observers, notification, args, information = {}) {
for (let observer of observers) {
if (information.isTagging && observer.skipTags) {
continue;
}
if (information.isDescendantRemoval && observer.skipDescendantsOnItemRemoval) {
continue;
}
try {
observer[notification](...args);
} catch (ex) {}
@ -1484,7 +1500,10 @@ Task.async(function* (db, folderGuids, options) {
notify(observers, "onItemRemoved", [ item._id, item._parentId,
item.index, item.type, uri,
item.guid, item.parentGuid,
source ]);
source ],
// Notify observers that this item is being
// removed as a descendent.
{ isDescendantRemoval: true });
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
if (isUntagging) {