aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/utils/csvParser.test.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-01-01 22:06:40 +0900
committernsfisis <nsfisis@gmail.com>2026-01-01 22:06:40 +0900
commit8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b (patch)
treebe39e1436f83c716fc45df133106fba7dd4bc23a /src/client/utils/csvParser.test.ts
parent830b370f1b8e0f3a384b2d242ab120812e81977d (diff)
downloadkioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.tar.gz
kioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.tar.zst
kioku-8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b.zip
feat(import): add CSV bulk import for notes
Add client-side CSV parsing and bulk import API endpoint for importing notes from CSV files. Supports quoted fields, newlines in values, and escaped quotes. - New POST /api/decks/{deckId}/notes/import endpoint for bulk creation - CSV parser with RFC 4180 compliance - Multi-phase import modal (upload → validate → preview → import) - Client-side validation with per-row error reporting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/utils/csvParser.test.ts')
-rw-r--r--src/client/utils/csvParser.test.ts159
1 files changed, 159 insertions, 0 deletions
diff --git a/src/client/utils/csvParser.test.ts b/src/client/utils/csvParser.test.ts
new file mode 100644
index 0000000..616e3e8
--- /dev/null
+++ b/src/client/utils/csvParser.test.ts
@@ -0,0 +1,159 @@
+import { describe, expect, it } from "vitest";
+import { parseCSV } from "./csvParser";
+
+describe("parseCSV", () => {
+ it("parses simple CSV with headers", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,hello,world
+English,Basic,goodbye,farewell`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.headers).toEqual([
+ "deck",
+ "note_type",
+ "Front",
+ "Back",
+ ]);
+ expect(result.data.rows).toHaveLength(2);
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "hello",
+ Back: "world",
+ });
+ expect(result.data.rows[1]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "goodbye",
+ Back: "farewell",
+ });
+ }
+ });
+
+ it("handles quoted fields with commas", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,"hello, world","foo, bar"`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "hello, world",
+ Back: "foo, bar",
+ });
+ }
+ });
+
+ it("handles quoted fields with newlines", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,"line1
+line2","back"`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows).toHaveLength(1);
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "line1\nline2",
+ Back: "back",
+ });
+ }
+ });
+
+ it("handles escaped quotes", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,"say ""hello""",world`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: 'say "hello"',
+ Back: "world",
+ });
+ }
+ });
+
+ it("skips empty lines", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,hello,world
+
+English,Basic,goodbye,farewell
+`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows).toHaveLength(2);
+ }
+ });
+
+ it("handles CRLF line endings", () => {
+ const csv = "deck,note_type,Front,Back\r\nEnglish,Basic,hello,world\r\n";
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows).toHaveLength(1);
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "hello",
+ Back: "world",
+ });
+ }
+ });
+
+ it("returns error for empty file", () => {
+ const result = parseCSV("");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error.message).toBe("CSV file is empty");
+ }
+ });
+
+ it("returns error for inconsistent column count", () => {
+ const csv = `deck,note_type,Front,Back
+English,Basic,hello`;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error.message).toBe("Row 2: Expected 4 columns, got 3");
+ expect(result.error.line).toBe(2);
+ }
+ });
+
+ it("trims whitespace from values", () => {
+ const csv = `deck,note_type,Front,Back
+English , Basic , hello , world `;
+
+ const result = parseCSV(csv);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.rows[0]).toEqual({
+ deck: "English",
+ note_type: "Basic",
+ Front: "hello",
+ Back: "world",
+ });
+ }
+ });
+});