/** * Import/export acceptance checks at the pure (de)serialisation layer -- the * `` glue is trivial and browser-only, so it is not exercised here. */ import { describe, expect, it } from 'vitest'; import { useDocument, type DocumentStore } from '../../store/document'; import { isError } from '../../engine'; import { serializeDocument, parseDocument, JsonImportError } from '../json'; import { tableToCsv, parseCsv, matrixToCells } from '../csv'; import { importJsonReplace } from '../operations'; function fresh(): DocumentStore { const s = useDocument(); s.resetToSeed(); return s; } function tableId(s: DocumentStore, name: string): string { for (const sh of s.doc.sheets) { const o = sh.objects.find((x) => x.kind === 'table' && x.name === name); if (o) return o.id; } throw new Error(name); } const coords = (a1: string) => { const m = /^([A-Z]+)([0-9]+)$/.exec(a1)!; return { col: m[1].split('').reduce((n, c) => n * 26 + (c.charCodeAt(0) - 64), 0) - 1, row: Number(m[2]) - 1, }; }; describe('JSON native format', () => { it('round-trips a document losslessly (export → reset → import → identical)', () => { const s = fresh(); const before = s.snapshot(); const json = serializeDocument(before); s.addSheet('scratch'); s.setCellInput(tableId(s, 'Umsatz'), 1, 1, '999999'); s.replaceDocument(parseDocument(json)); expect(s.snapshot()).toEqual(before); }); it('rejects corrupt / empty input and leaves the document untouched', () => { const s = fresh(); const before = s.snapshot(); expect(() => importJsonReplace(s, '{ not json')).toThrow(JsonImportError); expect(() => importJsonReplace(s, JSON.stringify({ version: 2, sheets: [] }))).toThrow( JsonImportError, ); expect(() => importJsonReplace(s, JSON.stringify({ version: 7 }))).toThrow(JsonImportError); expect(s.snapshot()).toEqual(before); }); it('an unknown table name in an imported formula yields #REF, no crash', () => { const s = fresh(); const doc = { version: 2, sheets: [ { id: 's1', name: 'S1', objects: [ { kind: 'table', id: 't1', name: 'T1', x: 0, y: 0, rows: 4, cols: 4, colWidths: {}, cells: { '0,0': '=Nirgends!A1', '1,0': '=A1+1' }, }, ], }, ], activeSheetId: 's1', activeObjectId: null, selection: null, }; importJsonReplace(s, JSON.stringify(doc)); const a1 = s.cellValue('t1', 0, 0); const b1 = s.cellValue('t1', 1, 0); expect(isError(a1) && a1.code).toBe('#REF'); expect(isError(b1) && b1.code).toBe('#REF'); // error propagates }); it('normalises a v2 doc saved by an older build (missing table.view)', () => { const stale = { version: 2, sheets: [ { id: 's1', name: 'S1', objects: [ // no `view` key -- as written before the frozen-panes feature { kind: 'table', id: 't1', name: 'T1', x: 10, y: 10, rows: 8, cols: 5, colWidths: {}, cells: {} }, ], }, ], activeSheetId: 's1', activeObjectId: null, selection: null, }; const doc = parseDocument(JSON.stringify(stale)); const t = doc.sheets[0].objects[0]; expect(t.kind).toBe('table'); if (t.kind === 'table') { expect(t.view).toEqual({ mode: 'auto', width: 640, height: 320, frozenRows: 0, frozenCols: 0 }); } }); it('migrates a v1 document on import', () => { const s = fresh(); const v1 = JSON.stringify({ version: 1, sheets: [ { id: 'a', name: 'Zahlen', rows: 5, cols: 3, colWidths: {}, cells: { '0,0': '10', '0,1': '=A1*3' } }, ], activeSheetId: 'a', selection: null, }); importJsonReplace(s, v1); expect(s.doc.version).toBe(2); const id = tableId(s, 'Zahlen'); expect(s.cellValue(id, 0, 1)).toBe(30); }); }); describe('CSV', () => { it('exports computed values of "Übersicht" and re-imports to the same numbers', () => { const s = fresh(); const src = tableId(s, 'Übersicht'); const csv = tableToCsv(s, src, { delimiter: ',', bom: false, crlf: true }); expect(csv.includes('\r\n')).toBe(true); // header row is text, body carries the computed numbers expect(csv.split('\r\n')[0]).toBe('Region,Umsatz Jahr,Kosten Jahr,Deckungsbeitrag,Marge %'); const { cells, rows, cols } = matrixToCells(parseCsv(csv, ','), true); const copyId = s.addTableFromCells(s.doc.sheets[1].id, 'Kopie', cells, rows, cols); for (const a1 of ['B2', 'C2', 'D2', 'B8', 'C8', 'D8']) { const { col, row } = coords(a1); expect(s.cellValue(copyId, col, row)).toBe(s.cellValue(src, col, row)); } }); it('RFC-4180 quoting: a field with a comma is wrapped and survives the round-trip', () => { const s = fresh(); const id = s.addTable(s.doc.sheets[1].id); s.setCellInput(id, 0, 0, 'Müller, Hans'); s.setCellInput(id, 1, 0, 'x"y'); const csv = tableToCsv(s, id, { delimiter: ',', bom: false, crlf: true }); expect(csv).toContain('"Müller, Hans"'); expect(csv).toContain('"x""y"'); const [line] = parseCsv(csv, ','); expect(line).toEqual(['Müller, Hans', 'x"y']); }); it('a CSV cell "=1+2" becomes a formula that evaluates to 3', () => { const s = fresh(); const { cells, rows, cols } = matrixToCells(parseCsv('=1+2\n', ','), true); const id = s.addTableFromCells(s.doc.sheets[1].id, 'Rechnen', cells, rows, cols); expect(s.cellValue(id, 0, 0)).toBe(3); }); it('locale number detection (de vs en)', () => { const { cells } = matrixToCells(parseCsv('1.234,56;7,89;1,234.50\n', ';'), true); expect(cells['0,0']).toBe('1234.56'); expect(cells['1,0']).toBe('7.89'); expect(cells['2,0']).toBe('1234.50'); }); });