diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-01-01 22:06:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-01-01 22:06:40 +0900 |
| commit | 8dbe5c2a1d8dc15bbdd6810b2582c680e1c0bb9b (patch) | |
| tree | be39e1436f83c716fc45df133106fba7dd4bc23a /src/client/utils/csvParser.test.ts | |
| parent | 830b370f1b8e0f3a384b2d242ab120812e81977d (diff) | |
| download | kioku-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.ts | 159 |
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", + }); + } + }); +}); |
