mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
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');
|
|
});
|
|
});
|