import { describe, it, expect } from 'vitest'; import { validateTipTapDocument, tipTapToPdfmeTemplate, substituteVariables, buildContentInputsFromDoc, type TipTapNode, } from '@/lib/pdf/tiptap-to-pdfme'; // ─── Fixtures ───────────────────────────────────────────────────────────────── function makeDoc(...children: TipTapNode[]): TipTapNode { return { type: 'doc', content: children }; } function paragraph(text: string): TipTapNode { return { type: 'paragraph', content: [{ type: 'text', text }], }; } function heading(level: number, text: string): TipTapNode { return { type: 'heading', attrs: { level }, content: [{ type: 'text', text }], }; } function bulletList(...items: string[]): TipTapNode { return { type: 'bulletList', content: items.map((item) => ({ type: 'listItem', content: [paragraph(item)], })), }; } // ─── validateTipTapDocument ─────────────────────────────────────────────────── describe('validateTipTapDocument', () => { it('returns empty array for a valid doc with only a paragraph', () => { const doc = makeDoc(paragraph('Hello world')); expect(validateTipTapDocument(doc)).toEqual([]); }); it('returns empty array for a doc with heading + paragraph', () => { const doc = makeDoc(heading(1, 'Title'), paragraph('Body text')); expect(validateTipTapDocument(doc)).toEqual([]); }); it('returns empty array for a doc with bulletList', () => { const doc = makeDoc(bulletList('Item 1', 'Item 2')); expect(validateTipTapDocument(doc)).toEqual([]); }); it('returns ["blockquote"] when doc contains a blockquote', () => { const doc = makeDoc( paragraph('Before'), { type: 'blockquote', content: [paragraph('Quoted')] }, ); const errors = validateTipTapDocument(doc); expect(errors).toContain('blockquote'); }); it('returns ["codeBlock"] when doc contains a codeBlock', () => { const doc = makeDoc( paragraph('Before'), { type: 'codeBlock', content: [{ type: 'text', text: 'const x = 1;' }] }, ); const errors = validateTipTapDocument(doc); expect(errors).toContain('codeBlock'); }); it('returns multiple unsupported types without duplicates', () => { const doc = makeDoc( { type: 'blockquote', content: [] }, { type: 'codeBlock', content: [] }, { type: 'blockquote', content: [] }, // duplicate — should only appear once ); const errors = validateTipTapDocument(doc); expect(errors).toContain('blockquote'); expect(errors).toContain('codeBlock'); expect(errors.filter((e) => e === 'blockquote')).toHaveLength(1); }); it('detects unsupported nodes nested inside valid nodes', () => { const doc = makeDoc({ type: 'paragraph', content: [{ type: 'blockquote', content: [] }], }); expect(validateTipTapDocument(doc)).toContain('blockquote'); }); }); // ─── tipTapToPdfmeTemplate ──────────────────────────────────────────────────── describe('tipTapToPdfmeTemplate', () => { it('returns a template with a schemas array', () => { const doc = makeDoc(paragraph('Hello')); const template = tipTapToPdfmeTemplate(doc); expect(template).toHaveProperty('schemas'); expect(Array.isArray(template.schemas)).toBe(true); }); it('produces one schema field per paragraph', () => { const doc = makeDoc(paragraph('One'), paragraph('Two'), paragraph('Three')); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat(); expect(allFields).toHaveLength(3); }); it('heading + paragraph → 2 schema fields', () => { const doc = makeDoc(heading(1, 'Title'), paragraph('Body')); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat(); expect(allFields).toHaveLength(2); }); it('bulletList with 3 items → 3 schema fields', () => { const doc = makeDoc(bulletList('A', 'B', 'C')); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat(); expect(allFields).toHaveLength(3); }); it('all schema fields have type "text"', () => { const doc = makeDoc(heading(2, 'Sub'), paragraph('Para'), bulletList('Item')); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat() as Array<{ type: string }>; for (const field of allFields) { expect(field.type).toBe('text'); } }); it('all schema fields have a name property', () => { const doc = makeDoc(paragraph('p1'), paragraph('p2')); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat() as Array<{ name: string }>; for (const field of allFields) { expect(typeof field.name).toBe('string'); expect(field.name.length).toBeGreaterThan(0); } }); it('field count matches content node count (round-trip check)', () => { const nodeCount = 4; const doc = makeDoc( heading(1, 'H1'), paragraph('Para 1'), paragraph('Para 2'), bulletList('Bullet'), ); const template = tipTapToPdfmeTemplate(doc); const allFields = template.schemas.flat(); expect(allFields).toHaveLength(nodeCount); }); }); // ─── substituteVariables ────────────────────────────────────────────────────── describe('substituteVariables', () => { it('replaces a single variable token', () => { const result = substituteVariables('Hello {{client.name}}', { 'client.name': 'Alice' }); expect(result).toBe('Hello Alice'); }); it('replaces multiple variable tokens', () => { const result = substituteVariables('{{client.name}} at {{port.name}}', { 'client.name': 'Alice', 'port.name': 'Port Nimara', }); expect(result).toBe('Alice at Port Nimara'); }); it('leaves unmatched tokens as-is', () => { const result = substituteVariables('Hello {{client.name}}', {}); expect(result).toBe('Hello {{client.name}}'); }); it('handles whitespace inside token braces', () => { const result = substituteVariables('Hello {{ client.name }}', { 'client.name': 'Bob' }); expect(result).toBe('Hello Bob'); }); it('replaces the same token multiple times', () => { const result = substituteVariables('{{x}} and {{x}}', { x: 'yes' }); expect(result).toBe('yes and yes'); }); }); // ─── buildContentInputsFromDoc ──────────────────────────────────────────────── describe('buildContentInputsFromDoc', () => { it('returns an array of records keyed by schema field names', () => { const doc = makeDoc(paragraph('Hello'), paragraph('World')); const template = tipTapToPdfmeTemplate(doc); const inputs = buildContentInputsFromDoc(doc, template); expect(Array.isArray(inputs)).toBe(true); expect(inputs).toHaveLength(template.schemas.length); const allFieldNames = (template.schemas.flat() as Array<{ name: string }>).map((f) => f.name); const allInputKeys = inputs.flatMap((record) => Object.keys(record)); for (const name of allFieldNames) { expect(allInputKeys).toContain(name); } }); it('input count matches schema field count', () => { const doc = makeDoc(heading(1, 'H'), paragraph('P1'), paragraph('P2')); const template = tipTapToPdfmeTemplate(doc); const inputs = buildContentInputsFromDoc(doc, template); const totalFields = template.schemas.reduce((acc, page) => acc + page.length, 0); const totalInputs = inputs.reduce((acc, record) => acc + Object.keys(record).length, 0); expect(totalInputs).toBe(totalFields); }); });