import { Editor } from '@tiptap/core' import Document from '@tiptap/extension-document' import Heading from '@tiptap/extension-heading' import Paragraph from '@tiptap/extension-paragraph' import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table' import Text from '@tiptap/extension-text' import { afterEach, describe, expect, it } from 'vite-plus/test' /** Regression tests for empty ``/`` insertion — https://github.com/ueberdosis/tiptap/issues/6237 */ describe('extension table empty cell/header parsing', () => { const editorElClass = 'tiptap' let editor: Editor | null = null const createEditorEl = () => { const editorEl = document.createElement('div') editorEl.classList.add(editorElClass) document.body.appendChild(editorEl) return editorEl } const getEditorEl = () => document.querySelector(`.${editorElClass}`) const createTableEditor = () => new Editor({ element: createEditorEl(), extensions: [ Document, Text, Paragraph, TableCell, TableHeader, TableRow, Table.configure({ resizable: true, }), ], content: '

', }) afterEach(() => { editor?.destroy() editor = null getEditorEl()?.remove() }) it('inserts a table with an empty cell without throwing, backfilling an empty paragraph', () => { editor = createTableEditor() expect(() => { editor?.commands.insertContentAt(0, '
') }).not.toThrow() const cellNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(cellNode?.type).toBe('tableCell') expect(cellNode?.content).toEqual([{ type: 'paragraph' }]) }) it('inserts a table with an empty header cell without throwing, backfilling an empty paragraph', () => { editor = createTableEditor() expect(() => { editor?.commands.insertContentAt(0, '
') }).not.toThrow() const headerNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(headerNode?.type).toBe('tableHeader') expect(headerNode?.content).toEqual([{ type: 'paragraph' }]) }) it('inserts an empty cell via insertContent without throwing', () => { editor = createTableEditor() expect(() => { editor?.commands.insertContent('
') }).not.toThrow() const cellNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(cellNode?.type).toBe('tableCell') expect(cellNode?.content).toEqual([{ type: 'paragraph' }]) }) it('inserts an empty header cell via insertContent without throwing', () => { editor = createTableEditor() expect(() => { editor?.commands.insertContent('
') }).not.toThrow() const headerNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(headerNode?.type).toBe('tableHeader') expect(headerNode?.content).toEqual([{ type: 'paragraph' }]) }) it('backfills a whitespace-only cell when preserveWhitespace is false', () => { editor = createTableEditor() expect(() => { editor?.commands.insertContentAt(0, '
', { parseOptions: { preserveWhitespace: false }, }) }).not.toThrow() const cellNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(cellNode?.content).toEqual([{ type: 'paragraph' }]) }) it.each([ ['default (no parseOptions)', undefined], ['preserveWhitespace: true', { preserveWhitespace: true as const }], ['preserveWhitespace: "full"', { preserveWhitespace: 'full' as const }], ])('backfills a whitespace-only cell identically to setContent — %s', (_label, parseOptions) => { const tableHTML = '
' // setContent collapses a spaces-only cell too, so no whitespace is lost. const setContentEditor = createTableEditor() setContentEditor.commands.setContent(tableHTML) const reference = setContentEditor.getJSON().content?.[0]?.content?.[0]?.content?.[0]?.content setContentEditor.destroy() editor = createTableEditor() expect(() => { editor?.commands.insertContentAt(0, tableHTML, parseOptions ? { parseOptions } : undefined) }).not.toThrow() const cellContent = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0]?.content expect(cellContent).toEqual(reference) expect(cellContent).toEqual([{ type: 'paragraph' }]) }) it('produces the same empty cell as setContent does', () => { const tableHTML = '
' const setContentEditor = createTableEditor() setContentEditor.commands.setContent(tableHTML) const setContentCell = setContentEditor.getJSON().content?.[0]?.content?.[0]?.content?.[0]?.content setContentEditor.destroy() editor = createTableEditor() editor.commands.insertContentAt(0, tableHTML) const insertContentCell = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0]?.content expect(insertContentCell).toEqual(setContentCell) expect(insertContentCell).toEqual([{ type: 'paragraph' }]) }) it('preserves a non-breaking-space cell instead of treating it as empty', () => { editor = createTableEditor() // trim() would strip the NBSP; it must survive as text, not be backfilled. editor.commands.insertContentAt(0, '
 
') const cellNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(cellNode?.type).toBe('tableCell') expect(cellNode?.content).toEqual([ { type: 'paragraph', content: [{ type: 'text', text: ' ' }] }, ]) }) it('backfills using the schema block when no paragraph node exists', () => { const el = createEditorEl() // A valid schema whose only block is Heading (no Paragraph). const headingOnlyEditor = new Editor({ element: el, extensions: [Document, Text, Heading, TableCell, TableHeader, TableRow, Table], content: '

x

', }) expect(() => { headingOnlyEditor.commands.insertContentAt(0, '
') }).not.toThrow() const cellNode = headingOnlyEditor.getJSON().content?.[0]?.content?.[0]?.content?.[0] // Fill uses the schema's block (heading here), not a hard-coded paragraph. expect(cellNode?.content?.[0]?.type).toBe('heading') headingOnlyEditor.destroy() el.remove() }) it('keeps non-empty cell content intact (regression)', () => { editor = createTableEditor() editor.commands.insertContentAt(0, '
hello
') const cellNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(cellNode?.content).toEqual([ { type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }, ]) }) it('keeps non-empty header content intact (regression)', () => { editor = createTableEditor() editor.commands.insertContentAt(0, '
hello
') const headerNode = editor.getJSON().content?.[0]?.content?.[0]?.content?.[0] expect(headerNode?.content).toEqual([ { type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }, ]) }) it('still parses colspan/colwidth on a multi-cell table (regression)', () => { editor = createTableEditor() editor.commands.insertContentAt( 0, '
NameDescription
', ) const row = editor.getJSON().content?.[0]?.content?.[0] expect(row?.content?.[0]?.attrs?.colwidth).toEqual([200]) expect(row?.content?.[1]?.attrs?.colspan).toBe(2) }) })