Skip to content

Commit 3aad2b5

Browse files
authored
Merge pull request #707 from TEEZY234/add-csv-export-tests-692
test(frontend): add csvExport unit tests and update Vitest discovery …
2 parents 1d75626 + 31945db commit 3aad2b5

4 files changed

Lines changed: 124 additions & 5 deletions

File tree

frontend/src/components/streamCreationForm.test.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { render, screen, fireEvent } from "@testing-library/react";
22
import { test, expect, vi } from "vitest";
3+
4+
vi.mock("next/navigation", () => ({
5+
useRouter: vi.fn(() => ({ push: vi.fn() })),
6+
}));
7+
38
import { StreamCreationWizard } from "./stream-creation/StreamCreationWizard";
49
import React from "react";
510

@@ -16,9 +21,9 @@ test("StreamCreationWizard — validation errors shown", () => {
1621
);
1722

1823
// The wizard starts at Template step. We need to go to Recipient step to test validation.
19-
// Actually, let's just click 'Next' and see if it goes to next step or shows errors if any.
20-
fireEvent.click(screen.getByText(/next/i));
24+
// Click the Next button explicitly instead of matching incidental text.
25+
fireEvent.click(screen.getByRole('button', { name: /next/i }));
2126

2227
// It should move to Recipient step.
23-
expect(screen.getByText(/recipient/i)).toBeInTheDocument();
28+
expect(screen.getByRole('heading', { name: /recipient address/i })).toBeInTheDocument();
2429
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { convertArrayToCSV, downloadCSV, escapeCsvCell } from './csvExport';
3+
4+
describe('escapeCsvCell', () => {
5+
it('returns empty string for null', () => {
6+
expect(escapeCsvCell(null)).toBe('');
7+
});
8+
9+
it('returns empty string for undefined', () => {
10+
expect(escapeCsvCell(undefined)).toBe('');
11+
});
12+
13+
it('returns plain strings unchanged', () => {
14+
expect(escapeCsvCell('hello')).toBe('hello');
15+
});
16+
17+
it('quotes values containing commas', () => {
18+
expect(escapeCsvCell('hello,world')).toBe('"hello,world"');
19+
});
20+
21+
it('quotes values containing newlines', () => {
22+
expect(escapeCsvCell('hello\nworld')).toBe('"hello\nworld"');
23+
});
24+
25+
it('doubles quotes inside values', () => {
26+
expect(escapeCsvCell('She said "Hi"')).toBe('"She said ""Hi"""');
27+
});
28+
29+
it('formats Date values via toLocaleString', () => {
30+
const original = Date.prototype.toLocaleString;
31+
Date.prototype.toLocaleString = function () {
32+
return '2026-06-01 12:00:00';
33+
};
34+
35+
try {
36+
expect(escapeCsvCell(new Date('2026-06-01T12:00:00Z'))).toBe('2026-06-01 12:00:00');
37+
} finally {
38+
Date.prototype.toLocaleString = original;
39+
}
40+
});
41+
});
42+
43+
describe('convertArrayToCSV', () => {
44+
it('returns an empty string for an empty array', () => {
45+
expect(convertArrayToCSV([])).toBe('');
46+
});
47+
48+
it('serializes a single row correctly', () => {
49+
const rows = [{ name: 'Alice', amount: 10 }];
50+
expect(convertArrayToCSV(rows)).toBe('name,amount\nAlice,10');
51+
});
52+
53+
it('serializes multiple rows with mixed values', () => {
54+
const rows = [
55+
{ id: 1, value: 'hello,world' },
56+
{ id: 2, value: 'line\nbreak' },
57+
{ id: 3, value: 'quote"test' },
58+
];
59+
60+
expect(convertArrayToCSV(rows)).toBe(
61+
'id,value\n1,"hello,world"\n2,"line\nbreak"\n3,"quote""test"'
62+
);
63+
});
64+
65+
it('preserves header order from the first row', () => {
66+
const rows = [
67+
{ b: 1, a: 'first' },
68+
{ b: 2, a: 'second' },
69+
];
70+
71+
expect(convertArrayToCSV(rows)).toBe('b,a\n1,first\n2,second');
72+
});
73+
});
74+
75+
describe('downloadCSV', () => {
76+
let originalURL: typeof URL;
77+
78+
beforeEach(() => {
79+
originalURL = globalThis.URL;
80+
document.body.innerHTML = '';
81+
});
82+
83+
afterEach(() => {
84+
vi.restoreAllMocks();
85+
Object.defineProperty(globalThis, 'URL', {
86+
configurable: true,
87+
writable: true,
88+
value: originalURL,
89+
});
90+
});
91+
92+
it('creates a Blob and triggers a download', () => {
93+
const createObjectURL = vi.fn(() => 'blob://test-url');
94+
const revokeObjectURL = vi.fn();
95+
96+
Object.defineProperty(globalThis, 'URL', {
97+
configurable: true,
98+
writable: true,
99+
value: {
100+
...globalThis.URL,
101+
createObjectURL,
102+
revokeObjectURL,
103+
} as unknown as typeof URL,
104+
});
105+
106+
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
107+
downloadCSV([{ foo: 'bar' }], 'test.csv');
108+
109+
expect(createObjectURL).toHaveBeenCalledTimes(1);
110+
expect(clickSpy).toHaveBeenCalledTimes(1);
111+
expect(revokeObjectURL).toHaveBeenCalledWith('blob://test-url');
112+
expect(document.querySelector('a[download="test.csv"]')).toBeNull();
113+
});
114+
});

frontend/src/utils/csvExport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
type CsvRow = Record<string, unknown>;
44

5-
const escapeCsvCell = (value: unknown): string => {
5+
export const escapeCsvCell = (value: unknown): string => {
66
if (value === null || value === undefined) {
77
return '';
88
}

frontend/vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default defineConfig({
77
environment: 'happy-dom',
88
globals: true,
99
setupFiles: ['./src/__tests__/setup.ts'],
10-
include: ['src/__tests__/**/*.{test,spec}.{ts,tsx}'],
10+
include: ['src/__tests__/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}'],
1111
coverage: {
1212
reporter: ['text', 'json', 'html'],
1313
include: ['src/utils/**', 'src/hooks/**', 'src/components/**'],

0 commit comments

Comments
 (0)