@veltra/sheet 2.3.4 → 2.5.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.
Files changed (129) hide show
  1. package/dist/index.d.ts +2 -17
  2. package/dist/index.js +2 -13
  3. package/dist/types/index.d.ts +1 -3
  4. package/package.json +5 -6
  5. package/src/index.ts +1 -27
  6. package/src/types/index.ts +0 -2
  7. package/dist/components/report/binding-badge.js +0 -112
  8. package/dist/components/report/designer/cell-coords.js +0 -46
  9. package/dist/components/report/designer/col-widths.js +0 -29
  10. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  11. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  12. package/dist/components/report/designer/float-panel.js +0 -422
  13. package/dist/components/report/designer/role.js +0 -17
  14. package/dist/components/report/designer/rule-preview.js +0 -33
  15. package/dist/components/report/designer/rule-row.js +0 -253
  16. package/dist/components/report/designer/rules-dialog.js +0 -118
  17. package/dist/components/report/designer/topology-overlay.js +0 -122
  18. package/dist/components/report/designer/topology.js +0 -64
  19. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  20. package/dist/components/report/designer-hub.js +0 -177
  21. package/dist/components/report/field-panel-helpers.js +0 -33
  22. package/dist/components/report/field-panel.js +0 -132
  23. package/dist/components/report/filter-bar.js +0 -74
  24. package/dist/components/report/hub-connection-form.js +0 -145
  25. package/dist/components/report/hub-dataset-editor.js +0 -409
  26. package/dist/components/report/preview-grid-size.js +0 -32
  27. package/dist/components/report/report-designer.js +0 -393
  28. package/dist/components/report/report-viewer.js +0 -124
  29. package/dist/components/report/style.d.ts +0 -17
  30. package/dist/components/report/style.js +0 -18
  31. package/dist/components/report/style2.css +0 -874
  32. package/dist/components/report/use-report-designer.js +0 -386
  33. package/dist/components/report/use-report-viewer.js +0 -55
  34. package/dist/report/binding.d.ts +0 -83
  35. package/dist/report/binding.js +0 -334
  36. package/dist/report/connector.d.ts +0 -66
  37. package/dist/report/connector.js +0 -159
  38. package/dist/report/export-xlsx.d.ts +0 -13
  39. package/dist/report/export-xlsx.js +0 -13
  40. package/dist/report/filter-bar.d.ts +0 -10
  41. package/dist/report/filter-bar.js +0 -31
  42. package/dist/report/index.d.ts +0 -12
  43. package/dist/report/params.d.ts +0 -13
  44. package/dist/report/params.js +0 -61
  45. package/dist/report/render/aggregate.js +0 -169
  46. package/dist/report/render/builder.d.ts +0 -12
  47. package/dist/report/render/builder.js +0 -139
  48. package/dist/report/render/coordinate.d.ts +0 -25
  49. package/dist/report/render/coordinate.js +0 -733
  50. package/dist/report/render/index.d.ts +0 -14
  51. package/dist/report/render/index.js +0 -21
  52. package/dist/report/render/style-resolver.js +0 -50
  53. package/dist/report/render/template-index.d.ts +0 -24
  54. package/dist/report/render/template-index.js +0 -137
  55. package/dist/report/rules/evaluate.d.ts +0 -25
  56. package/dist/report/rules/evaluate.js +0 -82
  57. package/dist/report/rules/index.d.ts +0 -2
  58. package/dist/report/template.d.ts +0 -63
  59. package/dist/report/template.js +0 -83
  60. package/dist/report/types.d.ts +0 -85
  61. package/dist/types/report-designer.d.ts +0 -38
  62. package/dist/types/report-viewer.d.ts +0 -38
  63. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  64. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  65. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  66. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  67. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  68. package/src/components/report/__test__/report-designer.test.ts +0 -464
  69. package/src/components/report/__test__/report-viewer.test.ts +0 -357
  70. package/src/components/report/__test__/topology.test.ts +0 -143
  71. package/src/components/report/__test__/use-report-designer.test.ts +0 -697
  72. package/src/components/report/binding-badge.ts +0 -117
  73. package/src/components/report/designer/cell-coords.ts +0 -95
  74. package/src/components/report/designer/col-widths.ts +0 -56
  75. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  76. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  77. package/src/components/report/designer/float-panel.vue +0 -432
  78. package/src/components/report/designer/role.ts +0 -32
  79. package/src/components/report/designer/rule-preview.vue +0 -40
  80. package/src/components/report/designer/rule-row.vue +0 -252
  81. package/src/components/report/designer/rules-dialog.vue +0 -110
  82. package/src/components/report/designer/topology-overlay.vue +0 -128
  83. package/src/components/report/designer/topology.ts +0 -124
  84. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  85. package/src/components/report/designer-hub.vue +0 -198
  86. package/src/components/report/field-panel-helpers.ts +0 -44
  87. package/src/components/report/field-panel.vue +0 -136
  88. package/src/components/report/filter-bar.vue +0 -77
  89. package/src/components/report/hub-connection-form.vue +0 -124
  90. package/src/components/report/hub-dataset-editor.vue +0 -387
  91. package/src/components/report/index.ts +0 -2
  92. package/src/components/report/preview-grid-size.ts +0 -33
  93. package/src/components/report/report-designer.vue +0 -421
  94. package/src/components/report/report-viewer.vue +0 -136
  95. package/src/components/report/style-designer.scss +0 -587
  96. package/src/components/report/style-hub.scss +0 -292
  97. package/src/components/report/style-viewer.scss +0 -104
  98. package/src/components/report/style.scss +0 -3
  99. package/src/components/report/style.ts +0 -20
  100. package/src/components/report/use-report-designer.ts +0 -602
  101. package/src/components/report/use-report-viewer.ts +0 -88
  102. package/src/report/__test__/binding.test.ts +0 -320
  103. package/src/report/__test__/connector.test.ts +0 -239
  104. package/src/report/__test__/export-xlsx.test.ts +0 -96
  105. package/src/report/__test__/filter-bar.test.ts +0 -58
  106. package/src/report/__test__/params.test.ts +0 -156
  107. package/src/report/__test__/render.test.ts +0 -856
  108. package/src/report/__test__/rules.test.ts +0 -131
  109. package/src/report/__test__/template.test.ts +0 -366
  110. package/src/report/binding.ts +0 -474
  111. package/src/report/connector.ts +0 -206
  112. package/src/report/export-xlsx.ts +0 -15
  113. package/src/report/filter-bar.ts +0 -36
  114. package/src/report/index.ts +0 -13
  115. package/src/report/params.ts +0 -96
  116. package/src/report/render/__test__/aggregate.test.ts +0 -16
  117. package/src/report/render/__test__/coordinate.test.ts +0 -405
  118. package/src/report/render/aggregate.ts +0 -261
  119. package/src/report/render/builder.ts +0 -211
  120. package/src/report/render/coordinate.ts +0 -1239
  121. package/src/report/render/index.ts +0 -30
  122. package/src/report/render/style-resolver.ts +0 -68
  123. package/src/report/render/template-index.ts +0 -200
  124. package/src/report/rules/evaluate.ts +0 -113
  125. package/src/report/rules/index.ts +0 -7
  126. package/src/report/template.ts +0 -145
  127. package/src/report/types.ts +0 -96
  128. package/src/types/report-designer.ts +0 -38
  129. package/src/types/report-viewer.ts +0 -37
@@ -1,320 +0,0 @@
1
- import type { CellAddress } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import {
5
- applyReportPreset,
6
- createReportBinding,
7
- formatBindingPlaceholder,
8
- formatCellAddress,
9
- inferColParentCandidate,
10
- inferDropPreset,
11
- inferParentsForPreset,
12
- inferReportPreset,
13
- inferRowParentCandidate,
14
- isExpansionBandRow,
15
- parseCellAddress,
16
- presetBindingPatch,
17
- presetStillApplies,
18
- setBindingCatalog
19
- } from '../binding'
20
- import type { DatasetCatalogItem, ReportBinding } from '../types'
21
-
22
- /** 内联 catalog fixture(单一事实源在测试自身,不依赖 playground mock) */
23
- const ORDERS_DATASET: DatasetCatalogItem = {
24
- id: 'orders',
25
- label: '销售明细',
26
- fields: [
27
- { name: 'customer', label: '客户', type: 'string' },
28
- { name: 'orderNo', label: '订单号', type: 'string' },
29
- { name: 'region', label: '地区', type: 'string' },
30
- { name: 'amount', label: '金额', type: 'number' },
31
- { name: 'orderDate', label: '下单日期', type: 'date' }
32
- ]
33
- }
34
-
35
- setBindingCatalog([ORDERS_DATASET])
36
-
37
- describe('report binding', () => {
38
- it('createReportBinding 默认 list + 纵向扩展 + 明细预设', () => {
39
- const binding = createReportBinding(ORDERS_DATASET, 'amount')
40
- expect(binding).toEqual({
41
- dataset: 'orders',
42
- field: 'amount',
43
- aggregate: 'list',
44
- expand: 'down',
45
- preset: 'detail',
46
- sort: 'none',
47
- conditionalRules: []
48
- })
49
- })
50
-
51
- it('formatBindingPlaceholder 中文标签(含 list / max / min)', () => {
52
- expect(formatBindingPlaceholder(createReportBinding(ORDERS_DATASET, 'amount'))).toBe(
53
- '明细 · 金额'
54
- )
55
-
56
- const group = createReportBinding(ORDERS_DATASET, 'customer')
57
- group.aggregate = 'group'
58
- expect(formatBindingPlaceholder(group)).toBe('分组 · 客户')
59
-
60
- const sum = createReportBinding(ORDERS_DATASET, 'amount')
61
- sum.aggregate = 'sum'
62
- sum.expand = 'none'
63
- expect(formatBindingPlaceholder(sum)).toBe('求和 · 金额')
64
-
65
- const max = createReportBinding(ORDERS_DATASET, 'amount')
66
- max.aggregate = 'max'
67
- max.expand = 'none'
68
- expect(formatBindingPlaceholder(max)).toBe('最大 · 金额')
69
-
70
- const min = createReportBinding(ORDERS_DATASET, 'amount')
71
- min.aggregate = 'min'
72
- min.expand = 'none'
73
- expect(formatBindingPlaceholder(min)).toBe('最小 · 金额')
74
- })
75
-
76
- it('presetBindingPatch / applyReportPreset 映射表', () => {
77
- const rowParent = { row: 1, col: 0 }
78
- const colParent = { row: 0, col: 1 }
79
- const base: ReportBinding = {
80
- dataset: 'orders',
81
- field: 'amount',
82
- expand: 'down',
83
- aggregate: 'list',
84
- rowParent,
85
- colParent
86
- }
87
-
88
- expect(presetBindingPatch('groupHeader')).toEqual({
89
- preset: 'groupHeader',
90
- expand: 'down',
91
- aggregate: 'group'
92
- })
93
- expect(presetBindingPatch('groupHeader', { transpose: true })).toMatchObject({
94
- expand: 'right',
95
- aggregate: 'group'
96
- })
97
-
98
- expect(presetBindingPatch('detail')).toEqual({
99
- preset: 'detail',
100
- expand: 'down',
101
- aggregate: 'list'
102
- })
103
- expect(presetBindingPatch('detail', { transpose: true })).toMatchObject({
104
- expand: 'right',
105
- aggregate: 'list'
106
- })
107
-
108
- expect(presetBindingPatch('subtotal')).toEqual({
109
- preset: 'subtotal',
110
- expand: 'none',
111
- aggregate: 'sum'
112
- })
113
- expect(applyReportPreset(base, 'subtotal').rowParent).toEqual(rowParent)
114
-
115
- expect(presetBindingPatch('grandTotal')).toEqual({
116
- preset: 'grandTotal',
117
- expand: 'none',
118
- aggregate: 'sum',
119
- rowParent: undefined,
120
- colParent: undefined
121
- })
122
- expect(applyReportPreset(base, 'grandTotal').rowParent).toBeUndefined()
123
- expect(applyReportPreset(base, 'grandTotal').colParent).toBeUndefined()
124
-
125
- expect(presetBindingPatch('cross')).toEqual({
126
- preset: 'cross',
127
- expand: 'none',
128
- aggregate: 'sum'
129
- })
130
- expect(applyReportPreset(base, 'cross').rowParent).toEqual(rowParent)
131
- expect(applyReportPreset(base, 'cross').colParent).toEqual(colParent)
132
- })
133
-
134
- it('inferReportPreset 从字段组合推断预设', () => {
135
- expect(
136
- inferReportPreset({
137
- dataset: 'orders',
138
- field: 'customer',
139
- aggregate: 'group',
140
- expand: 'down'
141
- })
142
- ).toBe('groupHeader')
143
-
144
- expect(
145
- inferReportPreset({
146
- dataset: 'orders',
147
- field: 'orderNo',
148
- aggregate: 'list',
149
- expand: 'down',
150
- rowParent: { row: 1, col: 0 }
151
- })
152
- ).toBe('detail')
153
-
154
- expect(
155
- inferReportPreset({
156
- dataset: 'orders',
157
- field: 'amount',
158
- aggregate: 'sum',
159
- expand: 'none',
160
- rowParent: { row: 1, col: 0 }
161
- })
162
- ).toBe('subtotal')
163
-
164
- expect(
165
- inferReportPreset({ dataset: 'orders', field: 'amount', aggregate: 'sum', expand: 'none' })
166
- ).toBe('grandTotal')
167
-
168
- expect(
169
- inferReportPreset({
170
- dataset: 'orders',
171
- field: 'amount',
172
- aggregate: 'sum',
173
- expand: 'none',
174
- rowParent: { row: 1, col: 0 },
175
- colParent: { row: 0, col: 1 }
176
- })
177
- ).toBe('cross')
178
-
179
- expect(
180
- inferReportPreset({
181
- dataset: 'orders',
182
- field: 'amount',
183
- aggregate: 'avg',
184
- expand: 'none',
185
- rowParent: { row: 1, col: 0 }
186
- })
187
- ).toBe('subtotal')
188
-
189
- expect(
190
- inferReportPreset({
191
- dataset: 'orders',
192
- field: 'amount',
193
- aggregate: 'sum',
194
- expand: 'none',
195
- colParent: { row: 0, col: 1 }
196
- })
197
- ).toBe('subtotal')
198
- })
199
-
200
- it('presetStillApplies 同族内改聚合仍成立', () => {
201
- const subtotal: ReportBinding = {
202
- dataset: 'orders',
203
- field: 'amount',
204
- preset: 'subtotal',
205
- aggregate: 'avg',
206
- expand: 'none',
207
- rowParent: { row: 1, col: 0 }
208
- }
209
- expect(presetStillApplies(subtotal)).toBe(true)
210
-
211
- const broken = { ...subtotal, aggregate: 'list' as const, expand: 'down' as const }
212
- expect(presetStillApplies(broken)).toBe(false)
213
- })
214
-
215
- it('inferDropPreset / isExpansionBandRow 落格推断', () => {
216
- const groupAddr = { row: 1, col: 0 }
217
- const group = createReportBinding(ORDERS_DATASET, 'customer')
218
- group.aggregate = 'group'
219
- group.expand = 'down'
220
-
221
- const getBindingAt = (addr: CellAddress) => {
222
- if (addr.row === groupAddr.row && addr.col === groupAddr.col) return group
223
- return undefined
224
- }
225
-
226
- expect(isExpansionBandRow(1, getBindingAt)).toBe(true)
227
- expect(isExpansionBandRow(0, getBindingAt)).toBe(false)
228
- expect(inferDropPreset({ row: 2, col: 0 }, 'number', getBindingAt)).toBe('subtotal')
229
- expect(inferDropPreset({ row: 2, col: 0 }, 'string', getBindingAt)).toBe('detail')
230
- })
231
-
232
- it('inferDropPreset:数值字段落在横向展开带正右相邻列也推断为小计', () => {
233
- const colGroup = createReportBinding(ORDERS_DATASET, 'region')
234
- colGroup.aggregate = 'group'
235
- colGroup.expand = 'right'
236
-
237
- const getBindingAt = (addr: CellAddress) => {
238
- if (addr.row === 0 && addr.col === 1) return colGroup
239
- return undefined
240
- }
241
-
242
- expect(inferDropPreset({ row: 0, col: 2 }, 'number', getBindingAt)).toBe('subtotal')
243
- expect(inferDropPreset({ row: 0, col: 2 }, 'string', getBindingAt)).toBe('detail')
244
- })
245
-
246
- it('inferRowParentCandidate:同行向左优先分组头', () => {
247
- const group = createReportBinding(ORDERS_DATASET, 'customer')
248
- group.aggregate = 'group'
249
- group.expand = 'down'
250
- const detail = createReportBinding(ORDERS_DATASET, 'orderNo')
251
-
252
- const getBindingAt = (addr: CellAddress) => {
253
- if (addr.row === 1 && addr.col === 0) return group
254
- if (addr.row === 1 && addr.col === 1) return detail
255
- return undefined
256
- }
257
-
258
- expect(inferRowParentCandidate({ row: 1, col: 2 }, getBindingAt)).toEqual({ row: 1, col: 0 })
259
- })
260
-
261
- it('inferRowParentCandidate:下方跨列小计推到行分组头而不是同列 list', () => {
262
- const group = createReportBinding(ORDERS_DATASET, 'customer')
263
- group.aggregate = 'group'
264
- group.expand = 'down'
265
- const detail = createReportBinding(ORDERS_DATASET, 'amount')
266
-
267
- const getBindingAt = (addr: CellAddress) => {
268
- if (addr.row === 1 && addr.col === 0) return group
269
- if (addr.row === 1 && addr.col === 2) return detail
270
- return undefined
271
- }
272
-
273
- expect(
274
- inferRowParentCandidate({ row: 2, col: 2 }, getBindingAt, { preferGroup: true })
275
- ).toEqual({ row: 1, col: 0 })
276
- expect(inferParentsForPreset({ row: 2, col: 2 }, 'subtotal', getBindingAt)).toEqual({
277
- rowParent: { row: 1, col: 0 }
278
- })
279
- })
280
-
281
- it('inferColParentCandidate:右侧列小计同列向上推到列分组头', () => {
282
- const colGroup = createReportBinding(ORDERS_DATASET, 'region')
283
- colGroup.aggregate = 'group'
284
- colGroup.expand = 'right'
285
- const cross = createReportBinding(ORDERS_DATASET, 'amount')
286
- cross.preset = 'cross'
287
- cross.aggregate = 'sum'
288
- cross.expand = 'none'
289
- cross.colParent = { row: 0, col: 1 }
290
-
291
- const getBindingAt = (addr: CellAddress) => {
292
- if (addr.row === 0 && addr.col === 1) return colGroup
293
- if (addr.row === 1 && addr.col === 1) return cross
294
- return undefined
295
- }
296
-
297
- expect(
298
- inferColParentCandidate({ row: 2, col: 1 }, getBindingAt, { preferGroup: true })
299
- ).toEqual({ row: 0, col: 1 })
300
- expect(inferParentsForPreset({ row: 2, col: 1 }, 'subtotal', getBindingAt)).toEqual({
301
- colParent: { row: 0, col: 1 }
302
- })
303
- })
304
-
305
- it('formatCellAddress / parseCellAddress 支持多字母列互逆', () => {
306
- const single = { row: 2, col: 1 }
307
- expect(formatCellAddress(single)).toBe('B3')
308
- expect(parseCellAddress('B3')).toEqual(single)
309
-
310
- const wide = { row: 0, col: 26 }
311
- expect(formatCellAddress(wide)).toBe('AA1')
312
- expect(parseCellAddress('AA1')).toEqual(wide)
313
-
314
- const wider = { row: 99, col: 701 }
315
- expect(formatCellAddress(wider)).toBe('ZZ100')
316
- expect(parseCellAddress('ZZ100')).toEqual(wider)
317
-
318
- expect(parseCellAddress('bad')).toBeNull()
319
- })
320
- })
@@ -1,239 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { createHttpConnector, resolvePortOnTypeChange } from '../connector'
4
- import type { DataConnection } from '../connector'
5
- import type { ParamValues } from '../types'
6
-
7
- const CONNECTION: DataConnection = {
8
- id: 'demo',
9
- label: '演示业务库',
10
- type: 'mysql',
11
- host: '127.0.0.1',
12
- port: 3306,
13
- database: 'demo_business',
14
- username: 'demo',
15
- password: 'demo'
16
- }
17
-
18
- const SQL = 'SELECT customer, amount FROM orders WHERE region = ${region}'
19
- const VALUES: ParamValues = { region: '华东' }
20
-
21
- const ENDPOINT = 'https://api.example.com/report'
22
-
23
- interface FetchCall {
24
- url: string
25
- method: string
26
- headers: Headers
27
- body: unknown
28
- }
29
-
30
- function stubFetch(respond: (url: string, init: RequestInit) => Promise<unknown>): FetchCall[] {
31
- const calls: FetchCall[] = []
32
- const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
33
- const url = String(input)
34
- const method = init?.method ?? 'GET'
35
- const headers = new Headers(init?.headers)
36
- const body = typeof init?.body === 'string' ? (JSON.parse(init.body) as unknown) : undefined
37
- calls.push({ url, method, headers, body })
38
- const payload = await respond(url, init ?? {})
39
- return new Response(JSON.stringify(payload), {
40
- status: 200,
41
- headers: { 'Content-Type': 'application/json' }
42
- })
43
- })
44
- vi.stubGlobal('fetch', fetchMock)
45
- return calls
46
- }
47
-
48
- afterEach(() => {
49
- vi.unstubAllGlobals()
50
- })
51
-
52
- describe('createHttpConnector', () => {
53
- it('test:POST {endpoint}/test,请求体为 { connection },成功返回 ok 且无 data', async () => {
54
- const calls = stubFetch(async () => ({ ok: true }))
55
- const connector = createHttpConnector({ endpoint: ENDPOINT })
56
-
57
- const result = await connector.test(CONNECTION)
58
-
59
- expect(result).toEqual({ ok: true, data: undefined })
60
- expect(calls).toHaveLength(1)
61
- expect(calls[0]!.url).toBe(`${ENDPOINT}/test`)
62
- expect(calls[0]!.method).toBe('POST')
63
- expect(calls[0]!.headers.get('content-type')).toBe('application/json')
64
- expect(calls[0]!.body).toEqual({ connection: CONNECTION })
65
- })
66
-
67
- it('describe:POST {endpoint}/describe,请求体 { connection, sql },fields 归一化为 DatasetField', async () => {
68
- const calls = stubFetch(async () => ({
69
- ok: true,
70
- fields: [{ name: 'customer' }, { name: 'amount', type: 'number' }]
71
- }))
72
- const connector = createHttpConnector({ endpoint: ENDPOINT })
73
-
74
- const result = await connector.describe(CONNECTION, SQL)
75
-
76
- expect(result).toEqual({
77
- ok: true,
78
- data: [
79
- { name: 'customer', label: 'customer', type: 'string' },
80
- { name: 'amount', label: 'amount', type: 'number' }
81
- ]
82
- })
83
- expect(calls[0]!.url).toBe(`${ENDPOINT}/describe`)
84
- expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL })
85
- })
86
-
87
- it('describe:服务端返回 label 时保留中文显示名', async () => {
88
- stubFetch(async () => ({
89
- ok: true,
90
- fields: [
91
- { name: 'customer', label: '客户名称', type: 'string' },
92
- { name: 'amount', label: '金额', type: 'number' }
93
- ]
94
- }))
95
- const connector = createHttpConnector({ endpoint: ENDPOINT })
96
-
97
- const result = await connector.describe(CONNECTION, SQL)
98
-
99
- expect(result).toEqual({
100
- ok: true,
101
- data: [
102
- { name: 'customer', label: '客户名称', type: 'string' },
103
- { name: 'amount', label: '金额', type: 'number' }
104
- ]
105
- })
106
- })
107
-
108
- it('query:POST {endpoint}/query,请求体 { connection, sql, values },返回 fields + rows', async () => {
109
- const calls = stubFetch(async () => ({
110
- ok: true,
111
- fields: [{ name: 'customer', type: 'string' }],
112
- rows: [{ customer: '甲公司' }]
113
- }))
114
- const connector = createHttpConnector({ endpoint: ENDPOINT })
115
-
116
- const result = await connector.query(CONNECTION, SQL, VALUES)
117
-
118
- expect(result).toEqual({
119
- ok: true,
120
- data: {
121
- fields: [{ name: 'customer', label: 'customer', type: 'string' }],
122
- rows: [{ customer: '甲公司' }]
123
- }
124
- })
125
- expect(calls[0]!.url).toBe(`${ENDPOINT}/query`)
126
- expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL, values: VALUES })
127
- })
128
-
129
- it('业务错误分叉:200 + { ok: false, error } 原样透传(三端点一致)', async () => {
130
- stubFetch(async () => ({
131
- ok: false,
132
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
133
- }))
134
- const connector = createHttpConnector({ endpoint: ENDPOINT })
135
-
136
- const testResult = await connector.test(CONNECTION)
137
- expect(testResult).toEqual({
138
- ok: false,
139
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
140
- })
141
-
142
- const describeResult = await connector.describe(CONNECTION, SQL)
143
- expect(describeResult).toEqual({
144
- ok: false,
145
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
146
- })
147
-
148
- const queryResult = await connector.query(CONNECTION, SQL, VALUES)
149
- expect(queryResult).toEqual({
150
- ok: false,
151
- error: { code: 'CONNECTION_REFUSED', message: '无法连接到数据库' }
152
- })
153
- })
154
-
155
- it('传输层错误:非 2xx 折叠为 HTTP_<status> 错误结果', async () => {
156
- const fetchMock = vi.fn(async () => new Response('Internal Server Error', { status: 500 }))
157
- vi.stubGlobal('fetch', fetchMock)
158
- const connector = createHttpConnector({ endpoint: ENDPOINT })
159
-
160
- const result = await connector.test(CONNECTION)
161
-
162
- expect(result).toEqual({
163
- ok: false,
164
- error: { code: 'HTTP_500', message: '请求失败(HTTP 500)' }
165
- })
166
- })
167
-
168
- it('网络异常:fetch reject 折叠为 NETWORK_ERROR', async () => {
169
- vi.stubGlobal(
170
- 'fetch',
171
- vi.fn(async () => Promise.reject(new Error('fetch failed')))
172
- )
173
- const connector = createHttpConnector({ endpoint: ENDPOINT })
174
-
175
- const result = await connector.test(CONNECTION)
176
-
177
- expect(result).toEqual({ ok: false, error: { code: 'NETWORK_ERROR', message: 'fetch failed' } })
178
- })
179
-
180
- it('响应不可解析:非 JSON / 缺字段折叠为 BAD_RESPONSE', async () => {
181
- const notJson = vi.fn(async () => new Response('<html>oops</html>', { status: 200 }))
182
- vi.stubGlobal('fetch', notJson)
183
- const connector = createHttpConnector({ endpoint: ENDPOINT })
184
-
185
- const invalidJson = await connector.describe(CONNECTION, SQL)
186
- expect(invalidJson).toEqual({
187
- ok: false,
188
- error: { code: 'BAD_RESPONSE', message: '响应不是合法 JSON' }
189
- })
190
-
191
- vi.stubGlobal(
192
- 'fetch',
193
- vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 }))
194
- )
195
- const missingFields = await connector.describe(CONNECTION, SQL)
196
- expect(missingFields).toEqual({
197
- ok: false,
198
- error: { code: 'BAD_RESPONSE', message: 'describe 响应缺少合法的 fields 数组' }
199
- })
200
-
201
- vi.stubGlobal(
202
- 'fetch',
203
- vi.fn(async () => new Response(JSON.stringify({ ok: true, fields: [] }), { status: 200 }))
204
- )
205
- const missingRows = await connector.query(CONNECTION, SQL, VALUES)
206
- expect(missingRows).toEqual({
207
- ok: false,
208
- error: { code: 'BAD_RESPONSE', message: 'query 响应缺少 rows 数组' }
209
- })
210
- })
211
-
212
- it('ok:false 但 error 形状非法时回退 SERVER_ERROR', async () => {
213
- stubFetch(async () => ({ ok: false, error: 'oops' }))
214
- const connector = createHttpConnector({ endpoint: ENDPOINT })
215
-
216
- const result = await connector.test(CONNECTION)
217
-
218
- expect(result).toEqual({
219
- ok: false,
220
- error: { code: 'SERVER_ERROR', message: '服务端返回未知错误' }
221
- })
222
- })
223
- })
224
-
225
- describe('resolvePortOnTypeChange', () => {
226
- it('默认端口随类型切换:MySQL 3306 ↔ PostgreSQL 5432', () => {
227
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 3306)).toBe(5432)
228
- expect(resolvePortOnTypeChange('postgresql', 'mysql', 5432)).toBe(3306)
229
- })
230
-
231
- it('自定义端口不随类型切换而改变', () => {
232
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 3307)).toBe(3307)
233
- expect(resolvePortOnTypeChange('postgresql', 'mysql', 5433)).toBe(5433)
234
- })
235
-
236
- it('端口为 0 时切到新类型默认端口', () => {
237
- expect(resolvePortOnTypeChange('mysql', 'postgresql', 0)).toBe(5432)
238
- })
239
- })
@@ -1,96 +0,0 @@
1
- import { Sheet } from '@veltra/sheet-core'
2
- import { importXlsx } from '@veltra/sheet-core/core/io/import'
3
- import { describe, expect, it } from 'vitest'
4
-
5
- import { REPORT_META_NAMESPACE, createReportBinding } from '../binding'
6
- import { exportFilledReportXlsx } from '../export-xlsx'
7
- import { renderReport } from '../render'
8
- import type { DatasetCatalogItem } from '../types'
9
-
10
- // ---- 内联 fixtures ----
11
-
12
- const ORDERS_DATASET: DatasetCatalogItem = {
13
- id: 'orders',
14
- label: '销售明细',
15
- fields: [
16
- { name: 'customer', label: '客户', type: 'string' },
17
- { name: 'amount', label: '金额', type: 'number' }
18
- ]
19
- }
20
-
21
- const ORDER_ROWS: Record<string, unknown>[] = [
22
- { customer: '甲公司', amount: 100 },
23
- { customer: '乙公司', amount: 400 }
24
- ]
25
-
26
- /** 金额 > 300 标红(条件样式打平进快照后随导出保真) */
27
- const RED_RULE = { operator: 'gt' as const, value: 300, style: { font: { color: '#DC2626' } } }
28
-
29
- /** 分组(客户)+ 明细(金额,带条件规则)模板 */
30
- function buildFilledSheet(): Sheet {
31
- const sheet = new Sheet()
32
- sheet.setCells([
33
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
34
- { addr: { row: 0, col: 1 }, data: { v: '金额' } }
35
- ])
36
- sheet.setCellStyle(
37
- { start: { row: 0, col: 0 }, end: { row: 0, col: 1 } },
38
- { font: { bold: true }, fill: { color: '#E8EEF7' } }
39
- )
40
-
41
- const groupAddr = { row: 1, col: 0 }
42
- const group = createReportBinding(ORDERS_DATASET, 'customer')
43
- group.preset = 'groupHeader'
44
- group.aggregate = 'group'
45
- group.expand = 'down'
46
- sheet.setCellMeta(groupAddr, REPORT_META_NAMESPACE, group)
47
-
48
- const amount = createReportBinding(ORDERS_DATASET, 'amount')
49
- amount.rowParent = groupAddr
50
- amount.conditionalRules = [RED_RULE]
51
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, amount)
52
-
53
- const filled = renderReport(sheet.snapshot(), { orders: ORDER_ROWS })
54
- const filledSheet = new Sheet()
55
- filledSheet.restore(filled)
56
- filledSheet.restoreContent(filled)
57
- return filledSheet
58
- }
59
-
60
- describe('exportFilledReportXlsx', () => {
61
- it('导出保真 XLSX:值 / 表头样式 / 条件样式打平后随快照导出', async () => {
62
- const filled = buildFilledSheet()
63
- filled.setColWidth(0, 120)
64
- filled.setColWidth(1, 140)
65
- const buffer = await exportFilledReportXlsx(filled)
66
-
67
- // 合法 ZIP(xlsx)字节
68
- expect(buffer[0]).toBe(0x50)
69
- expect(buffer[1]).toBe(0x4b)
70
-
71
- const workbook = await importXlsx(buffer)
72
- const sheet = workbook.getSheets()[0]!
73
-
74
- // 展开值保真(分组头 + 明细值)
75
- const values: unknown[] = []
76
- for (const [, data] of sheet.store.entries()) values.push(data.v)
77
- expect(values).toContain('甲公司')
78
- expect(values).toContain('乙公司')
79
- expect(values).toContain(400)
80
-
81
- // 条件样式打平:amount=400(>300)的明细格带红字
82
- let redFound = false
83
- for (const [, data] of sheet.store.entries()) {
84
- if (data.v !== 400 || data.s == null) continue
85
- const style = sheet.stylePool.get(data.s)
86
- if (style?.font?.color === '#DC2626') redFound = true
87
- }
88
- expect(redFound).toBe(true)
89
-
90
- // 表头静态样式保真(加粗 + 底色)
91
- const header = sheet.getCellData({ row: 0, col: 0 })
92
- const headerStyle = header?.s != null ? sheet.stylePool.get(header.s) : undefined
93
- expect(headerStyle?.font?.bold).toBe(true)
94
- expect(headerStyle?.fill?.color).toBe('#E8EEF7')
95
- })
96
- })