test(web): add 21 tests for exportAll/importRoutines/parseImportData
This commit is contained in:
parent
141fcc2a38
commit
fe75bc30de
258
web/src/lib/export.test.ts
Normal file
258
web/src/lib/export.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user