mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Update the global schema to 45, resolve a field's date type through its base-field mapping in ItemFields.isDate() (to cover priorityDate), and convert stored values of date-type fields to multipart dates on schema upgrade.
603 lines
20 KiB
JavaScript
603 lines
20 KiB
JavaScript
describe("Zotero.Schema", function () {
|
|
describe("#initializeSchema()", function () {
|
|
it("should set last client version", async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
|
|
var sql = "SELECT value FROM settings WHERE setting='client' AND key='lastVersion'";
|
|
var lastVersion = await Zotero.DB.valueQueryAsync(sql);
|
|
assert.equal(await Zotero.DB.valueQueryAsync(sql), Zotero.version);
|
|
});
|
|
});
|
|
|
|
describe("#updateSchema()", function () {
|
|
it("should set last client version", async function () {
|
|
var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)";
|
|
await Zotero.DB.queryAsync(sql, "5.0old");
|
|
|
|
await Zotero.Schema.updateSchema();
|
|
|
|
var sql = "SELECT value FROM settings WHERE setting='client' AND key='lastVersion'";
|
|
var lastVersion = await Zotero.DB.valueQueryAsync(sql);
|
|
assert.equal(await Zotero.DB.valueQueryAsync(sql), Zotero.version);
|
|
});
|
|
|
|
it("shouldn't repeat forced backup when retrying a failed update", async function () {
|
|
// Backups are disabled in the test profile
|
|
Zotero.Prefs.set('backup.numBackups', 2);
|
|
await Zotero.DB.queryAsync(
|
|
"DELETE FROM settings WHERE setting='backup' AND key='lastSchemaUpdateState'"
|
|
);
|
|
|
|
await Zotero.Schema.setIntegrityCheckRequired(true);
|
|
|
|
var backupSpy = sinon.spy(Zotero.DB, 'backUpDatabase');
|
|
var integrityCheckStub = sinon.stub(Zotero.Schema, 'integrityCheck')
|
|
.rejects(new Error("Simulated failure"));
|
|
try {
|
|
// First attempt makes a forced backup and fails
|
|
var e = await getPromiseError(Zotero.Schema.updateSchema());
|
|
assert.equal(e.message, "Simulated failure");
|
|
assert.equal(backupSpy.callCount, 1);
|
|
|
|
// Retrying with the same pending state doesn't make another backup
|
|
e = await getPromiseError(Zotero.Schema.updateSchema());
|
|
assert.equal(e.message, "Simulated failure");
|
|
assert.equal(backupSpy.callCount, 1);
|
|
|
|
// If the backup no longer exists, retrying makes a new one
|
|
await IOUtils.remove(Zotero.DB.path + '.bak');
|
|
e = await getPromiseError(Zotero.Schema.updateSchema());
|
|
assert.equal(e.message, "Simulated failure");
|
|
assert.equal(backupSpy.callCount, 2);
|
|
|
|
// A successful update clears the stored state
|
|
integrityCheckStub.restore();
|
|
await Zotero.Schema.updateSchema();
|
|
assert.isFalse(await Zotero.DB.valueQueryAsync(
|
|
"SELECT value FROM settings WHERE setting='backup' AND key='lastSchemaUpdateState'"
|
|
));
|
|
|
|
// A new pending state makes a fresh backup
|
|
await Zotero.Schema.setIntegrityCheckRequired(true);
|
|
await Zotero.Schema.updateSchema();
|
|
assert.equal(backupSpy.callCount, 3);
|
|
}
|
|
finally {
|
|
integrityCheckStub.restore();
|
|
backupSpy.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Global Schema", function () {
|
|
var schemaJSON, schema;
|
|
|
|
before(async function () {
|
|
schemaJSON = await Zotero.File.getResourceAsync('resource://zotero/schema/global/schema.json');
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
schema = JSON.parse(schemaJSON);
|
|
});
|
|
|
|
after(async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
});
|
|
|
|
describe("date-type field conversion", function () {
|
|
it("should convert stored text values of date-type fields to multipart dates", async function () {
|
|
var item = await createDataObject('item', { itemType: 'patent' });
|
|
// A value stored while the field was a text field
|
|
var fieldID = Zotero.ItemFields.getID('priorityDate');
|
|
var valueID = Zotero.ID.get('itemDataValues');
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO itemDataValues (valueID, value, valueNormalized) VALUES (?, ?, ?)",
|
|
[valueID, '1969-04-20', null]
|
|
);
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO itemData VALUES (?, ?, ?)",
|
|
[item.id, fieldID, valueID]
|
|
);
|
|
|
|
schema.version++;
|
|
await Zotero.Schema._updateGlobalSchemaForTest(schema);
|
|
await item.reload(['itemData'], true);
|
|
|
|
var stored = await Zotero.DB.valueQueryAsync(
|
|
"SELECT value FROM itemData JOIN itemDataValues USING (valueID) "
|
|
+ "WHERE itemID=? AND fieldID=?",
|
|
[item.id, fieldID]
|
|
);
|
|
assert.isTrue(Zotero.Date.isMultipart(stored));
|
|
assert.equal(item.getField('priorityDate'), '1969-04-20');
|
|
|
|
// The converted value matches a date search on the base field
|
|
var s = new Zotero.Search();
|
|
s.libraryID = item.libraryID;
|
|
s.addCondition('originalDate', 'isBefore', '1970');
|
|
var matches = await s.search();
|
|
assert.include(matches, item.id);
|
|
});
|
|
});
|
|
|
|
describe("#migrateExtraFields()", function () {
|
|
async function migrate(options) {
|
|
schema.version++;
|
|
schema.itemTypes.find(x => x.itemType == 'book').fields.splice(0, 1, { field: 'fooBar' })
|
|
var newLocales = {};
|
|
Object.keys(schema.locales).forEach((locale) => {
|
|
var o = schema.locales[locale];
|
|
o.fields.fooBar = 'Foo Bar';
|
|
newLocales[locale] = o;
|
|
});
|
|
await Zotero.Schema._updateGlobalSchemaForTest(schema);
|
|
await Zotero.Schema.migrateExtraFields(options);
|
|
}
|
|
|
|
it("should add a new field and migrate values from Extra", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setField('numPages', "10");
|
|
item.setField('extra', 'Foo Bar: This is a value.\nnumber-of-pages: 11\nThis is another line.');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.isNumber(Zotero.ItemFields.getID('fooBar'));
|
|
assert.equal(Zotero.ItemFields.getLocalizedString('fooBar'), 'Foo Bar');
|
|
assert.equal(item.getField('fooBar'), 'This is a value.');
|
|
// Existing fields shouldn't be overwritten and should be left in Extra
|
|
assert.equal(item.getField('numPages'), '10');
|
|
assert.equal(item.getField('extra'), 'number-of-pages: 11\nThis is another line.');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("should migrate valid creator", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setCreators([
|
|
{
|
|
firstName: 'Abc',
|
|
lastName: 'Def',
|
|
creatorType: 'author',
|
|
fieldMode: 0
|
|
}
|
|
]);
|
|
item.setField('extra', 'editor: Last || First\nFoo: Bar');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
var creators = item.getCreators();
|
|
assert.lengthOf(creators, 2);
|
|
assert.propertyVal(creators[0], 'firstName', 'Abc');
|
|
assert.propertyVal(creators[0], 'lastName', 'Def');
|
|
assert.propertyVal(creators[0], 'creatorTypeID', Zotero.CreatorTypes.getID('author'));
|
|
assert.propertyVal(creators[1], 'firstName', 'First');
|
|
assert.propertyVal(creators[1], 'lastName', 'Last');
|
|
assert.propertyVal(creators[1], 'creatorTypeID', Zotero.CreatorTypes.getID('editor'));
|
|
assert.equal(item.getField('extra'), 'Foo: Bar');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("shouldn't migrate creator not valid for item type", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setCreators([
|
|
{
|
|
firstName: 'Abc',
|
|
lastName: 'Def',
|
|
creatorType: 'author',
|
|
fieldMode: 0
|
|
}
|
|
]);
|
|
item.setField('extra', 'container-author: Last || First\nFoo: Bar');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
var creators = item.getCreators();
|
|
assert.lengthOf(creators, 1);
|
|
assert.propertyVal(creators[0], 'firstName', 'Abc');
|
|
assert.propertyVal(creators[0], 'lastName', 'Def');
|
|
assert.propertyVal(creators[0], 'creatorTypeID', Zotero.CreatorTypes.getID('author'));
|
|
assert.equal(item.getField('extra'), 'container-author: Last || First\nFoo: Bar');
|
|
assert.isTrue(item.synced);
|
|
});
|
|
|
|
it("shouldn't migrate fields in read-only library", async function () {
|
|
var library = await createGroup({ editable: false, filesEditable: false });
|
|
var item = createUnsavedDataObject('item', { libraryID: library.libraryID, itemType: 'book' });
|
|
item.setField('extra', 'Foo Bar: This is a value.');
|
|
item.synced = true;
|
|
await item.saveTx({
|
|
skipEditCheck: true
|
|
});
|
|
|
|
await migrate();
|
|
|
|
assert.isNumber(Zotero.ItemFields.getID('fooBar'));
|
|
assert.equal(item.getField('fooBar'), '');
|
|
assert.equal(item.getField('extra'), 'Foo Bar: This is a value.');
|
|
assert.isTrue(item.synced);
|
|
});
|
|
|
|
it("shouldn't report progress if Extra fields can't be migrated", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setField('numPages', "10");
|
|
item.setField('extra', 'number-of-pages: 11');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
var onProgress = sinon.spy();
|
|
await migrate({ onProgress });
|
|
|
|
assert.isFalse(onProgress.called);
|
|
assert.equal(item.getField('extra'), 'number-of-pages: 11');
|
|
assert.isTrue(item.synced);
|
|
});
|
|
|
|
it("should change item type if 'type:' is defined", async function () {
|
|
var item = await createDataObject('item', { itemType: 'document' });
|
|
item.setField('extra', 'type: personal_communication');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('letter'));
|
|
assert.equal(item.getField('extra'), '');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("should remove 'type:' line for CSL type if item is the first mapped Zotero type", async function () {
|
|
var item = await createDataObject('item', { itemType: 'letter' });
|
|
item.setField('extra', 'type: personal_communication');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('letter'));
|
|
assert.equal(item.getField('extra'), '');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("should remove 'type:' line for CSL type if item is a non-primary mapped Zotero type", async function () {
|
|
var item = await createDataObject('item', { itemType: 'instantMessage' });
|
|
item.setField('extra', 'type: personal_communication');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('instantMessage'));
|
|
assert.equal(item.getField('extra'), '');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("should move existing fields that would be invalid in the new 'type:' type to Extra", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setField('numPages', '123');
|
|
item.setField('extra', 'type: article-journal\nJournal Abbreviation: abc.\nnumPages: 234');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('journalArticle'));
|
|
assert.equal(item.getField('journalAbbreviation'), 'abc.');
|
|
// Migrated real field should be placed at beginning, followed by unused line from Extra
|
|
assert.equal(item.getField('extra'), 'Num Pages: 123\nnumPages: 234');
|
|
assert.isFalse(item.synced);
|
|
});
|
|
|
|
it("shouldn't migrate invalid item type", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
item.setField('numPages', 30);
|
|
item.setCreators(
|
|
[
|
|
{
|
|
firstName: 'Abc',
|
|
lastName: 'Def',
|
|
creatorType: 'author',
|
|
fieldMode: 0
|
|
},
|
|
{
|
|
firstName: 'Ghi',
|
|
lastName: 'Jkl',
|
|
creatorType: 'author',
|
|
fieldMode: 0
|
|
}
|
|
]
|
|
);
|
|
item.setField('extra', 'type: invalid');
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.getField('numPages'), 30);
|
|
var creators = item.getCreators();
|
|
assert.lengthOf(creators, 2);
|
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('book'));
|
|
assert.equal(item.getField('extra'), 'type: invalid');
|
|
assert.isTrue(item.synced);
|
|
});
|
|
|
|
it("shouldn't migrate certain fields temporarily", async function () {
|
|
var item = await createDataObject('item', { itemType: 'book' });
|
|
var extra = 'event-place: Event Place\npublisher-place: Publisher Place\nIssued: 2020/2023';
|
|
item.setField('extra', extra);
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
await migrate();
|
|
|
|
assert.equal(item.getField('place'), '');
|
|
assert.equal(item.getField('date'), '');
|
|
assert.equal(item.getField('extra'), extra);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe("Repository Check", function () {
|
|
describe("Notices", function () {
|
|
var win;
|
|
var server;
|
|
|
|
before(async function () {
|
|
// We need bundled files
|
|
await resetDB({
|
|
thisArg: this
|
|
});
|
|
|
|
win = await loadZoteroPane();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
server = sinon.fakeServer.create();
|
|
server.autoRespond = true;
|
|
});
|
|
|
|
afterEach(function () {
|
|
Zotero.Prefs.clear('hiddenNotices');
|
|
});
|
|
|
|
after(function () {
|
|
win.close();
|
|
Zotero.HTTP.mock = null;
|
|
});
|
|
|
|
function createResponseWithMessage(message) {
|
|
server.respond(function (req) {
|
|
if (req.method != "POST" || !req.url.includes('/repo/updated')) {
|
|
return;
|
|
}
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/xml"
|
|
},
|
|
'<xml>'
|
|
+ '<currentTime>1630219842</currentTime>'
|
|
+ message
|
|
+ '</xml>'
|
|
);
|
|
});
|
|
}
|
|
|
|
it("should show dialog if repo returns a message", async function () {
|
|
createResponseWithMessage(
|
|
`<message infoURL="https://example.com">This is a warning</message>`
|
|
);
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
var html = dialog.document.documentElement.outerHTML;
|
|
assert.include(html, "This is a warning");
|
|
});
|
|
await Zotero.Schema.updateFromRepository(3);
|
|
await promise;
|
|
|
|
// Don't show id-less message again for a day
|
|
var spy = sinon.spy(Zotero, 'debug');
|
|
await Zotero.Schema.updateFromRepository(3);
|
|
assert.notEqual(spy.args.findIndex(x => {
|
|
return typeof x[0] == 'string' && x[0].startsWith("Not showing hidden");
|
|
}), -1);
|
|
spy.restore();
|
|
});
|
|
|
|
it("shouldn't show message with id again for 1 day even if not hidden", async function () {
|
|
var id = Zotero.Utilities.randomString();
|
|
createResponseWithMessage(
|
|
`<message id="${id}" infoURL="https://example.com">This is a warning</message>`
|
|
);
|
|
|
|
var promise = waitForDialog();
|
|
await Zotero.Schema.updateFromRepository(3);
|
|
await promise;
|
|
|
|
// Make sure notice is hidden for 1 day
|
|
var hiddenNotices;
|
|
var tries = 0;
|
|
var ttl = 86400;
|
|
while (tries < 100) {
|
|
tries++;
|
|
hiddenNotices = Zotero.Prefs.get('hiddenNotices');
|
|
if (!hiddenNotices) {
|
|
await Zotero.Promise.delay(10);
|
|
continue;
|
|
}
|
|
hiddenNotices = JSON.parse(hiddenNotices);
|
|
assert.property(hiddenNotices, id);
|
|
assert.approximately(hiddenNotices[id], Math.round(Date.now() / 1000) + ttl, 10);
|
|
break;
|
|
}
|
|
});
|
|
|
|
it("shouldn't show message with id again for 30 days", async function () {
|
|
var id = Zotero.Utilities.randomString();
|
|
createResponseWithMessage(
|
|
`<message id="${id}" infoURL="https://example.com">This is a warning</message>`
|
|
);
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
var doc = dialog.document;
|
|
var innerHTML = doc.documentElement.innerHTML;
|
|
assert.include(innerHTML, "This is a warning");
|
|
assert.include(innerHTML, Zotero.getString('general.dontShowAgainFor', 30, 30));
|
|
// Check "Don't show again"
|
|
doc.getElementById('checkbox').click();
|
|
});
|
|
await Zotero.Schema.updateFromRepository(3);
|
|
await promise;
|
|
|
|
// Make sure notice is hidden for 30 days
|
|
var hiddenNotices;
|
|
var tries = 0;
|
|
var ttl = 30 * 86400;
|
|
while (tries < 100) {
|
|
tries++;
|
|
hiddenNotices = Zotero.Prefs.get('hiddenNotices');
|
|
if (!hiddenNotices) {
|
|
await Zotero.Promise.delay(10);
|
|
continue;
|
|
}
|
|
hiddenNotices = JSON.parse(hiddenNotices);
|
|
assert.property(hiddenNotices, id);
|
|
assert.approximately(hiddenNotices[id], Math.round(Date.now() / 1000) + ttl, 10);
|
|
break;
|
|
}
|
|
});
|
|
|
|
it("shouldn't show message with id if before expiration", async function () {
|
|
var id = Zotero.Utilities.randomString();
|
|
createResponseWithMessage(
|
|
`<message id="${id}" infoURL="https://example.com">This is a warning</message>`
|
|
);
|
|
|
|
// Set expiration for 30 days from now
|
|
var ttl = 30 * 86400;
|
|
Zotero.Prefs.set(
|
|
'hiddenNotices',
|
|
JSON.stringify({
|
|
[id]: Math.round(Date.now() / 1000) + ttl
|
|
})
|
|
);
|
|
|
|
// Message should be hidden
|
|
var spy = sinon.spy(Zotero, 'debug');
|
|
await Zotero.Schema.updateFromRepository(3);
|
|
assert.notEqual(spy.args.findIndex(x => {
|
|
return typeof x[0] == 'string' && x[0].startsWith("Not showing hidden");
|
|
}), -1);
|
|
spy.restore();
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe("#integrityCheck()", function () {
|
|
before(function* () {
|
|
yield resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
})
|
|
|
|
it("should create missing tables unless 'skipReconcile' is true", async function () {
|
|
await Zotero.DB.queryAsync("DROP TABLE retractedItems");
|
|
assert.isFalse(await Zotero.DB.tableExists('retractedItems'));
|
|
assert.isTrue(await Zotero.Schema.integrityCheck(false, { skipReconcile: true }));
|
|
|
|
assert.isFalse(await Zotero.Schema.integrityCheck());
|
|
assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
|
assert.isTrue(await Zotero.DB.tableExists('retractedItems'));
|
|
});
|
|
|
|
it("should repair a foreign key violation", async function () {
|
|
assert.isTrue(await Zotero.Schema.integrityCheck());
|
|
|
|
await Zotero.DB.queryAsync("PRAGMA foreign_keys = OFF");
|
|
await Zotero.DB.queryAsync("INSERT INTO itemTags VALUES (1234,1234,0)");
|
|
await Zotero.DB.queryAsync("PRAGMA foreign_keys = ON");
|
|
|
|
assert.isFalse(await Zotero.Schema.integrityCheck());
|
|
assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
|
assert.isTrue(await Zotero.Schema.integrityCheck());
|
|
})
|
|
|
|
it("should repair invalid nesting between two collections", async function () {
|
|
var c1 = await createDataObject('collection');
|
|
var c2 = await createDataObject('collection', { parentID: c1.id });
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE collections SET parentCollectionID=? WHERE collectionID=?",
|
|
[c2.id, c1.id]
|
|
);
|
|
|
|
await assert.isFalse(await Zotero.Schema.integrityCheck());
|
|
await assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
|
await assert.isTrue(await Zotero.Schema.integrityCheck());
|
|
});
|
|
|
|
it("should repair invalid nesting between three collections", async function () {
|
|
var c1 = await createDataObject('collection');
|
|
var c2 = await createDataObject('collection', { parentID: c1.id });
|
|
var c3 = await createDataObject('collection', { parentID: c2.id });
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE collections SET parentCollectionID=? WHERE collectionID=?",
|
|
[c3.id, c2.id]
|
|
);
|
|
|
|
await assert.isFalse(await Zotero.Schema.integrityCheck());
|
|
await assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
|
await assert.isTrue(await Zotero.Schema.integrityCheck());
|
|
});
|
|
|
|
it("should allow embedded-image attachments under notes", async function () {
|
|
var item = await createDataObject('item', { itemType: 'note' });
|
|
await createEmbeddedImage(item);
|
|
await assert.isTrue(await Zotero.Schema.integrityCheck());
|
|
});
|
|
})
|
|
|
|
describe("Database Upgrades", function () {
|
|
after(async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true,
|
|
});
|
|
});
|
|
|
|
it("should upgrade 4.0 database", async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true,
|
|
dbFile: OS.Path.join(getTestDataDirectory().path, 'zotero-4.0.sqlite.zip'),
|
|
// Backups are disabled in the test profile
|
|
prefs: { 'backup.numBackups': 2 }
|
|
});
|
|
// Make sure we can open the Zotero pane without errors
|
|
win = await loadZoteroPane();
|
|
win.close();
|
|
|
|
// A backup should have been made before the upgrade
|
|
assert.isTrue(
|
|
await IOUtils.exists(PathUtils.join(Zotero.DataDirectory.dir, 'zotero.sqlite.bak'))
|
|
);
|
|
});
|
|
});
|
|
})
|