hololake-platform/src/components/richEditorInputTransformExtension.test.ts

217 lines
6.8 KiB
TypeScript
Raw Normal View History

import { describe, expect, it, vi } from 'vitest'
import { MARKDOWN_HIGHLIGHT_STYLE } from '../utils/markdownHighlightMarkdown'
import { createRichEditorInputTransformExtension } from './richEditorInputTransform'
import { createRichEditorMarkdownInputTransformExtension } from './richEditorInputTransformExtension'
function createTransaction() {
const transaction = {
addMark: vi.fn(() => transaction),
delete: vi.fn(() => transaction),
insertText: vi.fn(() => transaction),
replaceWith: vi.fn(() => transaction),
scrollIntoView: vi.fn(() => transaction),
}
return transaction
}
function createFixture() {
let beforeInputListener: EventListener | null = null
let beforeText = ''
let parentStart = 0
let arrowPrefix = ''
const transaction = createTransaction()
const mathNode = { nodeSize: 1, type: 'mathInline' }
const highlightMark = { type: { name: MARKDOWN_HIGHLIGHT_STYLE } }
const highlightMarkType = { create: vi.fn(() => highlightMark) }
const view = {
composing: false,
dispatch: vi.fn(),
dom: { isConnected: true },
state: {
doc: {
nodesBetween: vi.fn(),
textBetween: vi.fn(() => arrowPrefix),
},
schema: {
marks: {
[MARKDOWN_HIGHLIGHT_STYLE]: highlightMarkType,
},
nodes: {
mathInline: {
createChecked: vi.fn(() => mathNode),
},
},
},
selection: {
from: 0,
to: 0,
$from: {
depth: 0,
marks: vi.fn(() => []),
node: vi.fn(() => ({ type: { name: 'paragraph', spec: {} } })),
parent: {
isTextblock: true,
textBetween: vi.fn(() => beforeText),
},
parentOffset: 0,
},
},
storedMarks: null,
tr: transaction,
},
}
const dom = {
addEventListener: vi.fn((type: string, listener: EventListener) => {
if (type === 'beforeinput') beforeInputListener = listener
}),
}
const editor = {
_tiptapEditor: { view },
prosemirrorView: view,
}
const extension = createRichEditorMarkdownInputTransformExtension()({ editor: editor as never })
function setCursorText(nextBeforeText: string, nextParentStart = 0) {
beforeText = nextBeforeText
parentStart = nextParentStart
view.state.selection.from = parentStart + beforeText.length
view.state.selection.to = parentStart + beforeText.length
view.state.selection.$from.parentOffset = beforeText.length
}
return {
dom,
fireBeforeInput(event: Partial<InputEvent>) {
if (!beforeInputListener) throw new Error('Combined input transform did not mount beforeinput')
const inputEvent = {
data: null,
inputType: 'insertText',
isComposing: false,
preventDefault: vi.fn(),
...event,
}
beforeInputListener(inputEvent as InputEvent)
return inputEvent
},
highlightMark,
highlightMarkType,
mathNode,
mount() {
const controller = new AbortController()
extension.mount?.({
dom: dom as never,
root: document,
signal: controller.signal,
})
return controller
},
setArrowPrefix(nextArrowPrefix: string) {
arrowPrefix = nextArrowPrefix
},
setCursorText,
transaction,
view,
}
}
describe('createRichEditorMarkdownInputTransformExtension', () => {
it('mounts one beforeinput listener for all markdown input transforms', () => {
const fixture = createFixture()
fixture.mount()
expect(fixture.dom.addEventListener).toHaveBeenCalledTimes(1)
expect(fixture.dom.addEventListener).toHaveBeenCalledWith(
'beforeinput',
expect.any(Function),
expect.objectContaining({
capture: true,
signal: expect.any(AbortSignal),
}),
)
})
it('routes arrow, inline math, and highlight syntax through the shared listener', () => {
const fixture = createFixture()
fixture.mount()
fixture.setArrowPrefix('-')
fixture.setCursorText('-', 0)
const arrowEvent = fixture.fireBeforeInput({ data: '>' })
expect(fixture.transaction.insertText).toHaveBeenCalledWith('→', 0, 1)
expect(fixture.view.dispatch).toHaveBeenLastCalledWith(fixture.transaction)
expect(arrowEvent.preventDefault).toHaveBeenCalledTimes(1)
vi.clearAllMocks()
fixture.setCursorText('Inline $x^2$', 0)
const mathEvent = fixture.fireBeforeInput({ data: ' ' })
expect(fixture.view.state.schema.nodes.mathInline.createChecked).toHaveBeenCalledWith({ latex: 'x^2' })
expect(fixture.transaction.replaceWith).toHaveBeenCalledWith(7, 12, fixture.mathNode)
expect(fixture.transaction.insertText).toHaveBeenCalledWith(' ', 8)
expect(fixture.view.dispatch).toHaveBeenLastCalledWith(fixture.transaction)
expect(mathEvent.preventDefault).toHaveBeenCalledTimes(1)
vi.clearAllMocks()
fixture.setCursorText('Plain ==marked=', 20)
const highlightEvent = fixture.fireBeforeInput({ data: '=' })
expect(fixture.transaction.delete).toHaveBeenNthCalledWith(1, 34, 35)
expect(fixture.transaction.delete).toHaveBeenNthCalledWith(2, 26, 28)
expect(fixture.highlightMarkType.create).toHaveBeenCalledWith()
expect(fixture.transaction.addMark).toHaveBeenCalledWith(26, 32, fixture.highlightMark)
expect(fixture.view.dispatch).toHaveBeenLastCalledWith(fixture.transaction)
expect(highlightEvent.preventDefault).toHaveBeenCalledTimes(1)
})
it('lets Chinese IME punctuation commits bypass rich-editor markdown transforms', () => {
let beforeInputListener: EventListener | null = null
const transaction = createTransaction()
const handleBeforeInput = vi.fn(() => ({
preventDefault: true,
transaction,
}))
const view = {
composing: false,
dispatch: vi.fn(),
dom: { isConnected: true },
}
const dom = {
addEventListener: vi.fn((type: string, listener: EventListener) => {
if (type === 'beforeinput') beforeInputListener = listener
}),
}
const editor = {
_tiptapEditor: { view },
prosemirrorView: view,
}
const extension = createRichEditorInputTransformExtension({
createTransforms: () => [{ handleBeforeInput }],
key: 'testTransform',
})()({ editor: editor as never })
const controller = new AbortController()
extension.mount?.({
dom: dom as never,
root: document,
signal: controller.signal,
})
if (!beforeInputListener) throw new Error('Input transform did not mount beforeinput')
const event = {
data: '',
inputType: 'insertCompositionText',
isComposing: false,
preventDefault: vi.fn(),
} as unknown as InputEvent
beforeInputListener(event)
expect(handleBeforeInput).not.toHaveBeenCalled()
expect(view.dispatch).not.toHaveBeenCalled()
expect(event.preventDefault).not.toHaveBeenCalled()
})
})