/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; function ensurePosition(info, parentGuid, index) { print(`Checking ${info.guid}`); checkBookmarkObject(info); Assert.equal( info.parentGuid, parentGuid, "Should be in the correct parent folder" ); Assert.equal(info.index, index, "Should have the correct index"); } function insertChildren(folder, items) { if (!items.length) { return []; } let children = []; for (let i = 0; i < items.length; i++) { if (items[i].type === TYPE_BOOKMARK) { children.push({ title: `${i}`, url: "http://example.com", }); } else { throw new Error(`Type ${items[i].type} is not supported.`); } } return PlacesUtils.bookmarks.insertTree({ guid: folder.guid, children, }); } async function dumpFolderChildren( folder, details, folderA, folderB, originalAChildren, originalBChildren ) { info(`${folder} Details:`); info(`Input: ${JSON.stringify(details.initial[folder])}`); info(`Expected: ${JSON.stringify(details.expected[folder])}`); info("Index\tOriginal\tExpected\tResult"); let originalChildren; let folderGuid; if (folder == "folderA") { originalChildren = originalAChildren; folderGuid = folderA.guid; } else { originalChildren = originalBChildren; folderGuid = folderB.guid; } let tree = await PlacesUtils.promiseBookmarksTree(folderGuid); let childrenCount = tree.children ? tree.children.length : 0; for (let i = 0; i < originalChildren.length || i < childrenCount; i++) { let originalGuid = i < originalChildren.length ? originalChildren[i].guid : " "; let resultGuid = i < childrenCount ? tree.children[i].guid : " "; let expectedGuid = " "; if (i < details.expected[folder].length) { let expected = details.expected[folder][i]; expectedGuid = expected.folder == "a" ? originalAChildren[expected.originalIndex].guid : originalBChildren[expected.originalIndex].guid; } info(`${i}\t${originalGuid}\t${expectedGuid}\t${resultGuid}\n`); } } async function checkExpectedResults( details, folder, folderGuid, lastModified, movedItems, folderAChildren, folderBChildren ) { let expectedResults = details.expected[folder]; for (let i = 0; i < expectedResults.length; i++) { let expectedDetails = expectedResults[i]; let originalItem = expectedDetails.folder == "a" ? folderAChildren[expectedDetails.originalIndex] : folderBChildren[expectedDetails.originalIndex]; // Check the item got updated correctly in the database. let updatedItem = await PlacesUtils.bookmarks.fetch(originalItem.guid); ensurePosition(updatedItem, folderGuid, i); Assert.greaterOrEqual( updatedItem.lastModified.getTime(), lastModified.getTime(), "Last modified should be later or equal to before" ); } if (details.expected.skipResultIndexChecks) { return; } // Check the items returned from the actual move() call are correct. let index = 0; for (let item of details.initial[folder]) { if (!("targetFolder" in item)) { // We weren't moving this item, so skip it and continue. continue; } let movedItem = movedItems[index]; let updatedItem = await PlacesUtils.bookmarks.fetch(movedItem.guid); ensurePosition(movedItem, updatedItem.parentGuid, updatedItem.index); index++; } } async function checkLastModifiedForFolders(details, folder, movedItems) { // For the tests, the moves always come from folderA. if ( details.initial.folderA.some( item => "targetFolder" in item && item.targetFolder == folder.title ) ) { let updatedFolder = await PlacesUtils.bookmarks.fetch(folder.guid); Assert.greaterOrEqual( updatedFolder.lastModified.getTime(), folder.lastModified.getTime(), "Should have updated the folder's last modified time." ); print(JSON.stringify(movedItems[0])); Assert.deepEqual( updatedFolder.lastModified, movedItems[0].lastModified, "Should have the same last modified as the moved items." ); } } async function testMoveToFolder(details) { await PlacesUtils.bookmarks.eraseEverything(); // Always create two folders by default. let [folderA, folderB] = await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.unfiledGuid, children: [ { title: "a", type: PlacesUtils.bookmarks.TYPE_FOLDER, }, { title: "b", type: PlacesUtils.bookmarks.TYPE_FOLDER, }, ], }); checkBookmarkObject(folderA); let folderAChildren = await insertChildren(folderA, details.initial.folderA); checkBookmarkObject(folderB); let folderBChildren = await insertChildren(folderB, details.initial.folderB); const originalAChildren = folderAChildren.map(child => { return { ...child }; }); const originalBChildren = folderBChildren.map(child => { return { ...child }; }); let lastModified; if (folderAChildren.length) { lastModified = folderAChildren[0].lastModified; } else if (folderBChildren.length) { lastModified = folderBChildren[0].lastModified; } else { throw new Error("No children added, can't determine lastModified"); } // Work out which children to move and to where. let childrenToUpdate = []; for (let i = 0; i < details.initial.folderA.length; i++) { if ("move" in details.initial.folderA[i]) { childrenToUpdate.push(folderAChildren[i].guid); } } let observer; if (details.notifications) { observer = expectPlacesObserverNotifications(["bookmark-moved"]); } let movedItems = await PlacesUtils.bookmarks.moveToFolder( childrenToUpdate, details.targetFolder == "a" ? folderA.guid : folderB.guid, details.targetIndex ); await dumpFolderChildren( "folderA", details, folderA, folderB, originalAChildren, originalBChildren ); await dumpFolderChildren( "folderB", details, folderA, folderB, originalAChildren, originalBChildren ); Assert.equal(movedItems.length, childrenToUpdate.length); await checkExpectedResults( details, "folderA", folderA.guid, lastModified, movedItems, folderAChildren, folderBChildren ); await checkExpectedResults( details, "folderB", folderB.guid, lastModified, movedItems, folderAChildren, folderBChildren ); if (details.notifications) { let expectedNotifications = []; for (let notification of details.notifications) { let origItem = notification.originalFolder == "folderA" ? originalAChildren[notification.originalIndex] : originalBChildren[notification.originalIndex]; let newFolder = notification.newFolder == "folderA" ? folderA : folderB; expectedNotifications.push({ type: "bookmark-moved", id: await PlacesUtils.promiseItemId(origItem.guid), itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: origItem.url, guid: origItem.guid, parentGuid: newFolder.guid, source: PlacesUtils.bookmarks.SOURCES.DEFAULT, index: notification.newIndex, oldParentGuid: origItem.parentGuid, oldIndex: notification.originalIndex, isTagging: false, }); } observer.check(expectedNotifications); } await checkLastModifiedForFolders(details, folderA, movedItems); await checkLastModifiedForFolders(details, folderB, movedItems); } const TYPE_BOOKMARK = PlacesUtils.bookmarks.TYPE_BOOKMARK; add_task(async function invalid_input_throws() { Assert.throws( () => PlacesUtils.bookmarks.moveToFolder(), /guids should be an array/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder({}), /guids should be an array/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder([]), /guids should be an array of at least one item/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder(["test"]), /Expected only valid GUIDs to be passed/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder([null]), /Expected only valid GUIDs to be passed/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder([123]), /Expected only valid GUIDs to be passed/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder(["123456789012"], 123), /Error: parentGuid should be a valid GUID/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder( ["123456789012"], PlacesUtils.bookmarks.rootGuid ), /Cannot move bookmarks into root/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder(["123456789012"], "123456789012", -2), /index should be a number greater than/ ); Assert.throws( () => PlacesUtils.bookmarks.moveToFolder( ["123456789012"], "123456789012", "sdffd" ), /index should be a number greater than/ ); }); add_task(async function test_move_nonexisting_bookmark_rejects() { await Assert.rejects( PlacesUtils.bookmarks.moveToFolder(["123456789012"], "123456789012", -1), /No bookmarks found for the provided GUID/, "Should reject when moving a non-existing bookmark" ); }); add_task(async function test_move_folder_into_descendant_rejects() { let parent = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); let descendant = await PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); await Assert.rejects( PlacesUtils.bookmarks.moveToFolder([parent.guid], parent.guid, 0), /Cannot insert a folder into itself or one of its descendants/, "Should reject when moving a folder into itself" ); await Assert.rejects( PlacesUtils.bookmarks.moveToFolder([parent.guid], descendant.guid, 0), /Cannot insert a folder into itself or one of its descendants/, "Should reject when moving a folder into a descendant" ); }); add_task(async function test_move_from_differnt_with_no_target_rejects() { let bm1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); let bm2 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); await Assert.rejects( PlacesUtils.bookmarks.moveToFolder([bm1.guid, bm2.guid], null, -1), /All bookmarks should be in the same folder if no parent is specified/, "Should reject when moving bookmarks from different folders with no target folder" ); }); add_task(async function test_move_append_same_folder() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, ], folderB: [], }, targetFolder: "a", targetIndex: -1, expected: { folderA: [ { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 2 }, { folder: "a", originalIndex: 1 }, ], folderB: [], }, notifications: [ { originalFolder: "folderA", originalIndex: 1, newFolder: "folderA", newIndex: 2, }, ], }); }); add_task(async function test_move_append_multiple_same_folder() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, ], folderB: [], }, targetFolder: "a", targetIndex: -1, expected: { folderA: [ { folder: "a", originalIndex: 3 }, { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 1 }, { folder: "a", originalIndex: 2 }, ], folderB: [], }, // These are all inserted at position 3 as that's what the views require // to be notified, to ensure the new items are displayed in their correct // positions. notifications: [ { originalFolder: "folderA", originalIndex: 0, newFolder: "folderA", newIndex: 3, }, { originalFolder: "folderA", originalIndex: 1, newFolder: "folderA", newIndex: 3, }, { originalFolder: "folderA", originalIndex: 2, newFolder: "folderA", newIndex: 3, }, ], }); }); add_task(async function test_move_append_multiple_new_folder() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, ], folderB: [], }, targetFolder: "b", targetIndex: -1, expected: { folderA: [], folderB: [ { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 1 }, { folder: "a", originalIndex: 2 }, ], }, notifications: [ { originalFolder: "folderA", originalIndex: 0, newFolder: "folderB", newIndex: 0, }, { originalFolder: "folderA", originalIndex: 1, newFolder: "folderB", newIndex: 1, }, { originalFolder: "folderA", originalIndex: 2, newFolder: "folderB", newIndex: 2, }, ], }); }); add_task(async function test_move_append_multiple_new_folder_with_existing() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK, move: true }, ], folderB: [{ type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }], }, targetFolder: "b", targetIndex: -1, expected: { folderA: [], folderB: [ { folder: "b", originalIndex: 0 }, { folder: "b", originalIndex: 1 }, { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 1 }, { folder: "a", originalIndex: 2 }, ], }, notifications: [ { originalFolder: "folderA", originalIndex: 0, newFolder: "folderB", newIndex: 2, }, { originalFolder: "folderA", originalIndex: 1, newFolder: "folderB", newIndex: 3, }, { originalFolder: "folderA", originalIndex: 2, newFolder: "folderB", newIndex: 4, }, ], }); }); add_task(async function test_move_insert_same_folder_up() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK, move: true }, ], folderB: [], }, targetFolder: "a", targetIndex: 0, expected: { folderA: [ { folder: "a", originalIndex: 2 }, { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 1 }, ], folderB: [], }, notifications: [ { originalFolder: "folderA", originalIndex: 2, newFolder: "folderA", newIndex: 0, }, ], }); }); add_task(async function test_move_insert_same_folder_down() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }, ], folderB: [], }, targetFolder: "a", targetIndex: 2, expected: { folderA: [ { folder: "a", originalIndex: 1 }, { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 2 }, ], folderB: [], }, notifications: [ { originalFolder: "folderA", originalIndex: 0, newFolder: "folderA", newIndex: 1, }, ], }); }); add_task( async function test_move_insert_multiple_same_folder_split_locations() { await testMoveToFolder({ initial: { folderA: [ { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK, move: true }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK, move: true }, ], folderB: [], }, targetFolder: "a", targetIndex: 2, expected: { folderA: [ { folder: "a", originalIndex: 1 }, { folder: "a", originalIndex: 0 }, { folder: "a", originalIndex: 3 }, { folder: "a", originalIndex: 6 }, { folder: "a", originalIndex: 9 }, { folder: "a", originalIndex: 2 }, { folder: "a", originalIndex: 4 }, { folder: "a", originalIndex: 5 }, { folder: "a", originalIndex: 7 }, { folder: "a", originalIndex: 8 }, ], folderB: [], }, notifications: [ { originalFolder: "folderA", originalIndex: 0, newFolder: "folderA", newIndex: 1, }, { originalFolder: "folderA", originalIndex: 3, newFolder: "folderA", newIndex: 2, }, { originalFolder: "folderA", originalIndex: 6, newFolder: "folderA", newIndex: 3, }, { originalFolder: "folderA", originalIndex: 9, newFolder: "folderA", newIndex: 4, }, ], }); } ); add_task(async function test_move_folder_with_descendant() { let parent = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); let bm = await PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, }); let descendant = await PlacesUtils.bookmarks.insert({ parentGuid: parent.guid, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); Assert.equal(descendant.index, 1); let lastModified = bm.lastModified; // This is moving to a nonexisting index by purpose, it will be appended. [bm] = await PlacesUtils.bookmarks.moveToFolder( [bm.guid], descendant.guid, 1 ); checkBookmarkObject(bm); Assert.equal(bm.parentGuid, descendant.guid); Assert.equal(bm.index, 0); Assert.ok(bm.lastModified >= lastModified); parent = await PlacesUtils.bookmarks.fetch(parent.guid); descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); Assert.deepEqual(parent.lastModified, bm.lastModified); Assert.deepEqual(descendant.lastModified, bm.lastModified); Assert.equal(descendant.index, 0); bm = await PlacesUtils.bookmarks.fetch(bm.guid); Assert.equal(bm.parentGuid, descendant.guid); Assert.equal(bm.index, 0); [bm] = await PlacesUtils.bookmarks.moveToFolder([bm.guid], parent.guid, 0); Assert.equal(bm.parentGuid, parent.guid); Assert.equal(bm.index, 0); descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); Assert.equal(descendant.index, 1); });