snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
489
packages/extension-ruby-text/tests/integration/rubyText.test.ts
Normal file
489
packages/extension-ruby-text/tests/integration/rubyText.test.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
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'
|
||||
|
||||
import { RubyText, type RubyTextOptions } from '../../src/ruby-text.js'
|
||||
|
||||
let container: HTMLElement | undefined
|
||||
|
||||
function createEditor(content = '<p></p>', options: Partial<RubyTextOptions> = {}) {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
container = element
|
||||
|
||||
return new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, RubyText.configure(options)],
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
describe('RubyText', () => {
|
||||
let editor: Editor
|
||||
|
||||
afterEach(() => {
|
||||
editor?.destroy()
|
||||
container?.remove()
|
||||
container = undefined
|
||||
})
|
||||
|
||||
it('stores base text in the document and annotation in mark attributes', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
expect(editor.getJSON()).toEqual({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '漢字',
|
||||
marks: [{ type: 'rubyText', attrs: { rt: 'かんじ' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('sets, toggles, and unsets the ruby mark', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
expect(editor.isActive('rubyText')).toBe(true)
|
||||
|
||||
editor.commands.selectAll()
|
||||
editor.commands.toggleRubyText({ rt: 'かんじ' })
|
||||
expect(editor.isActive('rubyText')).toBe(false)
|
||||
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
editor.commands.selectAll()
|
||||
editor.commands.unsetRubyText()
|
||||
expect(editor.getHTML()).toBe('<p>漢字</p>')
|
||||
})
|
||||
|
||||
it('updates the annotation on an existing mark', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ(新)' })
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ(新)')
|
||||
})
|
||||
|
||||
it('renders a non-editable rt element', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
|
||||
expect(editor.getHTML()).toContain('<rt contenteditable="false">かんじ</rt>')
|
||||
expect(rt.contentEditable).toBe('false')
|
||||
expect(rt.parentElement?.tagName).toBe('RUBY')
|
||||
})
|
||||
|
||||
it('applies configured HTML attributes to the mark view', () => {
|
||||
editor = createEditor('<p>漢字</p>', { HTMLAttributes: { class: 'ruby-text' } })
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
expect(editor.view.dom.querySelector('ruby')?.className).toBe('ruby-text')
|
||||
})
|
||||
|
||||
it('preserves an empty annotation', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: '' })
|
||||
|
||||
expect(editor.getHTML()).not.toContain('data-rt')
|
||||
expect(editor.getHTML()).toContain('<rt contenteditable="false"></rt>')
|
||||
})
|
||||
|
||||
it('does not render an annotation when it is null', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: null })
|
||||
|
||||
expect(editor.getHTML()).toBe('<p><ruby><rb>漢字</rb></ruby></p>')
|
||||
})
|
||||
|
||||
it('updates an annotation after submitting its inline editor', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
const input = rt.querySelector('input') as HTMLInputElement
|
||||
input.value = 'かんじ(新)'
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }))
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ(新)')
|
||||
expect(editor.view.dom.querySelector('ruby > rt')?.textContent).toBe('かんじ(新)')
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('closes the editor when submitting an unchanged annotation', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
const input = rt.querySelector('input') as HTMLInputElement
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }))
|
||||
|
||||
expect(rt.querySelector('input')).toBeNull()
|
||||
expect(rt.textContent).toBe('かんじ')
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('does not open an inline editor when click editing is disabled', () => {
|
||||
editor = createEditor('<p>漢字</p>', { allowClickToEdit: false })
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(rt.querySelector('input')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not open an inline editor when the editor is not editable', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
editor.setEditable(false)
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(rt.querySelector('input')).toBeNull()
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
})
|
||||
|
||||
it('discards an annotation edit when dismissed', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
const input = rt.querySelector('input') as HTMLInputElement
|
||||
input.value = 'かんじ(新)'
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }))
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
expect(editor.getHTML()).not.toContain('かんじ(新)')
|
||||
expect(rt.textContent).toBe('かんじ')
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('discards an annotation edit when it loses focus', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
const input = rt.querySelector('input') as HTMLInputElement
|
||||
input.value = 'かんじ(新)'
|
||||
input.blur()
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
expect(editor.getHTML()).not.toContain('かんじ(新)')
|
||||
expect(rt.textContent).toBe('かんじ')
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('focuses the default input after opening the inline editor', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(document.activeElement).toBe(rt.querySelector('input'))
|
||||
})
|
||||
|
||||
it('ignores Enter while an IME composition is in progress', () => {
|
||||
editor = createEditor('<p>漢字</p>')
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
const input = rt.querySelector('input') as HTMLInputElement
|
||||
input.value = 'かんじ(新)'
|
||||
input.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { bubbles: true, key: 'Enter', isComposing: true }),
|
||||
)
|
||||
|
||||
expect(editor.getHTML()).not.toContain('かんじ(新)')
|
||||
expect(rt.querySelector('input')).toBe(input)
|
||||
})
|
||||
|
||||
it('renders a custom annotation editor instead of the default input', () => {
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: () => {
|
||||
const button = document.createElement('button')
|
||||
button.dataset.testid = 'custom-editor'
|
||||
return button
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(rt.querySelector('[data-testid="custom-editor"]')).not.toBeNull()
|
||||
expect(rt.querySelector('input')).toBeNull()
|
||||
})
|
||||
|
||||
it('focuses the autofocus control inside a custom editor', () => {
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: () => {
|
||||
const wrapper = document.createElement('span')
|
||||
const input = document.createElement('input')
|
||||
input.autofocus = true
|
||||
wrapper.appendChild(input)
|
||||
return wrapper
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(document.activeElement).toBe(rt.querySelector('input'))
|
||||
})
|
||||
|
||||
it('passes the annotation and editor instance to the custom editor', () => {
|
||||
let receivedAnnotation: string | undefined
|
||||
let receivedEditor: Editor | undefined
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: ({ annotation, editor: editorInstance }) => {
|
||||
receivedAnnotation = annotation
|
||||
receivedEditor = editorInstance
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(receivedAnnotation).toBe('かんじ')
|
||||
expect(receivedEditor).toBe(editor)
|
||||
})
|
||||
|
||||
it('updates the annotation when a custom editor submits a value', () => {
|
||||
let submit: ((value: string) => void) | undefined
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: props => {
|
||||
submit = props.submit
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
submit?.('かんじ(新)')
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ(新)')
|
||||
expect(editor.view.dom.querySelector('ruby > rt')?.textContent).toBe('かんじ(新)')
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('restores the annotation when a custom editor dismisses', () => {
|
||||
let dismiss: (() => void) | undefined
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: props => {
|
||||
dismiss = props.dismiss
|
||||
const span = document.createElement('span')
|
||||
span.dataset.testid = 'custom-editor'
|
||||
return span
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
dismiss?.()
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
expect(rt.textContent).toBe('かんじ')
|
||||
expect(rt.querySelector('[data-testid="custom-editor"]')).toBeNull()
|
||||
expect(editor.state.selection.from).toBe(3)
|
||||
})
|
||||
|
||||
it('ignores submit and dismiss after the editor is closed', () => {
|
||||
let submit: ((value: string) => void) | undefined
|
||||
let dismiss: (() => void) | undefined
|
||||
let updates = 0
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: props => {
|
||||
submit = props.submit
|
||||
dismiss = props.dismiss
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
editor.on('update', () => {
|
||||
updates += 1
|
||||
})
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
submit?.('かんじ(新)')
|
||||
submit?.('かんじ(無視)')
|
||||
dismiss?.()
|
||||
|
||||
expect(updates).toBe(1)
|
||||
expect(editor.getHTML()).toContain('かんじ(新)')
|
||||
expect(editor.getHTML()).not.toContain('かんじ(無視)')
|
||||
})
|
||||
|
||||
it('dismisses instead of submitting when the editor became non-editable', () => {
|
||||
let submit: ((value: string) => void) | undefined
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: props => {
|
||||
submit = props.submit
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
editor.setEditable(false)
|
||||
|
||||
expect(submit).toBeDefined()
|
||||
submit?.('かんじ(新)')
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
expect(editor.getHTML()).not.toContain('かんじ(新)')
|
||||
})
|
||||
|
||||
it('ignores submit when the document changed while editing', () => {
|
||||
let submit: ((value: string) => void) | undefined
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
renderAnnotationEditor: props => {
|
||||
submit = props.submit
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(submit).toBeDefined()
|
||||
|
||||
// Shift the ruby range so the captured submit becomes stale.
|
||||
editor.commands.insertContentAt(1, 'あ')
|
||||
submit?.('かんじ(新)')
|
||||
|
||||
expect(editor.getHTML()).toContain('かんじ')
|
||||
expect(editor.getHTML()).not.toContain('かんじ(新)')
|
||||
})
|
||||
|
||||
it('does not invoke the custom editor when click editing is disabled', () => {
|
||||
let invoked = false
|
||||
|
||||
editor = createEditor('<p>漢字</p>', {
|
||||
allowClickToEdit: false,
|
||||
renderAnnotationEditor: () => {
|
||||
invoked = true
|
||||
return document.createElement('span')
|
||||
},
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
editor.commands.setRubyText({ rt: 'かんじ' })
|
||||
|
||||
const rt = editor.view.dom.querySelector('ruby > rt') as HTMLElement
|
||||
rt.click()
|
||||
|
||||
expect(invoked).toBe(false)
|
||||
})
|
||||
|
||||
it('parses ruby HTML with rb and rt elements', () => {
|
||||
editor = createEditor('<p><ruby><rb>漢字</rb><rt>かんじ</rt></ruby></p>')
|
||||
|
||||
expect(editor.getJSON()).toMatchObject({
|
||||
content: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
text: '漢字',
|
||||
marks: [{ type: 'rubyText', attrs: { rt: 'かんじ' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('parses native ruby HTML without an rb element', () => {
|
||||
editor = createEditor('<p><ruby>東京<rt>とうきょう</rt></ruby>は日本の首都です。</p>')
|
||||
|
||||
expect(editor.getJSON()).toMatchObject({
|
||||
content: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
text: '東京',
|
||||
marks: [{ type: 'rubyText', attrs: { rt: 'とうきょう' } }],
|
||||
},
|
||||
{
|
||||
text: 'は日本の首都です。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not parse ruby HTML without an annotation as a mark', () => {
|
||||
editor = createEditor('<p><ruby>漢字</ruby></p>')
|
||||
|
||||
expect(editor.getJSON()).toEqual({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: '漢字' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user