1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-03-03 15:32:30 +02:00

Tools: Cleaned up tests and splitted sync tests into smaller parts

This commit is contained in:
Laurent Cozic 2020-12-01 18:05:24 +00:00
parent fa50a8f5da
commit 717b8da1f8
68 changed files with 2322 additions and 2278 deletions

View File

@ -74,6 +74,24 @@ packages/app-cli/tests/InMemoryCache.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/Synchronizer.basics.d.ts
packages/app-cli/tests/Synchronizer.basics.js
packages/app-cli/tests/Synchronizer.basics.js.map
packages/app-cli/tests/Synchronizer.conflicts.d.ts
packages/app-cli/tests/Synchronizer.conflicts.js
packages/app-cli/tests/Synchronizer.conflicts.js.map
packages/app-cli/tests/Synchronizer.e2ee.d.ts
packages/app-cli/tests/Synchronizer.e2ee.js
packages/app-cli/tests/Synchronizer.e2ee.js.map
packages/app-cli/tests/Synchronizer.resources.d.ts
packages/app-cli/tests/Synchronizer.resources.js
packages/app-cli/tests/Synchronizer.resources.js.map
packages/app-cli/tests/Synchronizer.revisions.d.ts
packages/app-cli/tests/Synchronizer.revisions.js
packages/app-cli/tests/Synchronizer.revisions.js.map
packages/app-cli/tests/Synchronizer.tags.d.ts
packages/app-cli/tests/Synchronizer.tags.js
packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
@ -242,6 +260,9 @@ packages/app-cli/tests/synchronizer_LockHandler.js.map
packages/app-cli/tests/synchronizer_MigrationHandler.d.ts
packages/app-cli/tests/synchronizer_MigrationHandler.js
packages/app-cli/tests/synchronizer_MigrationHandler.js.map
packages/app-cli/tests/test-utils-synchronizer.d.ts
packages/app-cli/tests/test-utils-synchronizer.js
packages/app-cli/tests/test-utils-synchronizer.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@ -872,6 +893,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
packages/lib/errorUtils.d.ts
packages/lib/errorUtils.js
packages/lib/errorUtils.js.map

View File

@ -24,6 +24,7 @@ module.exports = {
'afterAll': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'jest': 'readonly',
// React Native variables
'__DEV__': 'readonly',

24
.gitignore vendored
View File

@ -65,6 +65,24 @@ packages/app-cli/tests/InMemoryCache.js.map
packages/app-cli/tests/MdToHtml.d.ts
packages/app-cli/tests/MdToHtml.js
packages/app-cli/tests/MdToHtml.js.map
packages/app-cli/tests/Synchronizer.basics.d.ts
packages/app-cli/tests/Synchronizer.basics.js
packages/app-cli/tests/Synchronizer.basics.js.map
packages/app-cli/tests/Synchronizer.conflicts.d.ts
packages/app-cli/tests/Synchronizer.conflicts.js
packages/app-cli/tests/Synchronizer.conflicts.js.map
packages/app-cli/tests/Synchronizer.e2ee.d.ts
packages/app-cli/tests/Synchronizer.e2ee.js
packages/app-cli/tests/Synchronizer.e2ee.js.map
packages/app-cli/tests/Synchronizer.resources.d.ts
packages/app-cli/tests/Synchronizer.resources.js
packages/app-cli/tests/Synchronizer.resources.js.map
packages/app-cli/tests/Synchronizer.revisions.d.ts
packages/app-cli/tests/Synchronizer.revisions.js
packages/app-cli/tests/Synchronizer.revisions.js.map
packages/app-cli/tests/Synchronizer.tags.d.ts
packages/app-cli/tests/Synchronizer.tags.js
packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
@ -233,6 +251,9 @@ packages/app-cli/tests/synchronizer_LockHandler.js.map
packages/app-cli/tests/synchronizer_MigrationHandler.d.ts
packages/app-cli/tests/synchronizer_MigrationHandler.js
packages/app-cli/tests/synchronizer_MigrationHandler.js.map
packages/app-cli/tests/test-utils-synchronizer.d.ts
packages/app-cli/tests/test-utils-synchronizer.js
packages/app-cli/tests/test-utils-synchronizer.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@ -863,6 +884,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
packages/lib/errorUtils.d.ts
packages/lib/errorUtils.js
packages/lib/errorUtils.js.map

View File

@ -12,10 +12,6 @@ const Setting = require('@joplin/lib/models/Setting').default;
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec;
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection', p, 'reason:', reason);
});
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
const joplinAppPath = `${__dirname}/main.js`;

View File

@ -22,10 +22,6 @@ const logger = new Logger();
logger.addTarget('console');
logger.setLevel(Logger.LEVEL_DEBUG);
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled promise rejection', p, 'reason:', reason);
});
function createClient(id) {
return {
id: id,

View File

@ -34,6 +34,7 @@ module.exports = {
'<rootDir>/tests/support/',
'<rootDir>/build/',
'<rootDir>/tests/test-utils.js',
'<rootDir>/tests/test-utils-synchronizer.js',
'<rootDir>/tests/file_api_driver.js',
'<rootDir>/tests/tmp/',
],

View File

@ -2,20 +2,16 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const ArrayUtils = require('@joplin/lib/ArrayUtils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
});
it('should remove array elements', asyncTest(async () => {
it('should remove array elements', (async () => {
let a = ['un', 'deux', 'trois'];
a = ArrayUtils.removeElement(a, 'deux');
@ -28,7 +24,7 @@ describe('ArrayUtils', function() {
expect(a.length).toBe(3);
}));
it('should find items using binary search', asyncTest(async () => {
it('should find items using binary search', (async () => {
let items = ['aaa', 'ccc', 'bbb'];
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
items.sort();
@ -41,14 +37,14 @@ describe('ArrayUtils', function() {
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
}));
it('should compare arrays', asyncTest(async () => {
it('should compare arrays', (async () => {
expect(ArrayUtils.contentEquals([], [])).toBe(true);
expect(ArrayUtils.contentEquals(['a'], ['a'])).toBe(true);
expect(ArrayUtils.contentEquals(['b', 'a'], ['a', 'b'])).toBe(true);
expect(ArrayUtils.contentEquals(['b'], ['a', 'b'])).toBe(false);
}));
it('should merge overlapping intervals', asyncTest(async () => {
it('should merge overlapping intervals', (async () => {
const testCases = [
[
[],

View File

@ -1,13 +1,9 @@
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const shim = require('@joplin/lib/shim').default;
const { enexXmlToHtml } = require('@joplin/lib/import-enex-html-gen.js');
const cleanHtml = require('clean-html');
process.on('unhandledRejection', (reason, p) => {
console.warn('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const fileWithPath = (filename) =>
`${__dirname}/enex_to_html/${filename}`;
@ -49,7 +45,7 @@ const compareOutputToExpected = (options) => {
const outputFile = fileWithPath(`${options.testName}.html`);
const testTitle = `should convert from Enex to Html: ${options.testName}`;
it(testTitle, asyncTest(async () => {
it(testTitle, (async () => {
const enexInput = await shim.fsDriver().readFile(inputFile);
const expectedOutput = await shim.fsDriver().readFile(outputFile);
const actualOutput = await beautifyHtml(await enexXmlToHtml(enexInput, options.resources));
@ -101,7 +97,7 @@ describe('EnexToHtml', function() {
}],
});
// it('fails when not given a matching resource', asyncTest(async () => {
// it('fails when not given a matching resource', (async () => {
// // To test the promise-unexpectedly-resolved case, add `audioResource` to the array.
// const resources = [];
// const inputFile = fileWithPath('en-media--image.enex');

View File

@ -4,7 +4,7 @@
const os = require('os');
const time = require('@joplin/lib/time').default;
const { filename } = require('@joplin/lib/path-utils');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
@ -12,10 +12,6 @@ const shim = require('@joplin/lib/shim').default;
const HtmlToHtml = require('@joplin/renderer/HtmlToHtml').default;
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('HtmlToHtml', function() {
beforeEach(async (done) => {
@ -24,7 +20,7 @@ describe('HtmlToHtml', function() {
done();
});
it('should convert from Html to Html', asyncTest(async () => {
it('should convert from Html to Html', (async () => {
const basePath = `${__dirname}/html_to_html`;
const files = await shim.fsDriver().readDirStats(basePath);
const htmlToHtml = new HtmlToHtml();

View File

@ -4,7 +4,7 @@
const os = require('os');
const time = require('@joplin/lib/time').default;
const { filename } = require('@joplin/lib/path-utils');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
@ -12,10 +12,6 @@ const shim = require('@joplin/lib/shim').default;
const HtmlToMd = require('@joplin/lib/HtmlToMd');
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('HtmlToMd', function() {
beforeEach(async (done) => {
@ -24,7 +20,7 @@ describe('HtmlToMd', function() {
done();
});
it('should convert from Html to Markdown', asyncTest(async () => {
it('should convert from Html to Markdown', (async () => {
const basePath = `${__dirname}/html_to_md`;
const files = await shim.fsDriver().readDirStats(basePath);
const htmlToMd = new HtmlToMd();

View File

@ -1,10 +1,9 @@
const { asyncTest } = require('./test-utils.js');
const MarkupToHtml = require('@joplin/renderer/MarkupToHtml').default;
describe('MarkupToHtml', function() {
it('should strip markup', asyncTest(async () => {
it('should strip markup', (async () => {
const service = new MarkupToHtml();
const testCases = {

View File

@ -1,7 +1,7 @@
import MdToHtml from '@joplin/renderer/MdToHtml';
const os = require('os');
const { filename } = require('@joplin/lib/path-utils');
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const shim = require('@joplin/lib/shim').default;
const { themeStyle } = require('@joplin/lib/theme');
@ -25,7 +25,7 @@ describe('MdToHtml', function() {
done();
});
it('should convert from Markdown to Html', asyncTest(async () => {
it('should convert from Markdown to Html', (async () => {
const basePath = `${__dirname}/md_to_html`;
const files = await shim.fsDriver().readDirStats(basePath);
const mdToHtml = newTestMdToHtml();
@ -82,7 +82,7 @@ describe('MdToHtml', function() {
}
}));
it('should return enabled plugin assets', asyncTest(async () => {
it('should return enabled plugin assets', (async () => {
const pluginOptions: any = {};
const pluginNames = MdToHtml.pluginNames();
@ -107,7 +107,7 @@ describe('MdToHtml', function() {
}
}));
it('should wrapped the rendered Markdown', asyncTest(async () => {
it('should wrapped the rendered Markdown', (async () => {
const mdToHtml = newTestMdToHtml();
// In this case, the HTML contains both the style and
@ -117,7 +117,7 @@ describe('MdToHtml', function() {
expect(result.html.indexOf('rendered-md') >= 0).toBe(true);
}));
it('should return the rendered body only', asyncTest(async () => {
it('should return the rendered body only', (async () => {
const mdToHtml = newTestMdToHtml();
// In this case, the HTML contains only the rendered markdown, with
@ -137,7 +137,7 @@ describe('MdToHtml', function() {
}
}));
it('should split HTML and CSS', asyncTest(async () => {
it('should split HTML and CSS', (async () => {
const mdToHtml = newTestMdToHtml();
// It is similar to the bodyOnly option, excepts that the rendered
@ -147,7 +147,7 @@ describe('MdToHtml', function() {
expect(result.html.trim()).toBe('<div id="rendered-md"><p>just <strong>testing</strong></p>\n</div>');
}));
// it('should render links correctly', asyncTest(async () => {
// it('should render links correctly', (async () => {
// const mdToHtml = newTestMdToHtml();
// const testCases = [

View File

@ -1,20 +1,14 @@
/* eslint-disable no-unused-vars */
const { asyncTest } = require('./test-utils.js');
const StringUtils = require('@joplin/lib/string-utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('StringUtils', function() {
beforeEach(async (done) => {
done();
});
it('should surround keywords with strings', asyncTest(async () => {
it('should surround keywords with strings', (async () => {
const testCases = [
[[], 'test', 'a', 'b', 'test'],
[['test'], 'test', 'a', 'b', 'atestb'],
@ -40,7 +34,7 @@ describe('StringUtils', function() {
}
}));
it('should find the next whitespace character', asyncTest(async () => {
it('should find the next whitespace character', (async () => {
const testCases = [
['', [[0, 0]]],
['Joplin', [[0, 6], [3, 6], [6, 6]]],

View File

@ -0,0 +1,398 @@
import Setting from '@joplin/lib/models/Setting';
import { allNotesFolders, remoteNotesAndFolders, localNotesFoldersSameAsRemote } from './test-utils-synchronizer';
const { syncTargetName, synchronizerStart, setupDatabaseAndSynchronizer, synchronizer, sleep, switchClient, syncTargetId, fileApi } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
describe('Synchronizer.basics', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
it('should create remote items', (async () => {
const folder = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', parent_id: folder.id });
const all = await allNotesFolders();
await synchronizerStart();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should update remote items', (async () => {
const folder = await Folder.save({ title: 'folder1' });
const note = await Note.save({ title: 'un', parent_id: folder.id });
await synchronizerStart();
await Note.save({ title: 'un UPDATE', id: note.id });
const all = await allNotesFolders();
await synchronizerStart();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should create local items', (async () => {
const folder = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', parent_id: folder.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
const all = await allNotesFolders();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should update local items', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
let note2 = await Note.load(note1.id);
note2.title = 'Updated on client 2';
await Note.save(note2);
note2 = await Note.load(note2.id);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const all = await allNotesFolders();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should delete remote notes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
await Note.delete(note1.id);
await synchronizerStart();
const remotes = await remoteNotesAndFolders();
expect(remotes.length).toBe(1);
expect(remotes[0].id).toBe(folder1.id);
const deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
it('should not created deleted_items entries for items deleted via sync', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Folder.delete(folder1.id);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
it('should delete local notes', (async () => {
// For these tests we pass the context around for each user. This is to make sure that the "deletedItemsProcessed"
// property of the basicDelta() function is cleared properly at the end of a sync operation. If it is not cleared
// it means items will no longer be deleted locally via sync.
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
const note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.delete(note1.id);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const items = await allNotesFolders();
expect(items.length).toBe(2);
const deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
await Note.delete(note2.id);
await synchronizerStart();
}));
it('should delete remote folder', (async () => {
await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
await Folder.delete(folder2.id);
await synchronizerStart();
const all = await allNotesFolders();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should delete local folder', (async () => {
await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Folder.delete(folder2.id);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const items = await allNotesFolders();
await localNotesFoldersSameAsRemote(items, expect);
}));
it('should cross delete all folders', (async () => {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
// 2 deletes item 2, they should both end up with no items after sync.
const folder1 = await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
await Folder.delete(folder1.id);
await switchClient(1);
await Folder.delete(folder2.id);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
const items2 = await allNotesFolders();
await switchClient(1);
await synchronizerStart();
const items1 = await allNotesFolders();
expect(items1.length).toBe(0);
expect(items1.length).toBe(items2.length);
}));
it('items should be downloaded again when user cancels in the middle of delta operation', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
synchronizer().testingHooks_ = ['cancelDeltaLoop2'];
await synchronizerStart();
let notes = await Note.all();
expect(notes.length).toBe(0);
synchronizer().testingHooks_ = [];
await synchronizerStart();
notes = await Note.all();
expect(notes.length).toBe(1);
}));
it('should skip items that cannot be synced', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
const noteId = note1.id;
await synchronizerStart();
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: 'un mod' });
synchronizer().testingHooks_ = ['notesRejectedByTarget'];
await synchronizerStart();
synchronizer().testingHooks_ = [];
await synchronizerStart(); // Another sync to check that this item is now excluded from sync
await switchClient(2);
await synchronizerStart();
const notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].title).toBe('un');
await switchClient(1);
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(1);
}));
it('should allow duplicate folder titles', (async () => {
await Folder.save({ title: 'folder' });
await switchClient(2);
let remoteF2 = await Folder.save({ title: 'folder' });
await synchronizerStart();
await switchClient(1);
await sleep(0.1);
await synchronizerStart();
const localF2 = await Folder.load(remoteF2.id);
expect(localF2.title == remoteF2.title).toBe(true);
// Then that folder that has been renamed locally should be set in such a way
// that synchronizing it applies the title change remotely, and that new title
// should be retrieved by client 2.
await synchronizerStart();
await switchClient(2);
await sleep(0.1);
await synchronizerStart();
remoteF2 = await Folder.load(remoteF2.id);
expect(remoteF2.title == localF2.title).toBe(true);
}));
it('should create remote items with UTF-8 content', (async () => {
const folder = await Folder.save({ title: 'Fahrräder' });
await Note.save({ title: 'Fahrräder', body: 'Fahrräder', parent_id: folder.id });
const all = await allNotesFolders();
await synchronizerStart();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should update remote items but not pull remote changes', (async () => {
const folder = await Folder.save({ title: 'folder1' });
const note = await Note.save({ title: 'un', parent_id: folder.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.save({ title: 'deux', parent_id: folder.id });
await synchronizerStart();
await switchClient(1);
await Note.save({ title: 'un UPDATE', id: note.id });
await synchronizerStart(null, { syncSteps: ['update_remote'] });
const all = await allNotesFolders();
expect(all.length).toBe(2);
await switchClient(2);
await synchronizerStart();
const note2 = await Note.load(note.id);
expect(note2.title).toBe('un UPDATE');
}));
it('should create a new Welcome notebook on each client', (async () => {
// Create the Welcome items on two separate clients
await WelcomeUtils.createWelcomeItems();
await synchronizerStart();
await switchClient(2);
await WelcomeUtils.createWelcomeItems();
const beforeFolderCount = (await Folder.all()).length;
const beforeNoteCount = (await Note.all()).length;
expect(beforeFolderCount === 1).toBe(true);
expect(beforeNoteCount > 1).toBe(true);
await synchronizerStart();
const afterFolderCount = (await Folder.all()).length;
const afterNoteCount = (await Note.all()).length;
expect(afterFolderCount).toBe(beforeFolderCount * 2);
expect(afterNoteCount).toBe(beforeNoteCount * 2);
// Changes to the Welcome items should be synced to all clients
const f1 = (await Folder.all())[0];
await Folder.save({ id: f1.id, title: 'Welcome MOD' });
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const f1_1 = await Folder.load(f1.id);
expect(f1_1.title).toBe('Welcome MOD');
}));
it('should not wipe out user data when syncing with an empty target', (async () => {
// Only these targets support the wipeOutFailSafe flag (in other words, the targets that use basicDelta)
if (!['nextcloud', 'memory', 'filesystem', 'amazon_s3'].includes(syncTargetName())) return;
for (let i = 0; i < 10; i++) await Note.save({ title: 'note' });
Setting.setValue('sync.wipeOutFailSafe', true);
await synchronizerStart();
await fileApi().clearRoot(); // oops
await synchronizerStart();
expect((await Note.all()).length).toBe(10); // but since the fail-safe if on, the notes have not been deleted
Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off
await synchronizerStart();
expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared
// Handle case where the sync target has been wiped out, then the user creates one note and sync.
for (let i = 0; i < 10; i++) await Note.save({ title: 'note' });
Setting.setValue('sync.wipeOutFailSafe', true);
await synchronizerStart();
await fileApi().clearRoot();
await Note.save({ title: 'ma note encore' });
await synchronizerStart();
expect((await Note.all()).length).toBe(11);
}));
});

View File

@ -0,0 +1,294 @@
import time from '@joplin/lib/time';
import Setting from '@joplin/lib/models/Setting';
import { allNotesFolders, localNotesFoldersSameAsRemote } from './test-utils-synchronizer';
const { synchronizerStart, setupDatabaseAndSynchronizer, sleep, switchClient, syncTargetId, loadEncryptionMasterKey, decryptionWorker } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
describe('Synchronizer.conflicts', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
it('should resolve note conflicts', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
let note2 = await Note.load(note1.id);
note2.title = 'Updated on client 2';
await Note.save(note2);
note2 = await Note.load(note2.id);
await synchronizerStart();
await switchClient(1);
let note2conf = await Note.load(note1.id);
note2conf.title = 'Updated on client 1';
await Note.save(note2conf);
note2conf = await Note.load(note1.id);
await synchronizerStart();
const conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(1);
// Other than the id (since the conflicted note is a duplicate), and the is_conflict property
// the conflicted and original note must be the same in every way, to make sure no data has been lost.
const conflictedNote = conflictedNotes[0];
expect(conflictedNote.id == note2conf.id).toBe(false);
for (const n in conflictedNote) {
if (!conflictedNote.hasOwnProperty(n)) continue;
if (n == 'id' || n == 'is_conflict') continue;
expect(conflictedNote[n]).toBe(note2conf[n]);
}
const noteUpdatedFromRemote = await Note.load(note1.id);
for (const n in noteUpdatedFromRemote) {
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
expect(noteUpdatedFromRemote[n]).toBe(note2[n]);
}
}));
it('should resolve folders conflicts', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2); // ----------------------------------
await synchronizerStart();
await sleep(0.1);
let folder1_modRemote = await Folder.load(folder1.id);
folder1_modRemote.title = 'folder1 UPDATE CLIENT 2';
await Folder.save(folder1_modRemote);
folder1_modRemote = await Folder.load(folder1_modRemote.id);
await synchronizerStart();
await switchClient(1); // ----------------------------------
await sleep(0.1);
let folder1_modLocal = await Folder.load(folder1.id);
folder1_modLocal.title = 'folder1 UPDATE CLIENT 1';
await Folder.save(folder1_modLocal);
folder1_modLocal = await Folder.load(folder1.id);
await synchronizerStart();
const folder1_final = await Folder.load(folder1.id);
expect(folder1_final.title).toBe(folder1_modRemote.title);
}));
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Folder.delete(folder1.id);
await synchronizerStart();
await switchClient(1);
await Note.save({ title: 'note1', parent_id: folder1.id });
await synchronizerStart();
const items = await allNotesFolders();
expect(items.length).toBe(1);
expect(items[0].title).toBe('note1');
expect(items[0].is_conflict).toBe(1);
}));
it('should resolve conflict if note has been deleted remotely and locally', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note = await Note.save({ title: 'note', parent_id: folder.title });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.delete(note.id);
await synchronizerStart();
await switchClient(1);
await Note.delete(note.id);
await synchronizerStart();
const items = await allNotesFolders();
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
await localNotesFoldersSameAsRemote(items, expect);
}));
it('should handle conflict when remote note is deleted then local note is modified', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
await Note.delete(note1.id);
await synchronizerStart();
await switchClient(1);
const newTitle = 'Modified after having been deleted';
await Note.save({ id: note1.id, title: newTitle });
await synchronizerStart();
const conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(1);
expect(conflictedNotes[0].title).toBe(newTitle);
const unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Folder.save({ title: 'folder2' });
await Note.save({ title: 'un', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await sleep(0.1);
await Folder.delete(folder1.id);
await synchronizerStart();
await switchClient(1);
await sleep(0.1);
const newTitle = 'Modified after having been deleted';
await Folder.save({ id: folder1.id, title: newTitle });
await synchronizerStart();
const items = await allNotesFolders();
expect(items.length).toBe(1);
}));
it('should not sync notes with conflicts', (async () => {
const f1 = await Folder.save({ title: 'folder' });
await Note.save({ title: 'mynote', parent_id: f1.id, is_conflict: 1 });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
const notes = await Note.all();
const folders = await Folder.all();
expect(notes.length).toBe(0);
expect(folders.length).toBe(1);
}));
it('should not try to delete on remote conflicted notes that have been deleted', (async () => {
const f1 = await Folder.save({ title: 'folder' });
const n1 = await Note.save({ title: 'mynote', parent_id: f1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.save({ id: n1.id, is_conflict: 1 });
await Note.delete(n1.id);
const deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
async function ignorableNoteConflictTest(withEncryption: boolean) {
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
}
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
if (withEncryption) {
await loadEncryptionMasterKey(null, true);
await decryptionWorker().start();
}
let note2 = await Note.load(note1.id);
note2.todo_completed = time.unixMs() - 1;
await Note.save(note2);
note2 = await Note.load(note2.id);
await synchronizerStart();
await switchClient(1);
let note2conf = await Note.load(note1.id);
note2conf.todo_completed = time.unixMs();
await Note.save(note2conf);
note2conf = await Note.load(note1.id);
await synchronizerStart();
if (!withEncryption) {
// That was previously a common conflict:
// - Client 1 mark todo as "done", and sync
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
// In theory it is a conflict because the todo_completed dates are different
// but in practice it doesn't matter, we can just take the date when the
// todo was marked as "done" the first time.
const conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(0);
const notes = await Note.all();
expect(notes.length).toBe(1);
expect(notes[0].id).toBe(note1.id);
expect(notes[0].todo_completed).toBe(note2.todo_completed);
} else {
// If the notes are encrypted however it's not possible to do this kind of
// smart conflict resolving since we don't know the content, so in that
// case it's handled as a regular conflict.
const conflictedNotes = await Note.conflictedNotes();
expect(conflictedNotes.length).toBe(1);
const notes = await Note.all();
expect(notes.length).toBe(2);
}
}
it('should not consider it is a conflict if neither the title nor body of the note have changed', (async () => {
await ignorableNoteConflictTest(false);
}));
it('should always handle conflict if local or remote are encrypted', (async () => {
await ignorableNoteConflictTest(true);
}));
});

View File

@ -0,0 +1,403 @@
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
const { synchronizerStart, allSyncTargetItemsEncrypted, kvStore, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, encryptionService, loadEncryptionMasterKey, decryptionWorker, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Resource = require('@joplin/lib/models/Resource.js');
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher');
const MasterKey = require('@joplin/lib/models/MasterKey');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
let insideBeforeEach = false;
describe('Synchronizer.e2ee', function() {
beforeEach(async (done) => {
insideBeforeEach = true;
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
insideBeforeEach = false;
});
it('notes and folders should get encrypted when encryption is enabled', (async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id });
await synchronizerStart();
// After synchronisation, remote items should be encrypted but local ones remain plain text
note1 = await Note.load(note1.id);
expect(note1.title).toBe('un');
await switchClient(2);
await synchronizerStart();
let folder1_2 = await Folder.load(folder1.id);
let note1_2 = await Note.load(note1.id);
const masterKey_2 = await MasterKey.load(masterKey.id);
// On this side however it should be received encrypted
expect(!note1_2.title).toBe(true);
expect(!folder1_2.title).toBe(true);
expect(!!note1_2.encryption_cipher_text).toBe(true);
expect(!!folder1_2.encryption_cipher_text).toBe(true);
// Master key is already encrypted so it does not get re-encrypted during sync
expect(masterKey_2.content).toBe(masterKey.content);
expect(masterKey_2.checksum).toBe(masterKey.checksum);
// Now load the master key we got from client 1 and try to decrypt
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
// Get the decrypted items back
await Folder.decrypt(folder1_2);
await Note.decrypt(note1_2);
folder1_2 = await Folder.load(folder1.id);
note1_2 = await Note.load(note1.id);
// Check that properties match the original items. Also check
// the encryption did not affect the updated_time timestamp.
expect(note1_2.title).toBe(note1.title);
expect(note1_2.body).toBe(note1.body);
expect(note1_2.updated_time).toBe(note1.updated_time);
expect(!note1_2.encryption_cipher_text).toBe(true);
expect(folder1_2.title).toBe(folder1.title);
expect(folder1_2.updated_time).toBe(folder1.updated_time);
expect(!folder1_2.encryption_cipher_text).toBe(true);
}));
it('should enable encryption automatically when downloading new master key (and none was previously available)',(async () => {
// Enable encryption on client 1 and sync an item
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: 'folder1' });
await synchronizerStart();
await switchClient(2);
// Synchronising should enable encryption since we're going to get a master key
expect(Setting.value('encryption.enabled')).toBe(false);
await synchronizerStart();
expect(Setting.value('encryption.enabled')).toBe(true);
// Check that we got the master key from client 1
const masterKey = (await MasterKey.all())[0];
expect(!!masterKey).toBe(true);
// Since client 2 hasn't supplied a password yet, no master key is currently loaded
expect(encryptionService().loadedMasterKeyIds().length).toBe(0);
// If we sync now, nothing should be sent to target since we don't have a password.
// Technically it's incorrect to set the property of an encrypted variable but it allows confirming
// that encryption doesn't work if user hasn't supplied a password.
await BaseItem.forceSync(folder1.id);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
folder1 = await Folder.load(folder1.id);
expect(folder1.title).toBe('folder1'); // Still at old value
await switchClient(2);
// Now client 2 set the master key password
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
// Now that master key should be loaded
expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id);
// Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted
await decryptionWorker().start();
await Folder.save({ id: folder1.id, title: 'change test' });
// If we sync now, this time client 1 should get the changes we did earlier
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
// Decrypt the data we just got
await decryptionWorker().start();
folder1 = await Folder.load(folder1.id);
expect(folder1.title).toBe('change test'); // Got title from client 2
}));
it('should encrypt existing notes too when enabling E2EE', (async () => {
// First create a folder, without encryption enabled, and sync it
await Folder.save({ title: 'folder1' });
await synchronizerStart();
let files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
let content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') >= 0).toBe(true);
// Then enable encryption and sync again
let masterKey = await encryptionService().generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
// Even though the folder has not been changed it should have been synced again so that
// an encrypted version of it replaces the decrypted version.
files = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
expect(files.items.length).toBe(2);
// By checking that the folder title is not present, we can confirm that the item has indeed been encrypted
// One of the two items is the master key
content = await fileApi().get(files.items[0].path);
expect(content.indexOf('folder1') < 0).toBe(true);
content = await fileApi().get(files.items[1].path);
expect(content.indexOf('folder1') < 0).toBe(true);
}));
it('should upload decrypted items to sync target after encryption disabled', (async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
await Folder.save({ title: 'folder1' });
await synchronizerStart();
let allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(true);
await encryptionService().disableEncryption();
await synchronizerStart();
allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(false);
}));
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', (async () => {
// For some reason I can't explain, this test is sometimes executed before beforeEach is finished
// which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done.
while (insideBeforeEach) await time.msleep(100);
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
await Folder.save({ title: 'folder1' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
expect(Setting.value('encryption.enabled')).toBe(true);
// If we try to disable encryption now, it should throw an error because some items are
// currently encrypted. They must be decrypted first so that they can be sent as
// plain text to the sync target.
// let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
// expect(hasThrown).toBe(true);
// Now supply the password, and decrypt the items
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
await decryptionWorker().start();
// Try to disable encryption again
const hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
expect(hasThrown).toBe(false);
// If we sync now the target should receive the decrypted items
await synchronizerStart();
const allEncrypted = await allSyncTargetItemsEncrypted();
expect(allEncrypted).toBe(false);
}));
it('should set the resource file size after decryption', (async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resource1 = (await Resource.all())[0];
await Resource.setFileSizeOnly(resource1.id, -1);
Resource.fullPath(resource1);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
const fetcher = new ResourceFetcher(() => { return synchronizer().api(); });
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
await decryptionWorker().start();
const resource1_2 = await Resource.load(resource1.id);
expect(resource1_2.size).toBe(2720);
}));
it('should encrypt remote resources after encryption has been enabled', (async () => {
while (insideBeforeEach) await time.msleep(100);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
await synchronizerStart();
expect(await allSyncTargetItemsEncrypted()).toBe(false);
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
expect(await allSyncTargetItemsEncrypted()).toBe(true);
}));
it('should upload encrypted resource, but it should not mark the blob as encrypted locally', (async () => {
while (insideBeforeEach) await time.msleep(100);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
const resource1 = (await Resource.all())[0];
expect(resource1.encryption_blob_encrypted).toBe(0);
}));
it('should decrypt the resource metadata, but not try to decrypt the file, if it is not present', (async () => {
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
expect(await allSyncTargetItemsEncrypted()).toBe(true);
await switchClient(2);
await synchronizerStart();
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
await decryptionWorker().start();
let resource = (await Resource.all())[0];
expect(!!resource.encryption_applied).toBe(false);
expect(!!resource.encryption_blob_encrypted).toBe(true);
const resourceFetcher = new ResourceFetcher(() => { return synchronizer().api(); });
await resourceFetcher.start();
await resourceFetcher.waitForAllFinished();
const ls = await Resource.localState(resource);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
await decryptionWorker().start();
resource = (await Resource.all())[0];
expect(!!resource.encryption_blob_encrypted).toBe(false);
}));
it('should stop trying to decrypt item after a few attempts', (async () => {
let hasThrown;
const note = await Note.save({ title: 'ma note' });
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
// First, simulate a broken note and check that the decryption worker
// gives up decrypting after a number of tries. This is mainly relevant
// for data that crashes the mobile application - we don't want to keep
// decrypting these.
const encryptedNote = await Note.load(note.id);
const goodCipherText = encryptedNote.encryption_cipher_text;
await Note.save({ id: note.id, encryption_cipher_text: 'doesntlookright' });
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' }));
expect(hasThrown).toBe(true);
hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' }));
expect(hasThrown).toBe(true);
// Third time, an error is logged and no error is thrown
hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' }));
expect(hasThrown).toBe(false);
const disabledItems = await decryptionWorker().decryptionDisabledItems();
expect(disabledItems.length).toBe(1);
expect(disabledItems[0].id).toBe(note.id);
expect((await kvStore().all()).length).toBe(1);
await kvStore().clear();
// Now check that if it fails once but succeed the second time, the note
// is correctly decrypted and the counters are cleared.
hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' }));
expect(hasThrown).toBe(true);
await Note.save({ id: note.id, encryption_cipher_text: goodCipherText });
hasThrown = await checkThrowAsync(async () => await decryptionWorker().start({ errorHandler: 'throw' }));
expect(hasThrown).toBe(false);
const decryptedNote = await Note.load(note.id);
expect(decryptedNote.title).toBe('ma note');
expect((await kvStore().all()).length).toBe(0);
expect((await decryptionWorker().decryptionDisabledItems()).length).toBe(0);
}));
it('should not encrypt notes that are shared', (async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await switchClient(1);
const origNote2 = Object.assign({}, note2);
await BaseItem.updateShareStatus(note2, true);
note2 = await Note.load(note2.id);
// Sharing a note should not modify the timestamps
expect(note2.user_updated_time).toBe(origNote2.user_updated_time);
expect(note2.user_created_time).toBe(origNote2.user_created_time);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
// The shared note should be decrypted
const note2_2 = await Note.load(note2.id);
expect(note2_2.title).toBe('deux');
expect(note2_2.is_shared).toBe(1);
// The non-shared note should be encrypted
const note1_2 = await Note.load(note1.id);
expect(note1_2.title).toBe('');
}));
});

View File

@ -0,0 +1,357 @@
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { remoteNotesFoldersResources, remoteResources } from './test-utils-synchronizer';
const { synchronizerStart, tempFilePath, resourceFetcher, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Resource = require('@joplin/lib/models/Resource.js');
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
let insideBeforeEach = false;
describe('Synchronizer.resources', function() {
beforeEach(async (done) => {
insideBeforeEach = true;
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
insideBeforeEach = false;
});
it('should sync resources', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resource1 = (await Resource.all())[0];
const resourcePath1 = Resource.fullPath(resource1);
await synchronizerStart();
expect((await remoteNotesFoldersResources()).length).toBe(3);
await switchClient(2);
await synchronizerStart();
const allResources = await Resource.all();
expect(allResources.length).toBe(1);
let resource1_2 = allResources[0];
let ls = await Resource.localState(resource1_2);
expect(resource1_2.id).toBe(resource1.id);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_IDLE);
const fetcher = new ResourceFetcher(() => { return synchronizer().api(); });
fetcher.queueDownload_(resource1_2.id);
await fetcher.waitForAllFinished();
resource1_2 = await Resource.load(resource1.id);
ls = await Resource.localState(resource1_2);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
const resourcePath1_2 = Resource.fullPath(resource1_2);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
}));
it('should handle resource download errors', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
let resource1 = (await Resource.all())[0];
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
const fetcher = new ResourceFetcher(() => {
return {
// Simulate a failed download
get: () => { return new Promise((_resolve: Function, reject: Function) => { reject(new Error('did not work')); }); },
};
});
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
resource1 = await Resource.load(resource1.id);
const ls = await Resource.localState(resource1);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_ERROR);
expect(ls.fetch_error).toBe('did not work');
}));
it('should set the resource file size if it is missing', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
let r1 = (await Resource.all())[0];
await Resource.setFileSizeOnly(r1.id, -1);
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(-1);
const fetcher = new ResourceFetcher(() => { return synchronizer().api(); });
fetcher.queueDownload_(r1.id);
await fetcher.waitForAllFinished();
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(2720);
}));
it('should delete resources', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resource1 = (await Resource.all())[0];
const resourcePath1 = Resource.fullPath(resource1);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
let allResources = await Resource.all();
expect(allResources.length).toBe(1);
expect((await remoteNotesFoldersResources()).length).toBe(3);
await Resource.delete(resource1.id);
await synchronizerStart();
expect((await remoteNotesFoldersResources()).length).toBe(2);
const remoteBlob = await fileApi().stat(`.resource/${resource1.id}`);
expect(!remoteBlob).toBe(true);
await switchClient(1);
expect(await shim.fsDriver().exists(resourcePath1)).toBe(true);
await synchronizerStart();
allResources = await Resource.all();
expect(allResources.length).toBe(0);
expect(await shim.fsDriver().exists(resourcePath1)).toBe(false);
}));
it('should encrypt resources', (async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resource1 = (await Resource.all())[0];
const resourcePath1 = Resource.fullPath(resource1);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
const fetcher = new ResourceFetcher(() => { return synchronizer().api(); });
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
let resource1_2 = (await Resource.all())[0];
resource1_2 = await Resource.decrypt(resource1_2);
const resourcePath1_2 = Resource.fullPath(resource1_2);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
}));
it('should sync resource blob changes', (async () => {
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
let resource1_2 = (await Resource.all())[0];
const modFile = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile, '1234 MOD', 'utf8');
await Resource.updateResourceBlobContent(resource1_2.id, modFile);
const originalSize = resource1_2.size;
resource1_2 = (await Resource.all())[0];
const newSize = resource1_2.size;
expect(originalSize).toBe(4);
expect(newSize).toBe(8);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const resource1_1 = (await Resource.all())[0];
expect(resource1_1.size).toBe(newSize);
expect(await Resource.resourceBlobContent(resource1_1.id, 'utf8')).toBe('1234 MOD');
}));
it('should handle resource conflicts', (async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizerStart();
}
await switchClient(2);
{
await synchronizerStart();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const resource = (await Resource.all())[0];
const modFile2 = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile2, '1234 MOD 2', 'utf8');
await Resource.updateResourceBlobContent(resource.id, modFile2);
await synchronizerStart();
}
await switchClient(1);
{
// Going to modify a resource without syncing first, which will cause a conflict
const resource = (await Resource.all())[0];
const modFile1 = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile1, '1234 MOD 1', 'utf8');
await Resource.updateResourceBlobContent(resource.id, modFile1);
await synchronizerStart(); // CONFLICT
// If we try to read the resource content now, it should throw because the local
// content has been moved to the conflict notebook, and the new local content
// has not been downloaded yet.
await checkThrowAsync(async () => await Resource.resourceBlobContent(resource.id));
// Now download resources, and our local content would have been overwritten by
// the content from client 2
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const localContent = await Resource.resourceBlobContent(resource.id, 'utf8');
expect(localContent).toBe('1234 MOD 2');
// Check that the Conflict note has been generated, with the conflict resource
// attached to it, and check that it has the original content.
const allNotes = await Note.all();
expect(allNotes.length).toBe(2);
const conflictNote = allNotes.find((v: NoteEntity) => {
return !!v.is_conflict;
});
expect(!!conflictNote).toBe(true);
const resourceIds = await Note.linkedResourceIds(conflictNote.body);
expect(resourceIds.length).toBe(1);
const conflictContent = await Resource.resourceBlobContent(resourceIds[0], 'utf8');
expect(conflictContent).toBe('1234 MOD 1');
}
}));
it('should handle resource conflicts if a resource is changed locally but deleted remotely', (async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizerStart();
}
await switchClient(2);
{
await synchronizerStart();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
}
await switchClient(1);
{
const resource = (await Resource.all())[0];
await Resource.delete(resource.id);
await synchronizerStart();
}
await switchClient(2);
{
const originalResource = (await Resource.all())[0];
await Resource.save({ id: originalResource.id, title: 'modified resource' });
await synchronizerStart(); // CONFLICT
const deletedResource = await Resource.load(originalResource.id);
expect(!deletedResource).toBe(true);
const allResources = await Resource.all();
expect(allResources.length).toBe(1);
const conflictResource = allResources[0];
expect(originalResource.id).not.toBe(conflictResource.id);
expect(conflictResource.title).toBe('modified resource');
}
}));
it('should not upload a resource if it has not been fetched yet', (async () => {
// In some rare cases, the synchronizer might try to upload a resource even though it
// doesn't have the resource file. It can happen in this situation:
// - C1 create resource
// - C1 sync
// - C2 sync
// - C2 resource metadata is received but ResourceFetcher hasn't downloaded the file yet
// - C2 enables E2EE - all the items are marked for forced sync
// - C2 sync
// The synchronizer will try to upload the resource, even though it doesn't have the file,
// so we need to make sure it doesn't. But also that once it gets the file, the resource
// does get uploaded.
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resource = (await Resource.all())[0];
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_IDLE });
await synchronizerStart();
expect((await remoteResources()).length).toBe(0);
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_DONE });
await synchronizerStart();
expect((await remoteResources()).length).toBe(1);
}));
it('should not download resources over the limit', (async () => {
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
await synchronizer().start();
await switchClient(2);
const previousMax = synchronizer().maxResourceSize_;
synchronizer().maxResourceSize_ = 1;
await synchronizerStart();
synchronizer().maxResourceSize_ = previousMax;
const syncItems = await BaseItem.allSyncItems(syncTargetId());
expect(syncItems.length).toBe(2);
expect(syncItems[1].item_location).toBe(BaseItem.SYNC_ITEM_LOCATION_REMOTE);
expect(syncItems[1].sync_disabled).toBe(1);
}));
});

View File

@ -0,0 +1,185 @@
import Setting from '@joplin/lib/models/Setting';
import BaseModel from '@joplin/lib/BaseModel';
const { synchronizerStart, revisionService, setupDatabaseAndSynchronizer, synchronizer, switchClient, encryptionService, loadEncryptionMasterKey, decryptionWorker } = require('./test-utils.js');
const Note = require('@joplin/lib/models/Note.js');
const Revision = require('@joplin/lib/models/Revision.js');
describe('Synchronizer.revisions', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
it('should not save revisions when updating a note via sync', (async () => {
// When a note is updated, a revision of the original is created.
// Here, on client 1, the note is updated for the first time, however since it is
// via sync, we don't create a revision - that revision has already been created on client
// 2 and is going to be synced.
const n1 = await Note.save({ title: 'testing' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.save({ id: n1.id, title: 'mod from client 2' });
await revisionService().collectRevisions();
const allRevs1 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs1.length).toBe(1);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const allRevs2 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs2.length).toBe(1);
expect(allRevs2[0].id).toBe(allRevs1[0].id);
}));
it('should not save revisions when deleting a note via sync', (async () => {
const n1 = await Note.save({ title: 'testing' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.delete(n1.id);
await revisionService().collectRevisions(); // REV 1
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
await synchronizerStart();
await switchClient(1);
await synchronizerStart(); // The local note gets deleted here, however a new rev is *not* created
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
const notes = await Note.all();
expect(notes.length).toBe(0);
}));
it('should not save revisions when an item_change has been generated as a result of a sync', (async () => {
// When a note is modified an item_change object is going to be created. This
// is used for example to tell the search engine, when note should be indexed. It is
// also used by the revision service to tell what note should get a new revision.
// When a note is modified via sync, this item_change object is also created. The issue
// is that we don't want to create revisions for these particular item_changes, because
// such revision has already been created on another client (whatever client initially
// modified the note), and that rev is going to be synced.
//
// So in the end we need to make sure that we don't create these unecessary additional revisions.
const n1 = await Note.save({ title: 'testing' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await Note.save({ id: n1.id, title: 'mod from client 2' });
await revisionService().collectRevisions();
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
await revisionService().collectRevisions();
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
}));
it('should handle case when new rev is created on client, then older rev arrives later via sync', (async () => {
// - C1 creates note 1
// - C1 modifies note 1 - REV1 created
// - C1 sync
// - C2 sync
// - C2 receives note 1
// - C2 modifies note 1 - REV2 created (but not based on REV1)
// - C2 receives REV1
//
// In that case, we need to make sure that REV1 and REV2 are both valid and can be retrieved.
// Even though REV1 was created before REV2, REV2 is *not* based on REV1. This is not ideal
// due to unecessary data being saved, but a possible edge case and we simply need to check
// all the data is valid.
// Note: this test seems to be a bit shaky because it doesn't work if the synchronizer
// context is passed around (via synchronizerStart()), but it should.
const n1 = await Note.save({ title: 'note' });
await Note.save({ id: n1.id, title: 'note REV1' });
await revisionService().collectRevisions(); // REV1
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
await synchronizer().start();
await switchClient(2);
synchronizer().testingHooks_ = ['skipRevisions'];
await synchronizer().start();
synchronizer().testingHooks_ = [];
await Note.save({ id: n1.id, title: 'note REV2' });
await revisionService().collectRevisions(); // REV2
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
await synchronizer().start(); // Sync the rev that had been skipped above with skipRevisions
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(revisions.length).toBe(2);
expect((await revisionService().revisionNote(revisions, 0)).title).toBe('note REV1');
expect((await revisionService().revisionNote(revisions, 1)).title).toBe('note REV2');
}));
it('should not create revisions when item is modified as a result of decryption', (async () => {
// Handle this scenario:
// - C1 creates note
// - C1 never changes it
// - E2EE is enabled
// - C1 sync
// - More than one week later (as defined by oldNoteCutOffDate_), C2 sync
// - C2 enters master password and note gets decrypted
//
// Technically at this point the note is modified (from encrypted to non-encrypted) and thus a ItemChange
// object is created. The note is also older than oldNoteCutOffDate. However, this should not lead to the
// creation of a revision because that change was not the result of a user action.
// I guess that's the general rule - changes that come from user actions should result in revisions,
// while automated changes (sync, decryption) should not.
const dateInPast = revisionService().oldNoteCutOffDate_() - 1000;
await Note.save({ title: 'ma note', updated_time: dateInPast, created_time: dateInPast }, { autoTimestamp: false });
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
Setting.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
await decryptionWorker().start();
await revisionService().collectRevisions();
expect((await Revision.all()).length).toBe(0);
}));
});

View File

@ -0,0 +1,75 @@
import Setting from '@joplin/lib/models/Setting';
const { synchronizerStart, setupDatabaseAndSynchronizer, switchClient, encryptionService, loadEncryptionMasterKey } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Tag = require('@joplin/lib/models/Tag.js');
const MasterKey = require('@joplin/lib/models/MasterKey');
describe('Synchronizer.tags', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
async function shoudSyncTagTest(withEncryption: boolean) {
let masterKey = null;
if (withEncryption) {
Setting.setValue('encryption.enabled', true);
masterKey = await loadEncryptionMasterKey();
}
await Folder.save({ title: 'folder' });
const n1 = await Note.save({ title: 'mynote' });
const n2 = await Note.save({ title: 'mynote2' });
const tag = await Tag.save({ title: 'mytag' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
if (withEncryption) {
const masterKey_2 = await MasterKey.load(masterKey.id);
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
const t = await Tag.load(tag.id);
await Tag.decrypt(t);
}
const remoteTag = await Tag.loadByTitle(tag.title);
expect(!!remoteTag).toBe(true);
expect(remoteTag.id).toBe(tag.id);
await Tag.addNote(remoteTag.id, n1.id);
await Tag.addNote(remoteTag.id, n2.id);
let noteIds = await Tag.noteIds(tag.id);
expect(noteIds.length).toBe(2);
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
let remoteNoteIds = await Tag.noteIds(tag.id);
expect(remoteNoteIds.length).toBe(2);
await Tag.removeNote(tag.id, n1.id);
remoteNoteIds = await Tag.noteIds(tag.id);
expect(remoteNoteIds.length).toBe(1);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
noteIds = await Tag.noteIds(tag.id);
expect(noteIds.length).toBe(1);
expect(remoteNoteIds[0]).toBe(noteIds[0]);
}
it('should sync tags', (async () => {
await shoudSyncTagTest(false);
}));
it('should sync encrypted tags', (async () => {
await shoudSyncTagTest(true);
}));
});

View File

@ -1,13 +1,9 @@
/* eslint-disable no-unused-vars */
const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const TaskQueue = require('@joplin/lib/TaskQueue.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('TaskQueue', function() {
beforeEach(async (done) => {
@ -16,7 +12,7 @@ describe('TaskQueue', function() {
done();
});
it('should queue and execute tasks', asyncTest(async () => {
it('should queue and execute tasks', (async () => {
const queue = new TaskQueue();
queue.push(1, async () => { await sleep(0.5); return 'a'; });
@ -37,7 +33,7 @@ describe('TaskQueue', function() {
expect(results[2].result).toBe('c');
}));
it('should handle errors', asyncTest(async () => {
it('should handle errors', (async () => {
const queue = new TaskQueue();
queue.push(1, async () => { await sleep(0.5); return 'a'; });

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { sortedIds, createNTestNotes, asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { sortedIds, createNTestNotes, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Setting = require('@joplin/lib/models/Setting').default;
@ -10,10 +10,6 @@ const BaseModel = require('@joplin/lib/BaseModel').default;
const ArrayUtils = require('@joplin/lib/ArrayUtils.js');
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('database', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
@ -21,7 +17,7 @@ describe('database', function() {
done();
});
it('should not modify cached field names', asyncTest(async () => {
it('should not modify cached field names', (async () => {
const db = BaseModel.db();
const fieldNames = db.tableFieldNames('notes');

View File

@ -1,13 +1,9 @@
'use strict';
const { asyncTest,checkThrow } = require('./test-utils.js');
const { checkThrow } = require('./test-utils.js');
const eventManager = require('@joplin/lib/eventManager').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('eventManager', function() {
beforeEach(async (done) => {
@ -20,7 +16,7 @@ describe('eventManager', function() {
done();
});
it('should watch state props', asyncTest(async () => {
it('should watch state props', (async () => {
let localStateName = '';
let callCount = 0;
@ -51,7 +47,7 @@ describe('eventManager', function() {
expect(callCount).toBe(2);
}));
it('should unwatch state props', asyncTest(async () => {
it('should unwatch state props', (async () => {
let localStateName = '';
function nameWatch(event) {
@ -69,7 +65,7 @@ describe('eventManager', function() {
expect(localStateName).toBe('');
}));
it('should watch nested props', asyncTest(async () => {
it('should watch nested props', (async () => {
let localStateName = '';
function nameWatch(event) {
@ -94,7 +90,7 @@ describe('eventManager', function() {
expect(localStateName).toBe('paul');
}));
it('should not be possible to modify state props', asyncTest(async () => {
it('should not be possible to modify state props', (async () => {
let localUser = {};
function userWatch(event) {

View File

@ -1,4 +1,4 @@
const { asyncTest, id, ids, createNTestFolders, sortedIds, createNTestNotes, TestApp } = require('./test-utils.js');
const { id, ids, createNTestFolders, sortedIds, createNTestNotes, TestApp } = require('./test-utils.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const uuid = require('@joplin/lib/uuid').default;
const Note = require('@joplin/lib/models/Note.js');
@ -35,7 +35,7 @@ describe('feature_NoteHistory', function() {
done();
});
it('should save history when navigating through notes', asyncTest(async () => {
it('should save history when navigating through notes', (async () => {
// setup
const folders = await createNTestFolders(2);
await testApp.wait();
@ -87,7 +87,7 @@ describe('feature_NoteHistory', function() {
}));
it('should save history when navigating through notebooks', asyncTest(async () => {
it('should save history when navigating through notebooks', (async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
@ -127,7 +127,7 @@ describe('feature_NoteHistory', function() {
}));
it('should save history when searching for a note', asyncTest(async () => {
it('should save history when searching for a note', (async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
@ -169,7 +169,7 @@ describe('feature_NoteHistory', function() {
expect(ids(state.forwardHistoryNotes)).toEqual([]);
}));
it('should ensure no adjacent duplicates', asyncTest(async () => {
it('should ensure no adjacent duplicates', (async () => {
const folders = await createNTestFolders(2);
const notes0 = await createNTestNotes(3, folders[0]);
await testApp.wait();
@ -207,7 +207,7 @@ describe('feature_NoteHistory', function() {
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history is not corrupted when notes get deleted.', asyncTest(async () => {
it('should ensure history is not corrupted when notes get deleted.', (async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
@ -237,7 +237,7 @@ describe('feature_NoteHistory', function() {
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history is not corrupted when notes get created.', asyncTest(async () => {
it('should ensure history is not corrupted when notes get created.', (async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
@ -298,7 +298,7 @@ describe('feature_NoteHistory', function() {
expect(state.selectedFolderId).toEqual(folders[0].id);
}));
it('should ensure history works when traversing all notes', asyncTest(async () => {
it('should ensure history works when traversing all notes', (async () => {
const folders = await createNTestFolders(2);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);
@ -356,7 +356,7 @@ describe('feature_NoteHistory', function() {
expect(state.selectedNoteIds).toEqual([notes0[4].id]);
}));
it('should ensure history works when traversing through conflict notes', asyncTest(async () => {
it('should ensure history works when traversing through conflict notes', (async () => {
const folders = await createNTestFolders(1);
await testApp.wait();
const notes0 = await createNTestNotes(5, folders[0]);

View File

@ -1,5 +1,5 @@
/* eslint-disable no-unused-vars */
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
@ -23,7 +23,7 @@ describe('integration_NoteList', function() {
});
// Reference: https://github.com/laurent22/joplin/issues/2709
it('should leave a conflict note in the conflict folder when it modified', asyncTest(async () => {
it('should leave a conflict note in the conflict folder when it modified', (async () => {
const folder = await Folder.save({ title: 'test' });
const note = await Note.save({ title: 'note 1', parent_id: folder.id, is_conflict: 1 });
await testApp.wait();

View File

@ -1,5 +1,5 @@
/* eslint-disable no-unused-vars */
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, id, ids, sortedIds, at, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, id, ids, sortedIds, at, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
@ -36,7 +36,7 @@ describe('integration_ShowAllNotes', function() {
done();
});
it('should show all notes', asyncTest(async () => {
it('should show all notes', (async () => {
// setup
const folders = await createNTestFolders(3);
Folder.moveToFolder(id(folders[2]), id(folders[1])); // subfolder
@ -57,7 +57,7 @@ describe('integration_ShowAllNotes', function() {
expect(sortedIds(state.notes)).toEqual(sortedIds(notes0.concat(notes1).concat(notes2)));
}));
it('should show retain note selection when going from a folder to all-notes', asyncTest(async () => {
it('should show retain note selection when going from a folder to all-notes', (async () => {
// setup
const folders = await createNTestFolders(2);
const notes0 = await createNTestNotes(3, folders[0]);
@ -88,7 +88,7 @@ describe('integration_ShowAllNotes', function() {
expect(state.selectedNoteIds).toEqual(ids([notes1[1]]));
}));
it('should support note duplication', asyncTest(async () => {
it('should support note duplication', (async () => {
// setup
const folder1 = await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });
@ -125,7 +125,7 @@ describe('integration_ShowAllNotes', function() {
expect(sortedIds(state.notes)).toEqual(sortedIds([note1, note2, newNote1, newNote2]));
}));
it('should support changing the note parent', asyncTest(async () => {
it('should support changing the note parent', (async () => {
// setup
const folder1 = await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });

View File

@ -1,5 +1,5 @@
/* eslint-disable no-unused-vars */
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('./test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
@ -23,7 +23,7 @@ describe('integration_TagList', function() {
});
// the tag list should be cleared if the next note has no tags
it('should clear tag list when a note is deleted', asyncTest(async () => {
it('should clear tag list when a note is deleted', (async () => {
// setup and select the note
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
@ -54,7 +54,7 @@ describe('integration_TagList', function() {
}));
// the tag list should be updated if the next note has tags
it('should update tag list when a note is deleted', asyncTest(async () => {
it('should update tag list when a note is deleted', (async () => {
// set up and select the note
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);

View File

@ -3,15 +3,11 @@
const uuid = require('@joplin/lib/uuid').default;
const time = require('@joplin/lib/time').default;
const { asyncTest, sleep, fileApi, fileContentEqual, checkThrowAsync } = require('./test-utils.js');
const { sleep, fileApi, fileContentEqual, checkThrowAsync } = require('./test-utils.js');
const shim = require('@joplin/lib/shim').default;
const fs = require('fs-extra');
const Setting = require('@joplin/lib/models/Setting').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const api = null;
// Adding empty test for Jest
@ -42,7 +38,7 @@ it('will pass', () => {
// });
// describe('list', function() {
// it('should return items with relative path', asyncTest(async () => {
// it('should return items with relative path', (async () => {
// await api.mkdir('.subfolder');
// await api.put('1', 'something on root 1');
// await api.put('.subfolder/1', 'something subfolder 1');
@ -57,7 +53,7 @@ it('will pass', () => {
// expect(items[0].updated_time).toMatch(/^\d+$/); // make sure it's using epoch timestamp
// }));
// it('should default to only files on root directory', asyncTest(async () => {
// it('should default to only files on root directory', (async () => {
// await api.mkdir('.subfolder');
// await api.put('.subfolder/1', 'something subfolder 1');
// await api.put('file1', 'something 1');
@ -70,12 +66,12 @@ it('will pass', () => {
// }); // list
// describe('delete', function() {
// it('should not error if file does not exist', asyncTest(async () => {
// it('should not error if file does not exist', (async () => {
// const hasThrown = await checkThrowAsync(async () => await api.delete('nonexistant_file'));
// expect(hasThrown).toBe(false);
// }));
// it('should delete specific file given full path', asyncTest(async () => {
// it('should delete specific file given full path', (async () => {
// await api.mkdir('deleteDir');
// await api.put('deleteDir/1', 'something 1');
// await api.put('deleteDir/2', 'something 2');
@ -90,19 +86,19 @@ it('will pass', () => {
// }); // delete
// describe('get', function() {
// it('should return null if object does not exist', asyncTest(async () => {
// it('should return null if object does not exist', (async () => {
// const response = await api.get('nonexistant_file');
// expect(response).toBe(null);
// }));
// it('should return UTF-8 encoded string by default', asyncTest(async () => {
// it('should return UTF-8 encoded string by default', (async () => {
// await api.put('testnote.md', 'something 2');
// const response = await api.get('testnote.md');
// expect(response).toBe('something 2');
// }));
// it('should return a Response object and writes file to options.path, if options.target is "file"', asyncTest(async () => {
// it('should return a Response object and writes file to options.path, if options.target is "file"', (async () => {
// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`;
// await api.put('testnote.md', 'something 2');
// sleep(0.2);
@ -116,7 +112,7 @@ it('will pass', () => {
// }); // get
// describe('put', function() {
// it('should create file to remote path and content', asyncTest(async () => {
// it('should create file to remote path and content', (async () => {
// await api.put('putTest.md', 'I am your content');
// sleep(0.2);
@ -124,7 +120,7 @@ it('will pass', () => {
// expect(response).toBe('I am your content');
// }));
// it('should upload file in options.path to remote path, if options.source is "file"', asyncTest(async () => {
// it('should upload file in options.path to remote path, if options.source is "file"', (async () => {
// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`;
// fs.writeFileSync(localFilePath, 'I am the local file.');

View File

@ -1,20 +1,14 @@
/* eslint-disable no-unused-vars */
const { asyncTest } = require('./test-utils.js');
const htmlUtils = require('@joplin/lib/htmlUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('htmlUtils', function() {
beforeEach(async (done) => {
done();
});
it('should extract image URLs', asyncTest(async () => {
it('should extract image URLs', (async () => {
const testCases = [
['<img src="http://test.com/img.png"/>', ['http://test.com/img.png']],
['<img src="http://test.com/img.png"/> <img src="http://test.com/img2.png"/>', ['http://test.com/img.png', 'http://test.com/img2.png']],
@ -32,7 +26,7 @@ describe('htmlUtils', function() {
}
}));
it('should replace image URLs', asyncTest(async () => {
it('should replace image URLs', (async () => {
const testCases = [
['<img src="http://test.com/img.png"/>', ['http://other.com/img2.png'], '<img src="http://other.com/img2.png"/>'],
['<img src="http://test.com/img.png"/> <img src="http://test.com/img2.png"/>', ['http://other.com/img2.png', 'http://other.com/img3.png'], '<img src="http://other.com/img2.png"/> <img src="http://other.com/img3.png"/>'],
@ -55,7 +49,7 @@ describe('htmlUtils', function() {
}
}));
it('should encode attributes', asyncTest(async () => {
it('should encode attributes', (async () => {
const testCases = [
[{ a: 'one', b: 'two' }, 'a="one" b="two"'],
[{ a: 'one&two' }, 'a="one&amp;two"'],
@ -68,7 +62,7 @@ describe('htmlUtils', function() {
}
}));
it('should prepend a base URL', asyncTest(async () => {
it('should prepend a base URL', (async () => {
const testCases = [
[
'<a href="a.html">Something</a>',

View File

@ -1,20 +1,14 @@
/* eslint-disable no-unused-vars */
const { asyncTest } = require('./test-utils.js');
const markdownUtils = require('@joplin/lib/markdownUtils').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at markdownUtils: Promise', p, 'reason:', reason);
});
describe('markdownUtils', function() {
beforeEach(async (done) => {
done();
});
it('should prepend a base URL', asyncTest(async () => {
it('should prepend a base URL', (async () => {
const baseUrl = 'https://test.com/site';
const testCases = [
@ -32,7 +26,7 @@ describe('markdownUtils', function() {
}
}));
it('should extract image URLs', asyncTest(async () => {
it('should extract image URLs', (async () => {
const testCases = [
['![something](http://test.com/img.png)', ['http://test.com/img.png']],
['![something](http://test.com/img.png) ![something2](http://test.com/img2.png)', ['http://test.com/img.png', 'http://test.com/img2.png']],
@ -50,7 +44,7 @@ describe('markdownUtils', function() {
}
}));
it('escape a markdown link', asyncTest(async () => {
it('escape a markdown link', (async () => {
const testCases = [
['file:///Users/who put spaces in their username??/.config/joplin', 'file:///Users/who%20put%20spaces%20in%20their%20username??/.config/joplin'],
@ -65,7 +59,7 @@ describe('markdownUtils', function() {
}
}));
it('escape a markdown link (title)', asyncTest(async () => {
it('escape a markdown link (title)', (async () => {
const testCases = [
['Helmut K. C. Tessarek', 'Helmut K. C. Tessarek'],
@ -80,7 +74,7 @@ describe('markdownUtils', function() {
}
}));
it('replace markdown link with description', asyncTest(async () => {
it('replace markdown link with description', (async () => {
const testCases = [
['Test case [one](link)', 'Test case one'],

View File

@ -2,27 +2,23 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('mimeUils', function() {
beforeEach(async (done) => {
done();
});
it('should get the file extension from the mime type', asyncTest(async () => {
it('should get the file extension from the mime type', (async () => {
expect(mimeUtils.toFileExtension('image/jpeg')).toBe('jpg');
expect(mimeUtils.toFileExtension('image/jpg')).toBe('jpg');
expect(mimeUtils.toFileExtension('IMAGE/JPG')).toBe('jpg');
expect(mimeUtils.toFileExtension('')).toBe(null);
}));
it('should get the mime type from the filename', asyncTest(async () => {
it('should get the mime type from the filename', (async () => {
expect(mimeUtils.fromFilename('test.jpg')).toBe('image/jpeg');
expect(mimeUtils.fromFilename('test.JPG')).toBe('image/jpeg');
expect(mimeUtils.fromFilename('test.doesntexist')).toBe(null);

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
@ -10,10 +10,6 @@ const Resource = require('@joplin/lib/models/Resource.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_BaseItem: Promise', p, 'reason:', reason);
});
async function allItems() {
const folders = await Folder.all();
const notes = await Note.all();
@ -30,7 +26,7 @@ describe('models_BaseItem', function() {
// This is to handle the case where a property is removed from a BaseItem table - in that case files in
// the sync target will still have the old property but we don't need it locally.
it('should ignore properties that are present in sync file but not in database when serialising', asyncTest(async () => {
it('should ignore properties that are present in sync file but not in database when serialising', (async () => {
const folder = await Folder.save({ title: 'folder1' });
let serialized = await Folder.serialize(folder);
@ -41,7 +37,7 @@ describe('models_BaseItem', function() {
expect('ignore_me' in unserialized).toBe(false);
}));
it('should not modify title when unserializing', asyncTest(async () => {
it('should not modify title when unserializing', (async () => {
const folder1 = await Folder.save({ title: '' });
const folder2 = await Folder.save({ title: 'folder1' });
@ -56,7 +52,7 @@ describe('models_BaseItem', function() {
expect(unserialized2.title).toBe(folder2.title);
}));
it('should correctly unserialize note timestamps', asyncTest(async () => {
it('should correctly unserialize note timestamps', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note = await Note.save({ title: 'note', parent_id: folder.id });
@ -69,7 +65,7 @@ describe('models_BaseItem', function() {
expect(unserialized.user_updated_time).toEqual(note.user_updated_time);
}));
it('should serialize geolocation fields', asyncTest(async () => {
it('should serialize geolocation fields', (async () => {
const folder = await Folder.save({ title: 'folder' });
let note = await Note.save({ title: 'note', parent_id: folder.id });
note = await Note.load(note.id);
@ -92,7 +88,7 @@ describe('models_BaseItem', function() {
expect(unserialized.altitude).toEqual(note.altitude);
}));
it('should serialize and unserialize notes', asyncTest(async () => {
it('should serialize and unserialize notes', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note = await Note.save({ title: 'note', parent_id: folder.id });
await Note.updateGeolocation(note.id);
@ -104,7 +100,7 @@ describe('models_BaseItem', function() {
expect(noteAfter).toEqual(noteBefore);
}));
it('should serialize and unserialize properties that contain new lines', asyncTest(async () => {
it('should serialize and unserialize properties that contain new lines', (async () => {
const sourceUrl = `
https://joplinapp.org/ \\n
`;
@ -118,7 +114,7 @@ https://joplinapp.org/ \\n
expect(noteAfter).toEqual(noteBefore);
}));
it('should not serialize the note title and body', asyncTest(async () => {
it('should not serialize the note title and body', (async () => {
const note = await Note.save({ title: 'my note', body: `one line
two line
three line \\n no escape` });

View File

@ -1,12 +1,8 @@
import { FolderEntity } from '@joplin/lib/services/database/types';
const { createNTestNotes, asyncTest, setupDatabaseAndSynchronizer, sleep, switchClient, checkThrowAsync } = require('./test-utils.js');
const { createNTestNotes, setupDatabaseAndSynchronizer, sleep, switchClient, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_Folder: Promise', p, 'reason:', reason);
});
async function allItems() {
const folders = await Folder.all();
const notes = await Note.all();
@ -21,7 +17,7 @@ describe('models_Folder', function() {
done();
});
it('should tell if a notebook can be nested under another one', asyncTest(async () => {
it('should tell if a notebook can be nested under another one', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
@ -37,7 +33,7 @@ describe('models_Folder', function() {
expect(await Folder.canNestUnder(f2.id, '')).toBe(true);
}));
it('should recursively delete notes and sub-notebooks', asyncTest(async () => {
it('should recursively delete notes and sub-notebooks', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
@ -55,7 +51,7 @@ describe('models_Folder', function() {
expect(all.length).toBe(0);
}));
it('should sort by last modified, based on content', asyncTest(async () => {
it('should sort by last modified, based on content', (async () => {
let folders;
const f1 = await Folder.save({ title: 'folder1' }); await sleep(0.1);
@ -89,7 +85,7 @@ describe('models_Folder', function() {
expect(folders[2].id).toBe(f2.id);
}));
it('should sort by last modified, based on content (sub-folders too)', asyncTest(async () => {
it('should sort by last modified, based on content (sub-folders too)', (async () => {
let folders;
const f1 = await Folder.save({ title: 'folder1' }); await sleep(0.1);
@ -128,7 +124,7 @@ describe('models_Folder', function() {
expect(folders[3].id).toBe(f2.id);
}));
it('should add node counts', asyncTest(async () => {
it('should add node counts', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
@ -163,7 +159,7 @@ describe('models_Folder', function() {
}
}));
it('should not count completed to-dos', asyncTest(async () => {
it('should not count completed to-dos', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
@ -190,7 +186,7 @@ describe('models_Folder', function() {
expect(foldersById[f4.id].note_count).toBe(0);
}));
it('should recursively find folder path', asyncTest(async () => {
it('should recursively find folder path', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
@ -204,7 +200,7 @@ describe('models_Folder', function() {
expect(folderPath[2].id).toBe(f3.id);
}));
it('should sort folders alphabetically', asyncTest(async () => {
it('should sort folders alphabetically', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f1.id });
@ -224,7 +220,7 @@ describe('models_Folder', function() {
expect(sortedFolderTree[2].id).toBe(f6.id);
}));
it('should not allow setting a notebook parent as itself', asyncTest(async () => {
it('should not allow setting a notebook parent as itself', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const hasThrown = await checkThrowAsync(() => Folder.save({ id: f1.id, parent_id: f1.id }, { userSideValidation: true }));
expect(hasThrown).toBe(true);

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, revisionService, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, revisionService, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
const ResourceService = require('@joplin/lib/services/ResourceService').default;
const ItemChangeUtils = require('@joplin/lib/services/ItemChangeUtils');
@ -10,10 +10,6 @@ const Note = require('@joplin/lib/models/Note');
const Setting = require('@joplin/lib/models/Setting').default;
const ItemChange = require('@joplin/lib/models/ItemChange');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
let searchEngine = null;
describe('models_ItemChange', function() {
@ -26,7 +22,7 @@ describe('models_ItemChange', function() {
done();
});
it('should delete old changes that have been processed', asyncTest(async () => {
it('should delete old changes that have been processed', (async () => {
const n1 = await Note.save({ title: 'abcd efgh' }); // 3
await ItemChange.waitForAllSaved();

View File

@ -1,15 +1,11 @@
import Setting from '@joplin/lib/models/Setting';
import BaseModel from '@joplin/lib/BaseModel';
import shim from '@joplin/lib/shim';
const { sortedIds, createNTestNotes, asyncTest, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync } = require('./test-utils.js');
const { sortedIds, createNTestNotes, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const ArrayUtils = require('@joplin/lib/ArrayUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_Note: Promise', p, 'reason:', reason);
});
async function allItems() {
const folders = await Folder.all();
const notes = await Note.all();
@ -23,7 +19,7 @@ describe('models_Note', function() {
done();
});
it('should find resource and note IDs', asyncTest(async () => {
it('should find resource and note IDs', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
let note2 = await Note.save({ title: 'ma deuxième note', body: `Lien vers première note : ${Note.markdownTag(note1)}`, parent_id: folder1.id });
@ -47,7 +43,7 @@ describe('models_Note', function() {
expect(items.length).toBe(4);
}));
it('should find linked items', asyncTest(async () => {
it('should find linked items', (async () => {
const testCases = [
['[](:/06894e83b8f84d3d8cbe0f1587f9e226)', ['06894e83b8f84d3d8cbe0f1587f9e226']],
['[](:/06894e83b8f84d3d8cbe0f1587f9e226) [](:/06894e83b8f84d3d8cbe0f1587f9e226)', ['06894e83b8f84d3d8cbe0f1587f9e226']],
@ -69,7 +65,7 @@ describe('models_Note', function() {
}
}));
it('should change the type of notes', asyncTest(async () => {
it('should change the type of notes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
note1 = await Note.load(note1.id);
@ -90,7 +86,7 @@ describe('models_Note', function() {
expect(!!changedNote.is_todo).toBe(false);
}));
it('should serialize and unserialize without modifying data', asyncTest(async () => {
it('should serialize and unserialize without modifying data', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const testCases = [
[{ title: '', body: 'Body and no title\nSecond line\nThird Line', parent_id: folder1.id },
@ -115,7 +111,7 @@ describe('models_Note', function() {
}
}));
it('should reset fields for a duplicate', asyncTest(async () => {
it('should reset fields for a duplicate', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'note', parent_id: folder1.id });
@ -128,7 +124,7 @@ describe('models_Note', function() {
expect(duplicatedNote.user_updated_time !== note1.user_updated_time).toBe(true);
}));
it('should delete a set of notes', asyncTest(async () => {
it('should delete a set of notes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const noOfNotes = 20;
await createNTestNotes(noOfNotes, folder1);
@ -141,7 +137,7 @@ describe('models_Note', function() {
expect(all[0].id).toBe(folder1.id);
}));
it('should delete only the selected notes', asyncTest(async () => {
it('should delete only the selected notes', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
@ -171,7 +167,7 @@ describe('models_Note', function() {
expect(intersection.length).toBe(0);
}));
it('should delete nothing', asyncTest(async () => {
it('should delete nothing', (async () => {
const f1 = await Folder.save({ title: 'folder1' });
const f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
const f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
@ -190,7 +186,7 @@ describe('models_Note', function() {
expect(sortedIds(afterDelete)).toEqual(sortedIds(beforeDelete));
}));
it('should not move to conflict folder', asyncTest(async () => {
it('should not move to conflict folder', (async () => {
const folder1 = await Folder.save({ title: 'Folder' });
const folder2 = await Folder.save({ title: Folder.conflictFolderTitle(), id: Folder.conflictFolderId() });
const note1 = await Note.save({ title: 'note', parent_id: folder1.id });
@ -202,7 +198,7 @@ describe('models_Note', function() {
expect(note.parent_id).toEqual(folder1.id);
}));
it('should not copy to conflict folder', asyncTest(async () => {
it('should not copy to conflict folder', (async () => {
const folder1 = await Folder.save({ title: 'Folder' });
const folder2 = await Folder.save({ title: Folder.conflictFolderTitle(), id: Folder.conflictFolderId() });
const note1 = await Note.save({ title: 'note', parent_id: folder1.id });
@ -211,7 +207,7 @@ describe('models_Note', function() {
expect(hasThrown).toBe(true);
}));
it('should convert resource paths from internal to external paths', asyncTest(async () => {
it('should convert resource paths from internal to external paths', (async () => {
const resourceDirName = Setting.value('resourceDirName');
const resourceDir = Setting.value('resourceDir');
const r1 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { sortedIds, createNTestNotes, asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { sortedIds, createNTestNotes, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Setting = require('@joplin/lib/models/Setting').default;
@ -10,10 +10,6 @@ const BaseModel = require('@joplin/lib/BaseModel').default;
const ArrayUtils = require('@joplin/lib/ArrayUtils.js');
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_Note_CustomSortOrder: Promise', p, 'reason:', reason);
});
async function allItems() {
const folders = await Folder.all();
const notes = await Note.all();
@ -27,7 +23,7 @@ describe('models_Note_CustomSortOrder', function() {
done();
});
it('should set the order property when saving a note', asyncTest(async () => {
it('should set the order property when saving a note', (async () => {
const now = Date.now();
const n1 = await Note.save({ title: 'testing' });
expect(n1.order).toBeGreaterThanOrEqual(now);
@ -36,7 +32,7 @@ describe('models_Note_CustomSortOrder', function() {
expect(n2.order).toBe(0);
}));
it('should insert notes at the specified position (order 0)', asyncTest(async () => {
it('should insert notes at the specified position (order 0)', (async () => {
// Notes always had an "order" property, but for a long time it wasn't used, and
// set to 0. For custom sorting to work though, it needs to be set to some number
// (which normally is the creation timestamp). So if the user tries to move notes
@ -90,7 +86,7 @@ describe('models_Note_CustomSortOrder', function() {
expect(sortedNotes[4].id).toBe(notes1[0].id);
}));
it('should insert notes at the specified position (targets with same orders)', asyncTest(async () => {
it('should insert notes at the specified position (targets with same orders)', (async () => {
// If the target notes all have the same order, inserting a note should work
// anyway, because the order of the other notes will be updated as needed.
@ -115,7 +111,7 @@ describe('models_Note_CustomSortOrder', function() {
expect(sortedNotes[3].id).toBe(notes[1].id);
}));
it('should insert notes at the specified position (insert at end)', asyncTest(async () => {
it('should insert notes at the specified position (insert at end)', (async () => {
const folder1 = await Folder.save({});
const notes = [];
@ -138,7 +134,7 @@ describe('models_Note_CustomSortOrder', function() {
expect(sortedNotes[3].id).toBe(notes[1].id);
}));
it('should insert notes at the specified position (insert at beginning)', asyncTest(async () => {
it('should insert notes at the specified position (insert at beginning)', (async () => {
const folder1 = await Folder.save({});
const notes = [];
@ -161,7 +157,7 @@ describe('models_Note_CustomSortOrder', function() {
expect(sortedNotes[3].id).toBe(notes[0].id);
}));
it('should insert notes even if sources are not adjacent', asyncTest(async () => {
it('should insert notes even if sources are not adjacent', (async () => {
const folder1 = await Folder.save({});
const notes = [];

View File

@ -2,17 +2,13 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Resource = require('@joplin/lib/models/Resource.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const testImagePath = `${__dirname}/../tests/support/photo.jpg`;
describe('models_Resource', function() {
@ -23,7 +19,7 @@ describe('models_Resource', function() {
done();
});
it('should have a "done" fetch_status when created locally', asyncTest(async () => {
it('should have a "done" fetch_status when created locally', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, testImagePath);
@ -32,7 +28,7 @@ describe('models_Resource', function() {
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
}));
it('should have a default local state', asyncTest(async () => {
it('should have a default local state', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, testImagePath);
@ -43,7 +39,7 @@ describe('models_Resource', function() {
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
}));
it('should save and delete local state', asyncTest(async () => {
it('should save and delete local state', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, testImagePath);
@ -59,7 +55,7 @@ describe('models_Resource', function() {
expect(!ls.id).toBe(true);
}));
it('should resize the resource if the image is below the required dimensions', asyncTest(async () => {
it('should resize the resource if the image is below the required dimensions', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
const previousMax = Resource.IMAGE_MAX_DIMENSION;
@ -74,7 +70,7 @@ describe('models_Resource', function() {
expect(newStat.size < originalStat.size).toBe(true);
}));
it('should not resize the resource if the image is below the required dimensions', asyncTest(async () => {
it('should not resize the resource if the image is below the required dimensions', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, testImagePath);

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const NoteTag = require('@joplin/lib/models/NoteTag.js');
@ -11,10 +11,6 @@ const Revision = require('@joplin/lib/models/Revision.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('models_Revision', function() {
beforeEach(async (done) => {
@ -23,7 +19,7 @@ describe('models_Revision', function() {
done();
});
it('should create patches of text and apply it', asyncTest(async () => {
it('should create patches of text and apply it', (async () => {
const note1 = await Note.save({ body: 'my note\nsecond line' });
const patch = Revision.createTextPatch(note1.body, 'my new note\nsecond line');
@ -32,7 +28,7 @@ describe('models_Revision', function() {
expect(merged).toBe('my new note\nsecond line');
}));
it('should create patches of objects and apply it', asyncTest(async () => {
it('should create patches of objects and apply it', (async () => {
const oldObject = {
one: '123',
two: '456',
@ -50,7 +46,7 @@ describe('models_Revision', function() {
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
}));
it('should move target revision to the top', asyncTest(async () => {
it('should move target revision to the top', (async () => {
const revs = [
{ id: '123' },
{ id: '456' },
@ -69,7 +65,7 @@ describe('models_Revision', function() {
expect(newRevs[2].id).toBe('789');
}));
it('should create patch stats', asyncTest(async () => {
it('should create patch stats', (async () => {
const tests = [
{
patch: `@@ -625,16 +625,48 @@

View File

@ -1,10 +1,6 @@
import Setting from '@joplin/lib/models/Setting';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_Setting: Promise', p, 'reason:', reason);
});
const { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
describe('models_Setting', function() {
@ -14,7 +10,7 @@ describe('models_Setting', function() {
done();
});
it('should return only sub-values', asyncTest(async () => {
it('should return only sub-values', (async () => {
const settings = {
'sync.5.path': 'http://example.com',
'sync.5.username': 'testing',
@ -29,7 +25,7 @@ describe('models_Setting', function() {
expect('username' in output).toBe(false);
}));
it('should allow registering new settings dynamically', asyncTest(async () => {
it('should allow registering new settings dynamically', (async () => {
await expectThrow(async () => Setting.setValue('myCustom', '123'));
await Setting.registerSetting('myCustom', {
@ -43,7 +39,7 @@ describe('models_Setting', function() {
expect(Setting.value('myCustom')).toBe('123');
}));
it('should not clear old custom settings', asyncTest(async () => {
it('should not clear old custom settings', (async () => {
// In general the following should work:
//
// - Plugin register a new setting
@ -85,7 +81,7 @@ describe('models_Setting', function() {
expect(Setting.value('myCustom')).toBe('123');
}));
it('should return values with correct type for custom settings', asyncTest(async () => {
it('should return values with correct type for custom settings', (async () => {
await Setting.registerSetting('myCustom', {
public: true,
value: 123,
@ -108,7 +104,7 @@ describe('models_Setting', function() {
expect(Setting.value('myCustom')).toBe(456);
}));
it('should validate registered keys', asyncTest(async () => {
it('should validate registered keys', (async () => {
const md = {
public: true,
value: 'default',
@ -124,7 +120,7 @@ describe('models_Setting', function() {
await expectNotThrow(async () => await Setting.registerSetting('so-ARE-dashes_123', md));
}));
it('should register new sections', asyncTest(async () => {
it('should register new sections', (async () => {
await Setting.registerSection('mySection', {
label: 'My section',
});

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const NoteTag = require('@joplin/lib/models/NoteTag.js');
@ -10,10 +10,6 @@ const Tag = require('@joplin/lib/models/Tag.js');
const BaseModel = require('@joplin/lib/BaseModel').default;
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at models_Tag: Promise', p, 'reason:', reason);
});
describe('models_Tag', function() {
beforeEach(async (done) => {
@ -22,7 +18,7 @@ describe('models_Tag', function() {
done();
});
it('should add tags by title', asyncTest(async () => {
it('should add tags by title', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
@ -32,7 +28,7 @@ describe('models_Tag', function() {
expect(noteTags.length).toBe(2);
}));
it('should not allow renaming tag to existing tag names', asyncTest(async () => {
it('should not allow renaming tag to existing tag names', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
@ -44,7 +40,7 @@ describe('models_Tag', function() {
expect(hasThrown).toBe(true);
}));
it('should not return tags without notes', asyncTest(async () => {
it('should not return tags without notes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await Tag.setNoteTagsByTitles(note1.id, ['un']);
@ -58,7 +54,7 @@ describe('models_Tag', function() {
expect(tags.length).toBe(0);
}));
it('should return tags with note counts', asyncTest(async () => {
it('should return tags with note counts', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
@ -81,7 +77,7 @@ describe('models_Tag', function() {
expect(tags.length).toBe(0);
}));
it('should load individual tags with note count', asyncTest(async () => {
it('should load individual tags with note count', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
const note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
@ -96,7 +92,7 @@ describe('models_Tag', function() {
expect(tagWithCount.note_count).toBe(2);
}));
it('should get common tags for set of notes', asyncTest(async () => {
it('should get common tags for set of notes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const taga = await Tag.save({ title: 'mytaga' });
const tagb = await Tag.save({ title: 'mytagb' });

View File

@ -2,11 +2,7 @@
const { extractExecutablePath, quotePath, unquotePath, friendlySafeFilename, toFileProtocolPath } = require('@joplin/lib/path-utils');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
describe('pathUtils', function() {
@ -14,7 +10,7 @@ describe('pathUtils', function() {
done();
});
it('should create friendly safe filename', asyncTest(async () => {
it('should create friendly safe filename', (async () => {
const testCases = [
['生活', '生活'],
['not/good', 'not_good'],
@ -35,7 +31,7 @@ describe('pathUtils', function() {
expect(!!friendlySafeFilename('...')).toBe(true);
}));
it('should quote and unquote paths', asyncTest(async () => {
it('should quote and unquote paths', (async () => {
const testCases = [
['', ''],
['/my/path', '/my/path'],
@ -52,7 +48,7 @@ describe('pathUtils', function() {
}
}));
it('should extract executable path from command', asyncTest(async () => {
it('should extract executable path from command', (async () => {
const testCases = [
['', ''],
['/my/cmd -some -args', '/my/cmd'],
@ -68,7 +64,7 @@ describe('pathUtils', function() {
}
}));
it('should create correct fileURL syntax', asyncTest(async () => {
it('should create correct fileURL syntax', (async () => {
const testCases_win32 = [
['C:\\handle\\space test', 'file:///C:/handle/space+test'],
['C:\\escapeplus\\+', 'file:///C:/escapeplus/%2B'],

View File

@ -1,6 +1,6 @@
/* eslint-disable no-unused-vars */
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, createNTestNotes, createNTestFolders, createNTestTags } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, createNTestNotes, createNTestFolders, createNTestTags } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Tag = require('@joplin/lib/models/Tag.js');
@ -101,7 +101,7 @@ describe('reducer', function() {
});
// tests for NOTE_DELETE
it('should delete selected note', asyncTest(async () => {
it('should delete selected note', (async () => {
// create 1 folder
const folders = await createNTestFolders(1);
// create 5 notes
@ -122,7 +122,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected note at top', asyncTest(async () => {
it('should delete selected note at top', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1]);
@ -136,7 +136,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete last remaining note', asyncTest(async () => {
it('should delete last remaining note', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(1, folders[0]);
let state = initTestState(folders, 0, notes, [0]);
@ -150,7 +150,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected note at bottom', asyncTest(async () => {
it('should delete selected note at bottom', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [4]);
@ -164,7 +164,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a note below is selected', asyncTest(async () => {
it('should delete note when a note below is selected', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3]);
@ -178,7 +178,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a note above is selected', asyncTest(async () => {
it('should delete note when a note above is selected', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1]);
@ -192,7 +192,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete selected notes', asyncTest(async () => {
it('should delete selected notes', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1,2]);
@ -207,7 +207,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a notes below it are selected', asyncTest(async () => {
it('should delete note when a notes below it are selected', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3,4]);
@ -221,7 +221,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete note when a notes above it are selected', asyncTest(async () => {
it('should delete note when a notes above it are selected', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [1,2]);
@ -235,7 +235,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete notes at end', asyncTest(async () => {
it('should delete notes at end', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [3,4]);
@ -250,7 +250,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should delete notes when non-contiguous selection', asyncTest(async () => {
it('should delete notes when non-contiguous selection', (async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 0, notes, [0,2,4]);
@ -267,7 +267,7 @@ describe('reducer', function() {
}));
// tests for FOLDER_DELETE
it('should delete selected notebook', asyncTest(async () => {
it('should delete selected notebook', (async () => {
const folders = await createNTestFolders(5);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 2, notes, [2]);
@ -281,7 +281,7 @@ describe('reducer', function() {
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
}));
it('should delete notebook when a book above is selected', asyncTest(async () => {
it('should delete notebook when a book above is selected', (async () => {
const folders = await createNTestFolders(5);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 1, notes, [2]);
@ -295,7 +295,7 @@ describe('reducer', function() {
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
}));
it('should delete notebook when a book below is selected', asyncTest(async () => {
it('should delete notebook when a book below is selected', (async () => {
const folders = await createNTestFolders(5);
const notes = await createNTestNotes(5, folders[0]);
let state = initTestState(folders, 4, notes, [2]);
@ -310,7 +310,7 @@ describe('reducer', function() {
}));
// tests for TAG_DELETE
it('should delete selected tag', asyncTest(async () => {
it('should delete selected tag', (async () => {
const tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
@ -323,7 +323,7 @@ describe('reducer', function() {
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
it('should delete tag when a tag above is selected', asyncTest(async () => {
it('should delete tag when a tag above is selected', (async () => {
const tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
@ -336,7 +336,7 @@ describe('reducer', function() {
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
it('should delete tag when a tag below is selected', asyncTest(async () => {
it('should delete tag when a tag below is selected', (async () => {
const tags = await createNTestTags(5);
let state = initTestState(null, null, null, null, tags, [2]);
@ -349,7 +349,7 @@ describe('reducer', function() {
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
}));
it('should select all notes', asyncTest(async () => {
it('should select all notes', (async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
@ -372,7 +372,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
it('should remove deleted note from history', asyncTest(async () => {
it('should remove deleted note from history', (async () => {
// create 1 folder
const folders = await createNTestFolders(1);
@ -399,7 +399,7 @@ describe('reducer', function() {
expect(getIds(state.backwardHistoryNotes)).not.toContain(notes[2].id);
}));
it('should remove all notes of a deleted notebook from history', asyncTest(async () => {
it('should remove all notes of a deleted notebook from history', (async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
@ -421,7 +421,7 @@ describe('reducer', function() {
expect(getIds(state.backwardHistoryNotes)).toEqual([]);
}));
it('should maintain history correctly when going backward and forward', asyncTest(async () => {
it('should maintain history correctly when going backward and forward', (async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
@ -454,7 +454,7 @@ describe('reducer', function() {
expect(getIds(state.forwardHistoryNotes)).toEqual([]);
}));
it('should remember the last seen note of a notebook', asyncTest(async () => {
it('should remember the last seen note of a notebook', (async () => {
const folders = await createNTestFolders(2);
const notes = [];
for (let i = 0; i < folders.length; i++) {
@ -483,7 +483,7 @@ describe('reducer', function() {
}));
it('should ensure that history is free of adjacent duplicates', asyncTest(async () => {
it('should ensure that history is free of adjacent duplicates', (async () => {
// create 1 folder
const folders = await createNTestFolders(1);
// create 5 notes
@ -552,7 +552,7 @@ describe('reducer', function() {
expect(state.selectedNoteIds).toEqual([notes[3].id]);
}));
it('should ensure history max limit is maintained', asyncTest(async () => {
it('should ensure history max limit is maintained', (async () => {
const folders = await createNTestFolders(1);
// create 5 notes
const notes = await createNTestNotes(5, folders[0]);

View File

@ -1,6 +1,6 @@
import sandboxProxy, { Target } from '@joplin/lib/services/plugins/sandboxProxy';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('../../test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient } = require('../../test-utils.js');
describe('services_plugins_sandboxProxy', function() {
@ -10,7 +10,7 @@ describe('services_plugins_sandboxProxy', function() {
done();
});
it('should create a new sandbox proxy', asyncTest(async () => {
it('should create a new sandbox proxy', (async () => {
interface Result {
path: string;
args: any[];
@ -33,7 +33,7 @@ describe('services_plugins_sandboxProxy', function() {
expect(results[1].args.join('_')).toBe('');
}));
it('should allow importing a namespace', asyncTest(async () => {
it('should allow importing a namespace', (async () => {
interface Result {
path: string;
args: any[];

View File

@ -4,7 +4,7 @@ import CommandService, { CommandDeclaration, CommandRuntime } from '@joplin/lib/
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import KeymapService from '@joplin/lib/services/KeymapService';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
interface TestCommand {
declaration: CommandDeclaration;
@ -52,7 +52,7 @@ describe('services_CommandService', function() {
done();
});
it('should create toolbar button infos from commands', asyncTest(async () => {
it('should create toolbar button infos from commands', (async () => {
const service = newService();
const toolbarButtonUtils = new ToolbarButtonUtils(service);
@ -80,7 +80,7 @@ describe('services_CommandService', function() {
expect(toolbarInfos[1].enabled).toBe(true);
}));
it('should enable and disable toolbar buttons depending on state', asyncTest(async () => {
it('should enable and disable toolbar buttons depending on state', (async () => {
const service = newService();
const toolbarButtonUtils = new ToolbarButtonUtils(service);
@ -103,7 +103,7 @@ describe('services_CommandService', function() {
expect(toolbarInfos[1].enabled).toBe(true);
}));
it('should enable commands by default', asyncTest(async () => {
it('should enable commands by default', (async () => {
const service = newService();
registerCommand(service, createCommand('test1', {
@ -113,7 +113,7 @@ describe('services_CommandService', function() {
expect(service.isEnabled('test1', {})).toBe(true);
}));
it('should return the same toolbarButtons array if nothing has changed', asyncTest(async () => {
it('should return the same toolbarButtons array if nothing has changed', (async () => {
const service = newService();
const toolbarButtonUtils = new ToolbarButtonUtils(service);
@ -161,7 +161,7 @@ describe('services_CommandService', function() {
}
}));
it('should create menu items from commands', asyncTest(async () => {
it('should create menu items from commands', (async () => {
const service = newService();
const utils = new MenuUtils(service);
@ -190,7 +190,7 @@ describe('services_CommandService', function() {
expect(utils.commandsToMenuItems(['test1', 'test2'], onClick)).toBe(utils.commandsToMenuItems(['test1', 'test2'], onClick));
}));
it('should give menu item props from state', asyncTest(async () => {
it('should give menu item props from state', (async () => {
const service = newService();
const utils = new MenuUtils(service);
@ -228,7 +228,7 @@ describe('services_CommandService', function() {
.toBe(utils.commandsToMenuItemProps(['test1', 'test2'], { cond1: true, cond2: true }));
}));
it('should create stateful menu items', asyncTest(async () => {
it('should create stateful menu items', (async () => {
const service = newService();
const utils = new MenuUtils(service);
@ -246,7 +246,7 @@ describe('services_CommandService', function() {
expect(propValue).toBe('hello');
}));
it('should throw an error for invalid when clause keys in dev mode', asyncTest(async () => {
it('should throw an error for invalid when clause keys in dev mode', (async () => {
const service = newService();
registerCommand(service, createCommand('test1', {

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Tag = require('@joplin/lib/models/Tag.js');
@ -14,10 +14,6 @@ const MasterKey = require('@joplin/lib/models/MasterKey');
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry.js');
const EncryptionService = require('@joplin/lib/services/EncryptionService.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at services_EncryptionService: Promise', p, 'reason:', reason);
});
let service = null;
describe('services_EncryptionService', function() {
@ -31,7 +27,7 @@ describe('services_EncryptionService', function() {
done();
});
it('should encode and decode header', asyncTest(async () => {
it('should encode and decode header', (async () => {
const header = {
encryptionMethod: EncryptionService.METHOD_SJCL,
masterKeyId: '01234568abcdefgh01234568abcdefgh',
@ -44,7 +40,7 @@ describe('services_EncryptionService', function() {
expect(objectsEqual(header, decodedHeader)).toBe(true);
}));
it('should generate and decrypt a master key', asyncTest(async () => {
it('should generate and decrypt a master key', (async () => {
const masterKey = await service.generateMasterKey('123456');
expect(!!masterKey.content).toBe(true);
@ -61,7 +57,7 @@ describe('services_EncryptionService', function() {
expect(decryptedMasterKey.length).toBe(512);
}));
it('should upgrade a master key', asyncTest(async () => {
it('should upgrade a master key', (async () => {
// Create an old style master key
let masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
@ -90,7 +86,7 @@ describe('services_EncryptionService', function() {
expect(plainTextFromOld).toBe(plainTextFromNew);
}));
it('should not upgrade master key if invalid password', asyncTest(async () => {
it('should not upgrade master key if invalid password', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
@ -98,7 +94,7 @@ describe('services_EncryptionService', function() {
const hasThrown = await checkThrowAsync(async () => await service.upgradeMasterKey(masterKey, '777'));
}));
it('should require a checksum only for old master keys', asyncTest(async () => {
it('should require a checksum only for old master keys', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
});
@ -107,7 +103,7 @@ describe('services_EncryptionService', function() {
expect(!!masterKey.content).toBe(true);
}));
it('should not require a checksum for new master keys', asyncTest(async () => {
it('should not require a checksum for new master keys', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_4,
});
@ -119,7 +115,7 @@ describe('services_EncryptionService', function() {
expect(decryptedMasterKey.length).toBe(512);
}));
it('should throw an error if master key decryption fails', asyncTest(async () => {
it('should throw an error if master key decryption fails', (async () => {
const masterKey = await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_4,
});
@ -129,7 +125,7 @@ describe('services_EncryptionService', function() {
expect(hasThrown).toBe(true);
}));
it('should return the master keys that need an upgrade', asyncTest(async () => {
it('should return the master keys that need an upgrade', (async () => {
const masterKey1 = await MasterKey.save(await service.generateMasterKey('123456', {
encryptionMethod: EncryptionService.METHOD_SJCL_2,
}));
@ -146,7 +142,7 @@ describe('services_EncryptionService', function() {
expect(needUpgrade.map(k => k.id).sort()).toEqual([masterKey1.id, masterKey2.id].sort());
}));
it('should encrypt and decrypt with a master key', asyncTest(async () => {
it('should encrypt and decrypt with a master key', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
@ -168,7 +164,7 @@ describe('services_EncryptionService', function() {
expect(plainText2 === veryLongSecret).toBe(true);
}));
it('should decrypt various encryption methods', asyncTest(async () => {
it('should decrypt various encryption methods', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey_(masterKey, '123456', true);
@ -194,7 +190,7 @@ describe('services_EncryptionService', function() {
}
}));
it('should fail to decrypt if master key not present', asyncTest(async () => {
it('should fail to decrypt if master key not present', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
@ -210,7 +206,7 @@ describe('services_EncryptionService', function() {
}));
it('should fail to decrypt if data tampered with', asyncTest(async () => {
it('should fail to decrypt if data tampered with', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
@ -224,7 +220,7 @@ describe('services_EncryptionService', function() {
expect(hasThrown).toBe(true);
}));
it('should encrypt and decrypt notes and folders', asyncTest(async () => {
it('should encrypt and decrypt notes and folders', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey_(masterKey, '123456', true);
@ -255,7 +251,7 @@ describe('services_EncryptionService', function() {
expect(decryptedNote.parent_id).toBe(note.parent_id);
}));
it('should encrypt and decrypt files', asyncTest(async () => {
it('should encrypt and decrypt files', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);
await service.loadMasterKey_(masterKey, '123456', true);
@ -271,7 +267,7 @@ describe('services_EncryptionService', function() {
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
}));
it('should encrypt invalid UTF-8 data', asyncTest(async () => {
it('should encrypt invalid UTF-8 data', (async () => {
let masterKey = await service.generateMasterKey('123456');
masterKey = await MasterKey.save(masterKey);

View File

@ -3,7 +3,7 @@ import { CustomExportContext, CustomImportContext, Module, ModuleType } from '@j
import shim from '@joplin/lib/shim';
const { asyncTest, fileContentEqual, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Tag = require('@joplin/lib/models/Tag.js');
@ -11,10 +11,6 @@ const Resource = require('@joplin/lib/models/Resource.js');
const fs = require('fs-extra');
const ArrayUtils = require('@joplin/lib/ArrayUtils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at services_InteropService: Promise', p, 'reason:', reason);
});
function exportDir() {
return `${__dirname}/export`;
}
@ -41,7 +37,7 @@ describe('services_InteropService', function() {
done();
});
it('should export and import folders', asyncTest(async () => {
it('should export and import folders', (async () => {
const service = InteropService.instance();
let folder1 = await Folder.save({ title: 'folder1' });
folder1 = await Folder.load(folder1.id);
@ -76,7 +72,7 @@ describe('services_InteropService', function() {
fieldsEqual(folder3, folder1, fieldNames);
}));
it('should import folders and de-duplicate titles when needed', asyncTest(async () => {
it('should import folders and de-duplicate titles when needed', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder' });
const folder2 = await Folder.save({ title: 'folder' });
@ -92,7 +88,7 @@ describe('services_InteropService', function() {
expect(allFolders.map((f: any) => f.title).sort().join(' - ')).toBe('folder - folder (1)');
}));
it('should import folders, and only de-duplicate titles when needed', asyncTest(async () => {
it('should import folders, and only de-duplicate titles when needed', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'folder2' });
@ -115,7 +111,7 @@ describe('services_InteropService', function() {
expect(importedSub2.title).toBe('Sub');
}));
it('should export and import folders and notes', asyncTest(async () => {
it('should export and import folders and notes', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
@ -154,7 +150,7 @@ describe('services_InteropService', function() {
fieldsEqual(note2, note3, fieldNames);
}));
it('should export and import notes to specific folder', asyncTest(async () => {
it('should export and import notes to specific folder', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
@ -173,7 +169,7 @@ describe('services_InteropService', function() {
expect(await checkThrowAsync(async () => await service.import({ path: filePath, destinationFolderId: 'oops' }))).toBe(true);
}));
it('should export and import tags', asyncTest(async () => {
it('should export and import tags', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
const folder1 = await Folder.save({ title: 'folder1' });
@ -213,7 +209,7 @@ describe('services_InteropService', function() {
expect(noteIds.length).toBe(2);
}));
it('should export and import resources', asyncTest(async () => {
it('should export and import resources', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
const folder1 = await Folder.save({ title: 'folder1' });
@ -249,7 +245,7 @@ describe('services_InteropService', function() {
expect(fileContentEqual(resourcePath1, resourcePath2)).toBe(true);
}));
it('should export and import single notes', asyncTest(async () => {
it('should export and import single notes', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
const folder1 = await Folder.save({ title: 'folder1' });
@ -269,7 +265,7 @@ describe('services_InteropService', function() {
expect(folder2.title).toBe('test');
}));
it('should export and import single folders', asyncTest(async () => {
it('should export and import single folders', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
const folder1 = await Folder.save({ title: 'folder1' });
@ -289,7 +285,7 @@ describe('services_InteropService', function() {
expect(folder2.title).toBe('folder1');
}));
it('should export and import folder and its sub-folders', asyncTest(async () => {
it('should export and import folder and its sub-folders', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
@ -324,7 +320,7 @@ describe('services_InteropService', function() {
expect(note1_2.parent_id).toBe(folder4_2.id);
}));
it('should export and import links to notes', asyncTest(async () => {
it('should export and import links to notes', (async () => {
const service = InteropService.instance();
const filePath = `${exportDir()}/test.jex`;
const folder1 = await Folder.save({ title: 'folder1' });
@ -348,7 +344,7 @@ describe('services_InteropService', function() {
expect(note2_2.body.indexOf(note1_2.id) >= 0).toBe(true);
}));
it('should export selected notes in md format', asyncTest(async () => {
it('should export selected notes in md format', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder1' });
let note11 = await Note.save({ title: 'title note11', parent_id: folder1.id });
@ -377,7 +373,7 @@ describe('services_InteropService', function() {
expect(await shim.fsDriver().exists(`${outDir}/folder3`)).toBe(false);
}));
it('should export MD with unicode filenames', asyncTest(async () => {
it('should export MD with unicode filenames', (async () => {
const service = InteropService.instance();
const folder1 = await Folder.save({ title: 'folder1' });
const folder2 = await Folder.save({ title: 'ジョプリン' });
@ -402,7 +398,7 @@ describe('services_InteropService', function() {
expect(await shim.fsDriver().exists(`${outDir}/ジョプリン/ジョプリン.md`)).toBe(true);
}));
it('should export a notebook as MD', asyncTest(async () => {
it('should export a notebook as MD', (async () => {
const folder1 = await Folder.save({ title: 'testexportfolder' });
await Note.save({ title: 'textexportnote1', parent_id: folder1.id });
await Note.save({ title: 'textexportnote2', parent_id: folder1.id });
@ -419,7 +415,7 @@ describe('services_InteropService', function() {
expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true);
}));
it('should export conflict notes', asyncTest(async () => {
it('should export conflict notes', (async () => {
const folder1 = await Folder.save({ title: 'testexportfolder' });
await Note.save({ title: 'textexportnote1', parent_id: folder1.id, is_conflict: 1 });
await Note.save({ title: 'textexportnote2', parent_id: folder1.id });
@ -449,7 +445,7 @@ describe('services_InteropService', function() {
expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true);
}));
it('should not try to export folders with a non-existing parent', asyncTest(async () => {
it('should not try to export folders with a non-existing parent', (async () => {
// Handles and edge case where user has a folder but this folder with a parent
// that doesn't exist. Can happen for example in this case:
//
@ -471,7 +467,7 @@ describe('services_InteropService', function() {
expect(result.warnings.length).toBe(0);
}));
it('should allow registering new import modules', asyncTest(async () => {
it('should allow registering new import modules', (async () => {
const testImportFilePath = `${exportDir()}/testImport${Math.random()}.test`;
await shim.fsDriver().writeFile(testImportFilePath, 'test', 'utf8');
@ -504,7 +500,7 @@ describe('services_InteropService', function() {
expect(result.sourcePath).toBe(testImportFilePath);
}));
it('should allow registering new export modules', asyncTest(async () => {
it('should allow registering new export modules', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
await Note.save({ title: 'note2', parent_id: folder1.id });

View File

@ -2,7 +2,7 @@
const fs = require('fs-extra');
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const InteropService_Exporter_Md = require('@joplin/lib/services/interop/InteropService_Exporter_Md').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const Folder = require('@joplin/lib/models/Folder.js');
@ -12,10 +12,6 @@ const shim = require('@joplin/lib/shim').default;
const exportDir = `${__dirname}/export`;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('services_InteropService_Exporter_Md', function() {
beforeEach(async (done) => {
@ -27,14 +23,14 @@ describe('services_InteropService_Exporter_Md', function() {
done();
});
it('should create resources directory', asyncTest(async () => {
it('should create resources directory', (async () => {
const service = new InteropService_Exporter_Md();
await service.init(exportDir);
expect(await shim.fsDriver().exists(`${exportDir}/_resources/`)).toBe(true);
}));
it('should create note paths and add them to context', asyncTest(async () => {
it('should create note paths and add them to context', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -76,7 +72,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
}));
it('should handle duplicate note names', asyncTest(async () => {
it('should handle duplicate note names', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -103,7 +99,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
}));
it('should not override existing files', asyncTest(async () => {
it('should not override existing files', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -130,7 +126,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
}));
it('should save resource files in _resource directory', asyncTest(async () => {
it('should save resource files in _resource directory', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -167,7 +163,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
}));
it('should create folders in fs', asyncTest(async () => {
it('should create folders in fs', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -198,7 +194,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(await shim.fsDriver().exists(`${exportDir}/folder1/folder3`)).toBe(true, 'Folder should be created in filesystem.');
}));
it('should save notes in fs', asyncTest(async () => {
it('should save notes in fs', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -235,7 +231,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
}));
it('should replace resource ids with relative paths', asyncTest(async () => {
it('should replace resource ids with relative paths', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -280,7 +276,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
}));
it('should replace note ids with relative paths', asyncTest(async () => {
it('should replace note ids with relative paths', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);
@ -332,7 +328,7 @@ describe('services_InteropService_Exporter_Md', function() {
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
}));
it('should url encode relative note links', asyncTest(async () => {
it('should url encode relative note links', (async () => {
const exporter = new InteropService_Exporter_Md();
await exporter.init(exportDir);

View File

@ -1,13 +1,9 @@
/* eslint-disable no-unused-vars */
const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const KvStore = require('@joplin/lib/services/KvStore').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function setupStore() {
const store = KvStore.instance();
store.setDb(db());
@ -22,7 +18,7 @@ describe('services_KvStore', function() {
done();
});
it('should set and get values', asyncTest(async () => {
it('should set and get values', (async () => {
const store = setupStore();
await store.setValue('a', 123);
expect(await store.value('a')).toBe(123);
@ -41,7 +37,7 @@ describe('services_KvStore', function() {
expect(await store.value('b')).toBe(789);
}));
it('should set and get values with the right type', asyncTest(async () => {
it('should set and get values with the right type', (async () => {
const store = setupStore();
await store.setValue('string', 'something');
await store.setValue('int', 123);
@ -49,7 +45,7 @@ describe('services_KvStore', function() {
expect(await store.value('int')).toBe(123);
}));
it('should increment values', asyncTest(async () => {
it('should increment values', (async () => {
const store = setupStore();
await store.setValue('int', 1);
const newValue = await store.incValue('int');
@ -61,12 +57,12 @@ describe('services_KvStore', function() {
expect(await store.countKeys()).toBe(2);
}));
it('should handle non-existent values', asyncTest(async () => {
it('should handle non-existent values', (async () => {
const store = setupStore();
expect(await store.value('nope')).toBe(null);
}));
it('should delete values', asyncTest(async () => {
it('should delete values', (async () => {
const store = setupStore();
await store.setValue('int', 1);
expect(await store.countKeys()).toBe(1);
@ -76,7 +72,7 @@ describe('services_KvStore', function() {
await store.deleteValue('int'); // That should not throw
}));
it('should increment in an atomic way', asyncTest(async () => {
it('should increment in an atomic way', (async () => {
const store = setupStore();
await store.setValue('int', 0);
@ -90,7 +86,7 @@ describe('services_KvStore', function() {
expect(await store.value('int')).toBe(20);
}));
it('should search by prefix', asyncTest(async () => {
it('should search by prefix', (async () => {
const store = setupStore();
await store.setValue('testing:1', 1);
await store.setValue('testing:2', 2);

View File

@ -6,14 +6,10 @@ import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
const fs = require('fs-extra');
const { asyncTest, expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const { expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at services_PluginService: Promise', p, 'reason:', reason);
});
const testPluginDir = `${__dirname}/../tests/support/plugins`;
function newPluginService(appVersion: string = '1.4') {
@ -41,7 +37,7 @@ describe('services_PluginService', function() {
done();
});
it('should load and run a simple plugin', asyncTest(async () => {
it('should load and run a simple plugin', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
@ -57,13 +53,13 @@ describe('services_PluginService', function() {
expect(allNotes[0].parent_id).toBe(allFolders[0].id);
}));
it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => {
it('should load and run a simple plugin and handle trailing slash', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
}));
it('should load and run a plugin that uses external packages', asyncTest(async () => {
it('should load and run a plugin that uses external packages', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
@ -76,7 +72,7 @@ describe('services_PluginService', function() {
expect(allFolders[0].title).toBe(' foo');
}));
it('should load multiple plugins from a directory', asyncTest(async () => {
it('should load multiple plugins from a directory', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
@ -90,7 +86,7 @@ describe('services_PluginService', function() {
expect(allFolders.map((f: any) => f.title).sort().join(', ')).toBe('multi - simple1, multi - simple2');
}));
it('should load plugins from JS bundles', asyncTest(async () => {
it('should load plugins from JS bundles', (async () => {
const service = newPluginService();
const plugin = await service.loadPluginFromJsBundle('/tmp', `
@ -123,21 +119,21 @@ describe('services_PluginService', function() {
expect(allFolders.length).toBe(1);
}));
it('should load plugins from JS bundle files', asyncTest(async () => {
it('should load plugins from JS bundle files', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should load plugins from JPL archive', asyncTest(async () => {
it('should load plugins from JPL archive', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should validate JS bundles', asyncTest(async () => {
it('should validate JS bundles', (async () => {
const invalidJsBundles = [
`
/* joplin-manifest:
@ -170,7 +166,7 @@ describe('services_PluginService', function() {
}
}));
it('should register a Markdown-it plugin', asyncTest(async () => {
it('should register a Markdown-it plugin', (async () => {
const tempDir = await createTempDir();
const contentScriptPath = `${tempDir}/markdownItTestPlugin.js`;
@ -222,7 +218,7 @@ describe('services_PluginService', function() {
await shim.fsDriver().remove(tempDir);
}));
it('should enable and disable plugins depending on what app version they support', asyncTest(async () => {
it('should enable and disable plugins depending on what app version they support', (async () => {
const pluginScript = `
/* joplin-manifest:
{
@ -260,7 +256,7 @@ describe('services_PluginService', function() {
}
}));
it('should install a plugin', asyncTest(async () => {
it('should install a plugin', (async () => {
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;
await service.installPlugin(pluginPath);
@ -268,7 +264,7 @@ describe('services_PluginService', function() {
expect(await fs.existsSync(installedPluginPath)).toBe(true);
}));
it('should rename the plugin archive to the right name', asyncTest(async () => {
it('should rename the plugin archive to the right name', (async () => {
const tempDir = await createTempDir();
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;

View File

@ -3,7 +3,7 @@ import NoteResource from '@joplin/lib/models/NoteResource';
import ResourceService from '@joplin/lib/services/ResourceService';
import shim from '@joplin/lib/shim';
const { asyncTest, resourceService, decryptionWorker, encryptionService, loadEncryptionMasterKey, allSyncTargetItemsEncrypted, setupDatabaseAndSynchronizer, db, synchronizer, switchClient } = require('./test-utils.js');
const { resourceService, decryptionWorker, encryptionService, loadEncryptionMasterKey, allSyncTargetItemsEncrypted, setupDatabaseAndSynchronizer, db, synchronizer, switchClient } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const Resource = require('@joplin/lib/models/Resource.js');
@ -18,7 +18,7 @@ describe('services_ResourceService', function() {
done();
});
it('should delete orphaned resources', asyncTest(async () => {
it('should delete orphaned resources', (async () => {
const service = new ResourceService();
const folder1 = await Folder.save({ title: 'folder1' });
@ -49,7 +49,7 @@ describe('services_ResourceService', function() {
expect(!(await NoteResource.all()).length).toBe(true);
}));
it('should not delete resource if still associated with at least one note', asyncTest(async () => {
it('should not delete resource if still associated with at least one note', (async () => {
const service = new ResourceService();
const folder1 = await Folder.save({ title: 'folder1' });
@ -73,7 +73,7 @@ describe('services_ResourceService', function() {
expect(!!(await Resource.load(resource1.id))).toBe(true);
}));
it('should not delete a resource that has never been associated with any note, because it probably means the resource came via sync, and associated note has not arrived yet', asyncTest(async () => {
it('should not delete a resource that has never been associated with any note, because it probably means the resource came via sync, and associated note has not arrived yet', (async () => {
const service = new ResourceService();
await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);
@ -83,7 +83,7 @@ describe('services_ResourceService', function() {
expect((await Resource.all()).length).toBe(1);
}));
it('should not delete resource if it is used in an IMG tag', asyncTest(async () => {
it('should not delete resource if it is used in an IMG tag', (async () => {
const service = new ResourceService();
const folder1 = await Folder.save({ title: 'folder1' });
@ -102,7 +102,7 @@ describe('services_ResourceService', function() {
expect(!!(await Resource.load(resource1.id))).toBe(true);
}));
it('should not process twice the same change', asyncTest(async () => {
it('should not process twice the same change', (async () => {
const service = new ResourceService();
const folder1 = await Folder.save({ title: 'folder1' });
@ -122,7 +122,7 @@ describe('services_ResourceService', function() {
expect(before.last_seen_time).toBe(after.last_seen_time);
}));
it('should not delete resources that are associated with an encrypted note', asyncTest(async () => {
it('should not delete resources that are associated with an encrypted note', (async () => {
// https://github.com/laurent22/joplin/issues/1433
//
// Client 1 and client 2 have E2EE setup.
@ -168,7 +168,7 @@ describe('services_ResourceService', function() {
expect((await Resource.all()).length).toBe(2);
}));
it('should double-check if the resource is still linked before deleting it', asyncTest(async () => {
it('should double-check if the resource is still linked before deleting it', (async () => {
SearchEngine.instance().setDb(db()); // /!\ Note that we use the global search engine here, which we shouldn't but will work for now
const folder1 = await Folder.save({ title: 'folder1' });
@ -187,7 +187,7 @@ describe('services_ResourceService', function() {
expect(!!nr.is_associated).toBe(true); // And it should have fixed the situation by re-indexing the note content
}));
// it('should auto-delete resource even if the associated note was deleted immediately', asyncTest(async () => {
// it('should auto-delete resource even if the associated note was deleted immediately', (async () => {
// // Previoulsy, when a resource was be attached to a note, then the
// // note was immediately deleted, the ResourceService would not have
// // time to quick in an index the resource/note relation. It means

View File

@ -2,7 +2,7 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Note = require('@joplin/lib/models/Note.js');
@ -14,10 +14,6 @@ const BaseModel = require('@joplin/lib/BaseModel').default;
const RevisionService = require('@joplin/lib/services/RevisionService.js');
const shim = require('@joplin/lib/shim').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at services_Revision: Promise', p, 'reason:', reason);
});
describe('services_Revision', function() {
beforeEach(async (done) => {
@ -27,7 +23,7 @@ describe('services_Revision', function() {
done();
});
it('should create diff and rebuild notes', asyncTest(async () => {
it('should create diff and rebuild notes', (async () => {
const service = new RevisionService();
const n1_v1 = await Note.save({ title: '', author: 'testing' });
@ -58,7 +54,7 @@ describe('services_Revision', function() {
expect(revisions2.length).toBe(0);
}));
it('should delete old revisions (1 note, 2 rev)', asyncTest(async () => {
it('should delete old revisions (1 note, 2 rev)', (async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
@ -81,7 +77,7 @@ describe('services_Revision', function() {
expect(rev1.title).toBe('hello welcome');
}));
it('should delete old revisions (1 note, 3 rev)', asyncTest(async () => {
it('should delete old revisions (1 note, 3 rev)', (async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
@ -122,7 +118,7 @@ describe('services_Revision', function() {
}
}));
it('should delete old revisions (2 notes, 2 rev)', asyncTest(async () => {
it('should delete old revisions (2 notes, 2 rev)', (async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
@ -157,7 +153,7 @@ describe('services_Revision', function() {
}
}));
it('should handle conflicts', asyncTest(async () => {
it('should handle conflicts', (async () => {
const service = new RevisionService();
// A conflict happens in this case:
@ -193,7 +189,7 @@ describe('services_Revision', function() {
expect(revNote3.title).toBe('hello John');
}));
it('should create a revision for notes that are older than a given interval', asyncTest(async () => {
it('should create a revision for notes that are older than a given interval', (async () => {
const n1 = await Note.save({ title: 'hello' });
const noteId = n1.id;
@ -229,7 +225,7 @@ describe('services_Revision', function() {
}
}));
it('should create a revision for notes that get deleted (recyle bin)', asyncTest(async () => {
it('should create a revision for notes that get deleted (recyle bin)', (async () => {
const n1 = await Note.save({ title: 'hello' });
const noteId = n1.id;
@ -243,7 +239,7 @@ describe('services_Revision', function() {
expect(rev1.title).toBe('hello');
}));
it('should not create a revision for notes that get deleted if there is already a revision', asyncTest(async () => {
it('should not create a revision for notes that get deleted if there is already a revision', (async () => {
const n1 = await Note.save({ title: 'hello' });
await revisionService().collectRevisions();
const noteId = n1.id;
@ -261,7 +257,7 @@ describe('services_Revision', function() {
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
}));
it('should not create a revision for new note the first time they are saved', asyncTest(async () => {
it('should not create a revision for new note the first time they are saved', (async () => {
const n1 = await Note.save({ title: 'hello' });
{
@ -277,7 +273,7 @@ describe('services_Revision', function() {
}
}));
it('should abort collecting revisions when one of them is encrypted', asyncTest(async () => {
it('should abort collecting revisions when one of them is encrypted', (async () => {
const n1 = await Note.save({ title: 'hello' }); // CHANGE 1
await revisionService().collectRevisions();
await Note.save({ id: n1.id, title: 'hello Ringo' }); // CHANGE 2
@ -311,7 +307,7 @@ describe('services_Revision', function() {
expect(Setting.value('revisionService.lastProcessedChangeId')).toBe(4);
}));
it('should not delete old revisions if one of them is still encrypted (1)', asyncTest(async () => {
it('should not delete old revisions if one of them is still encrypted (1)', (async () => {
// Test case 1: Two revisions and the first one is encrypted.
// Calling deleteOldRevisions() with low TTL, which means all revisions
// should be deleted, but they won't be due to the encrypted one.
@ -338,7 +334,7 @@ describe('services_Revision', function() {
expect((await Revision.all()).length).toBe(0);
}));
it('should not delete old revisions if one of them is still encrypted (2)', asyncTest(async () => {
it('should not delete old revisions if one of them is still encrypted (2)', (async () => {
// Test case 2: Two revisions and the first one is encrypted.
// Calling deleteOldRevisions() with higher TTL, which means the oldest
// revision should be deleted, but it won't be due to the encrypted one.
@ -362,7 +358,7 @@ describe('services_Revision', function() {
expect((await Revision.all()).length).toBe(2);
}));
it('should not delete old revisions if one of them is still encrypted (3)', asyncTest(async () => {
it('should not delete old revisions if one of them is still encrypted (3)', (async () => {
// Test case 2: Two revisions and the second one is encrypted.
// Calling deleteOldRevisions() with higher TTL, which means the oldest
// revision should be deleted, but it won't be due to the encrypted one.
@ -392,7 +388,7 @@ describe('services_Revision', function() {
expect((await Revision.all()).length).toBe(1);
}));
it('should not create a revision if the note has not changed', asyncTest(async () => {
it('should not create a revision if the note has not changed', (async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
@ -403,7 +399,7 @@ describe('services_Revision', function() {
expect((await Revision.all()).length).toBe(1);
}));
it('should preserve user update time', asyncTest(async () => {
it('should preserve user update time', (async () => {
// user_updated_time is kind of tricky and can be changed automatically in various
// places so make sure it is saved correctly with the revision
@ -423,7 +419,7 @@ describe('services_Revision', function() {
expect(revNote.user_updated_time).toBe(userUpdatedTime);
}));
it('should not create a revision if there is already a recent one', asyncTest(async () => {
it('should not create a revision if there is already a recent one', (async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1

View File

@ -3,16 +3,12 @@
const time = require('@joplin/lib/time').default;
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, restoreDate } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, restoreDate } = require('./test-utils.js');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
const Note = require('@joplin/lib/models/Note');
const ItemChange = require('@joplin/lib/models/ItemChange');
const Setting = require('@joplin/lib/models/Setting').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
let engine = null;
@ -76,7 +72,7 @@ describe('services_SearchEngine', function() {
done();
});
it('should keep the content and FTS table in sync', asyncTest(async () => {
it('should keep the content and FTS table in sync', (async () => {
let rows, n1, n2, n3;
n1 = await Note.save({ title: 'a' });
@ -111,7 +107,7 @@ describe('services_SearchEngine', function() {
expect(rows.length).toBe(1);
}));
it('should, after initial indexing, save the last change ID', asyncTest(async () => {
it('should, after initial indexing, save the last change ID', (async () => {
const n1 = await Note.save({ title: 'abcd efgh' }); // 3
const n2 = await Note.save({ title: 'abcd aaaaa abcd abcd' }); // 1
@ -127,7 +123,7 @@ describe('services_SearchEngine', function() {
}));
it('should order search results by relevance BM25', asyncTest(async () => {
it('should order search results by relevance BM25', (async () => {
// BM25 is based on term frequency - inverse document frequency
// The tf–idf value increases proportionally to the number of times a word appears in the document
// and is offset by the number of documents in the corpus that contain the word, which helps to adjust
@ -160,7 +156,7 @@ describe('services_SearchEngine', function() {
// TODO: Need to update and replace jasmine.mockDate() calls with Jest
// equivalent
// it('should correctly weigh notes using BM25 and user_updated_time', asyncTest(async () => {
// it('should correctly weigh notes using BM25 and user_updated_time', (async () => {
// await mockDate(2020, 9, 30, 50);
// const noteData = [
// {
@ -240,7 +236,7 @@ describe('services_SearchEngine', function() {
// await restoreDate();
// }));
it('should tell where the results are found', asyncTest(async () => {
it('should tell where the results are found', (async () => {
const notes = [
await Note.save({ title: 'abcd efgh', body: 'abcd' }),
await Note.save({ title: 'abcd' }),
@ -266,7 +262,7 @@ describe('services_SearchEngine', function() {
}
}));
it('should order search results by relevance (last updated first)', asyncTest(async () => {
it('should order search results by relevance (last updated first)', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd' });
@ -292,7 +288,7 @@ describe('services_SearchEngine', function() {
expect(rows[2].id).toBe(n2.id);
}));
it('should order search results by relevance (completed to-dos last)', asyncTest(async () => {
it('should order search results by relevance (completed to-dos last)', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', is_todo: 1 });
@ -318,7 +314,7 @@ describe('services_SearchEngine', function() {
expect(rows[2].id).toBe(n3.id);
}));
it('should supports various query types', asyncTest(async () => {
it('should supports various query types', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd efgh ijkl', body: 'aaaa bbbb' });
@ -369,7 +365,7 @@ describe('services_SearchEngine', function() {
expect(rows.length).toBe(1);
}));
it('should support queries with or without accents', asyncTest(async () => {
it('should support queries with or without accents', (async () => {
let rows;
const n1 = await Note.save({ title: 'père noël' });
@ -381,7 +377,7 @@ describe('services_SearchEngine', function() {
expect((await engine.search('noë*')).length).toBe(1);
}));
it('should support queries with Chinese characters', asyncTest(async () => {
it('should support queries with Chinese characters', (async () => {
let rows;
const n1 = await Note.save({ title: '我是法国人', body: '中文测试' });
@ -395,7 +391,7 @@ describe('services_SearchEngine', function() {
expect((await engine.search('测试*'))[0].fields).toEqual(['body']);
}));
it('should support queries with Japanese characters', asyncTest(async () => {
it('should support queries with Japanese characters', (async () => {
let rows;
const n1 = await Note.save({ title: '私は日本語を話すことができません', body: 'テスト' });
@ -408,7 +404,7 @@ describe('services_SearchEngine', function() {
}));
it('should support queries with Korean characters', asyncTest(async () => {
it('should support queries with Korean characters', (async () => {
let rows;
const n1 = await Note.save({ title: '이것은 한국말이다' });
@ -418,7 +414,7 @@ describe('services_SearchEngine', function() {
expect((await engine.search('말')).length).toBe(1);
}));
it('should support queries with Thai characters', asyncTest(async () => {
it('should support queries with Thai characters', (async () => {
let rows;
const n1 = await Note.save({ title: 'นี่คือคนไทย' });
@ -428,7 +424,7 @@ describe('services_SearchEngine', function() {
expect((await engine.search('ไทย')).length).toBe(1);
}));
it('should support field restricted queries with Chinese characters', asyncTest(async () => {
it('should support field restricted queries with Chinese characters', (async () => {
let rows;
const n1 = await Note.save({ title: '你好', body: '我是法国人' });
@ -450,7 +446,7 @@ describe('services_SearchEngine', function() {
// expect((await engine.search('title:你好 title:hello')).length).toBe(1);
}));
it('should parse normal query strings', asyncTest(async () => {
it('should parse normal query strings', (async () => {
let rows;
const testCases = [
@ -478,7 +474,7 @@ describe('services_SearchEngine', function() {
}
}));
it('should handle queries with special characters', asyncTest(async () => {
it('should handle queries with special characters', (async () => {
let rows;
const testCases = [
@ -505,7 +501,7 @@ describe('services_SearchEngine', function() {
}
}));
it('should allow using basic search', asyncTest(async () => {
it('should allow using basic search', (async () => {
const n1 = await Note.save({ title: '- [ ] abcd' });
const n2 = await Note.save({ title: '[ ] abcd' });

View File

@ -3,7 +3,7 @@
const time = require('@joplin/lib/time').default;
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('./test-utils.js');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
@ -15,10 +15,6 @@ const shim = require('@joplin/lib/shim').default;
const ResourceService = require('@joplin/lib/services/ResourceService').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at services_SearchFilter: Promise', p, 'reason:', reason);
});
let engine = null;
const ids = (array) => array.map(a => a.id);
@ -35,7 +31,7 @@ describe('services_SearchFilter', function() {
});
it('should return note matching title', asyncTest(async () => {
it('should return note matching title', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'body 1' });
const n2 = await Note.save({ title: 'efgh', body: 'body 2' });
@ -47,7 +43,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n1.id);
}));
it('should return note matching negated title', asyncTest(async () => {
it('should return note matching negated title', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'body 1' });
const n2 = await Note.save({ title: 'efgh', body: 'body 2' });
@ -59,7 +55,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n2.id);
}));
it('should return note matching body', asyncTest(async () => {
it('should return note matching body', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'body1' });
const n2 = await Note.save({ title: 'efgh', body: 'body2' });
@ -71,7 +67,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n1.id);
}));
it('should return note matching negated body', asyncTest(async () => {
it('should return note matching negated body', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'body1' });
const n2 = await Note.save({ title: 'efgh', body: 'body2' });
@ -83,7 +79,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n2.id);
}));
it('should return note matching title containing multiple words', asyncTest(async () => {
it('should return note matching title containing multiple words', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd xyz', body: 'body1' });
const n2 = await Note.save({ title: 'efgh ijk', body: 'body2' });
@ -95,7 +91,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n1.id);
}));
it('should return note matching body containing multiple words', asyncTest(async () => {
it('should return note matching body containing multiple words', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'ho ho ho' });
const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });
@ -107,7 +103,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n2.id);
}));
it('should return note matching title AND body', asyncTest(async () => {
it('should return note matching title AND body', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'ho ho ho' });
const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });
@ -121,7 +117,7 @@ describe('services_SearchFilter', function() {
expect(rows.length).toBe(0);
}));
it('should return note matching title OR body', asyncTest(async () => {
it('should return note matching title OR body', (async () => {
let rows;
const n1 = await Note.save({ title: 'abcd', body: 'ho ho ho' });
const n2 = await Note.save({ title: 'efgh', body: 'foo bar' });
@ -136,7 +132,7 @@ describe('services_SearchFilter', function() {
expect(rows.length).toBe(0);
}));
it('should return notes matching text', asyncTest(async () => {
it('should return notes matching text', (async () => {
let rows;
const n1 = await Note.save({ title: 'foo beef', body: 'dead bar' });
const n2 = await Note.save({ title: 'bar efgh', body: 'foo dog' });
@ -162,7 +158,7 @@ describe('services_SearchFilter', function() {
expect(rows.length).toBe(0);
}));
it('should return notes matching any negated text', asyncTest(async () => {
it('should return notes matching any negated text', (async () => {
let rows;
const n1 = await Note.save({ title: 'abc', body: 'def' });
const n2 = await Note.save({ title: 'def', body: 'ghi' });
@ -176,7 +172,7 @@ describe('services_SearchFilter', function() {
expect(rows.map(r=>r.id)).toContain(n3.id);
}));
it('should return notes matching any negated title', asyncTest(async () => {
it('should return notes matching any negated title', (async () => {
let rows;
const n1 = await Note.save({ title: 'abc', body: 'def' });
const n2 = await Note.save({ title: 'def', body: 'ghi' });
@ -190,7 +186,7 @@ describe('services_SearchFilter', function() {
expect(rows.map(r=>r.id)).toContain(n3.id);
}));
it('should return notes matching any negated body', asyncTest(async () => {
it('should return notes matching any negated body', (async () => {
let rows;
const n1 = await Note.save({ title: 'abc', body: 'def' });
const n2 = await Note.save({ title: 'def', body: 'ghi' });
@ -204,7 +200,7 @@ describe('services_SearchFilter', function() {
expect(rows.map(r=>r.id)).toContain(n3.id);
}));
it('should support phrase search', asyncTest(async () => {
it('should support phrase search', (async () => {
let rows;
const n1 = await Note.save({ title: 'foo beef', body: 'bar dog' });
const n2 = await Note.save({ title: 'bar efgh', body: 'foo dog' });
@ -215,7 +211,7 @@ describe('services_SearchFilter', function() {
expect(rows[0].id).toBe(n1.id);
}));
it('should support prefix search', asyncTest(async () => {
it('should support prefix search', (async () => {
let rows;
const n1 = await Note.save({ title: 'foo beef', body: 'bar dog' });
const n2 = await Note.save({ title: 'bar efgh', body: 'foo dog' });
@ -227,7 +223,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n2.id);
}));
it('should support filtering by tags', asyncTest(async () => {
it('should support filtering by tags', (async () => {
let rows;
const n1 = await Note.save({ title: 'But I would', body: 'walk 500 miles' });
const n2 = await Note.save({ title: 'And I would', body: 'walk 500 more' });
@ -253,7 +249,7 @@ describe('services_SearchFilter', function() {
}));
it('should support filtering by tags', asyncTest(async () => {
it('should support filtering by tags', (async () => {
let rows;
const n1 = await Note.save({ title: 'peace talks', body: 'battle ground' });
const n2 = await Note.save({ title: 'mouse', body: 'mister' });
@ -303,7 +299,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by notebook', asyncTest(async () => {
it('should support filtering by notebook', (async () => {
let rows;
const folder0 = await Folder.save({ title: 'notebook0' });
const folder1 = await Folder.save({ title: 'notebook1' });
@ -318,7 +314,7 @@ describe('services_SearchFilter', function() {
}));
it('should support filtering by nested notebook', asyncTest(async () => {
it('should support filtering by nested notebook', (async () => {
let rows;
const folder0 = await Folder.save({ title: 'notebook0' });
const folder00 = await Folder.save({ title: 'notebook00', parent_id: folder0.id });
@ -334,7 +330,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows).sort()).toEqual(ids(notes0.concat(notes00)).sort());
}));
it('should support filtering by multiple notebooks', asyncTest(async () => {
it('should support filtering by multiple notebooks', (async () => {
let rows;
const folder0 = await Folder.save({ title: 'notebook0' });
const folder00 = await Folder.save({ title: 'notebook00', parent_id: folder0.id });
@ -352,7 +348,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows).sort()).toEqual(ids(notes0).concat(ids(notes00).concat(ids(notes1))).sort());
}));
it('should support filtering by created date', asyncTest(async () => {
it('should support filtering by created date', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this on', body: 'May 20 2020', user_created_time: Date.parse('2020-05-20') });
const n2 = await Note.save({ title: 'I made this on', body: 'May 19 2020', user_created_time: Date.parse('2020-05-19') });
@ -375,7 +371,7 @@ describe('services_SearchFilter', function() {
}));
it('should support filtering by between two dates', asyncTest(async () => {
it('should support filtering by between two dates', (async () => {
let rows;
const n1 = await Note.save({ title: 'January 01 2020', body: 'January 01 2020', user_created_time: Date.parse('2020-01-01') });
const n2 = await Note.save({ title: 'February 15 2020', body: 'February 15 2020', user_created_time: Date.parse('2020-02-15') });
@ -399,7 +395,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n4.id);
}));
it('should support filtering by created with smart value: day', asyncTest(async () => {
it('should support filtering by created with smart value: day', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this', body: 'today', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'day'), 10) });
const n2 = await Note.save({ title: 'I made this', body: 'yesterday', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'day'), 10) });
@ -423,7 +419,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by created with smart value: week', asyncTest(async () => {
it('should support filtering by created with smart value: week', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this', body: 'this week', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'week'), 10) });
const n2 = await Note.save({ title: 'I made this', body: 'the week before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'week'), 10) });
@ -447,7 +443,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by created with smart value: month', asyncTest(async () => {
it('should support filtering by created with smart value: month', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this', body: 'this month', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'month'), 10) });
const n2 = await Note.save({ title: 'I made this', body: 'the month before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'month'), 10) });
@ -471,7 +467,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by created with smart value: year', asyncTest(async () => {
it('should support filtering by created with smart value: year', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this', body: 'this year', user_created_time: parseInt(time.goBackInTime(Date.now(), 0, 'year'), 10) });
const n2 = await Note.save({ title: 'I made this', body: 'the year before', user_created_time: parseInt(time.goBackInTime(Date.now(), 1, 'year'), 10) });
@ -495,7 +491,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by updated date', asyncTest(async () => {
it('should support filtering by updated date', (async () => {
let rows;
const n1 = await Note.save({ title: 'I updated this on', body: 'May 20 2020', updated_time: Date.parse('2020-05-20'), user_updated_time: Date.parse('2020-05-20') }, { autoTimestamp: false });
const n2 = await Note.save({ title: 'I updated this on', body: 'May 19 2020', updated_time: Date.parse('2020-05-19'), user_updated_time: Date.parse('2020-05-19') }, { autoTimestamp: false });
@ -512,7 +508,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n2.id);
}));
it('should support filtering by updated with smart value: day', asyncTest(async () => {
it('should support filtering by updated with smart value: day', (async () => {
let rows;
const today = parseInt(time.goBackInTime(Date.now(), 0, 'day'), 10);
const yesterday = parseInt(time.goBackInTime(Date.now(), 1, 'day'), 10);
@ -544,7 +540,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by type todo', asyncTest(async () => {
it('should support filtering by type todo', (async () => {
let rows;
const t1 = await Note.save({ title: 'This is a ', body: 'todo', is_todo: 1 });
const t2 = await Note.save({ title: 'This is another', body: 'todo but completed', is_todo: 1, todo_completed: 1590085027710 });
@ -571,7 +567,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(t1.id);
}));
it('should support filtering by type note', asyncTest(async () => {
it('should support filtering by type note', (async () => {
let rows;
const t1 = await Note.save({ title: 'This is a ', body: 'todo', is_todo: 1 });
const t2 = await Note.save({ title: 'This is another', body: 'todo but completed', is_todo: 1, todo_completed: 1590085027710 });
@ -584,7 +580,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(t3.id);
}));
it('should support filtering by latitude, longitude, altitude', asyncTest(async () => {
it('should support filtering by latitude, longitude, altitude', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this', body: 'this week', latitude: 12.97, longitude: 88.88, altitude: 69.96 });
const n2 = await Note.save({ title: 'I made this', body: 'the week before', latitude: 42.11, longitude: 77.77, altitude: 42.00 });
@ -631,7 +627,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n3.id);
}));
it('should support filtering by resource MIME type', asyncTest(async () => {
it('should support filtering by resource MIME type', (async () => {
let rows;
const service = new ResourceService();
// console.log(testImagePath)
@ -674,7 +670,7 @@ describe('services_SearchFilter', function() {
expect(ids(rows)).toContain(n4.id);
}));
it('should ignore dashes in a word', asyncTest(async () => {
it('should ignore dashes in a word', (async () => {
const n0 = await Note.save({ title: 'doesnotwork' });
const n1 = await Note.save({ title: 'does not work' });
const n2 = await Note.save({ title: 'does-not-work' });
@ -712,7 +708,7 @@ describe('services_SearchFilter', function() {
}));
it('should support filtering by sourceurl', asyncTest(async () => {
it('should support filtering by sourceurl', (async () => {
const n0 = await Note.save({ title: 'n0', source_url: 'https://discourse.joplinapp.org' });
const n1 = await Note.save({ title: 'n1', source_url: 'https://google.com' });
const n2 = await Note.save({ title: 'n2', source_url: 'https://reddit.com' });

View File

@ -2,7 +2,7 @@ import KeychainService from '@joplin/lib/services/keychain/KeychainService';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
const { db, asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const { db, setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
function describeIfCompatible(name: string, fn: any, elseFn: any) {
if (['win32', 'darwin'].includes(shim.platformName())) {
@ -26,11 +26,11 @@ describeIfCompatible('services_KeychainService', function() {
done();
});
it('should be enabled on macOS and Windows', asyncTest(async () => {
it('should be enabled on macOS and Windows', (async () => {
expect(Setting.value('keychain.supported')).toBe(1);
}));
it('should set, get and delete passwords', asyncTest(async () => {
it('should set, get and delete passwords', (async () => {
const service = KeychainService.instance();
const isSet = await service.setPassword('zz_testunit', 'password');
@ -44,7 +44,7 @@ describeIfCompatible('services_KeychainService', function() {
expect(await service.password('zz_testunit')).toBe(null);
}));
it('should save and load secure settings', asyncTest(async () => {
it('should save and load secure settings', (async () => {
Setting.setObjectValue('encryption.passwordCache', 'testing', '123456');
await Setting.saveAll();
await Setting.load();
@ -52,7 +52,7 @@ describeIfCompatible('services_KeychainService', function() {
expect(passwords.testing).toBe('123456');
}));
it('should delete db settings if they have been saved in keychain', asyncTest(async () => {
it('should delete db settings if they have been saved in keychain', (async () => {
// First save some secure settings and make sure it ends up in the databse
KeychainService.instance().enabled = false;

View File

@ -2,7 +2,7 @@ import { PaginationOrderDir } from '@joplin/lib/models/utils/types';
import Api, { RequestMethod } from '@joplin/lib/services/rest/Api';
import shim from '@joplin/lib/shim';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, db } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, db } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder');
const Resource = require('@joplin/lib/models/Resource');
const Note = require('@joplin/lib/models/Note');
@ -50,23 +50,23 @@ describe('services_rest_Api', function() {
done();
});
it('should ping', asyncTest(async () => {
it('should ping', (async () => {
const response = await api.route(RequestMethod.GET, 'ping');
expect(response).toBe('JoplinClipperServer');
}));
it('should handle Not Found errors', asyncTest(async () => {
it('should handle Not Found errors', (async () => {
const hasThrown = await checkThrowAsync(async () => await api.route(RequestMethod.GET, 'pong'));
expect(hasThrown).toBe(true);
}));
it('should get folders', asyncTest(async () => {
it('should get folders', (async () => {
await Folder.save({ title: 'mon carnet' });
const response = await api.route(RequestMethod.GET, 'folders');
expect(response.items.length).toBe(1);
}));
it('should update folders', asyncTest(async () => {
it('should update folders', (async () => {
const f1 = await Folder.save({ title: 'mon carnet' });
await api.route(RequestMethod.PUT, `folders/${f1.id}`, null, JSON.stringify({
title: 'modifié',
@ -76,7 +76,7 @@ describe('services_rest_Api', function() {
expect(f1b.title).toBe('modifié');
}));
it('should delete folders', asyncTest(async () => {
it('should delete folders', (async () => {
const f1 = await Folder.save({ title: 'mon carnet' });
await api.route(RequestMethod.DELETE, `folders/${f1.id}`);
@ -84,7 +84,7 @@ describe('services_rest_Api', function() {
expect(!f1b).toBe(true);
}));
it('should create folders', asyncTest(async () => {
it('should create folders', (async () => {
const response = await api.route(RequestMethod.POST, 'folders', null, JSON.stringify({
title: 'from api',
}));
@ -96,7 +96,7 @@ describe('services_rest_Api', function() {
expect(f[0].title).toBe('from api');
}));
it('should get one folder', asyncTest(async () => {
it('should get one folder', (async () => {
const f1 = await Folder.save({ title: 'mon carnet' });
const response = await api.route(RequestMethod.GET, `folders/${f1.id}`);
expect(response.id).toBe(f1.id);
@ -105,7 +105,7 @@ describe('services_rest_Api', function() {
expect(hasThrown).toBe(true);
}));
it('should get the folder notes', asyncTest(async () => {
it('should get the folder notes', (async () => {
const f1 = await Folder.save({ title: 'mon carnet' });
const response2 = await api.route(RequestMethod.GET, `folders/${f1.id}/notes`);
expect(response2.items.length).toBe(0);
@ -116,12 +116,12 @@ describe('services_rest_Api', function() {
expect(response.items.length).toBe(2);
}));
it('should fail on invalid paths', asyncTest(async () => {
it('should fail on invalid paths', (async () => {
const hasThrown = await checkThrowAsync(async () => await api.route(RequestMethod.GET, 'schtroumpf'));
expect(hasThrown).toBe(true);
}));
it('should get notes', asyncTest(async () => {
it('should get notes', (async () => {
let response = null;
const f1 = await Folder.save({ title: 'mon carnet' });
const f2 = await Folder.save({ title: 'mon deuxième carnet' });
@ -141,7 +141,7 @@ describe('services_rest_Api', function() {
expect(response.title).toBe('trois');
}));
it('should create notes', asyncTest(async () => {
it('should create notes', (async () => {
let response = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -160,7 +160,7 @@ describe('services_rest_Api', function() {
expect(!!response.id).toBe(true);
}));
it('should allow setting note properties', asyncTest(async () => {
it('should allow setting note properties', (async () => {
let response: any = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -195,7 +195,7 @@ describe('services_rest_Api', function() {
}
}));
it('should preserve user timestamps when creating notes', asyncTest(async () => {
it('should preserve user timestamps when creating notes', (async () => {
let response = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -222,7 +222,7 @@ describe('services_rest_Api', function() {
expect(newNote.user_created_time).toBeGreaterThanOrEqual(timeBefore);
}));
it('should preserve user timestamps when updating notes', asyncTest(async () => {
it('should preserve user timestamps when updating notes', (async () => {
const folder = await Folder.save({ title: 'mon carnet' });
const updatedTime = Date.now() - 1000;
@ -265,7 +265,7 @@ describe('services_rest_Api', function() {
}
}));
it('should create notes with supplied ID', asyncTest(async () => {
it('should create notes with supplied ID', (async () => {
let response = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -277,7 +277,7 @@ describe('services_rest_Api', function() {
expect(response.id).toBe('12345678123456781234567812345678');
}));
it('should create todos', asyncTest(async () => {
it('should create todos', (async () => {
let response = null;
const f = await Folder.save({ title: 'stuff to do' });
@ -308,7 +308,7 @@ describe('services_rest_Api', function() {
}));
}));
it('should create folders with supplied ID', asyncTest(async () => {
it('should create folders with supplied ID', (async () => {
const response = await api.route(RequestMethod.POST, 'folders', null, JSON.stringify({
id: '12345678123456781234567812345678',
title: 'from api',
@ -317,7 +317,7 @@ describe('services_rest_Api', function() {
expect(response.id).toBe('12345678123456781234567812345678');
}));
it('should create notes with images', asyncTest(async () => {
it('should create notes with images', (async () => {
let response = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -334,7 +334,7 @@ describe('services_rest_Api', function() {
expect(response.body.indexOf(resource.id) >= 0).toBe(true);
}));
it('should delete resources', asyncTest(async () => {
it('should delete resources', (async () => {
const f = await Folder.save({ title: 'mon carnet' });
await api.route(RequestMethod.POST, 'notes', null, JSON.stringify({
@ -353,7 +353,7 @@ describe('services_rest_Api', function() {
expect(!(await Resource.load(resource.id))).toBe(true);
}));
it('should create notes from HTML', asyncTest(async () => {
it('should create notes from HTML', (async () => {
let response = null;
const f = await Folder.save({ title: 'mon carnet' });
@ -366,7 +366,7 @@ describe('services_rest_Api', function() {
expect(response.body).toBe('**Bold text**');
}));
it('should handle tokens', asyncTest(async () => {
it('should handle tokens', (async () => {
api = new Api('mytoken');
let hasThrown = await checkThrowAsync(async () => await api.route(RequestMethod.GET, 'notes'));
@ -379,7 +379,7 @@ describe('services_rest_Api', function() {
expect(hasThrown).toBe(true);
}));
it('should add tags to notes', asyncTest(async () => {
it('should add tags to notes', (async () => {
const tag = await Tag.save({ title: 'mon étiquette' });
const note = await Note.save({ title: 'ma note' });
@ -391,7 +391,7 @@ describe('services_rest_Api', function() {
expect(noteIds[0]).toBe(note.id);
}));
it('should remove tags from notes', asyncTest(async () => {
it('should remove tags from notes', (async () => {
const tag = await Tag.save({ title: 'mon étiquette' });
const note = await Note.save({ title: 'ma note' });
await Tag.addNote(tag.id, note.id);
@ -402,7 +402,7 @@ describe('services_rest_Api', function() {
expect(noteIds.length).toBe(0);
}));
it('should list all tag notes', asyncTest(async () => {
it('should list all tag notes', (async () => {
const tag = await Tag.save({ title: 'mon étiquette' });
const tag2 = await Tag.save({ title: 'mon étiquette 2' });
const note1 = await Note.save({ title: 'ma note un' });
@ -422,7 +422,7 @@ describe('services_rest_Api', function() {
expect(response3.items.length).toBe(2);
}));
it('should update tags when updating notes', asyncTest(async () => {
it('should update tags when updating notes', (async () => {
const tag1 = await Tag.save({ title: 'mon étiquette 1' });
const tag2 = await Tag.save({ title: 'mon étiquette 2' });
const tag3 = await Tag.save({ title: 'mon étiquette 3' });
@ -443,7 +443,7 @@ describe('services_rest_Api', function() {
expect(tagIds.includes(tag3.id)).toBe(true);
}));
it('should create and update tags when updating notes', asyncTest(async () => {
it('should create and update tags when updating notes', (async () => {
const tag1 = await Tag.save({ title: 'mon étiquette 1' });
const tag2 = await Tag.save({ title: 'mon étiquette 2' });
const newTagTitle = 'mon étiquette 3';
@ -465,7 +465,7 @@ describe('services_rest_Api', function() {
expect(tagIds.includes(newTag.id)).toBe(true);
}));
it('should not update tags if tags is not mentioned when updating', asyncTest(async () => {
it('should not update tags if tags is not mentioned when updating', (async () => {
const tag1 = await Tag.save({ title: 'mon étiquette 1' });
const tag2 = await Tag.save({ title: 'mon étiquette 2' });
@ -485,7 +485,7 @@ describe('services_rest_Api', function() {
expect(tagIds.includes(tag2.id)).toBe(true);
}));
it('should remove tags from note if tags is set to empty string when updating', asyncTest(async () => {
it('should remove tags from note if tags is set to empty string when updating', (async () => {
const tag1 = await Tag.save({ title: 'mon étiquette 1' });
const tag2 = await Tag.save({ title: 'mon étiquette 2' });
@ -503,7 +503,7 @@ describe('services_rest_Api', function() {
expect(tagIds.length === 0).toBe(true);
}));
it('should paginate results', asyncTest(async () => {
it('should paginate results', (async () => {
await createFolderForPagination(1, 1001);
await createFolderForPagination(2, 1002);
await createFolderForPagination(3, 1003);
@ -564,7 +564,7 @@ describe('services_rest_Api', function() {
}
}));
it('should paginate results and handle duplicate field values', asyncTest(async () => {
it('should paginate results and handle duplicate field values', (async () => {
// If, for example, ordering by updated_time, and two of the rows
// have the same updated_time, it should make sure that the sort
// order is stable and all results are correctly returned.
@ -593,7 +593,7 @@ describe('services_rest_Api', function() {
expect(r2.items[1].title).toBe('folder4');
}));
it('should paginate results and return the requested fields only', asyncTest(async () => {
it('should paginate results and return the requested fields only', (async () => {
await createNoteForPagination(1, 1001);
await createNoteForPagination(2, 1002);
await createNoteForPagination(3, 1003);
@ -621,7 +621,7 @@ describe('services_rest_Api', function() {
expect(!!r2.items[0].id).toBe(true);
}));
it('should paginate folder notes', asyncTest(async () => {
it('should paginate folder notes', (async () => {
const folder = await Folder.save({});
const note1 = await Note.save({ parent_id: folder.id });
await msleep(1);
@ -646,7 +646,7 @@ describe('services_rest_Api', function() {
expect(r2.items[0].id).toBe(note3.id);
}));
it('should sort search paginated results', asyncTest(async () => {
it('should sort search paginated results', (async () => {
SearchEngine.instance().setDb(db());
await createNoteForPagination('note c', 1000);
@ -698,7 +698,7 @@ describe('services_rest_Api', function() {
}
}));
it('should return default fields', asyncTest(async () => {
it('should return default fields', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note1 = await Note.save({ title: 'note1', parent_id: folder.id });
await Note.save({ title: 'note2', parent_id: folder.id });
@ -740,7 +740,7 @@ describe('services_rest_Api', function() {
}
}));
it('should return the notes associated with a resource', asyncTest(async () => {
it('should return the notes associated with a resource', (async () => {
const note = await Note.save({});
await shim.attachFileToNote(note, `${__dirname}/../tests/support/photo.jpg`);
const resource = (await Resource.all())[0];
@ -754,7 +754,7 @@ describe('services_rest_Api', function() {
expect(r.items[0].id).toBe(note.id);
}));
it('should return the resources associated with a note', asyncTest(async () => {
it('should return the resources associated with a note', (async () => {
const note = await Note.save({});
await shim.attachFileToNote(note, `${__dirname}/../tests/support/photo.jpg`);
const resource = (await Resource.all())[0];
@ -765,7 +765,7 @@ describe('services_rest_Api', function() {
expect(r.items[0].id).toBe(resource.id);
}));
it('should return search results', asyncTest(async () => {
it('should return search results', (async () => {
SearchEngine.instance().setDb(db());
for (let i = 0; i < 10; i++) {

View File

@ -1,4 +1,4 @@
const { syncDir, asyncTest, fileApi, synchronizer, createSyncTargetSnapshot, loadEncryptionMasterKey, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('../test-utils.js');
const { syncDir, fileApi, synchronizer, createSyncTargetSnapshot, loadEncryptionMasterKey, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('../test-utils.js');
const Setting = require('@joplin/lib/models/Setting').default;
const Folder = require('@joplin/lib/models/Folder');
const Note = require('@joplin/lib/models/Note');

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
import LockHandler, { LockType, LockHandlerOptions, Lock } from '@joplin/lib/services/synchronizer/LockHandler';
const { isNetworkSyncTarget, asyncTest, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } = require('./test-utils.js');
process.on('unhandledRejection', (reason: any, p: any) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const { isNetworkSyncTarget, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } = require('./test-utils.js');
// For tests with memory of file system we can use low intervals to make the tests faster.
// However if we use such low values with network sync targets, some calls might randomly fail with
@ -39,7 +35,7 @@ describe('synchronizer_LockHandler', function() {
done();
});
it('should acquire and release a sync lock', asyncTest(async () => {
it('should acquire and release a sync lock', (async () => {
await lockHandler().acquireLock(LockType.Sync, 'mobile', '123456');
const locks = await lockHandler().locks(LockType.Sync);
expect(locks.length).toBe(1);
@ -51,7 +47,7 @@ describe('synchronizer_LockHandler', function() {
expect((await lockHandler().locks(LockType.Sync)).length).toBe(0);
}));
it('should not use files that are not locks', asyncTest(async () => {
it('should not use files that are not locks', (async () => {
await fileApi().put('locks/desktop.ini', 'a');
await fileApi().put('locks/exclusive.json', 'a');
await fileApi().put('locks/garbage.json', 'a');
@ -61,7 +57,7 @@ describe('synchronizer_LockHandler', function() {
expect(locks.length).toBe(1);
}));
it('should allow multiple sync locks', asyncTest(async () => {
it('should allow multiple sync locks', (async () => {
await lockHandler().acquireLock(LockType.Sync, 'mobile', '111');
await switchClient(2);
@ -78,7 +74,7 @@ describe('synchronizer_LockHandler', function() {
}
}));
it('should auto-refresh a lock', asyncTest(async () => {
it('should auto-refresh a lock', (async () => {
const handler = newLockHandler({ autoRefreshInterval: 100 * timeoutMultipler });
const lock = await handler.acquireLock(LockType.Sync, 'desktop', '111');
const lockBefore = await handler.activeLock(LockType.Sync, 'desktop', '111');
@ -89,7 +85,7 @@ describe('synchronizer_LockHandler', function() {
handler.stopAutoLockRefresh(lock);
}));
it('should call the error handler when lock has expired while being auto-refreshed', asyncTest(async () => {
it('should call the error handler when lock has expired while being auto-refreshed', (async () => {
const handler = newLockHandler({
lockTtl: 50 * timeoutMultipler,
autoRefreshInterval: 200 * timeoutMultipler,
@ -108,7 +104,7 @@ describe('synchronizer_LockHandler', function() {
handler.stopAutoLockRefresh(lock);
}));
it('should not allow sync locks if there is an exclusive lock', asyncTest(async () => {
it('should not allow sync locks if there is an exclusive lock', (async () => {
await lockHandler().acquireLock(LockType.Exclusive, 'desktop', '111');
await expectThrow(async () => {
@ -116,7 +112,7 @@ describe('synchronizer_LockHandler', function() {
}, 'hasExclusiveLock');
}));
it('should not allow exclusive lock if there are sync locks', asyncTest(async () => {
it('should not allow exclusive lock if there are sync locks', (async () => {
const lockHandler = newLockHandler({ lockTtl: 1000 * 60 * 60 });
await lockHandler.acquireLock(LockType.Sync, 'mobile', '111');
@ -127,7 +123,7 @@ describe('synchronizer_LockHandler', function() {
}, 'hasSyncLock');
}));
it('should allow exclusive lock if the sync locks have expired', asyncTest(async () => {
it('should allow exclusive lock if the sync locks have expired', (async () => {
const lockHandler = newLockHandler({ lockTtl: 500 * timeoutMultipler });
await lockHandler.acquireLock(LockType.Sync, 'mobile', '111');
@ -140,7 +136,7 @@ describe('synchronizer_LockHandler', function() {
});
}));
it('should decide what is the active exclusive lock', asyncTest(async () => {
it('should decide what is the active exclusive lock', (async () => {
const lockHandler = newLockHandler();
{
@ -155,7 +151,7 @@ describe('synchronizer_LockHandler', function() {
}
}));
// it('should not have race conditions', asyncTest(async () => {
// it('should not have race conditions', (async () => {
// const lockHandler = newLockHandler();
// const clients = [];

View File

@ -8,7 +8,7 @@ import { Dirnames } from '@joplin/lib/services/synchronizer/utils/types';
// gulp buildTests -L && node tests-build/support/createSyncTargetSnapshot.js normal && node tests-build/support/createSyncTargetSnapshot.js e2ee
const { asyncTest, setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
const { setSyncTargetName, fileApi, synchronizer, decryptionWorker, encryptionService, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
const { deploySyncTargetSnapshot, testData, checkTestData } = require('./support/syncTargetUtils');
const Setting = require('@joplin/lib/models/Setting').default;
const MasterKey = require('@joplin/lib/models/MasterKey');
@ -52,6 +52,13 @@ let previousSyncTargetName: string = '';
describe('synchronizer_MigrationHandler', function() {
beforeEach(async (done: Function) => {
// Note that, for undocumented reasons, the timeout argument passed
// to `test()` (or `it()`) is ignored if it is higher than the
// global Jest timeout. So we need to set it globally.
//
// https://github.com/facebook/jest/issues/5055#issuecomment-513585906
jest.setTimeout(specTimeout);
// To test the migrations, we have to use the filesystem sync target
// because the sync target snapshots are plain files. Eventually
// it should be possible to copy a filesystem target to memory
@ -70,7 +77,7 @@ describe('synchronizer_MigrationHandler', function() {
done();
});
it('should init a new sync target', asyncTest(async () => {
it('should init a new sync target', (async () => {
// Check that basic folders "locks" and "temp" are created for new sync targets.
await migrationHandler().upgrade(1);
const result = await fileApi().list();
@ -78,13 +85,13 @@ describe('synchronizer_MigrationHandler', function() {
expect(result.items.filter((i: any) => i.path === Dirnames.Temp).length).toBe(1);
}), specTimeout);
it('should not allow syncing if the sync target is out-dated', asyncTest(async () => {
it('should not allow syncing if the sync target is out-dated', (async () => {
await synchronizer().start();
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);
await expectThrow(async () => await migrationHandler().checkCanSync(), 'outdatedSyncTarget');
}), specTimeout);
it('should not allow syncing if the client is out-dated', asyncTest(async () => {
it('should not allow syncing if the client is out-dated', (async () => {
await synchronizer().start();
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') + 1}}`);
await expectThrow(async () => await migrationHandler().checkCanSync(), 'outdatedClient');
@ -93,7 +100,7 @@ describe('synchronizer_MigrationHandler', function() {
for (const migrationVersionString in migrationTests) {
const migrationVersion = Number(migrationVersionString);
it(`should migrate (${migrationVersion})`, asyncTest(async () => {
it(`should migrate (${migrationVersion})`, (async () => {
await deploySyncTargetSnapshot('normal', migrationVersion - 1);
const info = await migrationHandler().fetchSyncTargetInfo();
@ -120,7 +127,7 @@ describe('synchronizer_MigrationHandler', function() {
await expectNotThrow(async () => await checkTestData(testData));
}), specTimeout);
it(`should migrate (E2EE) (${migrationVersion})`, asyncTest(async () => {
it(`should migrate (E2EE) (${migrationVersion})`, (async () => {
// First create some test data that will be used to validate
// that the migration didn't alter any data.
await deploySyncTargetSnapshot('e2ee', migrationVersion - 1);

View File

@ -0,0 +1,86 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.localNotesFoldersSameAsRemote = exports.remoteResources = exports.remoteNotesFoldersResources = exports.remoteNotesAndFolders = exports.allNotesFolders = void 0;
const BaseModel_1 = require("@joplin/lib/BaseModel");
const { fileApi } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
function allNotesFolders() {
return __awaiter(this, void 0, void 0, function* () {
const folders = yield Folder.all();
const notes = yield Note.all();
return folders.concat(notes);
});
}
exports.allNotesFolders = allNotesFolders;
function remoteItemsByTypes(types) {
return __awaiter(this, void 0, void 0, function* () {
const list = yield fileApi().list('', { includeDirs: false, syncItemsOnly: true });
if (list.has_more)
throw new Error('Not implemented!!!');
const files = list.items;
const output = [];
for (const file of files) {
const remoteContent = yield fileApi().get(file.path);
const content = yield BaseItem.unserialize(remoteContent);
if (types.indexOf(content.type_) < 0)
continue;
output.push(content);
}
return output;
});
}
function remoteNotesAndFolders() {
return __awaiter(this, void 0, void 0, function* () {
return remoteItemsByTypes([BaseModel_1.default.TYPE_NOTE, BaseModel_1.default.TYPE_FOLDER]);
});
}
exports.remoteNotesAndFolders = remoteNotesAndFolders;
function remoteNotesFoldersResources() {
return __awaiter(this, void 0, void 0, function* () {
return remoteItemsByTypes([BaseModel_1.default.TYPE_NOTE, BaseModel_1.default.TYPE_FOLDER, BaseModel_1.default.TYPE_RESOURCE]);
});
}
exports.remoteNotesFoldersResources = remoteNotesFoldersResources;
function remoteResources() {
return __awaiter(this, void 0, void 0, function* () {
return remoteItemsByTypes([BaseModel_1.default.TYPE_RESOURCE]);
});
}
exports.remoteResources = remoteResources;
function localNotesFoldersSameAsRemote(locals, expect) {
return __awaiter(this, void 0, void 0, function* () {
let error = null;
try {
const nf = yield remoteNotesAndFolders();
expect(locals.length).toBe(nf.length);
for (let i = 0; i < locals.length; i++) {
const dbItem = locals[i];
const path = BaseItem.systemPath(dbItem);
const remote = yield fileApi().stat(path);
expect(!!remote).toBe(true);
if (!remote)
continue;
let remoteContent = yield fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel_1.default.TYPE_NOTE ? yield Note.unserialize(remoteContent) : yield Folder.unserialize(remoteContent);
expect(remoteContent.title).toBe(dbItem.title);
}
}
catch (e) {
error = e;
}
expect(error).toBe(null);
});
}
exports.localNotesFoldersSameAsRemote = localNotesFoldersSameAsRemote;
//# sourceMappingURL=test-utils-synchronizer.js.map

View File

@ -0,0 +1,65 @@
import BaseModel from '@joplin/lib/BaseModel';
const { fileApi } = require('./test-utils.js');
const Folder = require('@joplin/lib/models/Folder.js');
const Note = require('@joplin/lib/models/Note.js');
const BaseItem = require('@joplin/lib/models/BaseItem.js');
export async function allNotesFolders() {
const folders = await Folder.all();
const notes = await Note.all();
return folders.concat(notes);
}
async function remoteItemsByTypes(types: number[]) {
const list = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
if (list.has_more) throw new Error('Not implemented!!!');
const files = list.items;
const output = [];
for (const file of files) {
const remoteContent = await fileApi().get(file.path);
const content = await BaseItem.unserialize(remoteContent);
if (types.indexOf(content.type_) < 0) continue;
output.push(content);
}
return output;
}
export async function remoteNotesAndFolders() {
return remoteItemsByTypes([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER]);
}
export async function remoteNotesFoldersResources() {
return remoteItemsByTypes([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE]);
}
export async function remoteResources() {
return remoteItemsByTypes([BaseModel.TYPE_RESOURCE]);
}
export async function localNotesFoldersSameAsRemote(locals: any[], expect: Function) {
let error = null;
try {
const nf = await remoteNotesAndFolders();
expect(locals.length).toBe(nf.length);
for (let i = 0; i < locals.length; i++) {
const dbItem = locals[i];
const path = BaseItem.systemPath(dbItem);
const remote = await fileApi().stat(path);
expect(!!remote).toBe(true);
if (!remote) continue;
let remoteContent = await fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
expect(remoteContent.title).toBe(dbItem.title);
}
} catch (e) {
error = e;
}
expect(error).toBe(null);
}

View File

@ -139,7 +139,7 @@ setSyncTargetName('memory');
// console.info(`Testing with sync target: ${syncTargetName_}`);
const syncDir = `${__dirname}/../tests/sync`;
const syncDir = `${__dirname}/../tests/sync/${suiteName_}`;
// TODO: Should probably update this for Jest?
@ -559,27 +559,6 @@ function fileContentEqual(path1, path2) {
return content1 === content2;
}
// Wrap an async test in a try/catch block so that done() is always called
// and display a proper error message instead of "unhandled promise error"
function asyncTest(callback) {
return async function(done) {
try {
await callback();
} catch (error) {
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
// OK - will be reported by Jest
} else {
// Better to rethrow exception as stack trace is more useful in this case
throw error;
// console.error(error);
// expect(0).toBe(1, 'Test has thrown an exception - see above error');
}
} finally {
done();
}
};
}
async function allSyncTargetItemsEncrypted() {
const list = await fileApi().list('', { includeDirs: false });
const files = list.items;
@ -792,4 +771,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
module.exports = { newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@ -2,20 +2,16 @@
const time = require('@joplin/lib/time').default;
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('./test-utils.js');
const timeUtils = require('@joplin/lib/time');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('timeUtils', function() {
beforeEach(async (done) => {
done();
});
it('should go back in time', asyncTest(async () => {
it('should go back in time', (async () => {
let startDate = new Date('3 Aug 2020');
let endDate = new Date('2 Aug 2020');
@ -40,7 +36,7 @@ describe('timeUtils', function() {
expect(time.goBackInTime(startDate, 23, 'year')).toBe(endDate.getTime().toString());
}));
it('should go forward in time', asyncTest(async () => {
it('should go forward in time', (async () => {
let startDate = new Date('2 Aug 2020');
let endDate = new Date('3 Aug 2020');

View File

@ -1,18 +1,9 @@
const { asyncTest } = require('./test-utils.js');
const urlUtils = require('@joplin/lib/urlUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at urlUtils: Promise', p, 'reason:', reason);
});
describe('urlUtils', function() {
beforeEach(async (done) => {
done();
});
it('should prepend a base URL', asyncTest(async (done) => {
it('should prepend a base URL', (async () => {
expect(urlUtils.prependBaseUrl('testing.html', 'http://example.com')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('testing.html', 'http://example.com/')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('/jmp/?id=123&u=http://something.com/test', 'http://example.com/')).toBe('http://example.com/jmp/?id=123&u=http://something.com/test');
@ -31,7 +22,7 @@ describe('urlUtils', function() {
expect(urlUtils.prependBaseUrl('#local-anchor', 'http://example.com')).toBe('#local-anchor');
}));
it('should detect resource URLs', asyncTest(async (done) => {
it('should detect resource URLs', (async () => {
const testCases = [
[':/1234abcd1234abcd1234abcd1234abcd', { itemId: '1234abcd1234abcd1234abcd1234abcd', hash: '' }],
[':/1234abcd1234abcd1234abcd1234abcd "some text"', { itemId: '1234abcd1234abcd1234abcd1234abcd', hash: '' }],
@ -61,7 +52,7 @@ describe('urlUtils', function() {
}
}));
it('should extract resource URLs', asyncTest(async (done) => {
it('should extract resource URLs', (async () => {
const testCases = [
['Bla [](:/11111111111111111111111111111111) bla [](:/22222222222222222222222222222222) bla', ['11111111111111111111111111111111', '22222222222222222222222222222222']],
['Bla [](:/11111111111111111111111111111111 "Some title") bla [](:/22222222222222222222222222222222 "something else") bla', ['11111111111111111111111111111111', '22222222222222222222222222222222']],

View File

@ -11,10 +11,6 @@ process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');

View File

@ -7,10 +7,6 @@ process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');

View File

@ -8,10 +8,6 @@ process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');

View File

@ -0,0 +1,9 @@
// Dummy test because the Jest setup is done but there's for now no test.
describe('dummy', () => {
it('should pass', () => {
expect(1).toBe(1);
});
});

View File

@ -1,16 +0,0 @@
// Wrap an async test in a try/catch block so that done() is always called
// and display a proper error message instead of "unhandled promise error"
function asyncTest(callback) {
return async function(done) {
try {
await callback();
} catch (error) {
console.error(error);
expect('good').toBe('not good', 'Test has thrown an exception - see above error');
} finally {
done();
}
};
}
module.exports = { asyncTest };