mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 09:41:09 +02:00
exportToJSON, exportToCSV (with BOM for Excel), importFromJSON, downloadFile, timestampedFilename. Works with any Dexie table. Supports filtering, column selection, custom formatters, ID regeneration, and transform functions. 16 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { exportToJSON, exportToCSV, importFromJSON, timestampedFilename } from './data-export';
|
|
|
|
function createMockTable(records: Record<string, unknown>[]) {
|
|
return {
|
|
toArray: vi.fn(async () => [...records]),
|
|
bulkAdd: vi.fn(async () => undefined),
|
|
clear: vi.fn(async () => undefined),
|
|
};
|
|
}
|
|
|
|
describe('exportToJSON', () => {
|
|
it('exports all records as JSON string', async () => {
|
|
const table = createMockTable([
|
|
{ id: '1', name: 'Alice' },
|
|
{ id: '2', name: 'Bob' },
|
|
]);
|
|
const json = await exportToJSON(table as never);
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed).toHaveLength(2);
|
|
expect(parsed[0].name).toBe('Alice');
|
|
});
|
|
|
|
it('applies filter before export', async () => {
|
|
const table = createMockTable([
|
|
{ id: '1', name: 'Alice', deletedAt: null },
|
|
{ id: '2', name: 'Bob', deletedAt: '2024-01-01' },
|
|
]);
|
|
const json = await exportToJSON(table as never, {
|
|
filter: (r) => !r.deletedAt,
|
|
});
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed).toHaveLength(1);
|
|
expect(parsed[0].name).toBe('Alice');
|
|
});
|
|
|
|
it('supports compact output', async () => {
|
|
const table = createMockTable([{ id: '1' }]);
|
|
const json = await exportToJSON(table as never, { pretty: false });
|
|
expect(json).not.toContain('\n');
|
|
});
|
|
});
|
|
|
|
describe('exportToCSV', () => {
|
|
it('exports records as CSV with headers', async () => {
|
|
const table = createMockTable([
|
|
{ id: '1', name: 'Alice', email: 'alice@test.com' },
|
|
{ id: '2', name: 'Bob', email: 'bob@test.com' },
|
|
]);
|
|
const csv = await exportToCSV(table as never, {
|
|
columns: ['name', 'email'],
|
|
headers: ['Name', 'E-Mail'],
|
|
});
|
|
const lines = csv.replace('\uFEFF', '').split('\n');
|
|
expect(lines[0]).toBe('Name;E-Mail');
|
|
expect(lines[1]).toBe('Alice;alice@test.com');
|
|
expect(lines[2]).toBe('Bob;bob@test.com');
|
|
});
|
|
|
|
it('auto-quotes values with separator', async () => {
|
|
const table = createMockTable([{ id: '1', desc: 'foo;bar' }]);
|
|
const csv = await exportToCSV(table as never, { columns: ['desc'] });
|
|
expect(csv).toContain('"foo;bar"');
|
|
});
|
|
|
|
it('returns empty string for no records', async () => {
|
|
const table = createMockTable([]);
|
|
const csv = await exportToCSV(table as never);
|
|
expect(csv).toBe('');
|
|
});
|
|
|
|
it('uses all keys from first record as default columns', async () => {
|
|
const table = createMockTable([{ a: 1, b: 2, c: 3 }]);
|
|
const csv = await exportToCSV(table as never);
|
|
const header = csv.replace('\uFEFF', '').split('\n')[0];
|
|
expect(header).toBe('a;b;c');
|
|
});
|
|
|
|
it('applies filter', async () => {
|
|
const table = createMockTable([
|
|
{ id: '1', name: 'Keep' },
|
|
{ id: '2', name: 'Skip', deletedAt: '2024' },
|
|
]);
|
|
const csv = await exportToCSV(table as never, {
|
|
columns: ['name'],
|
|
filter: (r) => !r.deletedAt,
|
|
});
|
|
const lines = csv.replace('\uFEFF', '').split('\n');
|
|
expect(lines).toHaveLength(2); // header + 1 row
|
|
});
|
|
|
|
it('includes BOM for Excel', async () => {
|
|
const table = createMockTable([{ id: '1' }]);
|
|
const csv = await exportToCSV(table as never);
|
|
expect(csv.startsWith('\uFEFF')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('importFromJSON', () => {
|
|
it('imports records from JSON file', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File(
|
|
[
|
|
JSON.stringify([
|
|
{ id: 'x', name: 'Alice' },
|
|
{ id: 'y', name: 'Bob' },
|
|
]),
|
|
],
|
|
'test.json',
|
|
{ type: 'application/json' }
|
|
);
|
|
const count = await importFromJSON(table as never, file);
|
|
expect(count).toBe(2);
|
|
expect(table.bulkAdd).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('generates new IDs by default', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File([JSON.stringify([{ id: 'old-id', name: 'Test' }])], 'test.json');
|
|
await importFromJSON(table as never, file);
|
|
const added = table.bulkAdd.mock.calls[0][0] as Record<string, unknown>[];
|
|
expect(added[0].id).not.toBe('old-id');
|
|
});
|
|
|
|
it('keeps original IDs when newIds=false', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File([JSON.stringify([{ id: 'keep-me', name: 'Test' }])], 'test.json');
|
|
await importFromJSON(table as never, file, { newIds: false });
|
|
const added = table.bulkAdd.mock.calls[0][0] as Record<string, unknown>[];
|
|
expect(added[0].id).toBe('keep-me');
|
|
});
|
|
|
|
it('applies transform', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File([JSON.stringify([{ name: 'test' }])], 'test.json');
|
|
await importFromJSON(table as never, file, {
|
|
transform: (r) => ({ ...r, imported: true }),
|
|
});
|
|
const added = table.bulkAdd.mock.calls[0][0] as Record<string, unknown>[];
|
|
expect(added[0].imported).toBe(true);
|
|
});
|
|
|
|
it('clears table when clearFirst=true', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File([JSON.stringify([])], 'test.json');
|
|
await importFromJSON(table as never, file, { clearFirst: true });
|
|
expect(table.clear).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('throws on non-array JSON', async () => {
|
|
const table = createMockTable([]);
|
|
const file = new File([JSON.stringify({ not: 'array' })], 'test.json');
|
|
await expect(importFromJSON(table as never, file)).rejects.toThrow('array');
|
|
});
|
|
});
|
|
|
|
describe('timestampedFilename', () => {
|
|
it('generates filename with date', () => {
|
|
const name = timestampedFilename('contacts', 'json');
|
|
expect(name).toMatch(/^contacts-\d{4}-\d{2}-\d{2}\.json$/);
|
|
});
|
|
});
|