import { test, expect } from './util/test'; import MainScreen from './models/MainScreen'; import { msleep, Second } from '@joplin/utils/time'; import setSettingValue from './util/setSettingValue'; test.describe('pluginApi', () => { test('the editor.setText command should update the current note (use RTE: false)', async ({ startAppWithPlugins }) => { const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/execCommand.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('First note'); const editor = mainScreen.noteEditor; const markdownEditor = await editor.showMarkdownEditor(); await markdownEditor.typeText('This content should be overwritten.'); await editor.expectToHaveText('This content should be overwritten.'); await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText'); await editor.expectToHaveText('PASS'); // Should still have the same text after switching notes: await mainScreen.createNewNote('Second note'); await editor.goBack(); await editor.expectToHaveText('PASS'); }); test('should return form data from the dialog API', async ({ startAppWithPlugins }) => { const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('First note'); const editor = mainScreen.noteEditor; await editor.expectToHaveText('\n'); await mainScreen.goToAnything.runCommand(app, 'showTestDialog'); // Wait for the iframe to load const dialogContent = mainScreen.dialog.locator('iframe').contentFrame(); await dialogContent.locator('form').waitFor(); // Submitting the dialog should include form data in the output await mainScreen.dialog.getByRole('button', { name: 'Okay' }).click(); await editor.expectToHaveText(JSON.stringify({ id: 'ok', hasFormData: true, })); }); test('should report the correct visibility state for dialogs', async ({ startAppWithPlugins }) => { const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Dialog test note'); const editor = mainScreen.noteEditor; const markdownEditor = await editor.showMarkdownEditor(); const expectVisible = async (visible: boolean) => { // Check UI visibility if (visible) { await expect(mainScreen.dialog).toBeVisible(); } else { await expect(mainScreen.dialog).not.toBeVisible(); } // Check visibility reported through the plugin API await expect.poll(async () => { await mainScreen.goToAnything.runCommand(app, 'getTestDialogVisibility'); return markdownEditor.container.textContent(); }).toBe(JSON.stringify({ visible: visible, active: visible, })); }; await expectVisible(false); await mainScreen.goToAnything.runCommand(app, 'showTestDialog'); await expectVisible(true); // Submitting the dialog should include form data in the output await mainScreen.dialog.getByRole('button', { name: 'Okay' }).click(); await expectVisible(false); }); // Regression tests for #13718 for (const method of ['Cancel button', 'Escape key'] as const) { test(`should dismiss a plugin dialog via ${method} with isolated iframes`, async ({ startAppWithPlugins }) => { const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test note'); // WebView isolation is currently behind a feature flag: await setSettingValue(app, mainWindow, 'featureFlag.plugins.isolatePluginWebViews', true); await mainScreen.goToAnything.runCommand(app, 'showTestDialogWithDismiss'); const dialogContent = mainScreen.dialog.locator('iframe').contentFrame(); await dialogContent.locator('p').waitFor(); if (method === 'Cancel button') { await mainScreen.dialog.getByRole('button', { name: 'Cancel' }).click(); } else { await mainWindow.keyboard.press('Escape'); } await expect(mainScreen.dialog).toBeHidden(); }); } test('should be possible to create multiple toasts with the same text from a plugin', async ({ startAppWithPlugins }) => { const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/showToast.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.goToAnything.runCommand(app, 'testShowToastNotification'); const notificationLocator = mainWindow.getByText('Toast: This is a test info message.'); await expect(notificationLocator).toBeVisible(); // Running the command again, there should be two notifications with the same text. await mainScreen.goToAnything.runCommand(app, 'testShowToastNotification'); await expect(notificationLocator.nth(1)).toBeVisible(); await expect(notificationLocator.nth(0)).toBeVisible(); await mainScreen.goToAnything.runCommand(app, 'testShowToastNotification'); await expect(notificationLocator).toHaveCount(3); }); test('should be possible to switch between multiple editor plugins', async ({ startAppWithPlugins }) => { const { mainWindow } = await startAppWithPlugins(['resources/test-plugins/multipleEditorPlugins.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test note'); const toggleButton = mainScreen.noteEditor.toggleEditorPluginButton; // Initially, the toggle button should be visible const markdownEditor = await mainScreen.noteEditor.showMarkdownEditor(); await toggleButton.click(); const pluginFrame = mainScreen.noteEditor.editorPluginFrame; await expect(pluginFrame).toBeAttached(); await expect(markdownEditor.container).not.toBeAttached(); // Should describe the frame const frameViewIdLabel = pluginFrame.contentFrame().locator('#view-id-base'); await expect(frameViewIdLabel).toHaveText('test-editor-plugin-1'); // Clicking toggle again should cycle to the next editor plugin await toggleButton.click(); await expect(frameViewIdLabel).toHaveText('test-editor-plugin-2'); // Clicking toggle again should dismiss the editor plugins await toggleButton.click(); await expect(markdownEditor.container).toBeAttached(); await expect(pluginFrame).not.toBeAttached(); }); test('should be possible to save changes to a note using the editor plugin API', async ({ startAppWithPlugins }) => { const { mainWindow, app } = await startAppWithPlugins(['resources/test-plugins/editorPluginSaving.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test note'); const noteEditor = mainScreen.noteEditor; const markdownEditor = await noteEditor.showMarkdownEditor(); await markdownEditor.typeText('Initial content.'); const toggleButton = noteEditor.toggleEditorPluginButton; await toggleButton.click(); // Should switch to the editor const pluginFrame = noteEditor.editorPluginFrame; await expect(pluginFrame).toBeAttached(); const pluginFrameContent = pluginFrame.contentFrame(); await expect(pluginFrameContent.getByText('Loaded!')).toBeAttached(); // The plugin only knows which note to save after its first onUpdate event. await expect(pluginFrameContent.locator('#note-id')).not.toBeEmpty(); // Editor plugin tests should pass await mainScreen.goToAnything.runCommand(app, 'testEditorPluginSave-test-editor-plugin'); // Should have saved await toggleButton.click(); const expectedUpdatedText = 'Changed by test-editor-plugin'; await noteEditor.expectToHaveText(expectedUpdatedText); // Regression test: Historically the editor's content would very briefly be correct, then // almost immediately be replaced with the old content. Doing another check after a brief // delay should cause the test to fail if this bug returns: await msleep(Second); await noteEditor.expectToHaveText(expectedUpdatedText); }); test('should support hiding and showing panels', async ({ startAppWithPlugins }) => { const { mainWindow, app } = await startAppWithPlugins(['resources/test-plugins/panels.js']); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test note (panels)'); const panelLocator = await mainScreen.pluginPanelLocator('org.joplinapp.plugins.example.panels'); await expect(panelLocator).not.toBeVisible(); await mainScreen.goToAnything.runCommand(app, 'testShowPanel'); // Panel should be visible await expect(panelLocator).toBeVisible(); // The panel should have the expected content const panelContent = panelLocator.contentFrame(); await expect( panelContent.getByRole('heading', { name: 'Panel content' }), ).toBeAttached(); await mainScreen.goToAnything.runCommand(app, 'testHidePanel'); await expect(panelLocator).not.toBeVisible(); }); // Regression test for https://github.com/laurent22/joplin/issues/12793 test('a plugin that crashes before register() should not block other plugins', async ({ startAppWithPlugins }) => { // Load a crashing plugin alongside a working plugin. If the fix works, // startAppWithPlugins completes because startup-plugins-loaded fires. // Without the fix, this test would timeout because allPluginsStarted // never becomes true. const { app, mainWindow } = await startAppWithPlugins([ 'resources/test-plugins/crashBeforeRegister.js', 'resources/test-plugins/execCommand.js', ]); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test note'); // Verify the working plugin is functional const editor = mainScreen.noteEditor; const markdownEditor = await editor.showMarkdownEditor(); await markdownEditor.typeText('Should be overwritten.'); await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText'); await editor.expectToHaveText('PASS'); }); });