recording-fence-render.test.ts3.0 KBView on GitHub
/**
 * Does a ```recording fence actually become its node when real document markdown is parsed?
 *
 * Same hazard as the coaching fences: StarterKit's generic fenced-code rule will happily claim
 * ```recording and render a grey code block, which to a reader is indistinguishable from "the
 * video didn't load". This drives the fence through the real extension list, together with a
 * moment fence carrying the new `externalId:` key, and checks both survive a round trip back to
 * markdown — the node stores its body verbatim, so a lossy trip would silently rewrite a
 * document every time someone opened it.
 */
import { Editor } from '@tiptap/core';
import { Markdown } from '@tiptap/markdown';
import StarterKit from '@tiptap/starter-kit';

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

const extensions = [
  StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
  Markdown,
  MomentFenceNode,
  RecordingFenceNode,
];

const DOC = [
  '# Pirros — 2026-07-28',
  '',
  '```recording',
  'provider: fathom',
  'externalId: 168089693',
  'title: Pirros // Marmon Mok discovery',
  'at: 05:12',
  'recording: https://fathom.video/share/5mga_KS6Xbvz',
  '```',
  '',
  '## Moments',
  '',
  '```moment',
  'deal: Marmon Mok',
  'meeting: Pirros | Marmon Mok',
  'at: 02:16',
  'externalId: 168089693',
  'provider: fathom',
  'verdict: bad',
  'coaching: Ask what changed since the pilot stalled.',
  '',
  '## moment',
  '**Ben Zavala** (02:16) We are just revisiting whether we can afford it.',
  '```',
].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('recording fence renders from real document markdown', () => {
  let editor: Editor;
  beforeAll(() => {
    editor = new Editor({ extensions, content: DOC, contentType: 'markdown' as never });
  });
  afterAll(() => editor?.destroy());

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

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

  it('round-trips both fences back to markdown with the id intact', () => {
    const md = (editor as unknown as { getMarkdown: () => string }).getMarkdown();
    expect(md).toContain('```recording');
    expect(md).toContain('externalId: 168089693');
    expect(md).toContain('title: Pirros // Marmon Mok discovery');
    expect(md).toContain('```moment');
    // The id is what makes the card playable; losing it on a round trip would silently
    // downgrade every moment in the document to a link.
    expect(md.match(/externalId: 168089693/g)).toHaveLength(2);
  });
});