Files
edentext/tests/unit/table-header-row.test.ts

126 lines
5.3 KiB
TypeScript

// Unit test for the header-row styling toggle: drives toggleHeaderRowStyle against a
// ProseMirror EditorState and checks the first row gains/loses bold + the header fill.
import { describe, it, expect } from 'vitest';
import { getSchema } from '@tiptap/core';
import { EditorState, TextSelection } from '@tiptap/pm/state';
import type { Node as PMNode } from '@tiptap/pm/model';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import Bold from '@tiptap/extension-bold';
import { Table, TableHeader, TableCell } from '@tiptap/extension-table';
import { ResizableTableRow } from '../../src/lib/editor/extensions/tableRow';
import { TableCellBackground } from '../../src/lib/editor/extensions/tableCellBackground';
import {
HEADER_SHADE,
isHeaderStyled,
toggleHeaderStyle,
type HeaderAxis,
} from '../../src/lib/editor/extensions/tableHeaderRow';
type N = any;
const schema = getSchema([
Document, Paragraph, Text, Bold, Table, ResizableTableRow, TableCell, TableHeader,
TableCellBackground,
]);
const cell = (text: string): N => ({
type: 'tableCell',
attrs: { colspan: 1, rowspan: 1, colwidth: null },
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
});
const row = (...cells: N[]): N => ({ type: 'tableRow', content: cells });
function makeState(): EditorState {
const doc = schema.nodeFromJSON({
type: 'doc',
content: [{ type: 'table', content: [row(cell('A'), cell('B')), row(cell('c'), cell('d'))] }],
});
// Cursor in the first cell.
let pos = -1;
doc.descendants((n: PMNode, p: number) => { if (pos < 0 && n.isText) pos = p; });
return EditorState.create({ schema, doc, selection: TextSelection.create(doc, pos) });
}
// Apply the command factory to a state, returning the next state.
function run(state: EditorState, axis: HeaderAxis = 'row'): EditorState {
let next: EditorState | null = null;
toggleHeaderStyle(axis)({ state, tr: state.tr, dispatch: (tr: any) => { next = state.apply(tr); } } as any);
return next ?? state;
}
const isHeaderRowStyled = (s: EditorState) => isHeaderStyled(s, 'row');
const firstRow = (state: EditorState): N => state.doc.toJSON().content[0].content[0];
const boldMarks = (c: N): boolean => (c.content[0].content[0]?.marks ?? []).some((m: N) => m.type === 'bold');
describe('toggleHeaderRowStyle', () => {
it('toggles the header fill on the first row (bold is presentational/CSS)', () => {
let state = makeState();
expect(isHeaderRowStyled(state)).toBe(false);
// Toggle ON: only the fill marker is set (bold comes from the .cell-header CSS).
state = run(state);
expect(isHeaderRowStyled(state)).toBe(true);
for (const c of firstRow(state).content) {
expect(c.attrs.backgroundColor).toBe(HEADER_SHADE);
expect(boldMarks(c)).toBe(false); // no stored bold mark — CSS handles it
}
// Second row untouched.
expect(state.doc.toJSON().content[0].content[1].content[0].attrs.backgroundColor == null).toBe(true);
// Toggle OFF: fill cleared.
state = run(state);
expect(isHeaderRowStyled(state)).toBe(false);
for (const c of firstRow(state).content) {
expect(c.attrs.backgroundColor == null).toBe(true);
}
});
it('strips baked-in bold marks when turning the header off (import round-trip)', () => {
// Simulate an imported header: first row has the fill + bold runs (export bakes bold).
const doc = schema.nodeFromJSON({
type: 'doc',
content: [{ type: 'table', content: [
row(
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, colwidth: null, backgroundColor: HEADER_SHADE },
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'H', marks: [{ type: 'bold' }] }] }] },
),
row(cell('x')),
] }],
});
let pos = -1;
doc.descendants((n: PMNode, p: number) => { if (pos < 0 && n.isText) pos = p; });
let state = EditorState.create({ schema, doc, selection: TextSelection.create(doc, pos) });
expect(isHeaderRowStyled(state)).toBe(true);
state = run(state); // toggle OFF
expect(isHeaderRowStyled(state)).toBe(false);
const c = firstRow(state).content[0];
expect(c.attrs.backgroundColor == null).toBe(true);
expect(boldMarks(c)).toBe(false); // baked bold removed
});
it('toggles the first column, and the shared corner survives turning one off', () => {
let state = makeState();
state = run(state, 'column');
expect(isHeaderStyled(state, 'column')).toBe(true);
const col = (s: EditorState) => s.doc.toJSON().content[0].content.map((r: N) => r.content[0]);
for (const c of col(state)) expect(c.attrs.backgroundColor).toBe(HEADER_SHADE);
// Cell (0,1) untouched.
expect(state.doc.toJSON().content[0].content[0].content[1].attrs.backgroundColor == null).toBe(true);
// Both headers on: turning the row off keeps the corner (still the column header).
state = run(state, 'row');
expect(isHeaderStyled(state, 'row')).toBe(true);
state = run(state, 'row');
expect(isHeaderStyled(state, 'row')).toBe(false);
expect(isHeaderStyled(state, 'column')).toBe(true);
state = run(state, 'column');
expect(isHeaderStyled(state, 'column')).toBe(false);
for (const c of col(state)) expect(c.attrs.backgroundColor == null).toBe(true);
});
});