www

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

collectionTreeViewTest.js (39491B)


      1 "use strict";
      2 
      3 describe("Zotero.CollectionTreeView", function() {
      4 	var win, zp, cv, userLibraryID;
      5 	
      6 	before(function* () {
      7 		win = yield loadZoteroPane();
      8 		zp = win.ZoteroPane;
      9 		cv = zp.collectionsView;
     10 		userLibraryID = Zotero.Libraries.userLibraryID;
     11 	});
     12 	beforeEach(function () {
     13 		// TODO: Add a selectCollection() function and select a collection instead?
     14 		return selectLibrary(win);
     15 	})
     16 	after(function () {
     17 		win.close();
     18 	});
     19 	
     20 	describe("#refresh()", function () {
     21 		it("should show Duplicate Items and Unfiled Items by default", function* () {
     22 			Zotero.Prefs.clear('duplicateLibraries');
     23 			Zotero.Prefs.clear('unfiledLibraries');
     24 			yield cv.refresh();
     25 			assert.ok(cv.getRowIndexByID("D" + userLibraryID));
     26 			assert.ok(cv.getRowIndexByID("U" + userLibraryID));
     27 		});
     28 		
     29 		it("shouldn't show Duplicate Items and Unfiled Items if hidden", function* () {
     30 			Zotero.Prefs.set('duplicateLibraries', `{"${userLibraryID}": false}`);
     31 			Zotero.Prefs.set('unfiledLibraries', `{"${userLibraryID}": false}`);
     32 			yield cv.refresh();
     33 			assert.isFalse(cv.getRowIndexByID("D" + userLibraryID));
     34 			assert.isFalse(cv.getRowIndexByID("U" + userLibraryID));
     35 		});
     36 		
     37 		it("should maintain open state of group", function* () {
     38 			var group1 = yield createGroup();
     39 			var group2 = yield createGroup();
     40 			var group1Row = cv.getRowIndexByID(group1.treeViewID);
     41 			var group2Row = cv.getRowIndexByID(group2.treeViewID);
     42 			
     43 			// Open group 1 and close group 2
     44 			if (!cv.isContainerOpen(group1Row)) {
     45 				yield cv.toggleOpenState(group1Row);
     46 			}
     47 			if (cv.isContainerOpen(group2Row)) {
     48 				yield cv.toggleOpenState(group2Row);
     49 			}
     50 			// Don't wait for delayed save
     51 			cv._saveOpenStates();
     52 			
     53 			group1Row = cv.getRowIndexByID(group1.treeViewID);
     54 			group2Row = cv.getRowIndexByID(group2.treeViewID);
     55 			
     56 			yield cv.refresh();
     57 			
     58 			// Group rows shouldn't have changed
     59 			assert.equal(cv.getRowIndexByID(group1.treeViewID), group1Row);
     60 			assert.equal(cv.getRowIndexByID(group2.treeViewID), group2Row);
     61 			// Group open states shouldn't have changed
     62 			assert.isTrue(cv.isContainerOpen(group1Row));
     63 			assert.isFalse(cv.isContainerOpen(group2Row));
     64 		});
     65 		
     66 		it("should update associated item tree view", function* () {
     67 			var collection = yield createDataObject('collection');
     68 			var item = yield createDataObject('item', { collections: [collection.id] });
     69 			yield cv.reload();
     70 			yield cv.selectCollection(collection.id);
     71 			yield cv.selectItem(item.id);
     72 		});
     73 	});
     74 	
     75 	describe("collapse/expand", function () {
     76 		it("should close and open My Library repeatedly", function* () {
     77 			yield cv.selectLibrary(userLibraryID);
     78 			var row = cv.selection.currentIndex;
     79 			
     80 			cv.collapseLibrary(userLibraryID);
     81 			var nextRow = cv.getRow(row + 1);
     82 			assert.equal(cv.selection.currentIndex, row);
     83 			assert.ok(nextRow.isSeparator());
     84 			assert.isFalse(cv.isContainerOpen(row));
     85 			
     86 			yield cv.expandLibrary(userLibraryID);
     87 			nextRow = cv.getRow(row + 1);
     88 			assert.equal(cv.selection.currentIndex, row);
     89 			assert.ok(!nextRow.isSeparator());
     90 			assert.ok(cv.isContainerOpen(row));
     91 			
     92 			cv.collapseLibrary(userLibraryID);
     93 			nextRow = cv.getRow(row + 1);
     94 			assert.equal(cv.selection.currentIndex, row);
     95 			assert.ok(nextRow.isSeparator());
     96 			assert.isFalse(cv.isContainerOpen(row));
     97 			
     98 			yield cv.expandLibrary(userLibraryID);
     99 			nextRow = cv.getRow(row + 1);
    100 			assert.equal(cv.selection.currentIndex, row);
    101 			assert.ok(!nextRow.isSeparator());
    102 			assert.ok(cv.isContainerOpen(row));
    103 		})
    104 	})
    105 	
    106 	describe("#expandLibrary()", function () {
    107 		var libraryRow, col1, col2, col3;
    108 		
    109 		before(function* () {
    110 			yield cv.selectLibrary(userLibraryID);
    111 			libraryRow = cv.selection.currentIndex;
    112 		});
    113 		
    114 		beforeEach(function* () {
    115 			// My Library
    116 			//   - A
    117 			//     - B
    118 			//       - C
    119 			col1 = yield createDataObject('collection');
    120 			col2 = yield createDataObject('collection', { parentID: col1.id });
    121 			col3 = yield createDataObject('collection', { parentID: col2.id });
    122 		});
    123 		
    124 		it("should open a library and respect stored container state", function* () {
    125 			// Collapse B
    126 			yield cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
    127 			yield cv._saveOpenStates();
    128 			
    129 			// Close and reopen library
    130 			yield cv.toggleOpenState(libraryRow);
    131 			yield cv.expandLibrary(userLibraryID);
    132 			
    133 			assert.ok(cv.getRowIndexByID(col1.treeViewID))
    134 			assert.ok(cv.getRowIndexByID(col2.treeViewID))
    135 			assert.isFalse(cv.getRowIndexByID(col3.treeViewID))
    136 		});
    137 		
    138 		it("should open a library and all subcollections in recursive mode", function* () {
    139 			yield cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
    140 			yield cv._saveOpenStates();
    141 			
    142 			// Close and reopen library
    143 			yield cv.toggleOpenState(libraryRow);
    144 			yield cv.expandLibrary(userLibraryID, true);
    145 			
    146 			assert.ok(cv.getRowIndexByID(col1.treeViewID))
    147 			assert.ok(cv.getRowIndexByID(col2.treeViewID))
    148 			assert.ok(cv.getRowIndexByID(col3.treeViewID))
    149 		});
    150 		
    151 		it("should open a group and show top-level collections", function* () {
    152 			var group = yield createGroup();
    153 			var libraryID = group.libraryID;
    154 			var col1 = yield createDataObject('collection', { libraryID });
    155 			var col2 = yield createDataObject('collection', { libraryID });
    156 			var col3 = yield createDataObject('collection', { libraryID });
    157 			var col4 = yield createDataObject('collection', { libraryID, parentID: col1.id });
    158 			var col5 = yield createDataObject('collection', { libraryID, parentID: col4.id });
    159 			
    160 			// Close everything
    161 			[col4, col1, group].forEach(o => cv._closeContainer(cv.getRowIndexByID(o.treeViewID)));
    162 			
    163 			yield cv.expandLibrary(libraryID);
    164 			assert.isNumber(cv.getRowIndexByID(col1.treeViewID));
    165 			assert.isNumber(cv.getRowIndexByID(col2.treeViewID));
    166 			assert.isNumber(cv.getRowIndexByID(col3.treeViewID));
    167 			assert.isFalse(cv.getRowIndexByID(col4.treeViewID));
    168 			assert.isFalse(cv.getRowIndexByID(col5.treeViewID));
    169 		});
    170 	});
    171 	
    172 	describe("#expandToCollection()", function () {
    173 		it("should expand a collection to a subcollection", function* () {
    174 			var collection1 = yield createDataObject('collection');
    175 			var collection2 = createUnsavedDataObject('collection');
    176 			collection2.parentID = collection1.id;
    177 			yield collection2.saveTx({
    178 				skipSelect: true
    179 			});
    180 			var row = cv.getRowIndexByID("C" + collection1.id);
    181 			assert.isFalse(cv.isContainerOpen(row));
    182 			
    183 			yield cv.expandToCollection(collection2.id);
    184 			
    185 			// Make sure parent row position hasn't changed
    186 			assert.equal(cv.getRowIndexByID("C" + collection1.id), row);
    187 			// Parent should have been opened
    188 			assert.isTrue(cv.isContainerOpen(row));
    189 		})
    190 	})
    191 	
    192 	describe("#selectByID()", function () {
    193 		it("should select the trash", function* () {
    194 			yield cv.selectByID("T1");
    195 			var row = cv.selection.currentIndex;
    196 			var treeRow = cv.getRow(row);
    197 			assert.ok(treeRow.isTrash());
    198 			assert.equal(treeRow.ref.libraryID, userLibraryID);
    199 		})
    200 	})
    201 	
    202 	describe("#selectWait()", function () {
    203 		it("shouldn't hang if row is already selected", function* () {
    204 			var row = cv.getRowIndexByID("T" + userLibraryID);
    205 			cv.selection.select(row);
    206 			yield Zotero.Promise.delay(50);
    207 			yield cv.selectWait(row);
    208 		})
    209 	})
    210 	
    211 	describe("#notify()", function () {
    212 		it("should select a new collection", function* () {
    213 			// Create collection
    214 			var collection = new Zotero.Collection;
    215 			collection.name = "Select new collection";
    216 			var id = yield collection.saveTx();
    217 			
    218 			// New collection should be selected
    219 			var selected = cv.getSelectedCollection(true);
    220 			assert.equal(selected, id);
    221 		});
    222 		
    223 		it("shouldn't select a new collection if skipNotifier is passed", function* () {
    224 			// Create collection with skipNotifier flag
    225 			var collection = new Zotero.Collection;
    226 			collection.name = "No select on skipNotifier";
    227 			var id = yield collection.saveTx({
    228 				skipNotifier: true
    229 			});
    230 			
    231 			// Library should still be selected
    232 			assert.equal(cv.getSelectedLibraryID(), userLibraryID);
    233 		});
    234 		
    235 		it("shouldn't select a new collection if skipSelect is passed", function* () {
    236 			// Create collection with skipSelect flag
    237 			var collection = new Zotero.Collection;
    238 			collection.name = "No select on skipSelect";
    239 			var id = yield collection.saveTx({
    240 				skipSelect: true
    241 			});
    242 			
    243 			// Library should still be selected
    244 			assert.equal(cv.getSelectedLibraryID(), userLibraryID);
    245 		});
    246 		
    247 		it("shouldn't select a modified collection", function* () {
    248 			// Create collection
    249 			var collection = new Zotero.Collection;
    250 			collection.name = "No select on modify";
    251 			var id = yield collection.saveTx();
    252 			
    253 			yield selectLibrary(win);
    254 			
    255 			collection.name = "No select on modify 2";
    256 			yield collection.saveTx();
    257 			
    258 			// Modified collection should not be selected
    259 			assert.equal(cv.getSelectedLibraryID(), userLibraryID);
    260 		});
    261 		
    262 		it("should maintain selection on a selected modified collection", function* () {
    263 			// Create collection
    264 			var collection = new Zotero.Collection;
    265 			collection.name = "Reselect on modify";
    266 			var id = yield collection.saveTx();
    267 			
    268 			var selected = cv.getSelectedCollection(true);
    269 			assert.equal(selected, id);
    270 			
    271 			collection.name = "Reselect on modify 2";
    272 			yield collection.saveTx();
    273 			
    274 			// Modified collection should still be selected
    275 			selected = cv.getSelectedCollection(true);
    276 			assert.equal(selected, id);
    277 		});
    278 		
    279 		it("should update the editability of the current view", function* () {
    280 			var group = yield createGroup({
    281 				editable: false,
    282 				filesEditable: false
    283 			});
    284 			yield cv.selectLibrary(group.libraryID);
    285 			yield waitForItemsLoad(win);
    286 			
    287 			assert.isFalse(cv.selectedTreeRow.editable);
    288 			var cmd = win.document.getElementById('cmd_zotero_newStandaloneNote');
    289 			assert.isTrue(cmd.getAttribute('disabled') == 'true');
    290 			
    291 			group.editable = true;
    292 			yield group.saveTx();
    293 			
    294 			assert.isTrue(cv.selectedTreeRow.editable);
    295 			assert.isFalse(cmd.getAttribute('disabled') == 'true');
    296 		});
    297 		
    298 		it("should re-sort a modified collection", function* () {
    299 			var prefix = Zotero.Utilities.randomString() + " ";
    300 			var collectionA = yield createDataObject('collection', { name: prefix + "A" });
    301 			var collectionB = yield createDataObject('collection', { name: prefix + "B" });
    302 			
    303 			var aRow = cv.getRowIndexByID("C" + collectionA.id);
    304 			var aRowOriginal = aRow;
    305 			var bRow = cv.getRowIndexByID("C" + collectionB.id);
    306 			assert.equal(bRow, aRow + 1);
    307 			
    308 			collectionA.name = prefix + "C";
    309 			yield collectionA.saveTx();
    310 			
    311 			var aRow = cv.getRowIndexByID("C" + collectionA.id);
    312 			var bRow = cv.getRowIndexByID("C" + collectionB.id);
    313 			assert.equal(bRow, aRowOriginal);
    314 			assert.equal(aRow, bRow + 1);
    315 		})
    316 		
    317 		it("should re-sort a modified search", function* () {
    318 			var prefix = Zotero.Utilities.randomString() + " ";
    319 			var searchA = yield createDataObject('search', { name: prefix + "A" });
    320 			var searchB = yield createDataObject('search', { name: prefix + "B" });
    321 			
    322 			var aRow = cv.getRowIndexByID("S" + searchA.id);
    323 			var aRowOriginal = aRow;
    324 			var bRow = cv.getRowIndexByID("S" + searchB.id);
    325 			assert.equal(bRow, aRow + 1);
    326 			
    327 			searchA.name = prefix + "C";
    328 			yield searchA.saveTx();
    329 			
    330 			var aRow = cv.getRowIndexByID("S" + searchA.id);
    331 			var bRow = cv.getRowIndexByID("S" + searchB.id);
    332 			assert.equal(bRow, aRowOriginal);
    333 			assert.equal(aRow, bRow + 1);
    334 		})
    335 		
    336 		
    337 		it("should add collection after parent's subcollection and before non-sibling", function* () {
    338 			var c0 = yield createDataObject('collection', { name: "Test" });
    339 			var rootRow = cv.getRowIndexByID(c0.treeViewID);
    340 			
    341 			var c1 = yield createDataObject('collection', { name: "1", parentID: c0.id });
    342 			var c2 = yield createDataObject('collection', { name: "2", parentID: c0.id });
    343 			var c3 = yield createDataObject('collection', { name: "3", parentID: c1.id });
    344 			var c4 = yield createDataObject('collection', { name: "4", parentID: c3.id });
    345 			var c5 = yield createDataObject('collection', { name: "5", parentID: c1.id });
    346 			
    347 			assert.equal(cv.getRowIndexByID(c1.treeViewID), rootRow + 1);
    348 			
    349 			assert.isAbove(cv.getRowIndexByID(c1.treeViewID), cv.getRowIndexByID(c0.treeViewID));
    350 			assert.isAbove(cv.getRowIndexByID(c2.treeViewID), cv.getRowIndexByID(c0.treeViewID));
    351 			
    352 			assert.isAbove(cv.getRowIndexByID(c3.treeViewID), cv.getRowIndexByID(c1.treeViewID));
    353 			assert.isAbove(cv.getRowIndexByID(c5.treeViewID), cv.getRowIndexByID(c1.treeViewID));
    354 			assert.isBelow(cv.getRowIndexByID(c5.treeViewID), cv.getRowIndexByID(c2.treeViewID));
    355 			
    356 			assert.equal(cv.getRowIndexByID(c4.treeViewID), cv.getRowIndexByID(c3.treeViewID) + 1);
    357 		});
    358 		
    359 		
    360 		it("should add multiple collections", function* () {
    361 			var col1, col2;
    362 			yield Zotero.DB.executeTransaction(function* () {
    363 				col1 = createUnsavedDataObject('collection');
    364 				col2 = createUnsavedDataObject('collection');
    365 				yield col1.save();
    366 				yield col2.save();
    367 			});
    368 			
    369 			var aRow = cv.getRowIndexByID("C" + col1.id);
    370 			var bRow = cv.getRowIndexByID("C" + col2.id);
    371 			assert.isAbove(aRow, 0);
    372 			assert.isAbove(bRow, 0);
    373 			// skipSelect is implied for multiple collections, so library should still be selected
    374 			assert.equal(cv.selection.currentIndex, 0);
    375 		});
    376 		
    377 		
    378 		it("shouldn't refresh the items list when a collection is modified", function* () {
    379 			var collection = yield createDataObject('collection');
    380 			yield waitForItemsLoad(win);
    381 			var itemsView = zp.itemsView;
    382 			
    383 			collection.name = "New Name";
    384 			yield collection.saveTx();
    385 			
    386 			yield waitForItemsLoad(win);
    387 			assert.equal(zp.itemsView, itemsView);
    388 		})
    389 		
    390 		it("should add a saved search after collections", function* () {
    391 			var collection = new Zotero.Collection;
    392 			collection.name = "Test";
    393 			var collectionID = yield collection.saveTx();
    394 			
    395 			var search = new Zotero.Search;
    396 			search.name = "A Test Search";
    397 			search.addCondition('title', 'contains', 'test');
    398 			var searchID = yield search.saveTx();
    399 			
    400 			var collectionRow = cv._rowMap["C" + collectionID];
    401 			var searchRow = cv._rowMap["S" + searchID];
    402 			var duplicatesRow = cv._rowMap["D" + userLibraryID];
    403 			var unfiledRow = cv._rowMap["U" + userLibraryID];
    404 			
    405 			assert.isAbove(searchRow, collectionRow);
    406 			// If there's a duplicates row or an unfiled row, add before those.
    407 			// Otherwise, add before the trash
    408 			if (duplicatesRow !== undefined) {
    409 				assert.isBelow(searchRow, duplicatesRow);
    410 			}
    411 			else if (unfiledRow !== undefined) {
    412 				assert.isBelow(searchRow, unfiledRow);
    413 			}
    414 			else {
    415 				var trashRow = cv._rowMap["T" + userLibraryID];
    416 				assert.isBelow(searchRow, trashRow);
    417 			}
    418 		})
    419 		
    420 		it("shouldn't select a new group", function* () {
    421 			var group = yield createGroup();
    422 			// Library should still be selected
    423 			assert.equal(cv.getSelectedLibraryID(), userLibraryID);
    424 		})
    425 		
    426 		it("should remove a group and all children", function* () {
    427 			// Make sure Group Libraries separator and header exist already,
    428 			// since otherwise they'll interfere with the count
    429 			yield getGroup();
    430 			
    431 			var originalRowCount = cv.rowCount;
    432 			
    433 			var group = yield createGroup();
    434 			yield createDataObject('collection', { libraryID: group.libraryID });
    435 			var c = yield createDataObject('collection', { libraryID: group.libraryID });
    436 			yield createDataObject('collection', { libraryID: group.libraryID, parentID: c.id });
    437 			yield createDataObject('collection', { libraryID: group.libraryID });
    438 			yield createDataObject('collection', { libraryID: group.libraryID });
    439 			
    440 			// Group, collections, Duplicates, Unfiled, and trash
    441 			assert.equal(cv.rowCount, originalRowCount + 9);
    442 			
    443 			// Select group
    444 			yield cv.selectLibrary(group.libraryID);
    445 			yield waitForItemsLoad(win);
    446 			
    447 			var spy = sinon.spy(cv, "refresh");
    448 			try {
    449 				yield group.eraseTx();
    450 				
    451 				assert.equal(cv.rowCount, originalRowCount);
    452 				// Make sure the tree wasn't refreshed
    453 				sinon.assert.notCalled(spy);
    454 			}
    455 			finally {
    456 				spy.restore();
    457 			}
    458 		})
    459 		
    460 		it("should select a new feed", function* () {
    461 			var feed = yield createFeed();
    462 			// Feed should be selected
    463 			assert.equal(cv.getSelectedLibraryID(), feed.id);
    464 		});
    465 		
    466 		it("shouldn't select a new feed with skipSelect: true", function* () {
    467 			var feed = yield createFeed({
    468 				saveOptions: {
    469 					skipSelect: true
    470 				}
    471 			});
    472 			// Library should still be selected
    473 			assert.equal(cv.getSelectedLibraryID(), userLibraryID);
    474 		});
    475 		
    476 		it("should remove deleted feed", function* () {
    477 			var feed = yield createFeed();
    478 			yield cv.selectLibrary(feed.libraryID);
    479 			waitForDialog();
    480 			var id = feed.treeViewID;
    481 			yield win.ZoteroPane.deleteSelectedCollection();
    482 			assert.isFalse(cv.getRowIndexByID(id))
    483 		})
    484 	});
    485 	
    486 	describe("#selectItem()", function () {
    487 		it("should switch to library root if item isn't in collection", async function () {
    488 			var item = await createDataObject('item');
    489 			var collection = await createDataObject('collection');
    490 			await cv.selectItem(item.id);
    491 			await waitForItemsLoad(win);
    492 			assert.equal(cv.selection.currentIndex, 0);
    493 			assert.sameMembers(zp.itemsView.getSelectedItems(), [item]);
    494 		});
    495 	});
    496 	
    497 	describe("#drop()", function () {
    498 		/**
    499 		 * Simulate a drag and drop
    500 		 *
    501 		 * @param {String} type - 'item' or 'collection'
    502 		 * @param {String|Object} targetRow - Tree row id (e.g., "L123"), or { row, orient }
    503 		 * @param {Integer[]} collectionIDs
    504 		 * @param {Promise} [promise] - If a promise is provided, it will be waited for and its
    505 		 *     value returned after the drag. Otherwise, an 'add' event will be waited for, and
    506 		 *     an object with 'ids' and 'extraData' will be returned.
    507 		 */
    508 		var drop = Zotero.Promise.coroutine(function* (objectType, targetRow, ids, promise, action = 'copy') {
    509 			if (typeof targetRow == 'string') {
    510 				var row = cv.getRowIndexByID(targetRow);
    511 				var orient = 0;
    512 			}
    513 			else {
    514 				var { row, orient } = targetRow;
    515 			}
    516 			
    517 			var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
    518 			stub.returns(cv.getRow(row));
    519 			if (!promise) {
    520 				promise = waitForNotifierEvent("add", objectType);
    521 			}
    522 			yield cv.drop(row, orient, {
    523 				dropEffect: action,
    524 				effectAllowed: action,
    525 				mozSourceNode: win.document.getElementById(`zotero-${objectType}s-tree`).treeBoxObject.treeBody,
    526 				types: {
    527 					contains: function (type) {
    528 						return type == `zotero/${objectType}`;
    529 					}
    530 				},
    531 				getData: function (type) {
    532 					if (type == `zotero/${objectType}`) {
    533 						return ids.join(",");
    534 					}
    535 				}
    536 			});
    537 			
    538 			// Add observer to wait for add
    539 			var result = yield promise;
    540 			stub.restore();
    541 			return result;
    542 		});
    543 		
    544 		
    545 		var canDrop = Zotero.Promise.coroutine(function* (type, targetRowID, ids) {
    546 			var row = cv.getRowIndexByID(targetRowID);
    547 			
    548 			var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
    549 			stub.returns(cv.getRow(row));
    550 			var dt = {
    551 				dropEffect: 'copy',
    552 				effectAllowed: 'copy',
    553 				mozSourceNode: win.document.getElementById(`zotero-${type}s-tree`),
    554 				types: {
    555 					contains: function (type) {
    556 						return type == `zotero/${type}`;
    557 					}
    558 				},
    559 				getData: function (type) {
    560 					if (type == `zotero/${type}`) {
    561 						return ids.join(",");
    562 					}
    563 				}
    564 			};
    565 			var canDrop = cv.canDropCheck(row, 0, dt);
    566 			if (canDrop) {
    567 				canDrop = yield cv.canDropCheckAsync(row, 0, dt);
    568 			}
    569 			stub.restore();
    570 			return canDrop;
    571 		});
    572 		
    573 		describe("with items", function () {
    574 			it("should add an item to a collection", function* () {
    575 				var collection = yield createDataObject('collection', false, { skipSelect: true });
    576 				var item = yield createDataObject('item', false, { skipSelect: true });
    577 				
    578 				// Add observer to wait for collection add
    579 				var deferred = Zotero.Promise.defer();
    580 				var observerID = Zotero.Notifier.registerObserver({
    581 					notify: function (event, type, ids, extraData) {
    582 						if (type == 'collection-item' && event == 'add'
    583 								&& ids[0] == collection.id + "-" + item.id) {
    584 							setTimeout(function () {
    585 								deferred.resolve();
    586 							});
    587 						}
    588 					}
    589 				}, 'collection-item', 'test');
    590 				
    591 				yield drop('item', 'C' + collection.id, [item.id], deferred.promise);
    592 				
    593 				Zotero.Notifier.unregisterObserver(observerID);
    594 				
    595 				yield cv.selectCollection(collection.id);
    596 				yield waitForItemsLoad(win);
    597 				
    598 				var itemsView = win.ZoteroPane.itemsView
    599 				assert.equal(itemsView.rowCount, 1);
    600 				var treeRow = itemsView.getRow(0);
    601 				assert.equal(treeRow.ref.id, item.id);
    602 			})
    603 			
    604 			it("should move an item from one collection to another", function* () {
    605 				var collection1 = yield createDataObject('collection');
    606 				yield waitForItemsLoad(win);
    607 				var collection2 = yield createDataObject('collection', false, { skipSelect: true });
    608 				var item = yield createDataObject('item', { collections: [collection1.id] });
    609 				
    610 				// Add observer to wait for collection add
    611 				var deferred = Zotero.Promise.defer();
    612 				var observerID = Zotero.Notifier.registerObserver({
    613 					notify: function (event, type, ids, extraData) {
    614 						if (type == 'collection-item' && event == 'add'
    615 								&& ids[0] == collection2.id + "-" + item.id) {
    616 							setTimeout(function () {
    617 								deferred.resolve();
    618 							});
    619 						}
    620 					}
    621 				}, 'collection-item', 'test');
    622 				
    623 				yield drop('item', 'C' + collection2.id, [item.id], deferred.promise, 'move');
    624 				
    625 				Zotero.Notifier.unregisterObserver(observerID);
    626 				
    627 				// Source collection should be empty
    628 				assert.equal(zp.itemsView.rowCount, 0);
    629 				
    630 				yield cv.selectCollection(collection2.id);
    631 				yield waitForItemsLoad(win);
    632 				
    633 				// Target collection should have item
    634 				assert.equal(zp.itemsView.rowCount, 1);
    635 				var treeRow = zp.itemsView.getRow(0);
    636 				assert.equal(treeRow.ref.id, item.id);
    637 			});
    638 			
    639 			describe("My Publications", function () {
    640 				it("should add an item to My Publications", function* () {
    641 					// Remove other items in My Publications
    642 					var s = new Zotero.Search();
    643 					s.addCondition('libraryID', 'is', Zotero.Libraries.userLibraryID);
    644 					s.addCondition('publications', 'true');
    645 					var ids = yield s.search();
    646 					yield Zotero.Items.erase(ids);
    647 					
    648 					var item = yield createDataObject('item', false, { skipSelect: true });
    649 					var libraryID = item.libraryID;
    650 					
    651 					var stub = sinon.stub(zp, "showPublicationsWizard")
    652 						.returns({
    653 							includeNotes: false,
    654 							includeFiles: false,
    655 							keepRights: true
    656 						});
    657 					
    658 					// Add observer to wait for item modification
    659 					var deferred = Zotero.Promise.defer();
    660 					var observerID = Zotero.Notifier.registerObserver({
    661 						notify: function (event, type, ids, extraData) {
    662 							if (type == 'item' && event == 'modify' && ids[0] == item.id) {
    663 								setTimeout(function () {
    664 									deferred.resolve();
    665 								});
    666 							}
    667 						}
    668 					}, 'item', 'test');
    669 					
    670 					yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
    671 					
    672 					Zotero.Notifier.unregisterObserver(observerID);
    673 					stub.restore();
    674 					
    675 					// Select publications and check for item
    676 					yield cv.selectByID("P" + libraryID);
    677 					yield waitForItemsLoad(win);
    678 					var itemsView = win.ZoteroPane.itemsView
    679 					assert.equal(itemsView.rowCount, 1);
    680 					var treeRow = itemsView.getRow(0);
    681 					assert.equal(treeRow.ref.id, item.id);
    682 				});
    683 				
    684 				it("should add an item with a file attachment to My Publications", function* () {
    685 					var item = yield createDataObject('item', false, { skipSelect: true });
    686 					var attachment = yield importFileAttachment('test.png', { parentItemID: item.id });
    687 					var libraryID = item.libraryID;
    688 					
    689 					var stub = sinon.stub(zp, "showPublicationsWizard")
    690 						.returns({
    691 							includeNotes: false,
    692 							includeFiles: true,
    693 							keepRights: true
    694 						});
    695 					
    696 					// Add observer to wait for modify
    697 					var deferred = Zotero.Promise.defer();
    698 					var observerID = Zotero.Notifier.registerObserver({
    699 						notify: function (event, type, ids, extraData) {
    700 							if (type == 'item' && event == 'modify' && ids[0] == item.id) {
    701 								setTimeout(function () {
    702 									deferred.resolve();
    703 								});
    704 							}
    705 						}
    706 					}, 'item', 'test');
    707 					
    708 					yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
    709 					
    710 					Zotero.Notifier.unregisterObserver(observerID);
    711 					stub.restore();
    712 					
    713 					assert.isTrue(item.inPublications);
    714 					// File attachment should be in My Publications
    715 					assert.isTrue(attachment.inPublications);
    716 				});
    717 				
    718 				it("should add an item with a linked URL attachment to My Publications", function* () {
    719 					var item = yield createDataObject('item', false, { skipSelect: true });
    720 					var attachment = yield Zotero.Attachments.linkFromURL({
    721 						parentItemID: item.id,
    722 						title: 'Test',
    723 						url: 'http://127.0.0.1/',
    724 						contentType: 'text/html'
    725 					});
    726 					var libraryID = item.libraryID;
    727 					
    728 					var stub = sinon.stub(zp, "showPublicationsWizard")
    729 						.returns({
    730 							includeNotes: false,
    731 							includeFiles: false,
    732 							keepRights: true
    733 						});
    734 					
    735 					// Add observer to wait for modify
    736 					var deferred = Zotero.Promise.defer();
    737 					var observerID = Zotero.Notifier.registerObserver({
    738 						notify: function (event, type, ids, extraData) {
    739 							if (type == 'item' && event == 'modify' && ids[0] == item.id) {
    740 								setTimeout(function () {
    741 									deferred.resolve();
    742 								});
    743 							}
    744 						}
    745 					}, 'item', 'test');
    746 					
    747 					yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
    748 					
    749 					Zotero.Notifier.unregisterObserver(observerID);
    750 					stub.restore();
    751 					
    752 					assert.isTrue(item.inPublications);
    753 					// Link attachment should be in My Publications
    754 					assert.isTrue(attachment.inPublications);
    755 				});
    756 				
    757 				it("shouldn't add linked file attachment to My Publications", function* () {
    758 					var item = yield createDataObject('item', false, { skipSelect: true });
    759 					var attachment = yield Zotero.Attachments.linkFromFile({
    760 						parentItemID: item.id,
    761 						title: 'Test',
    762 						file: OS.Path.join(getTestDataDirectory().path, 'test.png'),
    763 						contentType: 'image/png'
    764 					});
    765 					var libraryID = item.libraryID;
    766 					
    767 					var stub = sinon.stub(zp, "showPublicationsWizard")
    768 						.returns({
    769 							includeNotes: false,
    770 							includeFiles: false,
    771 							keepRights: true
    772 						});
    773 					
    774 					// Add observer to wait for modify
    775 					var deferred = Zotero.Promise.defer();
    776 					var observerID = Zotero.Notifier.registerObserver({
    777 						notify: function (event, type, ids, extraData) {
    778 							if (type == 'item' && event == 'modify' && ids[0] == item.id) {
    779 								setTimeout(function () {
    780 									deferred.resolve();
    781 								});
    782 							}
    783 						}
    784 					}, 'item', 'test');
    785 					
    786 					yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
    787 					
    788 					Zotero.Notifier.unregisterObserver(observerID);
    789 					stub.restore();
    790 					
    791 					assert.isTrue(item.inPublications);
    792 					// Linked URL attachment shouldn't be in My Publications
    793 					assert.isFalse(attachment.inPublications);
    794 				});
    795 			});
    796 			
    797 			it("should copy an item with an attachment to a group", function* () {
    798 				var group = yield createGroup();
    799 				
    800 				var item = yield createDataObject('item', false, { skipSelect: true });
    801 				var file = getTestDataDirectory();
    802 				file.append('test.png');
    803 				var attachment = yield Zotero.Attachments.importFromFile({
    804 					file: file,
    805 					parentItemID: item.id
    806 				});
    807 				
    808 				var ids = (yield drop('item', 'L' + group.libraryID, [item.id])).ids;
    809 				
    810 				yield cv.selectLibrary(group.libraryID);
    811 				yield waitForItemsLoad(win);
    812 				
    813 				// Check parent
    814 				var itemsView = win.ZoteroPane.itemsView;
    815 				assert.equal(itemsView.rowCount, 1);
    816 				var treeRow = itemsView.getRow(0);
    817 				assert.equal(treeRow.ref.libraryID, group.libraryID);
    818 				assert.equal(treeRow.ref.id, ids[0]);
    819 				// New item should link back to original
    820 				var linked = yield item.getLinkedItem(group.libraryID);
    821 				assert.equal(linked.id, treeRow.ref.id);
    822 				
    823 				// Check attachment
    824 				assert.isTrue(itemsView.isContainer(0));
    825 				itemsView.toggleOpenState(0);
    826 				assert.equal(itemsView.rowCount, 2);
    827 				treeRow = itemsView.getRow(1);
    828 				assert.equal(treeRow.ref.id, ids[1]);
    829 				// New attachment should link back to original
    830 				linked = yield attachment.getLinkedItem(group.libraryID);
    831 				assert.equal(linked.id, treeRow.ref.id);
    832 				
    833 				return group.eraseTx();
    834 			})
    835 			
    836 			it("should not copy an item or its attachment to a group twice", function* () {
    837 				var group = yield getGroup();
    838 				
    839 				var itemTitle = Zotero.Utilities.randomString();
    840 				var item = yield createDataObject('item', false, { skipSelect: true });
    841 				var file = getTestDataDirectory();
    842 				file.append('test.png');
    843 				var attachment = yield Zotero.Attachments.importFromFile({
    844 					file: file,
    845 					parentItemID: item.id
    846 				});
    847 				var attachmentTitle = Zotero.Utilities.randomString();
    848 				attachment.setField('title', attachmentTitle);
    849 				yield attachment.saveTx();
    850 				
    851 				yield drop('item', 'L' + group.libraryID, [item.id]);
    852 				assert.isFalse(yield canDrop('item', 'L' + group.libraryID, [item.id]));
    853 			})
    854 			
    855 			it("should remove a linked, trashed item in a group from the trash and collections", function* () {
    856 				var group = yield getGroup();
    857 				var collection = yield createDataObject('collection', { libraryID: group.libraryID });
    858 				
    859 				var item = yield createDataObject('item', false, { skipSelect: true });
    860 				yield drop('item', 'L' + group.libraryID, [item.id]);
    861 				
    862 				var droppedItem = yield item.getLinkedItem(group.libraryID);
    863 				droppedItem.setCollections([collection.id]);
    864 				droppedItem.deleted = true;
    865 				yield droppedItem.saveTx();
    866 				
    867 				// Add observer to wait for collection add
    868 				var deferred = Zotero.Promise.defer();
    869 				var observerID = Zotero.Notifier.registerObserver({
    870 					notify: function (event, type, ids) {
    871 						if (event == 'refresh' && type == 'trash' && ids[0] == group.libraryID) {
    872 							setTimeout(function () {
    873 								deferred.resolve();
    874 							});
    875 						}
    876 					}
    877 				}, 'trash', 'test');
    878 				yield drop('item', 'L' + group.libraryID, [item.id], deferred.promise);
    879 				Zotero.Notifier.unregisterObserver(observerID);
    880 				
    881 				assert.isFalse(droppedItem.deleted);
    882 				// Should be removed from collections when removed from trash
    883 				assert.lengthOf(droppedItem.getCollections(), 0);
    884 			})
    885 		})
    886 		
    887 		
    888 		describe("with collections", function () {
    889 			it("should make a subcollection top-level", function* () {
    890 				var collection1 = yield createDataObject('collection', { name: "A" }, { skipSelect: true });
    891 				var collection2 = yield createDataObject('collection', { name: "C" }, { skipSelect: true });
    892 				var collection3 = yield createDataObject('collection', { name: "D" }, { skipSelect: true });
    893 				var collection4 = yield createDataObject('collection', { name: "B", parentKey: collection2.key });
    894 				
    895 				var colIndex1 = cv.getRowIndexByID('C' + collection1.id);
    896 				var colIndex2 = cv.getRowIndexByID('C' + collection2.id);
    897 				var colIndex3 = cv.getRowIndexByID('C' + collection3.id);
    898 				var colIndex4 = cv.getRowIndexByID('C' + collection4.id);
    899 				
    900 				// Add observer to wait for collection add
    901 				var deferred = Zotero.Promise.defer();
    902 				var observerID = Zotero.Notifier.registerObserver({
    903 					notify: function (event, type, ids, extraData) {
    904 						if (type == 'collection' && event == 'modify' && ids[0] == collection4.id) {
    905 							setTimeout(function () {
    906 								deferred.resolve();
    907 							}, 50);
    908 						}
    909 					}
    910 				}, 'collection', 'test');
    911 				
    912 				yield drop(
    913 					'collection',
    914 					{
    915 						row: 0,
    916 						orient: 1
    917 					},
    918 					[collection4.id],
    919 					deferred.promise
    920 				);
    921 				
    922 				Zotero.Notifier.unregisterObserver(observerID);
    923 				
    924 				var newColIndex1 = cv.getRowIndexByID('C' + collection1.id);
    925 				var newColIndex2 = cv.getRowIndexByID('C' + collection2.id);
    926 				var newColIndex3 = cv.getRowIndexByID('C' + collection3.id);
    927 				var newColIndex4 = cv.getRowIndexByID('C' + collection4.id);
    928 				
    929 				assert.equal(newColIndex1, colIndex1);
    930 				assert.isBelow(newColIndex4, newColIndex2);
    931 				assert.isBelow(newColIndex2, newColIndex3);
    932 				assert.equal(cv.getRow(newColIndex4).level, cv.getRow(newColIndex1).level);
    933 			})
    934 			                                                                                         
    935 			it("should move a subcollection and its subcollection down under another collection", function* () {
    936 				var collectionA = yield createDataObject('collection', { name: "A" }, { skipSelect: true });
    937 				var collectionB = yield createDataObject('collection', { name: "B", parentKey: collectionA.key });
    938 				var collectionC = yield createDataObject('collection', { name: "C", parentKey: collectionB.key });
    939 				var collectionD = yield createDataObject('collection', { name: "D" }, { skipSelect: true });
    940 				var collectionE = yield createDataObject('collection', { name: "E" }, { skipSelect: true });
    941 				var collectionF = yield createDataObject('collection', { name: "F" }, { skipSelect: true });
    942 				var collectionG = yield createDataObject('collection', { name: "G", parentKey: collectionD.key });
    943 				var collectionH = yield createDataObject('collection', { name: "H", parentKey: collectionG.key });
    944 				
    945 				var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
    946 				var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
    947 				var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
    948 				var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
    949 				var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
    950 				var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
    951 				var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
    952 				var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
    953 				
    954 				yield cv.selectCollection(collectionG.id);                                                 
    955 				
    956 				// Add observer to wait for collection add
    957 				var deferred = Zotero.Promise.defer();                                                                          
    958 				var observerID = Zotero.Notifier.registerObserver({
    959 					notify: function (event, type, ids, extraData) {
    960 						if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
    961 							setTimeout(function () {
    962 								deferred.resolve();
    963 							}, 50);
    964 						}
    965 					}
    966 				}, 'collection', 'test');
    967 				
    968 				yield drop(
    969 					'collection',
    970 					{
    971 						row: colIndexE,
    972 						orient: 0
    973 					},
    974 					[collectionG.id],
    975 					deferred.promise
    976 				);
    977 				
    978 				Zotero.Notifier.unregisterObserver(observerID);
    979 				
    980 				var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
    981 				var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
    982 				var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
    983 				var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
    984 				var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
    985 				var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
    986 				var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
    987 				var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
    988 				
    989 				assert.isFalse(cv.isContainerOpen(newColIndexD));
    990 				assert.isTrue(cv.isContainerEmpty(newColIndexD));
    991 				assert.isTrue(cv.isContainerOpen(newColIndexE));
    992 				assert.isFalse(cv.isContainerEmpty(newColIndexE));
    993 				assert.equal(newColIndexE, newColIndexG - 1);
    994 				assert.equal(newColIndexG, newColIndexH - 1);
    995 				
    996 				// TODO: Check deeper subcollection open states
    997 			})
    998 				
    999 			it("should move a subcollection and its subcollection up under another collection", function* () {
   1000 				var collectionA = yield createDataObject('collection', { name: "A" }, { skipSelect: true });
   1001 				var collectionB = yield createDataObject('collection', { name: "B", parentKey: collectionA.key });
   1002 				var collectionC = yield createDataObject('collection', { name: "C", parentKey: collectionB.key });
   1003 				var collectionD = yield createDataObject('collection', { name: "D" }, { skipSelect: true });
   1004 				var collectionE = yield createDataObject('collection', { name: "E" }, { skipSelect: true });
   1005 				var collectionF = yield createDataObject('collection', { name: "F" }, { skipSelect: true });
   1006 				var collectionG = yield createDataObject('collection', { name: "G", parentKey: collectionE.key });
   1007 				var collectionH = yield createDataObject('collection', { name: "H", parentKey: collectionG.key });
   1008 				
   1009 				var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
   1010 				var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
   1011 				var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
   1012 				var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
   1013 				var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
   1014 				var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
   1015 				var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
   1016 				var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
   1017 				
   1018 				yield cv.selectCollection(collectionG.id);
   1019 				
   1020 				// Add observer to wait for collection add
   1021 				var deferred = Zotero.Promise.defer();
   1022 				var observerID = Zotero.Notifier.registerObserver({
   1023 					notify: function (event, type, ids, extraData) {
   1024 						if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
   1025 							setTimeout(function () {
   1026 								deferred.resolve();
   1027 							}, 50);
   1028 						}
   1029 					}
   1030 				}, 'collection', 'test');
   1031 				
   1032 				yield drop(
   1033 					'collection',
   1034 					{
   1035 						row: colIndexD,
   1036 						orient: 0
   1037 					},
   1038 					[collectionG.id],
   1039 					deferred.promise
   1040 				);
   1041 				
   1042 				Zotero.Notifier.unregisterObserver(observerID);
   1043 				
   1044 				var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
   1045 				var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
   1046 				var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
   1047 				var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
   1048 				var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
   1049 				var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
   1050 				var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
   1051 				var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
   1052 				
   1053 				assert.isFalse(cv.isContainerOpen(newColIndexE));
   1054 				assert.isTrue(cv.isContainerEmpty(newColIndexE));
   1055 				assert.isTrue(cv.isContainerOpen(newColIndexD));
   1056 				assert.isFalse(cv.isContainerEmpty(newColIndexD));
   1057 				assert.equal(newColIndexD, newColIndexG - 1);
   1058 				assert.equal(newColIndexG, newColIndexH - 1);
   1059 				
   1060 				// TODO: Check deeper subcollection open states
   1061 			})
   1062 		})
   1063 
   1064 
   1065 		describe("with feed items", function () {
   1066 			it('should add a translated feed item recovered from an URL', function* (){
   1067 				var feed = yield createFeed();
   1068 				var collection = yield createDataObject('collection', false, { skipSelect: true });
   1069 				var url = getTestDataUrl('metadata/journalArticle-single.html');
   1070 				var feedItem = yield createDataObject('feedItem', {libraryID: feed.libraryID}, { skipSelect: true });
   1071 				feedItem.setField('url', url);
   1072 				yield feedItem.saveTx();
   1073 				var translateFn = sinon.spy(feedItem, 'translate');
   1074 				
   1075 				// Add observer to wait for collection add
   1076 				var deferred = Zotero.Promise.defer();
   1077 				var itemIds;
   1078 
   1079 				var ids = (yield drop('item', 'C' + collection.id, [feedItem.id])).ids;
   1080 				
   1081 				// Check that the translated item was the one that was created after drag
   1082 				var item;
   1083 				yield translateFn.returnValues[0].then(function(i) {
   1084 					item = i;
   1085 					assert.equal(item.id, ids[0]);
   1086 				});
   1087 				
   1088 				yield cv.selectCollection(collection.id);
   1089 				yield waitForItemsLoad(win);
   1090 				
   1091 				var itemsView = win.ZoteroPane.itemsView;
   1092 				assert.equal(itemsView.rowCount, 1);
   1093 				var treeRow = itemsView.getRow(0);
   1094 				assert.equal(treeRow.ref.id, item.id);
   1095 			})
   1096 		})
   1097 	})
   1098 })