import { sectionsFromRecord, recordPatchFromSections, readData, toCamelKey, type DocAttribute, } from '../sections'; import type { DocSection } from '../types'; const attrs: DocAttribute[] = [ { name: 'blog', dataType: 'longtext', label: 'Blog post' }, { name: 'linkedin', dataType: 'text' }, { name: 'seo', dataType: 'json' }, { name: 'sources', dataType: 'json' }, { name: 'status', dataType: 'enum' }, { name: 'stage', dataType: 'enum' }, { name: 'score', dataType: 'number' }, { name: 'created_at', dataType: 'date' }, ]; describe('toCamelKey / readData', () => { it('camelCases snake and kebab keys', () => { expect(toCamelKey('meta_description')).toBe('metaDescription'); expect(toCamelKey('primary-keyword')).toBe('primaryKeyword'); expect(toCamelKey('blog')).toBe('blog'); }); it('reads camel form first, then raw', () => { expect(readData({ metaDescription: 'camel' }, 'meta_description')).toBe('camel'); expect(readData({ meta_description: 'raw' }, 'meta_description')).toBe('raw'); expect(readData(undefined, 'x')).toBeUndefined(); }); }); describe('sectionsFromRecord', () => { const data = { blog: '# Hello\n\nBody', linkedin: 'A short post', seo: { meta_description: 'desc', primary_keyword: 'kw' }, sources: [{ url: 'https://a.com' }, 'https://b.com'], status: 'draft', score: 5, }; it('routes data types to the right kinds and skips enum/system/unknown', () => { const sections = sectionsFromRecord(attrs, data); const byKey = Object.fromEntries(sections.map((s) => [s.key, s])); expect(byKey.blog.kind).toBe('markdown'); expect(byKey.linkedin.kind).toBe('text'); expect(byKey.seo.kind).toBe('structured'); expect(byKey.sources.kind).toBe('list'); // enum (status, stage), number (score), date/system (created_at) are skipped expect(sections.map((s) => s.key).sort()).toEqual(['blog', 'linkedin', 'seo', 'sources']); }); it('coerces values into each kind shape', () => { const { blog, seo, sources } = Object.fromEntries( sectionsFromRecord(attrs, data).map((s) => [s.key, s]), ) as Record; expect(typeof blog.value).toBe('string'); expect(seo.value).toEqual({ meta_description: 'desc', primary_keyword: 'kw' }); // list: array of {url} objects + a bare string → url extracted from objects, strings kept expect(sources.value).toEqual(['https://a.com', 'https://b.com']); }); it('parses a newline-delimited string into a list', () => { const s = sectionsFromRecord([{ name: 'sources', dataType: 'text' }], { sources: 'https://a.com\nhttps://b.com\n' }); expect(s[0].kind).toBe('list'); expect(s[0].value).toEqual(['https://a.com', 'https://b.com']); }); it('honors label, camelCase reads, exclude, readOnlyKeys and includeKinds', () => { const camelData = { blog: 'x', linkedIn: 'y' }; const a: DocAttribute[] = [ { name: 'blog', dataType: 'longtext', label: 'Blog post' }, { name: 'linked_in', dataType: 'text' }, ]; const sections = sectionsFromRecord(a, camelData, { readOnlyKeys: ['blog'] }); expect(sections.find((s) => s.key === 'blog')!.label).toBe('Blog post'); expect(sections.find((s) => s.key === 'blog')!.readOnly).toBe(true); // linked_in humanized + camelCase-aware read of `linkedIn` expect(sections.find((s) => s.key === 'linked_in')!.label).toBe('Linked in'); expect(sections.find((s) => s.key === 'linked_in')!.value).toBe('y'); expect(sectionsFromRecord(attrs, data, { exclude: ['seo'] }).some((s) => s.key === 'seo')).toBe(false); expect( sectionsFromRecord(attrs, data, { includeKinds: ['markdown'] }).map((s) => s.kind), ).toEqual(['markdown']); }); }); describe('recordPatchFromSections', () => { it('produces a data patch keyed by section.key with normalized shapes', () => { const sections: DocSection[] = [ { key: 'blog', label: 'Blog', kind: 'markdown', value: '# hi' }, { key: 'linkedin', label: 'LinkedIn', kind: 'text', value: 'post' }, { key: 'seo', label: 'SEO', kind: 'structured', value: { meta_description: 'd', primary_keyword: 'k' } }, { key: 'sources', label: 'Sources', kind: 'list', value: ['https://a.com', ''] }, ]; const patch = recordPatchFromSections(sections); expect(patch).toEqual({ blog: '# hi', linkedin: 'post', seo: { meta_description: 'd', primary_keyword: 'k' }, sources: ['https://a.com'], }); }); });