@veltra/sheet 2.4.0 → 2.5.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.
Files changed (147) hide show
  1. package/dist/components/sheet/formula-bar.js +50 -7
  2. package/dist/components/sheet/style2.css +2 -2
  3. package/dist/index.d.ts +2 -19
  4. package/dist/index.js +2 -15
  5. package/dist/types/index.d.ts +1 -3
  6. package/package.json +5 -6
  7. package/src/components/sheet/__test__/formula-bar.test.ts +29 -0
  8. package/src/components/sheet/formula-bar.vue +67 -6
  9. package/src/components/sheet/style.scss +1 -1
  10. package/src/index.ts +1 -27
  11. package/src/types/index.ts +0 -2
  12. package/dist/components/report/binding-badge.js +0 -118
  13. package/dist/components/report/designer/cell-coords.js +0 -46
  14. package/dist/components/report/designer/col-widths.js +0 -29
  15. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  16. package/dist/components/report/designer/drill-dialog.js +0 -258
  17. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  18. package/dist/components/report/designer/float-panel.js +0 -434
  19. package/dist/components/report/designer/role.js +0 -17
  20. package/dist/components/report/designer/rule-preview.js +0 -33
  21. package/dist/components/report/designer/rule-row.js +0 -253
  22. package/dist/components/report/designer/rules-dialog.js +0 -118
  23. package/dist/components/report/designer/topology-overlay.js +0 -122
  24. package/dist/components/report/designer/topology.js +0 -64
  25. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  26. package/dist/components/report/designer-hub.js +0 -177
  27. package/dist/components/report/field-panel-helpers.js +0 -33
  28. package/dist/components/report/field-panel.js +0 -132
  29. package/dist/components/report/filter-bar.js +0 -74
  30. package/dist/components/report/hub-connection-form.js +0 -145
  31. package/dist/components/report/hub-dataset-editor.js +0 -409
  32. package/dist/components/report/preview-grid-size.js +0 -32
  33. package/dist/components/report/print.js +0 -39
  34. package/dist/components/report/report-designer.js +0 -428
  35. package/dist/components/report/report-viewer.js +0 -253
  36. package/dist/components/report/style.d.ts +0 -17
  37. package/dist/components/report/style.js +0 -18
  38. package/dist/components/report/style2.css +0 -965
  39. package/dist/components/report/use-report-designer.js +0 -387
  40. package/dist/components/report/use-report-viewer.js +0 -140
  41. package/dist/report/binding.d.ts +0 -83
  42. package/dist/report/binding.js +0 -334
  43. package/dist/report/connector.d.ts +0 -66
  44. package/dist/report/connector.js +0 -159
  45. package/dist/report/drill.d.ts +0 -48
  46. package/dist/report/drill.js +0 -78
  47. package/dist/report/export-xlsx.d.ts +0 -13
  48. package/dist/report/export-xlsx.js +0 -13
  49. package/dist/report/filter-bar.d.ts +0 -10
  50. package/dist/report/filter-bar.js +0 -31
  51. package/dist/report/index.d.ts +0 -14
  52. package/dist/report/params.d.ts +0 -13
  53. package/dist/report/params.js +0 -61
  54. package/dist/report/print.d.ts +0 -23
  55. package/dist/report/print.js +0 -197
  56. package/dist/report/render/aggregate.js +0 -193
  57. package/dist/report/render/builder.d.ts +0 -12
  58. package/dist/report/render/builder.js +0 -139
  59. package/dist/report/render/coordinate.d.ts +0 -25
  60. package/dist/report/render/coordinate.js +0 -733
  61. package/dist/report/render/index.d.ts +0 -14
  62. package/dist/report/render/index.js +0 -21
  63. package/dist/report/render/style-resolver.js +0 -50
  64. package/dist/report/render/template-index.d.ts +0 -24
  65. package/dist/report/render/template-index.js +0 -137
  66. package/dist/report/rules/evaluate.d.ts +0 -25
  67. package/dist/report/rules/evaluate.js +0 -82
  68. package/dist/report/rules/index.d.ts +0 -2
  69. package/dist/report/template.d.ts +0 -63
  70. package/dist/report/template.js +0 -83
  71. package/dist/report/types.d.ts +0 -103
  72. package/dist/types/report-designer.d.ts +0 -51
  73. package/dist/types/report-viewer.d.ts +0 -57
  74. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  75. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  76. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  77. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  78. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  79. package/src/components/report/__test__/report-designer.test.ts +0 -559
  80. package/src/components/report/__test__/report-drill.test.ts +0 -692
  81. package/src/components/report/__test__/report-viewer.test.ts +0 -400
  82. package/src/components/report/__test__/topology.test.ts +0 -143
  83. package/src/components/report/__test__/use-report-designer.test.ts +0 -804
  84. package/src/components/report/binding-badge.ts +0 -122
  85. package/src/components/report/designer/cell-coords.ts +0 -95
  86. package/src/components/report/designer/col-widths.ts +0 -56
  87. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  88. package/src/components/report/designer/drill-dialog.vue +0 -245
  89. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  90. package/src/components/report/designer/float-panel.vue +0 -444
  91. package/src/components/report/designer/role.ts +0 -32
  92. package/src/components/report/designer/rule-preview.vue +0 -40
  93. package/src/components/report/designer/rule-row.vue +0 -252
  94. package/src/components/report/designer/rules-dialog.vue +0 -110
  95. package/src/components/report/designer/topology-overlay.vue +0 -128
  96. package/src/components/report/designer/topology.ts +0 -124
  97. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  98. package/src/components/report/designer-hub.vue +0 -198
  99. package/src/components/report/field-panel-helpers.ts +0 -44
  100. package/src/components/report/field-panel.vue +0 -136
  101. package/src/components/report/filter-bar.vue +0 -77
  102. package/src/components/report/hub-connection-form.vue +0 -124
  103. package/src/components/report/hub-dataset-editor.vue +0 -387
  104. package/src/components/report/index.ts +0 -2
  105. package/src/components/report/preview-grid-size.ts +0 -33
  106. package/src/components/report/print.ts +0 -44
  107. package/src/components/report/report-designer.vue +0 -454
  108. package/src/components/report/report-viewer.vue +0 -289
  109. package/src/components/report/style-designer.scss +0 -653
  110. package/src/components/report/style-hub.scss +0 -292
  111. package/src/components/report/style-viewer.scss +0 -144
  112. package/src/components/report/style.scss +0 -3
  113. package/src/components/report/style.ts +0 -20
  114. package/src/components/report/use-report-designer.ts +0 -603
  115. package/src/components/report/use-report-viewer.ts +0 -227
  116. package/src/report/__test__/binding.test.ts +0 -320
  117. package/src/report/__test__/connector.test.ts +0 -239
  118. package/src/report/__test__/drill.test.ts +0 -194
  119. package/src/report/__test__/export-xlsx.test.ts +0 -96
  120. package/src/report/__test__/filter-bar.test.ts +0 -58
  121. package/src/report/__test__/params.test.ts +0 -156
  122. package/src/report/__test__/print.test.ts +0 -170
  123. package/src/report/__test__/render.test.ts +0 -856
  124. package/src/report/__test__/rules.test.ts +0 -131
  125. package/src/report/__test__/template.test.ts +0 -366
  126. package/src/report/binding.ts +0 -474
  127. package/src/report/connector.ts +0 -206
  128. package/src/report/drill.ts +0 -112
  129. package/src/report/export-xlsx.ts +0 -15
  130. package/src/report/filter-bar.ts +0 -36
  131. package/src/report/index.ts +0 -16
  132. package/src/report/params.ts +0 -96
  133. package/src/report/print.ts +0 -234
  134. package/src/report/render/__test__/aggregate.test.ts +0 -16
  135. package/src/report/render/__test__/coordinate.test.ts +0 -405
  136. package/src/report/render/aggregate.ts +0 -289
  137. package/src/report/render/builder.ts +0 -211
  138. package/src/report/render/coordinate.ts +0 -1239
  139. package/src/report/render/index.ts +0 -30
  140. package/src/report/render/style-resolver.ts +0 -68
  141. package/src/report/render/template-index.ts +0 -200
  142. package/src/report/rules/evaluate.ts +0 -113
  143. package/src/report/rules/index.ts +0 -7
  144. package/src/report/template.ts +0 -145
  145. package/src/report/types.ts +0 -116
  146. package/src/types/report-designer.ts +0 -55
  147. package/src/types/report-viewer.ts +0 -54
@@ -1,400 +0,0 @@
1
- import { Sheet, Workbook } from '@veltra/sheet-core'
2
- import { afterEach, describe, expect, it, vi } from 'vitest'
3
- import { createApp, h, nextTick, type App } from 'vue'
4
-
5
- import { UReportViewer } from '../../../index'
6
- import { REPORT_META_NAMESPACE } from '../../../report/binding'
7
- import type { DataConnection, DataConnector } from '../../../report/connector'
8
- import { createReportTemplate } from '../../../report/template'
9
- import type { ReportDatasetDef, ReportTemplate } from '../../../report/template'
10
- import type { ParamValues, ReportBinding } from '../../../report/types'
11
- import type { ReportViewerExposed, SheetExposed } from '../../../types'
12
-
13
- // ---- 内联 fixtures:stub connector(实现 DataConnector 接口的内存测试夹具)----
14
-
15
- const MYSQL: DataConnection = {
16
- id: 'c1',
17
- label: 'stub MySQL',
18
- type: 'mysql',
19
- host: '127.0.0.1',
20
- port: 3306,
21
- database: 'demo',
22
- username: 'root',
23
- password: ''
24
- }
25
-
26
- // 覆盖全部 5 种参数控件类型:LIKE→text、>=→number、覆盖→select、
27
- // BETWEEN ${a} AND ${b} 首参→date、BETWEEN 单参(SQL 末尾)→date-range
28
- const ORDERS_SQL =
29
- 'SELECT customer, amount FROM orders ' +
30
- 'WHERE customer LIKE ${keyword} AND amount >= ${minAmount} AND region = ${region} ' +
31
- 'AND orderDate BETWEEN ${dateFrom} AND ${dateTo} AND createdAt BETWEEN ${dateRange}'
32
-
33
- const ORDERS_DATASET: ReportDatasetDef = {
34
- id: 'orders',
35
- label: '销售明细',
36
- connection: MYSQL,
37
- sql: ORDERS_SQL,
38
- paramOverrides: {
39
- region: {
40
- label: '地区',
41
- type: 'select',
42
- defaultValue: '华东',
43
- options: [
44
- { label: '华东', value: '华东' },
45
- { label: '华南', value: '华南' }
46
- ]
47
- }
48
- }
49
- }
50
-
51
- /** 参数默认值(keyword/dateFrom/dateTo 文本 ''、minAmount 数值 0、region 覆盖默认、dateRange 元组) */
52
- const DEFAULT_VALUES: ParamValues = {
53
- keyword: '',
54
- minAmount: 0,
55
- region: '华东',
56
- dateFrom: '',
57
- dateTo: '',
58
- dateRange: ['', '']
59
- }
60
-
61
- const ORDER_ROWS: Record<string, unknown>[] = [
62
- { customer: '甲公司', amount: 100 },
63
- { customer: '甲公司', amount: 200 },
64
- { customer: '乙公司', amount: 300 }
65
- ]
66
-
67
- /** 带绑定的模板:表头 + 同一扩展带(客户分组 + 金额明细) */
68
- function createViewerTemplate(datasets: ReportDatasetDef[] = [ORDERS_DATASET]): ReportTemplate {
69
- const sheet = new Sheet()
70
- sheet.setCells([
71
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
72
- { addr: { row: 0, col: 1 }, data: { v: '金额' } }
73
- ])
74
-
75
- const group: ReportBinding = {
76
- dataset: 'orders',
77
- field: 'customer',
78
- preset: 'groupHeader',
79
- aggregate: 'group',
80
- expand: 'down'
81
- }
82
- sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, group)
83
-
84
- const amount: ReportBinding = {
85
- dataset: 'orders',
86
- field: 'amount',
87
- preset: 'detail',
88
- aggregate: 'list',
89
- expand: 'down',
90
- rowParent: { row: 1, col: 0 }
91
- }
92
- sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, amount)
93
-
94
- // 模拟设计态 canvas(旧预览用 max(50×10, 快照) 会铺出空白格)
95
- sheet.ensureTableSize(50, 10)
96
- return createReportTemplate(sheet.snapshot(), datasets)
97
- }
98
-
99
- interface QueryCall {
100
- connection: DataConnection
101
- sql: string
102
- values: ParamValues
103
- }
104
-
105
- type QueryOutcome =
106
- | { rows: Record<string, unknown>[] }
107
- | { error: { code: string; message: string } }
108
-
109
- /** stub connector:记录 query 调用;outcome 可为立即值或手动闸门(loading 测试) */
110
- function createStubConnector(outcome: QueryOutcome | (() => Promise<QueryOutcome>)) {
111
- const calls: QueryCall[] = []
112
- const connector: DataConnector = {
113
- test: () => Promise.resolve({ ok: true, data: undefined }),
114
- describe: () => Promise.resolve({ ok: true, data: [] }),
115
- query: async (connection, sql, values) => {
116
- calls.push({ connection, sql, values })
117
- const resolved = typeof outcome === 'function' ? await outcome() : outcome
118
- if ('error' in resolved) return { ok: false, error: resolved.error }
119
- return { ok: true, data: { fields: [], rows: resolved.rows } }
120
- }
121
- }
122
- return { connector, calls }
123
- }
124
-
125
- function deferred<T>() {
126
- let resolve!: (value: T | PromiseLike<T>) => void
127
- const promise = new Promise<T>((r) => {
128
- resolve = r
129
- })
130
- return { promise, resolve }
131
- }
132
-
133
- // ---- 挂载与等待 ----
134
-
135
- const apps: App[] = []
136
- const containers: HTMLElement[] = []
137
-
138
- function mountViewer(props: {
139
- connector: DataConnector
140
- template: ReportTemplate
141
- workbook?: Workbook
142
- }) {
143
- const el = document.createElement('div')
144
- el.style.width = '800px'
145
- el.style.height = '600px'
146
- document.body.appendChild(el)
147
- containers.push(el)
148
- const exposedRef = { value: undefined as ReportViewerExposed | undefined }
149
- const app = createApp({
150
- render: () =>
151
- h(UReportViewer, {
152
- ...props,
153
- ref: (value: unknown) => {
154
- exposedRef.value = value as ReportViewerExposed | undefined
155
- }
156
- })
157
- })
158
- app.mount(el)
159
- apps.push(app)
160
- return { app, el, exposedRef }
161
- }
162
-
163
- /** 内嵌 USheet 的 SheetGrid(报表预览断言行列头 / 渲染尺寸) */
164
- function nestedSheetGrid(el: HTMLElement) {
165
- const sheetEl = el.querySelector('.u-sheet') as
166
- | (HTMLElement & { __vueParentComponent?: { exposed?: SheetExposed } })
167
- | null
168
- return sheetEl?.__vueParentComponent?.exposed?.getGrid()
169
- }
170
-
171
- /** 等待取数(宏/微任务)+ 填充快照应用(watch + nextTick)落定 */
172
- async function flushViewer(): Promise<void> {
173
- await nextTick()
174
- await new Promise((resolve) => setTimeout(resolve, 0))
175
- await nextTick()
176
- await nextTick()
177
- }
178
-
179
- afterEach(() => {
180
- while (apps.length) apps.pop()!.unmount()
181
- while (containers.length) containers.pop()!.remove()
182
- })
183
-
184
- describe('UReportViewer', () => {
185
- it('全流程:带绑定的模板 + stub connector → 参数提取 → Filter Bar → 取数 → 展开渲染', async () => {
186
- const { connector, calls } = createStubConnector({ rows: ORDER_ROWS })
187
- const workbook = new Workbook()
188
- const { el } = mountViewer({ connector, template: createViewerTemplate(), workbook })
189
- await flushViewer()
190
-
191
- // 经连接器取数:连接对象 / SQL / 参数默认值原样透传
192
- expect(calls).toHaveLength(1)
193
- expect(calls[0]).toEqual({ connection: MYSQL, sql: ORDERS_SQL, values: DEFAULT_VALUES })
194
-
195
- // Filter Bar 按参数类型映射控件(text/number/select/date/date-range)
196
- const filterBar = el.querySelector('.u-report-filter-bar')!
197
- expect(filterBar).toBeTruthy()
198
- expect(filterBar.querySelector('.u-input')).toBeTruthy()
199
- expect(filterBar.querySelector('.u-number-input')).toBeTruthy()
200
- expect(filterBar.querySelector('.u-select')).toBeTruthy()
201
- expect(filterBar.querySelector('.u-date-picker')).toBeTruthy()
202
- expect(filterBar.querySelector('.u-date-range-picker')).toBeTruthy()
203
- // 覆盖元数据的 label 生效
204
- expect(filterBar.textContent).toContain('地区')
205
-
206
- // 展开结果落到只读网格(模型断言):表头 + 甲(2 明细) + 乙(1 明细)
207
- const sheet = workbook.activeSheet
208
- expect(sheet.getDisplayValue({ row: 0, col: 0 })).toBe('客户')
209
- expect(sheet.getDisplayValue({ row: 1, col: 0 })).toBe('甲公司')
210
- expect(sheet.getCellData({ row: 1, col: 1 })?.v).toBe(100)
211
- expect(sheet.getCellData({ row: 2, col: 1 })?.v).toBe(200)
212
- expect(sheet.getDisplayValue({ row: 3, col: 0 })).toBe('乙公司')
213
- expect(sheet.getCellData({ row: 3, col: 1 })?.v).toBe(300)
214
-
215
- // 网格铺到内容尺寸(表头 + 3 明细行 × 2 列),不再用 50×10 下限
216
- expect(sheet.rows).toBe(4)
217
- expect(sheet.cols).toBe(2)
218
-
219
- const table = nestedSheetGrid(el)!.getTable()
220
- expect(table.options.showHeader).toBe(false)
221
- expect(table.options.rowSeriesNumber).toBeUndefined()
222
- expect(table.columnHeaderLevelCount).toBe(0)
223
- expect(table.rowCount).toBe(4)
224
- expect(table.colCount).toBe(2)
225
- // 无行号/列头偏移:表格 (0,0) 即模型 A1
226
- expect(table.getCellValue(0, 0)).toBe('客户')
227
- })
228
-
229
- it('Filter Bar 改值重新取数,其余参数值保留', async () => {
230
- const { connector, calls } = createStubConnector({ rows: ORDER_ROWS })
231
- const workbook = new Workbook()
232
- const { el } = mountViewer({ connector, template: createViewerTemplate(), workbook })
233
- await flushViewer()
234
- expect(calls).toHaveLength(1)
235
-
236
- // keyword(text 控件)输入新值
237
- const input = el.querySelector<HTMLInputElement>('.u-report-filter-bar .u-input input')!
238
- input.value = '甲公司'
239
- input.dispatchEvent(
240
- new InputEvent('input', { bubbles: true, data: '甲公司', inputType: 'insertText' })
241
- )
242
- await flushViewer()
243
-
244
- expect(calls).toHaveLength(2)
245
- expect(calls[1]!.values).toEqual({ ...DEFAULT_VALUES, keyword: '甲公司' })
246
-
247
- // minAmount(number 控件)输入新值
248
- const numberInput = el.querySelector<HTMLInputElement>(
249
- '.u-report-filter-bar .u-number-input input'
250
- )!
251
- numberInput.value = '500'
252
- numberInput.dispatchEvent(
253
- new InputEvent('input', { bubbles: true, data: '500', inputType: 'insertText' })
254
- )
255
- await flushViewer()
256
-
257
- expect(calls).toHaveLength(3)
258
- expect(calls[2]!.values).toEqual({ ...DEFAULT_VALUES, keyword: '甲公司', minAmount: 500 })
259
- })
260
-
261
- it('取数中有 loading;业务错误(ok:false)有可读提示;refresh() 触发重新取数', async () => {
262
- let gate = deferred<QueryOutcome>()
263
- // 首次取数返回业务错误,refresh 后返回数据
264
- let outcome: QueryOutcome = { error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' } }
265
- const { connector, calls } = createStubConnector(() => gate.promise)
266
- const workbook = new Workbook()
267
- const { el, exposedRef } = mountViewer({
268
- connector,
269
- template: createViewerTemplate(),
270
- workbook
271
- })
272
- await nextTick()
273
-
274
- // 取数中:loading 遮罩可见
275
- expect(el.querySelector('.u-report-viewer__loading')).toBeTruthy()
276
-
277
- // 业务错误:可读错误提示可见,网格保留模板静态结构
278
- gate.resolve(outcome)
279
- await flushViewer()
280
- expect(el.querySelector('.u-report-viewer__loading')).toBeNull()
281
- const errorBanner = el.querySelector('.u-report-viewer__error')!
282
- expect(errorBanner.textContent).toContain('数据库连接失败')
283
- expect(workbook.activeSheet.getDisplayValue({ row: 0, col: 0 })).toBe('客户')
284
- expect(workbook.activeSheet.getDisplayValue({ row: 1, col: 0 })).toBeUndefined()
285
-
286
- // refresh() 重新取数:成功后错误消失、展开结果渲染
287
- outcome = { rows: ORDER_ROWS }
288
- gate = deferred<QueryOutcome>()
289
- const refreshed = exposedRef.value!.refresh()
290
- expect(calls).toHaveLength(2)
291
- await nextTick()
292
- expect(el.querySelector('.u-report-viewer__loading')).toBeTruthy()
293
- gate.resolve(outcome)
294
- await refreshed
295
- await flushViewer()
296
- expect(el.querySelector('.u-report-viewer__error')).toBeNull()
297
- expect(workbook.activeSheet.getDisplayValue({ row: 1, col: 0 })).toBe('甲公司')
298
- expect(workbook.activeSheet.getCellData({ row: 3, col: 1 })?.v).toBe(300)
299
- })
300
-
301
- it('模板无查询参数时不渲染 Filter Bar', async () => {
302
- const dataset: ReportDatasetDef = {
303
- id: 'orders',
304
- label: '销售明细',
305
- connection: MYSQL,
306
- sql: 'SELECT customer, amount FROM orders'
307
- }
308
- const { connector } = createStubConnector({ rows: ORDER_ROWS })
309
- const { el } = mountViewer({
310
- connector,
311
- template: createViewerTemplate([dataset]),
312
- workbook: new Workbook()
313
- })
314
- await flushViewer()
315
- expect(el.querySelector('.u-report-filter-bar')).toBeNull()
316
- })
317
-
318
- it('取数前 exportXlsx() 拒绝;取数失败或未就绪时同样拒绝', async () => {
319
- let gate = deferred<QueryOutcome>()
320
- let outcome: QueryOutcome = { error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' } }
321
- const { connector } = createStubConnector(() => gate.promise)
322
- const { exposedRef } = mountViewer({
323
- connector,
324
- template: createViewerTemplate(),
325
- workbook: new Workbook()
326
- })
327
- await nextTick()
328
-
329
- await expect(exposedRef.value!.exportXlsx()).rejects.toThrow('报表数据加载中')
330
-
331
- gate.resolve(outcome)
332
- await flushViewer()
333
- await expect(exposedRef.value!.exportXlsx()).rejects.toThrow('报表数据尚未就绪')
334
- })
335
-
336
- it('取数后 exportXlsx() 成功产出 blob', async () => {
337
- const { connector } = createStubConnector({ rows: ORDER_ROWS })
338
- const workbook = new Workbook()
339
- const { exposedRef } = mountViewer({ connector, template: createViewerTemplate(), workbook })
340
- await flushViewer()
341
-
342
- const exportModule = await import('../../../report/export-xlsx')
343
- const catKitFe = await import('@cat-kit/fe')
344
- const exportSpy = vi
345
- .spyOn(exportModule, 'exportFilledReportXlsx')
346
- .mockResolvedValue(new Uint8Array([80, 75, 3, 4]))
347
- const saveBlobSpy = vi.spyOn(catKitFe, 'saveBlob').mockImplementation(() => {})
348
-
349
- await exposedRef.value!.exportXlsx()
350
-
351
- expect(exportSpy).toHaveBeenCalled()
352
- expect(saveBlobSpy).toHaveBeenCalledWith(expect.any(Blob), expect.stringMatching(/\.xlsx$/))
353
-
354
- exportSpy.mockRestore()
355
- saveBlobSpy.mockRestore()
356
- })
357
-
358
- it('取数前 print() 拒绝;取数失败或未就绪时同样拒绝', async () => {
359
- let gate = deferred<QueryOutcome>()
360
- const outcome: QueryOutcome = {
361
- error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' }
362
- }
363
- const { connector } = createStubConnector(() => gate.promise)
364
- const { exposedRef } = mountViewer({
365
- connector,
366
- template: createViewerTemplate(),
367
- workbook: new Workbook()
368
- })
369
- await nextTick()
370
-
371
- expect(() => exposedRef.value!.print()).toThrow('报表数据加载中')
372
-
373
- gate.resolve(outcome)
374
- await flushViewer()
375
- expect(() => exposedRef.value!.print()).toThrow('报表数据尚未就绪')
376
- })
377
-
378
- it('取数后 print() 生成打印 HTML 并送隐藏 iframe 打印', async () => {
379
- const { connector } = createStubConnector({ rows: ORDER_ROWS })
380
- const workbook = new Workbook()
381
- const { exposedRef } = mountViewer({ connector, template: createViewerTemplate(), workbook })
382
- await flushViewer()
383
-
384
- const printModule = await import('../../../report/print')
385
- const domPrintModule = await import('../print')
386
- const buildSpy = vi.spyOn(printModule, 'buildFilledReportPrintHtml')
387
- const printSpy = vi.spyOn(domPrintModule, 'printHtmlDocument').mockImplementation(() => {})
388
-
389
- exposedRef.value!.print({ orientation: 'landscape' })
390
-
391
- expect(buildSpy).toHaveBeenCalledWith(workbook.activeSheet, { orientation: 'landscape' })
392
- const html = buildSpy.mock.results[0]!.value as string
393
- expect(html).toContain('甲公司')
394
- expect(html).toContain('@page { size: A4 landscape;')
395
- expect(printSpy).toHaveBeenCalledWith(html)
396
-
397
- buildSpy.mockRestore()
398
- printSpy.mockRestore()
399
- })
400
- })
@@ -1,143 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { createReportBinding } from '../../../report/binding'
4
- import type { DatasetCatalogItem, ReportBinding } from '../../../report/types'
5
- import {
6
- buildTopologyArcPath,
7
- collectTopologyLinks,
8
- findCellsWithColParent,
9
- findCellsWithRowParent
10
- } from '../designer/topology'
11
-
12
- // ---- 内联 fixtures ----
13
-
14
- const ORDERS_DATASET: DatasetCatalogItem = {
15
- id: 'orders',
16
- label: '销售明细',
17
- fields: [
18
- { name: 'customer', label: '客户', type: 'string' },
19
- { name: 'orderNo', label: '订单号', type: 'string' },
20
- { name: 'region', label: '地区', type: 'string' },
21
- { name: 'amount', label: '金额', type: 'number' }
22
- ]
23
- }
24
-
25
- function bindingMap(cells: Array<{ addr: { row: number; col: number }; binding: ReportBinding }>) {
26
- const map = new Map<string, ReportBinding>()
27
- for (const { addr, binding } of cells) {
28
- map.set(`${addr.row},${addr.col}`, binding)
29
- }
30
- return (addr: { row: number; col: number }) => map.get(`${addr.row},${addr.col}`)
31
- }
32
-
33
- describe('report topology', () => {
34
- it('collectTopologyLinks 沿 rowParent 链上行并包含直接子格', () => {
35
- const parentAddr = { row: 1, col: 0 }
36
- const parent = createReportBinding(ORDERS_DATASET, 'customer')
37
- parent.aggregate = 'group'
38
- parent.preset = 'groupHeader'
39
-
40
- const mid = createReportBinding(ORDERS_DATASET, 'orderNo')
41
- mid.rowParent = parentAddr
42
-
43
- const detail = createReportBinding(ORDERS_DATASET, 'region')
44
- detail.rowParent = { row: 1, col: 1 }
45
-
46
- const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
47
- subtotal.aggregate = 'sum'
48
- subtotal.expand = 'none'
49
- subtotal.rowParent = parentAddr
50
-
51
- const entries = [
52
- { addr: parentAddr, binding: parent },
53
- { addr: { row: 1, col: 1 }, binding: mid },
54
- { addr: { row: 1, col: 2 }, binding: detail },
55
- { addr: { row: 2, col: 3 }, binding: subtotal }
56
- ]
57
- const getBindingAt = bindingMap(entries)
58
-
59
- expect(collectTopologyLinks({ row: 1, col: 2 }, detail, entries, getBindingAt)).toEqual([
60
- { from: { row: 1, col: 2 }, to: { row: 1, col: 1 }, direction: 'row' },
61
- { from: { row: 1, col: 1 }, to: parentAddr, direction: 'row' }
62
- ])
63
-
64
- expect(collectTopologyLinks(parentAddr, parent, entries, getBindingAt)).toEqual([
65
- { from: { row: 1, col: 1 }, to: parentAddr, direction: 'row' },
66
- { from: { row: 2, col: 3 }, to: parentAddr, direction: 'row' }
67
- ])
68
- })
69
-
70
- it('collectTopologyLinks 沿 colParent 链上行并包含列方向子格', () => {
71
- const rowParentAddr = { row: 1, col: 0 }
72
- const colParentAddr = { row: 0, col: 1 }
73
-
74
- const rowGroup = createReportBinding(ORDERS_DATASET, 'customer')
75
- rowGroup.aggregate = 'group'
76
- rowGroup.expand = 'down'
77
-
78
- const colGroup = createReportBinding(ORDERS_DATASET, 'region')
79
- colGroup.aggregate = 'group'
80
- colGroup.expand = 'right'
81
-
82
- const cross = createReportBinding(ORDERS_DATASET, 'amount')
83
- cross.aggregate = 'sum'
84
- cross.expand = 'none'
85
- cross.rowParent = rowParentAddr
86
- cross.colParent = colParentAddr
87
-
88
- const entries = [
89
- { addr: rowParentAddr, binding: rowGroup },
90
- { addr: colParentAddr, binding: colGroup },
91
- { addr: { row: 1, col: 2 }, binding: cross }
92
- ]
93
- const getBindingAt = bindingMap(entries)
94
-
95
- expect(collectTopologyLinks({ row: 1, col: 2 }, cross, entries, getBindingAt)).toEqual([
96
- { from: { row: 1, col: 2 }, to: rowParentAddr, direction: 'row' },
97
- { from: { row: 1, col: 2 }, to: colParentAddr, direction: 'col' }
98
- ])
99
- })
100
-
101
- it('findCellsWithColParent 仅返回 colParent 指向目标父格的子格', () => {
102
- const parentAddr = { row: 0, col: 1 }
103
- const parent = createReportBinding(ORDERS_DATASET, 'region')
104
- parent.aggregate = 'group'
105
- parent.expand = 'right'
106
-
107
- const child = createReportBinding(ORDERS_DATASET, 'amount')
108
- child.colParent = parentAddr
109
-
110
- const entries = [
111
- { addr: parentAddr, binding: parent },
112
- { addr: { row: 1, col: 2 }, binding: child }
113
- ]
114
-
115
- expect(findCellsWithColParent(parentAddr, entries)).toEqual([{ row: 1, col: 2 }])
116
- })
117
-
118
- it('findCellsWithRowParent 仅返回 rowParent 指向目标父格的子格', () => {
119
- const parentAddr = { row: 1, col: 0 }
120
- const parent = createReportBinding(ORDERS_DATASET, 'customer')
121
- parent.aggregate = 'group'
122
- parent.preset = 'groupHeader'
123
-
124
- const child = createReportBinding(ORDERS_DATASET, 'orderNo')
125
- child.rowParent = parentAddr
126
-
127
- const other = createReportBinding(ORDERS_DATASET, 'region')
128
-
129
- const entries = [
130
- { addr: parentAddr, binding: parent },
131
- { addr: { row: 1, col: 1 }, binding: child },
132
- { addr: { row: 1, col: 2 }, binding: other }
133
- ]
134
-
135
- expect(findCellsWithRowParent(parentAddr, entries)).toEqual([{ row: 1, col: 1 }])
136
- })
137
-
138
- it('buildTopologyArcPath 输出合法 SVG 路径', () => {
139
- const path = buildTopologyArcPath({ x: 10, y: 20 }, { x: 100, y: 40 })
140
- expect(path.startsWith('M 10 20 Q')).toBe(true)
141
- expect(path.endsWith('100 40')).toBe(true)
142
- })
143
- })