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