文件
mynode/packages/app-desktop/integration-tests/richTextEditor.spec.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

406 行
16 KiB
TypeScript

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, `<!DOCTYPE html>
<html><body>
<p>Outside outline</p>
<div class="container-outline">
<p>Inside outline</p>
</div>
<div class="container-outline">
<div class="outline-element"><p>Text beside ink</p></div>
<div class="outline-element"><p><span class="ink-text" style="width: 40px; height: 30px;"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=" width="40" height="30"></span></p></div>
</div>
</body></html>
`);
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(`<a href=":/${resourceId}">HTML Link</a>`);
// 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([
'<img',
' src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAEklEQVQIW2P8z8AARBDAiJMDAIzoBf635fcVAAAAAElFTkSuQmCC"',
' width="200"',
' height="200"',
' alt="test image"',
'/>',
].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 <tab> 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);
});
});