578 lines
17 KiB
JavaScript
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);
|
|
})
|
|
})
|
|
})
|
|
})
|