Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 229 additions & 1 deletion react/src/helper/csv-util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
*/
// csv-util.test.ts
import { downloadCSV, JSONToCSVBody, UTF8_BOM } from './csv-util';
import {
downloadCSV,
exportCSVWithFormattingRules,
JSONToCSVBody,
parseCSV,
UTF8_BOM,
} from './csv-util';

describe('JSONToCSVBody', () => {
it('should convert JSON data to CSV format without formatting rules', () => {
Expand Down Expand Up @@ -185,3 +191,225 @@ describe('downloadCSV', () => {
expect(capturedBlob?.type).toBe('text/csv;charset=utf-8;');
});
});

describe('exportCSVWithFormattingRules', () => {
const originalCreateObjectURL = URL.createObjectURL;
const originalRevokeObjectURL = URL.revokeObjectURL;
let capturedBlob: Blob | undefined;
let capturedAnchor: HTMLAnchorElement | undefined;

beforeEach(() => {
capturedBlob = undefined;
capturedAnchor = undefined;
vi.useFakeTimers();
URL.createObjectURL = vi.fn((blob: Blob) => {
capturedBlob = blob;
return 'blob:mock-url';
}) as typeof URL.createObjectURL;
URL.revokeObjectURL = vi.fn() as typeof URL.revokeObjectURL;

const originalCreateElement = document.createElement.bind(document);
vi.spyOn(document, 'createElement').mockImplementation(
(tagName: string) => {
const element = originalCreateElement(tagName);
if (tagName === 'a') {
capturedAnchor = element as HTMLAnchorElement;
vi.spyOn(element as HTMLAnchorElement, 'click').mockImplementation(
() => {},
);
}
return element;
},
);
});

afterEach(() => {
vi.runAllTimers();
vi.useRealTimers();
vi.restoreAllMocks();
URL.createObjectURL = originalCreateObjectURL;
URL.revokeObjectURL = originalRevokeObjectURL;
});

const decodeKeepingBOM = async (blob: Blob) =>
new TextDecoder('utf-8', { ignoreBOM: true }).decode(
await blob.arrayBuffer(),
);

it('should export CSV with formatting rules applied', async () => {
const data = [
{ name: 'Alice', score: 95 },
{ name: 'Bob', score: 87 },
];
const format_rules = {
score: (value: number) => `${value}%`,
};

exportCSVWithFormattingRules(data, 'scores.csv', format_rules);

const text = await decodeKeepingBOM(capturedBlob!);
expect(text).toContain('"name","score"');
expect(text).toContain('"Alice","95%"');
expect(text).toContain('"Bob","87%"');
expect(capturedAnchor?.download).toBe('scores.csv');
});

it('should export CSV without formatting rules', async () => {
const data = [
{ id: 1, value: 'test' },
{ id: 2, value: 'data' },
];

exportCSVWithFormattingRules(data, 'export');

const text = await decodeKeepingBOM(capturedBlob!);
expect(text).toContain('"id","value"');
expect(text).toContain('1,"test"');
expect(text).toContain('2,"data"');
expect(capturedAnchor?.download).toBe('export.csv');
});

it('should include UTF-8 BOM in exported file', async () => {
const data = [{ name: '한글', value: 'Korean' }];

exportCSVWithFormattingRules(data, 'multilingual.csv');

const text = await decodeKeepingBOM(capturedBlob!);
expect(text.startsWith(UTF8_BOM)).toBe(true);
});
});

describe('parseCSV', () => {
it('should parse basic CSV with headers', () => {
const csv = 'name,age\nJohn,30\nJane,25';
const result = parseCSV(csv);

expect(result).toEqual([
{ name: 'John', age: '30' },
{ name: 'Jane', age: '25' },
]);
});

it('should handle UTF-8 BOM at the start', () => {
const csv = '\uFEFFname,value\ntest,123';
const result = parseCSV(csv);

expect(result).toEqual([{ name: 'test', value: '123' }]);
expect(result[0]).not.toHaveProperty('\uFEFFname');
});

it('should handle quoted fields with commas', () => {
const csv = 'name,description\n"Smith, John","A person"';
const result = parseCSV(csv);

expect(result).toEqual([{ name: 'Smith, John', description: 'A person' }]);
});

it('should handle escaped double quotes inside quoted fields', () => {
const csv = 'title,quote\n"Book","She said ""Hello"""';
const result = parseCSV(csv);

expect(result).toEqual([{ title: 'Book', quote: 'She said "Hello"' }]);
});

it('should handle line breaks inside quoted fields', () => {
const csv = 'name,bio\n"John","Line 1\nLine 2"';
const result = parseCSV(csv);

expect(result).toEqual([{ name: 'John', bio: 'Line 1\nLine 2' }]);
});

it('should handle CRLF line endings', () => {
const csv = 'a,b\r\n1,2\r\n3,4';
const result = parseCSV(csv);

expect(result).toEqual([
{ a: '1', b: '2' },
{ a: '3', b: '4' },
]);
});

it('should trim whitespace from unquoted values', () => {
const csv = 'name , age \n John , 30 ';
const result = parseCSV(csv);

expect(result).toEqual([{ name: 'John', age: '30' }]);
});

it('should skip fully empty lines', () => {
const csv = 'name,value\n\n\nJohn,1\n\nJane,2\n\n';
const result = parseCSV(csv);

expect(result).toEqual([
{ name: 'John', value: '1' },
{ name: 'Jane', value: '2' },
]);
});

it('should handle file without trailing newline', () => {
const csv = 'x,y\n1,2';
const result = parseCSV(csv);

expect(result).toEqual([{ x: '1', y: '2' }]);
});

it('should return empty array for empty input', () => {
expect(parseCSV('')).toEqual([]);
expect(parseCSV('\n\n')).toEqual([]);
expect(parseCSV(' \n \n ')).toEqual([]);
});

it('should handle missing values (sparse rows)', () => {
const csv = 'a,b,c\n1,2,3\n4,,6';
const result = parseCSV(csv);

expect(result).toEqual([
{ a: '1', b: '2', c: '3' },
{ a: '4', b: '', c: '6' },
]);
});

it('should throw error on unterminated quoted field', () => {
const csv = 'name,value\n"John,123';

expect(() => parseCSV(csv)).toThrow('Unterminated quoted field in CSV');
});

it('should handle empty quoted fields', () => {
const csv = 'a,b,c\n"",2,""';
const result = parseCSV(csv);

expect(result).toEqual([{ a: '', b: '2', c: '' }]);
});

it('should handle headers with special characters', () => {
const csv = 'email@domain,first-name,last_name\ntest@test.com,John,Doe';
const result = parseCSV(csv);

expect(result).toEqual([
{
'email@domain': 'test@test.com',
'first-name': 'John',
last_name: 'Doe',
},
]);
});

it('should handle complex mixed quoting scenarios', () => {
const csv = `name,address,notes
"John Doe","123 Main St, Apt ""5""","New customer
Very important"
Jane,"456 Oak Ave",Regular`;

const result = parseCSV(csv);

expect(result).toEqual([
{
name: 'John Doe',
address: '123 Main St, Apt "5"',
notes: 'New customer\nVery important',
},
{ name: 'Jane', address: '456 Oak Ave', notes: 'Regular' },
]);
});
});