Files
ink-note-app/test/csv.test.js
T
2026-10-15 11:57:09 +02:00

102 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseCsv, toCsv, databaseCsv, parseNumber, parseDate, inferType, autoMap, planImport } from '../src/lib/csv.js';
test('CSV: quotes, embedded commas and newlines, CRLF, BOM and other delimiters', () => {
const t = 'Name,Note\r\n"Smith, J","He said ""hi""\nand left"\r\nPlain,\r\n\r\n';
const { rows, delimiter } = parseCsv(t);
assert.equal(delimiter, ',');
assert.deepEqual(rows, [['Name', 'Note'], ['Smith, J', 'He said "hi"\nand left'], ['Plain', '']]);
assert.equal(parseCsv('a;b;c\n1;2;3').delimiter, ';');
assert.deepEqual(parseCsv('a\tb\n1\t2').rows, [['a', 'b'], ['1', '2']]);
assert.deepEqual(parseCsv('a,b\n1,2').rows, [['a', 'b'], ['1', '2']]); // no trailing newline
assert.deepEqual(parseCsv('').rows, []);
});
test('CSV: what toCsv writes parseCsv reads back', () => {
const rows = [['Name', 'Tags'], ['A, B', 'x"y'], [' pad ', 'line\nbreak'], ['', 'ü ☃']];
assert.deepEqual(parseCsv(toCsv(rows)).rows, rows.map((r) => r.map((c) => c)));
});
test('numbers and dates come in many shapes', () => {
assert.equal(parseNumber('$1,234.50'), 1234.5);
assert.equal(parseNumber('12%'), 12);
assert.equal(parseNumber('1.234,5'), 1234.5);
assert.equal(parseNumber('12,5'), 12.5);
assert.equal(parseNumber('5,200'), 5200);
assert.equal(parseNumber('$1,234,567'), 1234567);
assert.equal(parseNumber('abc'), null);
assert.equal(parseNumber(''), null);
assert.equal(parseDate('2026-03-01T10:00:00Z'), '2026-03-01');
assert.equal(parseDate('3/1/2026'), '2026-03-01');
assert.equal(parseDate('25/12/2026'), '2026-12-25');
assert.equal(parseDate('1.3.2026'), '2026-03-01');
assert.equal(parseDate('Mar 5, 2026'), '2026-03-05');
assert.equal(parseDate('2026-02-31'), '');
assert.equal(parseDate('soon'), '');
});
test('a column\'s type is guessed from its values', () => {
assert.equal(inferType(['1', '2.5', '', '10']).type, 'number');
assert.equal(inferType(['2026-01-01', '2026-02-03']).type, 'date');
assert.equal(inferType(['yes', 'no', 'yes']).type, 'checkbox');
assert.equal(inferType(['1', '0', '1']).type, 'number'); // only 0/1 stays a number
assert.equal(inferType(['https://a.b', 'http://c.d']).type, 'url');
assert.equal(inferType(['Todo', 'Done', 'Todo', 'Done', 'Doing', 'Done']).type, 'select');
assert.equal(inferType(['ui, api', 'api', 'ui, docs', 'api, docs', 'ui']).type, 'multiselect');
assert.equal(inferType(['one thing', 'another', 'a third', 'and more text here', 'x']).type, 'text');
assert.equal(inferType(['', '']).type, 'text');
});
test('importing maps columns, makes properties and gathers options', () => {
const { rows } = parseCsv('Title,Status,Due,Points,Done,Labels,Extra\nA,Todo,2026-03-01,5,yes,"ui, api",\nB,Done,3/2/2026,"1,200",no,api,\nC,Todo,,x,,,\nD,Todo,,,,ui,');
const [header, ...data] = rows;
const existing = [{ key: 'status', name: 'Status', type: 'select', options: ['Todo'] }];
const cols = autoMap(header, data, existing);
assert.deepEqual(cols.map((c) => c.target), ['title', 'existing', 'new', 'new', 'new', 'new', 'skip']);
assert.equal(cols[2].type, 'date');
assert.equal(cols[3].type, 'text'); // "x" is in the numbers
const plan = planImport({ rows: data, columns: cols, existing });
assert.deepEqual(plan.properties.map((p) => [p.key, p.type]), [['due', 'date'], ['points', 'text'], ['done', 'checkbox'], ['labels', 'multiselect']]);
assert.deepEqual(plan.options, { status: ['Done'] }); // Todo was there already
assert.deepEqual(plan.properties[3].options, ['ui', 'api']);
assert.deepEqual(plan.items[0], { title: 'A', meta: { status: 'Todo', due: '2026-03-01', points: '5', done: 'true', labels: ['ui', 'api'] } });
assert.equal(plan.items[1].meta.due, '2026-03-02');
assert.equal(plan.items[1].meta.points, '1,200');
assert.deepEqual(plan.items[2].meta, { status: 'Todo', points: 'x' });
});
test('a number column imports as numbers and names that clash get their own keys', () => {
const rows = [['Name', 'Tags', 'Due date', 'Due-date'], ['A', 't', '2026-01-01', '2026-02-02'], ['B', '', '2026-01-03', '']];
const cols = autoMap(rows[0], rows.slice(1), []);
const plan = planImport({ rows: rows.slice(1), columns: cols, existing: [] });
assert.deepEqual(plan.properties.map((p) => p.key), ['tags-2', 'due-date', 'due-date-2']);
assert.equal(plan.items[0].meta['due-date-2'], '2026-02-02');
const n = planImport({ rows: [['A', '7.5'], ['B', '']], columns: [{ index: 0, target: 'title' }, { index: 1, target: 'new', name: 'Score', type: 'number' }], existing: [] });
assert.deepEqual(n.items.map((i) => i.meta.score), ['7.5', undefined]);
});
test('export writes a Name column and one per property, and imports back the same', () => {
const db = { properties: [
{ key: 'status', name: 'Status', type: 'select', options: ['Todo'] },
{ key: 'points', name: 'Points', type: 'number' },
{ key: 'done', name: 'Done', type: 'checkbox' },
{ key: 'who', name: 'Who', type: 'people' }
] };
const recs = [
{ title: 'Write, docs', props: { status: 'Todo', points: '5', done: 'true', who: ['Ana', 'Ben'] } },
{ title: 'Plan', props: {} }
];
const csv = databaseCsv(db, recs);
assert.deepEqual(parseCsv(csv).rows, [
['Name', 'Status', 'Points', 'Done', 'Who'],
['Write, docs', 'Todo', '5', 'true', 'Ana, Ben'],
['Plan', '', '', 'false', '']
]);
const [h, ...data] = parseCsv(csv).rows;
const cols = autoMap(h, data, db.properties);
assert.deepEqual(cols.map((c) => c.target), ['title', 'existing', 'existing', 'existing', 'existing']);
const plan = planImport({ rows: data, columns: cols, existing: db.properties });
assert.deepEqual(plan.items[0].meta, { status: 'Todo', points: '5', done: 'true', who: ['Ana', 'Ben'] });
});