一些检查失败了
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
513 行
21 KiB
TypeScript
513 行
21 KiB
TypeScript
import { test, expect } from './util/test';
|
|
import MainScreen from './models/MainScreen';
|
|
import { join } from 'path';
|
|
import { readFile } from 'fs-extra';
|
|
import getImageSourceSize from './util/getImageSourceSize';
|
|
import setFilePickerResponse from './util/setFilePickerResponse';
|
|
import activateMainMenuItem from './util/activateMainMenuItem';
|
|
import setSettingValue from './util/setSettingValue';
|
|
import { toForwardSlashes } from '@joplin/utils/path';
|
|
import mockClipboard from './util/mockClipboard';
|
|
import { ElectronApplication, Page } from '@playwright/test';
|
|
import { extractResourceUrls } from '@joplin/lib/urlUtils';
|
|
|
|
const importAndOpenHtmlExport = async (mainWindow: Page, electronApp: ElectronApplication, noteTitle: string) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
|
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
|
await importedFolder.waitFor();
|
|
|
|
// Retry -- focusing the imported-folder may fail in some cases
|
|
await expect(async () => {
|
|
await importedFolder.click();
|
|
|
|
await mainScreen.noteList.focusContent(electronApp);
|
|
|
|
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle(noteTitle);
|
|
await importedHtmlFileItem.click({ timeout: 300 });
|
|
}).toPass();
|
|
|
|
return { mainScreen };
|
|
};
|
|
|
|
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');
|
|
|
|
await mainScreen.noteEditor.showMarkdownEditor();
|
|
// Regression test: The <span> should not be hidden by inline Markdown rendering (since this is an HTML note):
|
|
await mainScreen.noteEditor.expectToHaveText('<p><span style="margin-left: 100px;">test</span></p>');
|
|
});
|
|
|
|
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 = (await mainScreen.noteEditor.showNoteViewer()).content;
|
|
// Should render headers
|
|
await expect(viewerFrame.locator('h1')).toHaveText('Test HTML file!');
|
|
|
|
// Should render images
|
|
const image = viewerFrame.getByAltText('An SVG image.');
|
|
await expect(image).toBeAttached();
|
|
await expect(await getImageSourceSize(image)).toMatchObject([117, 30]);
|
|
});
|
|
|
|
test('preview pane should render PDFs', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.createNewNote('PDF attachments');
|
|
const editor = mainScreen.noteEditor;
|
|
|
|
await editor.showMarkdownEditor();
|
|
|
|
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'small-pdf.pdf')]);
|
|
await editor.attachFileButton.click();
|
|
|
|
const viewerFrame = (await mainScreen.noteEditor.showNoteViewer()).content;
|
|
const pdfLink = viewerFrame.getByText('small-pdf.pdf');
|
|
await expect(pdfLink).toBeVisible();
|
|
|
|
const expectToBeRendered = async () => {
|
|
|
|
// PDF preview should render
|
|
const pdfViewer = viewerFrame.locator('object[data$=".pdf"]');
|
|
// Should create the PDF viewer. Note: This is not sufficient to determine that the PDF viewer
|
|
// has rendered.
|
|
await expect(pdfViewer).toBeAttached();
|
|
|
|
// Verify that the PDF viewer has rendered. This relies on how Chrome/Electron loads custom PDFs
|
|
// in an object.
|
|
// If this breaks due to an Electron upgrade,
|
|
// 1. manually verify that the PDF viewer has loaded and
|
|
// 2. replace this test with a screenshot comparison (https://playwright.dev/docs/test-snapshots)
|
|
await expect.poll(
|
|
() => pdfViewer.evaluate((handle) => {
|
|
const contentDocument = (handle as HTMLObjectElement).contentDocument;
|
|
const pdfViewerAsset = contentDocument.querySelector('link[href*=pdf_embedder]');
|
|
return !!pdfViewerAsset;
|
|
}),
|
|
).toBe(true);
|
|
};
|
|
|
|
await expectToBeRendered();
|
|
|
|
// Should still render after switching editors
|
|
await mainScreen.noteEditor.showRichTextEditor();
|
|
await mainScreen.noteEditor.showNoteViewer();
|
|
|
|
await expectToBeRendered();
|
|
});
|
|
|
|
test('preview pane should render video attachments', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.createNewNote('Media attachments');
|
|
const editor = mainScreen.noteEditor;
|
|
|
|
await editor.showMarkdownEditor();
|
|
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'video.mp4')]);
|
|
await editor.attachFileButton.click();
|
|
|
|
const videoLocator = (await editor.showNoteViewer()).content.locator('video');
|
|
const expectVideoToRender = async () => {
|
|
await expect(videoLocator).toBeSeekableMediaElement(6.9, 7);
|
|
};
|
|
|
|
await expectVideoToRender();
|
|
|
|
// Should be able to render again if the editor is closed and re-opened.
|
|
await mainScreen.noteEditor.showRichTextEditor();
|
|
await mainScreen.noteEditor.showNoteViewer();
|
|
|
|
await expectVideoToRender();
|
|
});
|
|
|
|
test('arrow keys should navigate the toolbar', async ({ mainWindow }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.createNewNote('Note 1');
|
|
await mainScreen.createNewNote('Note 2');
|
|
const noteEditor = mainScreen.noteEditor;
|
|
await (await noteEditor.showMarkdownEditor()).focusContent();
|
|
|
|
|
|
// Escape, then Shift+Tab should focus the toolbar
|
|
await mainWindow.keyboard.press('Escape');
|
|
await mainWindow.keyboard.press('Shift+Tab');
|
|
|
|
// Should focus the first item by default, the "back" arrow (back to "Note 1")
|
|
const firstItemLocator = noteEditor.toolbarButtonLocator('Back');
|
|
await expect(firstItemLocator).toBeFocused();
|
|
|
|
// Left arrow should wrap to the end
|
|
await mainWindow.keyboard.press('ArrowLeft');
|
|
const lastItemLocator = noteEditor.toolbarButtonLocator('Toggle editors');
|
|
await expect(lastItemLocator).toBeFocused();
|
|
|
|
await mainWindow.keyboard.press('ArrowRight');
|
|
await expect(firstItemLocator).toBeFocused();
|
|
|
|
// ArrowRight should skip disabled items (Forward).
|
|
await mainWindow.keyboard.press('ArrowRight');
|
|
await expect(noteEditor.toolbarButtonLocator('Toggle external editing')).toBeFocused();
|
|
|
|
// Home/end should navigate to the first/last items
|
|
await mainWindow.keyboard.press('End');
|
|
await expect(lastItemLocator).toBeFocused();
|
|
|
|
await mainWindow.keyboard.press('Home');
|
|
await expect(firstItemLocator).toBeFocused();
|
|
});
|
|
|
|
test('should sync local search between the viewer and editor', async ({ mainWindow }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
const noteEditor = mainScreen.noteEditor;
|
|
|
|
await mainScreen.createNewNote('Note');
|
|
|
|
const { editor: markdownEditor, viewer } = await noteEditor.showNoteViewerAndMarkdownEditor();
|
|
|
|
await markdownEditor.typeText('# Testing');
|
|
await markdownEditor.pressKey('Enter');
|
|
await markdownEditor.pressKey('Enter');
|
|
await markdownEditor.typeText('This is a test of search. `Test inline code`');
|
|
|
|
await expect(viewer.content.locator('h1')).toHaveText('Testing');
|
|
|
|
const matches = viewer.content.locator('mark-ghost');
|
|
await expect(matches).toHaveCount(0);
|
|
|
|
await markdownEditor.pressKey(process.platform === 'darwin' ? 'Meta+f' : 'Control+f');
|
|
await expect(noteEditor.editorSearchInput).toBeVisible();
|
|
|
|
await noteEditor.editorSearchInput.click();
|
|
await noteEditor.editorSearchInput.fill('test');
|
|
await mainWindow.keyboard.press('Enter');
|
|
|
|
// Should show at least one match in the viewer
|
|
await expect(matches).toHaveCount(3);
|
|
await expect(matches.first()).toBeAttached();
|
|
|
|
// Should show matches in code regions
|
|
await noteEditor.editorSearchInput.fill('inline code');
|
|
await mainWindow.keyboard.press('Enter');
|
|
await expect(matches).toHaveCount(1);
|
|
|
|
// Should continue searching after switching to view-only mode
|
|
await noteEditor.toggleEditorLayout();
|
|
await noteEditor.toggleEditorLayout();
|
|
await expect(markdownEditor.container).not.toBeVisible();
|
|
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
|
await expect(noteEditor.viewerSearchInput).toBeVisible();
|
|
|
|
// Should stop searching after closing the search input
|
|
await noteEditor.viewerSearchInput.click();
|
|
await expect(matches).toHaveCount(1);
|
|
await mainWindow.keyboard.press('Escape');
|
|
await expect(noteEditor.viewerSearchInput).not.toBeVisible();
|
|
await expect(matches).toHaveCount(0);
|
|
|
|
// After showing the viewer again, search should still be hidden
|
|
await noteEditor.toggleEditorLayout();
|
|
await expect(markdownEditor.container).toBeVisible();
|
|
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
|
});
|
|
|
|
test('should move focus when the visible editor panes change', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
const noteEditor = mainScreen.noteEditor;
|
|
|
|
await mainScreen.createNewNote('Note');
|
|
const {
|
|
viewer: markdownViewer, editor: markdownEditor,
|
|
} = await noteEditor.showNoteViewerAndMarkdownEditor();
|
|
|
|
await markdownEditor.content.click();
|
|
await mainWindow.keyboard.type('test');
|
|
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();
|
|
// Markdown editor should be focused
|
|
await expect(focusInMarkdownEditor).toBeAttached();
|
|
|
|
// Viewer only
|
|
await toggleEditorLayout();
|
|
await expect(markdownEditor.container).not.toBeVisible();
|
|
// Viewer should be focused
|
|
await expect(markdownViewer.container).toBeFocused();
|
|
|
|
// Viewer and editor
|
|
await toggleEditorLayout();
|
|
await expect(markdownEditor.container).toBeAttached();
|
|
await expect(markdownViewer.container).toBeVisible();
|
|
// Editor should be focused
|
|
await expect(focusInMarkdownEditor).toBeAttached();
|
|
});
|
|
|
|
test('focusElementNoteViewer should move focus to the viewer', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
const noteEditor = mainScreen.noteEditor;
|
|
|
|
await mainScreen.createNewNote('Note');
|
|
|
|
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
|
|
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(noteViewer.container).toBeFocused();
|
|
});
|
|
|
|
test('local file URLs setting should allow loading images from local file URLs', async ({ mainWindow, electronApp }) => {
|
|
await setSettingValue(electronApp, mainWindow, 'renderer.fileUrls', true);
|
|
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
await mainScreen.createNewNote('Test local file URLs');
|
|
|
|
const editor = mainScreen.noteEditor;
|
|
const markdownEditor = await editor.showMarkdownEditor();
|
|
await markdownEditor.typeText(`)})`);
|
|
|
|
const renderedImage = (await editor.showNoteViewer()).content.getByRole('img', { name: 'Test image' });
|
|
await expect(renderedImage).toBeAttached();
|
|
|
|
const imageSize = await getImageSourceSize(renderedImage);
|
|
expect(imageSize[0]).toBeGreaterThan(0);
|
|
expect(imageSize[1]).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('ctrl-clicking on note links should open the linked note (when the viewer is hidden)', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow);
|
|
// Workaround: Required for extracting content accurately from the Markdown editor
|
|
await mainScreen.noteEditor.disableInlineRendering(electronApp);
|
|
|
|
await mainScreen.setup();
|
|
await mainScreen.createNewNote('Original');
|
|
const { editor: markdownEditor } = await mainScreen.noteEditor.showNoteViewerAndMarkdownEditor();
|
|
|
|
const noteEditor = mainScreen.noteEditor;
|
|
await noteEditor.hideViewer();
|
|
|
|
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 = markdownEditor.content;
|
|
|
|
// Extract the note ID
|
|
const note1Locator = mainScreen.noteList.getNoteItemByTitle('Original');
|
|
await note1Locator.dragTo(editorContent);
|
|
const linkExpression = /\[[^\]]*\]\(:\/([a-z0-9]{32})\)/;
|
|
await noteEditor.expectToHaveText(linkExpression);
|
|
const targetNoteId = (await editorContent.textContent()).match(linkExpression)[1];
|
|
|
|
await mainScreen.createNewNote('Test note links');
|
|
|
|
// Create a new link to a header
|
|
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\]?/);
|
|
await link.click({ modifiers: ['ControlOrMeta'] });
|
|
await expect(noteEditor.noteTitleInput).toHaveValue('Original');
|
|
await noteEditor.expectToHaveText(/^# Test/);
|
|
await expect.poll(() => editorContent.evaluate(async editor => {
|
|
const selection = getSelection();
|
|
return editor.contains(selection.anchorNode);
|
|
})).toBe(true);
|
|
|
|
// The cursor should be positioned on the linked-to header
|
|
await expect.poll(async () => {
|
|
await mainWindow.keyboard.type('[[cursor]]');
|
|
await noteEditor.expectToHaveText(/## Test 2\[\[cursor\]\]/);
|
|
return true;
|
|
}).toBe(true);
|
|
});
|
|
|
|
test('should still support the legacy Markdown editor', async ({ electronApp, mainWindow }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await setSettingValue(electronApp, mainWindow, 'editor.legacyMarkdown', true);
|
|
await mainScreen.createNewNote('Test');
|
|
|
|
// Should show the legacy editor
|
|
await expect(mainWindow.locator('.rli-editor .CodeMirror5')).toBeVisible();
|
|
});
|
|
|
|
test('should support the textCopy command', async ({ electronApp, mainWindow }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.createNewNote('Test copy');
|
|
const noteEditor = mainScreen.noteEditor;
|
|
const markdownEditor = await noteEditor.showMarkdownEditor();
|
|
await markdownEditor.typeText('Test content.');
|
|
|
|
const { expectClipboardToMatch } = await mockClipboard(electronApp, 'original');
|
|
|
|
await mainScreen.goToAnything.runCommand(electronApp, 'textCopy');
|
|
await expectClipboardToMatch('Test content.\n');
|
|
});
|
|
|
|
test('should support the textCut and textPaste commands', async ({ electronApp, mainWindow }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.createNewNote('Test paste');
|
|
await mainScreen.noteEditor.showMarkdownEditor();
|
|
|
|
const { expectClipboardToMatch } = await mockClipboard(electronApp, 'test!');
|
|
await expectClipboardToMatch('test!');
|
|
|
|
// Should paste text using the textPaste command
|
|
const goToAnything = mainScreen.goToAnything;
|
|
await goToAnything.runCommand(electronApp, 'textPaste');
|
|
const noteEditor = mainScreen.noteEditor;
|
|
await noteEditor.expectToHaveText('test!');
|
|
|
|
// Should cut text using the textCut command
|
|
await mainScreen.createNewNote('Test cut');
|
|
const markdownEditor = await noteEditor.showMarkdownEditor();
|
|
await markdownEditor.typeText('Test (new content!)');
|
|
|
|
await goToAnything.runCommand(electronApp, 'textCut');
|
|
await noteEditor.expectToHaveText('\n');
|
|
await expectClipboardToMatch('Test (new content!)\n');
|
|
|
|
// Should paste the content again with textPaste
|
|
await goToAnything.runCommand(electronApp, 'textPaste');
|
|
await noteEditor.expectToHaveText(/^Test \(new content!\)[\n]+/);
|
|
});
|
|
|
|
for (const testCase of [
|
|
{ name: 'CodeMirror 6', useLegacyMarkdownEditor: false },
|
|
{ name: 'CodeMirror 5', useLegacyMarkdownEditor: true },
|
|
]) {
|
|
test(`should paste Affinity Copy Merged clipboard data as an image resource (${testCase.name})`, async ({ electronApp, mainWindow }) => {
|
|
const mainScreen = new MainScreen(mainWindow);
|
|
await setSettingValue(electronApp, mainWindow, 'editor.legacyMarkdown', testCase.useLegacyMarkdownEditor);
|
|
await mainScreen.noteEditor.disableInlineRendering(electronApp);
|
|
await setSettingValue(electronApp, mainWindow, 'imageResizing', 'neverResize');
|
|
await mainScreen.setup();
|
|
|
|
const noteEditor = await mainScreen.createNewNote('Test Affinity image paste');
|
|
const markdownEditor = await noteEditor.showMarkdownEditor();
|
|
const editorContent = markdownEditor.container;
|
|
const noteBody = async () => editorContent.innerText();
|
|
await editorContent.click();
|
|
|
|
const affinityClipboardText = await readFile(join(__dirname, 'resources', 'affinity.txt'), 'utf8');
|
|
|
|
await mainWindow.evaluate((affinityClipboardText: string) => {
|
|
const { clipboard, nativeImage } = require('electron');
|
|
const affinityImageData = affinityClipboardText.match(/data:image\/png;base64,[^"]+/)?.[0];
|
|
if (!affinityImageData) throw new Error('Affinity clipboard text does not contain PNG image data');
|
|
const image = nativeImage.createFromDataURL(affinityImageData);
|
|
if (image.isEmpty()) throw new Error('Could not create image from Affinity clipboard data');
|
|
|
|
clipboard.write({
|
|
text: affinityClipboardText,
|
|
image,
|
|
});
|
|
}, affinityClipboardText);
|
|
|
|
await mainScreen.goToAnything.runCommand(electronApp, 'textPaste');
|
|
|
|
await expect.poll(async () => {
|
|
const currentNoteBody = (await noteBody()).trimEnd();
|
|
const resourceUrls = extractResourceUrls(currentNoteBody);
|
|
const isImageResource = currentNoteBody.startsWith('`);
|
|
const doesNotContainSvg = !currentNoteBody.includes('<svg');
|
|
const doesNotContainBase64 = !currentNoteBody.includes('data:image/png;base64,');
|
|
return isImageResource && doesNotContainSvg && doesNotContainBase64;
|
|
}).toBe(true);
|
|
|
|
const renderedImage = (await noteEditor.showNoteViewer()).content.locator('img');
|
|
await expect(renderedImage).toHaveCount(1);
|
|
await expect(await getImageSourceSize(renderedImage.first())).toMatchObject([64, 64]);
|
|
});
|
|
}
|
|
|
|
test('the undo and redo menu items should work', async ({ mainWindow, electronApp }) => {
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.createNewNote('Test undo/redo');
|
|
|
|
const noteEditor = mainScreen.noteEditor;
|
|
const markdownEditor = await noteEditor.showMarkdownEditor();
|
|
|
|
await markdownEditor.typeText('A');
|
|
await noteEditor.expectToHaveText('A');
|
|
|
|
await activateMainMenuItem(electronApp, 'Undo');
|
|
await noteEditor.expectToHaveText('\n');
|
|
|
|
await activateMainMenuItem(electronApp, 'Redo');
|
|
await noteEditor.expectToHaveText('A');
|
|
});
|
|
|
|
test('copying from the preview pane should not include theme background color and should preserve bold formatting', async ({ mainWindow, electronApp }) => {
|
|
// Set dark theme so background-color would be present in clipboard without the fix
|
|
await setSettingValue(electronApp, mainWindow, 'theme', 2);
|
|
|
|
const mainScreen = await new MainScreen(mainWindow).setup();
|
|
await mainScreen.waitFor();
|
|
|
|
await mainScreen.createNewNote('Test copy formatting');
|
|
const noteEditor = mainScreen.noteEditor;
|
|
const markdownEditor = await noteEditor.showMarkdownEditor();
|
|
await markdownEditor.typeText('**hello**');
|
|
|
|
const viewerFrame = (await mainScreen.noteEditor.showNoteViewer()).content;
|
|
await expect(viewerFrame.locator('strong')).toHaveText('hello');
|
|
|
|
// Double-click selects the text node inside <strong>, not <strong> itself.
|
|
// Without the ancestor re-wrapping fix, <strong> would be dropped.
|
|
await viewerFrame.locator('strong').dblclick();
|
|
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
await mainWindow.keyboard.press(`${modifier}+c`);
|
|
|
|
const clipboardHtml = await mainWindow.evaluate(() => {
|
|
const { clipboard } = require('electron');
|
|
return clipboard.readHTML();
|
|
});
|
|
|
|
expect(clipboardHtml).toContain('hello');
|
|
// Dark theme background (#1D2024) must not leak into clipboard
|
|
expect(clipboardHtml).not.toMatch(/1D2024/i);
|
|
expect(clipboardHtml).toContain('<strong');
|
|
expect(clipboardHtml).toMatch(/font-weight/i);
|
|
});
|
|
});
|