test(web): add 21 tests for exportAll/importRoutines/parseImportData

This commit is contained in:
saravanakumardb1 2026-02-28 19:29:39 -08:00
parent 141fcc2a38
commit fe75bc30de

258
web/src/lib/export.test.ts Normal file
View File

@ -0,0 +1,258 @@
// ── Export / Import Tests ────────────────────────────────────
import { describe, it, expect } from 'vitest';
import {
exportTimers,
exportAll,
parseImportData,
importTimers,
importRoutines,
} from './export';
import type { Timer } from './timer-engine';
import type { Routine } from './routines';
import type { ExportData } from './export';
// ── Fixtures ─────────────────────────────────────────────────
function makeTimer(overrides: Partial<Timer> = {}): Timer {
return {
id: 'timer-1',
type: 'countdown',
label: 'Test Timer',
targetTime: Date.now() + 60_000,
createdAt: Date.now(),
state: 'running',
urgency: 'standard',
cascadePreset: 'standard',
cascadeIntervals: [5, 1],
firedCascades: [],
snoozedUntil: null,
snoozeCount: 0,
...overrides,
} as Timer;
}
function makeRoutine(overrides: Partial<Routine> = {}): Routine {
return {
id: 'routine-1',
name: 'Morning Routine',
steps: [
{ id: 'step-1', label: 'Wake up', durationMs: 60_000, status: 'pending' },
{ id: 'step-2', label: 'Shower', durationMs: 300_000, status: 'pending' },
],
status: 'idle',
currentStepIndex: 0,
createdAt: Date.now(),
isTemplate: false,
...overrides,
} as Routine;
}
// ── exportTimers ─────────────────────────────────────────────
describe('exportTimers', () => {
it('returns correct structure with timers only', () => {
const timers = [makeTimer()];
const result = exportTimers(timers);
expect(result.version).toBe(1);
expect(result.app).toBe('chronomind');
expect(result.timers).toHaveLength(1);
expect(result.timers[0].id).toBe('timer-1');
expect(result.routines).toBeUndefined();
});
it('shallow-clones timers (not same reference)', () => {
const timers = [makeTimer()];
const result = exportTimers(timers);
expect(result.timers[0]).not.toBe(timers[0]);
expect(result.timers[0]).toEqual(timers[0]);
});
it('handles empty timer array', () => {
const result = exportTimers([]);
expect(result.timers).toHaveLength(0);
});
});
// ── exportAll ────────────────────────────────────────────────
describe('exportAll', () => {
it('includes both timers and routines', () => {
const timers = [makeTimer()];
const routines = [makeRoutine()];
const result = exportAll(timers, routines);
expect(result.version).toBe(1);
expect(result.app).toBe('chronomind');
expect(result.timers).toHaveLength(1);
expect(result.routines).toHaveLength(1);
expect(result.routines![0].name).toBe('Morning Routine');
});
it('deep-clones routine steps', () => {
const routines = [makeRoutine()];
const result = exportAll([], routines);
expect(result.routines![0].steps[0]).not.toBe(routines[0].steps[0]);
expect(result.routines![0].steps[0]).toEqual(routines[0].steps[0]);
});
it('handles empty arrays', () => {
const result = exportAll([], []);
expect(result.timers).toHaveLength(0);
expect(result.routines).toHaveLength(0);
});
});
// ── parseImportData ──────────────────────────────────────────
describe('parseImportData', () => {
it('parses valid export JSON', () => {
const data: ExportData = {
version: 1,
exportedAt: Date.now(),
app: 'chronomind',
timers: [makeTimer()],
routines: [makeRoutine()],
};
const result = parseImportData(JSON.stringify(data));
expect(result).not.toBeNull();
expect(result!.timers).toHaveLength(1);
expect(result!.routines).toHaveLength(1);
});
it('returns null for invalid JSON', () => {
expect(parseImportData('not json')).toBeNull();
});
it('returns null for wrong app', () => {
expect(parseImportData(JSON.stringify({ app: 'other', timers: [] }))).toBeNull();
});
it('returns null for missing timers array', () => {
expect(parseImportData(JSON.stringify({ app: 'chronomind' }))).toBeNull();
});
it('accepts data without routines', () => {
const data = { version: 1, exportedAt: Date.now(), app: 'chronomind', timers: [] };
const result = parseImportData(JSON.stringify(data));
expect(result).not.toBeNull();
expect(result!.routines).toBeUndefined();
});
});
// ── importTimers ─────────────────────────────────────────────
describe('importTimers', () => {
it('imports new timers', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [makeTimer({ id: 'new-1' })],
};
const result = importTimers(data, []);
expect(result.imported).toBe(1);
expect(result.skipped).toBe(0);
expect(result.success).toBe(true);
});
it('skips duplicate timers by ID', () => {
const existing = [makeTimer({ id: 'dup-1' })];
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [makeTimer({ id: 'dup-1' }), makeTimer({ id: 'new-2' })],
};
const result = importTimers(data, existing);
expect(result.imported).toBe(1);
expect(result.skipped).toBe(1);
});
it('rejects timers missing required fields', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [{ id: '', type: 'countdown', label: '' } as unknown as Timer],
};
const result = importTimers(data, []);
expect(result.imported).toBe(0);
expect(result.skipped).toBe(1);
expect(result.errors).toHaveLength(1);
});
it('rejects timers with invalid timestamps', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [makeTimer({ createdAt: 'bad' as unknown as number })],
};
const result = importTimers(data, []);
expect(result.imported).toBe(0);
expect(result.skipped).toBe(1);
});
});
// ── importRoutines ───────────────────────────────────────────
describe('importRoutines', () => {
it('imports new routines', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
routines: [makeRoutine({ id: 'r-1' })],
};
const result = importRoutines(data, []);
expect(result.imported).toBe(1);
expect(result.skipped).toBe(0);
expect(result.success).toBe(true);
});
it('skips duplicate routines by ID', () => {
const existing = [makeRoutine({ id: 'r-dup' })];
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
routines: [makeRoutine({ id: 'r-dup' }), makeRoutine({ id: 'r-new' })],
};
const result = importRoutines(data, existing);
expect(result.imported).toBe(1);
expect(result.skipped).toBe(1);
});
it('returns zero imported when no routines in data', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
};
const result = importRoutines(data, []);
expect(result.imported).toBe(0);
expect(result.success).toBe(true);
});
it('returns zero imported for empty routines array', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
routines: [],
};
const result = importRoutines(data, []);
expect(result.imported).toBe(0);
expect(result.success).toBe(true);
});
it('rejects routines missing required fields', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
routines: [{ id: '', name: '', steps: [] } as unknown as Routine],
};
const result = importRoutines(data, []);
expect(result.imported).toBe(0);
expect(result.skipped).toBe(1);
expect(result.errors).toHaveLength(1);
});
it('rejects routines with non-array steps', () => {
const data: ExportData = {
version: 1, exportedAt: Date.now(), app: 'chronomind',
timers: [],
routines: [{ id: 'r-bad', name: 'Bad', steps: 'not-array' } as unknown as Routine],
};
const result = importRoutines(data, []);
expect(result.imported).toBe(0);
expect(result.skipped).toBe(1);
});
});