@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,349 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Capture what would be downloaded instead of touching the DOM / Blob URLs.
|
|
4
|
+
const downloads: Array<{ text: string; filename: string; mime: string }> = []
|
|
5
|
+
vi.mock('./export-serialize', async (importOriginal) => {
|
|
6
|
+
const actual = (await importOriginal()) as Record<string, unknown>
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
downloadTextFile: (text: string, filename: string, mime: string) => {
|
|
10
|
+
downloads.push({ text, filename, mime })
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
import { exportGrid, copyExportToClipboard, buildGroupedPdfBody } from './export'
|
|
16
|
+
import { setLicenseKey } from './license'
|
|
17
|
+
|
|
18
|
+
type Row = { name: string; price: number; when: string; ratio: number }
|
|
19
|
+
|
|
20
|
+
const rows: Row[] = [
|
|
21
|
+
{ name: 'ACME', price: 19.95, when: '2026-03-04', ratio: 42 },
|
|
22
|
+
{ name: 'Globex, Inc', price: 1000, when: '2026-06-01', ratio: 8 },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
// Mimics the subset of SvGridApi the exporter reads.
|
|
26
|
+
function makeApi(opts: {
|
|
27
|
+
displayed?: Row[]
|
|
28
|
+
selected?: Row[]
|
|
29
|
+
all?: Row[]
|
|
30
|
+
grouping?: string[]
|
|
31
|
+
columns?: Array<Record<string, unknown>>
|
|
32
|
+
} = {}) {
|
|
33
|
+
const columns = opts.columns ?? [
|
|
34
|
+
{ id: 'name', field: 'name', header: 'Company', visible: true, align: 'left' },
|
|
35
|
+
{ id: 'price', field: 'price', header: 'Price', visible: true, format: { type: 'currency', currency: 'USD', locales: 'en-US' }, align: 'right' },
|
|
36
|
+
{ id: 'when', field: 'when', header: 'Date', visible: true, format: { type: 'date', pattern: 'y-m-d', locales: 'en-US' } },
|
|
37
|
+
{ id: 'ratio', field: 'ratio', header: 'Ratio', visible: true, format: { type: 'percent', valueIsPercentPoints: true, locales: 'en-US' }, align: 'right' },
|
|
38
|
+
]
|
|
39
|
+
return {
|
|
40
|
+
getDisplayedRows: () => opts.displayed ?? rows,
|
|
41
|
+
getSelectedRows: () => opts.selected ?? [],
|
|
42
|
+
getData: () => opts.all ?? rows,
|
|
43
|
+
getColumns: () => columns,
|
|
44
|
+
getState: () => ({ grouping: opts.grouping ?? [] }),
|
|
45
|
+
getColumnPinning: () => ({ left: [], right: [] }),
|
|
46
|
+
} as any
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
downloads.length = 0
|
|
51
|
+
setLicenseKey('SVENTERPRISE-DEV-TEST')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('exportGrid - faithful values (P0)', () => {
|
|
55
|
+
it('formats currency / date / percent like the grid does (csv)', async () => {
|
|
56
|
+
await exportGrid(makeApi(), { format: 'csv', filename: 'orders' })
|
|
57
|
+
expect(downloads).toHaveLength(1)
|
|
58
|
+
const { text, filename, mime } = downloads[0]!
|
|
59
|
+
expect(filename).toBe('orders.csv')
|
|
60
|
+
expect(mime).toContain('text/csv')
|
|
61
|
+
expect(text).toContain('Company,Price,Date,Ratio')
|
|
62
|
+
expect(text).toContain('$19.95')
|
|
63
|
+
expect(text).toContain('42%')
|
|
64
|
+
expect(text).toContain('2026') // formatted date, not raw passthrough only
|
|
65
|
+
// comma-bearing value is quoted (RFC-4180)
|
|
66
|
+
expect(text).toContain('"Globex, Inc"')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('rawValues:true writes underlying values, not formatted', async () => {
|
|
70
|
+
await exportGrid(makeApi(), { format: 'csv', rawValues: true })
|
|
71
|
+
const { text } = downloads[0]!
|
|
72
|
+
expect(text).toContain('19.95')
|
|
73
|
+
expect(text).not.toContain('$19.95')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('exportValue hook overrides the field value', async () => {
|
|
77
|
+
await exportGrid(makeApi(), {
|
|
78
|
+
format: 'csv',
|
|
79
|
+
columns: [{ field: 'name', header: 'Company', exportValue: (r: Row) => r.name.toUpperCase() }],
|
|
80
|
+
})
|
|
81
|
+
expect(downloads[0]!.text).toContain('ACME')
|
|
82
|
+
expect(downloads[0]!.text).toContain('"GLOBEX, INC"') // uppercased + comma-quoted
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('exportGrid - ExportResult / download:false', () => {
|
|
87
|
+
it('returns the built file and downloads by default', async () => {
|
|
88
|
+
const res = await exportGrid(makeApi(), { format: 'csv', filename: 'r' })
|
|
89
|
+
expect(downloads).toHaveLength(1)
|
|
90
|
+
expect(res).toBeDefined()
|
|
91
|
+
expect(res!.filename).toBe('r.csv')
|
|
92
|
+
expect(res!.mime).toContain('text/csv')
|
|
93
|
+
expect(res!.rowCount).toBe(2)
|
|
94
|
+
expect(res!.byteSize).toBeGreaterThan(0)
|
|
95
|
+
expect(res!.blob).toBeInstanceOf(Blob)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('download:false returns the result without downloading', async () => {
|
|
99
|
+
const res = await exportGrid(makeApi(), { format: 'csv', download: false })
|
|
100
|
+
expect(downloads).toHaveLength(0)
|
|
101
|
+
expect(res!.blob).toBeInstanceOf(Blob)
|
|
102
|
+
expect(await res!.blob.text()).toContain('$19.95')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('exportGrid - row scope (P1)', () => {
|
|
107
|
+
it("scope 'selected' exports only selected rows", async () => {
|
|
108
|
+
const api = makeApi({ selected: [rows[1]!] })
|
|
109
|
+
await exportGrid(api, { format: 'csv', rows: 'selected' })
|
|
110
|
+
const { text } = downloads[0]!
|
|
111
|
+
expect(text).toContain('Globex')
|
|
112
|
+
expect(text).not.toContain('ACME')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("scope 'all' uses the full dataset", async () => {
|
|
116
|
+
const api = makeApi({ displayed: [rows[0]!], all: rows })
|
|
117
|
+
await exportGrid(api, { format: 'csv', rows: 'all' })
|
|
118
|
+
expect(downloads[0]!.text).toContain('Globex')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('throws on an empty result set', async () => {
|
|
122
|
+
const api = makeApi({ displayed: [] })
|
|
123
|
+
await expect(exportGrid(api, { format: 'csv' })).rejects.toThrow(/nothing to export/)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('exportGrid - format tuning (P2/P3)', () => {
|
|
128
|
+
it('tsv uses a tab delimiter and .tsv extension', async () => {
|
|
129
|
+
await exportGrid(makeApi(), { format: 'tsv' })
|
|
130
|
+
const { text, filename } = downloads[0]!
|
|
131
|
+
expect(filename).toBe('grid.tsv')
|
|
132
|
+
expect(text).toContain('Company\tPrice\tDate\tRatio')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('html escapes and aligns', async () => {
|
|
136
|
+
await exportGrid(makeApi(), { format: 'html', filename: 'report' })
|
|
137
|
+
const { text, filename, mime } = downloads[0]!
|
|
138
|
+
expect(filename).toBe('report.html')
|
|
139
|
+
expect(mime).toContain('text/html')
|
|
140
|
+
expect(text).toContain('<th>Company</th>')
|
|
141
|
+
expect(text).toContain('text-align:right') // price/ratio columns
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('custom delimiter + no BOM', async () => {
|
|
145
|
+
await exportGrid(makeApi(), { format: 'csv', csv: { delimiter: ';', bom: false } })
|
|
146
|
+
const { text } = downloads[0]!
|
|
147
|
+
expect(text.charCodeAt(0)).not.toBe(0xfeff)
|
|
148
|
+
expect(text).toContain('Company;Price;Date;Ratio')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reports progress', async () => {
|
|
152
|
+
const onProgress = vi.fn()
|
|
153
|
+
await exportGrid(makeApi(), { format: 'csv', onProgress })
|
|
154
|
+
expect(onProgress).toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('exportGrid - legacy xls (SpreadsheetML)', () => {
|
|
159
|
+
it('emits a valid Excel 2003 XML workbook with typed cells', async () => {
|
|
160
|
+
await exportGrid(makeApi(), { format: 'xls', filename: 'legacy' })
|
|
161
|
+
const { text, filename, mime } = downloads[0]!
|
|
162
|
+
expect(filename).toBe('legacy.xls')
|
|
163
|
+
expect(mime).toContain('application/vnd.ms-excel')
|
|
164
|
+
expect(text).toContain('<?mso-application progid="Excel.Sheet"?>')
|
|
165
|
+
expect(text).toContain('urn:schemas-microsoft-com:office:spreadsheet')
|
|
166
|
+
// numbers stay numeric (Number type), not stringified currency
|
|
167
|
+
expect(text).toContain('<Data ss:Type="Number">19.95</Data>')
|
|
168
|
+
// percent points become the 0..1 fraction Excel expects
|
|
169
|
+
expect(text).toContain('<Data ss:Type="Number">0.42</Data>')
|
|
170
|
+
// dates are DateTime-typed
|
|
171
|
+
expect(text).toContain('ss:Type="DateTime"')
|
|
172
|
+
// header row + escaping of the comma value (no CSV quoting in XML)
|
|
173
|
+
expect(text).toContain('Company')
|
|
174
|
+
expect(text).toContain('Globex, Inc')
|
|
175
|
+
// a number format style is registered for the currency column
|
|
176
|
+
expect(text).toContain('<NumberFormat')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('escapes XML special characters', async () => {
|
|
180
|
+
const api = makeApi({
|
|
181
|
+
displayed: [{ name: 'A & <B>', price: 1, when: '2026-01-01', ratio: 1 }],
|
|
182
|
+
})
|
|
183
|
+
await exportGrid(api, { format: 'xls' })
|
|
184
|
+
expect(downloads[0]!.text).toContain('A & <B>')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('rawValues keeps raw numbers and skips number-format styles', async () => {
|
|
188
|
+
await exportGrid(makeApi(), { format: 'xls', rawValues: true })
|
|
189
|
+
const { text } = downloads[0]!
|
|
190
|
+
expect(text).toContain('<Data ss:Type="Number">19.95</Data>')
|
|
191
|
+
expect(text).toContain('<Data ss:Type="Number">42</Data>') // ratio raw, not 0.42
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('carries conditional formatting as real per-cell fills (xls)', async () => {
|
|
195
|
+
await exportGrid(makeApi(), {
|
|
196
|
+
format: 'xls',
|
|
197
|
+
conditionalFormats: [
|
|
198
|
+
{ type: 'rule', columns: ['price'], when: ({ value }) => Number(value) >= 1000, background: '#fee2e2' },
|
|
199
|
+
],
|
|
200
|
+
})
|
|
201
|
+
const { text } = downloads[0]!
|
|
202
|
+
expect(text).toContain('<Interior ss:Color="#fee2e2" ss:Pattern="Solid"/>')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('freezes panes, sizes columns, and links cells (xls fidelity)', async () => {
|
|
206
|
+
await exportGrid(makeApi(), {
|
|
207
|
+
format: 'xls',
|
|
208
|
+
columns: [
|
|
209
|
+
{ field: 'name', header: 'Company', link: (r: Row) => `https://x.test/${r.name}` },
|
|
210
|
+
{ field: 'price', header: 'Price', format: { type: 'currency', currency: 'USD' } },
|
|
211
|
+
],
|
|
212
|
+
freezeColumns: 1,
|
|
213
|
+
})
|
|
214
|
+
const { text } = downloads[0]!
|
|
215
|
+
expect(text).toContain('<FreezePanes/>') // freezeHeader defaults on
|
|
216
|
+
expect(text).toContain('<SplitVertical>1</SplitVertical>') // freezeColumns: 1
|
|
217
|
+
expect(text).toContain('<Column ss:Width=') // auto-fit widths
|
|
218
|
+
expect(text).toContain('ss:HRef="https://x.test/ACME"') // hyperlink
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('exportGrid - JSON / XML / Markdown', () => {
|
|
223
|
+
it('json defaults to raw values and machine keys', async () => {
|
|
224
|
+
await exportGrid(makeApi(), { format: 'json', filename: 'data' })
|
|
225
|
+
const { text, filename, mime } = downloads[0]!
|
|
226
|
+
expect(filename).toBe('data.json')
|
|
227
|
+
expect(mime).toContain('application/json')
|
|
228
|
+
const parsed = JSON.parse(text)
|
|
229
|
+
expect(parsed).toHaveLength(2)
|
|
230
|
+
expect(parsed[0]).toMatchObject({ name: 'ACME', price: 19.95, ratio: 42 }) // raw
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('json honors rawValues:false (formatted strings)', async () => {
|
|
234
|
+
await exportGrid(makeApi(), { format: 'json', rawValues: false })
|
|
235
|
+
const parsed = JSON.parse(downloads[0]!.text)
|
|
236
|
+
expect(parsed[0].price).toBe('$19.95')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('markdown emits a GFM table with alignment markers', async () => {
|
|
240
|
+
await exportGrid(makeApi(), { format: 'md' })
|
|
241
|
+
const { text, filename } = downloads[0]!
|
|
242
|
+
expect(filename).toBe('grid.md')
|
|
243
|
+
expect(text).toContain('| Company | Price | Date | Ratio |')
|
|
244
|
+
expect(text).toContain('---:') // right-aligned price/ratio
|
|
245
|
+
expect(text).toContain('$19.95')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('xml escapes and uses field element names', async () => {
|
|
249
|
+
await exportGrid(makeApi(), { format: 'xml' })
|
|
250
|
+
const { text, filename, mime } = downloads[0]!
|
|
251
|
+
expect(filename).toBe('grid.xml')
|
|
252
|
+
expect(mime).toContain('application/xml')
|
|
253
|
+
expect(text).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
254
|
+
expect(text).toContain('<name>ACME</name>')
|
|
255
|
+
expect(text).toContain('<name>Globex, Inc</name>')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('html carries conditional formatting into cell styles', async () => {
|
|
259
|
+
await exportGrid(makeApi(), {
|
|
260
|
+
format: 'html',
|
|
261
|
+
conditionalFormats: [
|
|
262
|
+
{ type: 'rule', columns: ['price'], when: ({ value }) => Number(value) >= 1000, background: '#fee2e2' },
|
|
263
|
+
],
|
|
264
|
+
})
|
|
265
|
+
expect(downloads[0]!.text).toContain('background:#fee2e2')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('html renders hyperlinks from a column link hook', async () => {
|
|
269
|
+
await exportGrid(makeApi(), {
|
|
270
|
+
format: 'html',
|
|
271
|
+
columns: [{ field: 'name', header: 'Company', link: (r: Row) => `https://x.test/${r.name}` }],
|
|
272
|
+
})
|
|
273
|
+
expect(downloads[0]!.text).toContain('<a href="https://x.test/ACME">ACME</a>')
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('buildGroupedPdfBody', () => {
|
|
278
|
+
const cols = [
|
|
279
|
+
{ field: 'region', header: 'Region' },
|
|
280
|
+
{ field: 'company', header: 'Company' },
|
|
281
|
+
{ field: 'price', header: 'Price', format: { type: 'currency', currency: 'USD', locales: 'en-US' } as const },
|
|
282
|
+
]
|
|
283
|
+
const gRows = [
|
|
284
|
+
{ region: 'North', company: 'A', price: 10 },
|
|
285
|
+
{ region: 'North', company: 'B', price: 20 },
|
|
286
|
+
{ region: 'South', company: 'C', price: 5 },
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
it('emits group header + data rows + a subtotal per cluster', () => {
|
|
290
|
+
const body = buildGroupedPdfBody(cols, gRows, ['region'])
|
|
291
|
+
expect(body.map((b) => b.kind)).toEqual([
|
|
292
|
+
'group',
|
|
293
|
+
'data',
|
|
294
|
+
'data',
|
|
295
|
+
'subtotal',
|
|
296
|
+
'group',
|
|
297
|
+
'data',
|
|
298
|
+
'subtotal',
|
|
299
|
+
])
|
|
300
|
+
expect(body[0]).toMatchObject({ kind: 'group', label: 'Region: North (2)' })
|
|
301
|
+
const subtotal = body[3] as { kind: 'subtotal'; cells: string[] }
|
|
302
|
+
expect(subtotal.cells[0]).toBe('Subtotal (North)') // label in first non-numeric col
|
|
303
|
+
expect(subtotal.cells[2]).toBe('$30.00') // summed currency
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('nests multi-level grouping', () => {
|
|
307
|
+
const body = buildGroupedPdfBody(cols, gRows, ['region', 'company'])
|
|
308
|
+
const groups = body.filter((b) => b.kind === 'group') as Array<{ label: string; level?: number }>
|
|
309
|
+
expect(groups.some((g) => g.level === 0 && g.label.startsWith('Region:'))).toBe(true)
|
|
310
|
+
expect(groups.some((g) => g.level === 1 && g.label.startsWith('Company:'))).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('copyExportToClipboard', () => {
|
|
315
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
316
|
+
|
|
317
|
+
it('copies tsv by default via writeText', async () => {
|
|
318
|
+
const writeText = vi.fn(async (_text: string) => {})
|
|
319
|
+
vi.stubGlobal('navigator', { clipboard: { writeText } })
|
|
320
|
+
await copyExportToClipboard(makeApi(), {})
|
|
321
|
+
expect(writeText).toHaveBeenCalledTimes(1)
|
|
322
|
+
const text = writeText.mock.calls[0]![0]
|
|
323
|
+
expect(text).toContain('Company\tPrice')
|
|
324
|
+
expect(text).toContain('$19.95')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('copies html as rich text/html via ClipboardItem', async () => {
|
|
328
|
+
const write = vi.fn(async () => {})
|
|
329
|
+
const items: Array<Record<string, Blob>> = []
|
|
330
|
+
vi.stubGlobal(
|
|
331
|
+
'ClipboardItem',
|
|
332
|
+
class {
|
|
333
|
+
constructor(public data: Record<string, Blob>) {
|
|
334
|
+
items.push(data)
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
vi.stubGlobal('navigator', { clipboard: { write, writeText: vi.fn() } })
|
|
339
|
+
await copyExportToClipboard(makeApi(), { format: 'html' })
|
|
340
|
+
expect(write).toHaveBeenCalledTimes(1)
|
|
341
|
+
expect(items[0]).toHaveProperty('text/html')
|
|
342
|
+
expect(items[0]).toHaveProperty('text/plain')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('throws when the clipboard API is unavailable', async () => {
|
|
346
|
+
vi.stubGlobal('navigator', {})
|
|
347
|
+
await expect(copyExportToClipboard(makeApi(), {})).rejects.toThrow(/clipboard is unavailable/)
|
|
348
|
+
})
|
|
349
|
+
})
|