Fix various conflict resolution bugs
Among other things, when choosing the local side for a conflict, the remote version could still end up being saved.
This commit is contained in:
parent
7f8699b937
commit
f0770fa84d
|
@ -168,7 +168,6 @@ var Zotero_Merge_Window = new function () {
|
||||||
if (x.data) {
|
if (x.data) {
|
||||||
x.data.version = _conflicts[i][x.selected].version;
|
x.data.version = _conflicts[i][x.selected].version;
|
||||||
}
|
}
|
||||||
a[i] = x.data;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
_io.dataOut = _merged;
|
_io.dataOut = _merged;
|
||||||
|
|
|
@ -880,10 +880,9 @@ Zotero.Sync.Data.Engine.prototype._downloadDeletions = Zotero.Promise.coroutine(
|
||||||
function (chunk) {
|
function (chunk) {
|
||||||
return Zotero.DB.executeTransaction(function* () {
|
return Zotero.DB.executeTransaction(function* () {
|
||||||
for (let json of chunk) {
|
for (let json of chunk) {
|
||||||
if (!json.deleted) continue;
|
let data = json.data;
|
||||||
let obj = objectsClass.getByLibraryAndKey(
|
if (!data.deleted) continue;
|
||||||
this.libraryID, json.key
|
let obj = objectsClass.getByLibraryAndKey(this.libraryID, data.key);
|
||||||
);
|
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
Zotero.logError("Remotely deleted " + objectType
|
Zotero.logError("Remotely deleted " + objectType
|
||||||
+ " didn't exist after conflict resolution");
|
+ " didn't exist after conflict resolution");
|
||||||
|
|
|
@ -835,7 +835,6 @@ Zotero.Sync.Data.Local = {
|
||||||
let cachedJSON = yield this.getCacheObject(
|
let cachedJSON = yield this.getCacheObject(
|
||||||
objectType, obj.libraryID, obj.key, obj.version
|
objectType, obj.libraryID, obj.key, obj.version
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = this._reconcileChanges(
|
let result = this._reconcileChanges(
|
||||||
objectType,
|
objectType,
|
||||||
cachedJSON.data,
|
cachedJSON.data,
|
||||||
|
@ -844,20 +843,21 @@ Zotero.Sync.Data.Local = {
|
||||||
['mtime', 'md5', 'dateAdded', 'dateModified']
|
['mtime', 'md5', 'dateAdded', 'dateModified']
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no changes, update local version number and mark as synced
|
// If no changes, just update local version number and mark as synced
|
||||||
if (!result.changes.length && !result.conflicts.length) {
|
if (!result.changes.length && !result.conflicts.length) {
|
||||||
Zotero.debug("No remote changes to apply to local "
|
Zotero.debug("No remote changes to apply to local "
|
||||||
+ objectType + " " + obj.libraryKey);
|
+ objectType + " " + obj.libraryKey);
|
||||||
|
saveOptions.skipData = true;
|
||||||
|
// If local object is different but we ignored the changes
|
||||||
|
// (e.g., ISBN hyphenation), save as unsynced. Since we're skipping
|
||||||
|
// data, the local version won't be overwritten.
|
||||||
|
if (result.localChanged) {
|
||||||
|
saveOptions.saveAsUnsynced = true;
|
||||||
|
}
|
||||||
let saveResults = yield this._saveObjectFromJSON(
|
let saveResults = yield this._saveObjectFromJSON(
|
||||||
obj,
|
obj,
|
||||||
jsonObject,
|
jsonObject,
|
||||||
{
|
saveOptions
|
||||||
skipData: true,
|
|
||||||
notifierQueue,
|
|
||||||
// Save as unsynced
|
|
||||||
saveAsChanged: !!result.localChanged
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
results.push(saveResults);
|
results.push(saveResults);
|
||||||
if (!saveResults.processed) {
|
if (!saveResults.processed) {
|
||||||
|
@ -902,11 +902,6 @@ Zotero.Sync.Data.Local = {
|
||||||
jsonDataLocal[x] = jsonData[x];
|
jsonDataLocal[x] = jsonData[x];
|
||||||
})
|
})
|
||||||
jsonObject.data = jsonDataLocal;
|
jsonObject.data = jsonDataLocal;
|
||||||
|
|
||||||
// Save as unsynced
|
|
||||||
if (results.localChanged) {
|
|
||||||
saveOptions.saveAsChanged = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Object doesn't exist locally
|
// Object doesn't exist locally
|
||||||
|
@ -1190,19 +1185,26 @@ Zotero.Sync.Data.Local = {
|
||||||
let notifierQueues = [];
|
let notifierQueues = [];
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < mergeData.length; i++) {
|
for (let i = 0; i < mergeData.length; i++) {
|
||||||
// Batch notifier updates
|
// Batch notifier updates, despite multiple transactions
|
||||||
if (notifierQueues.length == batchSize) {
|
if (notifierQueues.length == batchSize) {
|
||||||
yield Zotero.Notifier.commit(notifierQueues);
|
yield Zotero.Notifier.commit(notifierQueues);
|
||||||
notifierQueues = [];
|
notifierQueues = [];
|
||||||
}
|
}
|
||||||
let notifierQueue = new Zotero.Notifier.Queue;
|
let notifierQueue = new Zotero.Notifier.Queue;
|
||||||
|
|
||||||
let json = mergeData[i];
|
let json = mergeData[i].data;
|
||||||
|
|
||||||
let saveOptions = {};
|
let saveOptions = {};
|
||||||
Object.assign(saveOptions, options);
|
Object.assign(saveOptions, options);
|
||||||
// Tell _saveObjectFromJSON() to save as unsynced
|
// If choosing local object, save as unsynced with remote version (or 0 if remote is
|
||||||
saveOptions.saveAsChanged = true;
|
// deleted) and remote object in cache, to simulate a save and edit
|
||||||
|
if (mergeData[i].selected == 'left') {
|
||||||
|
json.version = conflicts[i].right.version || 0;
|
||||||
|
saveOptions.saveAsUnsynced = true;
|
||||||
|
if (conflicts[i].right.version) {
|
||||||
|
saveOptions.cacheObject = conflicts[i].right;
|
||||||
|
}
|
||||||
|
}
|
||||||
saveOptions.notifierQueue = notifierQueue;
|
saveOptions.notifierQueue = notifierQueue;
|
||||||
|
|
||||||
// Errors have to be thrown in order to roll back the transaction, so catch
|
// Errors have to be thrown in order to roll back the transaction, so catch
|
||||||
|
@ -1259,9 +1261,7 @@ Zotero.Sync.Data.Local = {
|
||||||
saveOptions.skipCache = true;
|
saveOptions.skipCache = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let saveResults = yield this._saveObjectFromJSON(
|
let saveResults = yield this._saveObjectFromJSON(obj, json, saveOptions);
|
||||||
obj, json, saveOptions
|
|
||||||
);
|
|
||||||
results.push(saveResults);
|
results.push(saveResults);
|
||||||
if (!saveResults.processed) {
|
if (!saveResults.processed) {
|
||||||
throw saveResults.error;
|
throw saveResults.error;
|
||||||
|
@ -1360,7 +1360,7 @@ Zotero.Sync.Data.Local = {
|
||||||
yield this._checkAttachmentForDownload(obj, json.data.mtime, options.isNewObject);
|
yield this._checkAttachmentForDownload(obj, json.data.mtime, options.isNewObject);
|
||||||
}
|
}
|
||||||
obj.version = json.data.version;
|
obj.version = json.data.version;
|
||||||
if (!options.saveAsChanged) {
|
if (!options.saveAsUnsynced) {
|
||||||
obj.synced = true;
|
obj.synced = true;
|
||||||
}
|
}
|
||||||
yield obj.save({
|
yield obj.save({
|
||||||
|
@ -1374,13 +1374,17 @@ Zotero.Sync.Data.Local = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
yield this.saveCacheObject(obj.objectType, obj.libraryID, json.data);
|
let cacheJSON = options.cacheObject ? options.cacheObject : json.data;
|
||||||
results.processed = true;
|
yield this.saveCacheObject(obj.objectType, obj.libraryID, cacheJSON);
|
||||||
|
|
||||||
// Delete older versions of the object in the cache
|
// Delete older versions of the object in the cache
|
||||||
yield this.deleteCacheObjectVersions(
|
yield this.deleteCacheObjectVersions(
|
||||||
obj.objectType, obj.libraryID, json.key, null, json.version - 1
|
obj.objectType,
|
||||||
|
obj.libraryID,
|
||||||
|
json.key,
|
||||||
|
null,
|
||||||
|
cacheJSON.version - 1
|
||||||
);
|
);
|
||||||
|
results.processed = true;
|
||||||
|
|
||||||
// Delete from sync queue
|
// Delete from sync queue
|
||||||
yield this._removeObjectFromSyncQueue(obj.objectType, obj.libraryID, json.key);
|
yield this._removeObjectFromSyncQueue(obj.objectType, obj.libraryID, json.key);
|
||||||
|
@ -1519,7 +1523,7 @@ Zotero.Sync.Data.Local = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changes: changeset2,
|
changes: changeset2,
|
||||||
conflicts: conflicts
|
conflicts
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -2972,9 +2972,9 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should show conflict resolution window on item conflicts", function* () {
|
it("should show conflict resolution window on item conflicts", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
({ engine, client, caller } = yield setup());
|
({ engine, client, caller } = await setup());
|
||||||
var type = 'item';
|
var type = 'item';
|
||||||
var objects = [];
|
var objects = [];
|
||||||
var values = [];
|
var values = [];
|
||||||
|
@ -2988,7 +2988,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create local object
|
// Create local object
|
||||||
let obj = objects[i] = yield createDataObject(
|
let obj = objects[i] = await createDataObject(
|
||||||
type,
|
type,
|
||||||
{
|
{
|
||||||
version: 10,
|
version: 10,
|
||||||
|
@ -3008,7 +3008,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
data: jsonData
|
data: jsonData
|
||||||
};
|
};
|
||||||
// Save original version in cache
|
// Save original version in cache
|
||||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||||
|
|
||||||
// Create updated JSON for download
|
// Create updated JSON for download
|
||||||
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
||||||
|
@ -3016,7 +3016,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
responseJSON.push(json);
|
responseJSON.push(json);
|
||||||
|
|
||||||
// Modify object locally
|
// Modify object locally
|
||||||
yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
||||||
values[i].left.title = obj.getField('title');
|
values[i].left.title = obj.getField('title');
|
||||||
values[i].left.version = obj.getField('version');
|
values[i].left.version = obj.getField('version');
|
||||||
}
|
}
|
||||||
|
@ -3056,20 +3056,29 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
}
|
}
|
||||||
wizard.getButton('finish').click();
|
wizard.getButton('finish').click();
|
||||||
})
|
})
|
||||||
yield engine._downloadObjects('item', objects.map(o => o.key));
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
||||||
|
|
||||||
assert.equal(objects[0].getField('title'), values[0].right.title);
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
||||||
assert.equal(objects[1].getField('title'), values[1].left.title);
|
assert.equal(objects[1].getField('title'), values[1].left.title);
|
||||||
assert.equal(objects[0].getField('version'), values[0].right.version);
|
assert.equal(objects[0].getField('version'), values[0].right.version);
|
||||||
assert.equal(objects[1].getField('version'), values[1].left.version);
|
assert.equal(objects[1].getField('version'), values[1].right.version);
|
||||||
|
|
||||||
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
// Cache versions should match remote
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
||||||
|
'item', libraryID, objects[i].key, values[i].right.version
|
||||||
|
);
|
||||||
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
||||||
|
assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
||||||
assert.lengthOf(keys, 0);
|
assert.lengthOf(keys, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show conflict resolution window on note conflicts", function* () {
|
it("should show conflict resolution window on note conflicts", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
({ engine, client, caller } = yield setup());
|
({ engine, client, caller } = await setup());
|
||||||
var type = 'item';
|
var type = 'item';
|
||||||
var objects = [];
|
var objects = [];
|
||||||
var values = [];
|
var values = [];
|
||||||
|
@ -3091,7 +3100,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
obj.dateModified = Zotero.Date.dateToSQL(
|
obj.dateModified = Zotero.Date.dateToSQL(
|
||||||
new Date(dateAdded + (i * 60000)), true
|
new Date(dateAdded + (i * 60000)), true
|
||||||
);
|
);
|
||||||
yield obj.saveTx();
|
await obj.saveTx();
|
||||||
|
|
||||||
let jsonData = obj.toJSON();
|
let jsonData = obj.toJSON();
|
||||||
jsonData.key = obj.key;
|
jsonData.key = obj.key;
|
||||||
|
@ -3102,7 +3111,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
data: jsonData
|
data: jsonData
|
||||||
};
|
};
|
||||||
// Save original version in cache
|
// Save original version in cache
|
||||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
||||||
|
|
||||||
// Create updated JSON for download
|
// Create updated JSON for download
|
||||||
values[i].right.note = jsonData.note = Zotero.Utilities.randomString();
|
values[i].right.note = jsonData.note = Zotero.Utilities.randomString();
|
||||||
|
@ -3111,7 +3120,7 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
|
|
||||||
// Modify object locally
|
// Modify object locally
|
||||||
obj.setNote(Zotero.Utilities.randomString());
|
obj.setNote(Zotero.Utilities.randomString());
|
||||||
yield obj.saveTx({
|
await obj.saveTx({
|
||||||
skipDateModifiedUpdate: true
|
skipDateModifiedUpdate: true
|
||||||
});
|
});
|
||||||
values[i].left.note = obj.getNote();
|
values[i].left.note = obj.getNote();
|
||||||
|
@ -3153,21 +3162,33 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
}
|
}
|
||||||
wizard.getButton('finish').click();
|
wizard.getButton('finish').click();
|
||||||
});
|
});
|
||||||
yield engine._downloadObjects('item', objects.map(o => o.key));
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
||||||
|
|
||||||
assert.equal(objects[0].getNote(), values[0].right.note);
|
assert.equal(objects[0].getNote(), values[0].right.note);
|
||||||
assert.equal(objects[1].getNote(), values[1].left.note);
|
assert.equal(objects[1].getNote(), values[1].left.note);
|
||||||
assert.equal(objects[0].getField('version'), values[0].right.version);
|
assert.equal(objects[0].version, values[0].right.version);
|
||||||
assert.equal(objects[1].getField('version'), values[1].left.version);
|
assert.equal(objects[1].version, values[1].right.version);
|
||||||
|
assert.isTrue(objects[0].synced);
|
||||||
|
assert.isFalse(objects[1].synced);
|
||||||
|
|
||||||
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
// Cache versions should match remote
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
||||||
|
'item', libraryID, objects[i].key, values[i].right.version
|
||||||
|
);
|
||||||
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
||||||
|
assert.nestedPropertyVal(cacheJSON, 'data.note', values[i].right.note);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
||||||
assert.lengthOf(keys, 0);
|
assert.lengthOf(keys, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should resolve all remaining conflicts with one side", function* () {
|
it("should resolve all remaining conflicts with local version", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
({ engine, client, caller } = yield setup());
|
({ engine, client, caller } = await setup());
|
||||||
var type = 'item';
|
var collectionA = await createDataObject('collection');
|
||||||
|
var collectionB = await createDataObject('collection');
|
||||||
var objects = [];
|
var objects = [];
|
||||||
var values = [];
|
var values = [];
|
||||||
var responseJSON = [];
|
var responseJSON = [];
|
||||||
|
@ -3179,8 +3200,8 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create object in cache
|
// Create object in cache
|
||||||
let obj = objects[i] = yield createDataObject(
|
let obj = objects[i] = await createDataObject(
|
||||||
type,
|
'item',
|
||||||
{
|
{
|
||||||
version: 10,
|
version: 10,
|
||||||
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
||||||
|
@ -3199,17 +3220,26 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
data: jsonData
|
data: jsonData
|
||||||
};
|
};
|
||||||
// Save original version in cache
|
// Save original version in cache
|
||||||
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
||||||
|
|
||||||
// Create new version in cache, simulating a download
|
// Create remote version
|
||||||
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
||||||
|
values[i].right.publisher = jsonData.publisher = Zotero.Utilities.randomString();
|
||||||
|
values[i].right.collections = jsonData.collections = [collectionB.key];
|
||||||
values[i].right.version = json.version = jsonData.version = 15;
|
values[i].right.version = json.version = jsonData.version = 15;
|
||||||
responseJSON.push(json);
|
responseJSON.push(json);
|
||||||
|
|
||||||
// Modify object locally
|
// Modify object locally
|
||||||
yield modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
obj.setField('title', Zotero.Utilities.randomString());
|
||||||
|
obj.setField('extra', Zotero.Utilities.randomString());
|
||||||
|
obj.setCollections([collectionA.key]);
|
||||||
|
await obj.saveTx({
|
||||||
|
skipDateModifiedUpdate: true
|
||||||
|
});
|
||||||
values[i].left.title = obj.getField('title');
|
values[i].left.title = obj.getField('title');
|
||||||
values[i].left.version = obj.getField('version');
|
values[i].left.extra = obj.getField('extra');
|
||||||
|
values[i].left.collections = [collectionA.key];
|
||||||
|
values[i].left.version = obj.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
setResponse({
|
setResponse({
|
||||||
|
@ -3256,18 +3286,147 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
}
|
}
|
||||||
wizard.getButton('finish').click();
|
wizard.getButton('finish').click();
|
||||||
})
|
})
|
||||||
yield engine._downloadObjects('item', objects.map(o => o.key));
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
||||||
|
|
||||||
|
Zotero.debug('=-=-=-=');
|
||||||
|
Zotero.debug(objects[0].toJSON());
|
||||||
|
Zotero.debug(objects[1].toJSON());
|
||||||
|
Zotero.debug(objects[2].toJSON());
|
||||||
|
|
||||||
|
// First object should match remote
|
||||||
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
||||||
|
assert.equal(objects[0].version, values[0].right.version);
|
||||||
|
assert.isTrue(objects[0].synced);
|
||||||
|
|
||||||
|
// Remaining objects should be marked as unsynced, with remote versions but original values,
|
||||||
|
// as if they were saved and then modified
|
||||||
|
assert.isFalse(objects[1].synced);
|
||||||
|
assert.equal(objects[1].version, values[1].right.version);
|
||||||
|
assert.equal(objects[1].getField('title'), values[1].left.title);
|
||||||
|
assert.isFalse(objects[2].synced);
|
||||||
|
assert.equal(objects[2].getField('title'), values[2].left.title);
|
||||||
|
assert.equal(objects[2].version, values[2].right.version);
|
||||||
|
|
||||||
|
// All cache versions should match remote
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
||||||
|
'item', libraryID, objects[i].key, values[i].right.version
|
||||||
|
);
|
||||||
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
||||||
|
assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
||||||
|
assert.lengthOf(keys, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("should resolve all remaining conflicts with remote version", async function () {
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
({ engine, client, caller } = await setup());
|
||||||
|
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] = await createDataObject(
|
||||||
|
'item',
|
||||||
|
{
|
||||||
|
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
|
||||||
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
||||||
|
|
||||||
|
// Create remote version
|
||||||
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
||||||
|
values[i].right.version = json.version = jsonData.version = 15;
|
||||||
|
responseJSON.push(json);
|
||||||
|
|
||||||
|
// Modify object locally
|
||||||
|
await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
||||||
|
values[i].left.title = obj.getField('title');
|
||||||
|
values[i].left.version = obj.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 click Resolve All checkbox
|
||||||
|
assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true');
|
||||||
|
assert.equal(
|
||||||
|
resolveAll.label,
|
||||||
|
Zotero.getString('sync.conflict.resolveAllRemoteFields')
|
||||||
|
);
|
||||||
|
resolveAll.click();
|
||||||
|
|
||||||
|
if (Zotero.isMac) {
|
||||||
|
assert.isTrue(wizard.getButton('next').hidden);
|
||||||
|
assert.isFalse(wizard.getButton('finish').hidden);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
wizard.getButton('finish').click();
|
||||||
|
})
|
||||||
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
||||||
|
|
||||||
assert.equal(objects[0].getField('title'), values[0].right.title);
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
||||||
assert.equal(objects[0].getField('version'), values[0].right.version);
|
assert.equal(objects[0].version, values[0].right.version);
|
||||||
assert.equal(objects[1].getField('title'), values[1].left.title);
|
assert.isTrue(objects[0].synced);
|
||||||
assert.equal(objects[1].getField('version'), values[1].left.version);
|
assert.equal(objects[1].getField('title'), values[1].right.title);
|
||||||
assert.equal(objects[2].getField('title'), values[2].left.title);
|
assert.equal(objects[1].version, values[1].right.version);
|
||||||
assert.equal(objects[2].getField('version'), values[2].left.version);
|
assert.isTrue(objects[1].synced);
|
||||||
|
assert.equal(objects[2].getField('title'), values[2].right.title);
|
||||||
|
assert.equal(objects[2].version, values[2].right.version);
|
||||||
|
assert.isTrue(objects[2].synced);
|
||||||
|
|
||||||
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
||||||
assert.lengthOf(keys, 0);
|
assert.lengthOf(keys, 0);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
// Note: Conflicts with remote deletions are handled in _startDownload()
|
// Note: Conflicts with remote deletions are handled in _startDownload()
|
||||||
it("should handle local item deletion, keeping deletion", function* () {
|
it("should handle local item deletion, keeping deletion", function* () {
|
||||||
|
@ -3527,67 +3686,6 @@ describe("Zotero.Sync.Data.Engine", function () {
|
||||||
// Deletion shouldn't be in sync delete log
|
// Deletion shouldn't be in sync delete log
|
||||||
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, obj.key));
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, obj.key));
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -440,15 +440,13 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
var type = 'item';
|
var type = 'item';
|
||||||
let obj = yield createDataObject(type, { version: 5 });
|
let obj = yield createDataObject(type, { version: 5 });
|
||||||
let data = obj.toJSON();
|
let data = obj.toJSON();
|
||||||
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [data]);
|
||||||
type, libraryID, [data]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Change local title
|
// Change local title
|
||||||
yield modifyDataObject(obj)
|
yield modifyDataObject(obj)
|
||||||
var changedTitle = obj.getField('title');
|
var changedTitle = obj.getField('title');
|
||||||
|
|
||||||
// Save remote version to cache without title but with changed place
|
// Create remote version without title but with changed place
|
||||||
data.key = obj.key;
|
data.key = obj.key;
|
||||||
data.version = 10;
|
data.version = 10;
|
||||||
var changedPlace = data.place = 'New York';
|
var changedPlace = data.place = 'New York';
|
||||||
|
@ -465,14 +463,14 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
assert.equal(obj.getField('place'), changedPlace);
|
assert.equal(obj.getField('place'), changedPlace);
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should save item with overriding local conflict as unsynced", function* () {
|
it("should save item with overriding local conflict as unsynced", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
||||||
var isbn = '978-0-335-22006-9';
|
var isbn = '978-0-335-22006-9';
|
||||||
var type = 'item';
|
var type = 'item';
|
||||||
let obj = createUnsavedDataObject(type, { version: 5 });
|
let obj = createUnsavedDataObject(type, { version: 5 });
|
||||||
obj.setField('ISBN', isbn);
|
obj.setField('ISBN', isbn);
|
||||||
yield obj.saveTx();
|
await obj.saveTx();
|
||||||
let data = obj.toJSON();
|
let data = obj.toJSON();
|
||||||
|
|
||||||
data.key = obj.key;
|
data.key = obj.key;
|
||||||
|
@ -483,7 +481,7 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
version: 10,
|
version: 10,
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
var results = yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
var results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
||||||
type, libraryID, [json], { stopOnError: true }
|
type, libraryID, [json], { stopOnError: true }
|
||||||
);
|
);
|
||||||
assert.isTrue(results[0].processed);
|
assert.isTrue(results[0].processed);
|
||||||
|
@ -492,6 +490,9 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
assert.equal(obj.version, 10);
|
assert.equal(obj.version, 10);
|
||||||
assert.equal(obj.getField('ISBN'), isbn);
|
assert.equal(obj.getField('ISBN'), isbn);
|
||||||
assert.isFalse(obj.synced);
|
assert.isFalse(obj.synced);
|
||||||
|
// Sync cache should match remote
|
||||||
|
var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, data.key, data.version);
|
||||||
|
assert.propertyVal(cacheJSON.data, "ISBN", data.ISBN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore locally deleted collections and searches that changed remotely", async function () {
|
it("should restore locally deleted collections and searches that changed remotely", async function () {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user