2021-05-21 15:17:21 +02:00
import PluginRunner from '../../../app/services/plugins/PluginRunner' ;
2024-01-18 13:24:44 +02:00
import PluginService , { PluginSettings } from '@joplin/lib/services/plugins/PluginService' ;
2020-11-07 17:59:37 +02:00
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types' ;
import MdToHtml from '@joplin/renderer/MdToHtml' ;
import shim from '@joplin/lib/shim' ;
2020-11-20 01:46:04 +02:00
import Setting from '@joplin/lib/models/Setting' ;
2021-01-24 17:51:35 +02:00
import * as fs from 'fs-extra' ;
2021-01-22 19:41:11 +02:00
import Note from '@joplin/lib/models/Note' ;
import Folder from '@joplin/lib/models/Folder' ;
2024-04-03 19:51:09 +02:00
import { expectNotThrow , setupDatabaseAndSynchronizer , switchClient , expectThrow , createTempDir , supportDir , mockMobilePlatform } from '@joplin/lib/testing/test-utils' ;
2021-05-25 17:50:51 +02:00
import { newPluginScript } from '../../testUtils' ;
2020-10-09 19:35:46 +02:00
2021-05-21 15:17:21 +02:00
const testPluginDir = ` ${ supportDir } /plugins ` ;
2020-10-09 19:35:46 +02:00
2023-06-30 10:11:26 +02:00
function newPluginService ( appVersion = '1.4' ) {
2020-10-09 19:35:46 +02:00
const runner = new PluginRunner ( ) ;
const service = new PluginService ( ) ;
service . initialize (
2020-11-15 16:18:46 +02:00
appVersion ,
2020-10-09 19:35:46 +02:00
{
2020-12-01 16:08:41 +02:00
joplin : { } ,
2020-10-09 19:35:46 +02:00
} ,
runner ,
{
dispatch : ( ) = > { } ,
getState : ( ) = > { } ,
2023-08-22 12:58:53 +02:00
} ,
2020-10-09 19:35:46 +02:00
) ;
return service ;
}
2023-02-20 17:02:29 +02:00
describe ( 'services_PluginService' , ( ) = > {
2020-10-09 19:35:46 +02:00
2022-11-15 12:23:50 +02:00
beforeEach ( async ( ) = > {
2020-10-09 19:35:46 +02:00
await setupDatabaseAndSynchronizer ( 1 ) ;
await switchClient ( 1 ) ;
} ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load and run a simple plugin' , ( async ( ) = > {
2020-10-09 19:35:46 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( [ ` ${ testPluginDir } /simple ` ] , { } ) ;
2020-10-09 19:35:46 +02:00
2020-11-18 12:17:27 +02:00
expect ( ( ) = > service . pluginById ( 'org.joplinapp.plugins.Simple' ) ) . not . toThrowError ( ) ;
2020-11-14 00:03:10 +02:00
2020-10-09 19:35:46 +02:00
const allFolders = await Folder . all ( ) ;
expect ( allFolders . length ) . toBe ( 1 ) ;
expect ( allFolders [ 0 ] . title ) . toBe ( 'my plugin folder' ) ;
const allNotes = await Note . all ( ) ;
expect ( allNotes . length ) . toBe ( 1 ) ;
expect ( allNotes [ 0 ] . title ) . toBe ( 'testing plugin!' ) ;
expect ( allNotes [ 0 ] . parent_id ) . toBe ( allFolders [ 0 ] . id ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load and run a simple plugin and handle trailing slash' , ( async ( ) = > {
2020-11-14 00:03:10 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( [ ` ${ testPluginDir } /simple/ ` ] , { } ) ;
2020-11-18 12:17:27 +02:00
expect ( ( ) = > service . pluginById ( 'org.joplinapp.plugins.Simple' ) ) . not . toThrowError ( ) ;
2020-11-14 00:03:10 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load and run a plugin that uses external packages' , ( async ( ) = > {
2020-10-09 19:35:46 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( [ ` ${ testPluginDir } /withExternalModules ` ] , { } ) ;
2020-11-18 12:17:27 +02:00
expect ( ( ) = > service . pluginById ( 'org.joplinapp.plugins.ExternalModuleDemo' ) ) . not . toThrowError ( ) ;
2020-10-09 19:35:46 +02:00
const allFolders = await Folder . all ( ) ;
expect ( allFolders . length ) . toBe ( 1 ) ;
2020-10-22 15:51:59 +02:00
// If you have an error here, it might mean you need to run `npm i` from
// the "withExternalModules" folder. Not clear exactly why.
2020-10-09 19:35:46 +02:00
expect ( allFolders [ 0 ] . title ) . toBe ( ' foo' ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load multiple plugins from a directory' , ( async ( ) = > {
2020-10-09 19:35:46 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( ` ${ testPluginDir } /multi_plugins ` , { } ) ;
2020-10-09 19:35:46 +02:00
2020-11-18 12:17:27 +02:00
const plugin1 = service . pluginById ( 'org.joplinapp.plugins.MultiPluginDemo1' ) ;
const plugin2 = service . pluginById ( 'org.joplinapp.plugins.MultiPluginDemo2' ) ;
2020-10-09 19:35:46 +02:00
expect ( ! ! plugin1 ) . toBe ( true ) ;
expect ( ! ! plugin2 ) . toBe ( true ) ;
const allFolders = await Folder . all ( ) ;
expect ( allFolders . length ) . toBe ( 2 ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
expect ( allFolders . map ( ( f : any ) = > f . title ) . sort ( ) . join ( ', ' ) ) . toBe ( 'multi - simple1, multi - simple2' ) ;
2020-10-09 19:35:46 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load plugins from JS bundles' , ( async ( ) = > {
2020-10-13 12:16:36 +02:00
const service = newPluginService ( ) ;
2020-11-18 12:17:27 +02:00
const plugin = await service . loadPluginFromJsBundle ( '/tmp' , `
2020-10-13 12:16:36 +02:00
/ * j o p l i n - m a n i f e s t :
{
2020-11-18 12:17:27 +02:00
"id" : "org.joplinapp.plugins.JsBundleTest" ,
2020-10-13 12:16:36 +02:00
"manifest_version" : 1 ,
2020-11-15 16:18:46 +02:00
"app_min_version" : "1.4" ,
2020-10-13 12:16:36 +02:00
"name" : "JS Bundle test" ,
"description" : "JS Bundle Test plugin" ,
"version" : "1.0.0" ,
"author" : "Laurent Cozic" ,
"homepage_url" : "https://joplinapp.org"
}
* /
2021-05-21 15:17:21 +02:00
2020-10-13 12:16:36 +02:00
joplin . plugins . register ( {
onStart : async function ( ) {
await joplin . data . post ( [ 'folders' ] , null , { title : "my plugin folder" } ) ;
} ,
} ) ;
` );
await service . runPlugin ( plugin ) ;
expect ( plugin . manifest . manifest_version ) . toBe ( 1 ) ;
expect ( plugin . manifest . name ) . toBe ( 'JS Bundle test' ) ;
const allFolders = await Folder . all ( ) ;
expect ( allFolders . length ) . toBe ( 1 ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load plugins from JS bundle files' , ( async ( ) = > {
2020-10-13 12:16:36 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( ` ${ testPluginDir } /jsbundles ` , { } ) ;
2020-11-18 12:17:27 +02:00
expect ( ! ! service . pluginById ( 'org.joplinapp.plugins.JsBundleDemo' ) ) . toBe ( true ) ;
2020-10-13 12:16:36 +02:00
expect ( ( await Folder . all ( ) ) . length ) . toBe ( 1 ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should load plugins from JPL archive' , ( async ( ) = > {
2020-11-17 20:26:24 +02:00
const service = newPluginService ( ) ;
2020-11-19 14:34:49 +02:00
await service . loadAndRunPlugins ( [ ` ${ testPluginDir } /jpl_test/org.joplinapp.FirstJplPlugin.jpl ` ] , { } ) ;
2020-11-17 20:26:24 +02:00
expect ( ! ! service . pluginById ( 'org.joplinapp.FirstJplPlugin' ) ) . toBe ( true ) ;
expect ( ( await Folder . all ( ) ) . length ) . toBe ( 1 ) ;
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should validate JS bundles' , ( async ( ) = > {
2020-10-13 12:16:36 +02:00
const invalidJsBundles = [
`
/ * j o p l i n - m a n i f e s t :
{
"not_a_valid_manifest_at_all" : 1
}
* /
2021-05-21 15:17:21 +02:00
2020-10-13 12:16:36 +02:00
joplin . plugins . register ( {
onStart : async function ( ) { } ,
} ) ;
` , `
/ * j o p l i n - m a n i f e s t :
* /
2021-05-21 15:17:21 +02:00
2020-10-13 12:16:36 +02:00
joplin . plugins . register ( {
onStart : async function ( ) { } ,
} ) ;
` , `
joplin . plugins . register ( {
onStart : async function ( ) { } ,
} ) ;
` , '',
] ;
const service = newPluginService ( ) ;
for ( const jsBundle of invalidJsBundles ) {
2020-11-18 12:17:27 +02:00
await expectThrow ( async ( ) = > await service . loadPluginFromJsBundle ( '/tmp' , jsBundle ) ) ;
2020-10-13 12:16:36 +02:00
}
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should register a Markdown-it plugin' , ( async ( ) = > {
2020-10-22 16:55:29 +02:00
const tempDir = await createTempDir ( ) ;
const contentScriptPath = ` ${ tempDir } /markdownItTestPlugin.js ` ;
2020-12-10 18:09:31 +02:00
const contentScriptCssPath = ` ${ tempDir } /markdownItTestPlugin.css ` ;
2021-01-03 15:21:48 +02:00
await shim . fsDriver ( ) . copy ( ` ${ testPluginDir } /markdownItTestPlugin.js ` , contentScriptPath ) ;
2020-12-10 18:09:31 +02:00
await shim . fsDriver ( ) . copy ( ` ${ testPluginDir } /content_script/src/markdownItTestPlugin.css ` , contentScriptCssPath ) ;
2020-10-21 01:23:55 +02:00
const service = newPluginService ( ) ;
2020-11-18 12:17:27 +02:00
const plugin = await service . loadPluginFromJsBundle ( tempDir , `
2020-10-21 01:23:55 +02:00
/ * j o p l i n - m a n i f e s t :
{
2020-11-18 12:17:27 +02:00
"id" : "org.joplinapp.plugin.MarkdownItPluginTest" ,
2020-10-21 01:23:55 +02:00
"manifest_version" : 1 ,
2020-11-15 16:18:46 +02:00
"app_min_version" : "1.4" ,
2020-10-21 01:23:55 +02:00
"name" : "JS Bundle test" ,
"description" : "JS Bundle Test plugin" ,
"version" : "1.0.0" ,
"author" : "Laurent Cozic" ,
"homepage_url" : "https://joplinapp.org"
}
* /
2021-05-21 15:17:21 +02:00
2020-10-21 01:23:55 +02:00
joplin . plugins . register ( {
onStart : async function ( ) {
2021-01-12 01:33:10 +02:00
await joplin . contentScripts . register ( 'markdownItPlugin' , 'justtesting' , './markdownItTestPlugin.js' ) ;
2020-10-21 01:23:55 +02:00
} ,
} ) ;
` );
await service . runPlugin ( plugin ) ;
const contentScripts = plugin . contentScriptsByType ( ContentScriptType . MarkdownItPlugin ) ;
expect ( contentScripts . length ) . toBe ( 1 ) ;
expect ( ! ! contentScripts [ 0 ] . path ) . toBe ( true ) ;
const contentScript = contentScripts [ 0 ] ;
const mdToHtml = new MdToHtml ( ) ;
const module = require ( contentScript . path ) . default ;
2023-11-03 21:45:21 +02:00
mdToHtml . loadExtraRendererRule ( contentScript . id , tempDir , module ( { } ) , '' ) ;
2020-10-21 01:23:55 +02:00
const result = await mdToHtml . render ( [
'```justtesting' ,
'something' ,
'```' ,
] . join ( '\n' ) ) ;
2020-12-10 18:09:31 +02:00
const asset = result . pluginAssets . find ( a = > a . name === 'justtesting/markdownItTestPlugin.css' ) ;
const assetContent : string = await shim . fsDriver ( ) . readFile ( asset . path , 'utf8' ) ;
expect ( assetContent . includes ( '.just-testing' ) ) . toBe ( true ) ;
2020-12-11 18:03:55 +02:00
expect ( assetContent . includes ( 'background-color: rgb(202, 255, 255)' ) ) . toBe ( true ) ;
2020-10-21 01:23:55 +02:00
expect ( result . html . includes ( 'JUST TESTING: something' ) ) . toBe ( true ) ;
2020-10-22 16:55:29 +02:00
await shim . fsDriver ( ) . remove ( tempDir ) ;
2020-10-21 01:23:55 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should enable and disable plugins depending on what app version they support' , ( async ( ) = > {
2020-11-15 16:18:46 +02:00
const pluginScript = `
/ * j o p l i n - m a n i f e s t :
{
2020-11-18 12:17:27 +02:00
"id" : "org.joplinapp.plugins.PluginTest" ,
2020-11-15 16:18:46 +02:00
"manifest_version" : 1 ,
"app_min_version" : "1.4" ,
"name" : "JS Bundle test" ,
"version" : "1.0.0"
}
* /
2021-05-21 15:17:21 +02:00
2020-11-15 16:18:46 +02:00
joplin . plugins . register ( {
onStart : async function ( ) { } ,
} ) ;
` ;
const testCases = [
[ '1.4' , true ] ,
[ '1.5' , true ] ,
[ '2.0' , true ] ,
[ '1.3' , false ] ,
[ '0.9' , false ] ,
] ;
for ( const testCase of testCases ) {
2020-11-19 14:34:49 +02:00
const [ appVersion , hasNoError ] = testCase ;
const service = newPluginService ( appVersion as string ) ;
const plugin = await service . loadPluginFromJsBundle ( '' , pluginScript ) ;
if ( hasNoError ) {
await expectNotThrow ( ( ) = > service . runPlugin ( plugin ) ) ;
} else {
await expectThrow ( ( ) = > service . runPlugin ( plugin ) ) ;
}
2020-11-15 16:18:46 +02:00
}
} ) ) ;
2024-04-03 19:51:09 +02:00
it . each ( [
{
manifestPlatforms : [ 'desktop' ] ,
isDesktop : true ,
appVersion : '3.0.0' ,
shouldRun : true ,
} ,
{
manifestPlatforms : [ 'desktop' ] ,
isDesktop : false ,
appVersion : '3.0.6' ,
shouldRun : false ,
} ,
{
manifestPlatforms : [ 'desktop' , 'mobile' ] ,
isDesktop : false ,
appVersion : '3.0.6' ,
shouldRun : true ,
} ,
{
2024-04-27 12:43:25 +02:00
// Should default to desktop-only
2024-04-03 19:51:09 +02:00
manifestPlatforms : [ ] ,
isDesktop : false ,
appVersion : '3.0.8' ,
2024-04-27 12:43:25 +02:00
shouldRun : false ,
2024-04-03 19:51:09 +02:00
} ,
] ) ( 'should enable and disable plugins depending on what platform(s) they support (case %#: %j)' , async ( { manifestPlatforms , isDesktop , appVersion , shouldRun } ) = > {
const pluginScript = `
/ * j o p l i n - m a n i f e s t :
{
"id" : "org.joplinapp.plugins.PluginTest" ,
"manifest_version" : 1 ,
"app_min_version" : "1.0.0" ,
"platforms" : $ { JSON . stringify ( manifestPlatforms ) } ,
"name" : "JS Bundle test" ,
"version" : "1.0.0"
}
* /
joplin . plugins . register ( {
onStart : async function ( ) { } ,
} ) ;
` ;
let resetPlatformMock = ( ) = > { } ;
if ( ! isDesktop ) {
resetPlatformMock = mockMobilePlatform ( 'android' ) . reset ;
}
try {
const service = newPluginService ( appVersion ) ;
const plugin = await service . loadPluginFromJsBundle ( '' , pluginScript ) ;
if ( shouldRun ) {
await expect ( service . runPlugin ( plugin ) ) . resolves . toBeUndefined ( ) ;
} else {
await expect ( service . runPlugin ( plugin ) ) . rejects . toThrow ( /disabled/ ) ;
}
} finally {
resetPlatformMock ( ) ;
}
} ) ;
2020-12-01 20:05:24 +02:00
it ( 'should install a plugin' , ( async ( ) = > {
2020-11-20 01:46:04 +02:00
const service = newPluginService ( ) ;
const pluginPath = ` ${ testPluginDir } /jpl_test/org.joplinapp.FirstJplPlugin.jpl ` ;
await service . installPlugin ( pluginPath ) ;
const installedPluginPath = ` ${ Setting . value ( 'pluginDir' ) } /org.joplinapp.FirstJplPlugin.jpl ` ;
2021-01-24 17:51:35 +02:00
expect ( await fs . pathExists ( installedPluginPath ) ) . toBe ( true ) ;
2020-11-20 01:46:04 +02:00
} ) ) ;
2020-12-01 20:05:24 +02:00
it ( 'should rename the plugin archive to the right name' , ( async ( ) = > {
2020-11-20 01:46:04 +02:00
const tempDir = await createTempDir ( ) ;
const service = newPluginService ( ) ;
const pluginPath = ` ${ testPluginDir } /jpl_test/org.joplinapp.FirstJplPlugin.jpl ` ;
const tempPath = ` ${ tempDir } /something.jpl ` ;
await shim . fsDriver ( ) . copy ( pluginPath , tempPath ) ;
const installedPluginPath = ` ${ Setting . value ( 'pluginDir' ) } /org.joplinapp.FirstJplPlugin.jpl ` ;
await service . installPlugin ( tempPath ) ;
2021-01-24 17:51:35 +02:00
expect ( await fs . pathExists ( installedPluginPath ) ) . toBe ( true ) ;
} ) ) ;
it ( 'should create the data directory' , ( async ( ) = > {
2021-05-21 15:17:21 +02:00
const pluginScript = newPluginScript ( `
2021-01-24 17:51:35 +02:00
joplin . plugins . register ( {
onStart : async function ( ) {
const dataDir = await joplin . plugins . dataDir ( ) ;
joplin . data . post ( [ 'folders' ] , null , { title : JSON.stringify ( dataDir ) } ) ;
} ,
} ) ;
` );
const expectedPath = ` ${ Setting . value ( 'pluginDataDir' ) } /org.joplinapp.plugins.PluginTest ` ;
expect ( await fs . pathExists ( expectedPath ) ) . toBe ( false ) ;
const service = newPluginService ( ) ;
const plugin = await service . loadPluginFromJsBundle ( '' , pluginScript ) ;
await service . runPlugin ( plugin ) ;
expect ( await fs . pathExists ( expectedPath ) ) . toBe ( true ) ;
const folders = await Folder . all ( ) ;
expect ( JSON . parse ( folders [ 0 ] . title ) ) . toBe ( expectedPath ) ;
2020-11-20 01:46:04 +02:00
} ) ) ;
2024-01-18 13:24:44 +02:00
it ( 'should uninstall multiple plugins' , async ( ) = > {
const service = newPluginService ( ) ;
const pluginId1 = 'org.joplinapp.FirstJplPlugin' ;
const pluginId2 = 'org.joplinapp.plugins.TocDemo' ;
const pluginPath1 = ` ${ testPluginDir } /jpl_test/ ${ pluginId1 } .jpl ` ;
const pluginPath2 = ` ${ testPluginDir } /toc/ ${ pluginId2 } .jpl ` ;
await service . installPlugin ( pluginPath1 ) ;
await service . installPlugin ( pluginPath2 ) ;
// Both should be installed
expect ( await fs . pathExists ( ` ${ Setting . value ( 'pluginDir' ) } / ${ pluginId1 } .jpl ` ) ) . toBe ( true ) ;
expect ( await fs . pathExists ( ` ${ Setting . value ( 'pluginDir' ) } / ${ pluginId2 } .jpl ` ) ) . toBe ( true ) ;
const pluginSettings : PluginSettings = {
[ pluginId1 ] : { enabled : true , deleted : true , hasBeenUpdated : false } ,
[ pluginId2 ] : { enabled : true , deleted : true , hasBeenUpdated : false } ,
} ;
const newPluginSettings = await service . uninstallPlugins ( pluginSettings ) ;
// Should have deleted plugins
expect ( await fs . pathExists ( ` ${ Setting . value ( 'pluginDir' ) } / ${ pluginId1 } .jpl ` ) ) . toBe ( false ) ;
expect ( await fs . pathExists ( ` ${ Setting . value ( 'pluginDir' ) } ${ pluginId2 } .jpl ` ) ) . toBe ( false ) ;
// Should clear deleted plugins from settings
expect ( newPluginSettings [ pluginId1 ] ) . toBe ( undefined ) ;
expect ( newPluginSettings [ pluginId2 ] ) . toBe ( undefined ) ;
} ) ;
2020-10-09 19:35:46 +02:00
} ) ;