www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 61cb01b7c20819669a377abab7369dfc98273ffe
parent a2d4b050644f4e93e875d4c0f75bcd6376cc4cd5
Author: Dan Stillman <dstillman@zotero.org>
Date:   Fri, 22 May 2015 19:15:21 -0400

Collection/item tree selection improvements

Wait for the pane's collectionSelected() to finish before returning from
collectionTreeView select methods (e.g., selectLibrary()), and wait for
previous items view to finish loading before creating a new one in
collectionSelected(). This ensures that the items view has been created (though
not loaded) before returning from a select. The tree can still get a bit
confused switching between collections, but I think we're getting closer to
fixing that.

Also switch the items tree to use the same pattern.

This also fixes dragging items to collections (#731).

Diffstat:
Mchrome/content/zotero/xpcom/collectionTreeView.js | 39++++++++++++++++++++++++++++++++-------
Mchrome/content/zotero/xpcom/data/collection.js | 31+++++++++++++++----------------
Mchrome/content/zotero/xpcom/itemTreeView.js | 61+++++++++++++++++++++++++++----------------------------------
Mchrome/content/zotero/xpcom/libraryTreeView.js | 35+++++++++++++++++++++++++++++++++--
Mchrome/content/zotero/zoteroPane.js | 234+++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mtest/tests/collectionTreeViewTest.js | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
6 files changed, 295 insertions(+), 188 deletions(-)

diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -71,7 +71,6 @@ Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", { }); - /* * Called by the tree itself */ @@ -248,6 +247,29 @@ Zotero.CollectionTreeView.prototype.reload = function() }.bind(this)); } + +/** + * Select a row and wait for its items view to be created + * + * Note that this doesn't wait for the items view to be loaded. For that, add a 'load' event + * listener to the items view. + * + * @param {Integer} row + * @return {Promise} + */ +Zotero.CollectionTreeView.prototype.selectWait = Zotero.Promise.method(function (row) { + if (this.selection.selectEventsSuppressed) { + this.selection.select(row); + return; + } + var deferred = Zotero.Promise.defer(); + this.addEventListener('select', () => deferred.resolve()); + this.selection.select(row); + return deferred.promise; +}); + + + /* * Called by Zotero.Notifier on any changes to collections in the data layer */ @@ -389,7 +411,10 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* } } + var deferred = Zotero.Promise.defer(); + this.addEventListener('select', () => deferred.resolve()); this.selection.selectEventsSuppressed = false; + return deferred.promise; }); /** @@ -823,7 +848,7 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi switch (type) { case 'L': - this.selectLibrary(id); + yield this.selectLibrary(id); return; case 'C': @@ -841,14 +866,14 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi } var row = this._rowMap[type + id]; this._treebox.ensureRowIsVisible(row); - this.selection.select(row); + yield this.selectWait(row); }); /** * @param {Integer} libraryID Library to select */ -Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) { +Zotero.CollectionTreeView.prototype.selectLibrary = Zotero.Promise.coroutine(function* (libraryID) { if (Zotero.suppressUIUpdates) { Zotero.debug("UI updates suppressed -- not changing library selection"); return false; @@ -857,7 +882,7 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) { // Select local library if (!libraryID) { this._treebox.ensureRowIsVisible(0); - this.selection.select(0); + yield this.selectWait(0); return true; } @@ -874,12 +899,12 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) { var row = this._rowMap['L' + libraryID]; if (row !== undefined) { this._treebox.ensureRowIsVisible(row); - this.selection.select(row); + this.selectWait(row); return true; } return false; -} +}); Zotero.CollectionTreeView.prototype.selectCollection = function (id) { diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -360,23 +360,22 @@ Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemI yield this.loadChildItems(); var current = this.getChildItems(true); - return Zotero.DB.executeTransaction(function* () { - for (let i=0; i<itemIDs.length; i++) { - let itemID = itemIDs[i]; - - if (current && current.indexOf(itemID) != -1) { - Zotero.debug("Item " + itemID + " already a child of collection " + this.id); - continue; - } - - let item = yield this.ChildObjects.getAsync(itemID); - yield item.loadCollections(); - item.addToCollection(this.id); - yield item.save({ - skipDateModifiedUpdate: true - }); + Zotero.DB.requireTransaction(); + for (let i = 0; i < itemIDs.length; i++) { + let itemID = itemIDs[i]; + + if (current && current.indexOf(itemID) != -1) { + Zotero.debug("Item " + itemID + " already a child of collection " + this.id); + continue; } - }.bind(this)); + + let item = yield this.ChildObjects.getAsync(itemID); + yield item.loadCollections(); + item.addToCollection(this.id); + yield item.save({ + skipDateModifiedUpdate: true + }); + } yield this.loadChildItems(true); }); diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js @@ -68,7 +68,7 @@ Zotero.ItemTreeView.prototype.type = 'item'; Zotero.ItemTreeView.prototype.setTree = Zotero.serial(Zotero.Promise.coroutine(function* (treebox) { try { - Zotero.debug("Setting item tree"); + Zotero.debug("Setting tree for items view " + this.id); var start = Date.now(); // Try to set the window document if not yet set if (treebox && !this._ownerDocument) { @@ -256,7 +256,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.serial(Zotero.Promise.coroutine(f this.collectionTreeRow.itemToSelect = null; } - Zotero.debug("Set tree in "+(Date.now()-start)+" ms"); + Zotero.debug("Set tree for items view " + this.id + " in " + (Date.now() - start) + " ms"); this._initialized = true; yield this._runListeners('load'); @@ -434,7 +434,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio yield this._refreshPromise; if (!this._treebox || !this._treebox.treeBody) { - Components.utils.reportError("Treebox didn't exist in itemTreeView.notify()"); + Zotero.debug("Treebox didn't exist in itemTreeView.notify()"); return; } @@ -934,12 +934,13 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio //this._treebox.endUpdateBatch(); if (madeChanges) { - var promise = this._getItemSelectedPromise(); + var deferred = Zotero.Promise.defer(); + this.addEventListener('select', () => deferred.resolve()); } this.selection.selectEventsSuppressed = false; if (madeChanges) { Zotero.debug("Yielding for select promise"); // TEMP - yield promise; + return deferred.promise; } }); @@ -1630,6 +1631,12 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i return false; } + var selected = this.getSelectedItems(true); + if (selected.length == 1 && selected[0] == id) { + Zotero.debug("Item " + id + " is already selected"); + return; + } + var row = this._rowMap[id]; // Get the row of the parent, if there is one @@ -1674,17 +1681,20 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i row = this._rowMap[id]; } - // This function calls nsITreeSelection.select(), which triggers the <tree>'s 'onselect' - // attribute, which calls ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), - // which refreshes the itembox. But since the 'onselect' doesn't handle promises, - // itemSelected() isn't waited for and 'yield selectItem(itemID)' continues before the - // itembox has been refreshed. To get around this, we make a promise resolver that's - // triggered by itemSelected() when it's done. - if (!this.selection.selectEventsSuppressed) { - var itemSelectedPromise = this._getItemSelectedPromise(id); + // this.selection.select() triggers the <tree>'s 'onselect' attribute, which calls + // ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), which refreshes the + // itembox. But since the 'onselect' doesn't handle promises, itemSelected() isn't waited for + // here, which means that 'yield selectItem(itemID)' continues before the itembox has been + // refreshed. To get around this, we wait for a select event that's triggered by + // itemSelected() when it's done. + if (this.selection.selectEventsSuppressed) { + this.selection.select(row); + } + else { + var deferred = Zotero.Promise.defer(); + this.addEventListener('select', () => deferred.resolve()); + this.selection.select(row); } - - this.selection.select(row); // If |expand|, open row if container if (expand && this.isContainer(row) && !this.isContainerOpen(row)) { @@ -1692,8 +1702,8 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i } this.selection.select(row); - if (!this.selection.selectEventsSuppressed) { - yield itemSelectedPromise; + if (deferred) { + yield deferred.promise; } // We aim for a row 5 below the target row, since ensureRowIsVisible() does @@ -1718,23 +1728,6 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i }); -Zotero.ItemTreeView.prototype._getItemSelectedPromise = function (itemID) { - if (itemID) { - var selected = this.getSelectedItems(true); - var alreadySelected = selected.length == 1 && selected[0] == itemID; - if (alreadySelected) { - return Zotero.Promise.resolve(); - } - } - return new Zotero.Promise(function () { - this._itemSelectedPromiseResolver = { - resolve: arguments[0], - reject: arguments[1] - }; - }.bind(this)); -} - - /** * Select multiple top-level items * diff --git a/chrome/content/zotero/xpcom/libraryTreeView.js b/chrome/content/zotero/xpcom/libraryTreeView.js @@ -26,10 +26,14 @@ Zotero.LibraryTreeView = function () { this._initialized = false; this._listeners = { - load: [] + load: [], + select: [] }; this._rows = []; this._rowMap = {}; + + this.id = Zotero.Utilities.randomString(); + Zotero.debug("Creating " + this.type + "s view with id " + this.id); }; Zotero.LibraryTreeView.prototype = { @@ -43,10 +47,17 @@ Zotero.LibraryTreeView.prototype = { this._listeners[event].push(listener); } } + else { + if (!this._listeners[event]) { + this._listeners[event] = []; + } + this._listeners[event].push(listener); + } }, _runListeners: Zotero.Promise.coroutine(function* (event) { + if (!this._listeners[event]) return; var listener; while (listener = this._listeners[event].shift()) { yield Zotero.Promise.resolve(listener()); @@ -55,7 +66,9 @@ Zotero.LibraryTreeView.prototype = { /** - * Returns a reference to the tree row at a given row + * Return a reference to the tree row at a given row + * + * @return {Zotero.CollectionTreeRow|Zotero.ItemTreeRow} */ getRow: function(row) { return this._rows[row]; @@ -63,6 +76,24 @@ Zotero.LibraryTreeView.prototype = { /** + * Return the index of the row with a given ID (e.g., "C123" for collection 123) + * + * @param {String} - Row id + * @return {Integer} + */ + getRowByID: function (id) { + var type = id[0]; + id = ('' + id).substr(1); + return this._rowMap[type + id]; + }, + + + onSelect: function () { + return this._runListeners('select'); + }, + + + /** * Add a tree row to the main array, update the row count, tell the treebox that the row * count changed, and update the row map * diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js @@ -1107,110 +1107,127 @@ var ZoteroPane = new function() }); - this.onCollectionSelected = Zotero.Promise.coroutine(function* () { - var collectionTreeRow = this.getCollectionTreeRow(); - if (!collectionTreeRow) { - return; - } - - if (this.itemsView && this.itemsView.collectionTreeRow == collectionTreeRow) { - Zotero.debug("Collection selection hasn't changed"); - return; - } - - if (this.itemsView) { - this.itemsView.unregister(); - document.getElementById('zotero-items-tree').view = this.itemsView = null; - } - - if (this.collectionsView.selection.count != 1) { - return; - } - - // Clear quick search and tag selector when switching views - document.getElementById('zotero-tb-search').value = ""; - - // XBL functions might not yet be available - var tagSelector = document.getElementById('zotero-tag-selector'); - if (tagSelector.clearAll) { - tagSelector.clearAll(); - } - - // Not necessary with seltype="cell", which calls nsITreeView::isSelectable() - /*if (collectionTreeRow.isSeparator()) { - document.getElementById('zotero-items-tree').view = this.itemsView = null; - return; - }*/ - - collectionTreeRow.setSearch(''); - collectionTreeRow.setTags(getTagSelection()); - - // Enable or disable toolbar icons and menu options as necessary - const disableIfNoEdit = [ - "cmd_zotero_newCollection", - "cmd_zotero_newSavedSearch", - "zotero-tb-add", - "cmd_zotero_newItemFromCurrentPage", - "zotero-tb-lookup", - "cmd_zotero_newStandaloneNote", - "zotero-tb-note-add", - "zotero-tb-attachment-add" - ]; - for(var i=0; i<disableIfNoEdit.length; i++) { - var el = document.getElementById(disableIfNoEdit[i]); + this.onCollectionSelected = function () { + return Zotero.spawn(function* () { + var collectionTreeRow = this.getCollectionTreeRow(); + if (!collectionTreeRow) { + return; + } - // If a trash is selected, new collection depends on the - // editability of the library - if (collectionTreeRow.isTrash() && - disableIfNoEdit[i] == 'cmd_zotero_newCollection') { - var overrideEditable = Zotero.Libraries.isEditable(collectionTreeRow.ref.libraryID); + if (this.itemsView && this.itemsView.collectionTreeRow == collectionTreeRow) { + Zotero.debug("Collection selection hasn't changed"); + return; } - else { - var overrideEditable = false; + + if (this.itemsView) { + // Wait for existing items view to finish loading before unloading it + // + // TODO: Cancel loading + let deferred = Zotero.Promise.defer(); + this.itemsView.addEventListener('load', function () { + deferred.resolve(); + }); + if (deferred.promise.isPending()) { + Zotero.debug("Waiting for items view " + this.itemsView.id + " to finish loading"); + } + yield deferred.promise; + + this.itemsView.unregister(); + document.getElementById('zotero-items-tree').view = this.itemsView = null; } - if (collectionTreeRow.editable || overrideEditable) { - if(el.hasAttribute("disabled")) el.removeAttribute("disabled"); - } else { - el.setAttribute("disabled", "true"); + if (this.collectionsView.selection.count != 1) { + return; } - } - - this.itemsView = new Zotero.ItemTreeView(collectionTreeRow); - this.itemsView.onError = function () { - ZoteroPane_Local.displayErrorMessage(); - }; - // If any queued load listeners, set them to run when the tree is ready - if (this._listeners.itemsLoaded) { - let listener; - while (listener = this._listeners.itemsLoaded.shift()) { - this.itemsView.addEventListener('load', listener); + + // Clear quick search and tag selector when switching views + document.getElementById('zotero-tb-search').value = ""; + + // XBL functions might not yet be available + var tagSelector = document.getElementById('zotero-tag-selector'); + if (tagSelector.clearAll) { + tagSelector.clearAll(); } - } - this.itemsView.addEventListener('load', this.setTagScope); - document.getElementById('zotero-items-tree').view = this.itemsView; - - // Add events to treecolpicker to update menu before showing/hiding - try { - let treecols = document.getElementById('zotero-items-columns-header'); - let treecolpicker = treecols.boxObject.firstChild.nextSibling; - let menupopup = treecolpicker.boxObject.firstChild.nextSibling; - let attr = menupopup.getAttribute('onpopupshowing'); - if (attr.indexOf('Zotero') == -1) { - menupopup.setAttribute('onpopupshowing', 'ZoteroPane.itemsView.onColumnPickerShowing(event);') - // Keep whatever else is there - + ' ' + attr; - menupopup.setAttribute('onpopuphidden', 'ZoteroPane.itemsView.onColumnPickerHidden(event);') - // Keep whatever else is there - + ' ' + menupopup.getAttribute('onpopuphidden'); + + // Not necessary with seltype="cell", which calls nsITreeView::isSelectable() + /*if (collectionTreeRow.isSeparator()) { + document.getElementById('zotero-items-tree').view = this.itemsView = null; + return; + }*/ + + collectionTreeRow.setSearch(''); + collectionTreeRow.setTags(getTagSelection()); + + // Enable or disable toolbar icons and menu options as necessary + const disableIfNoEdit = [ + "cmd_zotero_newCollection", + "cmd_zotero_newSavedSearch", + "zotero-tb-add", + "cmd_zotero_newItemFromCurrentPage", + "zotero-tb-lookup", + "cmd_zotero_newStandaloneNote", + "zotero-tb-note-add", + "zotero-tb-attachment-add" + ]; + for(var i=0; i<disableIfNoEdit.length; i++) { + var el = document.getElementById(disableIfNoEdit[i]); + + // If a trash is selected, new collection depends on the + // editability of the library + if (collectionTreeRow.isTrash() && + disableIfNoEdit[i] == 'cmd_zotero_newCollection') { + var overrideEditable = Zotero.Libraries.isEditable(collectionTreeRow.ref.libraryID); + } + else { + var overrideEditable = false; + } + + if (collectionTreeRow.editable || overrideEditable) { + if(el.hasAttribute("disabled")) el.removeAttribute("disabled"); + } else { + el.setAttribute("disabled", "true"); + } } - } - catch (e) { - Zotero.debug(e); - } - - Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); - }); + + this.itemsView = new Zotero.ItemTreeView(collectionTreeRow); + this.itemsView.onError = function () { + ZoteroPane_Local.displayErrorMessage(); + }; + // If any queued load listeners, set them to run when the tree is ready + if (this._listeners.itemsLoaded) { + let listener; + while (listener = this._listeners.itemsLoaded.shift()) { + this.itemsView.addEventListener('load', listener); + } + } + this.itemsView.addEventListener('load', this.setTagScope); + document.getElementById('zotero-items-tree').view = this.itemsView; + + // Add events to treecolpicker to update menu before showing/hiding + try { + let treecols = document.getElementById('zotero-items-columns-header'); + let treecolpicker = treecols.boxObject.firstChild.nextSibling; + let menupopup = treecolpicker.boxObject.firstChild.nextSibling; + let attr = menupopup.getAttribute('onpopupshowing'); + if (attr.indexOf('Zotero') == -1) { + menupopup.setAttribute('onpopupshowing', 'ZoteroPane.itemsView.onColumnPickerShowing(event);') + // Keep whatever else is there + + ' ' + attr; + menupopup.setAttribute('onpopuphidden', 'ZoteroPane.itemsView.onColumnPickerHidden(event);') + // Keep whatever else is there + + ' ' + menupopup.getAttribute('onpopuphidden'); + } + } + catch (e) { + Zotero.debug(e); + } + + Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); + }, this) + .finally(function () { + return this.collectionsView.onSelect(); + }.bind(this)); + }; this.getCollectionTreeRow = function () { @@ -1419,19 +1436,8 @@ var ZoteroPane = new function() return true; }, this) - .then(function (result) { - // See note in itemTreeView.js::selectItem() - if (this.itemsView && this.itemsView._itemSelectedPromiseResolver) { - this.itemsView._itemSelectedPromiseResolver.resolve(); - } - return result; - }.bind(this)) - .catch(function (e) { - Zotero.debug(e, 1); - if (this.itemsView && this.itemsView._itemSelectedPromiseResolver) { - this.itemsView._itemSelectedPromiseResolver.reject(e); - } - throw e; + .finally(function () { + return this.itemsView.onSelect(); }.bind(this)); } @@ -2052,11 +2058,11 @@ var ZoteroPane = new function() if (!selected) { if (item.deleted) { Zotero.debug("Item is deleted; switching to trash"); - self.collectionsView.selectTrash(item.libraryID); + yield self.collectionsView.selectTrash(item.libraryID); } else { Zotero.debug("Item was not selected; switching to library"); - self.collectionsView.selectLibrary(item.libraryID); + yield self.collectionsView.selectLibrary(item.libraryID); } yield self.itemsView.selectItem(itemID, expand); } @@ -2071,12 +2077,12 @@ var ZoteroPane = new function() // If in a different library if (item.libraryID != currentLibraryID) { Zotero.debug("Library ID differs; switching library"); - self.collectionsView.selectLibrary(item.libraryID); + yield self.collectionsView.selectLibrary(item.libraryID); } // Force switch to library view else if (!self.collectionsView.selectedTreeRow.isLibrary() && inLibrary) { Zotero.debug("Told to select in library; switching to library"); - self.collectionsView.selectLibrary(item.libraryID); + yield self.collectionsView.selectLibrary(item.libraryID); } } catch (e) { diff --git a/test/tests/collectionTreeViewTest.js b/test/tests/collectionTreeViewTest.js @@ -3,27 +3,31 @@ describe("Zotero.CollectionTreeView", function() { var win, collectionsView; + // Select library + // TODO: Add a selectCollection() function and select a collection instead + var resetSelection = Zotero.Promise.coroutine(function* () { + yield collectionsView.selectLibrary(Zotero.Libraries.userLibraryID); + yield waitForItemsLoad(win); + assert.equal(collectionsView.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); + }); + // Load Zotero pane and select library before(function* () { win = yield loadZoteroPane(); collectionsView = win.ZoteroPane.collectionsView; }); + beforeEach(function () { + return resetSelection(); + }) after(function () { win.close(); }); - // Select library - // TODO: Add a selectCollection() function and select a collection instead - var resetSelection = function () { - collectionsView.selectLibrary(Zotero.Libraries.userLibraryID); - assert.equal(collectionsView.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); - } - describe("collapse/expand", function () { it("should close and open My Library repeatedly", function* () { var libraryID = Zotero.Libraries.userLibraryID; var cv = collectionsView; - cv.selectLibrary(libraryID); + yield cv.selectLibrary(libraryID); var row = cv.selection.currentIndex; cv.collapseLibrary(libraryID); @@ -54,8 +58,6 @@ describe("Zotero.CollectionTreeView", function() { describe("#notify()", function () { it("should select a new collection", function* () { - resetSelection(); - // Create collection var collection = new Zotero.Collection; collection.name = "Select new collection"; @@ -67,8 +69,6 @@ describe("Zotero.CollectionTreeView", function() { }); it("shouldn't select a new collection if skipNotifier is passed", function* () { - resetSelection(); - // Create collection with skipNotifier flag var collection = new Zotero.Collection; collection.name = "No select on skipNotifier"; @@ -81,8 +81,6 @@ describe("Zotero.CollectionTreeView", function() { }); it("shouldn't select a new collection if skipSelect is passed", function* () { - resetSelection(); - // Create collection with skipSelect flag var collection = new Zotero.Collection; collection.name = "No select on skipSelect"; @@ -100,7 +98,7 @@ describe("Zotero.CollectionTreeView", function() { collection.name = "No select on modify"; var id = yield collection.saveTx(); - resetSelection(); + yield resetSelection(); collection.name = "No select on modify 2"; yield collection.saveTx(); @@ -157,4 +155,59 @@ describe("Zotero.CollectionTreeView", function() { } }) }) + + describe("#drop()", function () { + it("should add an item to a collection", function* () { + var collection = yield createDataObject('collection', false, { + skipSelect: true + }); + var item = yield createDataObject('item', false, { + skipSelect: true + }); + var row = collectionsView.getRowByID("C" + collection.id); + + // Add observer to wait for collection add + var deferred = Zotero.Promise.defer(); + var observerID = Zotero.Notifier.registerObserver({ + notify: function (event, type, ids) { + if (type == 'collection-item' && event == 'add' + && ids[0] == collection.id + "-" + item.id) { + setTimeout(function () { + deferred.resolve(); + }); + } + } + }, 'collection-item', 'test'); + + // Simulate a drag and drop + var stub = sinon.stub(Zotero.DragDrop, "getDragTarget"); + stub.returns(collectionsView.getRow(row)); + collectionsView.drop(row, 0, { + dropEffect: 'copy', + effectAllowed: 'copy', + mozSourceNode: win.document.getElementById('zotero-items-tree'), + types: { + contains: function (type) { + return type == 'zotero/item'; + } + }, + getData: function (type) { + if (type == 'zotero/item') { + return "" + item.id; + } + } + }) + + yield deferred.promise; + stub.restore(); + Zotero.Notifier.unregisterObserver(observerID); + yield collectionsView.selectCollection(collection.id); + yield waitForItemsLoad(win); + + var itemsView = win.ZoteroPane.itemsView + assert.equal(itemsView.rowCount, 1); + var treeRow = itemsView.getRow(0); + assert.equal(treeRow.ref.id, item.id); + }) + }) })