From 95dab219b87fada0a8d5ae73f4ff806a6a9e1f10 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 14 May 2026 07:48:04 +0200 Subject: [PATCH] fix: preserve cjk code block copy text --- src/components/SingleEditorView.test.tsx | 25 ++++++++++++++++++++++++ src/components/SingleEditorView.tsx | 2 +- tests/smoke/fenced-code-copy.spec.ts | 16 ++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/components/SingleEditorView.test.tsx b/src/components/SingleEditorView.test.tsx index 65ffd7e3..9714552f 100644 --- a/src/components/SingleEditorView.test.tsx +++ b/src/components/SingleEditorView.test.tsx @@ -796,6 +796,31 @@ describe('SingleEditorView', () => { expect(clipboardData.setData).toHaveBeenCalledWith('text/plain', json) }) + it('copies CJK fenced code text from the selected DOM fragment when selection text is mojibake', async () => { + const text = 'const label = "中文測試"' + const { container } = renderEditorHarness() + const { codeBlock, code } = createCodeBlockFixture(text) + await act(async () => { + container.appendChild(codeBlock) + await Promise.resolve() + }) + + const range = document.createRange() + range.selectNodeContents(code) + const getSelection = vi.spyOn(window, 'getSelection').mockReturnValue({ + rangeCount: 1, + isCollapsed: false, + getRangeAt: () => range, + toString: () => 'const label = "中文測試"', + } as Selection) + + const clipboardData = { setData: vi.fn() } + fireEvent.copy(code, { clipboardData }) + getSelection.mockRestore() + + expect(clipboardData.setData).toHaveBeenCalledWith('text/plain', text) + }) + it('copies fenced code from the code-block action button', async () => { const json = '{\n "id": "Demo"\n}' const writeText = vi.fn().mockResolvedValue(undefined) diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx index d05f4bfb..d0681da1 100644 --- a/src/components/SingleEditorView.tsx +++ b/src/components/SingleEditorView.tsx @@ -421,7 +421,7 @@ function selectedCodeBlockText(options: { const range = selectedCodeBlockRange(options) if (!range) return null - return options.selection?.toString() || range.cloneContents().textContent || '' + return range.cloneContents().textContent || options.selection?.toString() || '' } function selectedEditorRange(selection: Selection | null, container: HTMLElement): Range | null { diff --git a/tests/smoke/fenced-code-copy.spec.ts b/tests/smoke/fenced-code-copy.spec.ts index 337427dd..64327569 100644 --- a/tests/smoke/fenced-code-copy.spec.ts +++ b/tests/smoke/fenced-code-copy.spec.ts @@ -12,6 +12,7 @@ const CODE_COPY_NOTE_RELATIVE_PATH = path.join('note', 'fenced-code-copy.md') const CODE_COPY_NOTE_TITLE = 'Fenced Code Copy' const JSON_SNIPPET = '{\n "id": "Demo",\n "enabled": true\n}' const TYPESCRIPT_SNIPPET = 'const answer = 42\nconsole.log(answer)' +const CJK_SNIPPET = 'const label = "中文測試"\nconsole.log(label)' const RICH_CODE_SELECTOR = '.bn-block-content[data-content-type="codeBlock"] pre code' function writeCodeCopyFixtureNote(tempVaultDir: string) { @@ -35,6 +36,12 @@ Copy this TypeScript exactly: \`\`\`ts ${TYPESCRIPT_SNIPPET} \`\`\` + +Copy this CJK text exactly: + +\`\`\`ts +${CJK_SNIPPET} +\`\`\` `) } @@ -101,6 +108,8 @@ async function selectRawEditorText(page: Page, text: string) { test.describe('Fenced code copy', () => { let tempVaultDir: string + test.setTimeout(30_000) + test.beforeEach(async ({ page }) => { await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) tempVaultDir = createFixtureVaultCopy() @@ -112,14 +121,16 @@ test.describe('Fenced code copy', () => { removeFixtureVaultCopy(tempVaultDir) }) - test('copies rich code blocks from the button and selections without escape backslashes', async ({ page }) => { + test('copies rich code blocks from the button and selections without corruption', async ({ page }) => { const noteList = page.locator('[data-testid="note-list-container"]') const noteItem = noteList.getByText(CODE_COPY_NOTE_TITLE, { exact: true }) await expect(noteItem).toBeVisible({ timeout: 10_000 }) await noteItem.click() + await expect(page.locator(RICH_CODE_SELECTOR)).toHaveCount(3, { timeout: 10_000 }) await expect.poll(() => copyRichCodeBlockFromButton(page, 0)).toBe(JSON_SNIPPET) await expect.poll(() => copyRichCodeBlockFromButton(page, 1)).toBe(TYPESCRIPT_SNIPPET) + await expect.poll(() => copyRichCodeBlockFromButton(page, 2)).toBe(CJK_SNIPPET) await selectRichCodeBlock(page, 0) await expect.poll(() => copySelectedText(page)).toBe(JSON_SNIPPET) @@ -127,6 +138,9 @@ test.describe('Fenced code copy', () => { await selectRichCodeBlock(page, 1) await expect.poll(() => copySelectedText(page)).toBe(TYPESCRIPT_SNIPPET) + await selectRichCodeBlock(page, 2) + await expect.poll(() => copySelectedText(page)).toBe(CJK_SNIPPET) + await openCommandPalette(page) await executeCommand(page, 'Toggle Raw') await selectRawEditorText(page, JSON_SNIPPET)