www

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

itemTreeViewTest.js (36340B)


      1 "use strict";
      2 
      3 describe("Zotero.ItemTreeView", function() {
      4 	var win, zp, cv, itemsView, existingItemID;
      5 	
      6 	// Load Zotero pane and select library
      7 	before(function* () {
      8 		win = yield loadZoteroPane();
      9 		zp = win.ZoteroPane;
     10 		cv = zp.collectionsView;
     11 		
     12 		var item = yield createDataObject('item', { setTitle: true });
     13 		existingItemID = item.id;
     14 	});
     15 	beforeEach(function* () {
     16 		yield selectLibrary(win);
     17 		itemsView = zp.itemsView;
     18 	})
     19 	after(function () {
     20 		win.close();
     21 	});
     22 	
     23 	it("shouldn't show items in trash in library root", function* () {
     24 		var item = yield createDataObject('item', { title: "foo" });
     25 		var itemID = item.id;
     26 		item.deleted = true;
     27 		yield item.saveTx();
     28 		assert.isFalse(itemsView.getRowIndexByID(itemID));
     29 	})
     30 	
     31 	describe("#selectItem()", function () {
     32 		/**
     33 		 * Make sure that selectItem() doesn't hang if the pane's item-select handler is never
     34 		 * triggered due to the item already being selected
     35 		 */
     36 		it("should return if item is already selected", function* () {
     37 			yield itemsView.selectItem(existingItemID);
     38 			var selected = itemsView.getSelectedItems(true);
     39 			assert.lengthOf(selected, 1);
     40 			assert.equal(selected[0], existingItemID);
     41 			yield itemsView.selectItem(existingItemID);
     42 			selected = itemsView.getSelectedItems(true);
     43 			assert.lengthOf(selected, 1);
     44 			assert.equal(selected[0], existingItemID);
     45 		});
     46 	})
     47 	
     48 	describe("#getCellText()", function () {
     49 		it("should return new value after edit", function* () {
     50 			var str = Zotero.Utilities.randomString();
     51 			var item = yield createDataObject('item', { title: str });
     52 			var row = itemsView.getRowIndexByID(item.id);
     53 			assert.equal(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str);
     54 			yield modifyDataObject(item);
     55 			assert.notEqual(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str);
     56 		})
     57 	})
     58 	
     59 	describe("#notify()", function () {
     60 		beforeEach(function () {
     61 			sinon.spy(win.ZoteroPane, "itemSelected");
     62 		})
     63 		
     64 		afterEach(function () {
     65 			win.ZoteroPane.itemSelected.restore();
     66 		})
     67 		
     68 		it("should select a new item", function* () {
     69 			itemsView.selection.clearSelection();
     70 			assert.lengthOf(itemsView.getSelectedItems(), 0);
     71 			
     72 			assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
     73 			
     74 			// Create item
     75 			var item = new Zotero.Item('book');
     76 			var id = yield item.saveTx();
     77 			
     78 			// New item should be selected
     79 			var selected = itemsView.getSelectedItems();
     80 			assert.lengthOf(selected, 1);
     81 			assert.equal(selected[0].id, id);
     82 			
     83 			// Item should have been selected once
     84 			assert.equal(win.ZoteroPane.itemSelected.callCount, 2);
     85 			assert.ok(win.ZoteroPane.itemSelected.returnValues[1].value());
     86 		});
     87 		
     88 		it("shouldn't select a new item if skipNotifier is passed", function* () {
     89 			// Select existing item
     90 			yield itemsView.selectItem(existingItemID);
     91 			var selected = itemsView.getSelectedItems(true);
     92 			assert.lengthOf(selected, 1);
     93 			assert.equal(selected[0], existingItemID);
     94 			
     95 			// Reset call count on spy
     96 			win.ZoteroPane.itemSelected.reset();
     97 			
     98 			// Create item with skipNotifier flag
     99 			var item = new Zotero.Item('book');
    100 			var id = yield item.saveTx({
    101 				skipNotifier: true
    102 			});
    103 			
    104 			// No select events should have occurred
    105 			assert.equal(win.ZoteroPane.itemSelected.callCount, 0);
    106 			
    107 			// Existing item should still be selected
    108 			selected = itemsView.getSelectedItems(true);
    109 			assert.lengthOf(selected, 1);
    110 			assert.equal(selected[0], existingItemID);
    111 		});
    112 		
    113 		it("shouldn't select a new item if skipSelect is passed", function* () {
    114 			// Select existing item
    115 			yield itemsView.selectItem(existingItemID);
    116 			var selected = itemsView.getSelectedItems(true);
    117 			assert.lengthOf(selected, 1);
    118 			assert.equal(selected[0], existingItemID);
    119 			
    120 			// Reset call count on spy
    121 			win.ZoteroPane.itemSelected.reset();
    122 			
    123 			// Create item with skipSelect flag
    124 			var item = new Zotero.Item('book');
    125 			var id = yield item.saveTx({
    126 				skipSelect: true
    127 			});
    128 			
    129 			// itemSelected should have been called once (from 'selectEventsSuppressed = false'
    130 			// in notify()) as a no-op
    131 			assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
    132 			assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
    133 			
    134 			// Existing item should still be selected
    135 			selected = itemsView.getSelectedItems(true);
    136 			assert.lengthOf(selected, 1);
    137 			assert.equal(selected[0], existingItemID);
    138 		});
    139 		
    140 		it("should clear search and select new item if non-matching quick search is active", async function () {
    141 			await createDataObject('item');
    142 			
    143 			var quicksearch = win.document.getElementById('zotero-tb-search');
    144 			quicksearch.value = Zotero.randomString();
    145 			quicksearch.doCommand();
    146 			await itemsView._refreshPromise;
    147 			
    148 			assert.equal(itemsView.rowCount, 0);
    149 			
    150 			// Create item
    151 			var item = await createDataObject('item');
    152 			
    153 			assert.isAbove(itemsView.rowCount, 0);
    154 			assert.equal(quicksearch.value, '');
    155 			
    156 			// New item should be selected
    157 			var selected = itemsView.getSelectedItems();
    158 			assert.lengthOf(selected, 1);
    159 			assert.equal(selected[0].id, item.id);
    160 		});
    161 		
    162 		it("shouldn't clear quicksearch if skipSelect is passed", function* () {
    163 			var searchString = Zotero.Items.get(existingItemID).getField('title');
    164 			
    165 			yield createDataObject('item');
    166 			
    167 			var quicksearch = win.document.getElementById('zotero-tb-search');
    168 			quicksearch.value = searchString;
    169 			quicksearch.doCommand();
    170 			yield itemsView._refreshPromise;
    171 			
    172 			assert.equal(itemsView.rowCount, 1);
    173 			
    174 			// Create item with skipSelect flag
    175 			var item = new Zotero.Item('book');
    176 			var ran = Zotero.Utilities.randomString();
    177 			item.setField('title', ran);
    178 			var id = yield item.saveTx({
    179 				skipSelect: true
    180 			});
    181 			
    182 			assert.equal(itemsView.rowCount, 1);
    183 			assert.equal(quicksearch.value, searchString);
    184 			
    185 			// Clear search
    186 			quicksearch.value = "";
    187 			quicksearch.doCommand();
    188 			yield itemsView._refreshPromise;
    189 		});
    190 		
    191 		it("shouldn't change selection outside of trash if new trashed item is created with skipSelect", function* () {
    192 			yield selectLibrary(win);
    193 			yield waitForItemsLoad(win);
    194 			
    195 			itemsView.selection.clearSelection();
    196 			
    197 			var item = createUnsavedDataObject('item');
    198 			item.deleted = true;
    199 			var id = yield item.saveTx({
    200 				skipSelect: true
    201 			});
    202 			
    203 			// Nothing should be selected
    204 			var selected = itemsView.getSelectedItems(true);
    205 			assert.lengthOf(selected, 0);
    206 		})
    207 		
    208 		it("shouldn't select a modified item", function* () {
    209 			// Create item
    210 			var item = new Zotero.Item('book');
    211 			var id = yield item.saveTx();
    212 			
    213 			itemsView.selection.clearSelection();
    214 			assert.lengthOf(itemsView.getSelectedItems(), 0);
    215 			// Reset call count on spy
    216 			win.ZoteroPane.itemSelected.reset();
    217 			
    218 			// Modify item
    219 			item.setField('title', 'no select on modify');
    220 			yield item.saveTx();
    221 			
    222 			// itemSelected should have been called once (from 'selectEventsSuppressed = false'
    223 			// in notify()) as a no-op
    224 			assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
    225 			assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
    226 			
    227 			// Modified item should not be selected
    228 			assert.lengthOf(itemsView.getSelectedItems(), 0);
    229 		});
    230 		
    231 		it("should maintain selection on a selected modified item", function* () {
    232 			// Create item
    233 			var item = new Zotero.Item('book');
    234 			var id = yield item.saveTx();
    235 			
    236 			yield itemsView.selectItem(id);
    237 			var selected = itemsView.getSelectedItems(true);
    238 			assert.lengthOf(selected, 1);
    239 			assert.equal(selected[0], id);
    240 			
    241 			// Reset call count on spy
    242 			win.ZoteroPane.itemSelected.reset();
    243 			
    244 			// Modify item
    245 			item.setField('title', 'maintain selection on modify');
    246 			yield item.saveTx();
    247 			
    248 			// itemSelected should have been called once (from 'selectEventsSuppressed = false'
    249 			// in notify()) as a no-op
    250 			assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
    251 			assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
    252 			
    253 			// Modified item should still be selected
    254 			selected = itemsView.getSelectedItems(true);
    255 			assert.lengthOf(selected, 1);
    256 			assert.equal(selected[0], id);
    257 		});
    258 		
    259 		it("should reselect the same row when an item is removed", function* () {
    260 			var collection = yield createDataObject('collection');
    261 			yield waitForItemsLoad(win);
    262 			itemsView = zp.itemsView;
    263 			
    264 			var items = [];
    265 			var num = 6;
    266 			for (let i = 0; i < num; i++) {
    267 				let item = createUnsavedDataObject('item', { title: "" + i });
    268 				item.addToCollection(collection.id);
    269 				yield item.saveTx();
    270 				items.push(item);
    271 			}
    272 			assert.equal(itemsView.rowCount, num);
    273 			
    274 			// Select the third item in the list
    275 			itemsView.selection.select(2);
    276 			
    277 			// Remove item
    278 			var treeRow = itemsView.getRow(2);
    279 			yield Zotero.DB.executeTransaction(function* () {
    280 				yield collection.removeItems([treeRow.ref.id]);
    281 			}.bind(this));
    282 			
    283 			// Selection should stay on third row
    284 			assert.equal(itemsView.selection.currentIndex, 2);
    285 			
    286 			// Delete item
    287 			var treeRow = itemsView.getRow(2);
    288 			yield treeRow.ref.eraseTx();
    289 			
    290 			// Selection should stay on third row
    291 			assert.equal(itemsView.selection.currentIndex, 2);
    292 			
    293 			yield Zotero.Items.erase(items.map(item => item.id));
    294 		});
    295 		
    296 		it("shouldn't select sibling on attachment erase if attachment wasn't selected", function* () {
    297 			var item = yield createDataObject('item');
    298 			var att1 = yield importFileAttachment('test.png', { title: 'A', parentItemID: item.id });
    299 			var att2 = yield importFileAttachment('test.png', { title: 'B', parentItemID: item.id });
    300 			yield zp.itemsView.selectItem(att2.id); // expand
    301 			yield zp.itemsView.selectItem(item.id);
    302 			yield att1.eraseTx();
    303 			assert.sameMembers(zp.itemsView.getSelectedItems(true), [item.id]);
    304 		});
    305 		
    306 		it("should keep first visible item in view when other items are added with skipSelect and nothing in view is selected", function* () {
    307 			var collection = yield createDataObject('collection');
    308 			yield waitForItemsLoad(win);
    309 			itemsView = zp.itemsView;
    310 			
    311 			var treebox = itemsView._treebox;
    312 			var numVisibleRows = treebox.getPageLength();
    313 			
    314 			// Get a numeric string left-padded with zeroes
    315 			function getTitle(i, max) {
    316 				return new String(new Array(max + 1).join(0) + i).slice(-1 * max);
    317 			}
    318 			
    319 			var num = numVisibleRows + 10;
    320 			yield Zotero.DB.executeTransaction(function* () {
    321 				for (let i = 0; i < num; i++) {
    322 					let title = getTitle(i, num);
    323 					let item = createUnsavedDataObject('item', { title });
    324 					item.addToCollection(collection.id);
    325 					yield item.save();
    326 				}
    327 			}.bind(this));
    328 			
    329 			// Scroll halfway
    330 			treebox.scrollToRow(Math.round(num / 2) - Math.round(numVisibleRows / 2));
    331 			
    332 			var firstVisibleItemID = itemsView.getRow(treebox.getFirstVisibleRow()).ref.id;
    333 			
    334 			// Add one item at the beginning
    335 			var item = createUnsavedDataObject(
    336 				'item', { title: getTitle(0, num), collections: [collection.id] }
    337 			);
    338 			yield item.saveTx({
    339 				skipSelect: true
    340 			});
    341 			// Then add a few more in a transaction
    342 			yield Zotero.DB.executeTransaction(function* () {
    343 				for (let i = 0; i < 3; i++) {
    344 					var item = createUnsavedDataObject(
    345 						'item', { title: getTitle(0, num), collections: [collection.id] }
    346 					);
    347 					yield item.save({
    348 						skipSelect: true
    349 					});
    350 				}
    351 			}.bind(this));
    352 			
    353 			// Make sure the same item is still in the first visible row
    354 			assert.equal(itemsView.getRow(treebox.getFirstVisibleRow()).ref.id, firstVisibleItemID);
    355 		});
    356 		
    357 		it("should keep first visible selected item in position when other items are added with skipSelect", function* () {
    358 			var collection = yield createDataObject('collection');
    359 			yield waitForItemsLoad(win);
    360 			itemsView = zp.itemsView;
    361 			
    362 			var treebox = itemsView._treebox;
    363 			var numVisibleRows = treebox.getPageLength();
    364 			
    365 			// Get a numeric string left-padded with zeroes
    366 			function getTitle(i, max) {
    367 				return new String(new Array(max + 1).join(0) + i).slice(-1 * max);
    368 			}
    369 			
    370 			var num = numVisibleRows + 10;
    371 			yield Zotero.DB.executeTransaction(function* () {
    372 				for (let i = 0; i < num; i++) {
    373 					let title = getTitle(i, num);
    374 					let item = createUnsavedDataObject('item', { title });
    375 					item.addToCollection(collection.id);
    376 					yield item.save();
    377 				}
    378 			}.bind(this));
    379 			
    380 			// Scroll halfway
    381 			treebox.scrollToRow(Math.round(num / 2) - Math.round(numVisibleRows / 2));
    382 			
    383 			// Select an item
    384 			itemsView.selection.select(Math.round(num / 2));
    385 			var selectedItem = itemsView.getSelectedItems()[0];
    386 			var offset = itemsView.getRowIndexByID(selectedItem.treeViewID) - treebox.getFirstVisibleRow();
    387 			
    388 			// Add one item at the beginning
    389 			var item = createUnsavedDataObject(
    390 				'item', { title: getTitle(0, num), collections: [collection.id] }
    391 			);
    392 			yield item.saveTx({
    393 				skipSelect: true
    394 			});
    395 			// Then add a few more in a transaction
    396 			yield Zotero.DB.executeTransaction(function* () {
    397 				for (let i = 0; i < 3; i++) {
    398 					var item = createUnsavedDataObject(
    399 						'item', { title: getTitle(0, num), collections: [collection.id] }
    400 					);
    401 					yield item.save({
    402 						skipSelect: true
    403 					});
    404 				}
    405 			}.bind(this));
    406 			
    407 			// Make sure the selected item is still at the same position
    408 			assert.equal(itemsView.getSelectedItems()[0], selectedItem);
    409 			var newOffset = itemsView.getRowIndexByID(selectedItem.treeViewID) - treebox.getFirstVisibleRow();
    410 			assert.equal(newOffset, offset);
    411 		});
    412 		
    413 		it("shouldn't scroll items list if at top when other items are added with skipSelect", function* () {
    414 			var collection = yield createDataObject('collection');
    415 			yield waitForItemsLoad(win);
    416 			itemsView = zp.itemsView;
    417 			
    418 			var treebox = itemsView._treebox;
    419 			var numVisibleRows = treebox.getPageLength();
    420 			
    421 			// Get a numeric string left-padded with zeroes
    422 			function getTitle(i, max) {
    423 				return new String(new Array(max + 1).join(0) + i).slice(-1 * max);
    424 			}
    425 			
    426 			var num = numVisibleRows + 10;
    427 			yield Zotero.DB.executeTransaction(function* () {
    428 				// Start at "*1" so we can add items before
    429 				for (let i = 1; i < num; i++) {
    430 					let title = getTitle(i, num);
    431 					let item = createUnsavedDataObject('item', { title });
    432 					item.addToCollection(collection.id);
    433 					yield item.save();
    434 				}
    435 			}.bind(this));
    436 			
    437 			// Scroll to top
    438 			treebox.scrollToRow(0);
    439 			
    440 			// Add one item at the beginning
    441 			var item = createUnsavedDataObject(
    442 				'item', { title: getTitle(0, num), collections: [collection.id] }
    443 			);
    444 			yield item.saveTx({
    445 				skipSelect: true
    446 			});
    447 			// Then add a few more in a transaction
    448 			yield Zotero.DB.executeTransaction(function* () {
    449 				for (let i = 0; i < 3; i++) {
    450 					var item = createUnsavedDataObject(
    451 						'item', { title: getTitle(0, num), collections: [collection.id] }
    452 					);
    453 					yield item.save({
    454 						skipSelect: true
    455 					});
    456 				}
    457 			}.bind(this));
    458 			
    459 			// Make sure the first row is still at the top
    460 			assert.equal(treebox.getFirstVisibleRow(), 0);
    461 		});
    462 		
    463 		it("should update search results when items are added", function* () {
    464 			var search = yield createDataObject('search');
    465 			var title = search.getConditions()[0].value;
    466 			
    467 			yield waitForItemsLoad(win);
    468 			assert.equal(zp.itemsView.rowCount, 0);
    469 			
    470 			// Add an item matching search
    471 			var item = yield createDataObject('item', { title });
    472 			
    473 			yield waitForItemsLoad(win);
    474 			assert.equal(zp.itemsView.rowCount, 1);
    475 			assert.equal(zp.itemsView.getRowIndexByID(item.id), 0);
    476 		});
    477 		
    478 		it("should re-sort search results when an item is modified", function* () {
    479 			var search = yield createDataObject('search');
    480 			itemsView = zp.itemsView;
    481 			var title = search.getConditions()[0].value;
    482 			
    483 			var item1 = yield createDataObject('item', { title: title + " 1" });
    484 			var item2 = yield createDataObject('item', { title: title + " 3" });
    485 			var item3 = yield createDataObject('item', { title: title + " 5" });
    486 			var item4 = yield createDataObject('item', { title: title + " 7" });
    487 			
    488 			var col = itemsView._treebox.columns.getNamedColumn('zotero-items-column-title');
    489 			col.element.click();
    490 			if (col.element.getAttribute('sortDirection') == 'ascending') {
    491 				col.element.click();
    492 			}
    493 			
    494 			// Check initial sort order
    495 			assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 7");
    496 			assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 1");
    497 			
    498 			// Set first row to title that should be sorted in the middle
    499 			itemsView.getRow(0).ref.setField('title', title + " 4");
    500 			yield itemsView.getRow(0).ref.saveTx();
    501 			
    502 			assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 5");
    503 			assert.equal(itemsView.getRow(1).ref.getField('title'), title + " 4");
    504 			assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 1");
    505 		});
    506 		
    507 		it("should update search results when search conditions are changed", function* () {
    508 			var search = createUnsavedDataObject('search');
    509 			var title1 = Zotero.Utilities.randomString();
    510 			var title2 = Zotero.Utilities.randomString();
    511 			search.fromJSON({
    512 				name: "Test",
    513 				conditions: [
    514 					{
    515 						condition: "title",
    516 						operator: "is",
    517 						value: title1
    518 					}
    519 				]
    520 			});
    521 			yield search.saveTx();
    522 			
    523 			yield waitForItemsLoad(win);
    524 			
    525 			// Add an item that doesn't match search
    526 			var item = yield createDataObject('item', { title: title2 });
    527 			yield waitForItemsLoad(win);
    528 			assert.equal(zp.itemsView.rowCount, 0);
    529 			
    530 			// Modify conditions to match item
    531 			search.removeCondition(0);
    532 			search.addCondition("title", "is", title2);
    533 			yield search.saveTx();
    534 			
    535 			yield waitForItemsLoad(win);
    536 			
    537 			assert.equal(zp.itemsView.rowCount, 1);
    538 		});
    539 		
    540 		it("should remove items from Unfiled Items when added to a collection", function* () {
    541 			var userLibraryID = Zotero.Libraries.userLibraryID;
    542 			var collection = yield createDataObject('collection');
    543 			var item = yield createDataObject('item', { title: "Unfiled Item" });
    544 			yield zp.setVirtual(userLibraryID, 'unfiled', true);
    545 			var selected = yield cv.selectByID("U" + userLibraryID);
    546 			assert.ok(selected);
    547 			yield waitForItemsLoad(win);
    548 			assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
    549 			yield Zotero.DB.executeTransaction(function* () {
    550 				yield collection.addItem(item.id);
    551 			});
    552 			assert.isFalse(zp.itemsView.getRowIndexByID(item.id));
    553 		});
    554 		
    555 		describe("Trash", function () {
    556 			it("should remove untrashed parent item when last trashed child is deleted", function* () {
    557 				var userLibraryID = Zotero.Libraries.userLibraryID;
    558 				var item = yield createDataObject('item');
    559 				var note = yield createDataObject(
    560 					'item', { itemType: 'note', parentID: item.id, deleted: true }
    561 				);
    562 				yield cv.selectByID("T" + userLibraryID);
    563 				yield waitForItemsLoad(win);
    564 				assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
    565 				var promise = waitForDialog();
    566 				yield zp.emptyTrash();
    567 				yield promise;
    568 				assert.isFalse(zp.itemsView.getRowIndexByID(item.id));
    569 			});
    570 		});
    571 		
    572 		describe("My Publications", function () {
    573 			before(function* () {
    574 				var libraryID = Zotero.Libraries.userLibraryID;
    575 				
    576 				var s = new Zotero.Search;
    577 				s.libraryID = libraryID;
    578 				s.addCondition('publications', 'true');
    579 				var ids = yield s.search();
    580 				
    581 				yield Zotero.Items.erase(ids);
    582 				
    583 				yield zp.collectionsView.selectByID("P" + libraryID);
    584 				yield waitForItemsLoad(win);
    585 				
    586 				// Make sure we're showing the intro text
    587 				var deck = win.document.getElementById('zotero-items-pane-content');
    588 				assert.equal(deck.selectedIndex, 1);
    589 			});
    590 			
    591 			it("should replace My Publications intro text with items list on item add", function* () {
    592 				var item = yield createDataObject('item');
    593 				
    594 				yield zp.collectionsView.selectByID("P" + item.libraryID);
    595 				yield waitForItemsLoad(win);
    596 				var iv = zp.itemsView;
    597 				
    598 				item.inPublications = true;
    599 				yield item.saveTx();
    600 				
    601 				var deck = win.document.getElementById('zotero-items-pane-content');
    602 				assert.equal(deck.selectedIndex, 0);
    603 				
    604 				assert.isNumber(iv.getRowIndexByID(item.id));
    605 			});
    606 			
    607 			it("should add new item to My Publications items list", function* () {
    608 				var item1 = createUnsavedDataObject('item');
    609 				item1.inPublications = true;
    610 				yield item1.saveTx();
    611 				
    612 				yield zp.collectionsView.selectByID("P" + item1.libraryID);
    613 				yield waitForItemsLoad(win);
    614 				var iv = zp.itemsView;
    615 				
    616 				var deck = win.document.getElementById('zotero-items-pane-content');
    617 				assert.equal(deck.selectedIndex, 0);
    618 				
    619 				var item2 = createUnsavedDataObject('item');
    620 				item2.inPublications = true;
    621 				yield item2.saveTx();
    622 				
    623 				assert.isNumber(iv.getRowIndexByID(item2.id));
    624 			});
    625 			
    626 			it("should add modified item to My Publications items list", function* () {
    627 				var item1 = createUnsavedDataObject('item');
    628 				item1.inPublications = true;
    629 				yield item1.saveTx();
    630 				var item2 = yield createDataObject('item');
    631 				
    632 				yield zp.collectionsView.selectByID("P" + item1.libraryID);
    633 				yield waitForItemsLoad(win);
    634 				var iv = zp.itemsView;
    635 				
    636 				var deck = win.document.getElementById('zotero-items-pane-content');
    637 				assert.equal(deck.selectedIndex, 0);
    638 				
    639 				assert.isFalse(iv.getRowIndexByID(item2.id));
    640 				
    641 				item2.inPublications = true;
    642 				yield item2.saveTx();
    643 				
    644 				assert.isNumber(iv.getRowIndexByID(item2.id));
    645 			});
    646 			
    647 			it("should show Show/Hide button for imported file attachment", function* () {
    648 				var item = yield createDataObject('item', { inPublications: true });
    649 				var attachment = yield importFileAttachment('test.png', { parentItemID: item.id });
    650 				
    651 				yield zp.collectionsView.selectByID("P" + item.libraryID);
    652 				yield waitForItemsLoad(win);
    653 				var iv = zp.itemsView;
    654 				
    655 				yield iv.selectItem(attachment.id);
    656 				
    657 				var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
    658 				assert.isFalse(box.hidden);
    659 			});
    660 			
    661 			it("shouldn't show Show/Hide button for linked file attachment", function* () {
    662 				var item = yield createDataObject('item', { inPublications: true });
    663 				var attachment = yield Zotero.Attachments.linkFromFile({
    664 					file: OS.Path.join(getTestDataDirectory().path, 'test.png'),
    665 					parentItemID: item.id
    666 				});
    667 				
    668 				yield zp.collectionsView.selectByID("P" + item.libraryID);
    669 				yield waitForItemsLoad(win);
    670 				var iv = zp.itemsView;
    671 				
    672 				yield iv.selectItem(attachment.id);
    673 				
    674 				var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
    675 				assert.isTrue(box.hidden);
    676 			});
    677 		});
    678 	})
    679 	
    680 	
    681 	describe("#drop()", function () {
    682 		var httpd;
    683 		var port = 16213;
    684 		var baseURL = `http://localhost:${port}/`;
    685 		var pdfFilename = "test.pdf";
    686 		var pdfURL = baseURL + pdfFilename;
    687 		var pdfPath;
    688 		
    689 		// Serve a PDF to test URL dragging
    690 		before(function () {
    691 			Components.utils.import("resource://zotero-unit/httpd.js");
    692 			httpd = new HttpServer();
    693 			httpd.start(port);
    694 			var file = getTestDataDirectory();
    695 			file.append(pdfFilename);
    696 			pdfPath = file.path;
    697 			httpd.registerFile("/" + pdfFilename, file);
    698 		});
    699 		
    700 		beforeEach(() => {
    701 			// Don't run recognize on every file
    702 			Zotero.Prefs.set('autoRecognizeFiles', false);
    703 			Zotero.Prefs.clear('autoRenameFiles');
    704 		});
    705 		
    706 		after(function* () {
    707 			var defer = new Zotero.Promise.defer();
    708 			httpd.stop(() => defer.resolve());
    709 			yield defer.promise;
    710 			
    711 			Zotero.Prefs.clear('autoRecognizeFiles');
    712 			Zotero.Prefs.clear('autoRenameFiles');
    713 		});
    714 		
    715 		it("should move a child item from one item to another", function* () {
    716 			var collection = yield createDataObject('collection');
    717 			yield waitForItemsLoad(win);
    718 			var item1 = yield createDataObject('item', { title: "A", collections: [collection.id] });
    719 			var item2 = yield createDataObject('item', { title: "B", collections: [collection.id] });
    720 			var item3 = yield createDataObject('item', { itemType: 'note', parentID: item1.id });
    721 			
    722 			let view = zp.itemsView;
    723 			yield view.selectItem(item3.id, true);
    724 			
    725 			var promise = view.waitForSelect();
    726 			
    727 			view.drop(view.getRowIndexByID(item2.id), 0, {
    728 				dropEffect: 'copy',
    729 				effectAllowed: 'copy',
    730 				types: {
    731 					contains: function (type) {
    732 						return type == 'zotero/item';
    733 					}
    734 				},
    735 				getData: function (type) {
    736 					if (type == 'zotero/item') {
    737 						return item3.id + "";
    738 					}
    739 				},
    740 				mozItemCount: 1
    741 			})
    742 			
    743 			yield promise;
    744 			
    745 			// Old parent should be empty
    746 			assert.isFalse(view.isContainerOpen(view.getRowIndexByID(item1.id)));
    747 			assert.isTrue(view.isContainerEmpty(view.getRowIndexByID(item1.id)));
    748 			
    749 			// New parent should be open
    750 			assert.isTrue(view.isContainerOpen(view.getRowIndexByID(item2.id)));
    751 			assert.isFalse(view.isContainerEmpty(view.getRowIndexByID(item2.id)));
    752 		});
    753 		
    754 		it("should move a child item from last item in list to another", function* () {
    755 			var collection = yield createDataObject('collection');
    756 			yield waitForItemsLoad(win);
    757 			var item1 = yield createDataObject('item', { title: "A", collections: [collection.id] });
    758 			var item2 = yield createDataObject('item', { title: "B", collections: [collection.id] });
    759 			var item3 = yield createDataObject('item', { itemType: 'note', parentID: item2.id });
    760 			
    761 			let view = zp.itemsView;
    762 			yield view.selectItem(item3.id, true);
    763 			
    764 			var promise = view.waitForSelect();
    765 			
    766 			view.drop(view.getRowIndexByID(item1.id), 0, {
    767 				dropEffect: 'copy',
    768 				effectAllowed: 'copy',
    769 				types: {
    770 					contains: function (type) {
    771 						return type == 'zotero/item';
    772 					}
    773 				},
    774 				getData: function (type) {
    775 					if (type == 'zotero/item') {
    776 						return item3.id + "";
    777 					}
    778 				},
    779 				mozItemCount: 1
    780 			})
    781 			
    782 			yield promise;
    783 			
    784 			// Old parent should be empty
    785 			assert.isFalse(view.isContainerOpen(view.getRowIndexByID(item2.id)));
    786 			assert.isTrue(view.isContainerEmpty(view.getRowIndexByID(item2.id)));
    787 			
    788 			// New parent should be open
    789 			assert.isTrue(view.isContainerOpen(view.getRowIndexByID(item1.id)));
    790 			assert.isFalse(view.isContainerEmpty(view.getRowIndexByID(item1.id)));
    791 		});
    792 		
    793 		it("should create a stored top-level attachment when a file is dragged", function* () {
    794 			var file = getTestDataDirectory();
    795 			file.append('test.png');
    796 			
    797 			var promise = itemsView.waitForSelect();
    798 			
    799 			itemsView.drop(0, -1, {
    800 				dropEffect: 'copy',
    801 				effectAllowed: 'copy',
    802 				types: {
    803 					contains: function (type) {
    804 						return type == 'application/x-moz-file';
    805 					}
    806 				},
    807 				mozItemCount: 1,
    808 				mozGetDataAt: function (type, i) {
    809 					if (type == 'application/x-moz-file' && i == 0) {
    810 						return file;
    811 					}
    812 				}
    813 			})
    814 			
    815 			yield promise;
    816 			var items = itemsView.getSelectedItems();
    817 			var path = yield items[0].getFilePathAsync();
    818 			assert.equal(
    819 				(yield Zotero.File.getBinaryContentsAsync(path)),
    820 				(yield Zotero.File.getBinaryContentsAsync(file))
    821 			);
    822 		});
    823 		
    824 		it("should create a stored top-level attachment when a URL is dragged", function* () {
    825 			var promise = itemsView.waitForSelect();
    826 			
    827 			itemsView.drop(0, -1, {
    828 				dropEffect: 'copy',
    829 				effectAllowed: 'copy',
    830 				types: {
    831 					contains: function (type) {
    832 						return type == 'text/x-moz-url';
    833 					}
    834 				},
    835 				getData: function (type) {
    836 					if (type == 'text/x-moz-url') {
    837 						return pdfURL;
    838 					}
    839 				},
    840 				mozItemCount: 1,
    841 			})
    842 			
    843 			yield promise;
    844 			var item = itemsView.getSelectedItems()[0];
    845 			assert.equal(item.getField('url'), pdfURL);
    846 			assert.equal(
    847 				(yield Zotero.File.getBinaryContentsAsync(yield item.getFilePathAsync())),
    848 				(yield Zotero.File.getBinaryContentsAsync(pdfPath))
    849 			);
    850 		});
    851 		
    852 		it("should create a stored child attachment when a URL is dragged", function* () {
    853 			var view = zp.itemsView;
    854 			var parentItem = yield createDataObject('item');
    855 			var parentRow = view.getRowIndexByID(parentItem.id);
    856 			
    857 			var promise = waitForItemEvent('add');
    858 			
    859 			itemsView.drop(parentRow, 0, {
    860 				dropEffect: 'copy',
    861 				effectAllowed: 'copy',
    862 				types: {
    863 					contains: function (type) {
    864 						return type == 'text/x-moz-url';
    865 					}
    866 				},
    867 				getData: function (type) {
    868 					if (type == 'text/x-moz-url') {
    869 						return pdfURL;
    870 					}
    871 				},
    872 				mozItemCount: 1,
    873 			})
    874 			
    875 			var itemIDs = yield promise;
    876 			var item = Zotero.Items.get(itemIDs[0]);
    877 			assert.equal(item.parentItemID, parentItem.id);
    878 			assert.equal(item.getField('url'), pdfURL);
    879 			assert.equal(
    880 				(yield Zotero.File.getBinaryContentsAsync(yield item.getFilePathAsync())),
    881 				(yield Zotero.File.getBinaryContentsAsync(pdfPath))
    882 			);
    883 		});
    884 		
    885 		it("should automatically retrieve metadata for top-level PDF if pref is enabled", async function () {
    886 			Zotero.Prefs.set('autoRecognizeFiles', true);
    887 			
    888 			var view = zp.itemsView;
    889 			
    890 			var promise = waitForItemEvent('add');
    891 			var recognizerPromise = waitForRecognizer();
    892 			
    893 			// Fake recognizer response
    894 			Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
    895 			var server = sinon.fakeServer.create();
    896 			server.autoRespond = true;
    897 			setHTTPResponse(
    898 				server,
    899 				ZOTERO_CONFIG.RECOGNIZE_URL,
    900 				{
    901 					method: 'POST',
    902 					url: 'recognize',
    903 					status: 200,
    904 					headers: {
    905 						'Content-Type': 'application/json'
    906 					},
    907 					json: {
    908 						title: 'Test',
    909 						authors: []
    910 					}
    911 				}
    912 			);
    913 			
    914 			itemsView.drop(0, -1, {
    915 				dropEffect: 'copy',
    916 				effectAllowed: 'copy',
    917 				types: {
    918 					contains: function (type) {
    919 						return type == 'text/x-moz-url';
    920 					}
    921 				},
    922 				getData: function (type) {
    923 					if (type == 'text/x-moz-url') {
    924 						return pdfURL;
    925 					}
    926 				},
    927 				mozItemCount: 1,
    928 			})
    929 			
    930 			var itemIDs = await promise;
    931 			var item = Zotero.Items.get(itemIDs[0]);
    932 			
    933 			var progressWindow = await recognizerPromise;
    934 			progressWindow.close();
    935 			Zotero.RecognizePDF.cancel();
    936 			assert.isFalse(item.isTopLevelItem());
    937 			
    938 			Zotero.HTTP.mock = null;
    939 		});
    940 		
    941 		it("should rename a stored child attachment using parent metadata if no existing file attachments and pref enabled", async function () {
    942 			var view = zp.itemsView;
    943 			var parentTitle = Zotero.Utilities.randomString();
    944 			var parentItem = await createDataObject('item', { title: parentTitle });
    945 			await Zotero.Attachments.linkFromURL({
    946 				url: 'https://example.com',
    947 				title: 'Example',
    948 				parentItemID: parentItem.id
    949 			});
    950 			var parentRow = view.getRowIndexByID(parentItem.id);
    951 			
    952 			var file = getTestDataDirectory();
    953 			file.append('empty.pdf');
    954 			
    955 			var promise = waitForItemEvent('add');
    956 			
    957 			itemsView.drop(parentRow, 0, {
    958 				dropEffect: 'copy',
    959 				effectAllowed: 'copy',
    960 				types: {
    961 					contains: function (type) {
    962 						return type == 'application/x-moz-file';
    963 					}
    964 				},
    965 				mozItemCount: 1,
    966 				mozGetDataAt: function (type, i) {
    967 					if (type == 'application/x-moz-file' && i == 0) {
    968 						return file;
    969 					}
    970 				}
    971 			})
    972 			
    973 			var itemIDs = await promise;
    974 			var item = Zotero.Items.get(itemIDs[0]);
    975 			assert.equal(item.parentItemID, parentItem.id);
    976 			var title = item.getField('title');
    977 			var path = await item.getFilePathAsync();
    978 			assert.equal(title, parentTitle + '.pdf');
    979 			assert.equal(OS.Path.basename(path), parentTitle + '.pdf');
    980 		});
    981 		
    982 		it("should rename a linked child attachment using parent metadata if no existing file attachments and pref enabled", async function () {
    983 			var view = zp.itemsView;
    984 			var parentTitle = Zotero.Utilities.randomString();
    985 			var parentItem = await createDataObject('item', { title: parentTitle });
    986 			await Zotero.Attachments.linkFromURL({
    987 				url: 'https://example.com',
    988 				title: 'Example',
    989 				parentItemID: parentItem.id
    990 			});
    991 			var parentRow = view.getRowIndexByID(parentItem.id);
    992 			
    993 			var file = OS.Path.join(await getTempDirectory(), 'empty.pdf');
    994 			await OS.File.copy(
    995 				OS.Path.join(getTestDataDirectory().path, 'empty.pdf'),
    996 				file
    997 			);
    998 			file = Zotero.File.pathToFile(file);
    999 			
   1000 			var promise = waitForItemEvent('add');
   1001 			
   1002 			itemsView.drop(parentRow, 0, {
   1003 				dropEffect: 'link',
   1004 				effectAllowed: 'link',
   1005 				types: {
   1006 					contains: function (type) {
   1007 						return type == 'application/x-moz-file';
   1008 					}
   1009 				},
   1010 				mozItemCount: 1,
   1011 				mozGetDataAt: function (type, i) {
   1012 					if (type == 'application/x-moz-file' && i == 0) {
   1013 						return file;
   1014 					}
   1015 				}
   1016 			})
   1017 			
   1018 			var itemIDs = await promise;
   1019 			var item = Zotero.Items.get(itemIDs[0]);
   1020 			assert.equal(item.parentItemID, parentItem.id);
   1021 			var title = item.getField('title');
   1022 			var path = await item.getFilePathAsync();
   1023 			assert.equal(title, parentTitle + '.pdf');
   1024 			assert.equal(OS.Path.basename(path), parentTitle + '.pdf');
   1025 		});
   1026 		
   1027 		it("shouldn't rename a stored child attachment using parent metadata if pref disabled", async function () {
   1028 			Zotero.Prefs.set('autoRenameFiles', false);
   1029 			
   1030 			var view = zp.itemsView;
   1031 			var parentTitle = Zotero.Utilities.randomString();
   1032 			var parentItem = await createDataObject('item', { title: parentTitle });
   1033 			await Zotero.Attachments.linkFromURL({
   1034 				url: 'https://example.com',
   1035 				title: 'Example',
   1036 				parentItemID: parentItem.id
   1037 			});
   1038 			var parentRow = view.getRowIndexByID(parentItem.id);
   1039 			
   1040 			var originalFileName = 'empty.pdf';
   1041 			var file = getTestDataDirectory();
   1042 			file.append(originalFileName);
   1043 			
   1044 			var promise = waitForItemEvent('add');
   1045 			
   1046 			itemsView.drop(parentRow, 0, {
   1047 				dropEffect: 'copy',
   1048 				effectAllowed: 'copy',
   1049 				types: {
   1050 					contains: function (type) {
   1051 						return type == 'application/x-moz-file';
   1052 					}
   1053 				},
   1054 				mozItemCount: 1,
   1055 				mozGetDataAt: function (type, i) {
   1056 					if (type == 'application/x-moz-file' && i == 0) {
   1057 						return file;
   1058 					}
   1059 				}
   1060 			})
   1061 			
   1062 			var itemIDs = await promise;
   1063 			var item = Zotero.Items.get(itemIDs[0]);
   1064 			assert.equal(item.parentItemID, parentItem.id);
   1065 			var title = item.getField('title');
   1066 			var path = await item.getFilePathAsync();
   1067 			// Should match original filename, not parent title
   1068 			assert.equal(title, originalFileName);
   1069 			assert.equal(OS.Path.basename(path), originalFileName);
   1070 		});
   1071 		
   1072 		it("shouldn't rename a stored child attachment using parent metadata if existing file attachments", async function () {
   1073 			var view = zp.itemsView;
   1074 			var parentTitle = Zotero.Utilities.randomString();
   1075 			var parentItem = await createDataObject('item', { title: parentTitle });
   1076 			await Zotero.Attachments.linkFromFile({
   1077 				file: OS.Path.join(getTestDataDirectory().path, 'test.png'),
   1078 				parentItemID: parentItem.id
   1079 			});
   1080 			var parentRow = view.getRowIndexByID(parentItem.id);
   1081 			
   1082 			var originalFileName = 'empty.pdf';
   1083 			var file = getTestDataDirectory();
   1084 			file.append(originalFileName);
   1085 			
   1086 			var promise = waitForItemEvent('add');
   1087 			
   1088 			itemsView.drop(parentRow, 0, {
   1089 				dropEffect: 'copy',
   1090 				effectAllowed: 'copy',
   1091 				types: {
   1092 					contains: function (type) {
   1093 						return type == 'application/x-moz-file';
   1094 					}
   1095 				},
   1096 				mozItemCount: 1,
   1097 				mozGetDataAt: function (type, i) {
   1098 					if (type == 'application/x-moz-file' && i == 0) {
   1099 						return file;
   1100 					}
   1101 				}
   1102 			})
   1103 			
   1104 			var itemIDs = await promise;
   1105 			var item = Zotero.Items.get(itemIDs[0]);
   1106 			assert.equal(item.parentItemID, parentItem.id);
   1107 			var title = item.getField('title');
   1108 			var path = await item.getFilePathAsync();
   1109 			assert.equal(title, originalFileName);
   1110 			assert.equal(OS.Path.basename(path), originalFileName);
   1111 		});
   1112 		
   1113 		it("shouldn't rename a stored child attachment using parent metadata if drag includes multiple files", async function () {
   1114 			var view = zp.itemsView;
   1115 			var parentTitle = Zotero.Utilities.randomString();
   1116 			var parentItem = await createDataObject('item', { title: parentTitle });
   1117 			var parentRow = view.getRowIndexByID(parentItem.id);
   1118 			
   1119 			var originalFileName = 'empty.pdf';
   1120 			var file = getTestDataDirectory();
   1121 			file.append(originalFileName);
   1122 			
   1123 			var promise = waitForItemEvent('add');
   1124 			
   1125 			itemsView.drop(parentRow, 0, {
   1126 				dropEffect: 'copy',
   1127 				effectAllowed: 'copy',
   1128 				types: {
   1129 					contains: function (type) {
   1130 						return type == 'application/x-moz-file';
   1131 					}
   1132 				},
   1133 				mozItemCount: 2,
   1134 				mozGetDataAt: function (type, i) {
   1135 					if (type == 'application/x-moz-file' && i <= 1) {
   1136 						return file;
   1137 					}
   1138 				}
   1139 			})
   1140 			
   1141 			var itemIDs = await promise;
   1142 			var item = Zotero.Items.get(itemIDs[0]);
   1143 			assert.equal(item.parentItemID, parentItem.id);
   1144 			var title = item.getField('title');
   1145 			var path = await item.getFilePathAsync();
   1146 			assert.equal(title, originalFileName);
   1147 			assert.equal(OS.Path.basename(path), originalFileName);
   1148 		});
   1149 	});
   1150 })