diff --git a/.gitignore b/.gitignore index acca5d0c08..d23af8060a 100644 --- a/.gitignore +++ b/.gitignore @@ -608,10 +608,14 @@ packages/app-desktop/integration-tests/main.spec.js packages/app-desktop/integration-tests/markdownEditor.spec.js packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js packages/app-desktop/integration-tests/models/ChatPanel.js -packages/app-desktop/integration-tests/models/EditorCodeDialog.js packages/app-desktop/integration-tests/models/GoToAnything.js packages/app-desktop/integration-tests/models/MainScreen.js -packages/app-desktop/integration-tests/models/NoteEditorScreen.js +packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.js +packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.js +packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.js +packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.js +packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.js +packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.js packages/app-desktop/integration-tests/models/NoteList.js packages/app-desktop/integration-tests/models/SettingsScreen.js packages/app-desktop/integration-tests/models/Sidebar.js @@ -632,7 +636,6 @@ packages/app-desktop/integration-tests/util/getMainWindow.js packages/app-desktop/integration-tests/util/ignoreFlakyReporter.js packages/app-desktop/integration-tests/util/mockClipboard.js packages/app-desktop/integration-tests/util/retryOnFailure.js -packages/app-desktop/integration-tests/util/seedProfileSettings.js packages/app-desktop/integration-tests/util/setDarkMode.js packages/app-desktop/integration-tests/util/setFilePickerResponse.js packages/app-desktop/integration-tests/util/setMessageBoxResponse.js diff --git a/.ignore.eslint b/.ignore.eslint index d8abe0c763..1b98a13a3f 100644 --- a/.ignore.eslint +++ b/.ignore.eslint @@ -634,10 +634,14 @@ packages/app-desktop/integration-tests/main.spec.js packages/app-desktop/integration-tests/markdownEditor.spec.js packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js packages/app-desktop/integration-tests/models/ChatPanel.js -packages/app-desktop/integration-tests/models/EditorCodeDialog.js packages/app-desktop/integration-tests/models/GoToAnything.js packages/app-desktop/integration-tests/models/MainScreen.js -packages/app-desktop/integration-tests/models/NoteEditorScreen.js +packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.js +packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.js +packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.js +packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.js +packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.js +packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.js packages/app-desktop/integration-tests/models/NoteList.js packages/app-desktop/integration-tests/models/SettingsScreen.js packages/app-desktop/integration-tests/models/Sidebar.js @@ -658,7 +662,6 @@ packages/app-desktop/integration-tests/util/getMainWindow.js packages/app-desktop/integration-tests/util/ignoreFlakyReporter.js packages/app-desktop/integration-tests/util/mockClipboard.js packages/app-desktop/integration-tests/util/retryOnFailure.js -packages/app-desktop/integration-tests/util/seedProfileSettings.js packages/app-desktop/integration-tests/util/setDarkMode.js packages/app-desktop/integration-tests/util/setFilePickerResponse.js packages/app-desktop/integration-tests/util/setMessageBoxResponse.js diff --git a/packages/app-desktop/integration-tests/chatPanel.spec.ts b/packages/app-desktop/integration-tests/chatPanel.spec.ts index 821a223e30..223e7dc9f5 100644 --- a/packages/app-desktop/integration-tests/chatPanel.spec.ts +++ b/packages/app-desktop/integration-tests/chatPanel.spec.ts @@ -7,8 +7,8 @@ test.describe('chatPanel', () => { await mainScreen.createNewNote('test'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('Testing'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('Testing'); await noteEditor.expectToHaveText('Testing'); await mainScreen.chatPanel.configure(electronApp); diff --git a/packages/app-desktop/integration-tests/goToAnything.spec.ts b/packages/app-desktop/integration-tests/goToAnything.spec.ts index 7ea254cd9e..d5d8fa61cd 100644 --- a/packages/app-desktop/integration-tests/goToAnything.spec.ts +++ b/packages/app-desktop/integration-tests/goToAnything.spec.ts @@ -37,9 +37,11 @@ test.describe('goToAnything', () => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote(''); + const markdownEditor = await mainScreen.noteEditor.showMarkdownEditor(); + const initialFocusLocators: [Locator, boolean][] = [ [mainScreen.noteEditor.noteTitleInput, true], - [mainScreen.noteEditor.codeMirrorEditor, false], + [markdownEditor.container, false], ]; // Focus and start to fill the editor @@ -111,7 +113,7 @@ test.describe('goToAnything', () => { await goToAnything.expectToBeClosed(); // Should have added the link - await expect(mainScreen.noteEditor.codeMirrorEditor).toContainText('[Target note]'); + await mainScreen.noteEditor.expectToHaveText(/\[Target note\]/); }); } }); diff --git a/packages/app-desktop/integration-tests/main.spec.ts b/packages/app-desktop/integration-tests/main.spec.ts index b59d50a104..9207656d83 100644 --- a/packages/app-desktop/integration-tests/main.spec.ts +++ b/packages/app-desktop/integration-tests/main.spec.ts @@ -35,7 +35,8 @@ test.describe('main', () => { await expect(mainScreen.noteList.getNoteItemByTitle('Test note')).toBeVisible(); // Focus the editor - await editor.codeMirrorEditor.click(); + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.container.click(); // Type some text await mainWindow.keyboard.type('# Test note!'); @@ -44,8 +45,8 @@ test.describe('main', () => { await mainWindow.keyboard.type('New note content!'); // Should render - const viewerFrame = editor.getNoteViewerFrameLocator(); - await expect(viewerFrame.locator('h1')).toHaveText('Test note!'); + const viewer = await editor.showNoteViewer(); + await expect(viewer.content.locator('h1')).toHaveText('Test note!'); }); test('mermaid and KaTeX should render', async ({ mainWindow }) => { @@ -53,7 +54,8 @@ test.describe('main', () => { const editor = await mainScreen.createNewNote('🚧 Test 🚧'); const testCommitId = 'bf59b2'; - await editor.focusCodeMirrorEditor(); + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.focusContent(); const noteText = [ '```mermaid', 'gitGraph', @@ -76,32 +78,32 @@ test.describe('main', () => { if (!firstLine) { // Remove any auto-indentation, but avoid pressing shift-tab at // the beginning of the editor. - await mainWindow.keyboard.press('Shift+Tab'); + await markdownEditor.pressKey('Shift+Tab'); } - await mainWindow.keyboard.type(line); + await markdownEditor.typeText(line); } await mainWindow.keyboard.press('Enter'); firstLine = false; } // Should render mermaid - const viewerFrame = editor.getNoteViewerFrameLocator(); + const viewer = await editor.showNoteViewer(); await expect( - viewerFrame.locator('pre.mermaid text', { hasText: testCommitId }), + viewer.content.locator('pre.mermaid text', { hasText: testCommitId }), ).toBeVisible(); // Should render KaTeX (block) // toBeAttached: To be added to the DOM. - await expect(viewerFrame.locator('.joplin-editable > .katex-display').first()).toBeAttached(); + await expect(viewer.content.locator('.joplin-editable > .katex-display').first()).toBeAttached(); await expect( - viewerFrame.locator( + viewer.content.locator( '.katex-display *', { hasText: 'cos' }, ).last(), ).toBeVisible(); // Should render KaTeX (inline) - await expect(viewerFrame.locator('.joplin-editable > .katex').first()).toBeAttached(); + await expect(viewer.content.locator('.joplin-editable > .katex').first()).toBeAttached(); }); test('should correctly resize large images', async ({ electronApp, mainWindow }) => { @@ -109,7 +111,7 @@ test.describe('main', () => { await mainScreen.createNewNote('Image resize test (part 1)'); const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); const filename = 'large-jpg-image-with-exif-rotation.jpg'; await setFilePickerResponse(electronApp, [join(__dirname, 'resources', filename)]); @@ -117,14 +119,14 @@ test.describe('main', () => { // Should be possible to cancel attaching for large images await setMessageBoxResponse(electronApp, /^Cancel/i); await editor.attachFileButton.click(); - await expect(editor.codeMirrorEditor).toHaveText('', { useInnerText: true }); + await editor.expectToHaveText('\n'); // Clicking "No" should not resize await setMessageBoxResponse(electronApp, /^No/i); await editor.attachFileButton.click(); - const viewerFrame = editor.getNoteViewerFrameLocator(); - const renderedImage = viewerFrame + const viewer = await editor.showNoteViewer(); + const renderedImage = viewer.content .getByAltText(filename) // Work around occasional "resolved to 2 elements" errors in CI .last(); @@ -133,7 +135,7 @@ test.describe('main', () => { // To make it easier to find the image (one image per note), we switch to a new, empty note. await mainScreen.createNewNote('Image resize test (part 2)'); - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); // Clicking "Yes" should resize await setMessageBoxResponse(electronApp, /^Yes/i); diff --git a/packages/app-desktop/integration-tests/markdownEditor.spec.ts b/packages/app-desktop/integration-tests/markdownEditor.spec.ts index 6f4680f7bf..4c7f200a4e 100644 --- a/packages/app-desktop/integration-tests/markdownEditor.spec.ts +++ b/packages/app-desktop/integration-tests/markdownEditor.spec.ts @@ -36,15 +36,15 @@ test.describe('markdownEditor', () => { test('editor should render the full content of HTML notes', async ({ mainWindow, electronApp }) => { const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-spans'); - const editor = mainScreen.noteEditor.codeMirrorEditor; + await mainScreen.noteEditor.showMarkdownEditor(); // Regression test: The should not be hidden by inline Markdown rendering (since this is an HTML note): - await expect(editor).toHaveText('

test

'); + await mainScreen.noteEditor.expectToHaveText('

test

'); }); test('preview pane should render images in HTML notes', async ({ mainWindow, electronApp }) => { const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-image'); - const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator(); + const viewerFrame = (await mainScreen.noteEditor.showNoteViewer()).content; // Should render headers await expect(viewerFrame.locator('h1')).toHaveText('Test HTML file!'); @@ -59,12 +59,12 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('PDF attachments'); const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'small-pdf.pdf')]); await editor.attachFileButton.click(); - const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator(); + const viewerFrame = (await mainScreen.noteEditor.showNoteViewer()).content; const pdfLink = viewerFrame.getByText('small-pdf.pdf'); await expect(pdfLink).toBeVisible(); @@ -93,9 +93,8 @@ test.describe('markdownEditor', () => { await expectToBeRendered(); // Should still render after switching editors - await mainScreen.noteEditor.toggleEditorsButton.click(); - await mainScreen.noteEditor.richTextEditor.waitFor(); - await mainScreen.noteEditor.toggleEditorsButton.click(); + await mainScreen.noteEditor.showRichTextEditor(); + await mainScreen.noteEditor.showNoteViewer(); await expectToBeRendered(); }); @@ -105,11 +104,11 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Media attachments'); const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'video.mp4')]); await editor.attachFileButton.click(); - const videoLocator = editor.getNoteViewerFrameLocator().locator('video'); + const videoLocator = (await editor.showNoteViewer()).content.locator('video'); const expectVideoToRender = async () => { await expect(videoLocator).toBeSeekableMediaElement(6.9, 7); }; @@ -117,9 +116,8 @@ test.describe('markdownEditor', () => { await expectVideoToRender(); // Should be able to render again if the editor is closed and re-opened. - await mainScreen.noteEditor.toggleEditorsButton.click(); - await mainScreen.noteEditor.richTextEditor.waitFor(); - await mainScreen.noteEditor.toggleEditorsButton.click(); + await mainScreen.noteEditor.showRichTextEditor(); + await mainScreen.noteEditor.showNoteViewer(); await expectVideoToRender(); }); @@ -131,7 +129,8 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Note 1'); await mainScreen.createNewNote('Note 2'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); + await (await noteEditor.showMarkdownEditor()).focusContent(); + // Escape, then Shift+Tab should focus the toolbar await mainWindow.keyboard.press('Escape'); @@ -168,20 +167,19 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Note'); - await noteEditor.focusCodeMirrorEditor(); + const { editor: markdownEditor, viewer } = await noteEditor.showNoteViewerAndMarkdownEditor(); - await mainWindow.keyboard.type('# Testing'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('This is a test of search. `Test inline code`'); + await markdownEditor.typeText('# Testing'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('This is a test of search. `Test inline code`'); - const viewer = noteEditor.getNoteViewerFrameLocator(); - await expect(viewer.locator('h1')).toHaveText('Testing'); + await expect(viewer.content.locator('h1')).toHaveText('Testing'); - const matches = viewer.locator('mark-ghost'); + const matches = viewer.content.locator('mark-ghost'); await expect(matches).toHaveCount(0); - await mainWindow.keyboard.press(process.platform === 'darwin' ? 'Meta+f' : 'Control+f'); + await markdownEditor.pressKey(process.platform === 'darwin' ? 'Meta+f' : 'Control+f'); await expect(noteEditor.editorSearchInput).toBeVisible(); await noteEditor.editorSearchInput.click(); @@ -200,7 +198,7 @@ test.describe('markdownEditor', () => { // Should continue searching after switching to view-only mode await noteEditor.toggleEditorLayout(); await noteEditor.toggleEditorLayout(); - await expect(noteEditor.codeMirrorEditor).not.toBeVisible(); + await expect(markdownEditor.container).not.toBeVisible(); await expect(noteEditor.editorSearchInput).not.toBeVisible(); await expect(noteEditor.viewerSearchInput).toBeVisible(); @@ -213,7 +211,7 @@ test.describe('markdownEditor', () => { // After showing the viewer again, search should still be hidden await noteEditor.toggleEditorLayout(); - await expect(noteEditor.codeMirrorEditor).toBeVisible(); + await expect(markdownEditor.container).toBeVisible(); await expect(noteEditor.editorSearchInput).not.toBeVisible(); }); @@ -223,30 +221,33 @@ test.describe('markdownEditor', () => { const noteEditor = mainScreen.noteEditor; await mainScreen.createNewNote('Note'); + const { + viewer: markdownViewer, editor: markdownEditor, + } = await noteEditor.showNoteViewerAndMarkdownEditor(); - await noteEditor.focusCodeMirrorEditor(); + await markdownEditor.content.click(); await mainWindow.keyboard.type('test'); - const focusInMarkdownEditor = noteEditor.codeMirrorEditor.locator(':focus'); + const focusInMarkdownEditor = markdownEditor.container.locator(':focus'); await expect(focusInMarkdownEditor).toBeAttached(); + // Use the "toggle layout" menu item to avoid changing focus const toggleEditorLayout = () => activateMainMenuItem(electronApp, 'Toggle editor layout'); // Editor only await toggleEditorLayout(); - await expect(noteEditor.noteViewerContainer).not.toBeVisible(); // Markdown editor should be focused await expect(focusInMarkdownEditor).toBeAttached(); // Viewer only await toggleEditorLayout(); - await expect(noteEditor.codeMirrorEditor).not.toBeVisible(); + await expect(markdownEditor.container).not.toBeVisible(); // Viewer should be focused - await expect(noteEditor.noteViewerContainer).toBeFocused(); + await expect(markdownViewer.container).toBeFocused(); // Viewer and editor await toggleEditorLayout(); - await expect(noteEditor.noteViewerContainer).toBeAttached(); - await expect(noteEditor.codeMirrorEditor).toBeVisible(); + await expect(markdownEditor.container).toBeAttached(); + await expect(markdownViewer.container).toBeVisible(); // Editor should be focused await expect(focusInMarkdownEditor).toBeAttached(); }); @@ -258,20 +259,21 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Note'); - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('# Test'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('Test paragraph.'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('# Test'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('Test paragraph.'); // Wait for rendering - await expect(noteEditor.getNoteViewerFrameLocator().getByText('Test paragraph.')).toBeAttached(); + const noteViewer = await noteEditor.showNoteViewer(); + await expect(noteViewer.content.getByText('Test paragraph.')).toBeAttached(); // Move focus await mainScreen.goToAnything.runCommand(electronApp, 'focusElementNoteViewer'); // Note viewer should be focused - await expect(noteEditor.noteViewerContainer).toBeFocused(); + await expect(noteViewer.container).toBeFocused(); }); test('local file URLs setting should allow loading images from local file URLs', async ({ mainWindow, electronApp }) => { @@ -282,10 +284,10 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Test local file URLs'); const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type(`![Test image](file://${toForwardSlashes(join(__dirname, 'resources', 'test.png'))})`); + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.typeText(`![Test image](file://${toForwardSlashes(join(__dirname, 'resources', 'test.png'))})`); - const renderedImage = editor.getNoteViewerFrameLocator().getByRole('img', { name: 'Test image' }); + const renderedImage = (await editor.showNoteViewer()).content.getByRole('img', { name: 'Test image' }); await expect(renderedImage).toBeAttached(); const imageSize = await getImageSourceSize(renderedImage); @@ -300,17 +302,19 @@ test.describe('markdownEditor', () => { await mainScreen.setup(); await mainScreen.createNewNote('Original'); + const { editor: markdownEditor } = await mainScreen.noteEditor.showNoteViewerAndMarkdownEditor(); + const noteEditor = mainScreen.noteEditor; await noteEditor.hideViewer(); - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('# Test'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('## Test 2'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('### Test 3'); + await markdownEditor.focusContent(); + await markdownEditor.typeText('# Test'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('## Test 2'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('### Test 3'); - const editorContent = await noteEditor.contentLocator(); + const editorContent = markdownEditor.content; // Extract the note ID const note1Locator = mainScreen.noteList.getNoteItemByTitle('Original'); @@ -322,14 +326,14 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Test note links'); // Create a new link to a header - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('[link](:/'); - await mainWindow.keyboard.type(targetNoteId); - await mainWindow.keyboard.type('#test-2'); - await mainWindow.keyboard.type(')'); - await mainWindow.keyboard.press('Enter'); + await markdownEditor.focusContent(); + await markdownEditor.pressKey('Enter'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('[link](:/'); + await markdownEditor.typeText(targetNoteId); + await markdownEditor.typeText('#test-2'); + await markdownEditor.typeText(')'); + await markdownEditor.pressKey('Enter'); // Clicking the link should navigate to note1 const link = editorContent.getByText(/\[?link\]?/); @@ -366,8 +370,8 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Test copy'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('Test content.'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('Test content.'); const { expectClipboardToMatch } = await mockClipboard(electronApp, 'original'); @@ -380,6 +384,8 @@ test.describe('markdownEditor', () => { await mainScreen.waitFor(); await mainScreen.createNewNote('Test paste'); + await mainScreen.noteEditor.showMarkdownEditor(); + const { expectClipboardToMatch } = await mockClipboard(electronApp, 'test!'); await expectClipboardToMatch('test!'); @@ -391,8 +397,8 @@ test.describe('markdownEditor', () => { // Should cut text using the textCut command await mainScreen.createNewNote('Test cut'); - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('Test (new content!)'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('Test (new content!)'); await goToAnything.runCommand(electronApp, 'textCut'); await noteEditor.expectToHaveText('\n'); @@ -415,10 +421,10 @@ test.describe('markdownEditor', () => { await mainScreen.setup(); const noteEditor = await mainScreen.createNewNote('Test Affinity image paste'); - const editorContent = testCase.useLegacyMarkdownEditor ? mainWindow.locator('.rli-editor .CodeMirror5 .CodeMirror-code') : await noteEditor.contentLocator(); - const editorToFocus = testCase.useLegacyMarkdownEditor ? mainWindow.locator('.rli-editor .CodeMirror5') : editorContent; + const markdownEditor = await noteEditor.showMarkdownEditor(); + const editorContent = markdownEditor.container; const noteBody = async () => editorContent.innerText(); - await editorToFocus.click(); + await editorContent.click(); const affinityClipboardText = await readFile(join(__dirname, 'resources', 'affinity.txt'), 'utf8'); @@ -446,7 +452,7 @@ test.describe('markdownEditor', () => { return isImageResource && doesNotContainSvg && doesNotContainBase64; }).toBe(true); - const renderedImage = noteEditor.getNoteViewerFrameLocator().locator('img'); + const renderedImage = (await noteEditor.showNoteViewer()).content.locator('img'); await expect(renderedImage).toHaveCount(1); await expect(await getImageSourceSize(renderedImage.first())).toMatchObject([64, 64]); }); @@ -459,9 +465,9 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Test undo/redo'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); + const markdownEditor = await noteEditor.showMarkdownEditor(); - await mainWindow.keyboard.type('A'); + await markdownEditor.typeText('A'); await noteEditor.expectToHaveText('A'); await activateMainMenuItem(electronApp, 'Undo'); @@ -480,10 +486,10 @@ test.describe('markdownEditor', () => { await mainScreen.createNewNote('Test copy formatting'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('**hello**'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('**hello**'); - const viewerFrame = noteEditor.getNoteViewerFrameLocator(); + const viewerFrame = (await mainScreen.noteEditor.showNoteViewer()).content; await expect(viewerFrame.locator('strong')).toHaveText('hello'); // Double-click selects the text node inside , not itself. diff --git a/packages/app-desktop/integration-tests/models/MainScreen.ts b/packages/app-desktop/integration-tests/models/MainScreen.ts index 9e7ece91ed..69391658b0 100644 --- a/packages/app-desktop/integration-tests/models/MainScreen.ts +++ b/packages/app-desktop/integration-tests/models/MainScreen.ts @@ -1,5 +1,5 @@ import { Page, Locator, ElectronApplication } from '@playwright/test'; -import NoteEditorScreen from './NoteEditorScreen'; +import NoteEditorScreen from './NoteEditor/NoteEditorScreen'; import activateMainMenuItem from '../util/activateMainMenuItem'; import Sidebar from './Sidebar'; import GoToAnything from './GoToAnything'; diff --git a/packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.ts b/packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.ts new file mode 100644 index 0000000000..8f0fc5e9ca --- /dev/null +++ b/packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.ts @@ -0,0 +1,5 @@ +interface BaseEditor { + typeText(text: string): Promise; + waitFor(): Promise; +} +export default BaseEditor; diff --git a/packages/app-desktop/integration-tests/models/EditorCodeDialog.ts b/packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.ts similarity index 100% rename from packages/app-desktop/integration-tests/models/EditorCodeDialog.ts rename to packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.ts diff --git a/packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.ts b/packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.ts new file mode 100644 index 0000000000..0781db6184 --- /dev/null +++ b/packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.ts @@ -0,0 +1,33 @@ +import { Locator } from '@playwright/test'; +import BaseEditor from './BaseEditor'; + +export default class MarkdownEditor implements BaseEditor { + public readonly container: Locator; + public readonly content: Locator; + public constructor(parent: Locator) { + this.container = parent.locator('.CodeMirror'); + this.content = this.container.locator('[contentEditable]'); + } + + public async waitFor() { + await this.container.waitFor(); + } + + public async pressKey(text: string) { + await this.content.press(text); + } + + public async typeText(text: string) { + await this.focusContent(); + await this.content.pressSequentially(text); + } + + public async focusContent() { + // eslint-disable-next-line no-restricted-properties -- The focusHandler wrapper can't be used here, since Playwright's focus needs to be awaited + await this.content.focus(); + } + + public async innerText() { + return this.content.innerText(); + } +} diff --git a/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts b/packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.ts similarity index 55% rename from packages/app-desktop/integration-tests/models/NoteEditorScreen.ts rename to packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.ts index 4ac908bca9..e1a0710f1e 100644 --- a/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts +++ b/packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.ts @@ -1,18 +1,15 @@ import { ElectronApplication, Locator, Page } from '@playwright/test'; -import { expect } from '../util/test'; -import activateMainMenuItem from '../util/activateMainMenuItem'; -import EditorCodeDialog from './EditorCodeDialog'; -import setSettingValue from '../util/setSettingValue'; +import { expect } from '../../util/test'; +import activateMainMenuItem from '../../util/activateMainMenuItem'; +import setSettingValue from '../../util/setSettingValue'; +import NoteViewer from './NoteViewer'; +import MarkdownEditor from './MarkdownEditor'; +import RichTextEditor from './RichTextEditor'; export default class NoteEditorScreen { - public readonly codeMirrorEditor: Locator; - public readonly noteViewerContainer: Locator; public readonly editorPluginFrame: Locator; - public readonly richTextEditor: Locator; public readonly noteTitleInput: Locator; - public readonly richTextCodeEditor: EditorCodeDialog; - public readonly attachFileButton: Locator; public readonly toggleCodeBlockButton: Locator; public readonly toggleEditorsButton: Locator; @@ -25,25 +22,33 @@ export default class NoteEditorScreen { private readonly containerLocator: Locator; + private readonly noteViewer_: NoteViewer; + private readonly markdownEditor_: MarkdownEditor; + private readonly richTextEditor_: RichTextEditor; + public constructor(private page_: Page) { // .rli-editor is used in the main window, .note-editor-wrapper in secondary windows this.containerLocator = page_.locator('.rli-editor, .note-editor-wrapper'); - this.codeMirrorEditor = this.containerLocator.locator('.cm-editor'); - this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]'); + this.noteViewer_ = new NoteViewer(this.containerLocator); + this.markdownEditor_ = new MarkdownEditor(this.containerLocator); + this.richTextEditor_ = new RichTextEditor(this.containerLocator, page_); this.editorPluginFrame = this.containerLocator.locator('iframe[id^="plugin-view-"]'); this.noteTitleInput = this.containerLocator.locator('.title-input'); this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' }); this.toggleCodeBlockButton = this.containerLocator.getByRole('button', { name: 'Code Block' }); - this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' }); + this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors', exact: true }); this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' }); - this.noteViewerContainer = this.containerLocator.locator('iframe[src$="note-viewer/index.html"]'); // The editor and viewer have slightly different search UI this.editorSearchInput = this.containerLocator.getByPlaceholder('Find'); this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...'); this.disableTabNavigationButton = this.containerLocator.getByRole('button', { name: 'Tab moves focus' }); this.toggleEditorPluginButton = this.containerLocator.getByRole('button', { name: 'Toggle editor plugin' }); + } - this.richTextCodeEditor = new EditorCodeDialog(page_); + public async waitFor() { + await this.contentLocator_(); + await this.noteTitleInput.waitFor(); + await this.toggleEditorsButton.waitFor(); } public async undo(electronApp: ElectronApplication) { @@ -54,26 +59,89 @@ export default class NoteEditorScreen { return this.containerLocator.getByRole('button', { name: title }); } - public async switchToRichTextEditor() { - await expect(this.codeMirrorEditor).toBeAttached(); - await this.toggleEditorsButton.click(); - await this.richTextEditor.waitFor(); + private async markdownEditorActive() { + await this.toggleEditorsButton.waitFor(); + return this.toggleEditorsButton.evaluate(element => element.classList.contains('markdown-active')); } - public async contentLocator() { - const richTextBody = this.getRichTextFrameLocator().locator('body'); - const markdownEditor = this.codeMirrorEditor; + private async showMarkdownEditorOrViewer_() { + if (!await this.markdownEditorActive()) { + await this.toggleEditorsButton.click(); + } + + await Promise.race([ + this.markdownEditor_.waitFor().catch(() => {}), + this.noteViewer_.waitFor().catch(() => {}), + ]); + } + + public async showNoteViewerAndMarkdownEditor() { + await this.showMarkdownEditorOrViewer_(); + + const noteViewerVisible = await this.noteViewer_.container.isVisible(); + const noteEditorVisible = await this.markdownEditor_.container.isVisible(); + + if (noteViewerVisible && !noteEditorVisible) { + await this.toggleEditorLayout(); + } else if (!noteViewerVisible && noteEditorVisible) { + await this.toggleEditorLayout(); + await expect(this.noteViewer_.container).toBeVisible(); + await expect(this.markdownEditor_.content).not.toBeVisible(); + await this.toggleEditorLayout(); + } + + await expect(this.noteViewer_.container).toBeVisible(); + await expect(this.markdownEditor_.content).toBeVisible(); + + return { viewer: this.noteViewer_, editor: this.markdownEditor_ }; + } + + public async showNoteViewer() { + await this.showMarkdownEditorOrViewer_(); + + if (!await this.noteViewer_.container.isVisible()) { + await this.toggleEditorLayout(); + } + + return this.noteViewer_; + } + + public async showMarkdownEditor() { + await this.showMarkdownEditorOrViewer_(); + + if (!await this.markdownEditor_.container.isVisible()) { + await this.toggleEditorLayout(); + } + + return this.markdownEditor_; + } + + public async showRichTextEditor() { + if (await this.markdownEditorActive()) { + await this.toggleEditorsButton.click(); + } + await this.richTextEditor_.waitFor(); + return this.richTextEditor_; + } + + private async contentLocator_() { + const richTextBody = this.richTextEditor_.content.locator('body'); + const markdownEditor = this.markdownEditor_.container; + const noteViewer = this.noteViewer_.container; // Work around an issue where .or doesn't work with frameLocators. // See https://github.com/microsoft/playwright/issues/27688#issuecomment-1771403495 await Promise.race([ richTextBody.waitFor({ state: 'visible' }).catch(()=>{}), markdownEditor.waitFor({ state: 'visible' }).catch(()=>{}), + noteViewer.waitFor({ state: 'visible' }).catch(() => {}), ]); if (await richTextBody.isVisible()) { return richTextBody; - } else { + } else if (await markdownEditor.isVisible()) { return markdownEditor; + } else { + return noteViewer; } } @@ -87,7 +155,7 @@ export default class NoteEditorScreen { const expectResult = expect.poll( // Use .innerText: textContent doesn't handle line breaks correctly in the CodeMirror // editor. - async () => (await this.contentLocator()).innerText(), + async () => (await this.contentLocator_()).innerText(), ); // Allow `expected` to be either an exact match (a string) or a pattern if (typeof expected === 'string') { @@ -97,28 +165,6 @@ export default class NoteEditorScreen { } } - public getNoteViewerFrameLocator() { - // The note viewer can change content when the note re-renders. As such, - // a new locator needs to be created after re-renders (and this can't be a - // static property). - return this.noteViewerContainer.frameLocator(':scope'); - } - - public getRichTextFrameLocator() { - // We use frameLocator(':scope') to convert the richTextEditor Locator into - // a FrameLocator. (:scope selects the locator itself). - // https://playwright.dev/docs/api/class-framelocator - return this.richTextEditor.contentFrame(); - } - - public getRichTextEditorBody() { - return this.richTextEditor.contentFrame().locator('body'); - } - - public focusCodeMirrorEditor() { - return this.codeMirrorEditor.click(); - } - public async enableTabNavigation(electronApp: ElectronApplication) { await expect(this.disableTabNavigationButton).not.toBeVisible(); await activateMainMenuItem(electronApp, 'Tab moves focus'); @@ -131,11 +177,6 @@ export default class NoteEditorScreen { await expect(this.disableTabNavigationButton).not.toBeVisible(); } - public async waitFor() { - await this.noteTitleInput.waitFor(); - await this.toggleEditorsButton.waitFor(); - } - public async goBack() { const backButton = this.toolbarButtonLocator('Back'); await expect(backButton).not.toBeDisabled(); @@ -147,20 +188,16 @@ export default class NoteEditorScreen { } public async hideViewer() { - await expect(this.noteViewerContainer).toBeVisible(); + const editorVisible = await this.markdownEditor_.container.isVisible(); + await expect(this.noteViewer_.container).toBeVisible(); await this.toggleEditorLayout(); - await expect(this.noteViewerContainer).not.toBeVisible(); - } - public async getRichTextEditorSearchMatches() { - const body = this.getRichTextEditorBody(); - return body.evaluate(() => { - const highlights = CSS.highlights.get('jop-search-highlight') ?? new Set(); - const result = []; - for (const highlight of highlights) { - result.push(highlight.toString()); - } - return result; - }); + // An additional toggle was needed if initially in viewer-only mode + if (!editorVisible) { + await expect(this.markdownEditor_.container).toBeVisible(); + await this.toggleEditorLayout(); + } + + await expect(this.noteViewer_.container).not.toBeVisible(); } } diff --git a/packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.ts b/packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.ts new file mode 100644 index 0000000000..9cf8ff6780 --- /dev/null +++ b/packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.ts @@ -0,0 +1,15 @@ +import { FrameLocator, Locator } from '@playwright/test'; + +export default class NoteViewer { + public container: Locator; + public content: FrameLocator; + public constructor(parent: Locator) { + this.container = parent.locator('iframe[src$="note-viewer/index.html"]'); + this.content = this.container.contentFrame(); + } + + public async waitFor() { + await this.container.waitFor(); + await this.content.locator('body').waitFor(); + } +} diff --git a/packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.ts b/packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.ts new file mode 100644 index 0000000000..7c32de40c3 --- /dev/null +++ b/packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.ts @@ -0,0 +1,44 @@ +import { FrameLocator, Locator, Page } from '@playwright/test'; +import BaseEditor from './BaseEditor'; +import EditorCodeDialog from './EditorCodeDialog'; + +export default class RichTextEditor implements BaseEditor { + public readonly container: Locator; + public readonly content: FrameLocator; + public readonly codeEditor: EditorCodeDialog; + public readonly body: Locator; + + public constructor(parent: Locator, page: Page) { + this.container = parent.locator('iframe[title="Rich Text Area"]'); + this.content = this.container.contentFrame(); + this.codeEditor = new EditorCodeDialog(page); + this.body = this.content.locator('body'); + } + + public async waitFor() { + await this.container.waitFor(); + await this.content.locator('body').waitFor(); + } + + public async focusContent() { + // eslint-disable-next-line no-restricted-properties -- The focusHandler wrapper can't be used here, since Playwright's focus needs to be awaited + await this.body.focus(); + } + + public async typeText(text: string) { + await this.body.waitFor(); + await this.focusContent(); + await this.body.pressSequentially(text); + } + + public async getSearchMatches() { + return this.body.evaluate(() => { + const highlights = CSS.highlights.get('jop-search-highlight') ?? new Set(); + const result = []; + for (const highlight of highlights) { + result.push(highlight.toString()); + } + return result; + }); + } +} diff --git a/packages/app-desktop/integration-tests/multiWindow.spec.ts b/packages/app-desktop/integration-tests/multiWindow.spec.ts index dea891ca40..21df4410d7 100644 --- a/packages/app-desktop/integration-tests/multiWindow.spec.ts +++ b/packages/app-desktop/integration-tests/multiWindow.spec.ts @@ -1,6 +1,6 @@ -import { test, expect } from './util/test'; +import { test } from './util/test'; import MainScreen from './models/MainScreen'; -import NoteEditorScreen from './models/NoteEditorScreen'; +import NoteEditorScreen from './models/NoteEditor/NoteEditorScreen'; test.describe('multiWindow', () => { // Disabled: Playwright's page.close() triggers a different code path than @@ -23,7 +23,7 @@ test.describe('multiWindow', () => { await mainWindow.waitForTimeout(2000); // Main window should remain stable — no white screen or renderer crash - await expect(await mainPage.noteEditor.contentLocator()).toBeVisible(); + await mainPage.noteEditor.waitFor(); }); test.fixme('should support quickly creating, then closing secondary windows', async ({ mainWindow, electronApp }) => { @@ -47,6 +47,6 @@ test.describe('multiWindow', () => { } // Should not have crashed - await expect(await mainPage.noteEditor.contentLocator()).toBeVisible(); + await mainPage.noteEditor.waitFor(); }); }); diff --git a/packages/app-desktop/integration-tests/noteList.spec.ts b/packages/app-desktop/integration-tests/noteList.spec.ts index ced0b682fb..9742a52d09 100644 --- a/packages/app-desktop/integration-tests/noteList.spec.ts +++ b/packages/app-desktop/integration-tests/noteList.spec.ts @@ -28,11 +28,12 @@ test.describe('noteList', () => { // Typing should not cause the note to disappear const editor = mainScreen.noteEditor; - await editor.codeMirrorEditor.click(); - await mainWindow.keyboard.type('[Testing...](http://example.com/)'); + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.typeText('[Testing...](http://example.com/)'); // Wait to render - await expect(editor.getNoteViewerFrameLocator().locator('a', { hasText: 'Testing...' })).toBeVisible(); + const noteViewer = await editor.showNoteViewer(); + await expect(noteViewer.content.locator('a', { hasText: 'Testing...' })).toBeVisible(); // Updating the title should force the sidebar to update sooner await expect(editor.noteTitleInput).toHaveValue('note-1'); @@ -66,8 +67,8 @@ test.describe('noteList', () => { await expect(noteList.getNoteItemByTitle('test note 2')).not.toBeVisible(); // Should not delete when the editor is focused - await mainScreen.noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('test'); + const markdownEditor = await mainScreen.noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('test'); await pressShiftDelete(); await folderBHeader.click(); diff --git a/packages/app-desktop/integration-tests/pluginApi.spec.ts b/packages/app-desktop/integration-tests/pluginApi.spec.ts index 5a9f7cba4d..77351c1c2a 100644 --- a/packages/app-desktop/integration-tests/pluginApi.spec.ts +++ b/packages/app-desktop/integration-tests/pluginApi.spec.ts @@ -11,8 +11,8 @@ test.describe('pluginApi', () => { await mainScreen.createNewNote('First note'); const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('This content should be overwritten.'); + 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'); @@ -52,6 +52,7 @@ test.describe('pluginApi', () => { 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) { @@ -64,8 +65,7 @@ test.describe('pluginApi', () => { await expect.poll(async () => { await mainScreen.goToAnything.runCommand(app, 'getTestDialogVisibility'); - const editorContent = await editor.contentLocator(); - return editorContent.textContent(); + return markdownEditor.container.textContent(); }).toBe(JSON.stringify({ visible: visible, active: visible, @@ -129,12 +129,13 @@ test.describe('pluginApi', () => { await mainScreen.createNewNote('Test note'); const toggleButton = mainScreen.noteEditor.toggleEditorPluginButton; - // Initially, the toggle button should be visible, and so should the default editor. - await expect(mainScreen.noteEditor.codeMirrorEditor).toBeAttached(); - await toggleButton.click(); + // 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'); @@ -146,7 +147,7 @@ test.describe('pluginApi', () => { // Clicking toggle again should dismiss the editor plugins await toggleButton.click(); - await expect(mainScreen.noteEditor.codeMirrorEditor).toBeAttached(); + await expect(markdownEditor.container).toBeAttached(); await expect(pluginFrame).not.toBeAttached(); }); @@ -156,8 +157,8 @@ test.describe('pluginApi', () => { await mainScreen.createNewNote('Test note'); const noteEditor = mainScreen.noteEditor; - await noteEditor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('Initial content.'); + const markdownEditor = await noteEditor.showMarkdownEditor(); + await markdownEditor.typeText('Initial content.'); const toggleButton = noteEditor.toggleEditorPluginButton; await toggleButton.click(); @@ -174,13 +175,13 @@ test.describe('pluginApi', () => { // Should have saved await toggleButton.click(); const expectedUpdatedText = 'Changed by test-editor-plugin'; - await expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText); + 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 expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText); + await noteEditor.expectToHaveText(expectedUpdatedText); }); test('should support hiding and showing panels', async ({ startAppWithPlugins }) => { @@ -222,8 +223,8 @@ test.describe('pluginApi', () => { // Verify the working plugin is functional const editor = mainScreen.noteEditor; - await editor.focusCodeMirrorEditor(); - await mainWindow.keyboard.type('Should be overwritten.'); + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.typeText('Should be overwritten.'); await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText'); await editor.expectToHaveText('PASS'); }); diff --git a/packages/app-desktop/integration-tests/richTextEditor.spec.ts b/packages/app-desktop/integration-tests/richTextEditor.spec.ts index a54174afd9..3703d73739 100644 --- a/packages/app-desktop/integration-tests/richTextEditor.spec.ts +++ b/packages/app-desktop/integration-tests/richTextEditor.spec.ts @@ -11,18 +11,19 @@ test.describe('richTextEditor', () => { const editor = mainScreen.noteEditor; // Set the note's content - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); // Attach this file to the note (create a resource ID) await setFilePickerResponse(electronApp, [__filename]); await editor.attachFileButton.click(); // Wait to render - const viewerFrame = editor.getNoteViewerFrameLocator(); - await viewerFrame.locator('a[data-from-md]').waitFor(); + const viewerFrame = await editor.showNoteViewer(); + await viewerFrame.content.locator('a[data-from-md]').waitFor(); // Should have an attached resource - const codeMirrorContent = await editor.codeMirrorEditor.innerText(); + let markdownEditor = await editor.showMarkdownEditor(); + const codeMirrorContent = await markdownEditor.innerText(); const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/; expect(codeMirrorContent).toMatch(resourceUrlExpression); @@ -30,22 +31,19 @@ test.describe('richTextEditor', () => { // Create a new note with just an HTML link await mainScreen.createNewNote('Another test'); - await editor.codeMirrorEditor.click(); - await mainWindow.keyboard.type(`HTML Link`); + await markdownEditor.typeText(`HTML Link`); // Switch to the RTE - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.waitFor(); + const richTextEditor = await editor.showRichTextEditor(); // Edit the note to cause the original content to update - await editor.getRichTextFrameLocator().locator('a').click(); + await richTextEditor.body.locator('a').click(); await mainWindow.keyboard.type('Test...'); - await editor.toggleEditorsButton.click(); - await editor.codeMirrorEditor.waitFor(); + markdownEditor = await editor.showMarkdownEditor(); // Note should still contain the resource ID and note title - const finalCodeMirrorContent = await editor.codeMirrorEditor.innerText(); + const finalCodeMirrorContent = await markdownEditor.innerText(); expect(finalCodeMirrorContent).toContain(`:/${resourceId}`); }); @@ -55,7 +53,7 @@ test.describe('richTextEditor', () => { const editor = mainScreen.noteEditor; // Set the note's content - await editor.focusCodeMirrorEditor(); + await editor.showMarkdownEditor(); // Attach this file to the note (create a resource ID) const pathToAttach = join(__dirname, 'resources', 'test-file.txt'); @@ -63,17 +61,16 @@ test.describe('richTextEditor', () => { await editor.attachFileButton.click(); // Wait for it to render - await expect(editor.getNoteViewerFrameLocator().getByText('test-file.txt')).toBeVisible(); + const noteViewer = await editor.showNoteViewer(); + await expect(noteViewer.content.getByText('test-file.txt')).toBeVisible(); // Switch to the RTE - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.waitFor(); - - await editor.richTextEditor.click(); + const richTextEditor = await editor.showRichTextEditor(); + await richTextEditor.body.click(); // Click on the attached file URL const openPathResult = waitForNextOpenPath(electronApp); - const targetLink = editor.getRichTextFrameLocator().getByRole('link', { name: basename(pathToAttach) }); + const targetLink = richTextEditor.body.getByRole('link', { name: basename(pathToAttach) }); if (process.platform === 'darwin') { await targetLink.click({ modifiers: ['Meta'] }); } else { @@ -91,8 +88,8 @@ test.describe('richTextEditor', () => { const editor = mainScreen.noteEditor; // Set the initial content - await editor.codeMirrorEditor.click(); - await mainWindow.keyboard.type([ + const markdownEditor = await editor.showMarkdownEditor(); + await markdownEditor.typeText([ ' { ' alt="test image"', '/>', ].join(' ')); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.press('Enter'); - await mainWindow.keyboard.type('Test secondary paragraph.'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.pressKey('Enter'); + await markdownEditor.typeText('Test secondary paragraph.'); // Switch to the RTE - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.waitFor(); + const richTextEditor = await editor.showRichTextEditor(); - const richTextEditorFrame = editor.getRichTextFrameLocator(); - const testParagraph = richTextEditorFrame.getByText('Test secondary paragraph.'); + const testParagraph = richTextEditor.content.getByText('Test secondary paragraph.'); await expect(testParagraph).toBeAttached(); // Move the cursor just after the image, then press enter. - const testImage = richTextEditorFrame.getByRole('img', { name: 'test image' }); + const testImage = richTextEditor.content.getByRole('img', { name: 'test image' }); await testImage.click(); await mainWindow.keyboard.press('ArrowRight'); await mainWindow.keyboard.press('Enter'); @@ -128,8 +123,8 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing tabs!'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.click(); + const richTextEditor = await editor.showRichTextEditor(); + await richTextEditor.body.click(); await mainWindow.keyboard.type('This is a'); // Tab should add spaces @@ -144,21 +139,21 @@ test.describe('richTextEditor', () => { // Escape then tab should move focus await mainWindow.keyboard.press('Escape'); - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.press('Tab'); - await expect(editor.richTextEditor).not.toBeFocused(); + await expect(richTextEditor.container).not.toBeFocused(); // After re-focusing the editor, Tab should indent again. await mainWindow.keyboard.press('Shift+Tab'); - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.type(' Another:'); await mainWindow.keyboard.press('Tab'); await mainWindow.keyboard.type('!'); // After switching back to the Markdown editor, await expect(editor.toggleEditorsButton).not.toBeDisabled(); - await editor.toggleEditorsButton.click(); - await expect(editor.codeMirrorEditor).toHaveText('This is a test. Test! Another: !'); + const markdownEditor = await editor.showMarkdownEditor(); + await expect(markdownEditor.content).toHaveText('This is a test. Test! Another: !'); }); test('should be possible to disable tab indentation from the menu', async ({ mainWindow, electronApp }) => { @@ -166,23 +161,23 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing keyboard navigation!'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.click(); + const richTextEditor = await editor.showRichTextEditor(); + await richTextEditor.focusContent(); await editor.enableTabNavigation(electronApp); await mainWindow.keyboard.type('This is a'); // Tab should navigate - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.press('Tab'); - await expect(editor.richTextEditor).not.toBeFocused(); + await expect(richTextEditor.container).not.toBeFocused(); await editor.disableTabNavigation(electronApp); // Tab should not navigate - await editor.richTextEditor.click(); + await richTextEditor.container.click(); await mainWindow.keyboard.press('Tab'); - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); }); test('double-clicking a code block should edit it', async ({ mainWindow }) => { @@ -190,16 +185,16 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing code blocks'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); + const richTextEditor = await editor.showRichTextEditor(); // Make the code block await editor.toggleCodeBlockButton.click(); - const codeEditor = editor.richTextCodeEditor; + const codeEditor = richTextEditor.codeEditor; await codeEditor.textArea.fill('This is a test code block!'); await codeEditor.submit(); // Double-clicking the code block should open it - const renderedCode = editor.getRichTextFrameLocator().locator('pre.hljs', { hasText: 'This is a test code block!' }); + const renderedCode = richTextEditor.body.locator('pre.hljs', { hasText: 'This is a test code block!' }); await renderedCode.first().dblclick(); await codeEditor.waitFor(); }); @@ -209,11 +204,10 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing code blocks'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.click(); + const richTextEditor = await editor.showRichTextEditor(); await editor.toggleCodeBlockButton.click(); - const codeEditor = editor.richTextCodeEditor; + const codeEditor = richTextEditor.codeEditor; await codeEditor.waitFor(); // Initially, pressing in the textarea should add a tab @@ -235,23 +229,21 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing keyboard navigation!'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - - await editor.richTextEditor.waitFor(); + const richTextEditor = await editor.showRichTextEditor(); await editor.noteTitleInput.click(); await expect(editor.noteTitleInput).toBeFocused(); await mainWindow.keyboard.press('End'); await mainWindow.keyboard.press('ArrowDown'); - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.press('ArrowUp'); await expect(editor.noteTitleInput).toBeFocused(); await mainWindow.keyboard.press('Enter'); await expect(editor.noteTitleInput).not.toBeFocused(); - await expect(editor.richTextEditor).toBeFocused(); + await expect(richTextEditor.container).toBeFocused(); }); test('note should have correct content even if opened quickly after last edit', async ({ mainWindow }) => { @@ -262,10 +254,7 @@ test.describe('richTextEditor', () => { const test2Header = mainScreen.noteList.getNoteItemByTitle('Test 2'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.waitFor(); - - const editorBody = editor.getRichTextEditorBody(); + const editorBody = (await editor.showRichTextEditor()).body; const setEditorText = async (targetText: string) => { await editorBody.pressSequentially(targetText); await expect(editorBody).toHaveText(targetText); @@ -289,9 +278,7 @@ test.describe('richTextEditor', () => { const test2Header = mainScreen.noteList.getNoteItemByTitle('Test 2'); const editor = mainScreen.noteEditor; - await editor.toggleEditorsButton.click(); - await editor.richTextEditor.waitFor(); - const editorBody = editor.getRichTextEditorBody(); + const editorBody = (await editor.showRichTextEditor()).body; await test1Header.click(); await expect(editor.noteTitleInput).toHaveValue('Test 1'); @@ -315,26 +302,26 @@ test.describe('richTextEditor', () => { await mainScreen.createNewNote('Testing'); const editor = mainScreen.noteEditor; - await editor.switchToRichTextEditor(); - const editorBody = editor.getRichTextEditorBody(); + const richTextEditor = await editor.showRichTextEditor(); + const editorBody = richTextEditor.body; await editorBody.pressSequentially('search-match1 note search-match2'); // Should highlight initial matches await mainScreen.search('/search-match'); await expect.poll( - () => editor.getRichTextEditorSearchMatches(), + () => richTextEditor.getSearchMatches(), ).toEqual(['search-match', 'search-match']); // Should highlight new matches await editorBody.pressSequentially('search-match'); await expect.poll( - () => editor.getRichTextEditorSearchMatches(), + () => richTextEditor.getSearchMatches(), ).toHaveLength(3); // Should stop highlighting old matches await editorBody.press('Backspace'); await expect.poll( - () => editor.getRichTextEditorSearchMatches(), + () => richTextEditor.getSearchMatches(), ).toHaveLength(2); }); }); diff --git a/packages/app-desktop/integration-tests/util/seedProfileSettings.ts b/packages/app-desktop/integration-tests/util/seedProfileSettings.ts deleted file mode 100644 index e496095c45..0000000000 --- a/packages/app-desktop/integration-tests/util/seedProfileSettings.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { join } from 'path'; -import { writeFile } from 'fs-extra'; - -// Writes the profile's settings.json before the app starts, so that these values are -// picked up during startup. Currently used to keep the editor+viewer layout that many -// integration tests rely on -- the app's default only shows the editor. -const seedProfileSettings = async (profileDirectory: string, values: Record) => { - const settingFilePath = join(profileDirectory, 'settings.json'); - await writeFile(settingFilePath, JSON.stringify(values), 'utf8'); -}; - -export default seedProfileSettings; diff --git a/packages/app-desktop/integration-tests/util/test.ts b/packages/app-desktop/integration-tests/util/test.ts index e60132a5fe..7ad31cc0e8 100644 --- a/packages/app-desktop/integration-tests/util/test.ts +++ b/packages/app-desktop/integration-tests/util/test.ts @@ -6,7 +6,6 @@ import createStartupArgs from './createStartupArgs'; import getMainWindow from './getMainWindow'; import setDarkMode from './setDarkMode'; import evaluateWithRetry from './evaluateWithRetry'; -import seedProfileSettings from './seedProfileSettings'; type StartWithPluginsResult = { app: ElectronApplication; mainWindow: Page }; @@ -76,10 +75,6 @@ export const test = base.extend({ const profileSubdir = join(profilePath, uuid.createNano()); await mkdirp(profileSubdir); - // Many integration tests interact with the note viewer, which is hidden by the - // app's default layout (editor only). Force the editor+viewer layout for tests. - await seedProfileSettings(profileSubdir, { noteVisiblePanes: ['editor', 'viewer'] }); - await use(profileSubdir); // For debugging purposes, attach the Joplin log file to the test: diff --git a/packages/app-desktop/integration-tests/wcag.spec.ts b/packages/app-desktop/integration-tests/wcag.spec.ts index 23002844d6..0014eed011 100644 --- a/packages/app-desktop/integration-tests/wcag.spec.ts +++ b/packages/app-desktop/integration-tests/wcag.spec.ts @@ -86,16 +86,19 @@ test.describe('wcag', () => { await expect(folder2).toHaveJSProperty('ariaLevel', '3'); // Should be a sub-folder await mainScreen.createNewNote('Test'); + await mainScreen.noteEditor.showMarkdownEditor(); // Ensure that `:hover` styling is consistent between tests: await mainScreen.noteEditor.noteTitleInput.hover(); await expectNoViolations(mainWindow); - // Should not find issues with the Rich Text Editor - await mainScreen.noteEditor.toggleEditorsButton.click(); - await mainScreen.noteEditor.richTextEditor.click(); + // Should not find issues after showing the note viewer + await mainScreen.noteEditor.showNoteViewer(); + await expectNoViolations(mainWindow); + // Should not find issues with the Rich Text Editor + await mainScreen.noteEditor.showRichTextEditor(); await expectNoViolations(mainWindow); });