2015-07-20 21:27:55 +00:00
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
describe("Zotero.Sync.Data.Local", function() {
|
2015-11-02 08:22:37 +00:00
|
|
|
|
describe("#getAPIKey()/#setAPIKey()", function () {
|
|
|
|
|
it("should get and set an API key", function* () {
|
|
|
|
|
var apiKey1 = Zotero.Utilities.randomString(24);
|
|
|
|
|
var apiKey2 = Zotero.Utilities.randomString(24);
|
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey(apiKey1);
|
2016-09-05 07:59:42 +00:00
|
|
|
|
yield assert.eventually.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey1), apiKey1);
|
2015-11-02 08:22:37 +00:00
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey(apiKey2);
|
2016-09-05 07:59:42 +00:00
|
|
|
|
yield assert.eventually.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey2), apiKey2);
|
2015-11-02 08:22:37 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
it("should clear an API key by setting an empty string", function* () {
|
|
|
|
|
var apiKey = Zotero.Utilities.randomString(24);
|
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey(apiKey);
|
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey("");
|
2016-09-05 07:59:42 +00:00
|
|
|
|
yield assert.eventually.strictEqual(Zotero.Sync.Data.Local.getAPIKey(apiKey), "");
|
2015-11-02 08:22:37 +00:00
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2016-01-12 21:52:25 +00:00
|
|
|
|
|
2016-04-26 20:27:49 +00:00
|
|
|
|
describe("#checkUser()", function () {
|
2016-11-29 00:26:39 +00:00
|
|
|
|
var resetDataDirFile;
|
2016-06-27 16:40:38 +00:00
|
|
|
|
|
|
|
|
|
before(function() {
|
2016-11-29 00:26:39 +00:00
|
|
|
|
resetDataDirFile = OS.Path.join(Zotero.DataDirectory.dir, 'reset-data-directory');
|
2016-06-27 16:40:38 +00:00
|
|
|
|
sinon.stub(Zotero.Utilities.Internal, 'quitZotero');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
beforeEach(function* () {
|
|
|
|
|
yield OS.File.remove(resetDataDirFile, {ignoreAbsent: true});
|
|
|
|
|
Zotero.Utilities.Internal.quitZotero.reset();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
after(function() {
|
|
|
|
|
Zotero.Utilities.Internal.quitZotero.restore();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should prompt for data reset and create a temp 'reset-data-directory' file on accept", function* (){
|
2016-04-26 20:27:49 +00:00
|
|
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
|
|
|
yield Zotero.Users.setCurrentUsername("A");
|
|
|
|
|
|
2016-04-27 01:32:18 +00:00
|
|
|
|
var handled = false;
|
2016-04-26 20:27:49 +00:00
|
|
|
|
waitForDialog(function (dialog) {
|
|
|
|
|
var text = dialog.document.documentElement.textContent;
|
2016-04-27 01:32:18 +00:00
|
|
|
|
var matches = text.match(/‘[^’]*’/g);
|
2016-06-27 16:40:38 +00:00
|
|
|
|
assert.equal(matches.length, 3);
|
2016-04-27 01:32:18 +00:00
|
|
|
|
assert.equal(matches[0], "‘A’");
|
|
|
|
|
assert.equal(matches[1], "‘B’");
|
2016-06-27 16:40:38 +00:00
|
|
|
|
assert.equal(matches[2], "‘A’");
|
|
|
|
|
|
|
|
|
|
dialog.document.getElementById('zotero-hardConfirmationDialog-checkbox').checked = true;
|
|
|
|
|
dialog.document.getElementById('zotero-hardConfirmationDialog-checkbox')
|
|
|
|
|
.dispatchEvent(new Event('command'));
|
|
|
|
|
|
2016-04-27 01:32:18 +00:00
|
|
|
|
handled = true;
|
2016-06-27 16:40:38 +00:00
|
|
|
|
}, 'accept', 'chrome://zotero/content/hardConfirmationDialog.xul');
|
|
|
|
|
var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
|
|
|
var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile);
|
2016-04-27 01:32:18 +00:00
|
|
|
|
assert.isTrue(handled);
|
2016-04-26 20:27:49 +00:00
|
|
|
|
assert.isTrue(cont);
|
2016-06-27 16:40:38 +00:00
|
|
|
|
assert.isTrue(resetDataDirFileExists);
|
|
|
|
|
});
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
2016-06-27 16:40:38 +00:00
|
|
|
|
it("should prompt for data reset and cancel", function* () {
|
2016-04-26 20:27:49 +00:00
|
|
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
|
|
|
yield Zotero.Users.setCurrentUsername("A");
|
|
|
|
|
|
2016-06-27 16:40:38 +00:00
|
|
|
|
waitForDialog(false, 'cancel', 'chrome://zotero/content/hardConfirmationDialog.xul');
|
|
|
|
|
var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
|
|
|
var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile);
|
2016-04-26 20:27:49 +00:00
|
|
|
|
assert.isFalse(cont);
|
2016-06-27 16:40:38 +00:00
|
|
|
|
assert.isFalse(resetDataDirFileExists);
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
|
|
|
|
assert.equal(Zotero.Users.getCurrentUserID(), 1);
|
|
|
|
|
assert.equal(Zotero.Users.getCurrentUsername(), "A");
|
2016-06-27 16:40:38 +00:00
|
|
|
|
});
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
2016-06-27 16:40:38 +00:00
|
|
|
|
// extra1 functionality not used at the moment
|
|
|
|
|
it.skip("should prompt for data reset and allow to choose a new data directory", function* (){
|
2016-11-27 03:41:26 +00:00
|
|
|
|
sinon.stub(Zotero.DataDirectory, 'forceChange').returns(true);
|
2016-06-27 16:40:38 +00:00
|
|
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
|
|
|
yield Zotero.Users.setCurrentUsername("A");
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
2016-06-27 16:40:38 +00:00
|
|
|
|
waitForDialog(null, 'extra1', 'chrome://zotero/content/hardConfirmationDialog.xul');
|
|
|
|
|
waitForDialog();
|
|
|
|
|
var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
|
|
|
var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile);
|
2016-04-26 20:27:49 +00:00
|
|
|
|
assert.isTrue(cont);
|
2016-11-27 03:41:26 +00:00
|
|
|
|
assert.isTrue(Zotero.DataDirectory.forceChange.called);
|
2016-06-27 16:40:38 +00:00
|
|
|
|
assert.isFalse(resetDataDirFileExists);
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
2016-11-27 03:41:26 +00:00
|
|
|
|
Zotero.DataDirectory.forceChange.restore();
|
2016-06-27 16:40:38 +00:00
|
|
|
|
});
|
2016-04-26 20:27:49 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2016-07-19 22:58:48 +00:00
|
|
|
|
describe("#checkLibraryForAccess()", function () {
|
|
|
|
|
//
|
|
|
|
|
// editable
|
|
|
|
|
//
|
|
|
|
|
it("should prompt if library is changing from editable to non-editable and reset library on accept", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
|
|
|
var text = dialog.document.documentElement.textContent;
|
|
|
|
|
assert.include(text, group.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
2017-02-24 08:50:31 +00:00
|
|
|
|
mock.expects("_libraryHasUnsyncedData").once().returns(Zotero.Promise.resolve(true));
|
2016-07-20 02:12:13 +00:00
|
|
|
|
mock.expects("resetUnsyncedLibraryData").once().returns(Zotero.Promise.resolve());
|
|
|
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
|
|
|
|
);
|
|
|
|
|
yield promise;
|
|
|
|
|
|
|
|
|
|
mock.verify();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should prompt if library is changing from editable to non-editable but not reset library on cancel", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
|
|
|
var text = dialog.document.documentElement.textContent;
|
|
|
|
|
assert.include(text, group.name);
|
|
|
|
|
}, "cancel");
|
|
|
|
|
|
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
2017-02-24 08:50:31 +00:00
|
|
|
|
mock.expects("_libraryHasUnsyncedData").once().returns(Zotero.Promise.resolve(true));
|
2016-07-20 02:12:13 +00:00
|
|
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
|
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isFalse(
|
|
|
|
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
|
|
|
|
);
|
|
|
|
|
yield promise;
|
|
|
|
|
|
|
|
|
|
mock.verify();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not prompt if library is changing from editable to non-editable", function* () {
|
|
|
|
|
var group = yield createGroup({ editable: false, filesEditable: false });
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// filesEditable
|
|
|
|
|
//
|
|
|
|
|
it("should prompt if library is changing from filesEditable to non-filesEditable and reset library files on accept", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
|
|
|
var text = dialog.document.documentElement.textContent;
|
|
|
|
|
assert.include(text, group.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
2017-02-24 08:50:31 +00:00
|
|
|
|
mock.expects("_libraryHasUnsyncedFiles").once().returns(Zotero.Promise.resolve(true));
|
2016-07-20 02:12:13 +00:00
|
|
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
|
|
|
mock.expects("resetUnsyncedLibraryFiles").once().returns(Zotero.Promise.resolve());
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
|
|
|
|
);
|
|
|
|
|
yield promise;
|
|
|
|
|
|
|
|
|
|
mock.verify();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should prompt if library is changing from filesEditable to non-filesEditable but not reset library files on cancel", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
|
|
|
var text = dialog.document.documentElement.textContent;
|
|
|
|
|
assert.include(text, group.name);
|
|
|
|
|
}, "cancel");
|
|
|
|
|
|
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
2017-02-24 08:50:31 +00:00
|
|
|
|
mock.expects("_libraryHasUnsyncedFiles").once().returns(Zotero.Promise.resolve(true));
|
2016-07-20 02:12:13 +00:00
|
|
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
|
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isFalse(
|
|
|
|
|
yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
|
|
|
|
);
|
|
|
|
|
yield promise;
|
|
|
|
|
|
|
|
|
|
mock.verify();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe("#_libraryHasUnsyncedData()", function () {
|
|
|
|
|
it("should return true for unsynced setting", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
|
|
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should return true for unsynced item", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
yield createDataObject('item', { libraryID });
|
|
|
|
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should return false if no changes", function* () {
|
|
|
|
|
var group = yield createGroup();
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2016-07-20 02:12:13 +00:00
|
|
|
|
describe("#resetUnsyncedLibraryData()", function () {
|
2016-07-19 22:58:48 +00:00
|
|
|
|
it("should revert group and mark for full sync", function* () {
|
|
|
|
|
var group = yield createGroup({
|
|
|
|
|
version: 1,
|
|
|
|
|
libraryVersion: 2
|
|
|
|
|
});
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
|
|
|
|
|
// New setting
|
|
|
|
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
|
|
|
|
|
|
|
|
// Changed collection
|
|
|
|
|
var changedCollection = yield createDataObject('collection', { libraryID, version: 1 });
|
|
|
|
|
var originalCollectionName = changedCollection.name;
|
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObject(
|
|
|
|
|
'collection', libraryID, changedCollection.toJSON()
|
|
|
|
|
);
|
|
|
|
|
yield modifyDataObject(changedCollection);
|
|
|
|
|
|
|
|
|
|
// Unchanged item
|
|
|
|
|
var unchangedItem = yield createDataObject('item', { libraryID, version: 1, synced: true });
|
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObject(
|
|
|
|
|
'item', libraryID, unchangedItem.toJSON()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Changed item
|
|
|
|
|
var changedItem = yield createDataObject('item', { libraryID, version: 1 });
|
|
|
|
|
var originalChangedItemTitle = changedItem.getField('title');
|
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObject('item', libraryID, changedItem.toJSON());
|
|
|
|
|
yield modifyDataObject(changedItem);
|
|
|
|
|
|
|
|
|
|
// New item
|
|
|
|
|
var newItem = yield createDataObject('item', { libraryID, version: 1 });
|
|
|
|
|
var newItemKey = newItem.key;
|
|
|
|
|
|
|
|
|
|
// Delete item
|
|
|
|
|
var deletedItem = yield createDataObject('item', { libraryID });
|
|
|
|
|
var deletedItemKey = deletedItem.key;
|
|
|
|
|
yield deletedItem.eraseTx();
|
|
|
|
|
|
2016-07-20 02:12:13 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.resetUnsyncedLibraryData(libraryID);
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isNull(Zotero.SyncedSettings.get(group.libraryID, "testSetting"));
|
|
|
|
|
|
|
|
|
|
assert.equal(changedCollection.name, originalCollectionName);
|
|
|
|
|
assert.isTrue(changedCollection.synced);
|
|
|
|
|
|
|
|
|
|
assert.isTrue(unchangedItem.synced);
|
|
|
|
|
|
|
|
|
|
assert.equal(changedItem.getField('title'), originalChangedItemTitle);
|
|
|
|
|
assert.isTrue(changedItem.synced);
|
|
|
|
|
|
|
|
|
|
assert.isFalse(Zotero.Items.get(newItemKey));
|
|
|
|
|
|
|
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, deletedItemKey));
|
|
|
|
|
|
|
|
|
|
assert.equal(group.libraryVersion, -1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2016-07-20 02:12:13 +00:00
|
|
|
|
describe("#resetUnsyncedLibraryFiles", function () {
|
2016-07-19 22:58:48 +00:00
|
|
|
|
it("should delete unsynced files", function* () {
|
|
|
|
|
var group = yield createGroup({
|
|
|
|
|
version: 1,
|
|
|
|
|
libraryVersion: 2
|
|
|
|
|
});
|
|
|
|
|
var libraryID = group.libraryID;
|
|
|
|
|
|
|
|
|
|
var attachment1 = yield importFileAttachment('test.png', { libraryID });
|
|
|
|
|
attachment1.attachmentSyncState = "in_sync";
|
|
|
|
|
attachment1.attachmentSyncedModificationTime = 1234567890000;
|
|
|
|
|
attachment1.attachmentSyncedHash = "8caf2ee22919d6725eb0648b98ef6bad";
|
|
|
|
|
var attachment2 = yield importFileAttachment('test.pdf', { libraryID });
|
|
|
|
|
|
2016-07-20 02:12:13 +00:00
|
|
|
|
// Has to be called before resetUnsyncedLibraryFiles()
|
2016-07-19 22:58:48 +00:00
|
|
|
|
assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedFiles(libraryID));
|
|
|
|
|
|
2016-07-20 02:12:13 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.resetUnsyncedLibraryFiles(libraryID);
|
2016-07-19 22:58:48 +00:00
|
|
|
|
|
|
|
|
|
assert.isFalse(yield attachment1.fileExists());
|
|
|
|
|
assert.isFalse(yield attachment2.fileExists());
|
|
|
|
|
assert.equal(
|
|
|
|
|
attachment1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
attachment2.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2016-01-12 21:52:25 +00:00
|
|
|
|
describe("#getLatestCacheObjectVersions", function () {
|
|
|
|
|
before(function* () {
|
2016-01-17 22:56:38 +00:00
|
|
|
|
yield resetDB({
|
|
|
|
|
thisArg: this,
|
|
|
|
|
skipBundledFiles: true
|
|
|
|
|
});
|
|
|
|
|
|
2016-01-12 21:52:25 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
|
|
|
|
'item',
|
|
|
|
|
Zotero.Libraries.userLibraryID,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
key: 'AAAAAAAA',
|
|
|
|
|
version: 2,
|
|
|
|
|
title: "A2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'AAAAAAAA',
|
|
|
|
|
version: 1,
|
|
|
|
|
title: "A1"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'BBBBBBBB',
|
|
|
|
|
version: 1,
|
|
|
|
|
title: "B1"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'BBBBBBBB',
|
|
|
|
|
version: 2,
|
|
|
|
|
title: "B2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'CCCCCCCC',
|
|
|
|
|
version: 3,
|
|
|
|
|
title: "C"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return latest version of all objects if no keys passed", function* () {
|
|
|
|
|
var versions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions(
|
|
|
|
|
'item',
|
|
|
|
|
Zotero.Libraries.userLibraryID
|
|
|
|
|
);
|
|
|
|
|
var keys = Object.keys(versions);
|
|
|
|
|
assert.lengthOf(keys, 3);
|
|
|
|
|
assert.sameMembers(keys, ['AAAAAAAA', 'BBBBBBBB', 'CCCCCCCC']);
|
|
|
|
|
assert.equal(versions.AAAAAAAA, 2);
|
|
|
|
|
assert.equal(versions.BBBBBBBB, 2);
|
|
|
|
|
assert.equal(versions.CCCCCCCC, 3);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return latest version of objects with passed keys", function* () {
|
|
|
|
|
var versions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions(
|
|
|
|
|
'item',
|
|
|
|
|
Zotero.Libraries.userLibraryID,
|
|
|
|
|
['AAAAAAAA', 'CCCCCCCC']
|
|
|
|
|
);
|
|
|
|
|
var keys = Object.keys(versions);
|
|
|
|
|
assert.lengthOf(keys, 2);
|
|
|
|
|
assert.sameMembers(keys, ['AAAAAAAA', 'CCCCCCCC']);
|
|
|
|
|
assert.equal(versions.AAAAAAAA, 2);
|
|
|
|
|
assert.equal(versions.CCCCCCCC, 3);
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
|
describe("#processObjectsFromJSON()", function () {
|
2015-07-20 21:27:55 +00:00
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
|
beforeEach(function* () {
|
2016-01-17 22:56:38 +00:00
|
|
|
|
yield resetDB({
|
|
|
|
|
thisArg: this,
|
|
|
|
|
skipBundledFiles: true
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
|
it("should update local version number and mark as synced if remote version is identical", function* () {
|
2015-07-20 21:27:55 +00:00
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
|
|
for (let type of types) {
|
|
|
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
|
let obj = yield createDataObject(type);
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
let data = obj.toJSON();
|
2015-07-20 21:27:55 +00:00
|
|
|
|
data.key = obj.key;
|
|
|
|
|
data.version = 10;
|
|
|
|
|
let json = {
|
|
|
|
|
key: obj.key,
|
|
|
|
|
version: 10,
|
|
|
|
|
data: data
|
|
|
|
|
};
|
2016-02-29 09:23:00 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
type, libraryID, [json], { stopOnError: true }
|
2015-07-20 21:27:55 +00:00
|
|
|
|
);
|
2015-10-29 07:41:54 +00:00
|
|
|
|
let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key);
|
|
|
|
|
assert.equal(localObj.version, 10);
|
|
|
|
|
assert.isTrue(localObj.synced);
|
2015-07-20 21:27:55 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
2015-10-29 07:41:54 +00:00
|
|
|
|
|
|
|
|
|
it("should keep local item changes while applying non-conflicting remote changes", function* () {
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
|
|
var type = 'item';
|
|
|
|
|
let obj = yield createDataObject(type, { version: 5 });
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
let data = obj.toJSON();
|
2015-10-29 07:41:54 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
|
|
|
|
type, libraryID, [data]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Change local title
|
|
|
|
|
yield modifyDataObject(obj)
|
|
|
|
|
var changedTitle = obj.getField('title');
|
|
|
|
|
|
|
|
|
|
// Save remote version to cache without title but with changed place
|
|
|
|
|
data.key = obj.key;
|
|
|
|
|
data.version = 10;
|
|
|
|
|
var changedPlace = data.place = 'New York';
|
|
|
|
|
let json = {
|
|
|
|
|
key: obj.key,
|
|
|
|
|
version: 10,
|
|
|
|
|
data: data
|
|
|
|
|
};
|
2016-02-29 09:23:00 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
type, libraryID, [json], { stopOnError: true }
|
2015-10-29 07:41:54 +00:00
|
|
|
|
);
|
|
|
|
|
assert.equal(obj.version, 10);
|
|
|
|
|
assert.equal(obj.getField('title'), changedTitle);
|
|
|
|
|
assert.equal(obj.getField('place'), changedPlace);
|
|
|
|
|
})
|
|
|
|
|
|
2017-01-26 20:02:58 +00:00
|
|
|
|
it("should save item with overriding local conflict as unsynced", function* () {
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
|
|
var isbn = '978-0-335-22006-9';
|
|
|
|
|
var type = 'item';
|
|
|
|
|
let obj = createUnsavedDataObject(type, { version: 5 });
|
|
|
|
|
obj.setField('ISBN', isbn);
|
|
|
|
|
yield obj.saveTx();
|
|
|
|
|
let data = obj.toJSON();
|
|
|
|
|
|
|
|
|
|
data.key = obj.key;
|
|
|
|
|
data.version = 10;
|
|
|
|
|
data.ISBN = '9780335220069';
|
|
|
|
|
let json = {
|
|
|
|
|
key: obj.key,
|
|
|
|
|
version: 10,
|
|
|
|
|
data
|
|
|
|
|
};
|
|
|
|
|
var results = yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
type, libraryID, [json], { stopOnError: true }
|
|
|
|
|
);
|
|
|
|
|
assert.isTrue(results[0].processed);
|
|
|
|
|
assert.isUndefined(results[0].changes);
|
|
|
|
|
assert.isUndefined(results[0].conflicts);
|
|
|
|
|
assert.equal(obj.version, 10);
|
|
|
|
|
assert.equal(obj.getField('ISBN'), isbn);
|
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
|
});
|
|
|
|
|
|
2015-10-31 19:19:50 +00:00
|
|
|
|
it("should delete older versions in sync cache after processing", function* () {
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
|
|
for (let type of types) {
|
|
|
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
|
// Save original version
|
|
|
|
|
let obj = yield createDataObject(type, { version: 5 });
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
let data = obj.toJSON();
|
2015-10-31 19:19:50 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
|
|
|
|
type, libraryID, [data]
|
|
|
|
|
);
|
2016-02-29 09:23:00 +00:00
|
|
|
|
|
|
|
|
|
// Save newer version
|
|
|
|
|
data.version = 10;
|
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
type, libraryID, [data], { stopOnError: true }
|
2015-10-31 19:19:50 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key);
|
|
|
|
|
assert.equal(localObj.version, 10);
|
|
|
|
|
|
|
|
|
|
let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions(
|
|
|
|
|
type, libraryID, obj.key
|
|
|
|
|
);
|
|
|
|
|
assert.sameMembers(
|
|
|
|
|
versions,
|
|
|
|
|
[10],
|
|
|
|
|
"should have only latest version of " + type + " in cache"
|
|
|
|
|
);
|
|
|
|
|
}
|
2016-02-29 09:23:00 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should delete object from sync queue after processing", function* () {
|
|
|
|
|
var objectType = 'item';
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
|
|
|
|
|
yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [key]);
|
|
|
|
|
|
|
|
|
|
var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
|
|
|
|
assert.include(versions, key);
|
|
|
|
|
|
|
|
|
|
var json = {
|
|
|
|
|
key,
|
|
|
|
|
version: 10,
|
|
|
|
|
data: {
|
|
|
|
|
key,
|
|
|
|
|
version: 10,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Test"
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
objectType, libraryID, [json], { stopOnError: true }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
|
|
|
|
assert.notInclude(versions, key);
|
|
|
|
|
});
|
2015-10-31 19:19:50 +00:00
|
|
|
|
|
2016-05-21 03:39:23 +00:00
|
|
|
|
it("should mark new attachment items and library for download", function* () {
|
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
|
|
|
var libraryID = library.id;
|
2015-10-29 07:41:54 +00:00
|
|
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
|
|
|
|
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var version = 10;
|
|
|
|
|
var json = {
|
|
|
|
|
key,
|
|
|
|
|
version,
|
|
|
|
|
data: {
|
|
|
|
|
key,
|
|
|
|
|
version,
|
|
|
|
|
itemType: 'attachment',
|
|
|
|
|
linkMode: 'imported_file',
|
|
|
|
|
md5: '57f8a4fda823187b91e1191487b87fe6',
|
|
|
|
|
mtime: 1442261130615
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
'item', libraryID, [json], { stopOnError: true }
|
2015-10-29 07:41:54 +00:00
|
|
|
|
);
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
var item = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
|
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
2016-05-21 03:39:23 +00:00
|
|
|
|
assert.isTrue(library.storageDownloadNeeded);
|
2015-10-29 07:41:54 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should mark updated attachment items for download", function* () {
|
2016-05-21 03:39:23 +00:00
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
|
|
|
var libraryID = library.id;
|
2015-10-29 07:41:54 +00:00
|
|
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
|
|
|
|
|
|
var item = yield importFileAttachment('test.png');
|
|
|
|
|
item.version = 5;
|
|
|
|
|
item.synced = true;
|
|
|
|
|
yield item.saveTx();
|
|
|
|
|
|
|
|
|
|
// Set file as synced
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
item.attachmentSyncedModificationTime = yield item.attachmentModificationTime;
|
|
|
|
|
item.attachmentSyncedHash = yield item.attachmentHash;
|
|
|
|
|
item.attachmentSyncState = "in_sync";
|
|
|
|
|
yield item.saveTx({ skipAll: true });
|
2015-10-29 07:41:54 +00:00
|
|
|
|
|
|
|
|
|
// Simulate download of version with updated attachment
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
var json = item.toResponseJSON();
|
2015-10-29 07:41:54 +00:00
|
|
|
|
json.version = 10;
|
|
|
|
|
json.data.version = 10;
|
|
|
|
|
json.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
|
|
|
json.data.mtime = new Date().getTime() + 10000;
|
2016-02-29 09:23:00 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
'item', libraryID, [json], { stopOnError: true }
|
2015-10-29 07:41:54 +00:00
|
|
|
|
);
|
|
|
|
|
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
2016-05-21 03:39:23 +00:00
|
|
|
|
assert.isTrue(library.storageDownloadNeeded);
|
2015-10-29 07:41:54 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should ignore attachment metadata when resolving metadata conflict", function* () {
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
|
|
|
|
|
|
var item = yield importFileAttachment('test.png');
|
|
|
|
|
item.version = 5;
|
|
|
|
|
yield item.saveTx();
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
var json = item.toResponseJSON();
|
2015-10-29 07:41:54 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
|
|
|
|
|
|
|
|
|
// Set file as synced
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
item.attachmentSyncedModificationTime = yield item.attachmentModificationTime;
|
|
|
|
|
item.attachmentSyncedHash = yield item.attachmentHash;
|
|
|
|
|
item.attachmentSyncState = "in_sync";
|
|
|
|
|
yield item.saveTx({ skipAll: true });
|
2015-10-29 07:41:54 +00:00
|
|
|
|
|
|
|
|
|
// Modify title locally, leaving item unsynced
|
|
|
|
|
var newTitle = Zotero.Utilities.randomString();
|
|
|
|
|
item.setField('title', newTitle);
|
|
|
|
|
yield item.saveTx();
|
|
|
|
|
|
|
|
|
|
// Simulate download of version with original title but updated attachment
|
|
|
|
|
json.version = 10;
|
|
|
|
|
json.data.version = 10;
|
|
|
|
|
json.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
|
|
|
json.data.mtime = new Date().getTime() + 10000;
|
2016-02-29 09:23:00 +00:00
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
|
|
|
'item', libraryID, [json], { stopOnError: true }
|
2015-10-29 07:41:54 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.equal(item.getField('title'), newTitle);
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
2015-10-29 07:41:54 +00:00
|
|
|
|
})
|
2016-02-29 09:23:00 +00:00
|
|
|
|
|
|
|
|
|
it("should roll back partial object changes on error", function* () {
|
|
|
|
|
var libraryID = Zotero.Libraries.publicationsLibraryID;
|
|
|
|
|
var key1 = "AAAAAAAA";
|
|
|
|
|
var key2 = "BBBBBBBB";
|
|
|
|
|
var json = [
|
|
|
|
|
{
|
|
|
|
|
key: key1,
|
|
|
|
|
version: 1,
|
|
|
|
|
data: {
|
|
|
|
|
key: key1,
|
|
|
|
|
version: 1,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Test A"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: key2,
|
|
|
|
|
version: 1,
|
|
|
|
|
data: {
|
|
|
|
|
key: key2,
|
|
|
|
|
version: 1,
|
|
|
|
|
itemType: "journalArticle",
|
|
|
|
|
title: "Test B",
|
|
|
|
|
deleted: true // Not allowed in My Publications
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
yield Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, json);
|
|
|
|
|
|
|
|
|
|
// Shouldn't roll back the successful item
|
|
|
|
|
yield assert.eventually.equal(Zotero.DB.valueQueryAsync(
|
|
|
|
|
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key1]
|
|
|
|
|
), 1);
|
|
|
|
|
// Should rollback the unsuccessful item
|
|
|
|
|
yield assert.eventually.equal(Zotero.DB.valueQueryAsync(
|
|
|
|
|
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key2]
|
|
|
|
|
), 0);
|
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
})
|
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
|
describe("Sync Queue", function () {
|
|
|
|
|
var lib1, lib2;
|
|
|
|
|
|
|
|
|
|
before(function* () {
|
|
|
|
|
lib1 = Zotero.Libraries.userLibraryID;
|
|
|
|
|
lib2 = Zotero.Libraries.publicationsLibraryID;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
beforeEach(function* () {
|
|
|
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncQueue");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
after(function* () {
|
|
|
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncQueue");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("#addObjectsToSyncQueue()", function () {
|
|
|
|
|
it("should add new objects and update lastCheck and tries for existing objects", function* () {
|
|
|
|
|
var objectType = 'item';
|
|
|
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
|
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
|
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key4 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
yield Zotero.DB.queryAsync(
|
|
|
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
|
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
|
|
|
[
|
|
|
|
|
lib1, key1, syncObjectTypeID, now - 3700, 0,
|
|
|
|
|
lib1, key2, syncObjectTypeID, now - 7000, 1,
|
|
|
|
|
lib2, key3, syncObjectTypeID, now - 86400, 2
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib1, [key1, key2]);
|
|
|
|
|
yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib2, [key4]);
|
|
|
|
|
|
|
|
|
|
var sql = "SELECT lastCheck, tries FROM syncQueue WHERE libraryID=? "
|
|
|
|
|
+ `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`;
|
|
|
|
|
var row;
|
|
|
|
|
// key1
|
|
|
|
|
row = yield Zotero.DB.rowQueryAsync(sql, [lib1, key1]);
|
|
|
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
|
|
|
assert.equal(row.tries, 1);
|
|
|
|
|
// key2
|
|
|
|
|
row = yield Zotero.DB.rowQueryAsync(sql, [lib1, key2]);
|
|
|
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
|
|
|
assert.equal(row.tries, 2);
|
|
|
|
|
// key3
|
|
|
|
|
row = yield Zotero.DB.rowQueryAsync(sql, [lib2, key3]);
|
|
|
|
|
assert.equal(row.lastCheck, now - 86400);
|
|
|
|
|
assert.equal(row.tries, 2);
|
|
|
|
|
// key4
|
|
|
|
|
row = yield Zotero.DB.rowQueryAsync(sql, [lib2, key4]);
|
|
|
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
|
|
|
assert.equal(row.tries, 0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("#getObjectsToTryFromSyncQueue()", function () {
|
|
|
|
|
it("should get objects that should be retried", function* () {
|
|
|
|
|
var objectType = 'item';
|
|
|
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
|
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
|
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key4 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
yield Zotero.DB.queryAsync(
|
|
|
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
|
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
|
|
|
[
|
|
|
|
|
lib1, key1, syncObjectTypeID, now - (30 * 60) - 10, 0, // more than half an hour, so should be retried
|
|
|
|
|
lib1, key2, syncObjectTypeID, now - (16 * 60 * 60) + 10, 4, // less than 16 hours, shouldn't be retried
|
|
|
|
|
lib2, key3, syncObjectTypeID, now - 86400 * 7, 20 // more than 64 hours, so should be retried
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib1);
|
|
|
|
|
assert.sameMembers(keys, [key1]);
|
|
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib2);
|
|
|
|
|
assert.sameMembers(keys, [key3]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("#removeObjectsFromSyncQueue()", function () {
|
|
|
|
|
it("should remove objects from the sync queue", function* () {
|
|
|
|
|
var objectType = 'item';
|
|
|
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
|
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
|
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
|
yield Zotero.DB.queryAsync(
|
|
|
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
|
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
|
|
|
[
|
|
|
|
|
lib1, key1, syncObjectTypeID, now, 0,
|
|
|
|
|
lib1, key2, syncObjectTypeID, now, 4,
|
|
|
|
|
lib2, key3, syncObjectTypeID, now, 20
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
yield Zotero.Sync.Data.Local.removeObjectsFromSyncQueue('item', lib1, [key1]);
|
|
|
|
|
|
|
|
|
|
var sql = "SELECT COUNT(*) FROM syncQueue WHERE libraryID=? "
|
|
|
|
|
+ `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`;
|
|
|
|
|
assert.notOk(yield Zotero.DB.valueQueryAsync(sql, [lib1, key1]));
|
|
|
|
|
assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib1, key2]));
|
|
|
|
|
assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib2, key3]));
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("#resetSyncQueueTries", function () {
|
|
|
|
|
var spy;
|
|
|
|
|
|
|
|
|
|
after(function () {
|
|
|
|
|
if (spy) {
|
|
|
|
|
spy.restore();
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should be run on version upgrade", function* () {
|
|
|
|
|
var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)";
|
|
|
|
|
yield Zotero.DB.queryAsync(sql, "5.0foo");
|
|
|
|
|
|
|
|
|
|
spy = sinon.spy(Zotero.Sync.Data.Local, "resetSyncQueueTries");
|
|
|
|
|
yield Zotero.Schema.updateSchema();
|
|
|
|
|
assert.ok(spy.called);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2015-08-06 08:04:37 +00:00
|
|
|
|
|
2016-07-07 09:01:36 +00:00
|
|
|
|
describe("#showConflictResolutionWindow()", function () {
|
|
|
|
|
it("should show title of note parent", function* () {
|
|
|
|
|
var parentItem = yield createDataObject('item', { title: "Parent" });
|
|
|
|
|
var note = new Zotero.Item('note');
|
|
|
|
|
note.parentKey = parentItem.key;
|
|
|
|
|
note.setNote("Test");
|
|
|
|
|
yield note.saveTx();
|
|
|
|
|
|
|
|
|
|
var promise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
|
|
|
|
var doc = dialog.document;
|
|
|
|
|
var wizard = doc.documentElement;
|
|
|
|
|
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
|
|
|
|
|
|
|
|
|
|
// Show title for middle and right panes
|
|
|
|
|
var parentText = Zotero.getString('pane.item.parentItem') + " Parent";
|
|
|
|
|
assert.equal(mergeGroup.leftpane._id('parent-row').textContent, "");
|
|
|
|
|
assert.equal(mergeGroup.rightpane._id('parent-row').textContent, parentText);
|
|
|
|
|
assert.equal(mergeGroup.mergepane._id('parent-row').textContent, parentText);
|
|
|
|
|
|
|
|
|
|
wizard.getButton('finish').click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Zotero.Sync.Data.Local.showConflictResolutionWindow([
|
|
|
|
|
{
|
|
|
|
|
libraryID: note.libraryID,
|
|
|
|
|
key: note.key,
|
|
|
|
|
processed: false,
|
|
|
|
|
conflict: true,
|
|
|
|
|
left: {
|
|
|
|
|
deleted: true,
|
|
|
|
|
dateDeleted: "2016-07-07 12:34:56"
|
|
|
|
|
},
|
|
|
|
|
right: note.toJSON()
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
yield promise;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
|
describe("#_reconcileChanges()", function () {
|
|
|
|
|
describe("items", function () {
|
|
|
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
url: "http://zotero.org/",
|
|
|
|
|
publicationTitle: "Publisher", // Remove locally
|
|
|
|
|
extra: "Extra", // Removed on both
|
|
|
|
|
dateModified: "2015-05-14 12:34:56",
|
|
|
|
|
collections: [
|
|
|
|
|
'AAAAAAAA', // Removed locally
|
|
|
|
|
'DDDDDDDD', // Removed remotely,
|
|
|
|
|
'EEEEEEEE' // Removed from both
|
|
|
|
|
],
|
|
|
|
|
relations: {
|
|
|
|
|
a: 'A', // Unchanged string
|
|
|
|
|
c: ['C1', 'C2'], // Unchanged array
|
|
|
|
|
d: 'D', // String removed locally
|
|
|
|
|
e: ['E'], // Array removed locally
|
|
|
|
|
f: 'F1', // String changed locally
|
|
|
|
|
g: [
|
|
|
|
|
'G1', // Unchanged
|
|
|
|
|
'G2', // Removed remotely
|
|
|
|
|
'G3' // Removed from both
|
|
|
|
|
],
|
|
|
|
|
h: 'H', // String removed remotely
|
|
|
|
|
i: ['I'], // Array removed remotely
|
|
|
|
|
},
|
|
|
|
|
tags: [
|
|
|
|
|
{ tag: 'A' }, // Removed locally
|
|
|
|
|
{ tag: 'D' }, // Removed remotely
|
|
|
|
|
{ tag: 'E' } // Removed from both
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 2", // Changed locally
|
|
|
|
|
url: "https://www.zotero.org/", // Same change on local and remote
|
|
|
|
|
place: "Place", // Added locally
|
|
|
|
|
dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored
|
|
|
|
|
collections: [
|
|
|
|
|
'BBBBBBBB', // Added locally
|
|
|
|
|
'DDDDDDDD',
|
|
|
|
|
'FFFFFFFF' // Added on both
|
|
|
|
|
],
|
|
|
|
|
relations: {
|
|
|
|
|
'a': 'A',
|
|
|
|
|
'b': 'B', // String added locally
|
|
|
|
|
'f': 'F2',
|
|
|
|
|
'g': [
|
|
|
|
|
'G1',
|
|
|
|
|
'G2',
|
|
|
|
|
'G6' // Added locally and remotely
|
|
|
|
|
],
|
|
|
|
|
h: 'H', // String removed remotely
|
|
|
|
|
i: ['I'], // Array removed remotely
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
tags: [
|
|
|
|
|
{ tag: 'B' },
|
|
|
|
|
{ tag: 'D' },
|
|
|
|
|
{ tag: 'F', type: 1 }, // Added on both
|
|
|
|
|
{ tag: 'G' }, // Added on both, but with different types
|
|
|
|
|
{ tag: 'H', type: 1 } // Added on both, but with different types
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
url: "https://www.zotero.org/",
|
|
|
|
|
publicationTitle: "Publisher",
|
|
|
|
|
date: "2015-05-15", // Added remotely
|
|
|
|
|
dateModified: "2015-05-14 13:45:12",
|
|
|
|
|
collections: [
|
|
|
|
|
'AAAAAAAA',
|
|
|
|
|
'CCCCCCCC', // Added remotely
|
|
|
|
|
'FFFFFFFF'
|
|
|
|
|
],
|
|
|
|
|
relations: {
|
|
|
|
|
'a': 'A',
|
|
|
|
|
'd': 'D',
|
|
|
|
|
'e': ['E'],
|
|
|
|
|
'f': 'F1',
|
|
|
|
|
'g': [
|
|
|
|
|
'G1',
|
|
|
|
|
'G4', // Added remotely
|
|
|
|
|
'G6'
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
tags: [
|
|
|
|
|
{ tag: 'A' },
|
|
|
|
|
{ tag: 'C' },
|
|
|
|
|
{ tag: 'F', type: 1 },
|
|
|
|
|
{ tag: 'G', type: 1 },
|
|
|
|
|
{ tag: 'H' }
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "date",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "2015-05-15"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "collections",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: "CCCCCCCC"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "collections",
|
|
|
|
|
op: "member-remove",
|
|
|
|
|
value: "DDDDDDDD"
|
|
|
|
|
},
|
|
|
|
|
// Relations
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'g',
|
|
|
|
|
value: 'G2'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'g',
|
|
|
|
|
value: 'G4'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'h',
|
|
|
|
|
value: 'H'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'i',
|
|
|
|
|
value: 'I'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// Tags
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'C'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'D'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'H',
|
|
|
|
|
type: 1
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'H'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
|
|
|
// Similar to above but without differing remote changes
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
url: "http://zotero.org/",
|
|
|
|
|
publicationTitle: "Publisher", // Remove locally
|
|
|
|
|
extra: "Extra", // Removed on both
|
|
|
|
|
dateModified: "2015-05-14 12:34:56",
|
|
|
|
|
collections: [
|
|
|
|
|
'AAAAAAAA', // Removed locally
|
|
|
|
|
'DDDDDDDD',
|
|
|
|
|
'EEEEEEEE' // Removed from both
|
|
|
|
|
],
|
|
|
|
|
tags: [
|
|
|
|
|
{
|
|
|
|
|
tag: 'A' // Removed locally
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'D' // Removed remotely
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'E' // Removed from both
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 2", // Changed locally
|
|
|
|
|
url: "https://www.zotero.org/", // Same change on local and remote
|
|
|
|
|
place: "Place", // Added locally
|
|
|
|
|
dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored
|
|
|
|
|
collections: [
|
|
|
|
|
'BBBBBBBB', // Added locally
|
|
|
|
|
'DDDDDDDD',
|
|
|
|
|
'FFFFFFFF' // Added on both
|
|
|
|
|
],
|
|
|
|
|
tags: [
|
|
|
|
|
{
|
|
|
|
|
tag: 'B'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'D'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'F', // Added on both
|
|
|
|
|
type: 1
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'G' // Added on both, but with different types
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
url: "https://www.zotero.org/",
|
|
|
|
|
publicationTitle: "Publisher",
|
|
|
|
|
dateModified: "2015-05-14 13:45:12",
|
|
|
|
|
collections: [
|
|
|
|
|
'AAAAAAAA',
|
|
|
|
|
'DDDDDDDD',
|
|
|
|
|
'FFFFFFFF'
|
|
|
|
|
],
|
|
|
|
|
tags: [
|
|
|
|
|
{
|
|
|
|
|
tag: 'A'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'D'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'F',
|
|
|
|
|
type: 1
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
tag: 'G',
|
|
|
|
|
type: 1
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return conflict when changes can't be automatically resolved", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
dateModified: "2015-05-14 12:34:56"
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 2",
|
|
|
|
|
dateModified: "2015-05-14 14:12:34"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
title: "Title 3",
|
|
|
|
|
dateModified: "2015-05-14 13:45:12"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
Zotero.debug('=-=-=-=');
|
|
|
|
|
Zotero.debug(result);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.conflicts,
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Title 2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Title 3"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should automatically merge array/object members and generate conflicts for field changes in absence of cached version", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title",
|
|
|
|
|
creators: [
|
|
|
|
|
{
|
|
|
|
|
name: "Center for History and New Media",
|
|
|
|
|
creatorType: "author"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
place: "Place", // Local
|
|
|
|
|
dateModified: "2015-05-14 14:12:34", // Changed on both, but ignored
|
|
|
|
|
collections: [
|
|
|
|
|
'AAAAAAAA' // Local
|
|
|
|
|
],
|
|
|
|
|
relations: {
|
|
|
|
|
'a': 'A',
|
|
|
|
|
'b': 'B', // Local
|
|
|
|
|
'e': 'E1',
|
|
|
|
|
'f': [
|
|
|
|
|
'F1',
|
|
|
|
|
'F2' // Local
|
|
|
|
|
],
|
|
|
|
|
h: 'H', // String removed remotely
|
|
|
|
|
i: ['I'], // Array removed remotely
|
|
|
|
|
},
|
|
|
|
|
tags: [
|
|
|
|
|
{ tag: 'A' }, // Local
|
|
|
|
|
{ tag: 'C' },
|
|
|
|
|
{ tag: 'F', type: 1 },
|
|
|
|
|
{ tag: 'G' }, // Different types
|
|
|
|
|
{ tag: 'H', type: 1 } // Different types
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
title: "Title",
|
|
|
|
|
creators: [
|
|
|
|
|
{
|
|
|
|
|
creatorType: "author", // Different property order shouldn't matter
|
|
|
|
|
name: "Center for History and New Media"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
date: "2015-05-15", // Remote
|
|
|
|
|
dateModified: "2015-05-14 13:45:12",
|
|
|
|
|
collections: [
|
|
|
|
|
'BBBBBBBB' // Remote
|
|
|
|
|
],
|
|
|
|
|
relations: {
|
|
|
|
|
'a': 'A',
|
|
|
|
|
'c': 'C', // Remote
|
|
|
|
|
'd': ['D'], // Remote
|
|
|
|
|
'e': 'E2',
|
|
|
|
|
'f': [
|
|
|
|
|
'F1',
|
|
|
|
|
'F3' // Remote
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
tags: [
|
|
|
|
|
{ tag: 'B' }, // Remote
|
|
|
|
|
{ tag: 'C' },
|
|
|
|
|
{ tag: 'F', type: 1 },
|
|
|
|
|
{ tag: 'G', type: 1 }, // Different types
|
|
|
|
|
{ tag: 'H' } // Different types
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'item', false, json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
Zotero.debug(result);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
// Collections
|
|
|
|
|
{
|
|
|
|
|
field: "collections",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: "BBBBBBBB"
|
|
|
|
|
},
|
|
|
|
|
// Relations
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'c',
|
|
|
|
|
value: 'C'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'd',
|
|
|
|
|
value: 'D'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'e',
|
|
|
|
|
value: 'E2'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: 'f',
|
|
|
|
|
value: 'F3'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// Tags
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'B'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'G',
|
|
|
|
|
type: 1
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "tags",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
tag: 'H'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.conflicts,
|
|
|
|
|
[
|
2015-10-29 07:41:54 +00:00
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "place",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "Place"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "place",
|
|
|
|
|
op: "delete"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "date",
|
|
|
|
|
op: "delete"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "date",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "2015-05-15"
|
|
|
|
|
}
|
|
|
|
|
]
|
2015-07-20 21:27:55 +00:00
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
})
|
2017-01-26 23:07:01 +00:00
|
|
|
|
|
|
|
|
|
it("should automatically use remote version for unresolvable conflicts when both sides are in trash", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
dateModified: "2015-05-14 12:34:56"
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 2",
|
|
|
|
|
deleted: true,
|
|
|
|
|
dateModified: "2015-05-14 14:12:34"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
title: "Title 3",
|
|
|
|
|
deleted: true,
|
|
|
|
|
dateModified: "2015-05-14 13:45:12"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 1);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Title 3"
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe("collections", function () {
|
|
|
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
parentCollection: null,
|
|
|
|
|
relations: {
|
|
|
|
|
A: "A", // Removed locally
|
|
|
|
|
C: "C" // Removed on both
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2", // Changed locally
|
|
|
|
|
parentCollection: null,
|
|
|
|
|
relations: {}
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
parentCollection: "BBBBBBBB", // Added remotely
|
|
|
|
|
relations: {
|
|
|
|
|
A: "A",
|
|
|
|
|
B: "B" // Added remotely
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'collection', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "parentCollection",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "BBBBBBBB"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "relations",
|
|
|
|
|
op: "property-member-add",
|
|
|
|
|
value: {
|
|
|
|
|
key: "B",
|
|
|
|
|
value: "B"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
|
|
|
// Similar to above but without differing remote changes
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2", // Changed locally
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
// Added locally
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should automatically resolve conflicts with remote version", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1"
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 3"
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "name",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Name 3"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should automatically resolve conflicts in absence of cached version", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', false, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "name",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Name 2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "conditions",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe("searches", function () {
|
|
|
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2", // Changed locally
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
// Removed remotely
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
// Added remotely
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "conditions",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "conditions",
|
|
|
|
|
op: "member-remove",
|
|
|
|
|
value: {
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
|
|
|
// Similar to above but without differing remote changes
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2", // Changed locally
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
// Added locally
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should automatically resolve conflicts with remote version", function () {
|
|
|
|
|
var cacheJSON = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1"
|
|
|
|
|
};
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 3"
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', cacheJSON, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "name",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Name 3"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("should automatically resolve conflicts in absence of cached version", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 1",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
name: "Name 2",
|
|
|
|
|
conditions: [
|
|
|
|
|
{
|
|
|
|
|
condition: "title",
|
|
|
|
|
operator: "contains",
|
|
|
|
|
value: "A"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
|
|
|
'search', false, json1, json2
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "name",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Name 2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "conditions",
|
|
|
|
|
op: "member-add",
|
|
|
|
|
value: {
|
|
|
|
|
condition: "place",
|
|
|
|
|
operator: "is",
|
|
|
|
|
value: "Chicago"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
2015-10-29 07:41:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe("#reconcileChangesWithoutCache()", function () {
|
|
|
|
|
it("should return conflict for conflicting fields", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
pages: 10,
|
|
|
|
|
dateModified: "2015-05-14 14:12:34"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
title: "Title 2",
|
|
|
|
|
place: "New York",
|
|
|
|
|
dateModified: "2015-05-14 13:45:12"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
|
|
|
'item', json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.conflicts,
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "Title 1"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "Title 2"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "pages",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: 10
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "pages",
|
|
|
|
|
op: "delete"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "place",
|
|
|
|
|
op: "delete"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "place",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
})
|
2017-01-26 20:02:58 +00:00
|
|
|
|
|
2017-01-26 23:07:01 +00:00
|
|
|
|
it("should automatically use remote version for conflicting fields when both sides are in trash", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
title: "Title 1",
|
|
|
|
|
pages: 10,
|
|
|
|
|
deleted: true,
|
|
|
|
|
dateModified: "2015-05-14 14:12:34"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
title: "Title 2",
|
|
|
|
|
place: "New York",
|
|
|
|
|
deleted: true,
|
|
|
|
|
dateModified: "2015-05-14 13:45:12"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
|
|
|
'item', json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 3);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "title",
|
|
|
|
|
op: "modify",
|
|
|
|
|
value: "Title 2"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "pages",
|
|
|
|
|
op: "delete"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
field: "place",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "New York"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2017-01-26 20:02:58 +00:00
|
|
|
|
it("should automatically use local hyphenated ISBN value if only difference", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
ISBN: "978-0-335-22006-9"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
ISBN: "9780335220069"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
|
|
|
'item', json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.changes, 0);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
assert.isTrue(result.localChanged);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should automatically use remote hyphenated ISBN value if only difference", function () {
|
|
|
|
|
var json1 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1234,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
ISBN: "9780335220069"
|
|
|
|
|
};
|
|
|
|
|
var json2 = {
|
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
|
version: 1235,
|
|
|
|
|
itemType: "book",
|
|
|
|
|
ISBN: "978-0-335-22006-9"
|
|
|
|
|
};
|
|
|
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
|
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
|
|
|
'item', json1, json2, ignoreFields
|
|
|
|
|
);
|
|
|
|
|
assert.sameDeepMembers(
|
|
|
|
|
result.changes,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
field: "ISBN",
|
|
|
|
|
op: "add",
|
|
|
|
|
value: "978-0-335-22006-9"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert.lengthOf(result.conflicts, 0);
|
|
|
|
|
assert.isFalse(result.localChanged);
|
|
|
|
|
});
|
2015-10-29 07:41:54 +00:00
|
|
|
|
})
|
2015-07-20 21:27:55 +00:00
|
|
|
|
})
|