import { Editor, Node } from '@tiptap/core' import Code from '@tiptap/extension-code' import Document from '@tiptap/extension-document' import Paragraph from '@tiptap/extension-paragraph' import Text from '@tiptap/extension-text' import { afterEach, describe, expect, it } from 'vite-plus/test' // minimal stand-in for a mention-like node. inline atoms expand to %leaf% (6 chars) // in getTextContentFromNodes but only take up 1 position in the doc, // which used to break the input rule range math const InlineAtom = Node.create({ name: 'inlineAtom', group: 'inline', inline: true, atom: true, parseHTML: () => [{ tag: 'span[data-atom]' }], renderHTML: () => ['span', { 'data-atom': '' }], }) // same as above, but with a custom text representation instead of the %leaf% fallback const InlineAtomWithText = Node.create({ name: 'inlineAtomWithText', group: 'inline', inline: true, atom: true, parseHTML: () => [{ tag: 'span[data-atom-text]' }], renderHTML: () => ['span', { 'data-atom-text': '' }], renderText: () => '@mention', }) const createEditor = (content: string) => new Editor({ extensions: [Document, Paragraph, Text, InlineAtom, InlineAtomWithText, Code], content, }) const typeAtEndOfParagraph = (editor: Editor, char: string) => { const pos = editor.state.doc.content.size - 1 editor.commands.setTextSelection(pos) return editor.view.someProp('handleTextInput', f => f(editor.view, pos, pos, char)) } describe('input rules with inline atom nodes', () => { let editor: Editor afterEach(() => { editor.destroy() }) it('does not crash when the match spans an inline atom (#7933)', () => { editor = createEditor('

`a

') expect(() => typeAtEndOfParagraph(editor, '`')).not.toThrow() }) it('does not apply the rule when the match spans an inline atom', () => { editor = createEditor('

`a

') const handled = typeAtEndOfParagraph(editor, '`') // someProp returns undefined when the handler declines the input expect(handled).toBeFalsy() expect(editor.getHTML()).not.toContain('') }) it('does not crash with multiple atoms inside the match', () => { editor = createEditor( '

`a

', ) expect(() => typeAtEndOfParagraph(editor, '`')).not.toThrow() }) it('does not corrupt preceding text when the match spans an atom (#7833)', () => { // with enough text before the backtick the miscalculated range stays positive, // so instead of crashing it used to silently mark/replace the wrong content editor = createEditor('

hello world `a

') expect(() => typeAtEndOfParagraph(editor, '`')).not.toThrow() expect(editor.getHTML()).toBe('

hello world `a

') }) it('does not apply the rule when the match spans an atom with a custom text representation', () => { editor = createEditor('

hello `a

') const handled = typeAtEndOfParagraph(editor, '`') expect(handled).toBeFalsy() expect(editor.getHTML()).toBe('

hello `a

') }) it('still applies input rules to plain text', () => { editor = createEditor('

some `code

') const handled = typeAtEndOfParagraph(editor, '`') expect(handled).toBe(true) expect(editor.getHTML()).toContain('code') }) it('still applies input rules when an atom precedes the match', () => { editor = createEditor('

x `code

') const handled = typeAtEndOfParagraph(editor, '`') expect(handled).toBe(true) expect(editor.getHTML()).toContain('code') }) })