coaching-fence-render.test.ts3.5 KBView on GitHub
/**
 * Does a ```scorecard / ```moment fence actually become its node when real document markdown is
 * parsed by the editor?
 *
 * Registering an extension is not the same as it firing. StarterKit's generic fenced-code rule
 * will happily claim ```scorecard and render a grey code block instead of the card, which looks
 * to a reader exactly like "the scorecard didn't render". This drives the real markdown from a
 * generated coaching doc through the real extension list.
 */
import { Editor } from '@tiptap/core';
import { Markdown } from '@tiptap/markdown';
import StarterKit from '@tiptap/starter-kit';
import { Table } from '@tiptap/extension-table';
import { TableRow } from '@tiptap/extension-table-row';
import { TableCell } from '@tiptap/extension-table-cell';
import { TableHeader } from '@tiptap/extension-table-header';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';

import { ScorecardFenceNode } from '@/modules/documents/coaching/ScorecardFenceNode';
import { MomentFenceNode } from '@/modules/documents/coaching/MomentFenceNode';

const extensions = [
  StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
  Markdown,
  Table.configure({ resizable: false }),
  TableRow,
  TableHeader,
  TableCell,
  TaskList,
  TaskItem.configure({ nested: true }),
  ScorecardFenceNode,
  MomentFenceNode,
];

const DOC = [
  '# Marmon Mok',
  '',
  '## Discovery',
  '',
  '```scorecard',
  '| Discovery | Score | Why |',
  '| --- | --- | --- |',
  '| D1 Knew the must-asks | 1 | Only asked where projects are stored. |',
  '| D2 Genuinely curious | 1 | Budget objection dropped. |',
  '```',
  '',
  '## Checklist',
  '',
  '- [x] Built rapport before starting discovery',
  '- [ ] Asked "what piqued your interest"',
  '',
  '## Moments',
  '',
  '```moment',
  'deal: Marmon Mok',
  'meeting: Pirros | Marmon Mok',
  'criterion: D2',
  'verdict: bad',
  'coaching: Ask what changed since the pilot stalled.',
  '',
  '## moment',
  '**Ben Zavala** (02:16) We are just revisiting whether we can afford it.',
  '',
  '## reaction',
  '**Xander** (02:36) Got it. And remind me what you have set up.',
  '```',
].join('\n');

function types(json: unknown, acc: Set<string> = new Set()): Set<string> {
  if (!json || typeof json !== 'object') return acc;
  const n = json as { type?: string; content?: unknown[] };
  if (n.type) acc.add(n.type);
  for (const c of n.content ?? []) types(c, acc);
  return acc;
}

describe('coaching fences render from real document markdown', () => {
  let editor: Editor;
  beforeAll(() => {
    editor = new Editor({ extensions, content: DOC, contentType: 'markdown' as never });
  });
  afterAll(() => editor?.destroy());

  it('produces a scorecardBlock, not a generic code block', () => {
    const t = types(editor.getJSON());
    expect(t.has('scorecardBlock')).toBe(true);
    expect(t.has('codeBlock')).toBe(false);
  });

  it('produces a momentBlock', () => {
    expect(types(editor.getJSON()).has('momentBlock')).toBe(true);
  });

  it('still renders the checklist as task items alongside the fences', () => {
    expect(types(editor.getJSON()).has('taskItem')).toBe(true);
  });

  it('keeps the fence body verbatim on the node so it round-trips', () => {
    const md = (editor as unknown as { getMarkdown: () => string }).getMarkdown();
    expect(md).toContain('```scorecard');
    expect(md).toContain('| D1 Knew the must-asks | 1 |');
    expect(md).toContain('```moment');
    expect(md).toContain('coaching: Ask what changed since the pilot stalled.');
  });
});