zotero/test/tests/dataObjectTest.js
Abe Jellinek 97dbcc6870 Restore timeout in dataObjectTest.js, fix test
OK, the timeout removed in 05e8da wasn't actually useless, but this test
was.

41706a changed erase() so it no longer forwards skipNotifier when
deleting child items. That means that we *would* get a notify() call for
the deleted child, so the test should have been failing.

However! Because of a bug in Bluebird's filtered catch() implementation,
Promise.TimeoutError wasn't recognized as an Error subclass, and it was
instead used as a predicate function, which would always pass.

So all errors were caught as "timeouts" and suppressed, making this test
always pass. Now it no longer always passes (but does pass with the
current behavior).
2025-07-30 22:30:57 -04:00

697 lines
20 KiB
JavaScript

"use strict";
describe("Zotero.DataObject", function () {
var types = ['collection', 'item', 'search'];
describe("#library", function () {
it("should return a Zotero.Library", async function () {
var item = await createDataObject('item');
assert.equal(item.library, Zotero.Libraries.userLibrary);
});
});
describe("#libraryID", function () {
it("should return a libraryID", async function () {
var item = await createDataObject('item');
assert.isNumber(item.libraryID);
assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
});
});
describe("#key", function () {
it("shouldn't update .loaded on get if unset", async function () {
for (let type of types) {
let param;
if (type == 'item') {
param = 'book';
}
let obj = new (Zotero[Zotero.Utilities.capitalize(type)])(param);
obj.libraryID = Zotero.Libraries.userLibraryID;
assert.isNull(obj.key, 'key is null for ' + type);
assert.isFalse(obj._loaded.primaryData, 'primary data not loaded for ' + type);
obj.key = Zotero.DataObjectUtilities.generateKey();
}
})
})
describe("#version", function () {
it("should be set to 0 after creating object", async function () {
for (let type of types) {
let obj = await createDataObject(type);
assert.equal(obj.version, 0);
await obj.eraseTx();
}
})
it("should be set after creating object", async function () {
for (let type of types) {
let obj = await createDataObject(type, { version: 1234 });
assert.equal(obj.version, 1234, type + " version mismatch");
await obj.eraseTx();
}
})
})
describe("#synced", function () {
it("should be set to false after creating object", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = await obj.saveTx();
assert.isFalse(obj.synced);
await obj.eraseTx();
}
});
it("should be set to false after modifying object", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = await obj.saveTx();
obj.synced = true;
await obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
await obj.saveTx();
assert.isFalse(obj.synced);
await obj.eraseTx();
}
});
it("should be changed to true explicitly with no other changes", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = await obj.saveTx();
obj.synced = true;
await obj.saveTx();
assert.isTrue(obj.synced);
await obj.eraseTx();
}
});
it("should be changed to true explicitly with other field changes", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = await obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
obj.synced = true;
await obj.saveTx();
assert.isTrue(obj.synced);
await obj.eraseTx();
}
});
it("should remain at true if set explicitly", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
obj.synced = true;
var id = await obj.saveTx();
assert.isTrue(obj.synced);
if (type == 'item') {
obj.setField('title', 'test');
}
else {
obj.name = Zotero.Utilities.randomString();
}
obj.synced = true;
await obj.saveTx();
assert.isTrue(obj.synced);
await obj.eraseTx();
}
});
it("should be unchanged if skipSyncedUpdate passed", async function () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = await obj.saveTx();
obj.synced = true;
await obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
await obj.saveTx({
skipSyncedUpdate: true
});
assert.ok(obj.synced);
await obj.eraseTx();
}
});
})
describe("#deleted", function () {
it("should set trash status", async function () {
for (let type of types) {
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
// Set to true
var obj = await createDataObject(type);
assert.isFalse(obj.deleted, type);
obj.deleted = true;
// Sanity check for itemsTest#trash()
if (type == 'item') {
assert.isTrue(obj._changedData.deleted, type);
}
await obj.saveTx();
var id = obj.id;
await pluralClass.reload(id, false, true);
assert.isTrue(obj.deleted, type);
// Set to false
obj.deleted = false;
await obj.saveTx();
await pluralClass.reload(id, false, true);
assert.isFalse(obj.deleted, type);
}
});
});
describe("#loadPrimaryData()", function () {
it("should load unloaded primary data if partially set", async function () {
var objs = {};
for (let type of types) {
let obj = createUnsavedDataObject(type);
await obj.save({
skipCache: true
});
objs[type] = {
key: obj.key,
version: obj.version
};
}
for (let type of types) {
let obj = new (Zotero[Zotero.Utilities.capitalize(type)]);
obj.libraryID = Zotero.Libraries.userLibraryID;
obj.key = objs[type].key;
await obj.loadPrimaryData();
assert.equal(obj.version, objs[type].version);
}
});
it("shouldn't overwrite item type set in constructor", async function () {
var item = new Zotero.Item('book');
item.libraryID = Zotero.Libraries.userLibraryID;
item.key = Zotero.DataObjectUtilities.generateKey();
await item.loadPrimaryData();
var saved = await item.saveTx();
assert.ok(saved);
});
})
describe("#loadAllData()", function () {
it("should load data on a regular item", async function () {
var item = new Zotero.Item('book');
var id = await item.saveTx();
await item.loadAllData();
assert.throws(item.getNote.bind(item), 'getNote() can only be called on notes and attachments');
})
it("should load data on an attachment item", async function () {
var item = new Zotero.Item('attachment');
var id = await item.saveTx();
await item.loadAllData();
assert.equal(item.note, '');
})
it("should load data on a note item", async function () {
var item = new Zotero.Item('note');
var id = await item.saveTx();
await item.loadAllData();
assert.equal(item.note, '');
})
})
describe("#hasChanged()", function () {
it("should return false if 'synced' was set but unchanged and nothing else changed", async function () {
for (let type of types) {
// True
var obj = createUnsavedDataObject(type);
obj.synced = true;
var id = await obj.saveTx();
assert.isTrue(obj.synced);
obj.synced = true;
assert.isFalse(obj.hasChanged(), type + " shouldn't be changed");
// False
var obj = createUnsavedDataObject(type);
obj.synced = false;
var id = await obj.saveTx();
assert.isFalse(obj.synced);
obj.synced = false;
assert.isFalse(obj.hasChanged(), type + " shouldn't be changed");
}
})
it("should return true if 'synced' was set but unchanged and another primary field changed", async function () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
obj.synced = true;
await obj.saveTx();
obj.synced = true;
obj.version = 1234;
assert.isTrue(obj.hasChanged());
}
})
});
describe("#save()", function () {
it("should add new identifiers to cache", async function () {
for (let type of types) {
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
let obj = createUnsavedDataObject(type);
let id = await obj.saveTx();
let { libraryID, key } = objectsClass.getLibraryAndKeyFromID(id);
assert.typeOf(key, 'string');
assert.equal(objectsClass.getIDFromLibraryAndKey(libraryID, key), id);
}
})
it("should reset changed state on objects", async function () {
for (let type of types) {
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
let obj = createUnsavedDataObject(type);
await obj.saveTx();
assert.isFalse(obj.hasChanged());
}
})
it("should handle additional tag change in the middle of a save", async function () {
var item = await createDataObject('item');
item.setTags(['a']);
var deferred = Zotero.Promise.defer();
var origFunc = Zotero.Notifier.queue.bind(Zotero.Notifier);
sinon.stub(Zotero.Notifier, "queue").callsFake(function (event, type, ids, extraData) {
// Add a new tag after the first one has been added to the DB and before the save is
// finished. The changed state should've cleared before saving to the DB the first
// time, so the second setTags() should mark the item as changed and allow the new tag
// to be saved in the second saveTx().
if (event == 'add' && type == 'item-tag') {
item.setTags(['a', 'b']);
Zotero.Notifier.queue.restore();
deferred.resolve(item.saveTx());
}
origFunc(...arguments);
});
await Promise.all([item.saveTx(), deferred.promise]);
assert.sameMembers(item.getTags().map(o => o.tag), ['a', 'b']);
var tags = await Zotero.DB.columnQueryAsync(
"SELECT name FROM tags JOIN itemTags USING (tagID) WHERE itemID=?", item.id
);
assert.sameMembers(tags, ['a', 'b']);
});
describe("Edit Check", function () {
var group;
before(function* () {
group = yield createGroup({
editable: false
});
});
it("should disallow saving to read-only libraries", async function () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = await getPromiseError(item.saveTx());
assert.ok(e);
assert.include(e.message, "Cannot edit item");
});
it("should allow saving if skipEditCheck is passed", async function () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = await getPromiseError(item.saveTx({
skipEditCheck: true
}));
assert.isFalse(e);
});
it("should allow saving if skipAll is passed", async function () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = await getPromiseError(item.saveTx({
skipAll: true
}));
assert.isFalse(e);
});
});
})
describe("#erase()", function () {
it("shouldn't trigger notifier if skipNotifier is passed", async function () {
let observerIDs = [];
let promises = [];
for (let type of types) {
let obj = await createDataObject(type);
// For items, test automatic child item deletion
if (type == 'item') {
await createDataObject(type, { itemType: 'note', parentID: obj.id });
}
let deferred = Zotero.Promise.defer();
promises.push(deferred.promise);
observerIDs.push(Zotero.Notifier.registerObserver(
{
notify: function (event, type, ids) {
if (event == 'delete' && ids.includes(obj.id)) {
deferred.reject("Notifier called for erase on " + type);
}
}
},
type,
'test'
));
await obj.eraseTx({
skipNotifier: true
});
}
await Promise.race([
Promise.all(promises),
// Give notifier time to trigger
Zotero.Promise.delay(100),
]);
for (let id of observerIDs) {
Zotero.Notifier.unregisterObserver(id);
}
})
it("should delete object versions from sync cache", async function () {
for (let type of types) {
let obj = await createDataObject(type);
let libraryID = obj.libraryID;
let key = obj.key;
let json = obj.toJSON();
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
await obj.eraseTx();
let versions = await Zotero.Sync.Data.Local.getCacheObjectVersions(
type, libraryID, key
);
assert.lengthOf(versions, 0);
}
})
})
describe("#updateVersion()", function () {
it("should update the object version", async function () {
for (let type of types) {
let obj = await createDataObject(type);
assert.equal(obj.version, 0);
await obj.updateVersion(1234);
assert.equal(obj.version, 1234);
assert.isFalse(obj.hasChanged());
obj.synced = true;
assert.ok(obj.hasChanged());
await obj.updateVersion(1235);
assert.equal(obj.version, 1235);
assert.ok(obj.hasChanged());
await obj.eraseTx();
}
})
})
describe("#updateSynced()", function () {
it("should update the object sync status", async function () {
for (let type of types) {
let obj = await createDataObject(type);
assert.isFalse(obj.synced);
await obj.updateSynced(false);
assert.isFalse(obj.synced);
assert.isFalse(obj.hasChanged());
await obj.updateSynced(true);
assert.ok(obj.synced);
assert.isFalse(obj.hasChanged());
obj.version = 1234;
assert.ok(obj.hasChanged());
await obj.updateSynced(false);
assert.isFalse(obj.synced);
assert.ok(obj.hasChanged());
await obj.eraseTx();
}
})
it("should clear changed status", async function () {
var item = createUnsavedDataObject('item');
item.synced = true;
await item.saveTx();
// Only synced changed
item.synced = false;
assert.isTrue(item.hasChanged());
assert.isTrue(item._changed.primaryData.synced);
await item.updateSynced(true);
assert.isFalse(item.hasChanged());
// Should clear primary data change object
assert.isUndefined(item._changed.primaryData);
// Another primary field also changed
item.setField('dateModified', '2017-02-27 12:34:56');
item.synced = false;
assert.isTrue(item.hasChanged());
assert.isTrue(item._changed.primaryData.synced);
await item.updateSynced(true);
assert.isTrue(item.hasChanged());
// Should clear only 'synced' change status
assert.isUndefined(item._changed.primaryData.synced);
});
})
describe("Relations", function () {
var types = ['collection', 'item'];
function makeObjectURI(objectType) {
var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
return 'http://zotero.org/groups/1/' + objectTypePlural + '/'
+ Zotero.Utilities.generateObjectKey();
}
describe("#addRelation()", function () {
it("should add a relation to an object", async function () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
await obj.saveTx();
var relations = obj.getRelations();
assert.property(relations, predicate);
assert.include(relations[predicate], object);
}
})
})
describe("#removeRelation()", function () {
it("should remove a relation from an object", async function () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
await obj.saveTx();
obj.removeRelation(predicate, object);
await obj.saveTx();
assert.lengthOf(Object.keys(obj.getRelations()), 0);
}
})
})
describe("#hasRelation()", function () {
it("should return true if an object has a given relation", async function () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
await obj.saveTx();
assert.ok(obj.hasRelation(predicate, object));
}
})
})
describe("#setRelations()", function () {
it("shouldn't allow invalid 'relations' predicates", async function () {
var item = new Zotero.Item("book");
assert.throws(() => {
item.setRelations({
"0": ["http://example.com/foo"]
});
});
});
});
describe("#_getLinkedObject()", function () {
it("should return a linked object in another library", async function () {
var group = await getGroup();
var item1 = await createDataObject('item');
var item2 = await createDataObject('item', { libraryID: group.libraryID });
var item2URI = Zotero.URI.getItemURI(item2);
await item2.addLinkedItem(item1);
var linkedItem = await item1.getLinkedItem(item2.libraryID);
assert.equal(linkedItem.id, item2.id);
})
it("shouldn't return a linked item in the trash in another library", async function () {
var group = await getGroup();
var item1 = await createDataObject('item');
var item2 = await createDataObject('item', { libraryID: group.libraryID });
var item2URI = Zotero.URI.getItemURI(item2);
await item2.addLinkedItem(item1);
item2.deleted = true;
await item2.saveTx();
var linkedItem = await item1.getLinkedItem(item2.libraryID);
assert.isFalse(linkedItem);
})
it("shouldn't return reverse linked objects by default", async function () {
var group = await getGroup();
var item1 = await createDataObject('item');
var item1URI = Zotero.URI.getItemURI(item1);
var item2 = await createDataObject('item', { libraryID: group.libraryID });
await item2.addLinkedItem(item1);
var linkedItem = await item2.getLinkedItem(item1.libraryID);
assert.isFalse(linkedItem);
})
it("should return reverse linked objects with bidirectional flag", async function () {
var group = await getGroup();
var item1 = await createDataObject('item');
var item1URI = Zotero.URI.getItemURI(item1);
var item2 = await createDataObject('item', { libraryID: group.libraryID });
await item2.addLinkedItem(item1);
var linkedItem = await item2.getLinkedItem(item1.libraryID, true);
assert.equal(linkedItem.id, item1.id);
})
})
describe("#_addLinkedObject()", function () {
it("should add an owl:sameAs relation", async function () {
var group = await getGroup();
var item1 = await createDataObject('item');
var dateModified = item1.getField('dateModified');
var item2 = await createDataObject('item', { libraryID: group.libraryID });
var item2URI = Zotero.URI.getItemURI(item2);
await item2.addLinkedItem(item1);
var preds = item1.getRelationsByPredicate(Zotero.Relations.linkedObjectPredicate);
assert.include(preds, item2URI);
// Make sure Date Modified hasn't changed
assert.equal(item1.getField('dateModified'), dateModified);
})
})
});
describe("#fromJSON()", function () {
it("should remove object from trash if 'deleted' property not provided", async function () {
for (let type of types) {
let obj = await createDataObject(type, { deleted: true });
assert.isTrue(obj.deleted, type);
let json = obj.toJSON();
delete json.deleted;
obj.fromJSON(json);
await obj.saveTx();
assert.isFalse(obj.deleted, type);
}
});
});
describe("#toJSON()", function () {
it("should output 'deleted' as true", function () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
obj.deleted = true;
let json = obj.toJSON();
assert.isTrue(json.deleted, type);
}
});
it("shouldn't include 'deleted' if not set in default mode", function () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
let json = obj.toJSON();
assert.notProperty(json, 'deleted', type);
}
});
describe("'patch' mode", function () {
it("should include changed 'deleted' field", async function () {
for (let type of types) {
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
// True to false
let obj = createUnsavedDataObject(type)
obj.deleted = true;
let id = await obj.saveTx();
obj = await pluralClass.getAsync(id);
let patchBase = obj.toJSON();
obj.deleted = false;
let json = obj.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title, type);
assert.isFalse(json.deleted, type);
// False to true
obj = createUnsavedDataObject(type);
obj.deleted = false;
id = await obj.saveTx();
obj = await pluralClass.getAsync(id);
patchBase = obj.toJSON();
obj.deleted = true;
json = obj.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title, type);
assert.isTrue(json.deleted, type);
}
});
});
});
})