@svgrid/enterprise 1.2.0 → 2.0.0
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/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
- package/dist/designer/assets/index-Dp44bTid.js +939 -0
- package/dist/designer/assets/index-RJp6x8tw.css +1 -0
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +22554 -0
- package/package.json +10 -3
- package/src/SvAuthGate.svelte +115 -0
- package/src/SvBoard.dom.test.ts +67 -0
- package/src/SvBoard.svelte +192 -0
- package/src/SvExportMenu.svelte +553 -0
- package/src/SvFileInput.svelte +113 -0
- package/src/SvGridEditPanel.dom.test.ts +146 -0
- package/src/SvGridEditPanel.svelte +793 -0
- package/src/SvGridMasterDetail.svelte +95 -0
- package/src/SvImportDialog.svelte +1020 -0
- package/src/SvLookupInput.svelte +180 -0
- package/src/SvRecordDetail.dom.test.ts +137 -0
- package/src/SvRecordDetail.svelte +288 -0
- package/src/SvSchedule.dom.test.ts +57 -0
- package/src/SvSchedule.svelte +156 -0
- package/src/SvSchemaChart.svelte +233 -0
- package/src/SvSchemaDashboard.svelte +130 -0
- package/src/ai-export-pdf.test.ts +74 -0
- package/src/ai-export-xlsx.test.ts +87 -0
- package/src/ai-export.test.ts +110 -0
- package/src/ai.ts +1188 -782
- package/src/edit-panel.test.ts +213 -0
- package/src/edit-panel.ts +228 -0
- package/src/export-conditional.test.ts +60 -0
- package/src/export-conditional.ts +94 -0
- package/src/export-ooxml.test.ts +152 -0
- package/src/export-ooxml.ts +485 -0
- package/src/export-pdf.test.ts +138 -0
- package/src/export-pdf.ts +245 -0
- package/src/export-print.test.ts +66 -0
- package/src/export-print.ts +132 -0
- package/src/export-serialize.test.ts +56 -0
- package/src/export-serialize.ts +24 -0
- package/src/export-xls.ts +196 -0
- package/src/export-xlsx-roundtrip.test.ts +120 -0
- package/src/export-xlsx.test.ts +150 -0
- package/src/export.test.ts +349 -0
- package/src/export.ts +1452 -549
- package/src/import-automap.test.ts +131 -0
- package/src/import-hardening.test.ts +158 -0
- package/src/import.ts +1042 -648
- package/src/index.ts +282 -0
- package/src/install.ts +134 -114
- package/src/license-core.test.ts +23 -0
- package/src/license-core.ts +35 -0
- package/src/license.ts +103 -90
- package/src/master-detail.test.ts +61 -0
- package/src/master-detail.ts +42 -0
- package/src/print.ts +107 -127
- package/src/schema-designer.test.ts +121 -0
- package/src/schema-designer.ts +142 -0
- package/src/schema.test.ts +268 -0
- package/src/schema.ts +499 -0
- package/src/smart-shim.ts +107 -105
- package/src/sources/aggregate.test.ts +79 -0
- package/src/sources/aggregate.ts +102 -0
- package/src/sources/auth-supabase.test.ts +80 -0
- package/src/sources/auth-supabase.ts +87 -0
- package/src/sources/dashboard.kpi.test.ts +50 -0
- package/src/sources/dashboard.test.ts +61 -0
- package/src/sources/dashboard.ts +136 -0
- package/src/sources/field-inference.test.ts +60 -0
- package/src/sources/field-inference.ts +63 -0
- package/src/sources/filters.test.ts +58 -0
- package/src/sources/filters.ts +64 -0
- package/src/sources/index.ts +67 -0
- package/src/sources/introspect-supabase.test.ts +100 -0
- package/src/sources/introspect-supabase.ts +119 -0
- package/src/sources/realtime-supabase.test.ts +93 -0
- package/src/sources/realtime-supabase.ts +99 -0
- package/src/sources/relation-lookup.test.ts +99 -0
- package/src/sources/relation-lookup.ts +128 -0
- package/src/sources/rest-adapters.test.ts +95 -0
- package/src/sources/rest-adapters.ts +120 -0
- package/src/sources/rest.test.ts +104 -0
- package/src/sources/rest.ts +129 -0
- package/src/sources/schema-from-columns.test.ts +106 -0
- package/src/sources/schema-from-columns.ts +88 -0
- package/src/sources/supabase.test.ts +110 -0
- package/src/sources/supabase.ts +99 -0
- package/src/sources/with-entity-rules.test.ts +104 -0
- package/src/sources/with-entity-rules.ts +78 -0
- package/src/sources/with-relation-labels.test.ts +75 -0
- package/src/sources/with-relation-labels.ts +73 -0
- package/src/studio/bug-report.test.ts +101 -0
- package/src/studio/bug-report.ts +165 -0
- package/src/studio/cli.test.ts +187 -0
- package/src/studio/cli.ts +112 -0
- package/src/studio/csv.test.ts +90 -0
- package/src/studio/csv.ts +165 -0
- package/src/studio/db-connect-string.test.ts +94 -0
- package/src/studio/db-connect-string.ts +128 -0
- package/src/studio/emit-project.test.ts +923 -0
- package/src/studio/emit-project.ts +1273 -0
- package/src/studio/emit-schema.test.ts +94 -0
- package/src/studio/emit-schema.ts +920 -0
- package/src/studio/index.ts +195 -0
- package/src/studio/introspect-db.test.ts +186 -0
- package/src/studio/introspect-db.ts +312 -0
- package/src/studio/introspect-prisma.test.ts +99 -0
- package/src/studio/introspect-prisma.ts +175 -0
- package/src/studio/introspect.test.ts +172 -0
- package/src/studio/introspect.ts +310 -0
- package/src/studio/pipeline.test.ts +42 -0
- package/src/studio/project-robust.test.ts +157 -0
- package/src/studio/project.test.ts +473 -0
- package/src/studio/project.ts +916 -0
- package/src/studio/sample-data.test.ts +58 -0
- package/src/studio/sample-data.ts +200 -0
- package/src/studio/samples/ats.ts +202 -0
- package/src/studio/samples/clinic.ts +177 -0
- package/src/studio/samples/crm.ts +250 -0
- package/src/studio/samples/ecommerce.ts +187 -0
- package/src/studio/samples/events.ts +199 -0
- package/src/studio/samples/fleet.ts +184 -0
- package/src/studio/samples/gym.ts +213 -0
- package/src/studio/samples/hr.ts +193 -0
- package/src/studio/samples/index.ts +55 -0
- package/src/studio/samples/insurance.ts +195 -0
- package/src/studio/samples/inventory.ts +182 -0
- package/src/studio/samples/invoicing.ts +162 -0
- package/src/studio/samples/library.ts +180 -0
- package/src/studio/samples/live-data.test.ts +98 -0
- package/src/studio/samples/live-data.ts +308 -0
- package/src/studio/samples/projects.ts +190 -0
- package/src/studio/samples/realestate.ts +196 -0
- package/src/studio/samples/restaurant.ts +187 -0
- package/src/studio/samples/samples.test.ts +208 -0
- package/src/studio/samples/school.ts +197 -0
- package/src/studio/samples/seed-floor.test.ts +25 -0
- package/src/studio/samples/shared.ts +305 -0
- package/src/studio/samples/subscriptions.ts +183 -0
- package/src/studio/samples/support.ts +182 -0
- package/src/studio/scaffold-app.test.ts +91 -0
- package/src/studio/scaffold-app.ts +161 -0
- package/src/studio/scaffold.test.ts +178 -0
- package/src/studio/scaffold.ts +374 -0
- package/src/studio/themes.ts +172 -0
- package/src/studio/verify.test.ts +47 -0
- package/src/studio/verify.ts +79 -0
- package/src/sveltekit/in-memory.test.ts +104 -0
- package/src/sveltekit/in-memory.ts +129 -0
- package/src/sveltekit/index.ts +23 -0
- package/src/sveltekit/query-plan.test.ts +109 -0
- package/src/sveltekit/query-plan.ts +125 -0
- package/src/sveltekit/sql-source.test.ts +185 -0
- package/src/sveltekit/sql-source.ts +166 -0
- package/src/sveltekit/sql.test.ts +80 -0
- package/src/sveltekit/sql.ts +131 -0
- package/src/sveltekit/transport.test.ts +175 -0
- package/src/sveltekit/transport.ts +254 -0
- package/src/sveltekit/types.ts +11 -0
- package/src/upgrade-prompt.ts +149 -148
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
importData,
|
|
4
|
+
readImportMatrix,
|
|
5
|
+
mapImportMatrix,
|
|
6
|
+
autoMapColumns,
|
|
7
|
+
inferImportColumnTypes,
|
|
8
|
+
type ImportGridColumn,
|
|
9
|
+
} from './import'
|
|
10
|
+
import { clearLicenseKey, setLicenseKey } from './license'
|
|
11
|
+
|
|
12
|
+
// The grid columns the auto-mapper matches source headers against. Headers
|
|
13
|
+
// are phrased DIFFERENTLY from the file on purpose (Unit Price / Sold On) so
|
|
14
|
+
// the fuzzy match + type inference are actually exercised.
|
|
15
|
+
const gridColumns: ImportGridColumn[] = [
|
|
16
|
+
{ field: 'company', header: 'Company' },
|
|
17
|
+
{ field: 'country', header: 'Country' },
|
|
18
|
+
{ field: 'sellDate', header: 'Sell date', format: { type: 'date' } },
|
|
19
|
+
{ field: 'quantity', header: 'Qty', format: { type: 'number' } },
|
|
20
|
+
{ field: 'price', header: 'Unit price', format: { type: 'currency' } },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function fakeApi() {
|
|
24
|
+
const calls: Array<{ method: string; args: unknown[] }> = []
|
|
25
|
+
const api = {
|
|
26
|
+
getColumns: () => gridColumns,
|
|
27
|
+
addRows: (...args: unknown[]) => calls.push({ method: 'addRows', args }),
|
|
28
|
+
}
|
|
29
|
+
return { api: api as any, calls }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))
|
|
33
|
+
afterEach(() => clearLicenseKey())
|
|
34
|
+
|
|
35
|
+
describe('autoMapColumns', () => {
|
|
36
|
+
it('matches headers to fields by label or field name, ignoring case + spacing', () => {
|
|
37
|
+
const map = autoMapColumns(
|
|
38
|
+
['Company', 'unit_price', 'SellDate', 'Unknown Col'],
|
|
39
|
+
gridColumns,
|
|
40
|
+
)
|
|
41
|
+
expect(map['Company']).toBe('company')
|
|
42
|
+
expect(map['unit_price']).toBe('price') // "unit_price" ~ "Unit price"
|
|
43
|
+
expect(map['SellDate']).toBe('sellDate') // field-name match
|
|
44
|
+
expect('Unknown Col' in map).toBe(false) // no confident match -> left out
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('inferImportColumnTypes', () => {
|
|
49
|
+
it('derives strict types from each column format', () => {
|
|
50
|
+
const types = inferImportColumnTypes(gridColumns)
|
|
51
|
+
expect(types).toEqual({
|
|
52
|
+
sellDate: 'date',
|
|
53
|
+
quantity: 'number',
|
|
54
|
+
price: 'number', // currency -> number
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('mapImportMatrix', () => {
|
|
60
|
+
it('applies a column map + types to a pre-parsed matrix (no I/O)', () => {
|
|
61
|
+
const matrix = [
|
|
62
|
+
['Company', 'Unit Price'],
|
|
63
|
+
['ACME', '$1,299.00'],
|
|
64
|
+
['', ''], // blank row -> skipped
|
|
65
|
+
]
|
|
66
|
+
const { rows, skipped, total, errors } = mapImportMatrix(matrix, {
|
|
67
|
+
columnMap: { 'Company': 'company', 'Unit Price': 'price' },
|
|
68
|
+
columnTypes: { price: 'number' },
|
|
69
|
+
})
|
|
70
|
+
expect(errors).toHaveLength(0)
|
|
71
|
+
expect(rows).toEqual([{ company: 'ACME', price: 1299 }])
|
|
72
|
+
expect(skipped).toBe(1)
|
|
73
|
+
expect(total).toBe(2)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('reports a coercion error but parks the raw value', () => {
|
|
77
|
+
const { rows, errors } = mapImportMatrix(
|
|
78
|
+
[['Price'], ['not-a-price']],
|
|
79
|
+
{ columnMap: { Price: 'price' }, columnTypes: { price: 'number' } },
|
|
80
|
+
)
|
|
81
|
+
expect(errors).toEqual([{ rowIndex: 0, field: 'price', message: expect.stringContaining('not a number') }])
|
|
82
|
+
expect((rows[0] as Record<string, unknown>).price).toBe('not-a-price')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('readImportMatrix', () => {
|
|
87
|
+
it('parses inline CSV into a header-first matrix', async () => {
|
|
88
|
+
const { format, matrix } = await readImportMatrix('a,b\n1,2', 'auto')
|
|
89
|
+
expect(format).toBe('csv')
|
|
90
|
+
expect(matrix).toEqual([['a', 'b'], ['1', '2']])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('sniffs JSON text and flattens objects to a header-first matrix', async () => {
|
|
94
|
+
const { format, matrix } = await readImportMatrix('[{"x":1},{"x":2}]')
|
|
95
|
+
expect(format).toBe('json')
|
|
96
|
+
expect(matrix).toEqual([['x'], ['1'], ['2']])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('importData autoMap', () => {
|
|
101
|
+
it('lines a differently-headed file up with the grid and coerces by format', async () => {
|
|
102
|
+
const { api } = fakeApi()
|
|
103
|
+
// Headers phrased differently in case/spacing from the grid labels
|
|
104
|
+
// ("Sell Date" ~ "Sell date", "unit price" ~ "Unit price") still line up.
|
|
105
|
+
const csv = [
|
|
106
|
+
'Company,Country,Sell Date,Qty,unit price',
|
|
107
|
+
'Initech,USA,2026-05-02,12,"$1,299.00"',
|
|
108
|
+
].join('\n')
|
|
109
|
+
const r = await importData(api, { file: csv, format: 'csv', autoMap: true })
|
|
110
|
+
expect(r.errors).toHaveLength(0)
|
|
111
|
+
expect(r.rows[0]).toEqual({
|
|
112
|
+
company: 'Initech',
|
|
113
|
+
country: 'USA',
|
|
114
|
+
sellDate: '2026-05-02',
|
|
115
|
+
quantity: 12,
|
|
116
|
+
price: 1299, // "$1,299.00" -> number via the currency column
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('lets an explicit columnMap override the guess', async () => {
|
|
121
|
+
const { api } = fakeApi()
|
|
122
|
+
const r = await importData(api, {
|
|
123
|
+
file: 'Company,Widget Cost\nACME,$5.00',
|
|
124
|
+
format: 'csv',
|
|
125
|
+
autoMap: true,
|
|
126
|
+
columnMap: { 'Widget Cost': 'price' },
|
|
127
|
+
columnTypes: { price: 'number' },
|
|
128
|
+
})
|
|
129
|
+
expect(r.rows[0]).toEqual({ company: 'ACME', price: 5 })
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { importData, mapImportMatrix, mapImportMatrixAsync, readImportMatrix } from './import'
|
|
3
|
+
import { clearLicenseKey, setLicenseKey } from './license'
|
|
4
|
+
|
|
5
|
+
function fakeApi() {
|
|
6
|
+
const calls: Array<{ method: string; args: unknown[] }> = []
|
|
7
|
+
const api = {
|
|
8
|
+
getColumns: () => [{ field: 'name', header: 'Name' }],
|
|
9
|
+
addRows: (...args: unknown[]) => calls.push({ method: 'addRows', args }),
|
|
10
|
+
}
|
|
11
|
+
return { api: api as any, calls }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))
|
|
15
|
+
afterEach(() => clearLicenseKey())
|
|
16
|
+
|
|
17
|
+
describe('security: prototype pollution', () => {
|
|
18
|
+
// NOTE: each test here must FAIL if the UNSAFE_FIELDS guard is removed.
|
|
19
|
+
// A string assigned to `__proto__` is a JS no-op, so a naive test would
|
|
20
|
+
// pass either way - we force real reparenting with an object value, or use
|
|
21
|
+
// `constructor` (a normal writable key that DOES become an own prop).
|
|
22
|
+
|
|
23
|
+
it('drops a "constructor" header instead of writing it as an own prop', async () => {
|
|
24
|
+
const { api } = fakeApi()
|
|
25
|
+
// Without the guard: rec.constructor = 'X' becomes a real own property.
|
|
26
|
+
const r = await importData(api, { file: 'constructor,name\nX,ok', format: 'csv' })
|
|
27
|
+
expect(Object.prototype.hasOwnProperty.call(r.rows[0], 'constructor')).toBe(false)
|
|
28
|
+
expect(r.rows[0]).toEqual({ name: 'ok' })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('never reparents a row through a __proto__ column, even with json coercion', async () => {
|
|
32
|
+
const { api } = fakeApi()
|
|
33
|
+
// With json coercion the cell becomes an OBJECT; without the guard,
|
|
34
|
+
// `rec['__proto__'] = { isAdmin: true }` would reparent the row so that
|
|
35
|
+
// `row.isAdmin` reads true off the polluted prototype.
|
|
36
|
+
const json = '[{"__proto__":{"isAdmin":true},"id":1}]'
|
|
37
|
+
const r = await importData(api, { file: json, format: 'json', columnTypes: { __proto__: 'json' } })
|
|
38
|
+
expect(Object.getPrototypeOf(r.rows[0])).toBe(Object.prototype)
|
|
39
|
+
expect((r.rows[0] as Record<string, unknown>).isAdmin).toBeUndefined()
|
|
40
|
+
expect(r.rows[0]).toEqual({ id: 1 })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('drops constructor / prototype JSON keys', async () => {
|
|
44
|
+
const { api } = fakeApi()
|
|
45
|
+
// Both are own enumerable keys on the parsed object, so without the guard
|
|
46
|
+
// they land in the row and break the toEqual.
|
|
47
|
+
const json = JSON.stringify([{ constructor: 'x', prototype: 'y', a: 1 }])
|
|
48
|
+
const r = await importData(api, { file: json, format: 'json' })
|
|
49
|
+
expect(r.rows[0]).toEqual({ a: 1 })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('refuses an unsafe target even from an explicit columnMap', () => {
|
|
53
|
+
// Exercises the columnMap branch of computeFields (not the default one).
|
|
54
|
+
const r = mapImportMatrix(
|
|
55
|
+
[['h', 'name'], ['{"x":1}', 'ok']],
|
|
56
|
+
{ columnMap: { h: '__proto__' }, columnTypes: { __proto__: 'json' } },
|
|
57
|
+
)
|
|
58
|
+
expect(Object.getPrototypeOf(r.rows[0])).toBe(Object.prototype)
|
|
59
|
+
expect(r.rows[0]).toEqual({ name: 'ok' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('skips a blank header rather than writing an empty-key field', () => {
|
|
63
|
+
// Without the guard, the empty header maps to field '' and rec[''] = 'orphan'.
|
|
64
|
+
const r = mapImportMatrix([['name', ''], ['A', 'orphan']])
|
|
65
|
+
expect(r.rows[0]).toEqual({ name: 'A' })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('guard-rails: size + rows + errors', () => {
|
|
70
|
+
it('rejects a File/Blob over maxBytes before reading', async () => {
|
|
71
|
+
const { api } = fakeApi()
|
|
72
|
+
const blob = new Blob(['a,b\n1,2\n'])
|
|
73
|
+
await expect(importData(api, { file: blob, format: 'csv', maxBytes: 3 })).rejects.toThrow(/import limit/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('rejects more rows than maxRows', async () => {
|
|
77
|
+
const { api } = fakeApi()
|
|
78
|
+
const csv = 'name\n' + Array.from({ length: 5 }, (_, i) => `r${i}`).join('\n')
|
|
79
|
+
await expect(importData(api, { file: csv, format: 'csv', maxRows: 3 })).rejects.toThrow(/maxRows/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("overLimit:'truncate' keeps the first maxRows and flags it", async () => {
|
|
83
|
+
const { api } = fakeApi()
|
|
84
|
+
const csv = 'name\n' + Array.from({ length: 5 }, (_, i) => `r${i}`).join('\n')
|
|
85
|
+
const r = await importData(api, { file: csv, format: 'csv', maxRows: 3, overLimit: 'truncate' })
|
|
86
|
+
expect(r.rows).toHaveLength(3)
|
|
87
|
+
expect(r.rows.map((x: any) => x.name)).toEqual(['r0', 'r1', 'r2'])
|
|
88
|
+
expect(r.truncated).toBe(true)
|
|
89
|
+
expect(r.total).toBe(5) // original count preserved
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('caps collected errors at maxErrors and flags truncation', async () => {
|
|
93
|
+
const { api } = fakeApi()
|
|
94
|
+
const csv = 'n\nx\ny\nz\nw'
|
|
95
|
+
const r = await importData(api, {
|
|
96
|
+
file: csv,
|
|
97
|
+
format: 'csv',
|
|
98
|
+
columnTypes: { n: 'number' },
|
|
99
|
+
maxErrors: 2,
|
|
100
|
+
})
|
|
101
|
+
expect(r.errors).toHaveLength(2)
|
|
102
|
+
expect(r.errorsTruncated).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('dedupeBy', () => {
|
|
107
|
+
it('keeps the last occurrence of each key, in place', () => {
|
|
108
|
+
const r = mapImportMatrix(
|
|
109
|
+
[
|
|
110
|
+
['id', 'v'],
|
|
111
|
+
['1', 'a'],
|
|
112
|
+
['2', 'b'],
|
|
113
|
+
['1', 'c'], // dup of id 1 -> overrides "a" at position 0
|
|
114
|
+
],
|
|
115
|
+
{ dedupeBy: 'id' },
|
|
116
|
+
)
|
|
117
|
+
expect(r.rows).toEqual([{ id: 1, v: 'c' }, { id: 2, v: 'b' }])
|
|
118
|
+
expect(r.deduped).toBe(1)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('cancellation', () => {
|
|
123
|
+
it('throws AbortError when the signal is already aborted', async () => {
|
|
124
|
+
const { api } = fakeApi()
|
|
125
|
+
const controller = new AbortController()
|
|
126
|
+
controller.abort()
|
|
127
|
+
await expect(
|
|
128
|
+
importData(api, { file: 'name\nA', format: 'csv', signal: controller.signal }),
|
|
129
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('mapImportMatrixAsync', () => {
|
|
134
|
+
it('matches the sync mapper and reports progress to completion', async () => {
|
|
135
|
+
const matrix = [['name'], ...Array.from({ length: 10 }, (_, i) => [`n${i}`])]
|
|
136
|
+
const ticks: number[] = []
|
|
137
|
+
const r = await mapImportMatrixAsync(matrix, {
|
|
138
|
+
chunkSize: 3,
|
|
139
|
+
onProgress: (p) => ticks.push(p.ratio),
|
|
140
|
+
})
|
|
141
|
+
expect(r.rows).toHaveLength(10)
|
|
142
|
+
expect(r.rows).toEqual(mapImportMatrix(matrix).rows)
|
|
143
|
+
expect(ticks.at(-1)).toBe(1) // finishes at 100%
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('encoding', () => {
|
|
148
|
+
it('decodes a non-UTF-8 (windows-1252) CSV blob', async () => {
|
|
149
|
+
// 0xE9 is "é" in windows-1252 but an invalid lone byte in UTF-8.
|
|
150
|
+
const bytes = new Uint8Array([
|
|
151
|
+
...'name\nCaf'.split('').map((c) => c.charCodeAt(0)),
|
|
152
|
+
0xe9, // é
|
|
153
|
+
])
|
|
154
|
+
const blob = new Blob([bytes])
|
|
155
|
+
const { matrix } = await readImportMatrix(blob, 'csv', { encoding: 'windows-1252' })
|
|
156
|
+
expect(matrix[1]).toEqual(['Café'])
|
|
157
|
+
})
|
|
158
|
+
})
|