Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 252 lines 8.6 kB view raw
1import { describe, it, expect } from 'vitest'; 2import { exportToCsv, escapeField } from '../src/sheets/csv-export.js'; 3 4// UTF-8 BOM character 5const BOM = '\uFEFF'; 6 7/** 8 * Helper: create a getCellValue function from a 2D array. 9 * Array is 0-indexed; getCellValue is 1-based (row, col). 10 */ 11function gridFromArray(data) { 12 return (row, col) => { 13 const r = row - 1; 14 const c = col - 1; 15 if (r < 0 || r >= data.length) return ''; 16 if (c < 0 || c >= data[r].length) return ''; 17 return String(data[r][c] ?? ''); 18 }; 19} 20 21// ============================================================ 22// escapeField 23// ============================================================ 24describe('CSV Export — escapeField', () => { 25 it('returns empty string for empty input', () => { 26 expect(escapeField('', ',')).toBe(''); 27 }); 28 29 it('returns plain string unchanged when no special chars', () => { 30 expect(escapeField('hello', ',')).toBe('hello'); 31 }); 32 33 it('quotes fields containing the comma delimiter', () => { 34 expect(escapeField('a,b', ',')).toBe('"a,b"'); 35 }); 36 37 it('quotes fields containing double quotes and escapes them', () => { 38 expect(escapeField('say "hi"', ',')).toBe('"say ""hi"""'); 39 }); 40 41 it('quotes fields containing newlines', () => { 42 expect(escapeField('line1\nline2', ',')).toBe('"line1\nline2"'); 43 }); 44 45 it('quotes fields containing carriage returns', () => { 46 expect(escapeField('line1\rline2', ',')).toBe('"line1\rline2"'); 47 }); 48 49 it('handles tab delimiter — does not quote commas when delimiter is tab', () => { 50 expect(escapeField('a,b', '\t')).toBe('a,b'); 51 }); 52 53 it('quotes fields containing tab when delimiter is tab', () => { 54 expect(escapeField('a\tb', '\t')).toBe('"a\tb"'); 55 }); 56 57 it('handles field with all special characters', () => { 58 expect(escapeField('a,"b"\nc', ',')).toBe('"a,""b""\nc"'); 59 }); 60}); 61 62// ============================================================ 63// exportToCsv — basic output 64// ============================================================ 65describe('CSV Export — exportToCsv basic', () => { 66 it('generates CSV from a simple 2x2 grid', () => { 67 const data = [['A', 'B'], ['1', '2']]; 68 const result = exportToCsv(gridFromArray(data), 2, 2); 69 expect(result).toBe(BOM + 'A,B\r\n1,2'); 70 }); 71 72 it('starts with UTF-8 BOM', () => { 73 const data = [['x']]; 74 const result = exportToCsv(gridFromArray(data), 1, 1); 75 expect(result.charCodeAt(0)).toBe(0xFEFF); 76 }); 77 78 it('uses CRLF line endings (RFC 4180)', () => { 79 const data = [['a'], ['b'], ['c']]; 80 const result = exportToCsv(gridFromArray(data), 3, 1); 81 expect(result).toBe(BOM + 'a\r\nb\r\nc'); 82 }); 83 84 it('handles empty cells', () => { 85 const data = [['', 'B'], ['', '']]; 86 const result = exportToCsv(gridFromArray(data), 2, 2); 87 expect(result).toBe(BOM + ',B\r\n,'); 88 }); 89 90 it('handles single cell', () => { 91 const data = [['only']]; 92 const result = exportToCsv(gridFromArray(data), 1, 1); 93 expect(result).toBe(BOM + 'only'); 94 }); 95 96 it('handles numeric values', () => { 97 const data = [['42', '3.14']]; 98 const result = exportToCsv(gridFromArray(data), 1, 2); 99 expect(result).toBe(BOM + '42,3.14'); 100 }); 101}); 102 103// ============================================================ 104// exportToCsv — RFC 4180 escaping 105// ============================================================ 106describe('CSV Export — RFC 4180 escaping', () => { 107 it('quotes fields with commas', () => { 108 const data = [['a,b', 'c']]; 109 const result = exportToCsv(gridFromArray(data), 1, 2); 110 expect(result).toBe(BOM + '"a,b",c'); 111 }); 112 113 it('escapes double quotes within fields', () => { 114 const data = [['say "hello"']]; 115 const result = exportToCsv(gridFromArray(data), 1, 1); 116 expect(result).toBe(BOM + '"say ""hello"""'); 117 }); 118 119 it('quotes fields with embedded newlines', () => { 120 const data = [['line1\nline2']]; 121 const result = exportToCsv(gridFromArray(data), 1, 1); 122 expect(result).toBe(BOM + '"line1\nline2"'); 123 }); 124 125 it('handles field with commas, quotes, and newlines together', () => { 126 const data = [['a,"b"\nc']]; 127 const result = exportToCsv(gridFromArray(data), 1, 1); 128 expect(result).toBe(BOM + '"a,""b""\nc"'); 129 }); 130}); 131 132// ============================================================ 133// exportToCsv — TSV (tab delimiter) 134// ============================================================ 135describe('CSV Export — TSV variant', () => { 136 it('uses tab delimiter', () => { 137 const data = [['A', 'B'], ['1', '2']]; 138 const result = exportToCsv(gridFromArray(data), 2, 2, { delimiter: '\t' }); 139 expect(result).toBe(BOM + 'A\tB\r\n1\t2'); 140 }); 141 142 it('does not quote fields with commas in TSV mode', () => { 143 const data = [['a,b', 'c']]; 144 const result = exportToCsv(gridFromArray(data), 1, 2, { delimiter: '\t' }); 145 expect(result).toBe(BOM + 'a,b\tc'); 146 }); 147 148 it('quotes fields with tabs in TSV mode', () => { 149 const data = [['a\tb', 'c']]; 150 const result = exportToCsv(gridFromArray(data), 1, 2, { delimiter: '\t' }); 151 expect(result).toBe(BOM + '"a\tb"\tc'); 152 }); 153}); 154 155// ============================================================ 156// exportToCsv — includeHeaders option 157// ============================================================ 158describe('CSV Export — includeHeaders', () => { 159 it('prepends column letter headers when includeHeaders is true', () => { 160 const data = [['val1', 'val2']]; 161 const result = exportToCsv(gridFromArray(data), 1, 2, { includeHeaders: true }); 162 expect(result).toBe(BOM + 'A,B\r\nval1,val2'); 163 }); 164 165 it('generates correct headers for 27+ columns (AA, AB, ...)', () => { 166 const row = []; 167 for (let i = 0; i < 28; i++) row.push(String(i)); 168 const result = exportToCsv(gridFromArray([row]), 1, 28, { includeHeaders: true }); 169 const lines = result.slice(1).split('\r\n'); // skip BOM 170 const headers = lines[0].split(','); 171 expect(headers[0]).toBe('A'); 172 expect(headers[25]).toBe('Z'); 173 expect(headers[26]).toBe('AA'); 174 expect(headers[27]).toBe('AB'); 175 }); 176}); 177 178// ============================================================ 179// exportToCsv — range export 180// ============================================================ 181describe('CSV Export — range export', () => { 182 it('exports only the specified range', () => { 183 const data = [ 184 ['A1', 'B1', 'C1'], 185 ['A2', 'B2', 'C2'], 186 ['A3', 'B3', 'C3'], 187 ]; 188 const result = exportToCsv(gridFromArray(data), 3, 3, { 189 range: { startRow: 2, endRow: 3, startCol: 2, endCol: 3 }, 190 }); 191 expect(result).toBe(BOM + 'B2,C2\r\nB3,C3'); 192 }); 193 194 it('exports a single cell range', () => { 195 const data = [['A1', 'B1'], ['A2', 'B2']]; 196 const result = exportToCsv(gridFromArray(data), 2, 2, { 197 range: { startRow: 1, endRow: 1, startCol: 2, endCol: 2 }, 198 }); 199 expect(result).toBe(BOM + 'B1'); 200 }); 201 202 it('exports full range when range is null', () => { 203 const data = [['X', 'Y']]; 204 const result = exportToCsv(gridFromArray(data), 1, 2, { range: null }); 205 expect(result).toBe(BOM + 'X,Y'); 206 }); 207 208 it('combines range with includeHeaders', () => { 209 const data = [['A1', 'B1'], ['A2', 'B2']]; 210 const result = exportToCsv(gridFromArray(data), 2, 2, { 211 range: { startRow: 1, endRow: 2, startCol: 2, endCol: 2 }, 212 includeHeaders: true, 213 }); 214 expect(result).toBe(BOM + 'B\r\nB1\r\nB2'); 215 }); 216}); 217 218// ============================================================ 219// exportToCsv — edge cases 220// ============================================================ 221describe('CSV Export — edge cases', () => { 222 it('handles a completely empty grid', () => { 223 const result = exportToCsv(() => '', 2, 2); 224 // All cells empty => two rows of "," 225 expect(result).toBe(BOM + ',\r\n,'); 226 }); 227 228 it('preserves leading/trailing whitespace in values', () => { 229 const data = [[' spaced ']]; 230 const result = exportToCsv(gridFromArray(data), 1, 1); 231 expect(result).toBe(BOM + ' spaced '); 232 }); 233 234 it('handles unicode characters', () => { 235 const data = [['\u00e9', '\u00fc']]; 236 const result = exportToCsv(gridFromArray(data), 1, 2); 237 expect(result).toBe(BOM + '\u00e9,\u00fc'); 238 }); 239 240 it('handles formulas as string values', () => { 241 const data = [['=SUM(A1:A10)']]; 242 const result = exportToCsv(gridFromArray(data), 1, 1); 243 expect(result).toBe(BOM + '=SUM(A1:A10)'); 244 }); 245 246 it('handles very long string values', () => { 247 const longStr = 'x'.repeat(10000); 248 const data = [[longStr]]; 249 const result = exportToCsv(gridFromArray(data), 1, 1); 250 expect(result).toBe(BOM + longStr); 251 }); 252});