mirror of
https://github.com/zotero/zotero.git
synced 2026-10-01 02:01:24 +00:00
Since 56eb77b704, drags of file attachments allow only 'copy' so that
File Explorer doesn't move the file out of storage, but the trees set
dropEffect to 'move' in onDragOver() for drops within Zotero, and OLE
refuses a drop whose dropEffect isn't among the drag's allowed effects.
Have setDropEffect() fall back to an allowed effect and have onDrop()
act on the effect the tree chose, kept in
Zotero.DragDrop.currentDropEffect, rather than on the drop event's
dropEffect.
https://forums.zotero.org/discussion/133765/
2276 lines
86 KiB
JavaScript
2276 lines
86 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.CollectionTree", function () {
|
|
var win, zp, cv, userLibraryID;
|
|
|
|
before(function* () {
|
|
win = yield loadZoteroPane();
|
|
zp = win.ZoteroPane;
|
|
cv = zp.collectionsView;
|
|
userLibraryID = Zotero.Libraries.userLibraryID;
|
|
});
|
|
beforeEach(function () {
|
|
// TODO: Add a selectCollection() function and select a collection instead?
|
|
return selectLibrary(win);
|
|
})
|
|
after(function () {
|
|
win.close();
|
|
});
|
|
|
|
describe("#refresh()", function () {
|
|
it("should show Duplicate Items and Unfiled Items by default and shouldn't show Retracted Items", async function () {
|
|
Zotero.Prefs.clear('duplicateLibraries');
|
|
Zotero.Prefs.clear('unfiledLibraries');
|
|
Zotero.Prefs.clear('retractedLibraries');
|
|
await cv.refresh();
|
|
assert.ok(cv.getRowIndexByID("D" + userLibraryID));
|
|
assert.ok(cv.getRowIndexByID("U" + userLibraryID));
|
|
assert.isFalse(cv.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
|
|
it("shouldn't show virtual collections if hidden", async function () {
|
|
Zotero.Prefs.set('duplicateLibraries', `{"${userLibraryID}": false}`);
|
|
Zotero.Prefs.set('unfiledLibraries', `{"${userLibraryID}": false}`);
|
|
Zotero.Prefs.set('retractedLibraries', `{"${userLibraryID}": false}`);
|
|
await cv.refresh();
|
|
assert.isFalse(cv.getRowIndexByID("D" + userLibraryID));
|
|
assert.isFalse(cv.getRowIndexByID("U" + userLibraryID));
|
|
assert.isFalse(cv.getRowIndexByID("R" + userLibraryID));
|
|
});
|
|
|
|
it("should maintain open state of group", async function () {
|
|
var group1 = await createGroup();
|
|
var group2 = await createGroup();
|
|
var group1Row = cv.getRowIndexByID(group1.treeViewID);
|
|
var group2Row = cv.getRowIndexByID(group2.treeViewID);
|
|
|
|
// Open group 1 and close group 2
|
|
if (!cv.isContainerOpen(group1Row)) {
|
|
await cv.toggleOpenState(group1Row);
|
|
}
|
|
if (cv.isContainerOpen(group2Row)) {
|
|
await cv.toggleOpenState(group2Row);
|
|
}
|
|
|
|
cv._saveOpenStates();
|
|
// #_saveOpenStates is debounced
|
|
await Zotero.Promise.delay(500);
|
|
|
|
group1Row = cv.getRowIndexByID(group1.treeViewID);
|
|
group2Row = cv.getRowIndexByID(group2.treeViewID);
|
|
|
|
await cv.refresh();
|
|
|
|
// Group rows shouldn't have changed
|
|
assert.equal(cv.getRowIndexByID(group1.treeViewID), group1Row);
|
|
assert.equal(cv.getRowIndexByID(group2.treeViewID), group2Row);
|
|
// Group open states shouldn't have changed
|
|
assert.isTrue(cv.isContainerOpen(group1Row));
|
|
assert.isFalse(cv.isContainerOpen(group2Row));
|
|
});
|
|
|
|
it("should update associated item tree view", async function () {
|
|
var collection = await createDataObject('collection');
|
|
var item = await createDataObject('item', { collections: [collection.id] });
|
|
await cv.reload();
|
|
await cv.selectCollection(collection.id);
|
|
await cv.selectItem(item.id);
|
|
});
|
|
});
|
|
|
|
describe("collapse/expand", function () {
|
|
it("should close and open My Library repeatedly", async function () {
|
|
await cv.selectLibrary(userLibraryID);
|
|
var row = cv.selection.focused;
|
|
|
|
cv.collapseLibrary(userLibraryID);
|
|
assert.equal(cv.selection.focused, row);
|
|
assert.isFalse(cv.isContainerOpen(row));
|
|
|
|
await cv.expandLibrary(userLibraryID);
|
|
assert.equal(cv.selection.focused, row);
|
|
assert.ok(cv.isContainerOpen(row));
|
|
|
|
cv.collapseLibrary(userLibraryID);
|
|
assert.equal(cv.selection.focused, row);
|
|
assert.isFalse(cv.isContainerOpen(row));
|
|
|
|
await cv.expandLibrary(userLibraryID);
|
|
assert.equal(cv.selection.focused, row);
|
|
assert.ok(cv.isContainerOpen(row));
|
|
})
|
|
|
|
it("should expand/collapse selected containers when focus is on a multi-selection", async function () {
|
|
let collection1 = await createDataObject('collection');
|
|
let collection2 = await createDataObject('collection');
|
|
await createDataObject('collection', { parentID: collection1.id });
|
|
await createDataObject('collection', { parentID: collection2.id });
|
|
let collection1Row = cv.getRowIndexByID(collection1.treeViewID);
|
|
let collection2Row = cv.getRowIndexByID(collection2.treeViewID);
|
|
if (cv.isContainerOpen(collection1Row)) {
|
|
await cv.toggleOpenState(collection1Row);
|
|
}
|
|
collection2Row = cv.getRowIndexByID(collection2.treeViewID);
|
|
if (cv.isContainerOpen(collection2Row)) {
|
|
await cv.toggleOpenState(collection2Row);
|
|
}
|
|
|
|
cv.selection.select(cv.getRowIndexByID(collection1.treeViewID));
|
|
cv.selection.toggleSelect(cv.getRowIndexByID(collection2.treeViewID));
|
|
assert.equal(cv.selection.count, 2);
|
|
assert.isTrue(cv.selection.isSelected(cv.selection.focused));
|
|
let selectEventCount = 0;
|
|
let selectListener = () => selectEventCount++;
|
|
cv.onSelect.addListener(selectListener);
|
|
cv.tree._onKeyDown({
|
|
key: Zotero.arrowNextKey,
|
|
preventDefault: () => {},
|
|
stopPropagation: () => {}
|
|
});
|
|
await waitForCallback(() => selectEventCount
|
|
&& cv.isContainerOpen(cv.getRowIndexByID(collection1.treeViewID))
|
|
&& cv.isContainerOpen(cv.getRowIndexByID(collection2.treeViewID)));
|
|
assert.sameMembers(cv.getSelectedRows().map(row => row.id), [collection1.treeViewID, collection2.treeViewID]);
|
|
|
|
selectEventCount = 0;
|
|
cv.tree._onKeyDown({
|
|
key: Zotero.arrowPreviousKey,
|
|
preventDefault: () => {},
|
|
stopPropagation: () => {}
|
|
});
|
|
await waitForCallback(() => selectEventCount
|
|
&& !cv.isContainerOpen(cv.getRowIndexByID(collection1.treeViewID))
|
|
&& !cv.isContainerOpen(cv.getRowIndexByID(collection2.treeViewID)));
|
|
assert.sameMembers(cv.getSelectedRows().map(row => row.id), [collection1.treeViewID, collection2.treeViewID]);
|
|
cv.onSelect.removeListener(selectListener);
|
|
})
|
|
|
|
it("should preserve selection when focus is on a collapsed/expanded row", async function () {
|
|
let group = await createGroup();
|
|
let collection = await createDataObject('collection', { libraryID: group.libraryID });
|
|
let feed = await createFeed();
|
|
let groupRow = cv.getRowIndexByID(group.treeViewID);
|
|
if (!cv.isContainerOpen(groupRow)) {
|
|
await cv.toggleOpenState(groupRow);
|
|
}
|
|
await cv.selectByID(feed.treeViewID);
|
|
cv.selection.focused = groupRow;
|
|
cv.selection.pivot = groupRow;
|
|
|
|
await cv.toggleOpenState(groupRow);
|
|
assert.equal(cv.getSelectedRows()[0].id, feed.treeViewID);
|
|
|
|
groupRow = cv.getRowIndexByID(group.treeViewID);
|
|
cv.selection.focused = groupRow;
|
|
cv.selection.pivot = groupRow;
|
|
await cv.toggleOpenState(groupRow);
|
|
assert.equal(cv.getSelectedRows()[0].id, feed.treeViewID);
|
|
|
|
await cv.selectByID(collection.treeViewID);
|
|
groupRow = cv.getRowIndexByID(group.treeViewID);
|
|
cv.selection.focused = groupRow;
|
|
cv.selection.pivot = groupRow;
|
|
await cv.toggleOpenState(groupRow);
|
|
assert.equal(cv.getSelectedRows()[0].id, group.treeViewID);
|
|
})
|
|
})
|
|
|
|
describe("#expandLibrary()", function () {
|
|
var libraryRow, col1, col2, col3;
|
|
|
|
before(function* () {
|
|
yield cv.selectLibrary(userLibraryID);
|
|
libraryRow = cv.selection.focused;
|
|
});
|
|
|
|
beforeEach(function* () {
|
|
// My Library
|
|
// - A
|
|
// - B
|
|
// - C
|
|
col1 = yield createDataObject('collection');
|
|
col2 = yield createDataObject('collection', { parentID: col1.id });
|
|
col3 = yield createDataObject('collection', { parentID: col2.id });
|
|
yield select(win, col3);
|
|
});
|
|
|
|
it("should open a library and respect stored container state", async function () {
|
|
// Collapse B
|
|
await cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
|
|
cv._saveOpenStates();
|
|
// #_saveOpenStates is debounced
|
|
await Zotero.Promise.delay(500);
|
|
|
|
// Close and reopen library
|
|
await cv.toggleOpenState(libraryRow);
|
|
await cv.expandLibrary(userLibraryID);
|
|
|
|
assert.isTrue(cv.isContainerOpen(libraryRow));
|
|
assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col1.treeViewID)));
|
|
assert.isFalse(cv.isContainerOpen(cv.getRowIndexByID(col2.treeViewID)));
|
|
});
|
|
|
|
it("should open a library and all subcollections in recursive mode", async function () {
|
|
await cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
|
|
cv._saveOpenStates();
|
|
// #_saveOpenStates is debounced
|
|
await Zotero.Promise.delay(500);
|
|
|
|
// Close and reopen library
|
|
await cv.toggleOpenState(libraryRow);
|
|
await cv.expandLibrary(userLibraryID, true);
|
|
|
|
assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col1.treeViewID)));
|
|
assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col2.treeViewID)));
|
|
});
|
|
|
|
it("should open a group and show top-level collections", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var col1 = await createDataObject('collection', { libraryID });
|
|
var col2 = await createDataObject('collection', { libraryID });
|
|
var col3 = await createDataObject('collection', { libraryID });
|
|
var col4 = await createDataObject('collection', { libraryID, parentID: col1.id });
|
|
var col5 = await createDataObject('collection', { libraryID, parentID: col4.id });
|
|
|
|
// Close group
|
|
cv.toggleOpenState(cv.getRowIndexByID(group.treeViewID), false);
|
|
|
|
await cv.expandLibrary(libraryID);
|
|
|
|
assert.isNumber(cv.getRowIndexByID(col1.treeViewID));
|
|
assert.isNumber(cv.getRowIndexByID(col2.treeViewID));
|
|
assert.isNumber(cv.getRowIndexByID(col3.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(col4.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(col5.treeViewID));
|
|
});
|
|
});
|
|
|
|
describe("#expandToCollection()", function () {
|
|
it("should expand a collection to a subcollection", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
var row = cv.getRowIndexByID("C" + collection1.id);
|
|
assert.isFalse(cv.isContainerOpen(row));
|
|
|
|
await cv.expandToCollection(collection2.id);
|
|
cv.forceUpdate();
|
|
|
|
// Make sure parent row position hasn't changed
|
|
assert.equal(cv.getRowIndexByID("C" + collection1.id), row);
|
|
// Parent should have been opened
|
|
assert.isTrue(cv.isContainerOpen(row));
|
|
})
|
|
})
|
|
|
|
describe("#selectByID()", function () {
|
|
it("should select the trash", async function () {
|
|
await cv.selectByID("T1");
|
|
var row = cv.selection.focused;
|
|
var treeRow = cv.getRow(row);
|
|
assert.ok(treeRow.isTrash());
|
|
assert.equal(treeRow.ref.libraryID, userLibraryID);
|
|
})
|
|
})
|
|
|
|
describe("#selectWait()", function () {
|
|
it("shouldn't hang if row is already selected", async function () {
|
|
var row = cv.getRowIndexByID("T" + userLibraryID);
|
|
await cv.selectWait(row);
|
|
await Zotero.Promise.delay(50);
|
|
await cv.selectWait(row);
|
|
})
|
|
})
|
|
|
|
describe("Trash for collections/searches", function () {
|
|
for (let objectType of ['collection', 'search']) {
|
|
it(`should remove deleted ${objectType} from collectionTree`, async function () {
|
|
var o1 = await createDataObject(objectType);
|
|
var o2 = await createDataObject(objectType);
|
|
var o3 = await createDataObject(objectType);
|
|
|
|
// Move them to trash
|
|
o1.deleted = true;
|
|
o2.deleted = true;
|
|
o3.deleted = true;
|
|
await o1.saveTx();
|
|
await o2.saveTx();
|
|
await o3.saveTx();
|
|
|
|
// Make sure they're gone from collectionTree
|
|
assert.isFalse(cv.getRowIndexByID(o1.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(o2.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(o3.treeViewID));
|
|
});
|
|
|
|
it(`should put restored ${objectType} back into collectionTree`, async function () {
|
|
var o1 = await createDataObject(objectType, { deleted: true });
|
|
var o2 = await createDataObject(objectType, { deleted: true });
|
|
var o3 = await createDataObject(objectType, { deleted: true });
|
|
|
|
await selectTrash(win);
|
|
|
|
// Restore
|
|
await Zotero.DB.executeTransaction(async function () {
|
|
o1.deleted = false;
|
|
o2.deleted = false;
|
|
o3.deleted = false;
|
|
await o1.save();
|
|
await o2.save();
|
|
await o3.save();
|
|
});
|
|
|
|
// Check if trash is still selected
|
|
let trashRow = cv.getRowIndexByID("T" + userLibraryID);
|
|
assert.equal(cv.selection.focused, trashRow);
|
|
|
|
// Check if restored entries are back in collectionTree
|
|
assert.isNumber(cv.getRowIndexByID(o1.treeViewID));
|
|
assert.isNumber(cv.getRowIndexByID(o2.treeViewID));
|
|
assert.isNumber(cv.getRowIndexByID(o3.treeViewID));
|
|
// Make sure it's all gone from trash
|
|
assert.isFalse(zp.itemsView.getRowIndexByID(o1.treeViewID));
|
|
assert.isFalse(zp.itemsView.getRowIndexByID(o2.treeViewID));
|
|
assert.isFalse(zp.itemsView.getRowIndexByID(o3.treeViewID));
|
|
});
|
|
}
|
|
|
|
it("should delete subcollections when parent is deleted", async function () {
|
|
var c1 = await createDataObject('collection');
|
|
var c2 = await createDataObject('collection', { parentID: c1.id });
|
|
var c3 = await createDataObject('collection', { parentID: c2.id });
|
|
|
|
// Expand to bottom
|
|
await select(win, c3);
|
|
// Select top parent
|
|
await select(win, c1);
|
|
// Move parent to trash
|
|
await cv.deleteSelection();
|
|
|
|
// Make sure they're gone from collectionTree
|
|
assert.isFalse(cv.getRowIndexByID(c1.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(c2.treeViewID));
|
|
assert.isFalse(cv.getRowIndexByID(c3.treeViewID));
|
|
})
|
|
|
|
it("should restore deleted subcollections with parent", async function () {
|
|
var c1 = await createDataObject('collection', { deleted: true });
|
|
var c2 = await createDataObject('collection', { parentID: c1.id });
|
|
var c3 = await createDataObject('collection', { parentID: c2.id });
|
|
|
|
await selectTrash(win);
|
|
|
|
// Restore items
|
|
zp.itemsView.selectItem(c1.treeViewID);
|
|
await zp.restoreSelectedItems();
|
|
|
|
// Check if trash is still selected
|
|
let trashRow = cv.getRowIndexByID("T" + userLibraryID);
|
|
assert.equal(cv.selection.focused, trashRow);
|
|
|
|
// Check if restored collections are back in collectionTree
|
|
let parentRowIndex = cv.getRowIndexByID(c1.treeViewID);
|
|
await cv.toggleOpenState(parentRowIndex);
|
|
assert.equal(cv.getRow(parentRowIndex).level, 1);
|
|
|
|
let middleRowIndex = cv.getRowIndexByID(c2.treeViewID);
|
|
await cv.toggleOpenState(middleRowIndex);
|
|
assert.equal(cv.getRow(middleRowIndex).level, 2);
|
|
|
|
let bottomRowIndex = cv.getRowIndexByID(c3.treeViewID);
|
|
assert.equal(cv.getRow(bottomRowIndex).level, 3);
|
|
|
|
// Make sure they're gone from trash
|
|
assert.isFalse(zp.itemsView.getRowIndexByID(c1.treeViewID));
|
|
});
|
|
});
|
|
|
|
describe("#notify()", function () {
|
|
it("should select a new collection", async function () {
|
|
// Create collection
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "Select new collection";
|
|
var id = await collection.saveTx();
|
|
|
|
// New collection should be selected
|
|
var selected = cv.getSelectedCollections(true)[0];
|
|
assert.equal(selected, id);
|
|
});
|
|
|
|
it("shouldn't select a new collection if skipNotifier is passed", async function () {
|
|
// Create collection with skipNotifier flag
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "No select on skipNotifier";
|
|
var id = await collection.saveTx({
|
|
skipNotifier: true
|
|
});
|
|
|
|
// Library should still be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], userLibraryID);
|
|
});
|
|
|
|
it("shouldn't select a new collection if skipSelect is passed", async function () {
|
|
// Create collection with skipSelect flag
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "No select on skipSelect";
|
|
var id = await collection.saveTx({
|
|
skipSelect: true
|
|
});
|
|
|
|
// Library should still be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], userLibraryID);
|
|
});
|
|
|
|
it("shouldn't select a modified collection", async function () {
|
|
// Create collection
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "No select on modify";
|
|
var id = await collection.saveTx();
|
|
|
|
await selectLibrary(win);
|
|
|
|
collection.name = "No select on modify 2";
|
|
await collection.saveTx();
|
|
|
|
// Modified collection should not be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], userLibraryID);
|
|
});
|
|
|
|
it("should maintain selection on a selected modified collection", async function () {
|
|
// Create collection
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "Reselect on modify";
|
|
var id = await collection.saveTx();
|
|
|
|
var selected = cv.getSelectedCollections(true)[0];
|
|
assert.equal(selected, id);
|
|
|
|
collection.name = "Reselect on modify 2";
|
|
await collection.saveTx();
|
|
|
|
// Modified collection should still be selected
|
|
selected = cv.getSelectedCollections(true)[0];
|
|
assert.equal(selected, id);
|
|
});
|
|
|
|
describe(".deleted selection", function () {
|
|
for (let objectType of ['collection', 'search']) {
|
|
it(`should select next row when ${objectType} is moved to trash`, async function () {
|
|
var ran = Zotero.Utilities.randomString();
|
|
var o1 = await createDataObject(objectType, { name: ran + "AAA" });
|
|
var o2 = await createDataObject(objectType, { name: ran + "BBB" });
|
|
var o3 = await createDataObject(objectType, { name: ran + "CCC" });
|
|
|
|
await cv.selectByID(o2.treeViewID);
|
|
|
|
o2.deleted = true;
|
|
await o2.saveTx();
|
|
|
|
assert.equal(zp.getCollectionTreeRows()[0].ref.id, o3.id);
|
|
});
|
|
|
|
it(`should maintain selection on ${objectType} when row above is moved to trash`, async function () {
|
|
var ran = Zotero.Utilities.randomString();
|
|
var o1 = await createDataObject(objectType, { name: ran + "AAA" });
|
|
var o2 = await createDataObject(objectType, { name: ran + "BBB" });
|
|
var o3 = await createDataObject(objectType, { name: ran + "CCC" });
|
|
|
|
await cv.selectByID(o3.treeViewID);
|
|
assert.equal(zp.getCollectionTreeRows()[0].ref.id, o3.id);
|
|
|
|
o1.deleted = true;
|
|
await o1.saveTx();
|
|
|
|
assert.equal(zp.getCollectionTreeRows()[0].ref.id, o3.id);
|
|
});
|
|
|
|
it(`should maintain selection on trash when ${objectType} is restored`, async function () {
|
|
var o = await createDataObject(objectType, { deleted: true });
|
|
|
|
await cv.selectByID("T1");
|
|
|
|
o.deleted = false;
|
|
await o.saveTx();
|
|
|
|
assert.isTrue(zp.getCollectionTreeRows()[0].isTrash());
|
|
|
|
// Row should have been added back
|
|
assert.isAbove(cv.getRowIndexByID(o.treeViewID), 0);
|
|
});
|
|
}
|
|
|
|
it("should drop a collection from a multi-selection when it's moved to trash", async function () {
|
|
var ran = Zotero.Utilities.randomString();
|
|
var o1 = await createDataObject('collection', { name: ran + "AAA" });
|
|
var o2 = await createDataObject('collection', { name: ran + "BBB" });
|
|
var o3 = await createDataObject('collection', { name: ran + "CCC" });
|
|
|
|
await cv.selectByID(o1.treeViewID);
|
|
cv.selection.toggleSelect(cv.getRowIndexByID(o3.treeViewID));
|
|
cv.selection.focused = cv.getRowIndexByID(o1.treeViewID);
|
|
cv.selection.pivot = cv.selection.focused;
|
|
|
|
o3.deleted = true;
|
|
await o3.saveTx();
|
|
|
|
assert.sameMembers(cv.getSelectedRows().map(row => row.id), [o1.treeViewID]);
|
|
});
|
|
});
|
|
|
|
for (let objectType of ['collection', 'search']) {
|
|
it(`should select next row when ${objectType} is erased`, async function () {
|
|
var ran = Zotero.Utilities.randomString();
|
|
var o1 = await createDataObject(objectType, { name: ran + "AAA" });
|
|
var o2 = await createDataObject(objectType, { name: ran + "BBB" });
|
|
var o3 = await createDataObject(objectType, { name: ran + "CCC" });
|
|
|
|
await cv.selectByID(o2.treeViewID);
|
|
|
|
await o2.eraseTx();
|
|
|
|
assert.equal(zp.getCollectionTreeRows()[0].ref.id, o3.id);
|
|
});
|
|
}
|
|
|
|
it("should update the editability of the current view", async function () {
|
|
var group = await createGroup({
|
|
editable: false,
|
|
filesEditable: false
|
|
});
|
|
await cv.selectLibrary(group.libraryID);
|
|
await waitForItemsLoad(win);
|
|
|
|
assert.isFalse(zp.getCollectionTreeRows()[0].editable);
|
|
var cmd = win.document.getElementById('cmd_zotero_newStandaloneNote');
|
|
assert.isTrue(cmd.hasAttribute('disabled'));
|
|
|
|
group.editable = true;
|
|
await group.saveTx();
|
|
|
|
assert.isTrue(zp.getCollectionTreeRows()[0].editable);
|
|
assert.isFalse(cmd.hasAttribute('disabled'));
|
|
});
|
|
|
|
it("should re-sort a modified collection", async function () {
|
|
var prefix = Zotero.Utilities.randomString() + " ";
|
|
var collectionA = await createDataObject('collection', { name: prefix + "A" });
|
|
var collectionB = await createDataObject('collection', { name: prefix + "B" });
|
|
|
|
var aRow = cv.getRowIndexByID("C" + collectionA.id);
|
|
var aRowOriginal = aRow;
|
|
var bRow = cv.getRowIndexByID("C" + collectionB.id);
|
|
assert.equal(bRow, aRow + 1);
|
|
|
|
collectionA.name = prefix + "C";
|
|
await collectionA.saveTx();
|
|
|
|
var aRow = cv.getRowIndexByID("C" + collectionA.id);
|
|
var bRow = cv.getRowIndexByID("C" + collectionB.id);
|
|
assert.equal(bRow, aRowOriginal);
|
|
assert.equal(aRow, bRow + 1);
|
|
})
|
|
|
|
it("should re-sort a modified search", async function () {
|
|
var prefix = Zotero.Utilities.randomString() + " ";
|
|
var searchA = await createDataObject('search', { name: prefix + "A" });
|
|
var searchB = await createDataObject('search', { name: prefix + "B" });
|
|
|
|
var aRow = cv.getRowIndexByID("S" + searchA.id);
|
|
var aRowOriginal = aRow;
|
|
var bRow = cv.getRowIndexByID("S" + searchB.id);
|
|
assert.equal(bRow, aRow + 1);
|
|
|
|
searchA.name = prefix + "C";
|
|
await searchA.saveTx();
|
|
|
|
var aRow = cv.getRowIndexByID("S" + searchA.id);
|
|
var bRow = cv.getRowIndexByID("S" + searchB.id);
|
|
assert.equal(bRow, aRowOriginal);
|
|
assert.equal(aRow, bRow + 1);
|
|
})
|
|
|
|
|
|
it("should add collection after parent's subcollection and before non-sibling", async function () {
|
|
var c0 = await createDataObject('collection', { name: "Test" });
|
|
var rootRow = cv.getRowIndexByID(c0.treeViewID);
|
|
|
|
var c1 = await createDataObject('collection', { name: "1", parentID: c0.id });
|
|
var c2 = await createDataObject('collection', { name: "2", parentID: c0.id });
|
|
var c3 = await createDataObject('collection', { name: "3", parentID: c1.id });
|
|
var c4 = await createDataObject('collection', { name: "4", parentID: c3.id });
|
|
var c5 = await createDataObject('collection', { name: "5", parentID: c1.id });
|
|
await select(win, c4);
|
|
|
|
assert.equal(cv.getRowIndexByID(c1.treeViewID), rootRow + 1);
|
|
|
|
assert.isAbove(cv.getRowIndexByID(c1.treeViewID), cv.getRowIndexByID(c0.treeViewID));
|
|
assert.isAbove(cv.getRowIndexByID(c2.treeViewID), cv.getRowIndexByID(c0.treeViewID));
|
|
|
|
assert.isAbove(cv.getRowIndexByID(c3.treeViewID), cv.getRowIndexByID(c1.treeViewID));
|
|
assert.isAbove(cv.getRowIndexByID(c5.treeViewID), cv.getRowIndexByID(c1.treeViewID));
|
|
assert.isBelow(cv.getRowIndexByID(c5.treeViewID), cv.getRowIndexByID(c2.treeViewID));
|
|
|
|
assert.equal(cv.getRowIndexByID(c4.treeViewID), cv.getRowIndexByID(c3.treeViewID) + 1);
|
|
});
|
|
|
|
|
|
it("should add multiple collections", async function () {
|
|
var col1, col2;
|
|
await Zotero.DB.executeTransaction(async function () {
|
|
col1 = createUnsavedDataObject('collection');
|
|
col2 = createUnsavedDataObject('collection');
|
|
await col1.save();
|
|
await col2.save();
|
|
});
|
|
|
|
var aRow = cv.getRowIndexByID("C" + col1.id);
|
|
var bRow = cv.getRowIndexByID("C" + col2.id);
|
|
assert.isAbove(aRow, 0);
|
|
assert.isAbove(bRow, 0);
|
|
// skipSelect is implied for multiple collections, so library should still be selected
|
|
assert.equal(cv.selection.focused, 0);
|
|
});
|
|
|
|
|
|
it("shouldn't refresh the items list when a collection is modified", async function () {
|
|
var collection = await createDataObject('collection');
|
|
await waitForItemsLoad(win);
|
|
var itemsView = zp.itemsView;
|
|
|
|
collection.name = "New Name";
|
|
await collection.saveTx();
|
|
|
|
await waitForItemsLoad(win);
|
|
assert.equal(zp.itemsView, itemsView);
|
|
})
|
|
|
|
it("should add a saved search after collections", async function () {
|
|
var collection = new Zotero.Collection;
|
|
collection.name = "Test";
|
|
var collectionID = await collection.saveTx();
|
|
|
|
var search = new Zotero.Search;
|
|
search.name = "A Test Search";
|
|
search.addCondition('title', 'contains', 'test');
|
|
var searchID = await search.saveTx();
|
|
|
|
var collectionRow = cv._rowMap["C" + collectionID];
|
|
var searchRow = cv._rowMap["S" + searchID];
|
|
var duplicatesRow = cv._rowMap["D" + userLibraryID];
|
|
var unfiledRow = cv._rowMap["U" + userLibraryID];
|
|
|
|
assert.isAbove(searchRow, collectionRow);
|
|
// If there's a duplicates row or an unfiled row, add before those.
|
|
// Otherwise, add before the trash
|
|
if (duplicatesRow !== undefined) {
|
|
assert.isBelow(searchRow, duplicatesRow);
|
|
}
|
|
else if (unfiledRow !== undefined) {
|
|
assert.isBelow(searchRow, unfiledRow);
|
|
}
|
|
else {
|
|
var trashRow = cv._rowMap["T" + userLibraryID];
|
|
assert.isBelow(searchRow, trashRow);
|
|
}
|
|
})
|
|
|
|
it("shouldn't select a new group", async function () {
|
|
var group = await createGroup();
|
|
// Library should still be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], userLibraryID);
|
|
})
|
|
|
|
it("should remove a group and all children", async function () {
|
|
// Make sure Group Libraries separator and header exist already,
|
|
// since otherwise they'll interfere with the count
|
|
await getGroup();
|
|
|
|
var originalRowCount = cv._rows.length;
|
|
|
|
var group = await createGroup();
|
|
await createDataObject('collection', { libraryID: group.libraryID });
|
|
var c = await createDataObject('collection', { libraryID: group.libraryID });
|
|
var c2 = await createDataObject('collection', { libraryID: group.libraryID, parentID: c.id });
|
|
await createDataObject('collection', { libraryID: group.libraryID });
|
|
await createDataObject('collection', { libraryID: group.libraryID });
|
|
await select(win, c2);
|
|
|
|
// Group, collections, Recently Read, Duplicates, Unfiled, and trash
|
|
assert.equal(cv._rows.length, originalRowCount + 10);
|
|
|
|
// Select group
|
|
await cv.selectLibrary(group.libraryID);
|
|
await waitForItemsLoad(win);
|
|
|
|
var spy = sinon.spy(cv, "refresh");
|
|
try {
|
|
await group.eraseTx();
|
|
|
|
assert.equal(cv._rows.length, originalRowCount);
|
|
// Make sure the tree wasn't refreshed
|
|
sinon.assert.notCalled(spy);
|
|
}
|
|
finally {
|
|
spy.restore();
|
|
}
|
|
})
|
|
|
|
it("should select a new feed", async function () {
|
|
var feed = await createFeed();
|
|
// Feed should be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], feed.id);
|
|
});
|
|
|
|
it("shouldn't select a new feed with skipSelect: true", async function () {
|
|
var feed = await createFeed({
|
|
saveOptions: {
|
|
skipSelect: true
|
|
}
|
|
});
|
|
// Library should still be selected
|
|
assert.equal(cv.getSelectedLibraryIDs()[0], userLibraryID);
|
|
});
|
|
|
|
it("should remove deleted feed", async function () {
|
|
var feed = await createFeed();
|
|
await cv.selectLibrary(feed.libraryID);
|
|
waitForDialog();
|
|
var id = feed.treeViewID;
|
|
let promise = waitForCollectionTree(win);
|
|
await win.ZoteroPane.deleteSelectedCollection();
|
|
await promise;
|
|
assert.isFalse(cv.getRowIndexByID(id))
|
|
})
|
|
|
|
it("should not reload tree upon feed update", async function () {
|
|
var feed = await createFeed();
|
|
await cv.selectLibrary(Zotero.Libraries.userLibraryID);
|
|
try {
|
|
var reloadSpy = sinon.spy(cv, 'reload');
|
|
// A set of notifier calls when a feed update is running
|
|
Zotero.debug(feed.id, 2);
|
|
await Zotero.Notifier.trigger('statusChanged', 'feed', feed.id);
|
|
await Zotero.Notifier.trigger('modify', 'feed', feed.id);
|
|
await Zotero.Notifier.trigger('unreadCountUpdated', 'feed', feed.id);
|
|
await Zotero.Notifier.trigger('statusChanged', 'feed', feed.id);
|
|
assert.isFalse(reloadSpy.called);
|
|
} finally {
|
|
reloadSpy.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("#selectItem()", function () {
|
|
it("should switch to library root if item isn't in collection", async function () {
|
|
var item = await createDataObject('item');
|
|
var collection = await createDataObject('collection');
|
|
await cv.selectItem(item.id);
|
|
await waitForItemsLoad(win);
|
|
assert.equal(cv.selection.focused, 0);
|
|
assert.sameMembers(zp.itemsView.getSelectedItems(), [item]);
|
|
});
|
|
});
|
|
|
|
describe("#selectItems()", function () {
|
|
it("should switch to library root if at least one item isn't in the current collection", async function () {
|
|
var collection = await createDataObject('collection');
|
|
var item1 = await createDataObject('item', { collections: [collection.id] });
|
|
var item2 = await createDataObject('item');
|
|
await cv.selectItems([item1.id, item2.id]);
|
|
await waitForItemsLoad(win);
|
|
assert.equal(cv.selection.focused, 0);
|
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]);
|
|
});
|
|
|
|
it("should switch to trash if item is in trash", async function () {
|
|
var item = await createDataObject('item', { deleted: true });
|
|
await cv.selectItems([item.id]);
|
|
await waitForItemsLoad(win);
|
|
assert.isTrue(zp.getCollectionTreeRows()[0].isTrash());
|
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item.id]);
|
|
});
|
|
|
|
it("should switch to trash if parent of child note is in trash", async function () {
|
|
var item = await createDataObject('item', { deleted: true });
|
|
var note = await createDataObject('item', { itemType: 'note', parentItemID: item.id });
|
|
await cv.selectItems([note.id]);
|
|
await waitForItemsLoad(win);
|
|
assert.isTrue(zp.getCollectionTreeRows()[0].isTrash());
|
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [note.id]);
|
|
});
|
|
});
|
|
|
|
describe("#handleActivate()", function () {
|
|
it("shouldn't start editing when multiple collections are selected", async function () {
|
|
let c1 = await createDataObject('collection');
|
|
let c2 = await createDataObject('collection');
|
|
await cv.selectByID("C" + c1.id);
|
|
cv.selection.toggleSelect(cv.getRowIndexByID("C" + c2.id));
|
|
assert.equal(cv.selection.count, 2);
|
|
|
|
cv.handleActivate(new Event('keydown'), [cv.selection.focused]);
|
|
|
|
assert.notOk(cv._editing);
|
|
});
|
|
});
|
|
|
|
describe("selection", function () {
|
|
// Toggle a row the way a Cmd-click/Space does (through the table's selection handler)
|
|
function toggleRow(index) {
|
|
cv.tree._onSelection(index, false, true);
|
|
}
|
|
|
|
it("should return each selected library once, in tree order", async function () {
|
|
let group = await createGroup();
|
|
let collection1 = await createDataObject('collection');
|
|
let collection2 = await createDataObject('collection');
|
|
let groupCollection = await createDataObject(
|
|
'collection', { libraryID: group.libraryID }
|
|
);
|
|
|
|
await cv.expandLibrary(group.libraryID);
|
|
await cv.selectByID("C" + collection1.id);
|
|
// Toggle in reverse tree order, to show the result isn't in click order
|
|
toggleRow(cv.getRowIndexByID("C" + groupCollection.id));
|
|
toggleRow(cv.getRowIndexByID("C" + collection2.id));
|
|
|
|
assert.sameOrderedMembers(
|
|
cv.getSelectedLibraryIDs(),
|
|
[Zotero.Libraries.userLibraryID, group.libraryID]
|
|
);
|
|
|
|
await selectLibrary(win);
|
|
await group.eraseTx();
|
|
});
|
|
|
|
it("should keep a selection when the last selected row is toggled off", async function () {
|
|
let c = await createDataObject('collection');
|
|
await cv.selectByID("C" + c.id);
|
|
let row = cv.getRowIndexByID("C" + c.id);
|
|
assert.equal(cv.selection.count, 1);
|
|
|
|
// Toggling off the only selected row would empty the selection
|
|
toggleRow(row);
|
|
|
|
assert.equal(cv.selection.count, 1);
|
|
assert.isTrue(cv.selection.isSelected(row));
|
|
});
|
|
|
|
it("should allow toggling off a row when others remain selected", async function () {
|
|
let c1 = await createDataObject('collection');
|
|
let c2 = await createDataObject('collection');
|
|
await cv.selectByID("C" + c1.id);
|
|
let r2 = cv.getRowIndexByID("C" + c2.id);
|
|
toggleRow(r2);
|
|
assert.equal(cv.selection.count, 2);
|
|
|
|
// Toggling one off leaves the other
|
|
toggleRow(r2);
|
|
assert.equal(cv.selection.count, 1);
|
|
assert.isTrue(cv.selection.isSelected(cv.getRowIndexByID("C" + c1.id)));
|
|
});
|
|
|
|
it("should mark the focused-but-unselected row for the focus ring", async function () {
|
|
let c1 = await createDataObject('collection');
|
|
let c2 = await createDataObject('collection');
|
|
await cv.selectByID("C" + c1.id);
|
|
let r2 = cv.getRowIndexByID("C" + c2.id);
|
|
|
|
// Move focus to c2 without selecting it (macOS Cmd-arrow style)
|
|
cv.tree._onSelection(r2, false, false, true);
|
|
assert.equal(cv.selection.focused, r2);
|
|
assert.isFalse(cv.selection.isSelected(r2));
|
|
|
|
// The focused (unselected) row gets the 'focused' class that drives the
|
|
// dotted focus ring (rendering is async, so wait for it)
|
|
await waitForCallback(() => {
|
|
let n = win.document.getElementById(`${cv.id}-row-${r2}`);
|
|
return n && n.classList.contains('focused');
|
|
}, 50, 20);
|
|
let node = win.document.getElementById(`${cv.id}-row-${r2}`);
|
|
assert.isTrue(node.classList.contains('focused'));
|
|
assert.isFalse(node.classList.contains('selected'));
|
|
});
|
|
|
|
it("should not leave a stale multi-selection after filtering", async function () {
|
|
let cA = await createDataObject('collection', { name: 'filterAAA' });
|
|
let cB = await createDataObject('collection', { name: 'filterBBB' });
|
|
let cC = await createDataObject('collection', { name: 'filterCCC' });
|
|
|
|
// Select all three, focusing the one that will still match the filter
|
|
await cv.selectByID("C" + cB.id);
|
|
toggleRow(cv.getRowIndexByID("C" + cC.id));
|
|
toggleRow(cv.getRowIndexByID("C" + cA.id));
|
|
assert.equal(cv.selection.count, 3);
|
|
|
|
await cv.setFilter("filterAAA");
|
|
|
|
// Only the still-matching focused row is selected -- no stale indices
|
|
assert.deepEqual(cv.getSelectedRows().map(r => r.id), ["C" + cA.id]);
|
|
|
|
await cv.setFilter("");
|
|
});
|
|
});
|
|
|
|
describe("#_handleSelectAll()", function () {
|
|
beforeEach(async function () {
|
|
await clearFeeds();
|
|
});
|
|
after(async function () {
|
|
await clearFeeds();
|
|
});
|
|
|
|
it("should select all library roots without pulling in feeds", async function () {
|
|
var group = await createGroup();
|
|
await createFeed();
|
|
|
|
// Expand the Feeds container so its feed rows are in the tree, which is the
|
|
// state that triggered the bug
|
|
var feedsRow = cv._rows.findIndex(row => row.isFeeds());
|
|
assert.isAbove(feedsRow, -1);
|
|
if (!cv.isContainerOpen(feedsRow)) {
|
|
await cv.toggleOpenState(feedsRow);
|
|
}
|
|
assert.isTrue(cv._rows.some(row => row.isFeed()));
|
|
|
|
// Select My Library and do Cmd/Ctrl-A
|
|
await cv.selectByID("L" + userLibraryID);
|
|
await cv._handleSelectAll();
|
|
await zp.onCollectionSelected();
|
|
|
|
var selectedRows = cv.getSelectedRows();
|
|
// My Library and the group are both selected...
|
|
assert.isTrue(selectedRows.some(row => row.id == "L" + userLibraryID));
|
|
assert.isTrue(selectedRows.some(row => row.id == group.treeViewID));
|
|
// ...and no feeds were pulled in, so the selection wasn't collapsed to a
|
|
// single row for not being shown together
|
|
assert.isFalse(selectedRows.some(row => row.isFeed()));
|
|
assert.isAbove(cv.selection.count, 1);
|
|
});
|
|
});
|
|
|
|
describe("#deleteSelectedCollection()", function () {
|
|
it("shouldn't delete a mixed-type selection", async function () {
|
|
let collection = await createDataObject('collection');
|
|
let search = await createDataObject('search');
|
|
await cv.selectByID("C" + collection.id);
|
|
cv.selection.toggleSelect(cv.getRowIndexByID("S" + search.id));
|
|
await zp.onCollectionSelected();
|
|
assert.equal(cv.selection.count, 2);
|
|
|
|
let stub = sinon.stub().returns(0);
|
|
let promptService = win.Services.prompt;
|
|
win.Services.prompt = { confirmEx: stub };
|
|
try {
|
|
await zp.deleteSelectedCollection(false);
|
|
}
|
|
finally {
|
|
win.Services.prompt = promptService;
|
|
}
|
|
|
|
assert.isFalse(stub.called, "Mixed selection shouldn't prompt or delete");
|
|
assert.isFalse(collection.deleted);
|
|
assert.isFalse(search.deleted);
|
|
});
|
|
});
|
|
|
|
describe("#onDrop()", function () {
|
|
/**
|
|
* Simulate a drag and drop
|
|
*
|
|
* @param {String} type - 'item' or 'collection'
|
|
* @param {String|Object} targetRow - Tree row id (e.g., "L123"), or { row, orient }
|
|
* @param {Integer[]} collectionIDs
|
|
* @param {Promise} [promise] - If a promise is provided, it will be waited for and its
|
|
* value returned after the drag. Otherwise, an 'add' event will be waited for, and
|
|
* an object with 'ids' and 'extraData' will be returned.
|
|
*/
|
|
var onDrop = async function (objectType, targetRow, ids, promise, action = 'copy') {
|
|
if (typeof targetRow == 'string') {
|
|
var row = cv.getRowIndexByID(targetRow);
|
|
var orient = 0;
|
|
}
|
|
else {
|
|
var { row, orient } = targetRow;
|
|
}
|
|
|
|
Zotero.DragDrop.currentDragSource = objectType == "item"
|
|
? zp.itemsView.collectionTreeRows[0]
|
|
: null;
|
|
|
|
if (!promise) {
|
|
promise = waitForNotifierEvent("add", objectType);
|
|
}
|
|
await cv.onDrop({
|
|
persist: () => 0,
|
|
target: {ownerDocument: {defaultView: win}},
|
|
dataTransfer: {
|
|
dropEffect: action,
|
|
effectAllowed: action,
|
|
types: [`zotero/${objectType}`],
|
|
getData: function (type) {
|
|
if (type == `zotero/${objectType}`) {
|
|
return ids.join(",");
|
|
}
|
|
}
|
|
}
|
|
}, row);
|
|
|
|
// Add observer to wait for add
|
|
var result = await promise;
|
|
Zotero.DragDrop.currentDragSource = null;
|
|
return result;
|
|
};
|
|
|
|
|
|
var canDrop = async function (objectType, targetRowID, ids) {
|
|
var row = cv.getRowIndexByID(targetRowID);
|
|
|
|
Zotero.DragDrop.currentDragSource = objectType == "item"
|
|
? zp.itemsView.collectionTreeRows[0]
|
|
: null;
|
|
var dt = {
|
|
dropEffect: 'copy',
|
|
effectAllowed: 'copy',
|
|
types: [`zotero/${objectType}`],
|
|
getData: function (type) {
|
|
if (type == `zotero/${objectType}`) {
|
|
return ids.join(",");
|
|
}
|
|
}
|
|
};
|
|
var canDrop = cv.canDropCheck(row, 0, dt);
|
|
if (canDrop) {
|
|
canDrop = await cv.canDropCheckAsync(row, 0, dt);
|
|
}
|
|
Zotero.DragDrop.currentDragSource = null;
|
|
return canDrop;
|
|
};
|
|
|
|
// Simulate a drag over a row and return the resulting dropEffect ('copy', 'move', or
|
|
// 'none'). Pass { move: true } to simulate the platform's move modifier being held.
|
|
var dragOver = function (objectType, targetRowID, ids, { move = false, effectAllowed = 'copyMove' } = {}) {
|
|
var index = cv.getRowIndexByID(targetRowID);
|
|
|
|
Zotero.DragDrop.currentDragSource = objectType == "item"
|
|
? zp.itemsView.collectionTreeRows[0]
|
|
: null;
|
|
|
|
// Drop directly onto the middle of the row (orient 0)
|
|
var rowEl = {
|
|
classList: { contains: () => true },
|
|
getBoundingClientRect: () => ({ y: 0, height: 100 })
|
|
};
|
|
var dataTransfer = {
|
|
dropEffect: 'copy',
|
|
effectAllowed,
|
|
types: [`zotero/${objectType}`],
|
|
getData: function (type) {
|
|
if (type == `zotero/${objectType}`) {
|
|
return ids.join(",");
|
|
}
|
|
return "";
|
|
},
|
|
setDragImage: () => {}
|
|
};
|
|
cv.onDragOver({
|
|
preventDefault: () => {},
|
|
stopPropagation: () => {},
|
|
currentTarget: rowEl,
|
|
target: rowEl,
|
|
clientY: 50,
|
|
metaKey: move && Zotero.isMac,
|
|
shiftKey: move && !Zotero.isMac,
|
|
dataTransfer
|
|
}, index);
|
|
Zotero.DragDrop.currentDragSource = null;
|
|
Zotero.DragDrop.currentDropEffect = null;
|
|
return dataTransfer.dropEffect;
|
|
};
|
|
|
|
describe("with items", function () {
|
|
it("should add an item to a collection", async function () {
|
|
var collection = await createDataObject('collection');
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection-item' && event == 'add'
|
|
&& ids[0] == collection.id + "-" + item.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
});
|
|
}
|
|
}
|
|
}, 'collection-item', 'test');
|
|
|
|
await onDrop('item', 'C' + collection.id, [item.id], deferred.promise);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
await select(win, collection);
|
|
|
|
var itemsView = win.ZoteroPane.itemsView;
|
|
assert.equal(itemsView.rowCount, 1);
|
|
var treeRow = itemsView.getRow(0);
|
|
assert.equal(treeRow.ref.id, item.id);
|
|
})
|
|
|
|
it("should move an item when the drag only allows copying", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
await select(win, collection1);
|
|
var collection2 = await createDataObject('collection');
|
|
var item = await createDataObject('item', { collections: [collection1.id] });
|
|
|
|
var index = cv.getRowIndexByID('C' + collection2.id);
|
|
var rowEl = {
|
|
classList: { contains: () => true },
|
|
getBoundingClientRect: () => ({ y: 0, height: 100 })
|
|
};
|
|
var dataTransfer = {
|
|
dropEffect: 'copy',
|
|
effectAllowed: 'copy',
|
|
types: ['zotero/item'],
|
|
getData: function (type) {
|
|
if (type == 'zotero/item') {
|
|
return item.id + "";
|
|
}
|
|
return "";
|
|
},
|
|
setDragImage: () => {}
|
|
};
|
|
Zotero.DragDrop.currentDragSource = zp.itemsView.collectionTreeRows[0];
|
|
cv.onDragOver({
|
|
preventDefault: () => {},
|
|
stopPropagation: () => {},
|
|
currentTarget: rowEl,
|
|
target: rowEl,
|
|
clientY: 50,
|
|
metaKey: Zotero.isMac,
|
|
shiftKey: !Zotero.isMac,
|
|
dataTransfer
|
|
}, index);
|
|
// A file attachment drag allows only copying, so the requested move has to be
|
|
// sent as a copy for the drop to happen
|
|
assert.equal(dataTransfer.dropEffect, 'copy');
|
|
|
|
var promise = waitForNotifierEvent('add', 'collection-item');
|
|
await cv.onDrop({
|
|
persist: () => 0,
|
|
target: { ownerDocument: { defaultView: win } },
|
|
dataTransfer
|
|
}, index);
|
|
await promise;
|
|
Zotero.DragDrop.currentDragSource = null;
|
|
|
|
assert.sameMembers(item.getCollections(), [collection2.id]);
|
|
});
|
|
|
|
it("should move an item from one collection to another", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
await select(win, collection1);
|
|
var collection2 = await createDataObject('collection');
|
|
var item = await createDataObject('item', { collections: [collection1.id] });
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection-item' && event == 'add'
|
|
&& ids[0] == collection2.id + "-" + item.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
});
|
|
}
|
|
}
|
|
}, 'collection-item', 'test');
|
|
|
|
let promise = zp.itemsView.waitForSelect();
|
|
await onDrop('item', 'C' + collection2.id, [item.id], deferred.promise, 'move');
|
|
await promise;
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
// Source collection should be empty
|
|
assert.equal(zp.itemsView.rowCount, 0);
|
|
|
|
await select(win, collection2);
|
|
|
|
// Target collection should have item
|
|
assert.equal(zp.itemsView.rowCount, 1);
|
|
var treeRow = zp.itemsView.getRow(0);
|
|
assert.equal(treeRow.ref.id, item.id);
|
|
});
|
|
|
|
it("should add a multiple-library item selection to a collection, copying out-of-library items", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setName(1, 'Name');
|
|
|
|
var collection = await createDataObject('collection');
|
|
var libraryItem = await createDataObject('item', false, { skipSelect: true });
|
|
|
|
var group = await createGroup();
|
|
var groupItem = await createDataObject('item', { libraryID: group.libraryID });
|
|
|
|
// Drop one item from the personal library and one from the group onto a
|
|
// personal-library collection
|
|
await onDrop('item', 'C' + collection.id, [libraryItem.id, groupItem.id]);
|
|
await collection.loadDataType('childItems');
|
|
|
|
// The collection now contains the same-library item plus a copy of the group item
|
|
var childItemIDs = collection.getChildItems(true);
|
|
assert.lengthOf(childItemIDs, 2);
|
|
assert.include(childItemIDs, libraryItem.id);
|
|
|
|
// The group item was copied into the personal library, and the copy links back to it
|
|
var copiedItem = Zotero.Items.get(childItemIDs.find(id => id != libraryItem.id));
|
|
assert.equal(copiedItem.libraryID, collection.libraryID);
|
|
assert.equal((await copiedItem.getLinkedItem(group.libraryID)).id, groupItem.id);
|
|
|
|
await group.eraseTx();
|
|
});
|
|
|
|
it("should disallow moving a multiple-library item selection", async function () {
|
|
var sourceCollection = await createDataObject('collection');
|
|
var targetCollection = await createDataObject('collection');
|
|
var libraryItem = await createDataObject('item', { collections: [sourceCollection.id] });
|
|
|
|
var group = await createGroup();
|
|
var groupItem = await createDataObject('item', { libraryID: group.libraryID });
|
|
|
|
// Source collection has to be selected so it's used as the drag source
|
|
await select(win, sourceCollection);
|
|
await waitForItemsLoad(win);
|
|
|
|
var ids = [libraryItem.id, groupItem.id];
|
|
// A plain drag copies the selection
|
|
assert.equal(dragOver('item', 'C' + targetCollection.id, ids), 'copy');
|
|
// A move is disallowed, since the out-of-library item can't be moved
|
|
assert.equal(dragOver('item', 'C' + targetCollection.id, ids, { move: true }), 'none');
|
|
|
|
await group.eraseTx();
|
|
});
|
|
|
|
it("should copy out-of-library items from a multiple-library selection dropped on a library root", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setName(1, 'Name');
|
|
|
|
var libraryItem = await createDataObject('item', false, { skipSelect: true });
|
|
|
|
var group = await createGroup();
|
|
var groupItem = await createDataObject('item', { libraryID: group.libraryID });
|
|
|
|
// Drop a personal-library item and a group item onto the personal library root: the
|
|
// item already in the library is a no-op, and the group item is copied in
|
|
var ids = (await onDrop('item', 'L' + userLibraryID, [libraryItem.id, groupItem.id])).ids;
|
|
assert.lengthOf(ids, 1);
|
|
|
|
var copiedItem = Zotero.Items.get(ids[0]);
|
|
assert.equal(copiedItem.libraryID, userLibraryID);
|
|
assert.equal((await copiedItem.getLinkedItem(group.libraryID)).id, groupItem.id);
|
|
|
|
await group.eraseTx();
|
|
});
|
|
|
|
it("should refuse a single-library selection dropped on its own library root", async function () {
|
|
var item1 = await createDataObject('item', false, { skipSelect: true });
|
|
var item2 = await createDataObject('item', false, { skipSelect: true });
|
|
|
|
// With no out-of-library items to copy, the drag is refused
|
|
assert.isFalse(await canDrop('item', 'L' + userLibraryID, [item1.id, item2.id]));
|
|
});
|
|
|
|
it("should record an undo step when adding an item to a collection", async function () {
|
|
var collection = await createDataObject('collection');
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
Zotero.UndoHistory.clear();
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection-item' && event == 'add'
|
|
&& ids[0] == collection.id + "-" + item.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
});
|
|
}
|
|
}
|
|
}, 'collection-item', 'test');
|
|
|
|
await onDrop('item', 'C' + collection.id, [item.id], deferred.promise);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
assert.include(item.getCollections(), collection.id);
|
|
assert.isTrue(Zotero.UndoHistory.canUndo());
|
|
var action = Zotero.UndoHistory.getUndoAction();
|
|
assert.equal(action.action, 'undo-action-add-to-collection');
|
|
assert.equal(action.actionArgs.count, 1);
|
|
|
|
await Zotero.UndoHistory.undo();
|
|
assert.notInclude(item.getCollections(), collection.id);
|
|
|
|
await Zotero.UndoHistory.redo();
|
|
assert.include(item.getCollections(), collection.id);
|
|
});
|
|
|
|
describe("My Publications", function () {
|
|
function getItemModifyPromise(item) {
|
|
// Add observer to wait for item modification
|
|
return new Zotero.Promise((resolve) => {
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'item' && event == 'modify' && ids[0] == item.id) {
|
|
setTimeout(resolve);
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
}
|
|
}
|
|
}, 'item', 'test');
|
|
});
|
|
}
|
|
|
|
function acceptItemsWithoutFiles(win) {
|
|
var doc = win.document;
|
|
doc.getElementById('confirm-authorship-checkbox').click();
|
|
var wizard = doc.getElementById('publications-dialog-wizard');
|
|
if (!doc.getElementById('include-files').disabled) {
|
|
throw new Error("Include Files checkbox isn't disabled");
|
|
}
|
|
wizard.getButton('next').click();
|
|
}
|
|
|
|
function acceptItemsWithFiles(win) {
|
|
var doc = win.document;
|
|
doc.getElementById('include-files').click();
|
|
doc.getElementById('confirm-authorship-checkbox').click();
|
|
var wizard = doc.getElementById('publications-dialog-wizard');
|
|
if (doc.getElementById('include-files').disabled) {
|
|
throw new Error("Include Files checkbox shouldn't be disabled");
|
|
}
|
|
wizard.getButton('next').click();
|
|
wizard.getButton('next').click();
|
|
}
|
|
|
|
it("should add an item to My Publications", async function () {
|
|
// TEMP: Disabled due to spurious errors
|
|
// https://github.com/zotero/zotero/issues/5749
|
|
if (Zotero.automatedTest) {
|
|
this.skip();
|
|
return;
|
|
}
|
|
// Remove other items in My Publications
|
|
var s = new Zotero.Search();
|
|
s.addCondition('libraryID', 'is', Zotero.Libraries.userLibraryID);
|
|
s.addCondition('publications', 'true');
|
|
var ids = await s.search();
|
|
await Zotero.Items.erase(ids);
|
|
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var libraryID = item.libraryID;
|
|
|
|
var itemModifyPromise = getItemModifyPromise(item);
|
|
var winPromise = waitForWindow('chrome://zotero/content/publicationsDialog.xhtml')
|
|
var dropPromise = onDrop('item', 'P' + libraryID, [item.id], itemModifyPromise);
|
|
acceptItemsWithoutFiles(await winPromise);
|
|
await dropPromise;
|
|
|
|
// Select publications and check for item
|
|
await cv.selectByID("P" + libraryID);
|
|
await waitForItemsLoad(win);
|
|
var itemsView = win.ZoteroPane.itemsView
|
|
assert.equal(itemsView.rowCount, 1);
|
|
var treeRow = itemsView.getRow(0);
|
|
assert.equal(treeRow.ref.id, item.id);
|
|
});
|
|
|
|
it("should add an item with a file attachment to My Publications", async function () {
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var attachment = await importFileAttachment('test.png', { parentItemID: item.id });
|
|
var libraryID = item.libraryID;
|
|
|
|
var itemModifyPromise = getItemModifyPromise(item);
|
|
var winPromise = waitForWindow('chrome://zotero/content/publicationsDialog.xhtml')
|
|
var dropPromise = onDrop('item', 'P' + libraryID, [item.id], itemModifyPromise);
|
|
acceptItemsWithFiles(await winPromise);
|
|
await dropPromise;
|
|
|
|
assert.isTrue(item.inPublications);
|
|
// File attachment should be in My Publications
|
|
assert.isTrue(attachment.inPublications);
|
|
});
|
|
|
|
it("should add an item with a linked URL attachment to My Publications", async function () {
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var attachment = await Zotero.Attachments.linkFromURL({
|
|
parentItemID: item.id,
|
|
title: 'Test',
|
|
url: 'http://127.0.0.1/',
|
|
contentType: 'text/html'
|
|
});
|
|
var libraryID = item.libraryID;
|
|
|
|
var itemModifyPromise = getItemModifyPromise(item);
|
|
var winPromise = waitForWindow('chrome://zotero/content/publicationsDialog.xhtml')
|
|
var dropPromise = onDrop('item', 'P' + libraryID, [item.id], itemModifyPromise);
|
|
acceptItemsWithoutFiles(await winPromise);
|
|
await dropPromise;
|
|
|
|
assert.isTrue(item.inPublications);
|
|
// Link attachment should be in My Publications
|
|
assert.isTrue(attachment.inPublications);
|
|
});
|
|
|
|
it("shouldn't add linked file attachment to My Publications", async function () {
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var attachment = await Zotero.Attachments.linkFromFile({
|
|
parentItemID: item.id,
|
|
title: 'Test',
|
|
file: OS.Path.join(getTestDataDirectory().path, 'test.png'),
|
|
contentType: 'image/png'
|
|
});
|
|
var libraryID = item.libraryID;
|
|
|
|
var itemModifyPromise = getItemModifyPromise(item);
|
|
var winPromise = waitForWindow('chrome://zotero/content/publicationsDialog.xhtml')
|
|
var dropPromise = onDrop('item', 'P' + libraryID, [item.id], itemModifyPromise);
|
|
acceptItemsWithoutFiles(await winPromise);
|
|
await dropPromise;
|
|
|
|
assert.isTrue(item.inPublications);
|
|
// Linked URL attachment shouldn't be in My Publications
|
|
assert.isFalse(attachment.inPublications);
|
|
});
|
|
});
|
|
|
|
it("should copy an item with a PDF attachment containing annotations to a group", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setName(1, 'Name');
|
|
|
|
var group = await createGroup();
|
|
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var file = getTestDataDirectory();
|
|
file.append('test.pdf');
|
|
var attachment = await Zotero.Attachments.importFromFile({
|
|
file,
|
|
parentItemID: item.id
|
|
});
|
|
var annotation = await createAnnotation('highlight', attachment);
|
|
|
|
var ids = (await onDrop('item', 'L' + group.libraryID, [item.id])).ids;
|
|
|
|
await cv.selectLibrary(group.libraryID);
|
|
await waitForItemsLoad(win);
|
|
|
|
// Check parent
|
|
var itemsView = win.ZoteroPane.itemsView;
|
|
assert.equal(itemsView.rowCount, 1);
|
|
var treeRow = itemsView.getRow(0);
|
|
assert.equal(treeRow.ref.libraryID, group.libraryID);
|
|
assert.equal(treeRow.ref.id, ids[0]);
|
|
// New item should link back to original
|
|
var linked = await item.getLinkedItem(group.libraryID);
|
|
assert.equal(linked.id, treeRow.ref.id);
|
|
|
|
// Check attachment
|
|
assert.isTrue(itemsView.isContainer(0));
|
|
itemsView.toggleOpenState(0);
|
|
assert.equal(itemsView.rowCount, 2);
|
|
treeRow = itemsView.getRow(1);
|
|
assert.equal(treeRow.ref.id, ids[1]);
|
|
// New attachment should link back to original
|
|
linked = await attachment.getLinkedItem(group.libraryID);
|
|
assert.equal(linked.id, treeRow.ref.id);
|
|
|
|
// Check annotation
|
|
var groupAttachment = Zotero.Items.get(treeRow.ref.id);
|
|
var annotations = groupAttachment.getAnnotations();
|
|
assert.lengthOf(annotations, 1);
|
|
|
|
return group.eraseTx();
|
|
});
|
|
|
|
it("should copy a group item with a PDF attachment containing annotations to the personal library", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setName(1, 'Name 1');
|
|
await Zotero.Users.setName(12345, 'Name 2');
|
|
|
|
var group = await createGroup();
|
|
await cv.selectLibrary(group.libraryID);
|
|
|
|
var groupItem = await createDataObject('item', { libraryID: group.libraryID });
|
|
var file = getTestDataDirectory();
|
|
file.append('test.pdf');
|
|
var attachment = await Zotero.Attachments.importFromFile({
|
|
file,
|
|
parentItemID: groupItem.id
|
|
});
|
|
var annotation = await createAnnotation('highlight', attachment);
|
|
await annotation.saveTx();
|
|
|
|
var ids = (await onDrop('item', 'L1', [groupItem.id])).ids;
|
|
var newItem = Zotero.Items.get(ids[0]);
|
|
|
|
// Check annotation
|
|
var newAttachment = Zotero.Items.get(newItem.getAttachments())[0];
|
|
var annotations = newAttachment.getAnnotations();
|
|
assert.lengthOf(annotations, 1);
|
|
|
|
return group.eraseTx();
|
|
});
|
|
|
|
it("should copy a standalone attachment to a group", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setName(1, 'Name 1');
|
|
await Zotero.Users.setName(12345, 'Name 2');
|
|
|
|
var group = await createGroup();
|
|
|
|
var item = await importPDFAttachment();
|
|
|
|
var ids = (await onDrop('item', 'L' + group.libraryID, [item.id])).ids;
|
|
var newItem = Zotero.Items.get(ids[0]);
|
|
|
|
assert.equal(newItem.libraryID, group.libraryID);
|
|
assert.isTrue(newItem.isPDFAttachment());
|
|
|
|
return group.eraseTx();
|
|
});
|
|
|
|
it("should not copy an item or its attachment to a group twice", async function () {
|
|
var group = await getGroup();
|
|
|
|
var itemTitle = Zotero.Utilities.randomString();
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var file = getTestDataDirectory();
|
|
file.append('test.png');
|
|
var attachment = await Zotero.Attachments.importFromFile({
|
|
file: file,
|
|
parentItemID: item.id
|
|
});
|
|
var attachmentTitle = Zotero.Utilities.randomString();
|
|
attachment.setField('title', attachmentTitle);
|
|
await attachment.saveTx();
|
|
|
|
await onDrop('item', 'L' + group.libraryID, [item.id]);
|
|
assert.isFalse(await canDrop('item', 'L' + group.libraryID, [item.id]));
|
|
})
|
|
|
|
it("should copy an item from a read-only group to an editable group", async function () {
|
|
var group1 = await createGroup();
|
|
var item = await createDataObject('item', { libraryID: group1.libraryID });
|
|
group1.editable = false;
|
|
await group1.saveTx();
|
|
var group2 = await createGroup();
|
|
|
|
await cv.selectLibrary(group1.libraryID);
|
|
await waitForItemsLoad(win);
|
|
|
|
await onDrop('item', 'L' + group2.libraryID, [item.id]);
|
|
|
|
assert.isFalse(await item.getLinkedItem(group2.libraryID));
|
|
// New collection should link back to original
|
|
assert.ok(await item.getLinkedItem(group2.libraryID, true));
|
|
|
|
await group1.eraseTx();
|
|
await group2.eraseTx();
|
|
});
|
|
|
|
it("should ignore a linked, trashed item when re-dragging an item to a group", async function () {
|
|
var group = await getGroup();
|
|
var collection = await createDataObject('collection', { libraryID: group.libraryID });
|
|
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
await onDrop('item', 'L' + group.libraryID, [item.id]);
|
|
|
|
var droppedItem = await item.getLinkedItem(group.libraryID);
|
|
droppedItem.setCollections([collection.id]);
|
|
droppedItem.deleted = true;
|
|
await droppedItem.saveTx();
|
|
|
|
await onDrop('item', 'L' + group.libraryID, [item.id]);
|
|
|
|
var linkedItem = await item.getLinkedItem(group.libraryID);
|
|
assert.notEqual(linkedItem, droppedItem);
|
|
|
|
assert.isTrue(droppedItem.deleted);
|
|
})
|
|
|
|
it("can delete an item by dropping it into the trash", async function () {
|
|
var item = await createDataObject('item', false, { skipSelect: true });
|
|
var deferred = Zotero.Promise.defer();
|
|
deferred.resolve();
|
|
await onDrop('item', 'T' + userLibraryID, [item.id], deferred.promise);
|
|
assert.isTrue(item.deleted);
|
|
});
|
|
})
|
|
|
|
|
|
describe("with collections", function () {
|
|
it("should make a subcollection top-level", async function () {
|
|
var collection1 = await createDataObject('collection', { name: "A" }, { skipSelect: true });
|
|
var collection2 = await createDataObject('collection', { name: "C" }, { skipSelect: true });
|
|
var collection3 = await createDataObject('collection', { name: "D" }, { skipSelect: true });
|
|
var collection4 = await createDataObject('collection', { name: "B", parentKey: collection2.key });
|
|
|
|
var colIndex1 = cv.getRowIndexByID('C' + collection1.id);
|
|
var colIndex2 = cv.getRowIndexByID('C' + collection2.id);
|
|
var colIndex3 = cv.getRowIndexByID('C' + collection3.id);
|
|
var colIndex4 = cv.getRowIndexByID('C' + collection4.id);
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection' && event == 'modify' && ids[0] == collection4.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
}, 50);
|
|
}
|
|
}
|
|
}, 'collection', 'test');
|
|
|
|
await onDrop(
|
|
'collection',
|
|
{
|
|
row: 0,
|
|
orient: 1
|
|
},
|
|
[collection4.id],
|
|
deferred.promise
|
|
);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
var newColIndex1 = cv.getRowIndexByID('C' + collection1.id);
|
|
var newColIndex2 = cv.getRowIndexByID('C' + collection2.id);
|
|
var newColIndex3 = cv.getRowIndexByID('C' + collection3.id);
|
|
var newColIndex4 = cv.getRowIndexByID('C' + collection4.id);
|
|
|
|
assert.equal(newColIndex1, colIndex1);
|
|
assert.isBelow(newColIndex4, newColIndex2);
|
|
assert.isBelow(newColIndex2, newColIndex3);
|
|
assert.equal(cv.getRow(newColIndex4).level, cv.getRow(newColIndex1).level);
|
|
})
|
|
|
|
it("should move a subcollection and its subcollection down under another collection", async function () {
|
|
var collectionA = await createDataObject('collection', { name: "A" });
|
|
var collectionB = await createDataObject('collection', { name: "B", parentKey: collectionA.key });
|
|
var collectionC = await createDataObject('collection', { name: "C", parentKey: collectionB.key });
|
|
var collectionD = await createDataObject('collection', { name: "D" });
|
|
var collectionE = await createDataObject('collection', { name: "E" });
|
|
var collectionF = await createDataObject('collection', { name: "F" });
|
|
var collectionG = await createDataObject('collection', { name: "G", parentKey: collectionD.key });
|
|
var collectionH = await createDataObject('collection', { name: "H", parentKey: collectionG.key });
|
|
|
|
await cv.expandToCollection(collectionC.id);
|
|
await cv.expandToCollection(collectionH.id);
|
|
|
|
var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
|
var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
|
var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
|
var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
|
var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
|
var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
|
var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
|
var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
|
|
|
await select(win, collectionG);
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
}, 50);
|
|
}
|
|
}
|
|
}, 'collection', 'test');
|
|
|
|
await onDrop(
|
|
'collection',
|
|
{
|
|
row: colIndexE,
|
|
orient: 0
|
|
},
|
|
[collectionG.id],
|
|
deferred.promise
|
|
);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
|
var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
|
var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
|
var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
|
var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
|
var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
|
var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
|
var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
|
|
|
assert.isFalse(cv.isContainerOpen(newColIndexD));
|
|
assert.isTrue(cv.isContainerEmpty(newColIndexD));
|
|
assert.isTrue(cv.isContainerOpen(newColIndexE));
|
|
assert.isFalse(cv.isContainerEmpty(newColIndexE));
|
|
assert.equal(newColIndexE, newColIndexG - 1);
|
|
assert.equal(newColIndexG, newColIndexH - 1);
|
|
|
|
// TODO: Check deeper subcollection open states
|
|
});
|
|
|
|
it("should move a subcollection and its subcollection up under another collection", async function () {
|
|
var collectionA = await createDataObject('collection', { name: "A" });
|
|
var collectionB = await createDataObject('collection', { name: "B", parentKey: collectionA.key });
|
|
var collectionC = await createDataObject('collection', { name: "C", parentKey: collectionB.key });
|
|
var collectionD = await createDataObject('collection', { name: "D" });
|
|
var collectionE = await createDataObject('collection', { name: "E" });
|
|
var collectionF = await createDataObject('collection', { name: "F" });
|
|
var collectionG = await createDataObject('collection', { name: "G", parentKey: collectionE.key });
|
|
var collectionH = await createDataObject('collection', { name: "H", parentKey: collectionG.key });
|
|
|
|
await cv.expandToCollection(collectionC.id);
|
|
await cv.expandToCollection(collectionH.id);
|
|
|
|
var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
|
var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
|
var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
|
var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
|
var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
|
var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
|
var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
|
var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
|
|
|
await cv.selectCollection(collectionG.id);
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
}, 50);
|
|
}
|
|
}
|
|
}, 'collection', 'test');
|
|
|
|
await onDrop(
|
|
'collection',
|
|
{
|
|
row: colIndexD,
|
|
orient: 0
|
|
},
|
|
[collectionG.id],
|
|
deferred.promise
|
|
);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
|
var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
|
var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
|
var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
|
var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
|
var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
|
var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
|
var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
|
|
|
assert.isFalse(cv.isContainerOpen(newColIndexE));
|
|
assert.isTrue(cv.isContainerEmpty(newColIndexE));
|
|
assert.isTrue(cv.isContainerOpen(newColIndexD));
|
|
assert.isFalse(cv.isContainerEmpty(newColIndexD));
|
|
assert.equal(newColIndexD, newColIndexG - 1);
|
|
assert.equal(newColIndexG, newColIndexH - 1);
|
|
|
|
// TODO: Check deeper subcollection open states
|
|
});
|
|
|
|
it("should copy a collection and its subcollection to another library", async function () {
|
|
var group = await createGroup();
|
|
|
|
var collectionA = await createDataObject('collection', { name: "A" });
|
|
var collectionB = await createDataObject('collection', { name: "B", parentKey: collectionA.key });
|
|
var itemA = await createDataObject('item', { collections: [collectionA.key] }, { skipSelect: true });
|
|
var itemB = await createDataObject('item', { collections: [collectionB.key] }, { skipSelect: true });
|
|
|
|
await cv.selectCollection(collectionA.id);
|
|
|
|
// Add observer to wait for collection add
|
|
var deferred = Zotero.Promise.defer();
|
|
var observerID = Zotero.Notifier.registerObserver({
|
|
notify: function (event, type, ids, extraData) {
|
|
if (type == 'collection' && event == 'modify' && ids.includes(collectionB.id)) {
|
|
setTimeout(function () {
|
|
deferred.resolve();
|
|
}, 50);
|
|
}
|
|
}
|
|
}, 'collection', 'test');
|
|
|
|
await onDrop(
|
|
'collection',
|
|
'L' + group.libraryID,
|
|
[collectionA.id],
|
|
deferred.promise
|
|
);
|
|
|
|
Zotero.Notifier.unregisterObserver(observerID);
|
|
|
|
var pred = Zotero.Relations.linkedObjectPredicate;
|
|
var newCollectionA = await Zotero.URI.getURICollection(collectionA.getRelations()[pred][0]);
|
|
var newCollectionB = await Zotero.URI.getURICollection(collectionB.getRelations()[pred][0]);
|
|
var newItemA = await Zotero.URI.getURIItem(itemA.getRelations()[pred][0]);
|
|
var newItemB = await Zotero.URI.getURIItem(itemB.getRelations()[pred][0]);
|
|
assert.equal(newCollectionA.libraryID, group.libraryID);
|
|
assert.equal(newCollectionB.libraryID, group.libraryID);
|
|
assert.equal(newCollectionB.parentID, newCollectionA.id);
|
|
assert.equal(newItemA.libraryID, group.libraryID);
|
|
assert.equal(newItemB.libraryID, group.libraryID);
|
|
assert.isTrue(newCollectionA.hasItem(newItemA));
|
|
assert.isTrue(newCollectionB.hasItem(newItemB));
|
|
assert.isFalse(newCollectionA.hasItem(newItemB));
|
|
assert.isFalse(newCollectionB.hasItem(newItemA));
|
|
})
|
|
|
|
it("should copy a collection from a read-only group to another group", async function () {
|
|
var group1 = await createGroup();
|
|
var collection = await createDataObject('collection', { libraryID: group1.libraryID });
|
|
var item = await createDataObject('item', { libraryID: group1.libraryID, collections: [collection.id] });
|
|
group1.editable = false;
|
|
await group1.saveTx();
|
|
|
|
var group2 = await createGroup();
|
|
|
|
await select(win, collection);
|
|
|
|
await onDrop('collection', 'L' + group2.libraryID, [collection.id]);
|
|
|
|
assert.isFalse(await collection.getLinkedCollection(group2.libraryID));
|
|
// New collection should link back to original
|
|
assert.ok(await collection.getLinkedCollection(group2.libraryID, true));
|
|
|
|
assert.isFalse(await item.getLinkedItem(group2.libraryID));
|
|
// New item should link back to original
|
|
assert.ok(await item.getLinkedItem(group2.libraryID, true));
|
|
|
|
await group1.eraseTx();
|
|
await group2.eraseTx();
|
|
});
|
|
|
|
it("can delete a collection by dropping it into the trash", async function () {
|
|
var collection = await createDataObject('collection');
|
|
var deferred = Zotero.Promise.defer();
|
|
deferred.resolve();
|
|
await onDrop('collection', 'T' + userLibraryID, [collection.id], deferred.promise);
|
|
assert.isTrue(collection.deleted);
|
|
})
|
|
})
|
|
|
|
|
|
describe("with feed items", function () {
|
|
var { HttpServer } = ChromeUtils.importESModule("chrome://remote/content/server/httpd.sys.mjs");;
|
|
|
|
const httpdPort = 16214;
|
|
var httpd;
|
|
|
|
before(async function () {
|
|
httpd = new HttpServer();
|
|
httpd.start(httpdPort);
|
|
});
|
|
|
|
after(async function () {
|
|
await new Promise(resolve => httpd.stop(resolve));
|
|
Zotero.Prefs.clear('downloadAssociatedFiles');
|
|
Zotero.Prefs.clear('automaticSnapshots');
|
|
});
|
|
|
|
it("should add a translated feed item retrieved from a URL", async function () {
|
|
// Disable file/snapshot saving to avoid external network requests and async
|
|
// attachment processing
|
|
Zotero.Prefs.set('downloadAssociatedFiles', false);
|
|
Zotero.Prefs.set('automaticSnapshots', false);
|
|
|
|
// Serve the feed entry webpage via localhost
|
|
const urlPath = "/journalArticle-single.html";
|
|
const url = `http://localhost:${httpdPort}` + urlPath;
|
|
httpd.registerFile(
|
|
urlPath,
|
|
Zotero.File.pathToFile(OS.Path.join(
|
|
getTestDataDirectory().path, 'metadata', 'journalArticle-single.html'
|
|
))
|
|
);
|
|
|
|
var feed = await createFeed();
|
|
var collection = await createDataObject('collection', false, { skipSelect: true });
|
|
var feedItem = await createDataObject('feedItem', {libraryID: feed.libraryID}, { skipSelect: true });
|
|
feedItem.setField('url', url);
|
|
await feedItem.saveTx();
|
|
var translateFn = sinon.spy(feedItem, 'translate');
|
|
|
|
var ids = ((await onDrop('item', 'C' + collection.id, [feedItem.id]))).ids;
|
|
|
|
// Check that the translated item was the one that was created after drag
|
|
var item = await translateFn.returnValues[0];
|
|
assert.ok(item, 'Translation should return an item');
|
|
|
|
await select(win, collection);
|
|
// TEMP: Some extra asserts to debug flakiness in CI
|
|
var selectedTreeRow = win.ZoteroPane.getCollectionTreeRows()[0];
|
|
assert.ok(selectedTreeRow, 'a collection tree row should be selected');
|
|
assert.isTrue(selectedTreeRow.isCollection(),
|
|
'selected tree row should be a collection');
|
|
assert.equal(selectedTreeRow.ref.id, collection.id,
|
|
'selected collection tree row should match created collection');
|
|
|
|
var itemsView = win.ZoteroPane.itemsView;
|
|
assert.equal(itemsView.rowCount, 1);
|
|
var treeRow = itemsView.getRow(0);
|
|
assert.equal(treeRow.ref.id, item.id);
|
|
})
|
|
})
|
|
|
|
describe("with searches", function () {
|
|
it("can delete a saved search by dropping it into the trash", async function () {
|
|
var search = await createDataObject('search');
|
|
var deferred = Zotero.Promise.defer();
|
|
deferred.resolve();
|
|
await onDrop('search', 'T' + userLibraryID, [search.id], deferred.promise);
|
|
assert.isTrue(search.deleted);
|
|
})
|
|
});
|
|
})
|
|
|
|
describe("Feeds pseudo-library", function () {
|
|
beforeEach(async function () {
|
|
for (let feed of Zotero.Feeds.getAll()) {
|
|
await feed.eraseTx();
|
|
}
|
|
});
|
|
|
|
it("should contain feed items from all feeds", async function () {
|
|
let feed1 = await createFeed();
|
|
let feed2 = await createFeed();
|
|
let feedItem1 = await createDataObject('feedItem', { libraryID: feed1.libraryID }, { skipSelect: true });
|
|
let feedItem2 = await createDataObject('feedItem', { libraryID: feed2.libraryID }, { skipSelect: true });
|
|
await cv.selectFeeds();
|
|
await waitForItemsLoad(win);
|
|
|
|
let itemsView = zp.itemsView;
|
|
assert.equal(itemsView.rowCount, 2);
|
|
assert.equal(itemsView.getRow(0).ref.id, feedItem2.id);
|
|
assert.equal(itemsView.getRow(1).ref.id, feedItem1.id);
|
|
});
|
|
|
|
it("should be filterable", async function () {
|
|
let feed1 = await createFeed();
|
|
let feed2 = await createFeed();
|
|
let feedItem1 = await createDataObject('feedItem', { libraryID: feed1.libraryID, setTitle: true }, { skipSelect: true });
|
|
let feedItem2 = await createDataObject('feedItem', { libraryID: feed2.libraryID, setTitle: true }, { skipSelect: true });
|
|
await cv.selectFeeds();
|
|
await waitForItemsLoad(win);
|
|
|
|
var quickSearch = win.document.getElementById('zotero-tb-search-textbox');
|
|
quickSearch.value = feedItem1.getField('title');
|
|
quickSearch.doCommand();
|
|
|
|
let itemsView = zp.itemsView;
|
|
await itemsView._refreshPromise;
|
|
assert.equal(itemsView.rowCount, 1);
|
|
assert.equal(itemsView.getRow(0).ref.id, feedItem1.id);
|
|
});
|
|
|
|
it("should be bold if any feed items are unread", async function () {
|
|
let feed1 = await createFeed();
|
|
let feed2 = await createFeed();
|
|
let feedItem1 = await createDataObject('feedItem', { libraryID: feed1.libraryID, setTitle: true }, { skipSelect: true });
|
|
let feedItem2 = await createDataObject('feedItem', { libraryID: feed2.libraryID, setTitle: true }, { skipSelect: true });
|
|
|
|
await feedItem1.toggleRead(true);
|
|
|
|
// Unread count is automatically updated on feed refresh, but we need to do it manually here
|
|
await feed1.updateUnreadCount();
|
|
await feed2.updateUnreadCount();
|
|
|
|
assert.equal(cv.getRow(cv.getRowIndexByID('F1')).ref.unreadCount, 1);
|
|
assert.lengthOf(win.document.querySelectorAll('#zotero-collections-tree .row.unread'), 2);
|
|
});
|
|
});
|
|
|
|
describe("#setFilter()", function () {
|
|
var collection1, collection2, collection3, collection4, collection5, collection6, collection7, collection8;
|
|
var search1, search2, feed1, feed2;
|
|
var allRows = [];
|
|
let keyboardClick = (key) => {
|
|
return new KeyboardEvent('keydown', {
|
|
key: key,
|
|
code: key,
|
|
bubbles: true,
|
|
});
|
|
};
|
|
before(async function () {
|
|
// Delete all previously added collections, feeds, searches
|
|
for (let col of Zotero.Collections.getByLibrary(userLibraryID)) {
|
|
await col.eraseTx();
|
|
}
|
|
await clearFeeds();
|
|
for (let s of Zotero.Searches.getByLibrary(userLibraryID)) {
|
|
await s.eraseTx();
|
|
}
|
|
// Display the collection search bar
|
|
win.document.getElementById("zotero-tb-collections-search").click();
|
|
// Do not hide the search panel on blur
|
|
win.document.getElementById("zotero-collections-search").removeEventListener('blur', zp.hideCollectionSearch);
|
|
|
|
feed1 = await createFeed({ name: "feed_1 " });
|
|
feed2 = await createFeed({ name: "feed_2" });
|
|
|
|
collection1 = await createDataObject('collection', { name: "collection_level_one", libraryID: userLibraryID });
|
|
collection2 = await createDataObject('collection', { name: "collection_level_two_1", parentID: collection1.id, libraryID: userLibraryID });
|
|
collection3 = await createDataObject('collection', { name: "collection_level_two_2", parentID: collection1.id, libraryID: userLibraryID });
|
|
collection4 = await createDataObject('collection', { name: "collection_level_three_1", parentID: collection2.id, libraryID: userLibraryID });
|
|
collection5 = await createDataObject('collection', { name: "collection_level_three_11", parentID: collection2.id, libraryID: userLibraryID });
|
|
collection6 = await createDataObject('collection', { name: "collection_level_one_1", libraryID: userLibraryID });
|
|
collection7 = await createDataObject('collection', { name: "collection_level_two_21", parentID: collection6.id, libraryID: userLibraryID });
|
|
collection8 = await createDataObject('collection', { name: "collection_level_two_22", parentID: collection6.id, libraryID: userLibraryID });
|
|
search1 = await createDataObject('search', { name: "search_1", libraryID: userLibraryID });
|
|
search2 = await createDataObject('search', { name: "search_2", libraryID: userLibraryID });
|
|
allRows = [feed1, feed2, collection1, collection2, collection3, collection4, collection5, collection6, collection7, collection8, search1, search2];
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
// Empty filter and let it settle
|
|
await cv.setFilter("");
|
|
});
|
|
|
|
after(async function () {
|
|
await cv.setFilter("");
|
|
});
|
|
|
|
for (let type of ['collection', 'search', 'feed']) {
|
|
|
|
it(`should show only ${type} matching the filter`, async function () {
|
|
await cv.setFilter(type);
|
|
let displayedRowNames = cv._rows.filter(row => row.type == type).map(row => row.getName());
|
|
let expectedRowNames = allRows.filter(row => row.name.includes(type)).map(row => row.name);
|
|
assert.sameMembers(displayedRowNames, expectedRowNames);
|
|
});
|
|
}
|
|
|
|
it('should match an accented collection name from an unaccented filter', async function () {
|
|
var collection = await createDataObject('collection', { name: "zdiacrésumé", libraryID: userLibraryID });
|
|
await cv.setFilter("zdiacresume");
|
|
let displayedNames = cv._rows.filter(row => row.type == "collection").map(row => row.ref.name);
|
|
assert.include(displayedNames, "zdiacrésumé");
|
|
await cv.setFilter("");
|
|
await collection.eraseTx();
|
|
});
|
|
|
|
it('should show non-passing entries whose children pass the filter', async function () {
|
|
await cv.setFilter("three");
|
|
let displayedRowNames = cv._rows.filter(row => row.type == "collection").map(row => row.ref.name);
|
|
let expectedNames = [
|
|
"collection_level_one",
|
|
"collection_level_two_1",
|
|
"collection_level_three_1",
|
|
"collection_level_three_11"
|
|
];
|
|
assert.sameMembers(displayedRowNames, expectedNames);
|
|
});
|
|
|
|
it('should not move focus from selected collection during filtering', async function () {
|
|
await cv.selectByID("C" + collection5.id);
|
|
await cv.setFilter("three");
|
|
let focusedRow = cv.getRow(cv.selection.focused);
|
|
assert.equal(focusedRow.id, "C" + collection5.id);
|
|
await cv.setFilter("two");
|
|
focusedRow = cv.getRow(cv.selection.focused);
|
|
assert.equal(focusedRow.id, "C" + collection5.id);
|
|
});
|
|
|
|
it('should collapse collections collapsed before filtering', async function () {
|
|
// Collapse top level collections 1 and 6
|
|
for (let c of [collection1, collection6]) {
|
|
let index = cv.getRowIndexByID("C" + c.id);
|
|
let row = cv.getRow(index);
|
|
if (row.isOpen) {
|
|
await cv.toggleOpenState(index);
|
|
}
|
|
}
|
|
|
|
await cv.setFilter(collection5.name);
|
|
|
|
// Collection 1 and 2 have a matching child, so they are opened
|
|
let colOneRow = cv.getRow(cv.getRowIndexByID("C" + collection1.id));
|
|
assert.isTrue(colOneRow.isOpen);
|
|
let colTwoRow = cv.getRow(cv.getRowIndexByID("C" + collection2.id));
|
|
assert.isTrue(colTwoRow.isOpen);
|
|
// Collection 6 has no matches, it is filtered out
|
|
let colSixRowIndex = cv.getRowIndexByID("C" + collection6.id);
|
|
assert.isFalse(colSixRowIndex);
|
|
|
|
// Empty the filter
|
|
await cv.setFilter("");
|
|
|
|
// Collection 1 and 6 should remain collapsed as before filtering
|
|
colOneRow = cv.getRow(cv.getRowIndexByID("C" + collection1.id));
|
|
assert.isFalse(colOneRow.isOpen);
|
|
let colSixRow = cv.getRow(cv.getRowIndexByID("C" + collection6.id));
|
|
assert.isFalse(colSixRow.isOpen);
|
|
});
|
|
|
|
for (let type of ['collection', 'search']) {
|
|
|
|
it(`should only hide ${type} if it's renamed to not match the filter`, async function () {
|
|
await cv.setFilter(type);
|
|
let objectToSelect = type == 'collection' ? collection5 : search2;
|
|
objectToSelect.name += "_updated";
|
|
await objectToSelect.saveTx();
|
|
let displayedRowNames = cv._rows.map(row => row.getName());
|
|
assert.include(displayedRowNames, objectToSelect.name);
|
|
|
|
objectToSelect.name = "not_matching_filter";
|
|
await objectToSelect.saveTx();
|
|
displayedRowNames = cv._rows.map(row => row.getName());
|
|
assert.notInclude(displayedRowNames, objectToSelect.name);
|
|
});
|
|
}
|
|
|
|
for (let type of ['collection', 'search']) {
|
|
|
|
it(`should only add ${type} if its name matches the filter`, async function () {
|
|
await cv.setFilter(type);
|
|
let newCollection = await createDataObject(type, { name: `new_${type}`, libraryID: userLibraryID });
|
|
|
|
let displayedRowNames = cv._rows.map(row => row.ref.name);
|
|
assert.include(displayedRowNames, newCollection.name);
|
|
|
|
newCollection = await createDataObject(type, { name: `not_passing_${type.substring(1)}`, libraryID: userLibraryID });
|
|
|
|
displayedRowNames = cv._rows.map(row => row.ref.name);
|
|
assert.notInclude(displayedRowNames, newCollection.name);
|
|
});
|
|
}
|
|
|
|
it(`should focus selected collection on Enter if it matches filter`, async function () {
|
|
await cv.selectByID(`C${collection3.id}`);
|
|
win.document.getElementById("zotero-collections-search").value = "_2";
|
|
await cv.setFilter("_2");
|
|
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
|
assert.equal(cv.getSelectedCollections(true)[0], collection3.id);
|
|
assert.equal(win.document.activeElement.id, 'collection-tree');
|
|
});
|
|
|
|
it(`should focus first matching collection on Enter if selected collection does not match filter`, async function () {
|
|
await cv.selectByID(`C${collection2.id}`);
|
|
win.document.getElementById("zotero-collections-search").focus();
|
|
win.document.getElementById("zotero-collections-search").value = "_2";
|
|
await cv.setFilter("_2");
|
|
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
|
// Wait for the selection to go through
|
|
await Zotero.Promise.delay(100);
|
|
assert.equal(cv.getSelectedCollections(true)[0], collection3.id);
|
|
assert.equal(win.document.activeElement.id, 'collection-tree');
|
|
});
|
|
|
|
it(`should not move focus from collection filter on Enter if no rows pass the filter`, async function () {
|
|
await cv.selectByID(`C${collection3.id}`);
|
|
win.document.getElementById("zotero-collections-search").focus();
|
|
win.document.getElementById("zotero-collections-search").value = "Not matching anything";
|
|
await cv.setFilter("Not matching anything");
|
|
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
|
assert.equal(win.document.activeElement.id, 'zotero-collections-search');
|
|
});
|
|
|
|
it(`should skip context rows on arrow up/down`, async function () {
|
|
await cv.selectByID(`C${collection2.id}`);
|
|
await cv.setFilter("_2");
|
|
await cv.focusFirstMatchingRow();
|
|
// Skip collection6 that does not match on the way up and down
|
|
for (let col of [collection3, collection7, collection8]) {
|
|
assert.equal(cv.getSelectedCollections(true)[0], col.id);
|
|
await cv.focusNextMatchingRow(cv.selection.focused);
|
|
}
|
|
await cv.selectByID(`C${collection8.id}`);
|
|
for (let col of [collection8, collection7, collection3]) {
|
|
assert.equal(cv.getSelectedCollections(true)[0], col.id);
|
|
await cv.focusNextMatchingRow(cv.selection.focused, true);
|
|
}
|
|
});
|
|
|
|
it(`should clear filter on Escape from collectionTree`, async function () {
|
|
await cv.selectByID(`C${collection2.id}`);
|
|
let colTree = win.document.getElementById('collection-tree');
|
|
await cv.setFilter("_2");
|
|
cv.focusFirstMatchingRow();
|
|
colTree.dispatchEvent(keyboardClick("Escape"));
|
|
assert.equal(cv._filter, "");
|
|
assert.equal(cv.getSelectedCollections(true)[0], collection2.id);
|
|
});
|
|
});
|
|
|
|
describe("#setHighlightedRows", function () {
|
|
it("should not invalidate collectionTree if nothing was changed", async function () {
|
|
let collection = await createDataObject('collection');
|
|
let invalidateSpy = sinon.spy(cv.tree, 'invalidate');
|
|
await cv.setHighlightedRows(['C' + collection.id]);
|
|
await cv.setHighlightedRows(['C' + collection.id]);
|
|
// Only the first call should invalidate the tree
|
|
assert.isTrue(invalidateSpy.calledOnce);
|
|
invalidateSpy.restore();
|
|
});
|
|
})
|
|
|
|
describe("removed single-selection methods", function () {
|
|
var buildFlags = ['isBetaBuild', 'isDevBuild', 'isSourceBuild'];
|
|
var savedBuildFlags;
|
|
|
|
beforeEach(function () {
|
|
savedBuildFlags = buildFlags.map(flag => Zotero[flag]);
|
|
// Tests run in a source build, where these methods always throw
|
|
buildFlags.forEach(flag => Zotero[flag] = false);
|
|
});
|
|
|
|
afterEach(function () {
|
|
buildFlags.forEach((flag, i) => Zotero[flag] = savedBuildFlags[i]);
|
|
});
|
|
|
|
it("should return the selected row when focus is on a different row", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
var index1 = cv.getRowIndexByID(collection1.treeViewID);
|
|
var index2 = cv.getRowIndexByID(collection2.treeViewID);
|
|
|
|
cv.selection.select(index1);
|
|
// Ctrl/Cmd-arrow moves focus without changing the selection
|
|
cv.selection.focused = index2;
|
|
|
|
assert.equal(cv.getSelectedCollection(), collection1);
|
|
assert.equal(zp.getSelectedCollection(), collection1);
|
|
assert.equal(zp.getCollectionTreeRow().ref, collection1);
|
|
});
|
|
|
|
it("should throw when multiple rows are selected", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
cv.selection.select(cv.getRowIndexByID(collection1.treeViewID));
|
|
cv.selection.toggleSelect(cv.getRowIndexByID(collection2.treeViewID));
|
|
|
|
assert.throws(() => cv.getSelectedCollection());
|
|
assert.throws(() => zp.getSelectedLibraryID());
|
|
});
|
|
|
|
it("should throw in pre-release builds with a single row selected", async function () {
|
|
Zotero.isBetaBuild = true;
|
|
var collection = await createDataObject('collection');
|
|
cv.selection.select(cv.getRowIndexByID(collection.treeViewID));
|
|
|
|
assert.throws(() => cv.getSelectedCollection());
|
|
assert.throws(() => zp.getSelectedCollection());
|
|
});
|
|
});
|
|
})
|