add_task(async function invalid_input_rejects() { await Assert.throws( () => PlacesUtils.bookmarks.insertTree(), /Should be provided a valid tree object./ ); await Assert.throws( () => PlacesUtils.bookmarks.insertTree(null), /Should be provided a valid tree object./ ); await Assert.throws( () => PlacesUtils.bookmarks.insertTree("foo"), /Should be provided a valid tree object./ ); // All subsequent tests pass a valid parent guid. let guid = PlacesUtils.bookmarks.unfiledGuid; await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ guid, children: [] }), /Should have a non-zero number of children to insert./ ); await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ guid }), /Should have a non-zero number of children to insert./ ); await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ children: [{}], guid }), /The following properties were expected: url/ ); // Reuse another variable to make this easier to read: let tree = { guid, children: [{ guid: "test" }] }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'guid'/ ); tree.children = [{ guid: null }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'guid'/ ); tree.children = [{ guid: 123 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'guid'/ ); tree.children = [{ dateAdded: -10 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'dateAdded'/ ); tree.children = [{ dateAdded: "today" }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'dateAdded'/ ); tree.children = [{ dateAdded: Date.now() }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'dateAdded'/ ); tree.children = [{ dateAdded: new Date(NaN) }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'dateAdded'/ ); tree.children = [{ lastModified: -10 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'lastModified'/ ); tree.children = [{ lastModified: "today" }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'lastModified'/ ); tree.children = [{ lastModified: Date.now() }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'lastModified'/ ); let time = Date.now(); let future = new Date(time + 86400000); tree.children = [{ dateAdded: future, lastModified: time }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'dateAdded'/ ); let past = new Date(time - 86400000); tree.children = [{ lastModified: past }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'lastModified'/ ); tree.children = [{ type: -1 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'type'/ ); tree.children = [{ type: 100 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'type'/ ); tree.children = [{ type: "bookmark" }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'type'/ ); tree.children = [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, title: -1 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'title'/ ); tree.children = [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: 10 }]; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(tree), /Invalid value for property 'url'/ ); let treeWithBrokenURL = { children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://te st" }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(treeWithBrokenURL), /Invalid value for property 'url'/ ); let longurl = "http://www.example.com/" + "a".repeat(65536); let treeWithLongURL = { children: [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: longurl }], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(treeWithLongURL), /Invalid value for property 'url'/ ); let treeWithLongURI = { children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: NetUtil.newURI(longurl), }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(treeWithLongURI), /Invalid value for property 'url'/ ); let treeWithOtherBrokenURL = { children: [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "te st" }], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(treeWithOtherBrokenURL), /Invalid value for property 'url'/ ); }); add_task(async function invalid_properties_for_bookmark_type() { let folderWithURL = { children: [ { type: PlacesUtils.bookmarks.TYPE_FOLDER, url: "http://www.moz.com/" }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(folderWithURL), /Invalid value for property 'url'/ ); let separatorWithURL = { children: [ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, url: "http://www.moz.com/", }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(separatorWithURL), /Invalid value for property 'url'/ ); let separatorWithTitle = { children: [{ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, title: "test" }], guid: PlacesUtils.bookmarks.unfiledGuid, }; await Assert.throws( () => PlacesUtils.bookmarks.insertTree(separatorWithTitle), /Invalid value for property 'title'/ ); }); add_task(async function create_separator() { let [bm] = await PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }); checkBookmarkObject(bm); Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); Assert.equal(bm.index, 0); Assert.equal(bm.dateAdded, bm.lastModified); Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); Assert.strictEqual(bm.title, ""); }); add_task(async function create_plain_bm() { let [bm] = await PlacesUtils.bookmarks.insertTree({ children: [ { url: "http://www.example.com/", title: "Test", }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }); checkBookmarkObject(bm); Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); Assert.equal(bm.index, 1); Assert.equal(bm.dateAdded, bm.lastModified); Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); Assert.equal(bm.title, "Test"); Assert.equal(bm.url.href, "http://www.example.com/"); }); add_task(async function create_folder() { let [bm] = await PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "Test", }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }); checkBookmarkObject(bm); Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); Assert.equal(bm.index, 2); Assert.equal(bm.dateAdded, bm.lastModified); Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); Assert.equal(bm.title, "Test"); }); add_task(async function create_in_tags() { await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://www.example.com/", title: "Test adding a tag", }, ], guid: PlacesUtils.bookmarks.tagsGuid, }), /Can't use insertTree to insert tags/ ); let guidForTag = ( await PlacesUtils.bookmarks.insert({ title: "test-tag", url: "http://www.unused.com/", parentGuid: PlacesUtils.bookmarks.tagsGuid, }) ).guid; await Assert.rejects( PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://www.example.com/", title: "Test adding an item to a tag", }, ], guid: guidForTag, }), /Can't use insertTree to insert tags/ ); await PlacesUtils.bookmarks.remove(guidForTag); await PlacesTestUtils.promiseAsyncUpdates(); }); add_task(async function insert_into_root() { await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://www.example.com/", title: "Test inserting into root", }, ], guid: PlacesUtils.bookmarks.rootGuid, }), /Can't insert into the root/ ); }); add_task(async function tree_where_separator_or_folder_has_kids() { await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://www.example.com/", title: "Test inserting into separator", }, ], }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }), /Invalid value for property 'children'/ ); await Assert.throws( () => PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, children: [ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://www.example.com/", title: "Test inserting into separator", }, ], }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }), /Invalid value for property 'children'/ ); }); add_task(async function create_hierarchy() { let obsInvoked = 0; let listener = events => { for (let event of events) { obsInvoked++; Assert.greater(event.id, 0, "Should have a valid itemId"); } }; PlacesUtils.observers.addListener(["bookmark-added"], listener); let bms = await PlacesUtils.bookmarks.insertTree({ children: [ { type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "Root item", children: [ { url: "http://www.example.com/1", title: "BM 1", }, { url: "http://www.example.com/2", title: "BM 2", }, { title: "Sub", type: PlacesUtils.bookmarks.TYPE_FOLDER, children: [ { title: "Sub BM 1", url: "http://www.example.com/sub/1", }, { title: "Sub BM 2", url: "http://www.example.com/sub/2", }, ], }, ], }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); PlacesUtils.observers.removeListener(["bookmark-added"], listener); let parentFolder = null, subFolder = null; let prevBM = null; for (let bm of bms) { checkBookmarkObject(bm); if (prevBM && prevBM.parentGuid == bm.parentGuid) { Assert.equal(prevBM.index + 1, bm.index, "Indices should be subsequent"); Assert.equal( (await PlacesUtils.bookmarks.fetch(bm.guid)).index, bm.index, "Index reflects inserted index" ); } prevBM = bm; if (bm.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) { Assert.greater( await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url: bm.url, }), 0, "Check frecency has been updated for bookmark " + bm.url ); } if (bm.title == "Root item") { Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); parentFolder = bm; } else if (!bm.title.startsWith("Sub BM")) { Assert.equal(bm.parentGuid, parentFolder.guid); if (bm.type == PlacesUtils.bookmarks.TYPE_FOLDER) { subFolder = bm; } } else { Assert.equal(bm.parentGuid, subFolder.guid); } } Assert.equal(obsInvoked, bms.length); Assert.equal(obsInvoked, 6); }); add_task(async function insert_many_non_nested() { let obsInvoked = 0; let listener = events => { for (let event of events) { obsInvoked++; Assert.greater(event.id, 0, "Should have a valid itemId"); } }; PlacesUtils.observers.addListener(["bookmark-added"], listener); let bms = await PlacesUtils.bookmarks.insertTree({ children: [ { url: "http://www.example.com/1", title: "Item 1", }, { url: "http://www.example.com/2", title: "Item 2", }, { url: "http://www.example.com/3", title: "Item 3", }, { type: PlacesUtils.bookmarks.TYPE_SEPARATOR, }, { title: "Item 4", url: "http://www.example.com/4", }, { title: "Item 5", url: "http://www.example.com/5", }, ], guid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); PlacesUtils.observers.removeListener(["bookmark-added"], listener); let startIndex = -1; for (let bm of bms) { checkBookmarkObject(bm); if (startIndex == -1) { startIndex = bm.index; } else { Assert.equal(++startIndex, bm.index, "Indices should be subsequent"); } Assert.equal( (await PlacesUtils.bookmarks.fetch(bm.guid)).index, bm.index, "Index reflects inserted index" ); if (bm.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) { Assert.greater( await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url: bm.url, }), 0, "Check frecency has been updated for bookmark " + bm.url ); } Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); } Assert.equal(obsInvoked, bms.length); Assert.equal(obsInvoked, 6); }); add_task(async function create_in_folder() { let mozFolder = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "Mozilla", }); let notifications = []; let listener = events => { for (let event of events) { notifications.push({ itemId: event.id, parentId: event.parentId, index: event.index, title: event.title, guid: event.guid, parentGuid: event.parentGuid, }); } }; PlacesUtils.observers.addListener(["bookmark-added"], listener); let bms = await PlacesUtils.bookmarks.insertTree({ children: [ { url: "http://getfirefox.com", title: "Get Firefox!", }, { type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "Community", children: [ { url: "http://getthunderbird.com", title: "Get Thunderbird!", }, { url: "https://www.seamonkey-project.org", title: "SeaMonkey", }, ], }, ], guid: mozFolder.guid, }); await PlacesTestUtils.promiseAsyncUpdates(); PlacesUtils.observers.removeListener(["bookmark-added"], listener); let mozFolderId = await PlacesUtils.promiseItemId(mozFolder.guid); let commFolderId = await PlacesUtils.promiseItemId(bms[1].guid); deepEqual(notifications, [ { itemId: await PlacesUtils.promiseItemId(bms[0].guid), parentId: mozFolderId, index: 0, title: "Get Firefox!", guid: bms[0].guid, parentGuid: mozFolder.guid, }, { itemId: commFolderId, parentId: mozFolderId, index: 1, title: "Community", guid: bms[1].guid, parentGuid: mozFolder.guid, }, { itemId: await PlacesUtils.promiseItemId(bms[2].guid), parentId: commFolderId, index: 0, title: "Get Thunderbird!", guid: bms[2].guid, parentGuid: bms[1].guid, }, { itemId: await PlacesUtils.promiseItemId(bms[3].guid), parentId: commFolderId, index: 1, title: "SeaMonkey", guid: bms[3].guid, parentGuid: bms[1].guid, }, ]); });