diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js index cf6c0abeaee0..8455eb2c9257 100755 --- a/browser/components/places/content/controller.js +++ b/browser/components/places/content/controller.js @@ -264,9 +264,9 @@ PlacesController.prototype = { * is a policy decision that a removable item not be placed inside a non- * removable item. * @param aIsMoveCommand - * True if thecommand for which this method is called only moves the + * True if the command for which this method is called only moves the * selected items to another container, false otherwise. - * @returns true if the there's a selection which has no nodes that cannot be removed, + * @returns true if all nodes in the selection can be removed, * false otherwise. */ _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) { @@ -278,17 +278,8 @@ PlacesController.prototype = { if (nodes[i] == root) return false; - // Disallow removing shortcuts from the left pane - var nodeItemId = nodes[i].itemId; - if (PlacesUtils.annotations - .itemHasAnnotation(nodeItemId, ORGANIZER_QUERY_ANNO)) - return false; - - // Disallow removing the toolbar, menu and unfiled-bookmarks folders - if (!aIsMoveCommand && - (nodeItemId == PlacesUtils.toolbarFolderId || - nodeItemId == PlacesUtils.unfiledBookmarksFolderId || - nodeItemId == PlacesUtils.bookmarksMenuFolderId)) + if (PlacesUtils.nodeIsFolder(nodes[i]) && + !PlacesControllerDragHelper.canMoveContainerNode(nodes[i])) return false; // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that @@ -1006,6 +997,7 @@ PlacesController.prototype = { * elsewhere. */ getTransferData: function PC_getTransferData(dragAction) { + var copy = dragAction == Ci.nsIDragService.DRAGDROP_ACTION_COPY; var result = this._view.getResult(); var oldViewer = result.viewer; try { @@ -1025,7 +1017,7 @@ PlacesController.prototype = { var data = new TransferData(); function addData(type, overrideURI) { data.addDataForFlavour(type, PlacesUIUtils._wrapString( - PlacesUtils.wrapNode(node, type, overrideURI))); + PlacesUtils.wrapNode(node, type, overrideURI, copy))); } function addURIData(overrideURI) { @@ -1093,7 +1085,8 @@ PlacesController.prototype = { uri) + suffix); var placeSuffix = i < (nodes.length - 1) ? "," : ""; - return PlacesUtils.wrapNode(node, type, overrideURI) + placeSuffix; + var resolveShortcuts = !PlacesControllerDragHelper.canMoveContainerNode(node); + return PlacesUtils.wrapNode(node, type, overrideURI, resolveShortcuts) + placeSuffix; } // all items wrapped as TYPE_X_MOZ_PLACE @@ -1317,6 +1310,71 @@ var PlacesControllerDragHelper = { return true; }, + /** + * Determines if a container node can be moved. + * + * @param aNode + * A bookmark folder node. + * @param [optional] aInsertionPoint + * The insertion point of the drop target. + * @returns True if the container can be moved. + */ + canMoveContainerNode: + function PCDH_canMoveContainerNode(aNode, aInsertionPoint) { + // can't move query root + if (!aNode.parent) + return false; + + var targetId = aInsertionPoint ? aInsertionPoint.itemId : -1; + var parentId = PlacesUtils.getConcreteItemId(aNode.parent); + var concreteId = PlacesUtils.getConcreteItemId(aNode); + + // can't move tag containers + if (PlacesUtils.nodeIsTagQuery(aNode)) + return false; + + // check is child of a read-only container + if (PlacesUtils.nodeIsReadOnly(aNode.parent)) + return false; + + // check for special folders, etc + if (!this.canMoveContainer(aNode.itemId, parentId)) + return false; + + return true; + }, + + /** + * Determines if a container node can be moved. + * + * @param aId + * A bookmark folder id. + * @param [optional] aParentId + * The parent id of the folder. + * @returns True if the container can be moved to the target. + */ + canMoveContainer: + function PCDH_canMoveContainer(aId, aParentId) { + if (aId == -1) + return false; + + // Disallow moving of roots and special folders + const ROOTS = [PlacesUtils.placesRootId, PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.toolbarFolderId]; + if (ROOTS.indexOf(aId) != -1) + return false; + + // Get parent id if necessary + if (aParentId == null || aParentId == -1) + aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId); + + if(PlacesUtils.bookmarks.getFolderReadonly(aParentId)) + return false; + + return true; + }, + /** * Creates a Transferable object that can be filled with data of types * supported by a view. @@ -1343,6 +1401,8 @@ var PlacesControllerDragHelper = { */ onDrop: function PCDH_onDrop(insertionPoint) { var session = this.getSession(); + // XXX dragAction is not valid, so we also set copy below by checking + // whether the dropped item is moveable, before creating the transaction var copy = session.dragAction & Ci.nsIDragService.DRAGDROP_ACTION_COPY; var transactions = []; var xferable = this._initTransferable(session); @@ -1360,13 +1420,13 @@ var PlacesControllerDragHelper = { // There's only ever one in the D&D case. var unwrapped = PlacesUtils.unwrapNodes(data.value.data, flavor.value)[0]; + var index = insertionPoint.index; // Adjust insertion index to prevent reversal of dragged items. When you // drag multiple elts upward: need to increment index or each successive // elt will be inserted at the same index, each above the previous. - if ((index != -1) && ((index < unwrapped.index) || - (unwrapped.folder && (index < unwrapped.folder.index)))) { + if (index != -1 && index < unwrapped.index) { index = index + movedCount; movedCount++; } @@ -1378,6 +1438,12 @@ var PlacesControllerDragHelper = { transactions.push(PlacesUIUtils.ptm.tagURI(uri,[tagItemId])); } else { + if (!this.canMoveContainer(unwrapped.id, null)) + copy = true; + else if (unwrapped.concreteId && + !this.canMoveContainer(unwrapped.concreteId, null)) + copy = true; + transactions.push(PlacesUIUtils.makeTransaction(unwrapped, flavor.value, insertionPoint.itemId, index, copy)); diff --git a/browser/components/places/content/menu.xml b/browser/components/places/content/menu.xml index c87988047880..ead46e5363f4 100755 --- a/browser/components/places/content/menu.xml +++ b/browser/components/places/content/menu.xml @@ -189,9 +189,10 @@ + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +function test() { + // sanity check + ok(PlacesUtils, "checking PlacesUtils, running in chrome context?"); + ok(PlacesUIUtils, "checking PlacesUIUtils, running in chrome context?"); + ok(PlacesControllerDragHelper, "checking PlacesControllerDragHelper, running in chrome context?"); + + const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX; + + // setup + var rootId = PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX); + var rootNode = PlacesUtils.getFolderContents(rootId, false, true).root; + is(rootNode.childCount, 0, "confirm test root is empty"); + + var tests = []; + + // add a regular folder, should be moveable + tests.push({ + populate: function() { + this.id = + PlacesUtils.bookmarks.createFolder(rootId, "", IDX); + }, + validate: function() { + is(rootNode.childCount, 1, + "populate added data to the test root"); + is(PlacesControllerDragHelper.canMoveContainer(this.id), + true, "can move regular folder id"); + is(PlacesControllerDragHelper.canMoveContainerNode(rootNode.getChild(0)), + true, "can move regular folder node"); + } + }); + + // add a regular folder shortcut, should be moveable + tests.push({ + populate: function() { + this.folderId = + PlacesUtils.bookmarks.createFolder(rootId, "foo", IDX); + this.shortcutId = + PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder="+this.folderId), IDX, "bar"); + }, + validate: function() { + is(rootNode.childCount, 2, + "populated data to the test root"); + + var folderNode = rootNode.getChild(0); + is(folderNode.type, 6, "node is folder"); + is(this.folderId, folderNode.itemId, "folder id and folder node item id match"); + + var shortcutNode = rootNode.getChild(1); + is(shortcutNode.type, 9, "node is folder shortcut"); + is(this.shortcutId, shortcutNode.itemId, "shortcut id and shortcut node item id match"); + + var concreteId = PlacesUtils.getConcreteItemId(shortcutNode); + is(concreteId, folderNode.itemId, "shortcut node id and concrete id match"); + + is(PlacesControllerDragHelper.canMoveContainer(this.shortcutId), + true, "can move folder shortcut id"); + + is(PlacesControllerDragHelper.canMoveContainerNode(shortcutNode), + true, "can move folder shortcut node"); + } + }); + + // add a regular query, should be moveable + tests.push({ + populate: function() { + this.bookmarkId = + PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("http://foo.com"), IDX, "foo"); + this.queryId = + PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:terms=foo"), IDX, "bar"); + }, + validate: function() { + is(rootNode.childCount, 2, + "populated data to the test root"); + + var bmNode = rootNode.getChild(0); + is(bmNode.itemId, this.bookmarkId, "bookmark id and bookmark node item id match"); + + var queryNode = rootNode.getChild(1); + is(queryNode.itemId, this.queryId, "query id and query node item id match"); + + is(PlacesControllerDragHelper.canMoveContainer(this.queryId), + true, "can move query id"); + + is(PlacesControllerDragHelper.canMoveContainerNode(queryNode), + true, "can move query node"); + } + }); + + // test that special folders cannot be moved + // test that special folders shortcuts can be moved + tests.push({ + folders: [PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.toolbarFolderId], + shortcuts: {}, + populate: function() { + for (var i = 0; i < this.folders.length; i++) { + var id = this.folders[i]; + this.shortcuts[id] = + PlacesUtils.bookmarks.insertBookmark(rootId, makeURI("place:folder=" + id), IDX, ""); + } + }, + validate: function() { + // test toolbar shortcut node + is(rootNode.childCount, this.folders.length, + "populated data to the test root"); + + function getRootChildNode(aId) { + var node = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false, true).root; + for (var i = 0; i < node.childCount; i++) { + var child = node.getChild(i); + if (child.itemId == aId) + return child; + } + } + + for (var i = 0; i < this.folders.length; i++) { + var id = this.folders[i]; + + is(PlacesControllerDragHelper.canMoveContainer(id), + false, "shouldn't be able to move special folder id"); + + //var node = PlacesUtils.getFolderContents(id, false, true).root; + var node = getRootChildNode(id); + is(PlacesControllerDragHelper.canMoveContainerNode(node), + false, "shouldn't be able to move special folder node"); + + var shortcutId = this.shortcuts[id]; + var shortcutNode = rootNode.getChild(i); + + is(shortcutNode.itemId, shortcutId, "shortcut id and shortcut node item id match"); + + LOG("can move shortcut id?"); + is(PlacesControllerDragHelper.canMoveContainer(shortcutId), + true, "should be able to move special folder shortcut id"); + + LOG("can move shortcut node?"); + is(PlacesControllerDragHelper.canMoveContainerNode(shortcutNode), + true, "should be able to move special folder shortcut node"); + } + } + }); + + // test that a tag container cannot be moved + tests.push({ + populate: function() { + // tag a uri + this.uri = makeURI("http://foo.com"); + PlacesUtils.tagging.tagURI(this.uri, ["bar"]); + }, + validate: function() { + // get tag root + var query = PlacesUtils.history.getNewQuery(); + var options = PlacesUtils.history.getNewQueryOptions(); + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY; + var tagsNode = PlacesUtils.history.executeQuery(query, options).root; + + tagsNode.containerOpen = true; + is(tagsNode.childCount, 1, "has new tag"); + + var tagNode = tagsNode.getChild(0); + + is(PlacesControllerDragHelper.canMoveContainerNode(tagNode), + false, "should not be able to move tag container node"); + } + }); + + // test that any child of a read-only node cannot be moved + tests.push({ + populate: function() { + this.id = + PlacesUtils.bookmarks.createFolder(rootId, "foo", IDX); + PlacesUtils.bookmarks.createFolder(this.id, "bar", IDX); + PlacesUtils.bookmarks.setFolderReadonly(this.id, true); + }, + validate: function() { + is(rootNode.childCount, 1, + "populate added data to the test root"); + var readOnlyFolder = rootNode.getChild(0); + + // test that we can move the read-only folder + is(PlacesControllerDragHelper.canMoveContainer(this.id), + true, "can move read-only folder id"); + is(PlacesControllerDragHelper.canMoveContainerNode(readOnlyFolder), + true, "can move read-only folder node"); + + // test that we cannot move the child of a read-only folder + readOnlyFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode); + readOnlyFolder.containerOpen = true; + var childFolder = readOnlyFolder.getChild(0); + + is(PlacesControllerDragHelper.canMoveContainer(childFolder.itemId), + false, "cannot move a child of a read-only folder"); + is(PlacesControllerDragHelper.canMoveContainerNode(childFolder), + false, "cannot move a child node of a read-only folder node"); + } + }); + + tests.forEach(function(aTest) { + PlacesUtils.bookmarks.removeFolderChildren(rootId); + aTest.populate(); + aTest.validate(); + }); + + PlacesUtils.bookmarks.removeItem(rootId); +} diff --git a/toolkit/components/places/src/utils.js b/toolkit/components/places/src/utils.js index 976c46842aa0..cd9ad053b9e8 100644 --- a/toolkit/components/places/src/utils.js +++ b/toolkit/components/places/src/utils.js @@ -50,6 +50,7 @@ var Cc = Components.classes; var Cr = Components.results; const POST_DATA_ANNO = "bookmarkProperties/POSTData"; +const READ_ONLY_ANNO = "placesInternal/READ_ONLY"; const LMANNO_FEEDURI = "livemark/feedURI"; const LMANNO_SITEURI = "livemark/siteURI"; @@ -439,9 +440,11 @@ var PlacesUtils = { * Used instead of the node's URI if provided. * This is useful for wrapping a container as TYPE_X_MOZ_URL, * TYPE_HTML or TYPE_UNICODE. + * @param aForceCopy + * Does a full copy, resolving folder shortcuts. * @returns A string serialization of the node */ - wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI) { + wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI, aForceCopy) { var self = this; // when wrapping a node, we want all the items, even if the original @@ -449,8 +452,10 @@ var PlacesUtils = { // this can happen when copying from the left hand pane of the bookmarks // organizer function convertNode(cNode) { - if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) - return self.getFolderContents(cNode.itemId, false, true).root; + if (self.nodeIsFolder(cNode) && asQuery(cNode).queryOptions.excludeItems) { + var concreteId = self.getConcreteItemId(cNode); + return self.getFolderContents(concreteId, false, true).root; + } return cNode; } @@ -464,7 +469,7 @@ var PlacesUtils = { this.value += aStr; } }; - self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true); + self.serializeNodeAsJSONToOutputStream(convertNode(aNode), writer, true, aForceCopy); return writer.value; case this.TYPE_X_MOZ_URL: function gatherDataUrl(bNode) { @@ -490,7 +495,7 @@ var PlacesUtils = { return s; } // escape out potential HTML in the title - var escapedTitle = htmlEscape(bNode.title); + var escapedTitle = bNode.title ? htmlEscape(bNode.title) : ""; if (self.nodeIsLivemarkContainer(bNode)) { var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec; return "" + escapedTitle + "" + NEWLINE; @@ -1190,9 +1195,11 @@ var PlacesUtils = { * @param aIsUICommand * Boolean - If true, modifies serialization so that each node self-contained. * For Example, tags are serialized inline with each bookmark. + * @param aResolveShortcuts + * Converts folder shortcuts into actual folders. */ serializeNodeAsJSONToOutputStream: - function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand) { + function PU_serializeNodeAsJSONToOutputStream(aNode, aStream, aIsUICommand, aResolveShortcuts) { var self = this; function addGenericProperties(aPlacesNode, aJSNode) { @@ -1219,8 +1226,12 @@ var PlacesUtils = { // backup/restore of non-whitelisted annos // XXX causes JSON encoding errors, so utf-8 encode //anno.value = unescape(encodeURIComponent(anno.value)); - if (anno.name == "livemark/feedURI") + if (anno.name == LMANNO_FEEDURI) aJSNode.livemark = 1; + if (anno.name == READ_ONLY_ANNO && aResolveShortcuts) { + // When copying a read-only node, remove the read-only annotation. + return false; + } return anno.name != "placesInternal/GUID"; }); } catch(ex) { @@ -1259,14 +1270,17 @@ var PlacesUtils = { function addContainerProperties(aPlacesNode, aJSNode) { // saved queries - if (aJSNode.id != -1 && - self.bookmarks.getItemType(aJSNode.id) == self.bookmarks.TYPE_BOOKMARK) { + var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode); + if (aJSNode.id != -1 && (PlacesUtils.nodeIsQuery(aPlacesNode) || + (concreteId != aPlacesNode.itemId && !aResolveShortcuts))) { aJSNode.type = self.TYPE_X_MOZ_PLACE; aJSNode.uri = aPlacesNode.uri; - aJSNode.concreteId = PlacesUtils.getConcreteItemId(aPlacesNode); + aJSNode.concreteId = concreteId; return; } else if (aJSNode.id != -1) { // bookmark folder + if (concreteId != aPlacesNode.itemId) + aJSNode.type = self.TYPE_X_MOZ_PLACE; aJSNode.type = self.TYPE_X_MOZ_PLACE_CONTAINER; // mark special folders if (aJSNode.id == self.bookmarks.placesRoot) diff --git a/toolkit/components/places/tests/bookmarks/test_423515_forceCopyShortcuts.js b/toolkit/components/places/tests/bookmarks/test_423515_forceCopyShortcuts.js new file mode 100644 index 000000000000..106303f027f7 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_423515_forceCopyShortcuts.js @@ -0,0 +1,147 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bug 384370 code. + * + * The Initial Developer of the Original Code is Mozilla Corp. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dietrich Ayala + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +Components.utils.import("resource://gre/modules/utils.js"); + +const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX; + +function run_test() { + /* + - create folder A + - add a bookmark to it + - create a bookmark that's a place: folder shortcut to the new folder + - serialize it to JSON, forcing copy + - import JSON + - confirm that the newly imported folder is a full copy and not a shortcut + */ + + var folderA = + PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, + "test folder", DEFAULT_INDEX); + var bookmarkURI = uri("http://test"); + PlacesUtils.bookmarks.insertBookmark(folderA, bookmarkURI, + DEFAULT_INDEX, ""); + + // create the query + var queryURI = uri("place:folder=" + folderA); + var queryTitle = "test query"; + var queryId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + queryURI, DEFAULT_INDEX, queryTitle); + LOG("queryId: " + queryId); + + // create a query that's *not* a folder shortcut + var queryURI2 = uri("place:"); + var queryTitle2 = "non-folder test query"; + var queryId2 = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + queryURI2, DEFAULT_INDEX, queryTitle2); + + // check default state + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.toolbarFolderId], 1); + var options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = true; + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // check folder query node + var queryNode = root.getChild(root.childCount-2); + do_check_eq(queryNode.type, queryNode.RESULT_TYPE_FOLDER_SHORTCUT); + do_check_eq(queryNode.title, queryTitle); + do_check_true(queryURI.equals(uri(queryNode.uri))); + queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + queryNode.containerOpen = true; + do_check_eq(queryNode.childCount, 1); + var bookmark = queryNode.getChild(0); + do_check_true(bookmarkURI.equals(uri(bookmark.uri))); + queryNode.containerOpen = false; + + // check non-folder query node + var queryNode2 = root.getChild(root.childCount-1); + do_check_eq(queryNode2.type, queryNode2.RESULT_TYPE_QUERY); + do_check_eq(queryNode2.title, queryTitle2); + do_check_true(queryURI2.equals(uri(queryNode2.uri))); + queryNode2.QueryInterface(Ci.nsINavHistoryContainerResultNode); + queryNode2.containerOpen = true; + do_check_eq(queryNode2.childCount, 0); + queryNode2.containerOpen = false; + + // clean up + root.containerOpen = false; + + // serialize + var stream = { + _str: "", + write: function(aData, aLen) { + this._str += aData; + } + }; + PlacesUtils.serializeNodeAsJSONToOutputStream(queryNode, stream, false, true); + + LOG("SERIALIZED: " + stream._str); + + PlacesUtils.bookmarks.removeItem(queryId); + + // import + PlacesUtils.importJSONNode(stream._str, PlacesUtils.toolbarFolderId, -1); + + // query for node + var query = PlacesUtils.history.getNewQuery(); + query.setFolders([PlacesUtils.toolbarFolderId], 1); + var options = PlacesUtils.history.getNewQueryOptions(); + var result = PlacesUtils.history.executeQuery(query, options); + var root = result.root; + root.containerOpen = true; + + // check folder node (no longer shortcut) + var queryNode = root.getChild(root.childCount-2); + do_check_eq(queryNode.type, queryNode.RESULT_TYPE_FOLDER); + queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + queryNode.containerOpen = true; + do_check_eq(queryNode.childCount, 1); + var child = queryNode.getChild(0); + do_check_true(bookmarkURI.equals(uri(child.uri))); + + var queryNode2 = root.getChild(root.childCount-1); + do_check_eq(queryNode2.type, queryNode2.RESULT_TYPE_QUERY); + queryNode2.QueryInterface(Ci.nsINavHistoryContainerResultNode); + queryNode2.containerOpen = true; + do_check_eq(queryNode2.childCount, 0); +}