import { describe, expect, test } from 'vitest'; import { CURRENT_SNIPPET_VERSION, createSnippet, duplicateSnippet, formatSnippetSize, generateSnippetName, hasUnpublishedChanges, SAMPLE_SPEC, sampleSpecText, snippetSizeBytes, } from './snippet'; describe('createSnippet', () => { test('stamps current version, equal timestamps, and the sample template', () => { const now = new Date('2026-06-04T14:30:07.000Z'); const s = createSnippet({ now, id: 'fixed-id' }); expect(s.id).toBe('fixed-id'); expect(s.version).toBe(CURRENT_SNIPPET_VERSION); expect(s.created).toBe(now.toISOString()); expect(s.modified).toBe(s.created); expect(s.spec).toBe(sampleSpecText()); // draft starts identical to published — nothing to publish yet. expect(s.draftSpec).toBe(s.spec); expect(s.comment).toBe(''); expect(s.tags).toEqual([]); expect(s.datasetRefs).toEqual([]); expect(s.meta).toEqual({}); }); test('generates a unique id by default', () => { const a = createSnippet(); const b = createSnippet(); expect(a.id).not.toBe(b.id); }); test('accepts name and spec overrides', () => { const s = createSnippet({ name: 'Custom', spec: '{"mark":"point"}' }); expect(s.name).toBe('Custom'); expect(s.spec).toBe('{"mark":"point"}'); expect(s.draftSpec).toBe('{"mark":"point"}'); }); }); describe('the sample template', () => { test('is valid JSON that round-trips', () => { expect(JSON.parse(sampleSpecText())).toEqual(SAMPLE_SPEC); }); test('is a bar chart with inline data', () => { expect(SAMPLE_SPEC.mark).toBe('bar'); expect(SAMPLE_SPEC.data.values.length).toBeGreaterThan(0); }); }); describe('generateSnippetName', () => { test('formats as "Snippet YYYY-MM-DD HH:MM:SS" with zero-padding', () => { // Construct via local-time parts so the test is timezone-independent. const now = new Date(2026, 0, 5, 9, 7, 3); // 2026-01-05 09:07:03 local expect(generateSnippetName(now)).toBe('Snippet 2026-01-05 09:07:03'); }); test('differs second-to-second so quick successive creates stay distinct', () => { const a = generateSnippetName(new Date(2026, 5, 4, 14, 30, 7)); const b = generateSnippetName(new Date(2026, 5, 4, 14, 30, 8)); expect(a).not.toBe(b); }); }); describe('duplicateSnippet', () => { const source = { ...createSnippet({ id: 'src', name: 'My Chart', spec: '{"published":1}', now: new Date('2026-01-01T00:00:00Z'), }), draftSpec: '{"draft":1}', comment: 'a note', tags: ['imported'], datasetRefs: ['Sales'], meta: { createdWith: 'chart-builder' }, }; test('suffixes the name with "(copy)" and takes a new identity + timestamps', () => { const now = new Date('2026-03-04T05:06:07Z'); const copy = duplicateSnippet(source, { id: 'copy', now }); expect(copy.id).toBe('copy'); expect(copy.id).not.toBe(source.id); expect(copy.name).toBe('My Chart (copy)'); expect(copy.version).toBe(CURRENT_SNIPPET_VERSION); expect(copy.created).toBe(now.toISOString()); expect(copy.modified).toBe(now.toISOString()); }); test('carries over both spec versions, comment, tags, refs, and meta', () => { const copy = duplicateSnippet(source); expect(copy.spec).toBe('{"published":1}'); expect(copy.draftSpec).toBe('{"draft":1}'); expect(copy.comment).toBe('a note'); expect(copy.tags).toEqual(['imported']); expect(copy.datasetRefs).toEqual(['Sales']); expect(copy.meta).toEqual({ createdWith: 'chart-builder' }); }); test('clones mutable members so the copy shares no references with its source', () => { const copy = duplicateSnippet(source); expect(copy.tags).not.toBe(source.tags); expect(copy.datasetRefs).not.toBe(source.datasetRefs); expect(copy.meta).not.toBe(source.meta); }); test('generates a unique id by default', () => { expect(duplicateSnippet(source).id).not.toBe(duplicateSnippet(source).id); }); }); describe('hasUnpublishedChanges', () => { test('false when draft matches published, true once draft diverges', () => { const s = createSnippet({ now: new Date('2026-06-04T00:00:00Z') }); expect(hasUnpublishedChanges(s)).toBe(false); expect(hasUnpublishedChanges({ ...s, draftSpec: s.spec + ' ' })).toBe(true); }); }); describe('snippetSizeBytes', () => { test('measures the draft as UTF-8 byte length, not character count', () => { const s = createSnippet({ spec: 'abc' }); expect(snippetSizeBytes(s)).toBe(3); // A 3-character string of multibyte glyphs is more than 3 bytes. expect(snippetSizeBytes({ ...s, draftSpec: '€€€' })).toBe(9); }); test('reflects the working draft, not the published spec', () => { const s = createSnippet({ spec: 'ab' }); expect(snippetSizeBytes({ ...s, draftSpec: 'abcdef' })).toBe(6); }); }); describe('formatSnippetSize', () => { test('omits the size under ~1 KB (spec §02 clutter rule)', () => { expect(formatSnippetSize(0)).toBeNull(); expect(formatSnippetSize(512)).toBeNull(); expect(formatSnippetSize(1023)).toBeNull(); }); test('shows whole KB at and above the threshold', () => { expect(formatSnippetSize(1024)).toBe('1 KB'); expect(formatSnippetSize(2048)).toBe('2 KB'); expect(formatSnippetSize(1536)).toBe('2 KB'); // rounds to nearest KB }); test('rolls over to MB for large payloads', () => { expect(formatSnippetSize(1024 * 1024)).toBe('1 MB'); expect(formatSnippetSize(3 * 1024 * 1024)).toBe('3 MB'); }); });