zotero/test/tests/dataObjectTest.js

578 lines
17 KiB
JavaScript

"use strict";
describe("Zotero.DataObject", function() {
var types = ['collection', 'item', 'search'];
describe("#library", function () {
it("should return a Zotero.Library", function* () {
var item = yield createDataObject('item');
assert.equal(item.library, Zotero.Libraries.userLibrary);
});
});
describe("#libraryID", function () {
it("should return a libraryID", function* () {
var item = yield 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", 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", function* () {
for (let type of types) {
let obj = yield createDataObject(type);
assert.equal(obj.version, 0);
yield obj.eraseTx();
}
})
it("should be set after creating object", function* () {
for (let type of types) {
let obj = yield createDataObject(type, { version: 1234 });
assert.equal(obj.version, 1234, type + " version mismatch");
yield obj.eraseTx();
}
})
})
describe("#synced", function () {
it("should be set to false after creating object", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = yield obj.saveTx();
assert.isFalse(obj.synced);
yield obj.eraseTx();
}
});
it("should be set to false after modifying object", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = yield obj.saveTx();
obj.synced = true;
yield obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
yield obj.saveTx();
assert.isFalse(obj.synced);
yield obj.eraseTx();
}
});
it("should be changed to true explicitly with no other changes", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = yield obj.saveTx();
obj.synced = true;
yield obj.saveTx();
assert.isTrue(obj.synced);
yield obj.eraseTx();
}
});
it("should be changed to true explicitly with other field changes", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = yield obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
obj.synced = true;
yield obj.saveTx();
assert.isTrue(obj.synced);
yield obj.eraseTx();
}
});
it("should remain at true if set explicitly", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
obj.synced = true;
var id = yield obj.saveTx();
assert.isTrue(obj.synced);
if (type == 'item') {
obj.setField('title', 'test');
}
else {
obj.name = Zotero.Utilities.randomString();
}
obj.synced = true;
yield obj.saveTx();
assert.isTrue(obj.synced);
yield obj.eraseTx();
}
});
it("should be unchanged if skipSyncedUpdate passed", function* () {
for (let type of types) {
var obj = createUnsavedDataObject(type);
var id = yield obj.saveTx();
obj.synced = 1;
yield obj.saveTx();
if (type == 'item') {
obj.setField('title', Zotero.Utilities.randomString());
}
else {
obj.name = Zotero.Utilities.randomString();
}
yield obj.saveTx({
skipSyncedUpdate: true
});
assert.ok(obj.synced);
yield obj.eraseTx();
}
});
})
describe("#loadPrimaryData()", function () {
it("should load unloaded primary data if partially set", function* () {
var objs = {};
for (let type of types) {
let obj = createUnsavedDataObject(type);
yield 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;
yield obj.loadPrimaryData();
assert.equal(obj.version, objs[type].version);
}
})
})
describe("#loadAllData()", function () {
it("should load data on a regular item", function* () {
var item = new Zotero.Item('book');
var id = yield item.saveTx();
yield 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", function* () {
var item = new Zotero.Item('attachment');
var id = yield item.saveTx();
yield item.loadAllData();
assert.equal(item.getNote(), '');
})
it("should load data on a note item", function* () {
var item = new Zotero.Item('note');
var id = yield item.saveTx();
yield item.loadAllData();
assert.equal(item.getNote(), '');
})
})
describe("#hasChanged()", function () {
it("should return false if 'synced' was set but unchanged and nothing else changed", function* () {
for (let type of types) {
// True
var obj = createUnsavedDataObject(type);
obj.synced = true;
var id = yield 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 = yield 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", function* () {
for (let type of types) {
let obj = createUnsavedDataObject(type);
obj.synced = true;
yield obj.saveTx();
obj.synced = true;
obj.version = 1234;
assert.isTrue(obj.hasChanged());
}
})
});
describe("#save()", function () {
it("should add new identifiers to cache", function* () {
for (let type of types) {
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
let obj = createUnsavedDataObject(type);
let id = yield 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", function* () {
for (let type of types) {
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
let obj = createUnsavedDataObject(type);
yield obj.saveTx();
assert.isFalse(obj.hasChanged());
}
})
it("should handle additional tag change in the middle of a save", function* () {
var item = yield createDataObject('item');
item.setTags(['a']);
var deferred = new 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);
});
yield Zotero.Promise.all([item.saveTx(), deferred.promise]);
assert.sameMembers(item.getTags().map(o => o.tag), ['a', 'b']);
var tags = yield 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", function* () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = yield getPromiseError(item.saveTx());
assert.ok(e);
assert.include(e.message, "read-only");
});
it("should allow saving if skipEditCheck is passed", function* () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = yield getPromiseError(item.saveTx({
skipEditCheck: true
}));
assert.isFalse(e);
});
it("should allow saving if skipAll is passed", function* () {
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
var e = yield getPromiseError(item.saveTx({
skipAll: true
}));
assert.isFalse(e);
});
});
describe("Options", function () {
describe("#skipAll", function () {
it("should include edit check", function* () {
});
});
});
})
describe("#erase()", function () {
it("shouldn't trigger notifier if skipNotifier is passed", function* () {
let observerIDs = [];
let promises = [];
for (let type of types) {
let obj = yield createDataObject(type);
// For items, test automatic child item deletion
if (type == 'item') {
yield createDataObject(type, { itemType: 'note', parentID: obj.id });
}
let deferred = Zotero.Promise.defer();
promises.push(deferred.promise);
observerIDs.push(Zotero.Notifier.registerObserver(
{
notify: function (event) {
if (event == 'delete') {
deferred.reject("Notifier called for erase on " + type);
}
}
},
type,
'test'
));
yield obj.eraseTx({
skipNotifier: true
});
}
yield Zotero.Promise.all(promises)
// Give notifier time to trigger
.timeout(100).catch(Zotero.Promise.TimeoutError, (e) => {})
for (let id of observerIDs) {
Zotero.Notifier.unregisterObserver(id);
}
})
it("should delete object versions from sync cache", function* () {
for (let type of types) {
let obj = yield createDataObject(type);
let libraryID = obj.libraryID;
let key = obj.key;
let json = obj.toJSON();
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
yield obj.eraseTx();
let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions(
type, libraryID, key
);
assert.lengthOf(versions, 0);
}
})
})
describe("#updateVersion()", function() {
it("should update the object version", function* () {
for (let type of types) {
let obj = yield createDataObject(type);
assert.equal(obj.version, 0);
yield obj.updateVersion(1234);
assert.equal(obj.version, 1234);
assert.isFalse(obj.hasChanged());
obj.synced = true;
assert.ok(obj.hasChanged());
yield obj.updateVersion(1235);
assert.equal(obj.version, 1235);
assert.ok(obj.hasChanged());
yield obj.eraseTx();
}
})
})
describe("#updateSynced()", function() {
it("should update the object sync status", function* () {
for (let type of types) {
let obj = yield createDataObject(type);
assert.isFalse(obj.synced);
yield obj.updateSynced(false);
assert.isFalse(obj.synced);
assert.isFalse(obj.hasChanged());
yield obj.updateSynced(true);
assert.ok(obj.synced);
assert.isFalse(obj.hasChanged());
obj.version = 1234;
assert.ok(obj.hasChanged());
yield obj.updateSynced(false);
assert.isFalse(obj.synced);
assert.ok(obj.hasChanged());
yield obj.eraseTx();
}
})
it("should clear changed status", function* () {
var item = createUnsavedDataObject('item');
item.synced = true;
yield item.saveTx();
// Only synced changed
item.synced = false;
assert.isTrue(item.hasChanged());
assert.isTrue(item._changed.primaryData.synced);
yield 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);
yield 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", function* () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
yield 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", function* () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
yield obj.saveTx();
obj.removeRelation(predicate, object);
yield obj.saveTx();
assert.lengthOf(Object.keys(obj.getRelations()), 0);
}
})
})
describe("#hasRelation()", function () {
it("should return true if an object has a given relation", function* () {
for (let type of types) {
let predicate = 'owl:sameAs';
let object = makeObjectURI(type);
let obj = createUnsavedDataObject(type);
obj.addRelation(predicate, object);
yield obj.saveTx();
assert.ok(obj.hasRelation(predicate, object));
}
})
})
describe("#setRelations()", function () {
it("shouldn't allow invalid 'relations' predicates", 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", function* () {
var group = yield getGroup();
var item1 = yield createDataObject('item');
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
var item2URI = Zotero.URI.getItemURI(item2);
yield item2.addLinkedItem(item1);
var linkedItem = yield item1.getLinkedItem(item2.libraryID);
assert.equal(linkedItem.id, item2.id);
})
it("shouldn't return reverse linked objects by default", function* () {
var group = yield getGroup();
var item1 = yield createDataObject('item');
var item1URI = Zotero.URI.getItemURI(item1);
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
yield item2.addLinkedItem(item1);
var linkedItem = yield item2.getLinkedItem(item1.libraryID);
assert.isFalse(linkedItem);
})
it("should return reverse linked objects with bidirectional flag", function* () {
var group = yield getGroup();
var item1 = yield createDataObject('item');
var item1URI = Zotero.URI.getItemURI(item1);
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
yield item2.addLinkedItem(item1);
var linkedItem = yield item2.getLinkedItem(item1.libraryID, true);
assert.equal(linkedItem.id, item1.id);
})
})
describe("#_addLinkedObject()", function () {
it("should add an owl:sameAs relation", function* () {
var group = yield getGroup();
var item1 = yield createDataObject('item');
var dateModified = item1.getField('dateModified');
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
var item2URI = Zotero.URI.getItemURI(item2);
yield 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);
})
})
})
})