126 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|