Close #975, Process conflicts for all batches together
This commit is contained in:
parent
f8a0f9ad1d
commit
391f525a75
|
@ -326,7 +326,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
var mergeData = Zotero.Sync.Data.Local.resolveConflicts(conflicts);
|
var mergeData = Zotero.Sync.Data.Local.showConflictResolutionWindow(conflicts);
|
||||||
if (mergeData) {
|
if (mergeData) {
|
||||||
let concurrentObjects = 50;
|
let concurrentObjects = 50;
|
||||||
yield Zotero.Utilities.Internal.forEachChunkAsync(
|
yield Zotero.Utilities.Internal.forEachChunkAsync(
|
||||||
|
@ -562,6 +562,8 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
|
||||||
+ " in " + this.library.name
|
+ " in " + this.library.name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var conflicts = [];
|
||||||
|
|
||||||
// Process batches as soon as they're available
|
// Process batches as soon as they're available
|
||||||
yield Zotero.Promise.map(
|
yield Zotero.Promise.map(
|
||||||
json,
|
json,
|
||||||
|
@ -592,6 +594,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
|
||||||
)
|
)
|
||||||
.then(function (results) {
|
.then(function (results) {
|
||||||
let processedKeys = [];
|
let processedKeys = [];
|
||||||
|
let conflictResults = [];
|
||||||
results.forEach(x => {
|
results.forEach(x => {
|
||||||
// If data was processed, remove JSON
|
// If data was processed, remove JSON
|
||||||
if (x.processed) {
|
if (x.processed) {
|
||||||
|
@ -601,8 +604,12 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
|
||||||
if (x.processed || !x.retry) {
|
if (x.processed || !x.retry) {
|
||||||
processedKeys.push(x.key);
|
processedKeys.push(x.key);
|
||||||
}
|
}
|
||||||
|
if (x.conflict) {
|
||||||
|
conflictResults.push(x);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
keys = Zotero.Utilities.arrayDiff(keys, processedKeys);
|
keys = Zotero.Utilities.arrayDiff(keys, processedKeys);
|
||||||
|
conflicts.push(...conflictResults);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
);
|
);
|
||||||
|
@ -630,7 +637,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
|
||||||
this.failedItems = [];
|
this.failedItems = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastLength = keys.length;
|
lastLength = keys.length;
|
||||||
|
@ -638,6 +645,16 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
|
||||||
var remainingObjectDesc = `${keys.length == 1 ? objectType : objectTypePlural}`;
|
var remainingObjectDesc = `${keys.length == 1 ? objectType : objectTypePlural}`;
|
||||||
Zotero.debug(`Retrying ${keys.length} remaining ${remainingObjectDesc}`);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -485,11 +485,17 @@ Zotero.Sync.Data.Local = {
|
||||||
/**
|
/**
|
||||||
* Process downloaded JSON and update local objects
|
* Process downloaded JSON and update local objects
|
||||||
*
|
*
|
||||||
* @return {Promise<Array<Object>>} - Promise for an array of objects with the following properties:
|
* @return {Promise<Object[]>} - Promise for an array of objects with the following properties:
|
||||||
* {String} key
|
* {String} key
|
||||||
* {Boolean} processed
|
* {Boolean} processed
|
||||||
* {Object} [error]
|
* {Object} [error]
|
||||||
* {Boolean} [retry]
|
* {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 = {}) {
|
processObjectsFromJSON: Zotero.Promise.coroutine(function* (objectType, libraryID, json, options = {}) {
|
||||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||||
|
@ -508,7 +514,6 @@ Zotero.Sync.Data.Local = {
|
||||||
+ " for " + libraryName);
|
+ " for " + libraryName);
|
||||||
|
|
||||||
var results = [];
|
var results = [];
|
||||||
var conflicts = [];
|
|
||||||
|
|
||||||
if (!json.length) {
|
if (!json.length) {
|
||||||
return results;
|
return results;
|
||||||
|
@ -658,7 +663,10 @@ Zotero.Sync.Data.Local = {
|
||||||
Zotero.debug(jsonDataLocal);
|
Zotero.debug(jsonDataLocal);
|
||||||
Zotero.debug(jsonData);
|
Zotero.debug(jsonData);
|
||||||
Zotero.debug(result);
|
Zotero.debug(result);
|
||||||
conflicts.push({
|
results.push({
|
||||||
|
key: objectKey,
|
||||||
|
processed: false,
|
||||||
|
conflict: true,
|
||||||
left: jsonDataLocal,
|
left: jsonDataLocal,
|
||||||
right: jsonData,
|
right: jsonData,
|
||||||
changes: result.changes,
|
changes: result.changes,
|
||||||
|
@ -699,7 +707,10 @@ Zotero.Sync.Data.Local = {
|
||||||
|
|
||||||
switch (objectType) {
|
switch (objectType) {
|
||||||
case 'item':
|
case 'item':
|
||||||
conflicts.push({
|
results.push({
|
||||||
|
key: objectKey,
|
||||||
|
processed: false,
|
||||||
|
conflict: true,
|
||||||
left: {
|
left: {
|
||||||
deleted: true,
|
deleted: true,
|
||||||
dateDeleted: Zotero.Date.dateToSQL(dateDeleted, 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 processed = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
results.forEach(x => x.processed ? processed++ : skipped++);
|
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");
|
Zotero.debug("Showing conflict resolution window");
|
||||||
|
|
||||||
var io = {
|
var io = {
|
||||||
|
|
|
@ -102,6 +102,12 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
assert.propertyVal(cacheObject, 'key', obj.key);
|
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
|
// 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 () {
|
describe("#_upgradeCheck()", function () {
|
||||||
it("should upgrade a library last synced with the classic sync architecture", function* () {
|
it("should upgrade a library last synced with the classic sync architecture", function* () {
|
||||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
|
@ -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("#_reconcileChanges()", function () {
|
||||||
describe("items", function () {
|
describe("items", function () {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user