mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Bug 2008041 made them html-style boolean attributes, so presence alone
means true and setAttribute('disabled', false) now disables the element.
Switch the sites that can be passed a falsy value to toggleAttribute(),
read them with hasAttribute(), and drop a disabled="false" from markup.
Sites that only ever pass true are left alone.
2176 lines
82 KiB
JavaScript
2176 lines
82 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 } = {}) {
|
|
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: 'copyMove',
|
|
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;
|
|
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 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();
|
|
});
|
|
})
|
|
})
|