@svgrid/enterprise 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +97 -0
- package/README.md +37 -0
- package/package.json +72 -0
- package/src/ai.test.ts +522 -0
- package/src/ai.ts +782 -0
- package/src/export.ts +512 -0
- package/src/import.test.ts +415 -0
- package/src/import.ts +648 -0
- package/src/index.ts +88 -0
- package/src/install.ts +114 -0
- package/src/license.ts +90 -0
- package/src/pdfmake-shims.d.ts +22 -0
- package/src/pivot.test.ts +308 -0
- package/src/pivot.ts +549 -0
- package/src/print.ts +127 -0
- package/src/revoked.ts +49 -0
- package/src/smart-shim.ts +105 -0
- package/src/smart.export.d.ts +5 -0
- package/src/smart.export.js +7 -0
- package/src/staged-editing.ts +0 -0
- package/src/upgrade-prompt.test.ts +71 -0
- package/src/upgrade-prompt.ts +148 -0
- package/src/watermark.test.ts +97 -0
- package/src/watermark.ts +156 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { importData } from './import'
|
|
3
|
+
import { clearLicenseKey, setLicenseKey } from './license'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Test scaffolding
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Same minimal SvGridApi stub the AI tests use. The importer reads
|
|
11
|
+
* nothing from the api except `addRows` (when commit:true), so this is
|
|
12
|
+
* sufficient.
|
|
13
|
+
*/
|
|
14
|
+
function fakeApi() {
|
|
15
|
+
const calls: Array<{ method: string; args: unknown[] }> = []
|
|
16
|
+
const log = (method: string) => (...args: unknown[]) => {
|
|
17
|
+
calls.push({ method, args })
|
|
18
|
+
}
|
|
19
|
+
const api = {
|
|
20
|
+
getData: () => [],
|
|
21
|
+
getDisplayedRows: () => [],
|
|
22
|
+
setFilter: log('setFilter'),
|
|
23
|
+
clearAllFilters: log('clearAllFilters'),
|
|
24
|
+
setSort: log('setSort'),
|
|
25
|
+
clearSort: log('clearSort'),
|
|
26
|
+
setColumnVisible: log('setColumnVisible'),
|
|
27
|
+
isColumnVisible: () => true,
|
|
28
|
+
getCellValue: () => undefined,
|
|
29
|
+
setCellValue: log('setCellValue'),
|
|
30
|
+
addRow: log('addRow'),
|
|
31
|
+
addRows: log('addRows'),
|
|
32
|
+
removeRow: log('removeRow'),
|
|
33
|
+
removeRows: log('removeRows'),
|
|
34
|
+
addColumn: log('addColumn'),
|
|
35
|
+
addColumns: log('addColumns'),
|
|
36
|
+
removeColumn: log('removeColumn'),
|
|
37
|
+
setGroupBy: log('setGroupBy'),
|
|
38
|
+
clearFilter: log('clearFilter'),
|
|
39
|
+
getFilters: () => ({}),
|
|
40
|
+
clearRowSelection: log('clearRowSelection'),
|
|
41
|
+
}
|
|
42
|
+
return { api: api as any, calls }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
setLicenseKey('SVPRO-DEV-TEST')
|
|
47
|
+
})
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
clearLicenseKey()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// CSV
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe('importData CSV', () => {
|
|
57
|
+
it('parses a basic CSV with headers and types', async () => {
|
|
58
|
+
const csv = [
|
|
59
|
+
'id,name,price,active',
|
|
60
|
+
'1,Widget,19.95,true',
|
|
61
|
+
'2,Gadget,49.00,false',
|
|
62
|
+
].join('\n')
|
|
63
|
+
const { api } = fakeApi()
|
|
64
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
65
|
+
expect(r.format).toBe('csv')
|
|
66
|
+
expect(r.headers).toEqual(['id', 'name', 'price', 'active'])
|
|
67
|
+
expect(r.rows).toHaveLength(2)
|
|
68
|
+
expect(r.rows[0]).toEqual({ id: 1, name: 'Widget', price: 19.95, active: true })
|
|
69
|
+
expect(r.rows[1]).toEqual({ id: 2, name: 'Gadget', price: 49, active: false })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('handles quoted fields with embedded commas and newlines', async () => {
|
|
73
|
+
const csv =
|
|
74
|
+
'id,note\n' +
|
|
75
|
+
'1,"hello, world"\n' +
|
|
76
|
+
'2,"line one\nline two"\n'
|
|
77
|
+
const { api } = fakeApi()
|
|
78
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
79
|
+
expect(r.rows).toHaveLength(2)
|
|
80
|
+
expect(r.rows[0]).toEqual({ id: 1, note: 'hello, world' })
|
|
81
|
+
expect(r.rows[1]).toEqual({ id: 2, note: 'line one\nline two' })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('handles escaped quotes inside quoted fields', async () => {
|
|
85
|
+
const csv = 'id,quote\n1,"she said ""hi"" today"\n'
|
|
86
|
+
const { api } = fakeApi()
|
|
87
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
88
|
+
expect(r.rows[0]).toEqual({ id: 1, quote: 'she said "hi" today' })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('strips a UTF-8 BOM at the start of the file', async () => {
|
|
92
|
+
const csv = 'id,name\n1,Widget'
|
|
93
|
+
const { api } = fakeApi()
|
|
94
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
95
|
+
expect(r.headers).toEqual(['id', 'name']) // BOM stripped from "id"
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('coerces currency, comma-grouped numbers, and ISO dates', async () => {
|
|
99
|
+
const csv = [
|
|
100
|
+
'id,amount,big_value,created',
|
|
101
|
+
'1,$1234.56,"1,234,567",2024-03-15',
|
|
102
|
+
'2,99.99,500,2024-03-16T12:30:00Z',
|
|
103
|
+
].join('\n')
|
|
104
|
+
const { api } = fakeApi()
|
|
105
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
106
|
+
expect(r.rows[0]).toEqual({
|
|
107
|
+
id: 1, amount: 1234.56, big_value: 1234567, created: '2024-03-15',
|
|
108
|
+
})
|
|
109
|
+
expect(r.rows[1]).toEqual({
|
|
110
|
+
id: 2, amount: 99.99, big_value: 500, created: '2024-03-16T12:30:00Z',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('skips entirely blank rows and counts them', async () => {
|
|
115
|
+
const csv = 'id,name\n1,A\n\n2,B\n,,\n'
|
|
116
|
+
const { api } = fakeApi()
|
|
117
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
118
|
+
expect(r.rows).toHaveLength(2)
|
|
119
|
+
expect(r.skipped).toBeGreaterThanOrEqual(1)
|
|
120
|
+
expect(r.total).toBeGreaterThanOrEqual(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('handles \\r\\n line endings (Windows / Excel CSVs)', async () => {
|
|
124
|
+
const csv = 'id,name\r\n1,Widget\r\n2,Gadget\r\n'
|
|
125
|
+
const { api } = fakeApi()
|
|
126
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
127
|
+
expect(r.rows).toEqual([
|
|
128
|
+
{ id: 1, name: 'Widget' },
|
|
129
|
+
{ id: 2, name: 'Gadget' },
|
|
130
|
+
])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// TSV
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe('importData TSV', () => {
|
|
139
|
+
it('parses tab-separated values', async () => {
|
|
140
|
+
const tsv = 'id\tname\tprice\n1\tWidget\t19.95\n2\tGadget\t49\n'
|
|
141
|
+
const { api } = fakeApi()
|
|
142
|
+
const r = await importData(api, { file: tsv, format: 'tsv' })
|
|
143
|
+
expect(r.headers).toEqual(['id', 'name', 'price'])
|
|
144
|
+
expect(r.rows).toEqual([
|
|
145
|
+
{ id: 1, name: 'Widget', price: 19.95 },
|
|
146
|
+
{ id: 2, name: 'Gadget', price: 49 },
|
|
147
|
+
])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('auto-detects TSV when format = "auto" and a string has tabs', async () => {
|
|
151
|
+
const tsv = 'a\tb\n1\t2\n'
|
|
152
|
+
const { api } = fakeApi()
|
|
153
|
+
const r = await importData(api, { file: tsv, format: 'auto' })
|
|
154
|
+
expect(r.format).toBe('tsv')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// JSON
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
describe('importData JSON', () => {
|
|
163
|
+
it('parses a JSON array of records', async () => {
|
|
164
|
+
const json = JSON.stringify([
|
|
165
|
+
{ id: 1, name: 'Widget', price: 19.95 },
|
|
166
|
+
{ id: 2, name: 'Gadget', price: 49 },
|
|
167
|
+
])
|
|
168
|
+
const { api } = fakeApi()
|
|
169
|
+
const r = await importData(api, { file: json, format: 'json' })
|
|
170
|
+
expect(r.format).toBe('json')
|
|
171
|
+
expect(r.headers).toEqual(['id', 'name', 'price'])
|
|
172
|
+
// Numbers round-trip through the matrix; JSON values get re-coerced from
|
|
173
|
+
// string form.
|
|
174
|
+
expect(r.rows[0]).toEqual({ id: 1, name: 'Widget', price: 19.95 })
|
|
175
|
+
expect(r.rows[1]).toEqual({ id: 2, name: 'Gadget', price: 49 })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('collects the union of keys when records have differing shapes', async () => {
|
|
179
|
+
const json = JSON.stringify([
|
|
180
|
+
{ id: 1, name: 'A' },
|
|
181
|
+
{ id: 2, name: 'B', extra: 'late-bound' },
|
|
182
|
+
])
|
|
183
|
+
const { api } = fakeApi()
|
|
184
|
+
const r = await importData(api, { file: json, format: 'json' })
|
|
185
|
+
expect(r.headers).toEqual(['id', 'name', 'extra'])
|
|
186
|
+
expect(r.rows[0]).toEqual({ id: 1, name: 'A', extra: '' })
|
|
187
|
+
expect(r.rows[1]).toEqual({ id: 2, name: 'B', extra: 'late-bound' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('throws a clear message on non-array JSON', async () => {
|
|
191
|
+
const { api } = fakeApi()
|
|
192
|
+
await expect(
|
|
193
|
+
importData(api, { file: '{"id":1}', format: 'json' }),
|
|
194
|
+
).rejects.toThrow(/expects a top-level array/i)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('auto-detects JSON via leading "[" or "{"', async () => {
|
|
198
|
+
const { api } = fakeApi()
|
|
199
|
+
const r = await importData(api, { file: '[{"id":1}]', format: 'auto' })
|
|
200
|
+
expect(r.format).toBe('json')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Column mapping
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
describe('column mapping', () => {
|
|
209
|
+
it('renames source headers to target fields via columnMap', async () => {
|
|
210
|
+
const csv = 'Order ID,Customer Name,Total\n1,Acme Corp,99.00'
|
|
211
|
+
const { api } = fakeApi()
|
|
212
|
+
const r = await importData(api, {
|
|
213
|
+
file: csv,
|
|
214
|
+
format: 'csv',
|
|
215
|
+
columnMap: {
|
|
216
|
+
'Order ID': 'orderId',
|
|
217
|
+
'Customer Name': 'customer',
|
|
218
|
+
Total: 'totalUsd',
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
expect(r.rows[0]).toEqual({ orderId: 1, customer: 'Acme Corp', totalUsd: 99 })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('default mapping lowercases + snakes the source headers', async () => {
|
|
225
|
+
const csv = 'Order ID,Customer Name\n1,Acme'
|
|
226
|
+
const { api } = fakeApi()
|
|
227
|
+
const r = await importData(api, { file: csv, format: 'csv' })
|
|
228
|
+
expect(Object.keys(r.rows[0] as object)).toEqual(['order_id', 'customer_name'])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('drops a source column when its map entry is null', async () => {
|
|
232
|
+
const csv = 'id,secret,name\n1,xxxxx,Widget'
|
|
233
|
+
const { api } = fakeApi()
|
|
234
|
+
const r = await importData(api, {
|
|
235
|
+
file: csv,
|
|
236
|
+
format: 'csv',
|
|
237
|
+
columnMap: { id: 'id', secret: null as unknown as string, name: 'name' },
|
|
238
|
+
})
|
|
239
|
+
expect(r.rows[0]).toEqual({ id: 1, name: 'Widget' })
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Validator + commit
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
describe('validation + commit', () => {
|
|
248
|
+
it('returns validator errors with row indices', async () => {
|
|
249
|
+
const csv = 'id,price\n1,-5\n2,10\n3,-1'
|
|
250
|
+
const { api } = fakeApi()
|
|
251
|
+
const r = await importData(api, {
|
|
252
|
+
file: csv, format: 'csv',
|
|
253
|
+
validator: (row, i) => {
|
|
254
|
+
const errs = []
|
|
255
|
+
const price = (row as any).price as number
|
|
256
|
+
if (price < 0) errs.push({ field: 'price', message: 'must be >= 0' })
|
|
257
|
+
return errs
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
expect(r.errors).toEqual([
|
|
261
|
+
{ rowIndex: 0, field: 'price', message: 'must be >= 0' },
|
|
262
|
+
{ rowIndex: 2, field: 'price', message: 'must be >= 0' },
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('commit:true calls api.addRows when there are no validator errors', async () => {
|
|
267
|
+
const csv = 'id,name\n1,A\n2,B'
|
|
268
|
+
const { api, calls } = fakeApi()
|
|
269
|
+
await importData(api, { file: csv, format: 'csv', commit: true })
|
|
270
|
+
const add = calls.find((c) => c.method === 'addRows')
|
|
271
|
+
expect(add).toBeDefined()
|
|
272
|
+
expect((add!.args[0] as unknown[]).length).toBe(2)
|
|
273
|
+
expect(add!.args[1]).toBe('bottom')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('commit:true does NOT call addRows when there are validation errors', async () => {
|
|
277
|
+
const csv = 'id,price\n1,-5'
|
|
278
|
+
const { api, calls } = fakeApi()
|
|
279
|
+
await importData(api, {
|
|
280
|
+
file: csv, format: 'csv', commit: true,
|
|
281
|
+
validator: (row) => (row as any).price < 0 ? [{ field: 'price', message: 'bad' }] : [],
|
|
282
|
+
})
|
|
283
|
+
expect(calls.find((c) => c.method === 'addRows')).toBeUndefined()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('commitAt is forwarded to addRows', async () => {
|
|
287
|
+
const csv = 'id,name\n1,A'
|
|
288
|
+
const { api, calls } = fakeApi()
|
|
289
|
+
await importData(api, { file: csv, format: 'csv', commit: true, commitAt: 'top' })
|
|
290
|
+
const add = calls.find((c) => c.method === 'addRows')!
|
|
291
|
+
expect(add.args[1]).toBe('top')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Format sniffing edge cases
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// columnTypes (strict per-type coercion)
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
describe('columnTypes', () => {
|
|
304
|
+
it('coerces declared types and emits errors on bad values', async () => {
|
|
305
|
+
const csv = [
|
|
306
|
+
'id,amount,placed,active',
|
|
307
|
+
'1,1234.56,2024-03-15,true',
|
|
308
|
+
'not-a-number,$50,bad-date,maybe',
|
|
309
|
+
].join('\n')
|
|
310
|
+
const { api } = fakeApi()
|
|
311
|
+
const r = await importData(api, {
|
|
312
|
+
file: csv, format: 'csv',
|
|
313
|
+
columnTypes: { id: 'integer', amount: 'number', placed: 'date', active: 'boolean' },
|
|
314
|
+
})
|
|
315
|
+
expect(r.rows[0]).toEqual({ id: 1, amount: 1234.56, placed: '2024-03-15', active: true })
|
|
316
|
+
// Row 1 has 3 bad values -> 3 errors
|
|
317
|
+
const row1 = r.errors.filter((e) => e.rowIndex === 1).map((e) => e.field).sort()
|
|
318
|
+
expect(row1).toEqual(['active', 'id', 'placed'])
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('strict mode strips $ and commas for number fields', async () => {
|
|
322
|
+
// Commas in numeric values must be quoted in CSV (per RFC 4180);
|
|
323
|
+
// the parser respects that and the type coercion strips them.
|
|
324
|
+
const csv = 'price\n"$1,234.56"\n$200\n'
|
|
325
|
+
const { api } = fakeApi()
|
|
326
|
+
const r = await importData(api, {
|
|
327
|
+
file: csv, format: 'csv', columnTypes: { price: 'number' },
|
|
328
|
+
})
|
|
329
|
+
expect(r.rows[0]).toEqual({ price: 1234.56 })
|
|
330
|
+
expect(r.rows[1]).toEqual({ price: 200 })
|
|
331
|
+
expect(r.errors).toHaveLength(0)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('integer type rejects floats', async () => {
|
|
335
|
+
const csv = 'qty\n5\n5.5\n'
|
|
336
|
+
const { api } = fakeApi()
|
|
337
|
+
const r = await importData(api, {
|
|
338
|
+
file: csv, format: 'csv', columnTypes: { qty: 'integer' },
|
|
339
|
+
})
|
|
340
|
+
expect(r.rows[0]).toEqual({ qty: 5 })
|
|
341
|
+
expect(r.errors.filter((e) => e.rowIndex === 1 && e.field === 'qty')).toHaveLength(1)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('boolean type accepts true/false/yes/no/1/0', async () => {
|
|
345
|
+
const csv = 'active\ntrue\nFALSE\nyes\nNo\n1\n0\n'
|
|
346
|
+
const { api } = fakeApi()
|
|
347
|
+
const r = await importData(api, {
|
|
348
|
+
file: csv, format: 'csv', columnTypes: { active: 'boolean' },
|
|
349
|
+
})
|
|
350
|
+
expect(r.rows.map((row: any) => row.active)).toEqual([true, false, true, false, true, false])
|
|
351
|
+
expect(r.errors).toHaveLength(0)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('date type normalises common shapes to ISO yyyy-mm-dd', async () => {
|
|
355
|
+
const csv = 'placed\n2024-03-15\n03/15/2024\n'
|
|
356
|
+
const { api } = fakeApi()
|
|
357
|
+
const r = await importData(api, {
|
|
358
|
+
file: csv, format: 'csv', columnTypes: { placed: 'date' },
|
|
359
|
+
})
|
|
360
|
+
expect(r.rows[0]).toEqual({ placed: '2024-03-15' })
|
|
361
|
+
expect(r.rows[1]).toEqual({ placed: '2024-03-15' })
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('empty cells become null for non-string types (validator can require)', async () => {
|
|
365
|
+
const csv = 'id,name\n1,Ada\n,'
|
|
366
|
+
const { api } = fakeApi()
|
|
367
|
+
const r = await importData(api, {
|
|
368
|
+
file: csv, format: 'csv',
|
|
369
|
+
columnTypes: { id: 'integer', name: 'string' },
|
|
370
|
+
})
|
|
371
|
+
// Row 1 is all-blank → skipped (not row 0).
|
|
372
|
+
expect(r.rows).toHaveLength(1)
|
|
373
|
+
// First row has both values.
|
|
374
|
+
expect(r.rows[0]).toEqual({ id: 1, name: 'Ada' })
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('falls back to inferAndCoerce for fields not in columnTypes', async () => {
|
|
378
|
+
const csv = 'id,tag\n1,foo\n'
|
|
379
|
+
const { api } = fakeApi()
|
|
380
|
+
const r = await importData(api, {
|
|
381
|
+
file: csv, format: 'csv', columnTypes: { id: 'integer' },
|
|
382
|
+
})
|
|
383
|
+
// 'tag' has no declared type → inferAndCoerce keeps it as string.
|
|
384
|
+
expect(r.rows[0]).toEqual({ id: 1, tag: 'foo' })
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('json type parses the cell as JSON', async () => {
|
|
388
|
+
const csv = 'config\n"{""theme"":""dark""}"\n"[1,2,3]"\n'
|
|
389
|
+
const { api } = fakeApi()
|
|
390
|
+
const r = await importData(api, {
|
|
391
|
+
file: csv, format: 'csv', columnTypes: { config: 'json' },
|
|
392
|
+
})
|
|
393
|
+
expect(r.rows[0]).toEqual({ config: { theme: 'dark' } })
|
|
394
|
+
expect(r.rows[1]).toEqual({ config: [1, 2, 3] })
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
describe('format sniffing', () => {
|
|
399
|
+
it('refuses xlsx mode when given a string', async () => {
|
|
400
|
+
const { api } = fakeApi()
|
|
401
|
+
await expect(
|
|
402
|
+
importData(api, { file: 'id,name\n1,A', format: 'xlsx' }),
|
|
403
|
+
).rejects.toThrow(/expects a File or Blob/i)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('uses file extension when format = "auto" with a File', async () => {
|
|
407
|
+
// jsdom's File constructor is enough for the auto-sniff path to fire.
|
|
408
|
+
const blob = new Blob(['id,name\n1,A'], { type: 'text/csv' })
|
|
409
|
+
const file = new File([blob], 'orders.csv', { type: 'text/csv' })
|
|
410
|
+
const { api } = fakeApi()
|
|
411
|
+
const r = await importData(api, { file, format: 'auto' })
|
|
412
|
+
expect(r.format).toBe('csv')
|
|
413
|
+
expect(r.rows).toHaveLength(1)
|
|
414
|
+
})
|
|
415
|
+
})
|