1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

64 Commits

Author SHA1 Message Date
Laurent Cozic
f37bd462a2 custom layout 2020-12-02 17:26:35 +00:00
Laurent Cozic
717b8da1f8 Tools: Cleaned up tests and splitted sync tests into smaller parts 2020-12-01 18:05:24 +00:00
Laurent Cozic
fa50a8f5da Tools: Publish desktop files only on release tag. Also added debug info. 2020-12-01 15:35:28 +00:00
Laurent Cozic
05e9000087 Plugins: Improved note change event handling. Also added tests and improved debugging plugins. 2020-12-01 14:08:41 +00:00
Laurent Cozic
eed3dc8617 Merge branch 'release-1.4' into dev 2020-12-01 11:29:07 +00:00
Laurent Cozic
de123ee586 Desktop release v1.4.19 2020-12-01 10:14:45 +00:00
Laurent Cozic
89abc4395f All: Disable soft-break by default in Markdown rendering
Although soft-break is part of the CommonMark spec, it requires a
special editor that can wrap text at a certain limit. That doesn't make
much sense in Joplin, where the editor can have various sizes, from
desktop to mobile, and where the tools to wrap text are not present.
2020-12-01 09:49:59 +00:00
Laurent Cozic
4b0d230815 Doc: Update installation info 2020-12-01 09:47:18 +00:00
Laurent Cozic
48e3811fbd Desktop, Cli: Fixed importing ENEX files that contain empty resources 2020-11-30 19:29:44 +00:00
Laurent Cozic
e8d216016e Chore: Make debugging log less verbose for link replacement code 2020-11-30 18:37:58 +00:00
Laurent Cozic
751d0e0745 Plugin Generator: Fixed issue with the same files being added multiple time to JPL archive 2020-11-30 18:32:39 +00:00
Laurent Cozic
d0f22140fd Desktop: Fixed issue with note not being saved when a column is added or remove from Rich Text editor 2020-11-30 18:20:27 +00:00
Laurent Cozic
d63378b4e3 Doc: Updated sponsors 2020-11-30 14:06:06 +00:00
Laurent Cozic
a17b77b495 Update website 2020-11-30 12:06:32 +00:00
Laurent Cozic
04814eefb5 Desktop release v1.5.3 2020-11-29 19:38:40 +00:00
Laurent Cozic
da3e5acc94 Fix XCode version for notarization 2020-11-29 19:38:19 +00:00
Laurent Cozic
ef53c42f0a Desktop release v1.5.2 2020-11-29 18:46:04 +00:00
Laurent Cozic
c8b40bfdb2 Chore: Getting notarization to work 2020-11-29 18:45:37 +00:00
Laurent Cozic
dbb8b4d895 Tools: Fixed changelog script 2020-11-29 17:47:41 +00:00
Laurent Cozic
a654419881 Desktop release v1.5.1 2020-11-29 17:36:24 +00:00
Laurent Cozic
497cf996e8 Setup new version to 1.5 2020-11-29 17:35:41 +00:00
Roman Musin
d5dbc421b1 Android: Fixes #4122: Try to fix external storage access on Android 10 (#4134) 2020-11-29 17:30:51 +00:00
Jan Blunck
f965708ad3 Mobile: Fixes #3601: Fix uploading resource files to S3 with RN (#4127)
When the mobile application tries to synchronize a note with attached
resource files, e.g. from the notebook "Welcome! (Mobile)", readFile()
is called to return a Buffer although that isn't implemented in
FsDriverRN.readFile(). This is changing the code to return base64
encoded content from react-native and turn it into a Buffer before the S3
putObject() API call is used to create the remote object.

Tested with AVD Android 10.0 target.

Signed-off-by: Jan Blunck <jblunck@users.noreply.github.com>
2020-11-29 17:29:46 +00:00
Laurent Cozic
f001d197a8 macOS: Notarize application 2020-11-29 17:15:42 +00:00
Laurent Cozic
d588bddfaa Revert "Tools: Adding debug info to tests"
This reverts commit 154b3573a4.
2020-11-29 16:47:07 +00:00
Laurent Cozic
b780a62588 Tools: Fixed test units on CI 2020-11-29 16:23:14 +00:00
Laurent Cozic
154b3573a4 Tools: Adding debug info to tests 2020-11-29 13:13:53 +00:00
Laurent Cozic
7d2551c9c3 Tools: Run all tests in parallel 2020-11-29 11:29:43 +00:00
Laurent Cozic
7644d05225 Tools: Trying to get tests to build in dev branch 2020-11-29 01:20:49 +00:00
Laurent Cozic
1851b0e7d1 Merge branch 'release-1.4' into dev 2020-11-29 00:22:17 +00:00
Laurent Cozic
76c4d99b87 Desktop release v1.4.18 2020-11-28 12:03:29 +00:00
Laurent Cozic
849ef418a6 Desktop: Fixed notifications on macOS 2020-11-28 12:03:06 +00:00
Laurent Cozic
d733c0e010 Desktop release v1.4.16 2020-11-27 18:19:31 +00:00
Laurent Cozic
a48e5cd4e8 Desktop: Fixed spell checker crash when no language is selected 2020-11-27 18:15:22 +00:00
Laurent Cozic
03942a0073 All: Fix sorting by title in a case insensitive way 2020-11-27 15:16:50 +00:00
Laurent Cozic
0bc53dc063 Merge branch 'release-1.4' into dev 2020-11-27 12:43:40 +00:00
Laurent Cozic
56605beea2 Desktop release v1.4.15 2020-11-27 12:41:24 +00:00
Laurent Cozic
8059d3fbd1 Desktop release v1.4.14 2020-11-27 12:41:12 +00:00
Laurent Cozic
46c38ce0e0 Desktop: Remove support for buggy asar packing as it breaks notifications on macOS. Also make sure only DMG is built for macOS. 2020-11-27 12:40:43 +00:00
Laurent Cozic
dfa928c1f7 Log info 2020-11-27 12:21:59 +00:00
Laurent Cozic
cb696276da Desktop: Fixes #4146: Prevents crash when invalid spell checker language is selected, and provide fallback for invalid language codes 2020-11-27 12:21:34 +00:00
Laurent Cozic
5f80628a4d Desktop: Fixed potential crash when watching note files or resources 2020-11-27 12:19:57 +00:00
Laurent Cozic
b77f868fc8 Log info 2020-11-27 12:03:32 +00:00
Laurent Cozic
6ad9931e43 Desktop: Fixes #4146: Prevents crash when invalid spell checker language is selected, and provide fallback for invalid language codes 2020-11-27 11:12:28 +00:00
Laurent Cozic
011a65f73b Desktop: Fixed potential crash when watching note files or resources 2020-11-27 11:08:42 +00:00
Laurent Cozic
72ccc90ea0 Merge branch 'release-1.4' into dev 2020-11-27 01:16:52 +00:00
Laurent Cozic
f3e6c0da32 Desktop release v1.4.13 2020-11-26 23:32:30 +00:00
Laurent Cozic
9308c3f38c Plugins: Fixed webview postMessage call 2020-11-26 23:31:31 +00:00
Laurent Cozic
c8a7c70838 ios-v10.4.1 2020-11-26 22:17:55 +00:00
Laurent Cozic
40f6dcfb4c Android release 1.4 2020-11-26 19:42:05 +00:00
Laurent Cozic
09785cf366 Tools: Ignored files 2020-11-26 18:15:20 +00:00
Laurent Cozic
7279b508db Tools: Fixed ignore file script 2020-11-26 18:14:49 +00:00
Laurent Cozic
d7996c9707 Merge branch 'release-1.4' into dev 2020-11-26 15:10:18 +00:00
Laurent Cozic
f0432e724a Fix CLI release 2020-11-26 15:09:51 +00:00
Laurent Cozic
2f9bb7b8c0 Doc: Typo 2020-11-26 14:42:50 +00:00
Laurent Cozic
f4b8b5b160 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-11-26 14:42:21 +00:00
Zhang YANG
e2962322be Update zh_CN translations (#4121)
* Update zh_CN.po

* Update zh_CN translations
2020-11-26 14:41:11 +00:00
Mustafa Al-Dailemi
c982e42999 Update da_DK.po (#4117)
Co-authored-by: Mustafa Al-Dailemi <Mustafa-ALD@users.noreply.github.com>
2020-11-26 14:40:30 +00:00
Laurent Cozic
eed52a5cfd Tools: Fixed tests on CI 2020-11-26 14:40:16 +00:00
MichBoi
6272a2eb4f Desktop: Fixes #3917: Fixed numbered list bug in markdown editor (#4116) 2020-11-26 14:34:13 +00:00
Naveen M V
69a4a895d4 All: Fixed basic search when executing a query in Chinese (#4034) 2020-11-26 12:35:04 +00:00
Laurent Cozic
511e4b1da0 Merge branch 'release-1.4' into dev 2020-11-26 12:16:52 +00:00
Laurent Cozic
7fa483d27c Doc: Organise community links 2020-11-25 14:50:27 +00:00
Laurent Cozic
9b64c1fbdb Added no-floating-promises eslint rule 2020-11-25 14:40:25 +00:00
191 changed files with 10605 additions and 6132 deletions

View File

@@ -46,6 +46,7 @@ packages/app-mobile/ios
packages/app-mobile/locales
packages/app-mobile/node_modules
packages/app-mobile/pluginAssets/
packages/app-mobile/lib/rnInjectedJs/
packages/lib/assets/
packages/lib/rnInjectedJs/
packages/lib/vendor/
@@ -58,93 +59,6 @@ plugin_types/
readme/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
Assets/TinyMCE/JoplinLists/src/main/ts/Main.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/Main.js
Assets/TinyMCE/JoplinLists/src/main/ts/Main.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.js
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.js
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.js
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.js
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.js.map
packages/app-cli/app/LinkSelector.d.ts
packages/app-cli/app/LinkSelector.js
packages/app-cli/app/LinkSelector.js.map
@@ -160,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
@@ -172,6 +104,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
packages/app-cli/tests/services/plugins/sandboxProxy.js
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
@@ -325,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
@@ -955,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
@@ -1210,6 +1151,9 @@ packages/lib/services/plugins/utils/executeSandboxCall.js.map
packages/lib/services/plugins/utils/loadContentScripts.d.ts
packages/lib/services/plugins/utils/loadContentScripts.js
packages/lib/services/plugins/utils/loadContentScripts.js.map
packages/lib/services/plugins/utils/makeListener.d.ts
packages/lib/services/plugins/utils/makeListener.js
packages/lib/services/plugins/utils/makeListener.js.map
packages/lib/services/plugins/utils/manifestFromObject.d.ts
packages/lib/services/plugins/utils/manifestFromObject.js
packages/lib/services/plugins/utils/manifestFromObject.js.map

View File

@@ -24,6 +24,7 @@ module.exports = {
'afterAll': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'jest': 'readonly',
// React Native variables
'__DEV__': 'readonly',
@@ -126,6 +127,10 @@ module.exports = {
{
// enable the rule specifically for TypeScript files
'files': ['*.ts', '*.tsx'],
'parserOptions': {
// Required for @typescript-eslint/no-floating-promises
'project': './tsconfig.eslint.json',
},
'rules': {
// Warn only because it would make it difficult to convert JS classes to TypeScript, unless we
// make everything public which is not great. New code however should specify member accessibility.
@@ -152,6 +157,7 @@ module.exports = {
'requireLast': false,
},
}],
'@typescript-eslint/no-floating-promises': ['error'],
},
},
],

117
.gitignore vendored
View File

@@ -50,93 +50,6 @@ packages/tools/github_oauth_token.txt
lerna-debug.log
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
Assets/TinyMCE/JoplinLists/src/main/ts/Main.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/Main.js
Assets/TinyMCE/JoplinLists/src/main/ts/Main.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.js
Assets/TinyMCE/JoplinLists/src/main/ts/Plugin.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.js
Assets/TinyMCE/JoplinLists/src/main/ts/actions/Indendation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.js
Assets/TinyMCE/JoplinLists/src/main/ts/actions/ToggleList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Api.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Commands.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Events.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.js
Assets/TinyMCE/JoplinLists/src/main/ts/api/Settings.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Bookmark.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Delete.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/DlIndentation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Keyboard.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/ListAction.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/NodeType.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/NormalizeLists.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Range.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Selection.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/SplitList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/TextBlock.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.js
Assets/TinyMCE/JoplinLists/src/main/ts/core/Util.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ComposeList.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Entry.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Indentation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/JoplinListUtil.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ListsIndendation.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/NormalizeEntries.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/ParseLists.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.js
Assets/TinyMCE/JoplinLists/src/main/ts/listModel/Util.js.map
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.d.ts
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.js
Assets/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.js.map
packages/app-cli/app/LinkSelector.d.ts
packages/app-cli/app/LinkSelector.js
packages/app-cli/app/LinkSelector.js.map
@@ -152,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
@@ -164,6 +95,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
packages/app-cli/tests/services/plugins/sandboxProxy.js
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
@@ -317,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
@@ -947,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
@@ -1202,6 +1142,9 @@ packages/lib/services/plugins/utils/executeSandboxCall.js.map
packages/lib/services/plugins/utils/loadContentScripts.d.ts
packages/lib/services/plugins/utils/loadContentScripts.js
packages/lib/services/plugins/utils/loadContentScripts.js.map
packages/lib/services/plugins/utils/makeListener.d.ts
packages/lib/services/plugins/utils/makeListener.js
packages/lib/services/plugins/utils/makeListener.js.map
packages/lib/services/plugins/utils/manifestFromObject.d.ts
packages/lib/services/plugins/utils/manifestFromObject.js
packages/lib/services/plugins/utils/manifestFromObject.js.map

View File

@@ -1,5 +1,5 @@
# Only build tags (Doesn't work - doesn't build anything)
if: tag IS present OR type = pull_request
if: tag IS present OR type = pull_request OR branch = dev
rvm: 2.3.3
@@ -15,21 +15,30 @@ branches:
matrix:
include:
- os: osx
osx_image: xcode9.0
osx_image: xcode12
language: node_js
node_js: "10"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
node_js: "12"
cache:
npm: false
# Cache was disabled because when changing from node_js 10 to node_js 12
# it was still using build files from Node 10 when building SQLite which
# was making it fail. Might be ok to re-enable later on, although it doesn't
# make build that much faster.
#
# env:
# - ELECTRON_CACHE=$HOME/.cache/electron
# - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- os: linux
sudo: required
dist: trusty
language: node_js
node_js: "10"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
node_js: "12"
cache:
npm: false
# env:
# - ELECTRON_CACHE=$HOME/.cache/electron
# - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
# cache:
# directories:
@@ -59,13 +68,19 @@ before_install:
script:
- |
# Prints some env variables
echo "TRAVIS_OS_NAME=$TRAVIS_OS_NAME"
echo "TRAVIS_BRANCH=$TRAVIS_BRANCH"
echo "TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST"
echo "TRAVIS_TAG=$TRAVIS_TAG"
# Install tools
npm install
# Run test units.
# Only do it for pull requests because Travis randomly fails to run them
# and that would break the desktop release.
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
if [ "$TRAVIS_PULL_REQUEST" != "false" ] || [ "$TRAVIS_BRANCH" = "dev" ]; then
npm run test-ci
testResult=$?
if [ $testResult -ne 0 ]; then
@@ -107,5 +122,17 @@ script:
fi
# Prepare the Electron app and build it
#
# If the current tag is a desktop release tag (starts with "v", such as
# "v1.4.7"), we build and publish to github
#
# Otherwise we only build but don't publish to GitHub. It helps finding
# out any issue in pull requests and dev branch.
cd packages/app-desktop
USE_HARD_LINKS=false npm run dist
if [[ $TRAVIS_TAG = v* ]]; then
USE_HARD_LINKS=false npm run dist
else
USE_HARD_LINKS=false npm run dist -- --publish=never
fi

View File

@@ -91,7 +91,7 @@ Note that you should most likely always specify a scope because otherwise it wil
## TypeScript
The application was originally written JavaScript, however it has slowly been migrated to [TypeScript](https://www.typescriptlang.org/). New classes and files should be written in TypeScript. All compiled files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
The application was originally written in JavaScript, however it has slowly been migrated to [TypeScript](https://www.typescriptlang.org/). New classes and files should be written in TypeScript. All compiled files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
## Hot reload

View File

@@ -19,23 +19,23 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
## Desktop applications
Operating System | Download | Alternative
-----------------|--------|-------------------
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-Setup-1.3.18.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-1.3.18.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew (unsupported): `brew cask install joplin`
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-1.3.18.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package (unsupported) [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, and Mint; the desktop environments supported are GNOME, KDE, Xfce, MATE, LXQT, LXDE, Unity, Cinnamon, Deepin and Pantheon), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash`
---|---|---
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-Setup-1.4.18.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-1.4.18.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | -
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-1.4.18.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | The recommended way is to use the following installation script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh \| bash`
## Mobile applications
Operating System | Download | Alt. Download
-----------------|----------|----------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.3.13/joplin-v1.3.13.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.3.13/joplin-v1.3.13-32bit.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.4.11/joplin-v1.4.11.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.4.11/joplin-v1.4.11-32bit.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
## Terminal application
Operating system | Method
-----------------|----------------
macOS, Linux, or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 10+](https://nodejs.org/en/download/package-manager/).<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
macOS, Linux, or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 12+](https://nodejs.org/en/download/package-manager/).<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
To start it, type `joplin`.
@@ -64,7 +64,8 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
| :---: | :---: | :---: |
| <img width="50" src="https://avatars0.githubusercontent.com/u/6979755?s=96&v=4"/></br>[Devon Zuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[小西 孝宗](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[Alexander van der Berg](https://github.com/avanderberg)
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[Frank Bloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[Thomas Broussard](https://github.com/thomasbroussard)
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[Brandon Johnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars1.githubusercontent.com/u/3061769?s=96&v=4"/></br>[@cnagy](https://github.com/c-nagy) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[Brandon Johnson](https://github.com/dbrandonjohnson) | <img width="50" src="https://avatars1.githubusercontent.com/u/3061769?s=96&v=4"/></br>[@cnagy](https://github.com/c-nagy) | <img width="50" src="https://avatars3.githubusercontent.com/u/53228972?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl)
| <img width="50" src="https://avatars1.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp)
<!-- TOC -->
# Table of contents
@@ -402,13 +403,14 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Community
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplinapp.org/). It is possible to login with your GitHub account.
- Also see here for information about [the latest releases and general news](https://discourse.joplinapp.org/c/news).
- For bug reports go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues). Please follow the template accordingly.
- Feature requests must not be opened on GitHub unless they have been discussed and accepted on the forum.
- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).
- You can also follow us on <a rel="me" href="https://mastodon.social/@joplinapp">the Mastodon feed</a> or [the Twitter feed](https://twitter.com/joplinapp).
- You can join the live community on [the JoplinApp discord server](https://discordapp.com/invite/d2HMPwE) to get help with Joplin or to discuss anything Joplin related.
Name | Description
--- | ---
[Support Forum](https://discourse.joplinapp.org/) | This is the main place for general discussion about Joplin, user support, software development questions, and to discuss new features. Also where the latest beta versions are released and discussed.
[Sub-reddit](https://www.reddit.com/r/joplinapp/) | Also a good place to get help
[Discord server](https://discordapp.com/invite/d2HMPwE) | Our chat server
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
[Mastodon feed](https://mastodon.social/@joplinapp) | Follow us on Mastodon
[Twitter feed](https://twitter.com/joplinapp) | Follow us on Twitter
# Contributing

View File

@@ -418,10 +418,10 @@ https://github.com/laurent22/joplin/blob/dev/readme/api/overview.md
<li>Create a module to export or import data into Joplin</li>
<li>Define new settings and setting sections, and get/set them from the plugin</li>
<li>Create a new Markdown plugin to render custom markup.</li>
<li>Create an editor plugin to modify low-level the behaviour of the Markdown editor (CodeMirror)</li>
<li>Create an editor plugin to modify, at a low-level, the behaviour of the Markdown editor (CodeMirror)</li>
</ul>
<p>To get started with the plugin API, check the <a href="https://joplinapp.org/api/get_started/plugins/">Get Started</a> page or have a look at the <a href="https://joplinapp.org/api/tutorials/toc_plugin/">TOC tutorial</a>.</p>
<p>Once you are familiar with the API, you can have a look at the <a href="https://joplinapp.org/api/get_started/plugins/">plugin API reference</a> for a detailed documentation about each supported feature.</p>
<p>Once you are familiar with the API, you can have a look at the <a href="https://joplinapp.org/api/references/plugin_api/classes/joplin.html">plugin API reference</a> for a detailed documentation about each supported feature.</p>
<div class="bottom-links">
<a href="https://github.com/laurent22/joplin/blob/dev/readme/api/overview.md">

File diff suppressed because one or more lines are too long

View File

@@ -155,7 +155,8 @@
</div>
<p>Note that registering a content script in itself will do nothing - it will only be loaded in specific cases by the relevant app modules
(eg. the Markdown renderer or the code editor). So it is not a way to inject and run arbitrary code in the app, which for safety and performance reasons is not supported.</p>
<p><a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">View the demo plugin</a></p>
<p><a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script">View the renderer demo plugin</a>
<a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">View the editor demo plugin</a></p>
</div>
<h4 class="tsd-parameters-title">Parameters</h4>
<ul class="tsd-parameters">

View File

@@ -86,6 +86,41 @@
<div class="tsd-signature tsd-kind-icon">Code<wbr>Mirror<wbr>Plugin<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = &quot;codeMirrorPlugin&quot;</span></div>
<aside class="tsd-sources">
</aside>
<div class="tsd-comment tsd-typography">
<div class="lead">
<p>Registers a new CodeMirror plugin, which should follow the template below.</p>
</div>
<pre><code class="language-javascript"><span class="hljs-built_in">module</span>.exports = {
<span class="hljs-attr">default</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">context</span>) </span>{
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">plugin</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">CodeMirror</span>) </span>{
<span class="hljs-comment">// ...</span>
},
<span class="hljs-attr">codeMirrorResources</span>: [],
<span class="hljs-attr">codeMirrorOptions</span>: {
<span class="hljs-comment">// ...</span>
},
<span class="hljs-attr">assets</span>: {
<span class="hljs-comment">// ...</span>
},
}
}
}</code></pre>
<ul>
<li><p>The <code>context</code> argument is currently unused but could be used later on to provide access to your own plugin so that the content script and plugin can communicate.</p>
</li>
<li><p>The <code>plugin</code> key is your CodeMirror plugin. This is where you can register new commands with CodeMirror or interact with the CodeMirror instance as needed.</p>
</li>
<li><p>The <code>codeMirrorResources</code> key is an array of CodeMirror resources that will be loaded and attached to the CodeMirror module. These are made up of addons, keymaps, and modes. For example, for a plugin that want&#39;s to enable clojure highlighting in code blocks. <code>codeMirrorResources</code> would be set to <code>[&#39;mode/clojure/clojure&#39;]</code>.</p>
</li>
<li><p>The <code>codeMirrorOptions</code> key contains all the <a href="https://codemirror.net/doc/manual.html#config">CodeMirror</a> options that will be set or changed by this plugin. New options can alse be declared via <a href="https://codemirror.net/doc/manual.html#defineOption"><code>CodeMirror.defineOption</code></a>, and then have their value set here. For example, a plugin that enables line numbers would set <code>codeMirrorOptions</code> to <code>{&#39;lineNumbers&#39;: true}</code>.</p>
</li>
<li><p>Using the <strong>optional</strong> <code>assets</code> key you may specify <strong>only</strong> CSS assets that should be loaded in the rendered HTML document. Check for example the Joplin <a href="https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/lib/joplin-renderer/MdToHtml/rules/mermaid.ts">Mermaid plugin</a> to see how the data should be structured.</p>
</li>
</ul>
<p>One of the <code>plugin</code>, <code>codeMirrorResources</code>, or <code>codeMirrorOptions</code> keys must be provided for the plugin to be valid. Having multiple or all provided is also okay.</p>
<p>See the <a href="https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script">demo plugin</a> for an example of all these keys being used in one plugin.</p>
</div>
</section>
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
<a name="markdownitplugin" class="tsd-anchor"></a>

View File

@@ -121,6 +121,12 @@
<li class="tsd-kind-type-alias"><a href="globals.html#viewhandle" class="tsd-kind-icon">View<wbr>Handle</a></li>
</ul>
</section>
<section class="tsd-index-section ">
<h3>Variables</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-variable"><a href="globals.html#logger" class="tsd-kind-icon">logger</a></li>
</ul>
</section>
</div>
</section>
</section>
@@ -156,6 +162,16 @@
</aside>
</section>
</section>
<section class="tsd-panel-group tsd-member-group ">
<h2>Variables</h2>
<section class="tsd-panel tsd-member tsd-kind-variable">
<a name="logger" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagConst">Const</span> logger</h3>
<div class="tsd-signature tsd-kind-icon">logger<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">LoggerWrapper</span><span class="tsd-signature-symbol"> = Logger.create(&#x27;joplin.plugins&#x27;)</span></div>
<aside class="tsd-sources">
</aside>
</section>
</section>
</div>
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
<!--

View File

@@ -84,7 +84,6 @@
<h3>Properties</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="exportoptions.html#format" class="tsd-kind-icon">format</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="exportoptions.html#modulepath" class="tsd-kind-icon">module<wbr>Path</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="exportoptions.html#path" class="tsd-kind-icon">path</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="exportoptions.html#sourcefolderids" class="tsd-kind-icon">source<wbr>Folder<wbr>Ids</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="exportoptions.html#sourcenoteids" class="tsd-kind-icon">source<wbr>Note<wbr>Ids</a></li>
@@ -103,13 +102,6 @@
<aside class="tsd-sources">
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface">
<a name="modulepath" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagOptional">Optional</span> module<wbr>Path</h3>
<div class="tsd-signature tsd-kind-icon">module<wbr>Path<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span></div>
<aside class="tsd-sources">
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface">
<a name="path" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagOptional">Optional</span> path</h3>
@@ -238,9 +230,6 @@
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="exportoptions.html#format" class="tsd-kind-icon">format</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="exportoptions.html#modulepath" class="tsd-kind-icon">module<wbr>Path</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="exportoptions.html#path" class="tsd-kind-icon">path</a>
</li>

View File

@@ -399,6 +399,98 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog.md
<div class="main">
<h1>Joplin changelog<a name="joplin-changelog" href="#joplin-changelog" class="heading-anchor">🔗</a></h1>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.18">v1.4.18</a> - 2020-11-28T12:21:41Z<a name="v1-4-18-https-github-com-laurent22-joplin-releases-tag-v1-4-18-2020-11-28t12-21-41z" href="#v1-4-18-https-github-com-laurent22-joplin-releases-tag-v1-4-18-2020-11-28t12-21-41z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Fixed: Fixed notifications on macOS</li>
<li>Fixed: Re-enabled ASAR packing to improve startup time</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.16">v1.4.16</a> - 2020-11-27T19:40:16Z<a name="v1-4-16-https-github-com-laurent22-joplin-releases-tag-v1-4-16-2020-11-27t19-40-16z" href="#v1-4-16-https-github-com-laurent22-joplin-releases-tag-v1-4-16-2020-11-27t19-40-16z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Fixed: Fix sorting by title in a case insensitive way</li>
<li>Fixed: Fixed spell checker crash when no language is selected</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.15">v1.4.15</a> - 2020-11-27T13:25:43Z<a name="v1-4-15-https-github-com-laurent22-joplin-releases-tag-v1-4-15-2020-11-27t13-25-43z" href="#v1-4-15-https-github-com-laurent22-joplin-releases-tag-v1-4-15-2020-11-27t13-25-43z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Fixed: Notifications on macOS</li>
<li>Fixed: Fixed potential crash when watching note files or resources</li>
<li>Fixed: Prevents crash when invalid spell checker language is selected, and provide fallback for invalid language codes (<a href="https://github.com/laurent22/joplin/issues/4146">#4146</a>)</li>
<li>Plugins: Fixed webview postMessage call</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.12">v1.4.12</a> - 2020-11-23T18:58:07Z<a name="v1-4-12-https-github-com-laurent22-joplin-releases-tag-v1-4-12-2020-11-23t18-58-07z" href="#v1-4-12-https-github-com-laurent22-joplin-releases-tag-v1-4-12-2020-11-23t18-58-07z" class="heading-anchor">🔗</a></h2>
<p><strong>Breaking Changes:</strong></p>
<ul>
<li>If you use the Clipper API, please note that there are a few breaking changes in this version. See this link for more information: <a href="https://github.com/laurent22/joplin/pull/3983">https://github.com/laurent22/joplin/pull/3983</a></li>
<li>Plugins: <code>joplin.views.dialogs.open()</code> now returns an object instead of the button ID that was clicked. So for example instead of getting just <code>&quot;ok&quot;</code>, you will get <code>{ &quot;id&quot;: &quot;ok&quot; }</code>. This is to allow adding form data to that object.</li>
</ul>
<p><strong>Deprecated:</strong></p>
<p>The following features are deprecated. It will still work for now but please update your code:</p>
<ul>
<li>Plugins: All <code>create()</code> functions under <code>joplin.views</code> now take a <code>viewId</code> as a first parameter.</li>
<li>Plugins: <code>MenuItemLocation.Context</code> is deprecated and is now an alias for <code>MenuItemLocation.NoteListContextMenu</code></li>
<li>Plugins: The <code>app_min_version</code> manifest property is now required. If not provided it will assume v14.</li>
<li>Plugins: The <code>id</code> manifest property is now required. If not set, it will be the plugin filename or directory.</li>
</ul>
<p>Plugin doc has been updated with some info about the <a href="https://joplinapp.org/api/references/plugin_api/classes/joplin.html">development process</a>.</p>
<ul>
<li>New: Add {{bowm}} and {{bows}} - Beginning Of Week (Monday/Sunday) (<a href="https://github.com/laurent22/joplin/issues/4023">#4023</a> by Helmut K. C. Tessarek)</li>
<li>New: Add config screen to add, remove or enable, disable plugins</li>
<li>New: Add option to toggle spellchecking for the markdown editor (<a href="https://github.com/laurent22/joplin/issues/4109">#4109</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>New: Added toolbar button to switch spell checker language</li>
<li>New: Adds spell checker support for Rich Text editor (<a href="https://github.com/laurent22/joplin/issues/3974">#3974</a>)</li>
<li>New: Allow customising application layout</li>
<li>New: Api: Added ability to watch resource file</li>
<li>New: Api: Added way to get the notes associated with a resource</li>
<li>New: API: Adds ability to paginate data (<a href="https://github.com/laurent22/joplin/issues/3983">#3983</a>)</li>
<li>New: Plugins: Add command &quot;editorSetText&quot; for desktop app</li>
<li>New: Plugins: Add support for editor context menu</li>
<li>New: Plugins: Add support for external CodeMirror plugins (<a href="https://github.com/laurent22/joplin/issues/4015">#4015</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>New: Plugins: Add support for JPL archive format</li>
<li>New: Plugins: Added command to export folders and notes</li>
<li>New: Plugins: Added support app_min_version property and made it required</li>
<li>Fixed: Api: Fix note and resource association end points</li>
<li>Fixed: Display note count for conflict folder, and display notes even if they are completed to-dos (<a href="https://github.com/laurent22/joplin/issues/3997">#3997</a>)</li>
<li>Fixed: Fix crash due to React when trying to upgrade sync target (<a href="https://github.com/laurent22/joplin/issues/4098">#4098</a>)</li>
<li>Fixed: Fix drag and drop behaviour to &quot;copy&quot; instead of &quot;move&quot; (<a href="https://github.com/laurent22/joplin/issues/4031">#4031</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>Fixed: Fix handling of certain keys in shortcut editor (<a href="https://github.com/laurent22/joplin/issues/4022">#4022</a> by Helmut K. C. Tessarek)</li>
<li>Fixed: Fix handling of new line escaping when using external edit</li>
<li>Fixed: Fix size of search bar area when notebook is empty</li>
<li>Fixed: Fixed importing certain ENEX files that contain invalid dates</li>
<li>Fixed: Fixed inconsistent note list state when using search (<a href="https://github.com/laurent22/joplin/issues/3904">#3904</a>)</li>
<li>Fixed: Fixed issue when a newly created note would be automatically moved to the wrong folder on save (<a href="https://github.com/laurent22/joplin/issues/4038">#4038</a>)</li>
<li>Fixed: Fixed issue with note being saved after word has been replaced by spell checker</li>
<li>Fixed: Fixed links imported from ENEX as HTML (<a href="https://github.com/laurent22/joplin/issues/4119">#4119</a>)</li>
<li>Fixed: Fixed Markdown rendering when code highlighting is disabled</li>
<li>Fixed: Fixed note list overflow when resized very small</li>
<li>Fixed: Fixed text editor button tooltips</li>
<li>Fixed: Plugins: Fix crash when path includes trailing slash</li>
<li>Fixed: Plugins: Fixed issue with dialog being empty in some cases</li>
<li>Fixed: Plugins: Fixed issue with toolbar button key not being unique</li>
<li>Fixed: Prevent log from filling up when certain external editors trigger many watch events (<a href="https://github.com/laurent22/joplin/issues/4011">#4011</a>)</li>
<li>Fixed: Regression: Fix application name</li>
<li>Fixed: Regression: Fix exporting to HTML and PDF</li>
<li>Fixed: Regression: Fixed external edit file watching</li>
<li>Fixed: Resource links could not be opened from Rich Text editor on Linux (<a href="https://github.com/laurent22/joplin/issues/4073">#4073</a>)</li>
<li>Fixed: Tags could not be selected in some cases (<a href="https://github.com/laurent22/joplin/issues/3876">#3876</a>)</li>
<li>Improved: Allow exporting conflict notes (<a href="https://github.com/laurent22/joplin/issues/4095">#4095</a>)</li>
<li>Improved: Allow lowercase filters when doing search</li>
<li>Improved: Api: Always include 'has_more' field for paginated data</li>
<li>Improved: Api: Make sure pagination sort options are respected for search and other requests</li>
<li>Improved: Attempt to fix Outlook drag and drop on Markdown editor (<a href="https://github.com/laurent22/joplin/issues/4093">#4093</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>Improved: Change Markdown rendering to align with CommonMark spec (<a href="https://github.com/laurent22/joplin/issues/3839">#3839</a>)</li>
<li>Improved: Disable spell checker on config and search input fields</li>
<li>Improved: Disabled the auto update option in linux (<a href="https://github.com/laurent22/joplin/issues/4102">#4102</a>) (<a href="https://github.com/laurent22/joplin/issues/4096">#4096</a> by Anshuman Pandey)</li>
<li>Improved: Make Markdown editor selection more visible in Dark mode</li>
<li>Improved: Optimized resizing window</li>
<li>Improved: Plugins: Allow retrieving form values from dialogs</li>
<li>Improved: Plugins: Force plugin devtool dialog to be detached</li>
<li>Improved: Plugins: Make sure &quot;replaceSelection&quot; command can be undone in Rich Text editor</li>
<li>Improved: Plugins: Provides selected notes when triggering a command from the note list context menu</li>
<li>Improved: Plugins: Rename command &quot;editorSetText&quot; to &quot;editor.setText&quot;</li>
<li>Improved: Prevent lines from shifting in Markdown Editor when Scrollbar appears (<a href="https://github.com/laurent22/joplin/issues/4110">#4110</a> by <a href="https://github.com/CalebJohn">@CalebJohn</a>)</li>
<li>Improved: Put title bar and toolbar button over two lines when window size is below 800px</li>
<li>Improved: Refresh sidebar and notes when moving note outside of conflict folder</li>
<li>Improved: Upgrade to Electron 10</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/v1.3.18">v1.3.18</a> - 2020-11-06T12:07:02Z<a name="v1-3-18-https-github-com-laurent22-joplin-releases-tag-v1-3-18-2020-11-06t12-07-02z" href="#v1-3-18-https-github-com-laurent22-joplin-releases-tag-v1-3-18-2020-11-06t12-07-02z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Regression: Random crash when syncing due to undefined tags (<a href="https://github.com/laurent22/joplin/issues/4051">#4051</a>)</li>

View File

@@ -399,6 +399,14 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog_cli.md
<div class="main">
<h1>Joplin terminal app changelog<a name="joplin-terminal-app-changelog" href="#joplin-terminal-app-changelog" class="heading-anchor">🔗</a></h1>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.4.9">cli-v1.4.9</a> - 2020-11-26T15:00:37Z<a name="cli-v1-4-9-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-9-2020-11-26t15-00-37z" href="#cli-v1-4-9-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-9-2020-11-26t15-00-37z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Improved: Allow exporting conflict notes (#4095)</li>
<li>Improved: Allow lowercase filters when doing search</li>
<li>Improved: Refresh sidebar and notes when moving note outside of conflict folder</li>
<li>Fixed: Fix handling of new line escaping when using external edit</li>
<li>Fixed: Fixed importing certain ENEX files that contain invalid dates</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.4.3">cli-v1.4.3</a> - 2020-11-06T21:19:29Z<a name="cli-v1-4-3-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-3-2020-11-06t21-19-29z" href="#cli-v1-4-3-https-github-com-laurent22-joplin-releases-tag-cli-v1-4-3-2020-11-06t21-19-29z" class="heading-anchor">🔗</a></h2>
<p>IMPORTANT: If you use the web API, please note that there are a few breaking changes in this release. See here for more information: <a href="https://github.com/laurent22/joplin/pull/3983#issue-509624899">https://github.com/laurent22/joplin/pull/3983#issue-509624899</a></p>
<ul>

View File

@@ -400,21 +400,39 @@ https://github.com/laurent22/joplin/blob/dev/readme/donate.md
<div class="main">
<h1>Support Joplin development<a name="support-joplin-development" href="#support-joplin-development" class="heading-anchor">🔗</a></h1>
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standards.</p>
<h2>PayPal<a name="paypal" href="#paypal" class="heading-anchor">🔗</a></h2>
<p>To donate via PayPal, please follow this link:</p>
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=E8JMYD2LQ8MMA&amp;lc=GB&amp;item_name=Joplin+Development&amp;currency_code=EUR&amp;bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate on PayPal"></a></p>
<h2>GitHub Sponsor<a name="github-sponsor" href="#github-sponsor" class="heading-anchor">🔗</a></h2>
<p>Or follow this link to become a GitHub Sponsor:</p>
<p><a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a></p>
<h2>Patreon<a name="patreon" href="#patreon" class="heading-anchor">🔗</a></h2>
<p>Alternatively you may support the project on Patreon:</p>
<p><a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a></p>
<h2>Donations<a name="donations" href="#donations" class="heading-anchor">🔗</a></h2>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Link</th>
</tr>
</thead>
<tbody>
<tr>
<td>Paypal</td>
<td><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=E8JMYD2LQ8MMA&amp;lc=GB&amp;item_name=Joplin+Development&amp;currency_code=EUR&amp;bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate on PayPal"></a></td>
</tr>
<tr>
<td>GitHub Sponsor</td>
<td><a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a></td>
</tr>
<tr>
<td>Patreon</td>
<td><a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a></td>
</tr>
<tr>
<td>Bank Transfer</td>
<td><strong>IBAN:</strong> FR76 4061 8803 5200 0400 7415 938<br><strong>BIC/SWIFT:</strong> BOUS FRPP XXX</td>
</tr>
</tbody>
</table>
<h2>Other way to support the development<a name="other-way-to-support-the-development" href="#other-way-to-support-the-development" class="heading-anchor">🔗</a></h2>
<p>Finally, there are other ways to support the development of Joplin:</p>
<ul>
<li>Consider rating the app on <a href="https://play.google.com/store/apps/details?id=net.cozic.joplin&amp;utm_source=GitHub&amp;utm_campaign=README&amp;pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1">Google Play</a> or <a href="https://itunes.apple.com/us/app/joplin/id1315599797">App Store</a>.</li>
<li><a href="https://joplinapp.org/#localisation">Create or update a translation</a>.</li>
<li>Vote for or review the app on <a href="https://alternativeto.net/software/joplin/">alternativeTo</a> or <a href="https://www.producthunt.com/posts/joplin">Product Hunt</a>.</li>
<li><a href="https://joplinapp.org/#localisation">Create or update a translation</a>.</li>
</ul>
<div class="bottom-links">

View File

@@ -419,17 +419,17 @@ https://github.com/laurent22/joplin/blob/dev/README.md
<tbody>
<tr>
<td>Windows (32 and 64-bit)</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-Setup-1.3.18.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
<td>Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/JoplinPortable.exe'>Portable version</a><br><br>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called &quot;JoplinProfile&quot; next to the executable file.</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-Setup-1.4.18.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
<td>Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/JoplinPortable.exe'>Portable version</a><br><br>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called &quot;JoplinProfile&quot; next to the executable file.</td>
</tr>
<tr>
<td>macOS</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-1.3.18.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-1.4.18.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
<td>You can also use Homebrew (unsupported): <code>brew cask install joplin</code></td>
</tr>
<tr>
<td>Linux</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.3.18/Joplin-1.3.18.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.4.18/Joplin-1.4.18.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
<td>An Arch Linux package (unsupported) <a href="#terminal-application">is also available</a>.<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, and Mint; the desktop environments supported are GNOME, KDE, Xfce, MATE, LXQT, LXDE, Unity, Cinnamon, Deepin and Pantheon), the recommended way is to use this script as it will handle the desktop icon too:<br><br> <code>wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash</code></td>
</tr>
</tbody>
@@ -447,7 +447,7 @@ https://github.com/laurent22/joplin/blob/dev/README.md
<tr>
<td>Android</td>
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a></td>
<td>or download the APK file: <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.3.13/joplin-v1.3.13.apk">64-bit</a> <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.3.13/joplin-v1.3.13-32bit.apk">32-bit</a></td>
<td>or download the APK file: <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.4.11/joplin-v1.4.11.apk">64-bit</a> <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.4.11/joplin-v1.4.11-32bit.apk">32-bit</a></td>
</tr>
<tr>
<td>iOS</td>
@@ -856,15 +856,40 @@ Eg. <code>:search -- &quot;-tag:tag1&quot;</code>.</p>
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.</p>
<p>Please see the <a href="https://joplinapp.org/donate/">donation page</a> for information on how to support the development of Joplin.</p>
<h1>Community<a name="community" href="#community" class="heading-anchor">🔗</a></h1>
<ul>
<li>For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the <a href="https://discourse.joplinapp.org/">Joplin Forum</a>. It is possible to login with your GitHub account.</li>
<li>Also see here for information about <a href="https://discourse.joplinapp.org/c/news">the latest releases and general news</a>.</li>
<li>For bug reports go to the <a href="https://github.com/laurent22/joplin/issues">GitHub Issue Tracker</a>. Please follow the template accordingly.</li>
<li>Feature requests must not be opened on GitHub unless they have been discussed and accepted on the forum.</li>
<li>The latest news are posted <a href="https://www.patreon.com/joplin">on the Patreon page</a>.</li>
<li>You can also follow us on <a rel="me" href="https://mastodon.social/@joplinapp">the Mastodon feed</a> or <a href="https://twitter.com/joplinapp">the Twitter feed</a>.</li>
<li>You can join the live community on <a href="https://discordapp.com/invite/d2HMPwE">the JoplinApp discord server</a> to get help with Joplin or to discuss anything Joplin related.</li>
</ul>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://discourse.joplinapp.org/">Support Forum</a></td>
<td>This is the main place for general discussion about Joplin, user support, software development questions, and to discuss new features. Also where the latest beta versions are released and discussed.</td>
</tr>
<tr>
<td><a href="https://www.reddit.com/r/joplinapp/">Sub-reddit</a></td>
<td>Also a good place to get help</td>
</tr>
<tr>
<td><a href="https://discordapp.com/invite/d2HMPwE">Discord server</a></td>
<td>Our chat server</td>
</tr>
<tr>
<td><a href="https://www.patreon.com/joplin">Patreon page</a></td>
<td>The latest news are often posted there</td>
</tr>
<tr>
<td><a href="https://mastodon.social/@joplinapp">Mastodon feed</a></td>
<td>Follow us on Mastodon</td>
</tr>
<tr>
<td><a href="https://twitter.com/joplinapp">Twitter feed</a></td>
<td>Follow us on Twitter</td>
</tr>
</tbody>
</table>
<h1>Contributing<a name="contributing" href="#contributing" class="heading-anchor">🔗</a></h1>
<p>Please see the guide for information on how to contribute to the development of Joplin: <a href="https://github.com/laurent22/joplin/blob/dev/CONTRIBUTING.md">https://github.com/laurent22/joplin/blob/dev/CONTRIBUTING.md</a></p>
<h1>Localisation<a name="localisation" href="#localisation" class="heading-anchor">🔗</a></h1>

View File

@@ -409,15 +409,15 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<tbody>
<tr>
<td>Total Windows downloads</td>
<td>1,096,772</td>
<td>1,121,074</td>
</tr>
<tr>
<td>Total macOs downloads</td>
<td>425,802</td>
<td>434,816</td>
</tr>
<tr>
<td>Total Linux downloads</td>
<td>310,292</td>
<td>318,581</td>
</tr>
<tr>
<td>Windows %</td>
@@ -446,116 +446,148 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
</thead>
<tbody>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.18">v1.4.18</a></td>
<td>2020-11-28T12:21:41Z</td>
<td>3,390</td>
<td>1,584</td>
<td>1,141</td>
<td>6,115</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.16">v1.4.16</a></td>
<td>2020-11-27T19:40:16Z</td>
<td>1,333</td>
<td>798</td>
<td>573</td>
<td>2,704</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.15">v1.4.15</a></td>
<td>2020-11-27T13:25:43Z</td>
<td>808</td>
<td>465</td>
<td>248</td>
<td>1,521</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.4.12">v1.4.12</a></td>
<td>2020-11-23T18:58:07Z</td>
<td>2,807</td>
<td>1,278</td>
<td>1,256</td>
<td>5,341</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.3.18">v1.3.18</a></td>
<td>2020-11-06T12:07:02Z</td>
<td>14,764</td>
<td>6,490</td>
<td>5,476</td>
<td>26,730</td>
<td>29,861</td>
<td>11,217</td>
<td>10,451</td>
<td>51,529</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.3.15">v1.3.15</a></td>
<td>2020-11-04T12:22:50Z</td>
<td>2,133</td>
<td>1,260</td>
<td>824</td>
<td>4,217</td>
<td>2,155</td>
<td>1,270</td>
<td>826</td>
<td>4,251</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.2.6">v1.2.6</a></td>
<td>2020-10-09T13:56:59Z</td>
<td>43,408</td>
<td>17,645</td>
<td>13,992</td>
<td>75,045</td>
<td>43,519</td>
<td>17,667</td>
<td>14,004</td>
<td>75,190</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.1.4">v1.1.4</a></td>
<td>2020-09-21T11:20:09Z</td>
<td>27,403</td>
<td>13,458</td>
<td>7,694</td>
<td>48,555</td>
<td>27,413</td>
<td>13,460</td>
<td>7,696</td>
<td>48,569</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.245">v1.0.245</a></td>
<td>2020-09-09T12:56:10Z</td>
<td>20,771</td>
<td>9,961</td>
<td>5,617</td>
<td>36,349</td>
<td>20,791</td>
<td>9,964</td>
<td>5,618</td>
<td>36,373</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.242">v1.0.242</a></td>
<td>2020-09-04T22:00:34Z</td>
<td>12,301</td>
<td>6,394</td>
<td>12,309</td>
<td>6,396</td>
<td>3,007</td>
<td>21,702</td>
<td>21,712</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.241">v1.0.241</a></td>
<td>2020-09-04T18:06:00Z</td>
<td>23,069</td>
<td>5,684</td>
<td>4,959</td>
<td>33,712</td>
<td>23,078</td>
<td>5,689</td>
<td>4,961</td>
<td>33,728</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.233">v1.0.233</a></td>
<td>2020-08-01T14:51:15Z</td>
<td>42,195</td>
<td>18,151</td>
<td>42,344</td>
<td>18,156</td>
<td>12,344</td>
<td>72,690</td>
<td>72,844</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.227">v1.0.227</a></td>
<td>2020-07-07T20:44:54Z</td>
<td>40,135</td>
<td>15,235</td>
<td>9,610</td>
<td>64,980</td>
<td>40,163</td>
<td>15,240</td>
<td>9,611</td>
<td>65,014</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.224">v1.0.224</a></td>
<td>2020-06-20T22:26:08Z</td>
<td>24,705</td>
<td>10,977</td>
<td>24,711</td>
<td>10,980</td>
<td>5,999</td>
<td>41,681</td>
<td>41,690</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.220">v1.0.220</a></td>
<td>2020-06-13T18:26:22Z</td>
<td>31,510</td>
<td>9,887</td>
<td>31,522</td>
<td>9,890</td>
<td>6,407</td>
<td>47,804</td>
<td>47,819</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.218">v1.0.218</a></td>
<td>2020-06-07T10:43:34Z</td>
<td>14,488</td>
<td>6,946</td>
<td>14,498</td>
<td>6,948</td>
<td>2,950</td>
<td>24,384</td>
<td>24,396</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.216">v1.0.216</a></td>
<td>2020-05-24T14:21:01Z</td>
<td>36,371</td>
<td>14,235</td>
<td>36,541</td>
<td>14,240</td>
<td>10,169</td>
<td>60,775</td>
<td>60,950</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.201">v1.0.201</a></td>
<td>2020-04-15T22:55:13Z</td>
<td>52,378</td>
<td>20,032</td>
<td>18,167</td>
<td>90,577</td>
<td>52,539</td>
<td>20,035</td>
<td>18,168</td>
<td>90,742</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.200">v1.0.200</a></td>
@@ -568,106 +600,106 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.199">v1.0.199</a></td>
<td>2020-04-10T18:41:58Z</td>
<td>19,239</td>
<td>19,248</td>
<td>5,878</td>
<td>3,783</td>
<td>28,900</td>
<td>28,909</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.197">v1.0.197</a></td>
<td>2020-03-30T17:21:22Z</td>
<td>22,062</td>
<td>9,506</td>
<td>5,610</td>
<td>37,178</td>
<td>22,073</td>
<td>9,507</td>
<td>5,618</td>
<td>37,198</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.195">v1.0.195</a></td>
<td>2020-03-22T19:56:12Z</td>
<td>18,860</td>
<td>18,867</td>
<td>7,942</td>
<td>4,502</td>
<td>31,304</td>
<td>31,311</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.193">v1.0.193</a></td>
<td>2020-03-08T08:58:53Z</td>
<td>28,595</td>
<td>28,598</td>
<td>10,895</td>
<td>7,356</td>
<td>46,846</td>
<td>46,849</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.179">v1.0.179</a></td>
<td>2020-01-24T22:42:41Z</td>
<td>70,959</td>
<td>28,467</td>
<td>22,491</td>
<td>121,917</td>
<td>70,962</td>
<td>28,472</td>
<td>22,494</td>
<td>121,928</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.178">v1.0.178</a></td>
<td>2020-01-20T19:06:45Z</td>
<td>17,526</td>
<td>17,527</td>
<td>5,956</td>
<td>2,579</td>
<td>26,061</td>
<td>26,062</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.175">v1.0.175</a></td>
<td>2019-12-08T11:48:47Z</td>
<td>71,981</td>
<td>16,855</td>
<td>16,478</td>
<td>105,314</td>
<td>72,026</td>
<td>16,856</td>
<td>16,481</td>
<td>105,363</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.174">v1.0.174</a></td>
<td>2019-11-12T18:20:58Z</td>
<td>30,388</td>
<td>11,688</td>
<td>8,216</td>
<td>50,292</td>
<td>30,390</td>
<td>11,689</td>
<td>8,217</td>
<td>50,296</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.173">v1.0.173</a></td>
<td>2019-11-11T08:33:35Z</td>
<td>5,058</td>
<td>2,071</td>
<td>5,060</td>
<td>2,072</td>
<td>740</td>
<td>7,869</td>
<td>7,872</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.170">v1.0.170</a></td>
<td>2019-10-13T22:13:04Z</td>
<td>27,372</td>
<td>8,737</td>
<td>27,375</td>
<td>8,739</td>
<td>7,668</td>
<td>43,777</td>
<td>43,782</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.169">v1.0.169</a></td>
<td>2019-09-27T18:35:13Z</td>
<td>17,081</td>
<td>5,915</td>
<td>17,083</td>
<td>5,916</td>
<td>3,750</td>
<td>26,746</td>
<td>26,749</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.168">v1.0.168</a></td>
<td>2019-09-25T21:21:38Z</td>
<td>5,323</td>
<td>2,265</td>
<td>2,267</td>
<td>714</td>
<td>8,302</td>
<td>8,304</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.167">v1.0.167</a></td>
<td>2019-09-10T08:48:37Z</td>
<td>16,779</td>
<td>5,699</td>
<td>3,700</td>
<td>26,178</td>
<td>3,701</td>
<td>26,179</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.166">v1.0.166</a></td>
@@ -680,370 +712,370 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.165">v1.0.165</a></td>
<td>2019-08-14T21:46:29Z</td>
<td>18,875</td>
<td>18,876</td>
<td>6,968</td>
<td>5,460</td>
<td>31,303</td>
<td>31,304</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.161">v1.0.161</a></td>
<td>2019-07-13T18:30:00Z</td>
<td>19,272</td>
<td>19,274</td>
<td>6,348</td>
<td>4,133</td>
<td>29,753</td>
<td>29,755</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.160">v1.0.160</a></td>
<td>2019-06-15T00:21:40Z</td>
<td>30,459</td>
<td>30,463</td>
<td>7,742</td>
<td>8,098</td>
<td>46,299</td>
<td>8,099</td>
<td>46,304</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.159">v1.0.159</a></td>
<td>2019-06-08T00:00:19Z</td>
<td>5,189</td>
<td>2,174</td>
<td>5,190</td>
<td>2,175</td>
<td>1,105</td>
<td>8,468</td>
<td>8,470</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.158">v1.0.158</a></td>
<td>2019-05-27T19:01:18Z</td>
<td>9,809</td>
<td>3,534</td>
<td>9,810</td>
<td>3,536</td>
<td>1,934</td>
<td>15,277</td>
<td>15,280</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.157">v1.0.157</a></td>
<td>2019-05-26T17:55:53Z</td>
<td>2,173</td>
<td>841</td>
<td>842</td>
<td>289</td>
<td>3,303</td>
<td>3,304</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.152">v1.0.152</a></td>
<td>2019-05-13T09:08:07Z</td>
<td>13,861</td>
<td>4,423</td>
<td>13,862</td>
<td>4,424</td>
<td>4,060</td>
<td>22,344</td>
<td>22,346</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.151">v1.0.151</a></td>
<td>2019-05-12T15:14:32Z</td>
<td>1,948</td>
<td>1,950</td>
<td>530</td>
<td>955</td>
<td>3,433</td>
<td>956</td>
<td>3,436</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.150">v1.0.150</a></td>
<td>2019-05-12T11:27:48Z</td>
<td>418</td>
<td>129</td>
<td>66</td>
<td>613</td>
<td>67</td>
<td>614</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.145">v1.0.145</a></td>
<td>2019-05-03T09:16:53Z</td>
<td>7,000</td>
<td>2,858</td>
<td>7,003</td>
<td>2,859</td>
<td>1,434</td>
<td>11,292</td>
<td>11,296</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.143">v1.0.143</a></td>
<td>2019-04-22T10:51:38Z</td>
<td>11,911</td>
<td>3,546</td>
<td>2,776</td>
<td>18,233</td>
<td>11,912</td>
<td>3,548</td>
<td>2,777</td>
<td>18,237</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.142">v1.0.142</a></td>
<td>2019-04-02T16:44:51Z</td>
<td>14,649</td>
<td>4,557</td>
<td>14,650</td>
<td>4,558</td>
<td>4,724</td>
<td>23,930</td>
<td>23,932</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.140">v1.0.140</a></td>
<td>2019-03-10T20:59:58Z</td>
<td>13,622</td>
<td>4,166</td>
<td>3,172</td>
<td>20,960</td>
<td>3,178</td>
<td>20,966</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.135">v1.0.135</a></td>
<td>2019-02-27T23:36:57Z</td>
<td>12,484</td>
<td>3,953</td>
<td>4,073</td>
<td>20,510</td>
<td>12,486</td>
<td>3,954</td>
<td>4,074</td>
<td>20,514</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.134">v1.0.134</a></td>
<td>2019-02-27T10:21:44Z</td>
<td>1,463</td>
<td>563</td>
<td>1,464</td>
<td>564</td>
<td>217</td>
<td>2,243</td>
<td>2,245</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.132">v1.0.132</a></td>
<td>2019-02-26T23:02:05Z</td>
<td>1,081</td>
<td>447</td>
<td>93</td>
<td>1,621</td>
<td>448</td>
<td>94</td>
<td>1,623</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.127">v1.0.127</a></td>
<td>2019-02-14T23:12:48Z</td>
<td>9,734</td>
<td>3,164</td>
<td>9,741</td>
<td>3,165</td>
<td>2,928</td>
<td>15,826</td>
<td>15,834</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.125">v1.0.125</a></td>
<td>2019-01-26T18:14:33Z</td>
<td>10,245</td>
<td>3,552</td>
<td>10,246</td>
<td>3,554</td>
<td>1,701</td>
<td>15,498</td>
<td>15,501</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.120">v1.0.120</a></td>
<td>2019-01-10T21:42:53Z</td>
<td>15,598</td>
<td>5,196</td>
<td>6,512</td>
<td>27,306</td>
<td>15,599</td>
<td>5,197</td>
<td>6,514</td>
<td>27,310</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.119">v1.0.119</a></td>
<td>2018-12-18T12:40:22Z</td>
<td>8,902</td>
<td>3,257</td>
<td>3,259</td>
<td>2,013</td>
<td>14,172</td>
<td>14,174</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.118">v1.0.118</a></td>
<td>2019-01-11T08:34:13Z</td>
<td>713</td>
<td>244</td>
<td>87</td>
<td>1,044</td>
<td>246</td>
<td>88</td>
<td>1,047</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.117">v1.0.117</a></td>
<td>2018-11-24T12:05:24Z</td>
<td>16,252</td>
<td>4,889</td>
<td>4,892</td>
<td>6,379</td>
<td>27,520</td>
<td>27,523</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.116">v1.0.116</a></td>
<td>2018-11-20T19:09:24Z</td>
<td>3,468</td>
<td>1,117</td>
<td>3,469</td>
<td>1,119</td>
<td>712</td>
<td>5,297</td>
<td>5,300</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.115">v1.0.115</a></td>
<td>2018-11-16T16:52:02Z</td>
<td>3,652</td>
<td>1,299</td>
<td>3,653</td>
<td>1,300</td>
<td>797</td>
<td>5,748</td>
<td>5,750</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.114">v1.0.114</a></td>
<td>2018-10-24T20:14:10Z</td>
<td>11,393</td>
<td>3,492</td>
<td>3,494</td>
<td>3,828</td>
<td>18,713</td>
<td>18,715</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.111">v1.0.111</a></td>
<td>2018-09-30T20:15:09Z</td>
<td>12,007</td>
<td>3,286</td>
<td>3,663</td>
<td>18,956</td>
<td>12,008</td>
<td>3,290</td>
<td>3,667</td>
<td>18,965</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.110">v1.0.110</a></td>
<td>2018-09-29T12:29:21Z</td>
<td>956</td>
<td>405</td>
<td>116</td>
<td>1,477</td>
<td>407</td>
<td>117</td>
<td>1,480</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.109">v1.0.109</a></td>
<td>2018-09-27T18:01:41Z</td>
<td>2,096</td>
<td>700</td>
<td>2,098</td>
<td>703</td>
<td>326</td>
<td>3,122</td>
<td>3,127</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a></td>
<td>2018-09-16T19:51:07Z</td>
<td>7,145</td>
<td>2,132</td>
<td>7,146</td>
<td>2,134</td>
<td>1,705</td>
<td>10,982</td>
<td>10,985</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.106">v1.0.106</a></td>
<td>2018-09-08T15:23:40Z</td>
<td>4,553</td>
<td>1,453</td>
<td>4,554</td>
<td>1,455</td>
<td>316</td>
<td>6,322</td>
<td>6,325</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a></td>
<td>2018-09-05T11:29:36Z</td>
<td>4,652</td>
<td>1,585</td>
<td>1,587</td>
<td>1,453</td>
<td>7,690</td>
<td>7,692</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.104">v1.0.104</a></td>
<td>2018-06-28T20:25:36Z</td>
<td>15,038</td>
<td>4,696</td>
<td>7,327</td>
<td>27,061</td>
<td>15,043</td>
<td>4,698</td>
<td>7,328</td>
<td>27,069</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.103">v1.0.103</a></td>
<td>2018-06-21T19:38:13Z</td>
<td>2,049</td>
<td>882</td>
<td>2,050</td>
<td>883</td>
<td>679</td>
<td>3,610</td>
<td>3,612</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.101">v1.0.101</a></td>
<td>2018-06-17T18:35:11Z</td>
<td>1,304</td>
<td>604</td>
<td>407</td>
<td>2,315</td>
<td>1,306</td>
<td>606</td>
<td>408</td>
<td>2,320</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.100">v1.0.100</a></td>
<td>2018-06-14T17:41:43Z</td>
<td>875</td>
<td>429</td>
<td>239</td>
<td>1,543</td>
<td>877</td>
<td>431</td>
<td>240</td>
<td>1,548</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.99">v1.0.99</a></td>
<td>2018-06-10T13:18:23Z</td>
<td>1,250</td>
<td>593</td>
<td>1,251</td>
<td>594</td>
<td>379</td>
<td>2,222</td>
<td>2,224</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.97">v1.0.97</a></td>
<td>2018-06-09T19:23:34Z</td>
<td>309</td>
<td>154</td>
<td>59</td>
<td>522</td>
<td>156</td>
<td>60</td>
<td>525</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.96">v1.0.96</a></td>
<td>2018-05-26T16:36:39Z</td>
<td>2,715</td>
<td>1,221</td>
<td>1,606</td>
<td>5,542</td>
<td>1,222</td>
<td>1,617</td>
<td>5,554</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.95">v1.0.95</a></td>
<td>2018-05-25T13:04:30Z</td>
<td>415</td>
<td>215</td>
<td>116</td>
<td>746</td>
<td>217</td>
<td>118</td>
<td>750</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.94">v1.0.94</a></td>
<td>2018-05-21T20:52:59Z</td>
<td>1,128</td>
<td>580</td>
<td>582</td>
<td>395</td>
<td>2,103</td>
<td>2,105</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.93">v1.0.93</a></td>
<td>2018-05-14T11:36:01Z</td>
<td>1,786</td>
<td>1,081</td>
<td>755</td>
<td>3,622</td>
<td>1,787</td>
<td>1,090</td>
<td>756</td>
<td>3,633</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.91">v1.0.91</a></td>
<td>2018-05-10T14:48:04Z</td>
<td>824</td>
<td>547</td>
<td>303</td>
<td>1,674</td>
<td>825</td>
<td>548</td>
<td>304</td>
<td>1,677</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.89">v1.0.89</a></td>
<td>2018-05-09T13:05:05Z</td>
<td>488</td>
<td>227</td>
<td>489</td>
<td>228</td>
<td>107</td>
<td>822</td>
<td>824</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.85">v1.0.85</a></td>
<td>2018-05-01T21:08:24Z</td>
<td>1,647</td>
<td>946</td>
<td>627</td>
<td>3,220</td>
<td>1,648</td>
<td>948</td>
<td>628</td>
<td>3,224</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.83">v1.0.83</a></td>
<td>2018-04-04T19:43:58Z</td>
<td>4,809</td>
<td>4,814</td>
<td>2,529</td>
<td>2,656</td>
<td>9,994</td>
<td>9,999</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.82">v1.0.82</a></td>
<td>2018-03-31T19:16:31Z</td>
<td>692</td>
<td>400</td>
<td>401</td>
<td>119</td>
<td>1,211</td>
<td>1,212</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.81">v1.0.81</a></td>
@@ -1072,10 +1104,10 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.77">v1.0.77</a></td>
<td>2018-03-16T15:12:35Z</td>
<td>176</td>
<td>177</td>
<td>103</td>
<td>44</td>
<td>323</td>
<td>324</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.72">v1.0.72</a></td>
@@ -1090,8 +1122,8 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<td>2018-02-28T20:04:30Z</td>
<td>1,853</td>
<td>1,049</td>
<td>1,251</td>
<td>4,153</td>
<td>1,252</td>
<td>4,154</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.67">v1.0.67</a></td>
@@ -1113,9 +1145,9 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.65">v1.0.65</a></td>
<td>2018-02-17T20:02:25Z</td>
<td>193</td>
<td>124</td>
<td>126</td>
<td>133</td>
<td>450</td>
<td>452</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.64">v1.0.64</a></td>
@@ -1146,8 +1178,8 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<td>2018-02-08T18:27:39Z</td>
<td>971</td>
<td>630</td>
<td>958</td>
<td>2,559</td>
<td>960</td>
<td>2,561</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.60">v0.10.60</a></td>
@@ -1162,8 +1194,8 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<td>2018-01-31T20:21:30Z</td>
<td>1,819</td>
<td>1,458</td>
<td>321</td>
<td>3,598</td>
<td>322</td>
<td>3,599</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.52">v0.10.52</a></td>
@@ -1224,10 +1256,10 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.39">v0.10.39</a></td>
<td>2017-12-11T21:19:44Z</td>
<td>5,784</td>
<td>4,258</td>
<td>3,160</td>
<td>13,202</td>
<td>5,790</td>
<td>4,262</td>
<td>3,164</td>
<td>13,216</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.38">v0.10.38</a></td>
@@ -1314,8 +1346,8 @@ https://github.com/laurent22/joplin/blob/dev/readme/stats.md
<td>2017-11-24T14:27:49Z</td>
<td>148</td>
<td>694</td>
<td>6,378</td>
<td>7,220</td>
<td>6,389</td>
<td>7,231</td>
</tr>
<tr>
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.23">v0.10.23</a></td>

View File

@@ -37,7 +37,8 @@
"tsc": "lerna run tsc --stream --parallel",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel"
"watch": "lerna run watch --stream --parallel",
"i": "lerna add --no-bootstrap --scope"
},
"husky": {
"hooks": {

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

@@ -5,6 +5,7 @@ import BasePluginRunner from '@joplin/lib/services/plugins/BasePluginRunner';
import executeSandboxCall from '@joplin/lib/services/plugins/utils/executeSandboxCall';
import Global from '@joplin/lib/services/plugins/api/Global';
import mapEventHandlersToIds, { EventHandlers } from '@joplin/lib/services/plugins/utils/mapEventHandlersToIds';
import uuid from '@joplin/lib/uuid';
function createConsoleWrapper(pluginId: string) {
const wrapper: any = {};
@@ -31,6 +32,7 @@ function createConsoleWrapper(pluginId: string) {
export default class PluginRunner extends BasePluginRunner {
private eventHandlers_: EventHandlers = {};
private activeSandboxCalls_: any = {};
constructor() {
super();
@@ -45,7 +47,13 @@ export default class PluginRunner extends BasePluginRunner {
private newSandboxProxy(pluginId: string, sandbox: Global) {
const target = async (path: string, args: any[]) => {
return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
const callId = `${pluginId}::${path}::${uuid.createNano()}`;
this.activeSandboxCalls_[callId] = true;
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
promise.finally(() => {
delete this.activeSandboxCalls_[callId];
});
return promise;
};
return {
@@ -69,10 +77,25 @@ export default class PluginRunner extends BasePluginRunner {
vm.runInContext(plugin.scriptText, vmSandbox);
} catch (error) {
reject(error);
// this.logger().error(`In plugin ${plugin.id}:`, error);
// return;
}
});
}
public async waitForSandboxCalls(): Promise<void> {
const startTime = Date.now();
return new Promise((resolve: Function, reject: Function) => {
const iid = setInterval(() => {
if (!Object.keys(this.activeSandboxCalls_).length) {
clearInterval(iid);
resolve();
}
if (Date.now() - startTime > 4000) {
clearInterval(iid);
reject(new Error(`Timeout while waiting for sandbox calls to complete: ${JSON.stringify(this.activeSandboxCalls_)}`));
}
}, 10);
});
}
}

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

@@ -5,9 +5,9 @@
"author": "Laurent Cozic",
"private": true,
"scripts": {
"test": "jest --config=jest.config.js --runInBand --bail --forceExit",
"test-one": "jest --verbose=false --config=jest.config.js --runInBand --bail --forceExit",
"test-ci": "jest --config=jest.config.js --runInBand --forceExit",
"test": "jest --config=jest.config.js --bail --forceExit",
"test-one": "jest --verbose=false --config=jest.config.js --bail --forceExit",
"test-ci": "jest --config=jest.config.js --forceExit",
"build": "gulp build",
"start": "gulp build -L && node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
@@ -31,7 +31,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.4.7",
"version": "1.5.0",
"bin": {
"joplin": "./main.js"
},

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 @@ import shim from '@joplin/lib/shim';
const fs = require('fs-extra');
const os = require('os');
const { filename } = require('@joplin/lib/path-utils');
const { setupDatabaseAndSynchronizer, switchClient } = require('./test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, expectNotThrow } = require('./test-utils.js');
const { enexXmlToMd } = require('@joplin/lib/import-enex-md-gen.js');
const { importEnex } = require('@joplin/lib/import-enex');
const Note = require('@joplin/lib/models/Note');
@@ -96,4 +96,21 @@ describe('EnexToMd', function() {
expect(note.updated_time).toBe(1521822724000); // Because this date was invalid, it is set to the created time instead
});
it('should handle empty resources', async () => {
const filePath = `${enexSampleBaseDir}/empty_resource.enex`;
await expectNotThrow(() => importEnex('', filePath));
const all = await Resource.all();
expect(all.length).toBe(1);
expect(all[0].size).toBe(0);
});
it('should handle empty note content', async () => {
const filePath = `${enexSampleBaseDir}/empty_content.enex`;
await expectNotThrow(() => importEnex('', filePath));
const all = await Note.all();
expect(all.length).toBe(1);
expect(all[0].title).toBe('China and the case for stimulus.');
expect(all[0].body).toBe('');
});
});

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

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export2.dtd">
<en-export export-date="20201130T021533Z" application="Evernote/Windows" version="6.x">
<note>
<title>China and the case for stimulus.</title>
<content></content>
<created>20120904T185210Z</created>
<note-attributes>
<source>web.clip</source>
</note-attributes>
</note></en-export>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export2.dtd">
<en-export export-date="20201130T021533Z" application="Evernote/Windows" version="6.x">
<note><title>China and the case for stimulus.</title><content></content><created>20120904T185210Z</created><note-attributes><source>web.clip</source><source-url>http://www.slate.com/blogs/moneybox/2012/09/04/china_and_the_case_for_stimulus.html</source-url></note-attributes>
<resource>
<data/>
<mime>application/octet-stream</mime>
<resource-attributes>
<file-name>04\</file-name>
</resource-attributes>
</resource>
</note></en-export>

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

@@ -0,0 +1,52 @@
import PluginService from '@joplin/lib/services/plugins/PluginService';
const { newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } = require('../../../test-utils');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
const ItemChange = require('@joplin/lib/models/ItemChange');
describe('JoplinWorkspace', () => {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterEach(async () => {
await afterEachCleanUp();
});
test('should listen to noteChange events', async () => {
const service = new newPluginService() as PluginService;
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
await joplin.workspace.onNoteChange(async (event) => {
await joplin.data.post(['folders'], null, { title: JSON.stringify(event) });
});
},
});
`);
const note = await Note.save({});
await ItemChange.waitForAllSaved();
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
await Note.save({ id: note.id, body: 'testing' });
await ItemChange.waitForAllSaved();
const folder = (await Folder.all())[0];
const result: any = JSON.parse(folder.title);
expect(result.id).toBe(note.id);
expect(result.event).toBe(ItemChange.TYPE_UPDATE);
await service.destroy();
});
});

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') {
@@ -22,9 +18,7 @@ function newPluginService(appVersion: string = '1.4') {
service.initialize(
appVersion,
{
joplin: {
workspace: {},
},
joplin: {},
},
runner,
{
@@ -43,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`], {});
@@ -59,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();
@@ -78,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`, {});
@@ -92,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', `
@@ -125,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:
@@ -172,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`;
@@ -224,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:
{
@@ -262,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);
@@ -270,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: '我是法国人' });
@@ -447,10 +443,10 @@ describe('services_SearchEngine', function() {
expect((await engine.search('title:bla 我是')).length).toBe(0);
// For non-alpha char, only the first field is looked at, the following ones are ignored
expect((await engine.search('title:你好 title:hello')).length).toBe(1);
// 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

@@ -20,4 +20,4 @@
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
}
}
}

View File

@@ -1,6 +1,7 @@
document.addEventListener('click', event => {
const element = event.target;
if (element.className === 'toc-item-link') {
console.debug('TOC Plugin Webview: Sending scrollToHash message', element.dataset.slug);
webviewApi.postMessage({
name: 'scrollToHash',
hash: element.dataset.slug,

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

@@ -50,9 +50,18 @@ const KeychainServiceDriver = require('@joplin/lib/services/keychain/KeychainSer
const KeychainServiceDriverDummy = require('@joplin/lib/services/keychain/KeychainServiceDriver.dummy').default;
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
const PluginRunner = require('../app/services/plugins/PluginRunner').default;
const PluginService = require('@joplin/lib/services/plugins/PluginService').default;
const { Dirnames } = require('@joplin/lib/services/synchronizer/utils/types');
const sharp = require('sharp');
// Each suite has its own separate data and temp directory so that multiple
// suites can be run at the same time. suiteName is what is used to
// differentiate between suite and it is currently set to a random string
// (Ideally it would be something like the filename currently being executed by
// Jest, to make debugging easier, but it's not clear how to get this info).
const suiteName_ = uuid.createNano();
const databases_ = [];
let synchronizers_ = [];
const synchronizerContexts_ = {};
@@ -89,10 +98,11 @@ EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
const logDir = `${__dirname}/../tests/logs`;
const baseTempDir = `${__dirname}/../tests/tmp`;
const baseTempDir = `${__dirname}/../tests/tmp/${suiteName_}`;
const dataDir = `${__dirname}/data/${suiteName_}`;
fs.mkdirpSync(logDir, 0o755);
fs.mkdirpSync(baseTempDir, 0o755);
fs.mkdirpSync(`${__dirname}/data`);
fs.mkdirpSync(dataDir);
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
@@ -129,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?
@@ -139,12 +149,12 @@ const syncDir = `${__dirname}/../tests/sync`;
const dbLogger = new Logger();
dbLogger.addTarget('console');
dbLogger.addTarget('file', { path: `${logDir}/log.txt` });
// dbLogger.addTarget('file', { path: `${logDir}/log.txt` });
dbLogger.setLevel(Logger.LEVEL_WARN);
const logger = new Logger();
logger.addTarget('console');
logger.addTarget('file', { path: `${logDir}/log.txt` });
// logger.addTarget('file', { path: `${logDir}/log.txt` });
logger.setLevel(Logger.LEVEL_WARN); // Set to DEBUG to display sync process in console
Logger.initializeGlobalLogger(logger);
@@ -269,7 +279,7 @@ async function setupDatabase(id = null, options = null) {
return;
}
const filePath = `${__dirname}/data/test-${id}.sqlite`;
const filePath = `${dataDir}/test-${id}.sqlite`;
try {
await fs.unlink(filePath);
@@ -292,15 +302,15 @@ function resourceDirName(id = null) {
function resourceDir(id = null) {
if (id === null) id = currentClient_;
return `${__dirname}/data/${resourceDirName(id)}`;
return `${dataDir}/${resourceDirName(id)}`;
}
function pluginDir(id = null) {
if (id === null) id = currentClient_;
return `${__dirname}/data/plugins-${id}`;
return `${dataDir}/plugins-${id}`;
}
async function setupDatabaseAndSynchronizer(id = null, options = null) {
async function setupDatabaseAndSynchronizer(id, options = null) {
if (id === null) id = currentClient_;
BaseService.logger_ = logger;
@@ -549,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;
@@ -669,6 +658,39 @@ async function createTempDir() {
return tempDirPath;
}
function newPluginService(appVersion = '1.4') {
const runner = new PluginRunner();
const service = new PluginService();
service.initialize(
appVersion,
{
joplin: {},
},
runner,
{
dispatch: () => {},
getState: () => {},
}
);
return service;
}
function newPluginScript(script) {
return `
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.PluginTest",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"version": "1.0.0"
}
*/
${script}
`;
}
// TODO: Update for Jest
// function mockDate(year, month, day, tick) {
@@ -749,4 +771,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { 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

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "1.4.3",
"version": "1.5.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

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

@@ -156,7 +156,7 @@ export default class InteropServiceHelper {
if (Array.isArray(path)) path = path[0];
CommandService.instance().execute('showModalMessage', _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));
void CommandService.instance().execute('showModalMessage', _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));
const exportOptions: ExportOptions = {};
exportOptions.path = path;
@@ -177,7 +177,7 @@ export default class InteropServiceHelper {
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
}
CommandService.instance().execute('hideModalMessage');
void CommandService.instance().execute('hideModalMessage');
}
}

View File

@@ -424,7 +424,7 @@ class Application extends BaseApplication {
const contextMenu = Menu.buildFromTemplate([
{ label: _('Open %s', app.electronApp().name), click: () => { app.window().show(); } },
{ type: 'separator' },
{ label: _('Quit'), click: () => { app.quit(); } },
{ label: _('Quit'), click: () => { void app.quit(); } },
]);
app.createTray(contextMenu);
}
@@ -664,7 +664,7 @@ class Application extends BaseApplication {
this.updateTray();
shim.setTimeout(() => {
AlarmService.garbageCollect();
void AlarmService.garbageCollect();
}, 1000 * 60 * 60);
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
@@ -676,12 +676,12 @@ class Application extends BaseApplication {
ResourceService.runInBackground();
if (Setting.value('env') === 'dev') {
AlarmService.updateAllNotifications();
void AlarmService.updateAllNotifications();
} else {
reg.scheduleSync(1000).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
AlarmService.updateAllNotifications();
void AlarmService.updateAllNotifications();
DecryptionWorker.instance().scheduleStart();
});

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

View File

@@ -11,7 +11,7 @@ export const declaration: CommandDeclaration = {
export const runtime = (): CommandRuntime => {
return {
execute: async () => {
bridge().openItem(Setting.value('profileDir'));
void bridge().openItem(Setting.value('profileDir'));
},
};
};

View File

@@ -18,7 +18,7 @@ export const runtime = (): CommandRuntime => {
try {
const note = await Note.load(noteId);
ExternalEditWatcher.instance().openAndWatch(note);
void ExternalEditWatcher.instance().openAndWatch(note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}

View File

@@ -13,7 +13,7 @@ export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, noteId: string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
ExternalEditWatcher.instance().stopWatching(noteId);
void ExternalEditWatcher.instance().stopWatching(noteId);
},
enabledCondition: 'oneNoteSelected',
};

View File

@@ -17,9 +17,9 @@ export const runtime = (): CommandRuntime => {
if (!noteId) return;
if (context.state.watchedNoteFiles.includes(noteId)) {
CommandService.instance().execute('stopExternalEditing', noteId);
void CommandService.instance().execute('stopExternalEditing', noteId);
} else {
CommandService.instance().execute('startExternalEditing', noteId);
void CommandService.instance().execute('startExternalEditing', noteId);
}
},
enabledCondition: 'oneNoteSelected',

View File

@@ -696,7 +696,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
{this.restartMessage()}
<a style={{ ...theme.urlStyle, marginLeft: 10 }} href="#" onClick={() => { this.restartApp(); }}>{_('Restart now')}</a>
<a style={{ ...theme.urlStyle, marginLeft: 10 }} href="#" onClick={() => { void this.restartApp(); }}>{_('Restart now')}</a>
</div>
) : null;

View File

@@ -86,7 +86,7 @@ const useKeymap = (): [
}
}
saveKeymap();
void saveKeymap();
}, [keymapItems, mustSave]);
return [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator];

View File

@@ -30,6 +30,7 @@ import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem';
import Logger from '@joplin/lib/Logger';
const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js');
@@ -38,6 +39,8 @@ const PluginManager = require('@joplin/lib/services/PluginManager');
const EncryptionService = require('@joplin/lib/services/EncryptionService');
const ipcRenderer = require('electron').ipcRenderer;
const logger = Logger.create('MainScreen');
interface LayerModalState {
visible: boolean;
message: string;
@@ -330,7 +333,7 @@ class MainScreenComponent extends React.Component<Props, State> {
layoutModeListenerKeyDown(event: any) {
if (event.key !== 'Escape') return;
if (!this.props.layoutMoveMode) return;
CommandService.instance().execute('toggleLayoutMoveMode');
void CommandService.instance().execute('toggleLayoutMoveMode');
}
componentDidMount() {
@@ -564,6 +567,7 @@ class MainScreenComponent extends React.Component<Props, State> {
}
userWebview_message(event: any) {
logger.debug('Got message (WebView => Plugin) (2)', event);
PluginService.instance().pluginById(event.pluginId).viewController(event.viewId).emitMessage(event);
}

Some files were not shown because too many files have changed in this diff Show More