diff --git a/packages/core/src/extensions/code-block.test.ts b/packages/core/src/extensions/code-block.test.ts index cd91414..a29a21e 100644 --- a/packages/core/src/extensions/code-block.test.ts +++ b/packages/core/src/extensions/code-block.test.ts @@ -1,6 +1,7 @@ +import { isFirefox } from '@meowdown/vitest/helpers' import { DOMParser, DOMSerializer } from '@prosekit/pm/model' -import { describe, expect, it } from 'vitest' -import { userEvent } from 'vitest/browser' +import { describe, expect, it, vi } from 'vitest' +import { page, userEvent } from 'vitest/browser' import { setupFixture } from '../testing/index.ts' @@ -95,3 +96,38 @@ describe('dollar fence rules', () => { expect(fixture.doc.child(0).type.name).toBe('paragraph') }) }) + +const codeTokens = page.locate('.ProseMirror pre code [class*="tok-"]') + +describe('typing over code block selections', () => { + it('keeps the typed text over a partial selection', async () => { + using fixture = setupFixture() + const { n } = fixture + fixture.set(n.doc(n.codeBlock({ language: 'js' }, 'const answer = 42'))) + // The bug only triggers once highlight token spans wrap the code text. + await expect.element(codeTokens.first(), { timeout: 15000 }).toBeInTheDocument() + fixture.view.focus() + await userEvent.keyboard('X') + const expected = n.doc(n.codeBlock({ language: 'js' }, 'X2')) + await vi.waitFor(() => { + expect(fixture.doc.eq(expected)).toBe(true) + }) + }) + + it.skipIf( + // Firefox edits the text node in place and is not affected. + isFirefox(), + ).fails('keeps the typed text over the full code text', async () => { + using fixture = setupFixture() + const { n } = fixture + fixture.set(n.doc(n.codeBlock({ language: 'js' }, 'const answer = 42'))) + // The bug only triggers once highlight token spans wrap the code text. + await expect.element(codeTokens.first(), { timeout: 15000 }).toBeInTheDocument() + fixture.view.focus() + await userEvent.keyboard('X') + const expected = n.doc(n.codeBlock({ language: 'js' }, 'X')) + await vi.waitFor(() => { + expect(fixture.doc.eq(expected)).toBe(true) + }) + }) +}) diff --git a/packages/react/src/components/code-block-view.test.tsx b/packages/react/src/components/code-block-view.test.tsx index e1dcfef..aaa5132 100644 --- a/packages/react/src/components/code-block-view.test.tsx +++ b/packages/react/src/components/code-block-view.test.tsx @@ -1,6 +1,8 @@ import '../testing/index.ts' -import { isSafari } from '@meowdown/vitest/helpers' +import { isFirefox, isSafari } from '@meowdown/vitest/helpers' +import { TextSelection } from '@prosekit/pm/state' +import type { EditorView } from '@prosekit/pm/view' import { createRef } from 'react' import { describe, expect, it, vi } from 'vitest' import { render } from 'vitest-browser-react' @@ -160,3 +162,67 @@ describe('code block math preview', () => { }) }) }) + +describe('typing over code block selections', () => { + async function setupHighlightedCodeBlock(onDocChange?: VoidFunction) { + const ref = createRef() + await render( + , + ) + // The bug only triggers once highlight token spans wrap the code text. + await expect.element(tokens.first(), { timeout: 15000 }).toBeInTheDocument() + const view = ref.current?.editor?.view + if (!view) throw new Error('editor not mounted') + const codeStart = 1 + const codeEnd = codeStart + view.state.doc.child(0).content.size + return { ref, view, codeStart, codeEnd } + } + + function selectCodeRange(view: EditorView, anchor: number, head: number) { + view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, anchor, head))) + view.focus() + } + + it('updates the markdown when typing at a caret', async () => { + const { ref, view, codeEnd } = await setupHighlightedCodeBlock() + selectCodeRange(view, codeEnd, codeEnd) + await userEvent.keyboard('X') + await vi.waitFor(() => { + expect(ref.current?.getMarkdown()).toContain('fn main() {}X') + }) + }) + + it('updates the markdown when typing over a partial selection', async () => { + const { ref, view, codeStart, codeEnd } = await setupHighlightedCodeBlock() + selectCodeRange(view, codeStart, codeEnd - 1) + await userEvent.keyboard('X') + await vi.waitFor(() => { + expect(ref.current?.getMarkdown()).toContain('```rust\nX}\n```') + }) + }) + + it.skipIf( + // Firefox edits the text node in place and is not affected. + isFirefox(), + ).fails('updates the markdown when typing over the full code text', async () => { + const { ref, view, codeStart, codeEnd } = await setupHighlightedCodeBlock() + selectCodeRange(view, codeStart, codeEnd) + await userEvent.keyboard('X') + await vi.waitFor(() => { + expect(ref.current?.getMarkdown()).toContain('```rust\nX\n```') + }) + }) + + it.skipIf( + // Firefox edits the text node in place and is not affected. + isFirefox(), + ).fails('notifies onDocChange when typing over the full code text', async () => { + const onDocChange = vi.fn() + const { view, codeStart, codeEnd } = await setupHighlightedCodeBlock(onDocChange) + selectCodeRange(view, codeStart, codeEnd) + await userEvent.keyboard('X') + await vi.waitFor(() => { + expect(onDocChange).toHaveBeenCalled() + }) + }) +})