commit 391f525a75492a1de18810611b184df9da9b48a1
parent f8a0f9ad1d0e8ac8bdf0f1d5cf1927e7c2f94bcf
Author: Dan Stillman <dstillman@zotero.org>
Date: Tue, 3 May 2016 23:09:38 -0400
Close #975, Process conflicts for all batches together
Diffstat:
4 files changed, 577 insertions(+), 480 deletions(-)
diff --git a/chrome/content/zotero/xpcom/sync/syncEngine.js b/chrome/content/zotero/xpcom/sync/syncEngine.js
@@ -326,7 +326,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func
}
return 0;
});
- var mergeData = Zotero.Sync.Data.Local.resolveConflicts(conflicts);
+ var mergeData = Zotero.Sync.Data.Local.showConflictResolutionWindow(conflicts);
if (mergeData) {
let concurrentObjects = 50;
yield Zotero.Utilities.Internal.forEachChunkAsync(
@@ -562,6 +562,8 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
+ " in " + this.library.name
);
+ var conflicts = [];
+
// Process batches as soon as they're available
yield Zotero.Promise.map(
json,
@@ -592,6 +594,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
)
.then(function (results) {
let processedKeys = [];
+ let conflictResults = [];
results.forEach(x => {
// If data was processed, remove JSON
if (x.processed) {
@@ -601,8 +604,12 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
if (x.processed || !x.retry) {
processedKeys.push(x.key);
}
+ if (x.conflict) {
+ conflictResults.push(x);
+ }
});
keys = Zotero.Utilities.arrayDiff(keys, processedKeys);
+ conflicts.push(...conflictResults);
}.bind(this));
}.bind(this)
);
@@ -630,7 +637,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
this.failedItems = [];
}
}
- return;
+ break;
}
lastLength = keys.length;
@@ -638,6 +645,16 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
var remainingObjectDesc = `${keys.length == 1 ? objectType : objectTypePlural}`;
Zotero.debug(`Retrying ${keys.length} remaining ${remainingObjectDesc}`);
}
+
+ // Show conflict resolution window
+ if (conflicts.length) {
+ let results = yield Zotero.Sync.Data.Local.processConflicts(
+ objectType, this.libraryID, conflicts, this._getOptions()
+ );
+ // Keys can be unprocessed if conflict resolution is cancelled
+ let keys = results.filter(x => x.processed).map(x => x.key);
+ yield Zotero.Sync.Data.Local.removeObjectsFromSyncQueue(objectType, this.libraryID, keys);
+ }
});
diff --git a/chrome/content/zotero/xpcom/sync/syncLocal.js b/chrome/content/zotero/xpcom/sync/syncLocal.js
@@ -485,11 +485,17 @@ Zotero.Sync.Data.Local = {
/**
* Process downloaded JSON and update local objects
*
- * @return {Promise<Array<Object>>} - Promise for an array of objects with the following properties:
- * {String} key
- * {Boolean} processed
- * {Object} [error]
- * {Boolean} [retry]
+ * @return {Promise<Object[]>} - Promise for an array of objects with the following properties:
+ * {String} key
+ * {Boolean} processed
+ * {Object} [error]
+ * {Boolean} [retry]
+ * {Boolean} [conflict=false]
+ * {Object} [left] - Local JSON data for conflict (or .deleted and .dateDeleted)
+ * {Object} [right] - Remote JSON data for conflict
+ * {Object[]} [changes] - An array of operations to apply locally to resolve conflicts,
+ * as returned by _reconcileChanges()
+ * {Object[]} [conflicts] - An array of conflicting fields that can't be resolved automatically
*/
processObjectsFromJSON: Zotero.Promise.coroutine(function* (objectType, libraryID, json, options = {}) {
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
@@ -508,7 +514,6 @@ Zotero.Sync.Data.Local = {
+ " for " + libraryName);
var results = [];
- var conflicts = [];
if (!json.length) {
return results;
@@ -658,7 +663,10 @@ Zotero.Sync.Data.Local = {
Zotero.debug(jsonDataLocal);
Zotero.debug(jsonData);
Zotero.debug(result);
- conflicts.push({
+ results.push({
+ key: objectKey,
+ processed: false,
+ conflict: true,
left: jsonDataLocal,
right: jsonData,
changes: result.changes,
@@ -699,7 +707,10 @@ Zotero.Sync.Data.Local = {
switch (objectType) {
case 'item':
- conflicts.push({
+ results.push({
+ key: objectKey,
+ processed: false,
+ conflict: true,
left: {
deleted: true,
dateDeleted: Zotero.Date.dateToSQL(dateDeleted, true)
@@ -783,147 +794,6 @@ Zotero.Sync.Data.Local = {
}
}
-
- //
- // Conflict resolution
- //
- if (conflicts.length) {
- // Sort conflicts by local Date Modified/Deleted
- conflicts.sort(function (a, b) {
- var d1 = a.left.dateDeleted || a.left.dateModified;
- var d2 = b.left.dateDeleted || b.left.dateModified;
- if (d1 > d2) {
- return 1
- }
- if (d1 < d2) {
- return -1;
- }
- return 0;
- })
-
- var mergeData = this.resolveConflicts(conflicts);
- if (mergeData) {
- Zotero.debug("Processing resolved conflicts");
-
- let batchSize = 50;
- let notifierQueues = [];
- try {
- for (let i = 0; i < mergeData.length; i++) {
- // Batch notifier updates
- if (notifierQueues.length == batchSize) {
- yield Zotero.Notifier.commit(notifierQueues);
- notifierQueues = [];
- }
- let notifierQueue = new Zotero.Notifier.Queue;
-
- let json = mergeData[i];
-
- let saveOptions = {};
- Object.assign(saveOptions, options);
- // Tell _saveObjectFromJSON to save as unsynced
- saveOptions.saveAsChanged = true;
- saveOptions.notifierQueue = notifierQueue;
-
- // Errors have to be thrown in order to roll back the transaction, so catch
- // those here and continue
- try {
- yield Zotero.DB.executeTransaction(function* () {
- let obj = yield objectsClass.getByLibraryAndKeyAsync(
- libraryID, json.key, { noCache: true }
- );
- // Update object with merge data
- if (obj) {
- // Delete local object
- if (json.deleted) {
- try {
- yield obj.erase({
- notifierQueue
- });
- }
- catch (e) {
- results.push({
- key: json.key,
- processed: false,
- error: e,
- retry: false
- });
- throw e;
- }
- results.push({
- key: json.key,
- processed: true
- });
- return;
- }
-
- // Save merged changes below
- }
- // If no local object and merge wanted a delete, we're good
- else if (json.deleted) {
- results.push({
- key: json.key,
- processed: true
- });
- return;
- }
- // Recreate locally deleted object
- else {
- obj = new Zotero[ObjectType];
- obj.libraryID = libraryID;
- obj.key = json.key;
- yield obj.loadPrimaryData();
-
- // Don't cache new items immediately,
- // which skips reloading after save
- saveOptions.skipCache = true;
- }
-
- let saveResults = yield this._saveObjectFromJSON(
- obj, json, saveOptions
- );
- results.push(saveResults);
- if (!saveResults.processed) {
- throw saveResults.error;
- }
-
- }.bind(this));
-
- if (notifierQueue.size) {
- notifierQueues.push(notifierQueue);
- }
- }
- catch (e) {
- Zotero.logError(e);
-
- if (options.onError) {
- options.onError(e);
- }
-
- if (options.stopOnError) {
- throw e;
- }
- }
- }
- }
- finally {
- if (notifierQueues.length) {
- yield Zotero.Notifier.commit(notifierQueues);
- }
- }
- }
- else {
- Zotero.debug("Conflict resolution was cancelled", 2);
- for (let conflict of conflicts) {
- results.push({
- // Use key from either, in case one side is deleted
- key: conflict.left.key || conflict.right.key,
- processed: false,
- retry: false
- });
- }
- }
- }
-
let processed = 0;
let skipped = 0;
results.forEach(x => x.processed ? processed++ : skipped++);
@@ -1049,7 +919,153 @@ Zotero.Sync.Data.Local = {
},
- resolveConflicts: function (conflicts) {
+ processConflicts: Zotero.Promise.coroutine(function* (objectType, libraryID, conflicts, options = {}) {
+ if (!conflicts.length) return [];
+
+ var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
+ var ObjectType = Zotero.Utilities.capitalize(objectType);
+
+ // Sort conflicts by local Date Modified/Deleted
+ conflicts.sort(function (a, b) {
+ var d1 = a.left.dateDeleted || a.left.dateModified;
+ var d2 = b.left.dateDeleted || b.left.dateModified;
+ if (d1 > d2) {
+ return 1
+ }
+ if (d1 < d2) {
+ return -1;
+ }
+ return 0;
+ })
+
+ var results = [];
+
+ var mergeData = this.showConflictResolutionWindow(conflicts);
+ if (!mergeData) {
+ Zotero.debug("Conflict resolution was cancelled", 2);
+ for (let conflict of conflicts) {
+ results.push({
+ // Use key from either, in case one side is deleted
+ key: conflict.left.key || conflict.right.key,
+ processed: false,
+ retry: false
+ });
+ }
+ return results;
+ }
+
+ Zotero.debug("Processing resolved conflicts");
+
+ let batchSize = 50;
+ let notifierQueues = [];
+ try {
+ for (let i = 0; i < mergeData.length; i++) {
+ // Batch notifier updates
+ if (notifierQueues.length == batchSize) {
+ yield Zotero.Notifier.commit(notifierQueues);
+ notifierQueues = [];
+ }
+ let notifierQueue = new Zotero.Notifier.Queue;
+
+ let json = mergeData[i];
+
+ let saveOptions = {};
+ Object.assign(saveOptions, options);
+ // Tell _saveObjectFromJSON to save as unsynced
+ saveOptions.saveAsChanged = true;
+ saveOptions.notifierQueue = notifierQueue;
+
+ // Errors have to be thrown in order to roll back the transaction, so catch
+ // those here and continue
+ try {
+ yield Zotero.DB.executeTransaction(function* () {
+ let obj = yield objectsClass.getByLibraryAndKeyAsync(
+ libraryID, json.key, { noCache: true }
+ );
+ // Update object with merge data
+ if (obj) {
+ // Delete local object
+ if (json.deleted) {
+ try {
+ yield obj.erase({
+ notifierQueue
+ });
+ }
+ catch (e) {
+ results.push({
+ key: json.key,
+ processed: false,
+ error: e,
+ retry: false
+ });
+ throw e;
+ }
+ results.push({
+ key: json.key,
+ processed: true
+ });
+ return;
+ }
+
+ // Save merged changes below
+ }
+ // If no local object and merge wanted a delete, we're good
+ else if (json.deleted) {
+ results.push({
+ key: json.key,
+ processed: true
+ });
+ return;
+ }
+ // Recreate locally deleted object
+ else {
+ obj = new Zotero[ObjectType];
+ obj.libraryID = libraryID;
+ obj.key = json.key;
+ yield obj.loadPrimaryData();
+
+ // Don't cache new items immediately,
+ // which skips reloading after save
+ saveOptions.skipCache = true;
+ }
+
+ let saveResults = yield this._saveObjectFromJSON(
+ obj, json, saveOptions
+ );
+ results.push(saveResults);
+ if (!saveResults.processed) {
+ throw saveResults.error;
+ }
+ }.bind(this));
+
+ if (notifierQueue.size) {
+ notifierQueues.push(notifierQueue);
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+
+ if (options.onError) {
+ options.onError(e);
+ }
+
+ if (options.stopOnError) {
+ throw e;
+ }
+ }
+ }
+ }
+ finally {
+ if (notifierQueues.length) {
+ yield Zotero.Notifier.commit(notifierQueues);
+ }
+ }
+
+ return results;
+ }),
+
+
+ showConflictResolutionWindow: function (conflicts) {
Zotero.debug("Showing conflict resolution window");
var io = {
diff --git a/test/tests/syncEngineTest.js b/test/tests/syncEngineTest.js
@@ -102,6 +102,12 @@ describe("Zotero.Sync.Data.Engine", function () {
assert.propertyVal(cacheObject, 'key', obj.key);
});
+ var assertNotInCache = Zotero.Promise.coroutine(function* (obj) {
+ assert.isFalse(yield Zotero.Sync.Data.Local.getCacheObject(
+ obj.objectType, obj.libraryID, obj.key, obj.version
+ ));
+ });
+
//
// Tests
//
@@ -1636,6 +1642,392 @@ describe("Zotero.Sync.Data.Engine", function () {
});
})
+
+ describe("Conflict Resolution", function () {
+ beforeEach(function* () {
+ yield Zotero.DB.queryAsync("DELETE FROM syncCache");
+ })
+
+ after(function* () {
+ yield Zotero.DB.queryAsync("DELETE FROM syncCache");
+ })
+
+ it("should show conflict resolution window on item conflicts", function* () {
+ var libraryID = Zotero.Libraries.userLibraryID;
+ ({ engine, client, caller } = yield setup());
+ var type = 'item';
+ var objects = [];
+ var values = [];
+ var dateAdded = Date.now() - 86400000;
+ var responseJSON = [];
+
+ for (let i = 0; i < 2; i++) {
+ values.push({
+ left: {},
+ right: {}
+ });
+
+ // Create local object
+ let obj = objects[i] = yield createDataObject(
+ type,
+ {
+ version: 10,
+ dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
+ // Set Date Modified values one minute apart to enforce order
+ dateModified: Zotero.Date.dateToSQL(
+ new Date(dateAdded + (i * 60000)), true
+ )
+ }
+ );
+ let jsonData = obj.toJSON();
+ jsonData.key = obj.key;
+ jsonData.version = 10;
+ let json = {
+ key: obj.key,
+ version: jsonData.version,
+ data: jsonData
+ };
+ // Save original version in cache
+ yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
+
+ // Create updated JSON for download
+ values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
+ values[i].right.version = json.version = jsonData.version = 15;
+ responseJSON.push(json);
+
+ // Modify object locally
+ yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
+ values[i].left.title = obj.getField('title');
+ values[i].left.version = obj.getField('version');
+ }
+
+ setResponse({
+ method: "GET",
+ url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}`
+ + `&includeTrashed=1`,
+ status: 200,
+ headers: {
+ "Last-Modified-Version": 15
+ },
+ json: responseJSON
+ });
+
+ waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
+ var doc = dialog.document;
+ var wizard = doc.documentElement;
+ var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
+
+ // 1 (remote)
+ // Remote version should be selected by default
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ wizard.getButton('next').click();
+
+ // 2 (local)
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ // Select local object
+ mergeGroup.leftpane.click();
+ assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true');
+ if (Zotero.isMac) {
+ assert.isTrue(wizard.getButton('next').hidden);
+ assert.isFalse(wizard.getButton('finish').hidden);
+ }
+ else {
+ // TODO
+ }
+ wizard.getButton('finish').click();
+ })
+ yield engine._downloadObjects('item', objects.map(o => o.key));
+
+ assert.equal(objects[0].getField('title'), values[0].right.title);
+ assert.equal(objects[1].getField('title'), values[1].left.title);
+ assert.equal(objects[0].getField('version'), values[0].right.version);
+ assert.equal(objects[1].getField('version'), values[1].left.version);
+
+ var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
+ assert.lengthOf(keys, 0);
+ });
+
+ it("should resolve all remaining conflicts with one side", function* () {
+ var libraryID = Zotero.Libraries.userLibraryID;
+ ({ engine, client, caller } = yield setup());
+ var type = 'item';
+ var objects = [];
+ var values = [];
+ var responseJSON = [];
+ var dateAdded = Date.now() - 86400000;
+ for (let i = 0; i < 3; i++) {
+ values.push({
+ left: {},
+ right: {}
+ });
+
+ // Create object in cache
+ let obj = objects[i] = yield createDataObject(
+ type,
+ {
+ version: 10,
+ dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
+ // Set Date Modified values one minute apart to enforce order
+ dateModified: Zotero.Date.dateToSQL(
+ new Date(dateAdded + (i * 60000)), true
+ )
+ }
+ );
+ let jsonData = obj.toJSON();
+ jsonData.key = obj.key;
+ jsonData.version = 10;
+ let json = {
+ key: obj.key,
+ version: jsonData.version,
+ data: jsonData
+ };
+ // Save original version in cache
+ yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
+
+ // Create new version in cache, simulating a download
+ values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
+ values[i].right.version = json.version = jsonData.version = 15;
+ responseJSON.push(json);
+
+ // Modify object locally
+ yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
+ values[i].left.title = obj.getField('title');
+ values[i].left.version = obj.getField('version');
+ }
+
+ setResponse({
+ method: "GET",
+ url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}`
+ + `&includeTrashed=1`,
+ status: 200,
+ headers: {
+ "Last-Modified-Version": 15
+ },
+ json: responseJSON
+ });
+
+ waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
+ var doc = dialog.document;
+ var wizard = doc.documentElement;
+ var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
+ var resolveAll = doc.getElementById('resolve-all');
+
+ // 1 (remote)
+ // Remote version should be selected by default
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ assert.equal(
+ resolveAll.label,
+ Zotero.getString('sync.conflict.resolveAllRemoteFields')
+ );
+ wizard.getButton('next').click();
+
+ // 2 (local and Resolve All checkbox)
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ mergeGroup.leftpane.click();
+ assert.equal(
+ resolveAll.label,
+ Zotero.getString('sync.conflict.resolveAllLocalFields')
+ );
+ resolveAll.click();
+
+ if (Zotero.isMac) {
+ assert.isTrue(wizard.getButton('next').hidden);
+ assert.isFalse(wizard.getButton('finish').hidden);
+ }
+ else {
+ // TODO
+ }
+ wizard.getButton('finish').click();
+ })
+ yield engine._downloadObjects('item', objects.map(o => o.key));
+
+ assert.equal(objects[0].getField('title'), values[0].right.title);
+ assert.equal(objects[0].getField('version'), values[0].right.version);
+ assert.equal(objects[1].getField('title'), values[1].left.title);
+ assert.equal(objects[1].getField('version'), values[1].left.version);
+ assert.equal(objects[2].getField('title'), values[2].left.title);
+ assert.equal(objects[2].getField('version'), values[2].left.version);
+
+ var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
+ assert.lengthOf(keys, 0);
+ })
+
+ it("should handle local item deletion, keeping deletion", function* () {
+ var libraryID = Zotero.Libraries.userLibraryID;
+ ({ engine, client, caller } = yield setup());
+ var type = 'item';
+ var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
+ var responseJSON = [];
+
+ // Create object, generate JSON, and delete
+ var obj = yield createDataObject(type, { version: 10 });
+ var jsonData = obj.toJSON();
+ var key = jsonData.key = obj.key;
+ jsonData.version = 10;
+ let json = {
+ key: obj.key,
+ version: jsonData.version,
+ data: jsonData
+ };
+ // Delete object locally
+ yield obj.eraseTx();
+
+ json.version = jsonData.version = 15;
+ jsonData.title = Zotero.Utilities.randomString();
+ responseJSON.push(json);
+
+ setResponse({
+ method: "GET",
+ url: `users/1/items?format=json&itemKey=${obj.key}&includeTrashed=1`,
+ status: 200,
+ headers: {
+ "Last-Modified-Version": 15
+ },
+ json: responseJSON
+ });
+
+ var windowOpened = false;
+ waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
+ windowOpened = true;
+
+ var doc = dialog.document;
+ var wizard = doc.documentElement;
+ var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
+
+ // Remote version should be selected by default
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ assert.ok(mergeGroup.leftpane.pane.onclick);
+ // Select local deleted version
+ mergeGroup.leftpane.pane.click();
+ wizard.getButton('finish').click();
+ })
+ yield engine._downloadObjects('item', [obj.key]);
+ assert.isTrue(windowOpened);
+
+ obj = objectsClass.getByLibraryAndKey(libraryID, key);
+ assert.isFalse(obj);
+
+ var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
+ assert.lengthOf(keys, 0);
+ })
+
+ it("should restore locally deleted item", function* () {
+ var libraryID = Zotero.Libraries.userLibraryID;
+ ({ engine, client, caller } = yield setup());
+ var type = 'item';
+ var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
+ var responseJSON = [];
+
+ // Create object, generate JSON, and delete
+ var obj = yield createDataObject(type, { version: 10 });
+ var jsonData = obj.toJSON();
+ var key = jsonData.key = obj.key;
+ jsonData.version = 10;
+ let json = {
+ key: obj.key,
+ version: jsonData.version,
+ data: jsonData
+ };
+ yield obj.eraseTx();
+
+ json.version = jsonData.version = 15;
+ jsonData.title = Zotero.Utilities.randomString();
+ responseJSON.push(json);
+
+ setResponse({
+ method: "GET",
+ url: `users/1/items?format=json&itemKey=${key}&includeTrashed=1`,
+ status: 200,
+ headers: {
+ "Last-Modified-Version": 15
+ },
+ json: responseJSON
+ });
+
+ waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
+ var doc = dialog.document;
+ var wizard = doc.documentElement;
+ var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
+
+ assert.isTrue(doc.getElementById('resolve-all').hidden);
+
+ // Remote version should be selected by default
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ wizard.getButton('finish').click();
+ })
+ yield engine._downloadObjects('item', [key]);
+
+ obj = objectsClass.getByLibraryAndKey(libraryID, key);
+ assert.ok(obj);
+ assert.equal(obj.getField('title'), jsonData.title);
+
+ var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
+ assert.lengthOf(keys, 0);
+ })
+
+ it("should handle note conflict", function* () {
+ var libraryID = Zotero.Libraries.userLibraryID;
+ ({ engine, client, caller } = yield setup());
+ var type = 'item';
+ var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
+ var responseJSON = [];
+
+ var noteText1 = "<p>A</p>";
+ var noteText2 = "<p>B</p>";
+
+ // Create object in cache
+ var obj = new Zotero.Item('note');
+ obj.setNote("");
+ obj.version = 10;
+ yield obj.saveTx();
+ var jsonData = obj.toJSON();
+ var key = jsonData.key = obj.key;
+ let json = {
+ key: obj.key,
+ version: jsonData.version,
+ data: jsonData
+ };
+ yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
+
+ // Create new version in cache, simulating a download
+ json.version = jsonData.version = 15;
+ json.data.note = noteText2;
+ responseJSON.push(json);
+
+ // Modify local version
+ obj.setNote(noteText1);
+
+ setResponse({
+ method: "GET",
+ url: `users/1/items?format=json&itemKey=${key}&includeTrashed=1`,
+ status: 200,
+ headers: {
+ "Last-Modified-Version": 15
+ },
+ json: responseJSON
+ });
+
+ waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
+ var doc = dialog.document;
+ var wizard = doc.documentElement;
+ var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
+
+ // Remote version should be selected by default
+ assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
+ wizard.getButton('finish').click();
+ })
+ yield engine._downloadObjects('item', [key]);
+
+ obj = objectsClass.getByLibraryAndKey(libraryID, key);
+ assert.ok(obj);
+ assert.equal(obj.getNote(), noteText2);
+
+ var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
+ assert.lengthOf(keys, 0);
+ })
+ });
+
+
describe("#_upgradeCheck()", function () {
it("should upgrade a library last synced with the classic sync architecture", function* () {
var userLibraryID = Zotero.Libraries.userLibraryID;
diff --git a/test/tests/syncLocalTest.js b/test/tests/syncLocalTest.js
@@ -547,334 +547,6 @@ describe("Zotero.Sync.Data.Local", function() {
});
});
- describe("Conflict Resolution", function () {
- beforeEach(function* () {
- yield Zotero.DB.queryAsync("DELETE FROM syncCache");
- })
-
- after(function* () {
- yield Zotero.DB.queryAsync("DELETE FROM syncCache");
- })
-
- it("should show conflict resolution window on item conflicts", function* () {
- var libraryID = Zotero.Libraries.userLibraryID;
-
- var type = 'item';
- var objects = [];
- var values = [];
- var dateAdded = Date.now() - 86400000;
- var downloadedJSON = [];
- for (let i = 0; i < 2; i++) {
- values.push({
- left: {},
- right: {}
- });
-
- // Create object in cache
- let obj = objects[i] = yield createDataObject(
- type,
- {
- version: 10,
- dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
- // Set Date Modified values one minute apart to enforce order
- dateModified: Zotero.Date.dateToSQL(
- new Date(dateAdded + (i * 60000)), true
- )
- }
- );
- let jsonData = obj.toJSON();
- jsonData.key = obj.key;
- jsonData.version = 10;
- let json = {
- key: obj.key,
- version: jsonData.version,
- data: jsonData
- };
- yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
-
- // Create updated JSON, simulating a download
- values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
- values[i].right.version = json.version = jsonData.version = 15;
- downloadedJSON.push(json);
-
- // Modify object locally
- yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
- values[i].left.title = obj.getField('title');
- values[i].left.version = obj.getField('version');
- }
-
- waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
- var doc = dialog.document;
- var wizard = doc.documentElement;
- var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
-
- // 1 (remote)
- // Remote version should be selected by default
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- wizard.getButton('next').click();
-
- // 2 (local)
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- // Select local object
- mergeGroup.leftpane.click();
- assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true');
- if (Zotero.isMac) {
- assert.isTrue(wizard.getButton('next').hidden);
- assert.isFalse(wizard.getButton('finish').hidden);
- }
- else {
- // TODO
- }
- wizard.getButton('finish').click();
- })
- yield Zotero.Sync.Data.Local.processObjectsFromJSON(
- type, libraryID, downloadedJSON, { stopOnError: true }
- );
-
- assert.equal(objects[0].getField('title'), values[0].right.title);
- assert.equal(objects[1].getField('title'), values[1].left.title);
- assert.equal(objects[0].getField('version'), values[0].right.version);
- assert.equal(objects[1].getField('version'), values[1].left.version);
- })
-
- it("should resolve all remaining conflicts with one side", function* () {
- var libraryID = Zotero.Libraries.userLibraryID;
-
- var type = 'item';
-
- var objects = [];
- var values = [];
- var downloadedJSON = [];
- var dateAdded = Date.now() - 86400000;
- for (let i = 0; i < 3; i++) {
- values.push({
- left: {},
- right: {}
- });
-
- // Create object in cache
- let obj = objects[i] = yield createDataObject(
- type,
- {
- version: 10,
- dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
- // Set Date Modified values one minute apart to enforce order
- dateModified: Zotero.Date.dateToSQL(
- new Date(dateAdded + (i * 60000)), true
- )
- }
- );
- let jsonData = obj.toJSON();
- jsonData.key = obj.key;
- jsonData.version = 10;
- let json = {
- key: obj.key,
- version: jsonData.version,
- data: jsonData
- };
- yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
-
- // Create new version in cache, simulating a download
- values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
- values[i].right.version = json.version = jsonData.version = 15;
- yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
- downloadedJSON.push(json);
-
- // Modify object locally
- yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
- values[i].left.title = obj.getField('title');
- values[i].left.version = obj.getField('version');
- }
-
- waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
- var doc = dialog.document;
- var wizard = doc.documentElement;
- var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
- var resolveAll = doc.getElementById('resolve-all');
-
- // 1 (remote)
- // Remote version should be selected by default
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- assert.equal(
- resolveAll.label,
- Zotero.getString('sync.conflict.resolveAllRemoteFields')
- );
- wizard.getButton('next').click();
-
- // 2 (local and Resolve All checkbox)
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- mergeGroup.leftpane.click();
- assert.equal(
- resolveAll.label,
- Zotero.getString('sync.conflict.resolveAllLocalFields')
- );
- resolveAll.click();
-
- if (Zotero.isMac) {
- assert.isTrue(wizard.getButton('next').hidden);
- assert.isFalse(wizard.getButton('finish').hidden);
- }
- else {
- // TODO
- }
- wizard.getButton('finish').click();
- })
- yield Zotero.Sync.Data.Local.processObjectsFromJSON(
- type, libraryID, downloadedJSON, { stopOnError: true }
- );
-
- assert.equal(objects[0].getField('title'), values[0].right.title);
- assert.equal(objects[0].getField('version'), values[0].right.version);
- assert.equal(objects[1].getField('title'), values[1].left.title);
- assert.equal(objects[1].getField('version'), values[1].left.version);
- assert.equal(objects[2].getField('title'), values[2].left.title);
- assert.equal(objects[2].getField('version'), values[2].left.version);
- })
-
- it("should handle local item deletion, keeping deletion", function* () {
- var libraryID = Zotero.Libraries.userLibraryID;
-
- var type = 'item';
- var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
-
- var downloadedJSON = [];
-
- // Create object, generate JSON, and delete
- var obj = yield createDataObject(type, { version: 10 });
- var jsonData = obj.toJSON();
- var key = jsonData.key = obj.key;
- jsonData.version = 10;
- let json = {
- key: obj.key,
- version: jsonData.version,
- data: jsonData
- };
- // Delete object locally
- yield obj.eraseTx();
-
- // Create new version in cache, simulating a download
- json.version = jsonData.version = 15;
- jsonData.title = Zotero.Utilities.randomString();
- downloadedJSON.push(json);
-
- var windowOpened = false;
- waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
- windowOpened = true;
-
- var doc = dialog.document;
- var wizard = doc.documentElement;
- var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
-
- // Remote version should be selected by default
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- assert.ok(mergeGroup.leftpane.pane.onclick);
- // Select local deleted version
- mergeGroup.leftpane.pane.click();
- wizard.getButton('finish').click();
- })
- yield Zotero.Sync.Data.Local.processObjectsFromJSON(
- type, libraryID, downloadedJSON, { stopOnError: true }
- );
- assert.isTrue(windowOpened);
-
- obj = objectsClass.getByLibraryAndKey(libraryID, key);
- assert.isFalse(obj);
- })
-
- it("should restore locally deleted item", function* () {
- var libraryID = Zotero.Libraries.userLibraryID;
-
- var type = 'item';
- var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
-
- var downloadedJSON = [];
-
- // Create object, generate JSON, and delete
- var obj = yield createDataObject(type, { version: 10 });
- var jsonData = obj.toJSON();
- var key = jsonData.key = obj.key;
- jsonData.version = 10;
- let json = {
- key: obj.key,
- version: jsonData.version,
- data: jsonData
- };
- yield obj.eraseTx();
-
- // Create new version in cache, simulating a download
- json.version = jsonData.version = 15;
- jsonData.title = Zotero.Utilities.randomString();
- downloadedJSON.push(json);
-
- waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
- var doc = dialog.document;
- var wizard = doc.documentElement;
- var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
-
- assert.isTrue(doc.getElementById('resolve-all').hidden);
-
- // Remote version should be selected by default
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- wizard.getButton('finish').click();
- })
- yield Zotero.Sync.Data.Local.processObjectsFromJSON(
- type, libraryID, downloadedJSON, { stopOnError: true }
- );
-
- obj = objectsClass.getByLibraryAndKey(libraryID, key);
- assert.ok(obj);
- assert.equal(obj.getField('title'), jsonData.title);
- })
-
- it("should handle note conflict", function* () {
- var libraryID = Zotero.Libraries.userLibraryID;
- var type = 'item';
- var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
- var downloadedJSON = [];
-
- var noteText1 = "<p>A</p>";
- var noteText2 = "<p>B</p>";
-
- // Create object in cache
- var obj = new Zotero.Item('note');
- obj.setNote("");
- obj.version = 10;
- yield obj.saveTx();
- var jsonData = obj.toJSON();
- var key = jsonData.key = obj.key;
- let json = {
- key: obj.key,
- version: jsonData.version,
- data: jsonData
- };
- yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
-
- // Create new version in cache, simulating a download
- json.version = jsonData.version = 15;
- json.data.note = noteText2;
- downloadedJSON.push(json);
-
- // Modify local version
- obj.setNote(noteText1);
-
- waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
- var doc = dialog.document;
- var wizard = doc.documentElement;
- var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0];
-
- // Remote version should be selected by default
- assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
- wizard.getButton('finish').click();
- })
- yield Zotero.Sync.Data.Local.processObjectsFromJSON(
- type, libraryID, downloadedJSON, { stopOnError: true }
- );
-
- obj = objectsClass.getByLibraryAndKey(libraryID, key);
- assert.ok(obj);
- assert.equal(obj.getNote(), noteText2);
- })
- })
describe("#_reconcileChanges()", function () {
describe("items", function () {