import { test, expect } from './util/test'; import MainScreen from './models/MainScreen'; import setFilePickerResponse from './util/setFilePickerResponse'; import waitForNextOpenPath from './util/waitForNextOpenPath'; import { basename, join } from 'path'; import { writeFile } from 'fs-extra'; test.describe('richTextEditor', () => { test('should preserve text size in OneNote outlines with and without ink', async ({ electronApp, mainWindow, profileDirectory }) => { const htmlPath = join(profileDirectory, 'onenote-text.html'); await writeFile(htmlPath, `

Outside outline

Inside outline

Text beside ink

`); const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.importHtmlFile(electronApp, htmlPath); await mainScreen.noteList.getNoteItemByTitle('onenote-text').click(); const richTextEditor = await mainScreen.noteEditor.showRichTextEditor(); const outsideText = richTextEditor.body.getByText('Outside outline'); await expect(outsideText).toBeVisible(); const fontSize = await outsideText.evaluate(element => getComputedStyle(element).fontSize); await expect(richTextEditor.body.locator('.container-outline').getByText('Inside outline')).toHaveCSS('font-size', fontSize); const mixedOutline = richTextEditor.body.locator('.container-outline').filter({ hasText: 'Text beside ink' }); await expect(mixedOutline.locator('.ink-text img')).toHaveCount(1); await expect(mixedOutline.getByText('Text beside ink')).toHaveCSS('font-size', fontSize); const inkFontSize = '8px'; await expect(mixedOutline.locator('p:has(.ink-text)')).toHaveCSS('font-size', inkFontSize); }); test('HTML links should be preserved when editing a note', async ({ electronApp, mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing!'); const editor = mainScreen.noteEditor; // Set the note's content 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 = await editor.showNoteViewer(); await viewerFrame.content.locator('a[data-from-md]').waitFor(); // Should have an attached resource let markdownEditor = await editor.showMarkdownEditor(); const codeMirrorContent = await markdownEditor.innerText(); const resourceUrlExpression = /\[.*\]\(:\/(\w+)\)/; expect(codeMirrorContent).toMatch(resourceUrlExpression); const resourceId = codeMirrorContent.match(resourceUrlExpression)[1]; // Create a new note with just an HTML link await mainScreen.createNewNote('Another test'); await markdownEditor.typeText(`HTML Link`); // Switch to the RTE const richTextEditor = await editor.showRichTextEditor(); // Edit the note to cause the original content to update await richTextEditor.body.locator('a').click(); await mainWindow.keyboard.type('Test...'); markdownEditor = await editor.showMarkdownEditor(); // Note should still contain the resource ID and note title const finalCodeMirrorContent = await markdownEditor.innerText(); expect(finalCodeMirrorContent).toContain(`:/${resourceId}`); }); test('should watch resources for changes when opened with ctrl+click', async ({ electronApp, mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing!'); const editor = mainScreen.noteEditor; // Set the note's content await editor.showMarkdownEditor(); // Attach this file to the note (create a resource ID) const pathToAttach = join(__dirname, 'resources', 'test-file.txt'); await setFilePickerResponse(electronApp, [pathToAttach]); await editor.attachFileButton.click(); // Wait for it to render const noteViewer = await editor.showNoteViewer(); await expect(noteViewer.content.getByText('test-file.txt')).toBeVisible(); // Switch to the RTE const richTextEditor = await editor.showRichTextEditor(); await richTextEditor.body.click(); // Click on the attached file URL const openPathResult = waitForNextOpenPath(electronApp); const targetLink = richTextEditor.body.getByRole('link', { name: basename(pathToAttach) }); if (process.platform === 'darwin') { await targetLink.click({ modifiers: ['Meta'] }); } else { await targetLink.click({ modifiers: ['Control'] }); } // Should watch the file await mainWindow.getByText(/^The following attachments are being watched for changes/i).waitFor(); expect(await openPathResult).toContain(basename(pathToAttach)); }); test('should not remove text when pressing [enter] at the end of a line with an image', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing pressing enter!'); const editor = mainScreen.noteEditor; // Set the initial content const markdownEditor = await editor.showMarkdownEditor(); await markdownEditor.typeText([ '', ].join(' ')); await markdownEditor.pressKey('Enter'); await markdownEditor.pressKey('Enter'); await markdownEditor.typeText('Test secondary paragraph.'); // Switch to the RTE const richTextEditor = await editor.showRichTextEditor(); const testParagraph = richTextEditor.content.getByText('Test secondary paragraph.'); await expect(testParagraph).toBeAttached(); // Move the cursor just after the image, then press enter. const testImage = richTextEditor.content.getByRole('img', { name: 'test image' }); await testImage.click(); await mainWindow.keyboard.press('ArrowRight'); await mainWindow.keyboard.press('Enter'); // Should not have removed the image or the test paragraph. await expect(testImage).toBeAttached(); await expect(testParagraph).toBeAttached(); }); test('pressing Tab should indent', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing tabs!'); const editor = mainScreen.noteEditor; const richTextEditor = await editor.showRichTextEditor(); await richTextEditor.body.click(); await mainWindow.keyboard.type('This is a'); // Tab should add spaces await mainWindow.keyboard.press('Tab'); await mainWindow.keyboard.type('test.'); // Shift-tab should remove spaces await mainWindow.keyboard.press('Tab'); await mainWindow.keyboard.press('Tab'); await mainWindow.keyboard.press('Shift+Tab'); await mainWindow.keyboard.type('Test!'); // Escape then tab should move focus await mainWindow.keyboard.press('Escape'); await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.press('Tab'); await expect(richTextEditor.container).not.toBeFocused(); // After re-focusing the editor, Tab should indent again. await mainWindow.keyboard.press('Shift+Tab'); 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(); 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 }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing keyboard navigation!'); const editor = mainScreen.noteEditor; const richTextEditor = await editor.showRichTextEditor(); await richTextEditor.focusContent(); await editor.enableTabNavigation(electronApp); await mainWindow.keyboard.type('This is a'); // Tab should navigate await expect(richTextEditor.container).toBeFocused(); await mainWindow.keyboard.press('Tab'); await expect(richTextEditor.container).not.toBeFocused(); await editor.disableTabNavigation(electronApp); // Tab should not navigate await richTextEditor.container.click(); await mainWindow.keyboard.press('Tab'); await expect(richTextEditor.container).toBeFocused(); }); test('double-clicking a code block should edit it', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing code blocks'); const editor = mainScreen.noteEditor; const richTextEditor = await editor.showRichTextEditor(); // Make the code block await editor.toggleCodeBlockButton.click(); 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 = richTextEditor.body.locator('pre.hljs', { hasText: 'This is a test code block!' }); await renderedCode.first().dblclick(); await codeEditor.waitFor(); }); test('disabling tab indentation should also disable it in code dialogs', async ({ mainWindow, electronApp }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing code blocks'); const editor = mainScreen.noteEditor; const richTextEditor = await editor.showRichTextEditor(); await editor.toggleCodeBlockButton.click(); const codeEditor = richTextEditor.codeEditor; await codeEditor.waitFor(); // Initially, pressing in the textarea should add a tab await codeEditor.textArea.click(); await mainWindow.keyboard.press('Tab'); await expect(codeEditor.textArea).toHaveValue('\t'); await expect(codeEditor.textArea).toBeFocused(); await editor.enableTabNavigation(electronApp); // After enabling tab navigation, pressing tab should navigate. await expect(codeEditor.textArea).toBeFocused(); await mainWindow.keyboard.press('Tab'); await expect(codeEditor.textArea).not.toBeFocused(); }); test('should be possible to navigate between the note title and rich text editor with enter/down/up keys', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing keyboard navigation!'); const editor = mainScreen.noteEditor; 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(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(richTextEditor.container).toBeFocused(); }); test('note should have correct content even if opened quickly after last edit', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test 1'); await mainScreen.createNewNote('Test 2'); const test1Header = mainScreen.noteList.getNoteItemByTitle('Test 1'); const test2Header = mainScreen.noteList.getNoteItemByTitle('Test 2'); const editor = mainScreen.noteEditor; const editorBody = (await editor.showRichTextEditor()).body; const setEditorText = async (targetText: string) => { await editorBody.pressSequentially(targetText); await expect(editorBody).toHaveText(targetText); }; await test1Header.click(); await expect(editorBody).toHaveText(''); await setEditorText('Test 1'); await test2Header.click(); // Previously, after switching to note 2, the "Test 1" text would remain present in the // editor. await expect(editorBody).toHaveText(''); }); test('should save rich text changes when switching notes immediately after typing', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Test 1'); await mainScreen.createNewNote('Test 2'); const test1Header = mainScreen.noteList.getNoteItemByTitle('Test 1'); const test2Header = mainScreen.noteList.getNoteItemByTitle('Test 2'); const editor = mainScreen.noteEditor; const editorBody = (await editor.showRichTextEditor()).body; await test1Header.click(); await expect(editor.noteTitleInput).toHaveValue('Test 1'); await expect(editorBody).toHaveText(''); await expect(editor.toggleEditorsButton).toBeEnabled(); await editorBody.pressSequentially('Unsaved text'); // This confirms that onWillChange has reached the parent without waiting for // TinyMCE's delayed onChange/save. await expect(editor.toggleEditorsButton).toBeDisabled(); await test2Header.click(); await expect(editor.noteTitleInput).toHaveValue('Test 2'); await expect(editorBody).toHaveText(''); await test1Header.click(); await expect(editor.noteTitleInput).toHaveValue('Test 1'); await expect(editorBody).toHaveText('Unsaved text'); }); test('should not drop empty lines made by pressing Enter at the start of a heading', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Other note'); await mainScreen.createNewNote('Heading note'); const otherNoteHeader = mainScreen.noteList.getNoteItemByTitle('Other note'); const headingNoteHeader = mainScreen.noteList.getNoteItemByTitle('Heading note'); const editor = mainScreen.noteEditor; const markdownEditor = await editor.showMarkdownEditor(); await markdownEditor.typeText('Text above'); await markdownEditor.pressKey('Enter'); await markdownEditor.pressKey('Enter'); await markdownEditor.typeText('## Heading'); const richTextEditor = await editor.showRichTextEditor(); const headings = richTextEditor.content.locator('h2'); await expect(headings).toHaveCount(1); await headings.getByText('Heading').click(); await mainWindow.keyboard.press('Home'); await mainWindow.keyboard.press('Enter'); await mainWindow.keyboard.press('Enter'); await expect(headings).toHaveCount(3); const switchToOtherNoteAndBack = async (expectedText: string) => { await otherNoteHeader.click(); await expect(editor.noteTitleInput).toHaveValue('Other note'); await headingNoteHeader.click(); await expect(editor.noteTitleInput).toHaveValue('Heading note'); await expect(richTextEditor.body).toContainText(expectedText); }; await switchToOtherNoteAndBack('Text above'); await expect(headings).toHaveCount(3); // Saving again after the note was reloaded from Markdown, when the empty headings can get an id await richTextEditor.content.getByText('Text above').click(); await mainWindow.keyboard.press('End'); await mainWindow.keyboard.type('!'); await switchToOtherNoteAndBack('Text above!'); await expect(headings).toHaveCount(3); }); test('should highlight search matches', async ({ mainWindow }) => { const mainScreen = await new MainScreen(mainWindow).setup(); await mainScreen.createNewNote('Testing'); const editor = mainScreen.noteEditor; const richTextEditor = await editor.showRichTextEditor(); const editorBody = richTextEditor.body; await editorBody.pressSequentially('search-match1 note search-match2'); await expect(editorBody).toHaveText('search-match1 note search-match2'); // Should highlight initial matches await mainScreen.search('/search-match'); await expect.poll( () => richTextEditor.getSearchMatches(), { timeout: 30_000 }, ).toEqual(['search-match', 'search-match']); // Should highlight new matches await editorBody.pressSequentially('search-match'); await expect.poll( () => richTextEditor.getSearchMatches(), ).toHaveLength(3); // Should stop highlighting old matches await editorBody.press('Backspace'); await expect.poll( () => richTextEditor.getSearchMatches(), ).toHaveLength(2); }); });