Files
solelog/apps/api/test/csv.test.ts
Bas van Rossem b72076ee15 test(api): Worker export/CSV — fill gaps + lifecycle test
Drove src/lib/csv.ts to 100% stmts/branch/funcs/lines (was 97.95%/55.55%):
- added a null-fallback row test covering every ?? default (activityName,
  userName, insoleType→Kurk, pairCount→2, duration/paused→0) and the null
  end-time branch (line 65).
- added CSV-escaping edge tests: commas, embedded quotes, and newlines inside
  a field stay inside the quoted cell.
- added formatDuration edge (hours >99 → 100:00:00).
- added a /api/export ordering-by-start-time behavior test and adopted
  resetDb per-test isolation in the export suite.

No bugs found; CSV code behaves correctly. sessions.ts /api/export route
already at 100%. Full api suite green (150 tests).
2026-07-22 12:24:04 +02:00

83 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { buildSessionsCsv, quote, formatDuration, type SessionCsvRow } from '../src/lib/csv';
const row: SessionCsvRow = {
id: 7,
activityName: 'Frezen',
userName: 'Jan',
insoleType: 'Kurk',
pairCount: 2,
startTime: new Date('2026-06-17T08:00:00Z'),
endTime: new Date('2026-06-17T08:01:30Z'),
durationSeconds: 90,
pausedSeconds: 0,
};
describe('buildSessionsCsv', () => {
it('omits the Worker column by default (legacy header)', () => {
const lines = buildSessionsCsv([row]).split('\n');
expect(lines[0]).toBe(
'"ID","Task","Insole Type","No. of Insoles","Date","Total Duration","Paused Duration","Start Time","End Time"',
);
expect(lines[1]).toContain('"Frezen"');
expect(lines[1]).toContain('"00:01:30"');
expect(lines[1]).not.toContain('"Jan"');
});
it('prepends a Worker column when includeWorker is true', () => {
const lines = buildSessionsCsv([row], { includeWorker: true }).split('\n');
expect(lines[0]).toBe(
'"Worker","ID","Task","Insole Type","No. of Insoles","Date","Total Duration","Paused Duration","Start Time","End Time"',
);
expect(lines[1].startsWith('"Jan"')).toBe(true);
});
it('applies legacy fallbacks for null fields and blanks a missing end time', () => {
// Every nullable field null (+ pairCount null via cast, since the type is non-null
// but the code still carries a legacy `?? 2` default). includeWorker exercises the
// userName fallback too. Covers each `??` fallback branch and the null-end-time branch.
const emptyRow = {
id: 1,
activityName: null,
userName: null,
insoleType: null,
pairCount: null,
startTime: new Date('2026-06-17T08:00:00Z'),
endTime: null,
durationSeconds: null,
pausedSeconds: null,
} as unknown as SessionCsvRow;
const cells = buildSessionsCsv([emptyRow], { includeWorker: true }).split('\n')[1].split(',');
expect(cells[0]).toBe('""'); // Worker → ''
expect(cells[1]).toBe('"1"'); // ID
expect(cells[2]).toBe('""'); // Task → ''
expect(cells[3]).toBe('"Kurk"'); // Insole Type → legacy default
expect(cells[4]).toBe('"2"'); // No. of Insoles → legacy default
expect(cells[6]).toBe('"00:00:00"'); // Total Duration → 0
expect(cells[7]).toBe('"00:00:00"'); // Paused Duration → 0
expect(cells[9]).toBe('""'); // End Time → '' (null end)
});
it('escapes commas, quotes and newlines inside a field', () => {
const tricky: SessionCsvRow = { ...row, activityName: 'Frezen, "spoed"\n2e regel' };
const csv = buildSessionsCsv([tricky]);
// Comma stays inside the quoted cell, embedded quotes are doubled, newline is preserved.
expect(csv).toContain('"Frezen, ""spoed""\n2e regel"');
});
});
describe('csv helpers (edges)', () => {
it('quote wraps and escapes commas, quotes and newlines', () => {
expect(quote('a,b')).toBe('"a,b"');
expect(quote('a"b')).toBe('"a""b"');
expect(quote('a\nb')).toBe('"a\nb"');
});
it('formatDuration renders zero and lets hours exceed 99', () => {
expect(formatDuration(0)).toBe('00:00:00');
expect(formatDuration(3661)).toBe('01:01:01');
expect(formatDuration(360000)).toBe('100:00:00');
});
});