ef7da3486a
If this works out I think we'll want to use this approach for all data layer changes. Previously, an unsaved change on an object would update its state immediately, which was fine for synchronous code but breaks down if a save involves multiple asynchronous calls, because modifying state after the relevant data has been saved to the DB but before the `_changed` object has been cleared would mean that new changes would be lost. Now, changes are written to _changedData, and a get for the data first checks _changedData before checking the state property (e.g., _tags) directly. The changedData property is cleared as it's written, and once the object is saved, the reload updates the state property with the new data.
567 lines
16 KiB
JavaScript
567 lines
16 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("#_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);
|
|
})
|
|
})
|
|
})
|
|
})
|