import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
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'
describe('colgroup updates after column commands', () => {
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 createEditor = (resizable: boolean) =>
new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
TableCell,
TableHeader,
TableRow,
Table.configure({ resizable }),
],
})
const countCols = (e: Editor) => e.view.dom.querySelectorAll('colgroup > col').length
const countCellsInFirstRow = (e: Editor) => {
const firstRow = e.view.dom.querySelector('tbody > tr')
return firstRow ? firstRow.querySelectorAll('td, th').length : 0
}
// Place cursor inside the first cell of the table so column commands have a target
const focusFirstCell = (e: Editor) => {
e.commands.focus()
e.commands.setTextSelection(3)
}
afterEach(() => {
editor?.destroy()
editor = null
getEditorEl()?.remove()
})
describe('resizable: false (default — bug case)', () => {
it('updates
when a column is deleted', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
expect(countCols(editor)).toBe(3)
expect(countCellsInFirstRow(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.deleteColumn()
expect(countCellsInFirstRow(editor)).toBe(2)
expect(countCols(editor)).toBe(2)
})
it('updates when a column is added after', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
expect(countCols(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.addColumnAfter()
expect(countCellsInFirstRow(editor)).toBe(4)
expect(countCols(editor)).toBe(4)
})
it('updates when a column is added before', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
expect(countCols(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.addColumnBefore()
expect(countCellsInFirstRow(editor)).toBe(4)
expect(countCols(editor)).toBe(4)
})
})
describe('resizable: true (control — should already work)', () => {
it('updates when a column is deleted', () => {
editor = createEditor(true)
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
expect(countCols(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.deleteColumn()
expect(countCellsInFirstRow(editor)).toBe(2)
expect(countCols(editor)).toBe(2)
})
it('updates when a column is added after', () => {
editor = createEditor(true)
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
expect(countCols(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.addColumnAfter()
expect(countCellsInFirstRow(editor)).toBe(4)
expect(countCols(editor)).toBe(4)
})
})
describe('edge cases (resizable: false)', () => {
const getTable = (e: Editor) => e.view.dom.querySelector('table') as HTMLTableElement
it('updates min-width style when columns change', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 2, cols: 3, withHeaderRow: false })
// Default cellMinWidth is 25px → 3 cols × 25 = 75px
expect(getTable(editor).style.minWidth).toBe('75px')
focusFirstCell(editor)
editor.commands.deleteColumn()
expect(getTable(editor).style.minWidth).toBe('50px')
editor.commands.addColumnAfter()
expect(getTable(editor).style.minWidth).toBe('75px')
})
it('keeps consistent after mergeCells (no col change expected)', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 2, cols: 3, withHeaderRow: false })
const initialCols = countCols(editor)
// select two adjacent cells then merge
editor.commands.focus()
editor.commands.setTextSelection({ from: 3, to: 8 })
editor.commands.mergeCells()
// mergeCells uses colspan; column count must not change
expect(countCols(editor)).toBe(initialCols)
})
it('updates in read-only mode when columns change programmatically', () => {
editor = createEditor(false)
editor.commands.insertTable({ rows: 2, cols: 3, withHeaderRow: false })
editor.setEditable(false)
expect(countCols(editor)).toBe(3)
focusFirstCell(editor)
editor.commands.deleteColumn()
expect(countCellsInFirstRow(editor)).toBe(2)
expect(countCols(editor)).toBe(2)
})
it('preserves remaining colwidths when a column is deleted', () => {
editor = createEditor(false)
editor.commands.setContent(
'',
)
const widthsOf = () =>
Array.from(editor!.view.dom.querySelectorAll('colgroup > col')).map(
c => c.style.width,
)
expect(widthsOf()).toEqual(['100px', '200px', '300px'])
// place cursor in the first cell, then delete it
editor.commands.focus()
editor.commands.setTextSelection(3)
editor.commands.deleteColumn()
// remaining widths must be the original 200 and 300, not 100/200 (which would
// indicate widths got reassigned to the wrong columns after the deletion)
expect(widthsOf()).toEqual(['200px', '300px'])
})
})
})