test: unit and end-to-end suites

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 27fafa0bcd
commit 9b00e21c44
49 files changed
+6694 -1

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
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'] });
});