2d3375e9f6
- revert change from2401a34031
that only loads un-trashed collections in _loadCollections. If an item only belongs to deleted collections, item._loaded.collections = true from _loadCollections will never run, so an exception will be thrown in item.toJSON() when syncing happens. Instead, to address the problem of item.getCollections() having stale data #4307, add 'includeTrashed' parameter to item.getCollections() based on which item._collections will be filtered. Fixes: #4346 - revert earlier, no more necessary, changes froma532cfb475
to not alter item._collections cache when collections are being trashed or restored. Collection is removed from item._collections only when it is permanently erased. - removed unnecessary test checking for consistent item._collections value before and after reload, since item._collections is no longer modified - fix encountered bug where a trashed child collection is not unloaded if a parent collection is erased without being trashed first. - tweaked Zotero.Search sql construction to count items that only belong to trashed collections into 'unfiled'. Fixes: #4347 --------- Co-authored-by: Dan Stillman
478 lines
18 KiB
JavaScript
478 lines
18 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.Collection", function() {
|
|
describe("#save()", function () {
|
|
it("should save a new collection", function* () {
|
|
var name = "Test";
|
|
var collection = new Zotero.Collection;
|
|
collection.name = name;
|
|
var id = yield collection.saveTx();
|
|
assert.equal(collection.name, name);
|
|
collection = yield Zotero.Collections.getAsync(id);
|
|
assert.equal(collection.name, name);
|
|
});
|
|
})
|
|
|
|
describe("#erase()", function () {
|
|
it("should delete a collection but not its descendant item by default", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [collection.id] });
|
|
assert.isTrue(collection.hasItem(item.id));
|
|
|
|
yield collection.eraseTx();
|
|
|
|
assert.isFalse((yield Zotero.Items.getAsync(item.id)).deleted);
|
|
})
|
|
|
|
it("should delete a collection and trash its descendant items with deleteItems: true", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item1 = yield createDataObject('item', { collections: [collection.id] });
|
|
var item2 = yield createDataObject('item', { collections: [collection.id] });
|
|
assert.isTrue(collection.hasItem(item1.id));
|
|
assert.isTrue(collection.hasItem(item2.id));
|
|
|
|
yield collection.eraseTx({ deleteItems: true });
|
|
|
|
assert.isTrue((yield Zotero.Items.getAsync(item1.id)).deleted);
|
|
assert.isTrue((yield Zotero.Items.getAsync(item2.id)).deleted);
|
|
});
|
|
|
|
it("should clear collection from item cache", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [collection.id] });
|
|
assert.lengthOf(item.getCollections(), 1);
|
|
yield collection.eraseTx();
|
|
assert.lengthOf(item.getCollections(), 0);
|
|
});
|
|
|
|
it("should clear subcollection from descendent item cache", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var subcollection = yield createDataObject('collection', { parentID: collection.id });
|
|
var item = yield createDataObject('item', { collections: [subcollection.id] });
|
|
assert.lengthOf(item.getCollections(), 1);
|
|
yield collection.eraseTx();
|
|
assert.lengthOf(item.getCollections(), 0);
|
|
});
|
|
|
|
it("should clear collection from item cache in deleteItems mode", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [collection.id] });
|
|
assert.lengthOf(item.getCollections(), 1);
|
|
yield collection.eraseTx({ deleteItems: true });
|
|
assert.lengthOf(item.getCollections(), 0);
|
|
});
|
|
|
|
it("should apply 'skipDeleteLog: true' to subcollections", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
var collection3 = await createDataObject('collection', { parentID: collection2.id });
|
|
|
|
await collection1.eraseTx({ skipDeleteLog: true });
|
|
|
|
var deleted = await Zotero.Sync.Data.Local.getDeleted('collection', collection1.libraryID);
|
|
|
|
// No collections should be in the delete log
|
|
assert.notInclude(deleted, collection1.key);
|
|
assert.notInclude(deleted, collection2.key);
|
|
assert.notInclude(deleted, collection3.key);
|
|
});
|
|
|
|
it("should send deleted collections to trash", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
var collection3 = await createDataObject('collection', { parentID: collection2.id });
|
|
|
|
collection1.deleted = true;
|
|
await collection1.saveTx();
|
|
|
|
var deleted = await Zotero.Collections.getDeleted(collection1.libraryID, true);
|
|
|
|
assert.include(deleted, collection1.id);
|
|
assert.include(deleted, collection2.id);
|
|
assert.include(deleted, collection3.id);
|
|
});
|
|
|
|
it("should restore deleted collection", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
var item1 = await createDataObject('item', { collections: [collection1.id, collection2.id] });
|
|
|
|
assert.include(item1.getCollections(), collection1.id);
|
|
|
|
collection1.deleted = true;
|
|
await collection1.saveTx();
|
|
|
|
// Trashed collection does not count as one of item's containers
|
|
assert.notInclude(item1.getCollections(), collection1.id);
|
|
// But it should still return it if includeTrashed=true is passed
|
|
assert.include(item1.getCollections(true), collection1.id);
|
|
|
|
// Restore deleted collection
|
|
collection1.deleted = false;
|
|
await collection1.saveTx();
|
|
|
|
var deleted = await Zotero.Collections.getDeleted(collection1.libraryID, true);
|
|
|
|
// Collection is restored from trash
|
|
assert.notInclude(deleted, collection1.id);
|
|
|
|
// Item belongs to the restored collection
|
|
assert.include(item1.getCollections(), collection1.id);
|
|
});
|
|
|
|
it("should permanently delete collections from trash", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
var collection3 = await createDataObject('collection', { parentID: collection2.id, deleted: true });
|
|
var item = await createDataObject('item', { collections: [collection1.id, collection2.id, collection3.id] });
|
|
|
|
await collection1.eraseTx();
|
|
|
|
assert.equal(await Zotero.Collections.getAsync(collection1.id), false);
|
|
assert.equal(await Zotero.Collections.getAsync(collection2.id), false);
|
|
assert.equal(await Zotero.Collections.getAsync(collection3.id), false);
|
|
|
|
// Erased collections are fully removed as item's containers
|
|
assert.equal(item.getCollections().length, 0);
|
|
assert.equal(item.getCollections(true).length, 0);
|
|
});
|
|
})
|
|
|
|
describe("#version", function () {
|
|
it("should set object version", function* () {
|
|
var version = 100;
|
|
var collection = new Zotero.Collection
|
|
collection.version = version;
|
|
collection.name = "Test";
|
|
var id = yield collection.saveTx();
|
|
assert.equal(collection.version, version);
|
|
collection = yield Zotero.Collections.getAsync(id);
|
|
assert.equal(collection.version, version);
|
|
});
|
|
})
|
|
|
|
describe("#parentKey", function () {
|
|
it("should set parent collection for new collections", function* () {
|
|
var parentCol = new Zotero.Collection
|
|
parentCol.name = "Parent";
|
|
var parentID = yield parentCol.saveTx();
|
|
var {libraryID, key: parentKey} = Zotero.Collections.getLibraryAndKeyFromID(parentID);
|
|
|
|
var col = new Zotero.Collection
|
|
col.name = "Child";
|
|
col.parentKey = parentKey;
|
|
var id = yield col.saveTx();
|
|
assert.equal(col.parentKey, parentKey);
|
|
col = yield Zotero.Collections.getAsync(id);
|
|
assert.equal(col.parentKey, parentKey);
|
|
});
|
|
|
|
it("should change parent collection for existing collections", function* () {
|
|
// Create initial parent collection
|
|
var parentCol = new Zotero.Collection
|
|
parentCol.name = "Parent";
|
|
var parentID = yield parentCol.saveTx();
|
|
var {libraryID, key: parentKey} = Zotero.Collections.getLibraryAndKeyFromID(parentID);
|
|
|
|
// Create subcollection
|
|
var col = new Zotero.Collection
|
|
col.name = "Child";
|
|
col.parentKey = parentKey;
|
|
var id = yield col.saveTx();
|
|
|
|
// Create new parent collection
|
|
var newParentCol = new Zotero.Collection
|
|
newParentCol.name = "New Parent";
|
|
var newParentID = yield newParentCol.saveTx();
|
|
var {libraryID, key: newParentKey} = Zotero.Collections.getLibraryAndKeyFromID(newParentID);
|
|
|
|
// Change parent collection
|
|
col.parentKey = newParentKey;
|
|
yield col.saveTx();
|
|
assert.equal(col.parentKey, newParentKey);
|
|
col = yield Zotero.Collections.getAsync(id);
|
|
assert.equal(col.parentKey, newParentKey);
|
|
});
|
|
|
|
it("should not mark collection as unchanged if set to existing value", function* () {
|
|
// Create initial parent collection
|
|
var parentCol = new Zotero.Collection
|
|
parentCol.name = "Parent";
|
|
var parentID = yield parentCol.saveTx();
|
|
var {libraryID, key: parentKey} = Zotero.Collections.getLibraryAndKeyFromID(parentID);
|
|
|
|
// Create subcollection
|
|
var col = new Zotero.Collection
|
|
col.name = "Child";
|
|
col.parentKey = parentKey;
|
|
var id = yield col.saveTx();
|
|
|
|
// Set to existing parent
|
|
col.parentKey = parentKey;
|
|
assert.isFalse(col.hasChanged());
|
|
});
|
|
|
|
it("should not resave a collection with no parent if set to false", function* () {
|
|
var col = new Zotero.Collection
|
|
col.name = "Test";
|
|
var id = yield col.saveTx();
|
|
|
|
col.parentKey = false;
|
|
var ret = yield col.saveTx();
|
|
assert.isFalse(ret);
|
|
});
|
|
})
|
|
|
|
describe("#hasChildCollections()", function () {
|
|
it("should be false if child made top-level", function* () {
|
|
var collection1 = yield createDataObject('collection');
|
|
var collection2 = yield createDataObject('collection', { parentID: collection1.id });
|
|
|
|
assert.isTrue(collection1.hasChildCollections());
|
|
collection2.parentKey = false;
|
|
yield collection2.saveTx();
|
|
assert.isFalse(collection1.hasChildCollections());
|
|
})
|
|
|
|
it("should be false if child moved to another collection", function* () {
|
|
var collection1 = yield createDataObject('collection');
|
|
var collection2 = yield createDataObject('collection', { parentID: collection1.id });
|
|
var collection3 = yield createDataObject('collection');
|
|
|
|
assert.isTrue(collection1.hasChildCollections());
|
|
collection2.parentKey = collection3.key;
|
|
yield collection2.saveTx();
|
|
assert.isFalse(collection1.hasChildCollections());
|
|
});
|
|
|
|
it("should return false if all child collections are moved to trash", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
var collection3 = await createDataObject('collection', { parentID: collection1.id });
|
|
|
|
assert.isTrue(collection1.hasChildCollections());
|
|
collection2.deleted = true;
|
|
await collection2.saveTx();
|
|
assert.isTrue(collection1.hasChildCollections());
|
|
collection3.deleted = true;
|
|
await collection3.saveTx();
|
|
assert.isFalse(collection1.hasChildCollections());
|
|
});
|
|
|
|
it("should return true if child collection is in trash and includeTrashed is true", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id });
|
|
|
|
assert.isTrue(collection1.hasChildCollections(true));
|
|
collection2.deleted = true;
|
|
await collection2.saveTx();
|
|
assert.isTrue(collection1.hasChildCollections(true));
|
|
});
|
|
})
|
|
|
|
describe("#getChildCollections()", function () {
|
|
it("should include child collections", function* () {
|
|
var collection1 = yield createDataObject('collection');
|
|
var collection2 = yield createDataObject('collection', { parentID: collection1.id });
|
|
yield collection1.saveTx();
|
|
|
|
var childCollections = collection1.getChildCollections();
|
|
assert.lengthOf(childCollections, 1);
|
|
assert.equal(childCollections[0].id, collection2.id);
|
|
})
|
|
|
|
it("should not include collections that have been removed", function* () {
|
|
var collection1 = yield createDataObject('collection');
|
|
var collection2 = yield createDataObject('collection', { parentID: collection1.id });
|
|
yield collection1.saveTx();
|
|
|
|
collection2.parentID = false;
|
|
yield collection2.save()
|
|
|
|
var childCollections = collection1.getChildCollections();
|
|
assert.lengthOf(childCollections, 0);
|
|
})
|
|
|
|
it("should not include collections in trash by default", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id, deleted: true });
|
|
|
|
var childCollections = collection1.getChildCollections();
|
|
assert.lengthOf(childCollections, 0);
|
|
});
|
|
|
|
it("should include collections in trash if includeTrashed=true", async function () {
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection', { parentID: collection1.id, deleted: true });
|
|
|
|
var childCollections = collection1.getChildCollections(false, true);
|
|
assert.lengthOf(childCollections, 1);
|
|
});
|
|
|
|
it("should not include collections that have been deleted", function* () {
|
|
var collection1 = yield createDataObject('collection');
|
|
var collection2 = yield createDataObject('collection', { parentID: collection1.id });
|
|
yield collection1.saveTx();
|
|
|
|
yield collection2.eraseTx()
|
|
|
|
var childCollections = collection1.getChildCollections();
|
|
assert.lengthOf(childCollections, 0);
|
|
})
|
|
})
|
|
|
|
describe("#getChildItems()", function () {
|
|
it("should include child items", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = createUnsavedDataObject('item');
|
|
item.addToCollection(collection.key);
|
|
yield item.saveTx();
|
|
|
|
assert.lengthOf(collection.getChildItems(), 1);
|
|
})
|
|
|
|
it("should not include items in trash by default", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = createUnsavedDataObject('item');
|
|
item.deleted = true;
|
|
item.addToCollection(collection.key);
|
|
yield item.saveTx();
|
|
|
|
assert.lengthOf(collection.getChildItems(), 0);
|
|
})
|
|
|
|
it("should include items in trash if includeTrashed=true", function* () {
|
|
var collection = yield createDataObject('collection');
|
|
var item = createUnsavedDataObject('item');
|
|
item.deleted = true;
|
|
item.addToCollection(collection.key);
|
|
yield item.saveTx();
|
|
|
|
assert.lengthOf(collection.getChildItems(false, true), 1);
|
|
})
|
|
|
|
it("should not include removed items", function* () {
|
|
var col = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [ col.id ] });
|
|
assert.lengthOf(col.getChildItems(), 1);
|
|
item.setCollections([]);
|
|
yield item.saveTx();
|
|
Zotero.debug(col.getChildItems());
|
|
assert.lengthOf(col.getChildItems(), 0);
|
|
});
|
|
|
|
it("should not include deleted items", function* () {
|
|
var col = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [ col.id ] });
|
|
assert.lengthOf(col.getChildItems(), 1);
|
|
yield item.erase();
|
|
assert.lengthOf(col.getChildItems(), 0);
|
|
});
|
|
|
|
it("should not include items emptied from trash", function* () {
|
|
var col = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [ col.id ], deleted: true });
|
|
yield item.erase();
|
|
assert.lengthOf(col.getChildItems(), 0);
|
|
});
|
|
})
|
|
|
|
describe("#fromJSON()", function () {
|
|
it("should ignore unknown property in non-strict mode", function () {
|
|
var json = {
|
|
name: "Collection",
|
|
foo: "Bar"
|
|
};
|
|
var s = new Zotero.Collection();
|
|
s.fromJSON(json);
|
|
});
|
|
|
|
it("should throw on unknown property in strict mode", function () {
|
|
var json = {
|
|
name: "Collection",
|
|
foo: "Bar"
|
|
};
|
|
var s = new Zotero.Collection();
|
|
var f = () => {
|
|
s.fromJSON(json, { strict: true });
|
|
};
|
|
assert.throws(f, /^Unknown collection property/);
|
|
});
|
|
});
|
|
|
|
describe("#toJSON()", function () {
|
|
it("should set 'parentCollection' to false when cleared", function* () {
|
|
var col1 = yield createDataObject('collection');
|
|
var col2 = yield createDataObject('collection', { parentID: col1.id });
|
|
// Create initial JSON with parentCollection
|
|
var patchBase = col2.toJSON();
|
|
// Clear parent collection and regenerate JSON
|
|
col2.parentID = false;
|
|
yield col2.saveTx();
|
|
var json = col2.toJSON({ patchBase });
|
|
assert.isFalse(json.parentCollection);
|
|
});
|
|
});
|
|
|
|
describe("#getDescendents()", function () {
|
|
var collection0, collection1, collection2, collection3, item1, item2, item3;
|
|
|
|
before(function* () {
|
|
collection0 = yield createDataObject('collection');
|
|
item1 = yield createDataObject('item', { collections: [collection0.id] });
|
|
collection1 = yield createDataObject('collection', { parentKey: collection0.key });
|
|
item2 = yield createDataObject('item', { collections: [collection1.id] });
|
|
collection2 = yield createDataObject('collection', { parentKey: collection1.key });
|
|
collection3 = yield createDataObject('collection', { parentKey: collection1.key });
|
|
item3 = yield createDataObject('item', { collections: [collection2.id] });
|
|
item3.deleted = true;
|
|
yield item3.saveTx();
|
|
});
|
|
|
|
it("should return a flat array of collections and items", function* () {
|
|
var desc = collection0.getDescendents();
|
|
assert.lengthOf(desc, 5);
|
|
assert.sameMembers(
|
|
desc.map(x => x.type + ':' + x.id + ':' + (x.name || '') + ':' + x.parent),
|
|
[
|
|
'item:' + item1.id + '::' + collection0.id,
|
|
'item:' + item2.id + '::' + collection1.id,
|
|
'collection:' + collection1.id + ':' + collection1.name + ':' + collection0.id,
|
|
'collection:' + collection2.id + ':' + collection2.name + ':' + collection1.id,
|
|
'collection:' + collection3.id + ':' + collection3.name + ':' + collection1.id
|
|
]
|
|
);
|
|
});
|
|
|
|
it("should return nested arrays of collections and items", function* () {
|
|
var desc = collection0.getDescendents(true);
|
|
assert.lengthOf(desc, 2);
|
|
assert.sameMembers(
|
|
desc.map(x => x.type + ':' + x.id + ':' + (x.name || '') + ':' + x.parent),
|
|
[
|
|
'item:' + item1.id + '::' + collection0.id,
|
|
'collection:' + collection1.id + ':' + collection1.name + ':' + collection0.id,
|
|
]
|
|
);
|
|
var c = desc[0].type == 'collection' ? desc[0] : desc[1];
|
|
assert.lengthOf(c.children, 3);
|
|
assert.sameMembers(
|
|
c.children.map(x => x.type + ':' + x.id + ':' + (x.name || '') + ':' + x.parent),
|
|
[
|
|
'item:' + item2.id + '::' + collection1.id,
|
|
'collection:' + collection2.id + ':' + collection2.name + ':' + collection1.id,
|
|
'collection:' + collection3.id + ':' + collection3.name + ':' + collection1.id
|
|
]
|
|
);
|
|
});
|
|
|
|
it("should not include deleted items", function* () {
|
|
var col = yield createDataObject('collection');
|
|
var item = yield createDataObject('item', { collections: [col.id] });
|
|
assert.lengthOf(col.getDescendents(), 1);
|
|
yield item.eraseTx();
|
|
assert.lengthOf(col.getDescendents(), 0);
|
|
});
|
|
|
|
});
|
|
})
|