From 391f525a75492a1de18810611b184df9da9b48a1 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 3 May 2016 23:09:38 -0400 Subject: [PATCH] Close #975, Process conflicts for all batches together --- .../content/zotero/xpcom/sync/syncEngine.js | 21 +- chrome/content/zotero/xpcom/sync/syncLocal.js | 316 +++++++------- test/tests/syncEngineTest.js | 392 ++++++++++++++++++ test/tests/syncLocalTest.js | 328 --------------- 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 index b8cae9e02..90c87eb1f 100644 --- 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 index 842a68cb1..adc747128 100644 --- 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>} - Promise for an array of objects with the following properties: - * {String} key - * {Boolean} processed - * {Object} [error] - * {Boolean} [retry] + * @return {Promise} - 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 index b2df3d72b..d8f96bb2b 100644 --- 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 = "

A

"; + var noteText2 = "

B

"; + + // 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 index 5a5cdda82..19da5cbe6 100644 --- 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 = "

A

"; - var noteText2 = "

B

"; - - // 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 () {