@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,131 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import {
4
- evaluateCondition,
5
- evaluateConditionalStyle,
6
- resolveRuleCompareValue,
7
- type ConditionalEvalContext
8
- } from '../rules'
9
- import type { ConditionalRule } from '../types'
10
-
11
- function rule(
12
- operator: ConditionalRule['operator'],
13
- value: unknown,
14
- style: ConditionalRule['style'],
15
- extra?: Partial<ConditionalRule>
16
- ): ConditionalRule {
17
- return { operator, value, style, ...extra }
18
- }
19
-
20
- function ctx(
21
- cellValue: unknown,
22
- record?: Record<string, unknown>,
23
- bindingField = 'amount'
24
- ): ConditionalEvalContext {
25
- return { cellValue, bindingField, record }
26
- }
27
-
28
- describe('report rules', () => {
29
- describe('evaluateCondition', () => {
30
- it('数值比较运算符', () => {
31
- expect(evaluateCondition(150, rule('gt', 100, {}))).toBe(true)
32
- expect(evaluateCondition(100, rule('gt', 100, {}))).toBe(false)
33
- expect(evaluateCondition(100, rule('gte', 100, {}))).toBe(true)
34
- expect(evaluateCondition(99, rule('lt', 100, {}))).toBe(true)
35
- expect(evaluateCondition(100, rule('lte', 100, {}))).toBe(true)
36
- expect(evaluateCondition(42, rule('eq', 42, {}))).toBe(true)
37
- expect(evaluateCondition('42', rule('eq', 42, {}))).toBe(true)
38
- })
39
-
40
- it('between 含端点范围比较', () => {
41
- const between = rule('between', [100, 200], {})
42
- expect(evaluateCondition(100, between)).toBe(true)
43
- expect(evaluateCondition(200, between)).toBe(true)
44
- expect(evaluateCondition(150, between)).toBe(true)
45
- expect(evaluateCondition(99, between)).toBe(false)
46
- expect(evaluateCondition(201, between)).toBe(false)
47
- })
48
-
49
- it('contains 字符串包含', () => {
50
- expect(evaluateCondition('华东-上海', rule('contains', '上海', {}))).toBe(true)
51
- expect(evaluateCondition('华北', rule('contains', '上海', {}))).toBe(false)
52
- expect(evaluateCondition(null, rule('contains', 'x', {}))).toBe(false)
53
- })
54
-
55
- it('非数值比较在 gt 等运算符下返回 false', () => {
56
- expect(evaluateCondition('abc', rule('gt', 1, {}))).toBe(false)
57
- })
58
- })
59
-
60
- describe('resolveRuleCompareValue', () => {
61
- it('缺省 field 时取 cellValue', () => {
62
- const context = ctx(150, { amount: 150, region: '华东' })
63
- expect(resolveRuleCompareValue(context, rule('gt', 100, {}))).toBe(150)
64
- })
65
-
66
- it('指定 field 时取同记录另一字段', () => {
67
- const context = ctx(150, { amount: 150, region: '华东' })
68
- expect(resolveRuleCompareValue(context, rule('eq', '华东', {}, { field: 'region' }))).toBe(
69
- '华东'
70
- )
71
- })
72
- })
73
-
74
- describe('evaluateConditionalStyle', () => {
75
- const baseStyle = { fill: { color: '#FFFFFF' }, font: { color: '#000000' } }
76
-
77
- it('未命中规则时原样返回 baseStyle', () => {
78
- const rules = [rule('gt', 1000, { fill: { color: '#FF0000' } })]
79
- expect(evaluateConditionalStyle(ctx(50), baseStyle, rules)).toBe(baseStyle)
80
- })
81
-
82
- it('命中单条规则时叠加样式增量', () => {
83
- const rules = [rule('gt', 100, { fill: { color: '#FFCCCC' }, font: { bold: true } })]
84
- expect(evaluateConditionalStyle(ctx(150), baseStyle, rules)).toEqual({
85
- fill: { color: '#FFCCCC' },
86
- font: { color: '#000000', bold: true }
87
- })
88
- })
89
-
90
- it('跨字段求值命中时叠加样式', () => {
91
- const rules = [rule('eq', '华东', { fill: { color: '#FFCCCC' } }, { field: 'region' })]
92
- const context = ctx(100, { amount: 100, region: '华东' })
93
- expect(evaluateConditionalStyle(context, baseStyle, rules)?.fill?.color).toBe('#FFCCCC')
94
- })
95
-
96
- it('scope: row 仅评估行级规则', () => {
97
- const rules = [
98
- rule('gt', 100, { fill: { color: '#FFCCCC' } }, { scope: 'row' }),
99
- rule('gt', 100, { font: { bold: true } })
100
- ]
101
- expect(evaluateConditionalStyle(ctx(150), baseStyle, rules, 'row')?.fill?.color).toBe(
102
- '#FFCCCC'
103
- )
104
- expect(evaluateConditionalStyle(ctx(150), baseStyle, rules, 'cell')?.font?.bold).toBe(true)
105
- expect(evaluateConditionalStyle(ctx(150), baseStyle, rules, 'cell')?.fill?.color).toBe(
106
- '#FFFFFF'
107
- )
108
- })
109
-
110
- it('多规则按优先级合并,后命中规则覆盖同名字段', () => {
111
- const rules = [
112
- rule('gt', 0, { fill: { color: '#FFFFCC' } }),
113
- rule('gt', 100, { fill: { color: '#FFCCCC' }, font: { bold: true } }),
114
- rule('gt', 200, { font: { color: '#FF0000' } })
115
- ]
116
-
117
- const style150 = evaluateConditionalStyle(ctx(150), baseStyle, rules)
118
- expect(style150?.fill?.color).toBe('#FFCCCC')
119
- expect(style150?.font).toEqual({ color: '#000000', bold: true })
120
-
121
- const style250 = evaluateConditionalStyle(ctx(250), baseStyle, rules)
122
- expect(style250?.fill?.color).toBe('#FFCCCC')
123
- expect(style250?.font).toEqual({ color: '#FF0000', bold: true })
124
- })
125
-
126
- it('无规则或未传规则时返回 baseStyle', () => {
127
- expect(evaluateConditionalStyle(ctx(100), baseStyle, undefined)).toBe(baseStyle)
128
- expect(evaluateConditionalStyle(ctx(100), baseStyle, [])).toBe(baseStyle)
129
- })
130
- })
131
- })
@@ -1,366 +0,0 @@
1
- import { Sheet } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { REPORT_META_NAMESPACE } from '../binding'
5
- import type { DataConnection, DataConnector } from '../connector'
6
- import {
7
- assertCompatibleTemplateVersion,
8
- createReportTemplate,
9
- fetchTemplateRecords,
10
- getBoundDatasetIds,
11
- getTemplateDatasets,
12
- IncompatibleTemplateVersionError,
13
- REPORT_TEMPLATE_VERSION,
14
- resolveParamDefaults,
15
- resolveTemplateParams,
16
- type ReportDatasetDef,
17
- type ReportTemplate
18
- } from '../template'
19
- import type { ParamValues, ReportBinding } from '../types'
20
-
21
- // ---- 内联 fixtures ----
22
-
23
- const MYSQL: DataConnection = {
24
- id: 'c-mysql',
25
- label: '本地 MySQL',
26
- type: 'mysql',
27
- host: '127.0.0.1',
28
- port: 3306,
29
- database: 'demo',
30
- username: 'root',
31
- password: 'secret'
32
- }
33
-
34
- const PG: DataConnection = {
35
- ...MYSQL,
36
- id: 'c-pg',
37
- label: '本地 PG',
38
- type: 'postgresql',
39
- port: 5432
40
- }
41
-
42
- // BETWEEN ${dateRange} 置于 SQL 末尾:后随 AND 时不会推断为 date-range(params.ts 既有推断规则)
43
- const ORDERS_SQL =
44
- 'SELECT customer, region, amount, orderDate FROM orders ' +
45
- 'WHERE region = ${region} AND amount >= ${minAmount} ' +
46
- 'AND customer LIKE ${keyword} AND orderDate BETWEEN ${dateRange}'
47
-
48
- const CUSTOMERS_SQL = 'SELECT id, region FROM customers WHERE region = ${region}'
49
-
50
- const ORDERS_DATASET: ReportDatasetDef = {
51
- id: 'orders',
52
- label: '销售明细',
53
- connection: MYSQL,
54
- sql: ORDERS_SQL
55
- }
56
-
57
- const CUSTOMERS_DATASET: ReportDatasetDef = {
58
- id: 'customers',
59
- label: '客户',
60
- connection: PG,
61
- sql: CUSTOMERS_SQL,
62
- paramOverrides: {
63
- region: {
64
- label: '地区',
65
- type: 'select',
66
- defaultValue: '华东',
67
- options: [
68
- { label: '华东', value: '华东' },
69
- { label: '华南', value: '华南' }
70
- ]
71
- }
72
- }
73
- }
74
-
75
- const UNUSED_DATASET: ReportDatasetDef = {
76
- id: 'unused',
77
- label: '未绑定',
78
- connection: MYSQL,
79
- sql: 'SELECT id FROM t WHERE x = ${unusedParam}'
80
- }
81
-
82
- function binding(dataset: string, field: string): ReportBinding {
83
- return { dataset, field, aggregate: 'list', expand: 'down', preset: 'detail' }
84
- }
85
-
86
- /** 构造自包含 Report Template:SheetSnapshot + 内嵌数据集定义 */
87
- function createTemplate(
88
- datasets: ReportDatasetDef[],
89
- bindings: Array<{ row: number; col: number; dataset: string; field: string }>
90
- ): ReportTemplate {
91
- const sheet = new Sheet()
92
- for (const item of bindings) {
93
- sheet.setCellMeta(
94
- { row: item.row, col: item.col },
95
- REPORT_META_NAMESPACE,
96
- binding(item.dataset, item.field)
97
- )
98
- }
99
- return createReportTemplate(sheet.snapshot(), datasets)
100
- }
101
-
102
- describe('ReportTemplate version', () => {
103
- it('createReportTemplate 写入当前版本', () => {
104
- const template = createTemplate([ORDERS_DATASET], [])
105
- expect(template.version).toBe(REPORT_TEMPLATE_VERSION)
106
- })
107
-
108
- it('createReportTemplate 继承 snapshot.colWidths', () => {
109
- const sheet = new Sheet()
110
- sheet.setColWidth(0, 120)
111
- sheet.setColWidth(2, 90)
112
- const template = createReportTemplate(sheet.snapshot(), [ORDERS_DATASET])
113
- expect(template.colWidths).toEqual([
114
- [0, 120],
115
- [2, 90]
116
- ])
117
- })
118
-
119
- it('version 缺失时 assertCompatibleTemplateVersion 抛可读错误', () => {
120
- const sheet = new Sheet()
121
- const template = sheet.snapshot() as ReportTemplate
122
- expect(() => assertCompatibleTemplateVersion(template)).toThrow(
123
- IncompatibleTemplateVersionError
124
- )
125
- expect(() => assertCompatibleTemplateVersion(template)).toThrow(/缺少 version/)
126
- })
127
-
128
- it('version 高于当前支持时 assertCompatibleTemplateVersion 抛可读错误', () => {
129
- const template = createTemplate([ORDERS_DATASET], [])
130
- template.version = REPORT_TEMPLATE_VERSION + 1
131
- expect(() => assertCompatibleTemplateVersion(template)).toThrow(
132
- IncompatibleTemplateVersionError
133
- )
134
- expect(() => assertCompatibleTemplateVersion(template)).toThrow(/高于当前支持/)
135
- })
136
-
137
- it('version 为当前值时通过校验', () => {
138
- const template = createTemplate([ORDERS_DATASET], [])
139
- expect(() => assertCompatibleTemplateVersion(template)).not.toThrow()
140
- })
141
- })
142
-
143
- describe('getTemplateDatasets', () => {
144
- it('返回模板内嵌的数据集定义', () => {
145
- const template = createTemplate([ORDERS_DATASET], [])
146
- expect(getTemplateDatasets(template)).toEqual([ORDERS_DATASET])
147
- })
148
-
149
- it('未内嵌数据集(旧模板)回退为空数组', () => {
150
- const sheet = new Sheet()
151
- const template = sheet.snapshot() as ReportTemplate
152
- expect(getTemplateDatasets(template)).toEqual([])
153
- })
154
- })
155
-
156
- describe('getBoundDatasetIds', () => {
157
- it('按绑定出现顺序返回去重的数据集 id(绑定即真相)', () => {
158
- const template = createTemplate(
159
- [],
160
- [
161
- { row: 1, col: 0, dataset: 'orders', field: 'customer' },
162
- { row: 1, col: 1, dataset: 'orders', field: 'amount' },
163
- { row: 2, col: 0, dataset: 'customers', field: 'region' }
164
- ]
165
- )
166
- expect(getBoundDatasetIds(template)).toEqual(['orders', 'customers'])
167
- })
168
-
169
- it('无绑定时返回空数组', () => {
170
- expect(getBoundDatasetIds(createTemplate([], []))).toEqual([])
171
- })
172
- })
173
-
174
- describe('resolveTemplateParams', () => {
175
- it('从实际绑定的数据集提取参数并集(未绑定数据集的参数不出现)', () => {
176
- const template = createTemplate(
177
- [ORDERS_DATASET, CUSTOMERS_DATASET, UNUSED_DATASET],
178
- [
179
- { row: 1, col: 0, dataset: 'orders', field: 'customer' },
180
- { row: 2, col: 0, dataset: 'customers', field: 'region' }
181
- ]
182
- )
183
- const params = resolveTemplateParams(template)
184
- expect(params.map((p) => p.id)).toEqual(['region', 'minAmount', 'keyword', 'dateRange'])
185
- })
186
-
187
- it('按 SQL 上下文推断控件类型(= 数值 / LIKE 文本 / BETWEEN 单参 date-range)', () => {
188
- const template = createTemplate(
189
- [ORDERS_DATASET],
190
- [{ row: 1, col: 0, dataset: 'orders', field: 'customer' }]
191
- )
192
- const params = resolveTemplateParams(template)
193
- const byId = new Map(params.map((p) => [p.id, p]))
194
- expect(byId.get('region')?.type).toBe('number')
195
- expect(byId.get('minAmount')?.type).toBe('number')
196
- expect(byId.get('dateRange')?.type).toBe('date-range')
197
- expect(byId.get('keyword')?.type).toBe('text')
198
- })
199
-
200
- it('同名参数合并,先见数据集为准', () => {
201
- const ordersFirst = createTemplate(
202
- [ORDERS_DATASET, CUSTOMERS_DATASET],
203
- [
204
- { row: 1, col: 0, dataset: 'orders', field: 'customer' },
205
- { row: 2, col: 0, dataset: 'customers', field: 'region' }
206
- ]
207
- )
208
- const region = resolveTemplateParams(ordersFirst).find((p) => p.id === 'region')
209
- // orders 先见:无覆盖元数据,label 回退 id、类型按 orders SQL 推断
210
- expect(region?.label).toBe('region')
211
- expect(region?.type).toBe('number')
212
- })
213
-
214
- it('应用数据集参数元数据覆盖(label / 类型 / 默认值 / 选项)', () => {
215
- const template = createTemplate(
216
- [CUSTOMERS_DATASET],
217
- [{ row: 1, col: 0, dataset: 'customers', field: 'region' }]
218
- )
219
- const [region] = resolveTemplateParams(template)
220
- expect(region).toEqual({
221
- id: 'region',
222
- label: '地区',
223
- type: 'select',
224
- defaultValue: '华东',
225
- options: [
226
- { label: '华东', value: '华东' },
227
- { label: '华南', value: '华南' }
228
- ]
229
- })
230
- })
231
-
232
- it('绑定引用了模板未定义的数据集时跳过该数据集', () => {
233
- const template = createTemplate(
234
- [],
235
- [
236
- { row: 1, col: 0, dataset: 'ghost', field: 'x' },
237
- { row: 1, col: 1, dataset: 'orders', field: 'amount' }
238
- ]
239
- )
240
- // orders 未内嵌 → 也无参数;整体为空
241
- expect(resolveTemplateParams(template)).toEqual([])
242
-
243
- const withOrders = createTemplate(
244
- [ORDERS_DATASET],
245
- [
246
- { row: 1, col: 0, dataset: 'ghost', field: 'x' },
247
- { row: 1, col: 1, dataset: 'orders', field: 'amount' }
248
- ]
249
- )
250
- expect(resolveTemplateParams(withOrders).map((p) => p.id)).toEqual([
251
- 'region',
252
- 'minAmount',
253
- 'keyword',
254
- 'dateRange'
255
- ])
256
- })
257
- })
258
-
259
- describe('resolveParamDefaults', () => {
260
- it('以参数默认值生成初始运行时值', () => {
261
- const template = createTemplate(
262
- [ORDERS_DATASET, CUSTOMERS_DATASET],
263
- [
264
- { row: 1, col: 0, dataset: 'customers', field: 'region' },
265
- { row: 2, col: 0, dataset: 'orders', field: 'amount' }
266
- ]
267
- )
268
- const values = resolveParamDefaults(resolveTemplateParams(template))
269
- expect(values.region).toBe('华东')
270
- expect(values.minAmount).toBe(0)
271
- expect(values.dateRange).toEqual(['', ''])
272
- expect(values.keyword).toBe('')
273
- })
274
- })
275
-
276
- // ---- fetchTemplateRecords(stub connector:实现 DataConnector 接口的内存测试夹具)----
277
-
278
- function createStubConnector(handlers: {
279
- rowsBySql?: Record<string, Record<string, unknown>[]>
280
- errorBySql?: Record<string, { code: string; message: string }>
281
- }) {
282
- const calls: Array<{ connection: DataConnection; sql: string; values: ParamValues }> = []
283
- const connector: DataConnector = {
284
- test: () => Promise.resolve({ ok: true, data: undefined }),
285
- describe: () => Promise.resolve({ ok: true, data: [] }),
286
- query: (connection, sql, values) => {
287
- calls.push({ connection, sql, values })
288
- const error = handlers.errorBySql?.[sql]
289
- if (error) return Promise.resolve({ ok: false, error })
290
- return Promise.resolve({
291
- ok: true,
292
- data: { fields: [], rows: handlers.rowsBySql?.[sql] ?? [] }
293
- })
294
- }
295
- }
296
- return { connector, calls }
297
- }
298
-
299
- describe('fetchTemplateRecords', () => {
300
- it('按绑定的数据集逐一经连接器取数并按数据集 id 折叠行记录', async () => {
301
- const template = createTemplate(
302
- [ORDERS_DATASET, CUSTOMERS_DATASET, UNUSED_DATASET],
303
- [
304
- { row: 1, col: 0, dataset: 'orders', field: 'customer' },
305
- { row: 2, col: 0, dataset: 'customers', field: 'region' }
306
- ]
307
- )
308
- const { connector, calls } = createStubConnector({
309
- rowsBySql: {
310
- [ORDERS_SQL]: [{ customer: '甲公司', amount: 100 }],
311
- [CUSTOMERS_SQL]: [{ id: 'C-01', region: '华东' }]
312
- }
313
- })
314
- const values: ParamValues = { region: '华东' }
315
- const result = await fetchTemplateRecords(connector, template, values)
316
-
317
- expect(result).toEqual({
318
- ok: true,
319
- data: {
320
- orders: [{ customer: '甲公司', amount: 100 }],
321
- customers: [{ id: 'C-01', region: '华东' }]
322
- }
323
- })
324
- // 未绑定的数据集不取数;连接对象与参数原样透传
325
- expect(calls).toHaveLength(2)
326
- expect(calls[0]).toEqual({ connection: MYSQL, sql: ORDERS_SQL, values })
327
- expect(calls[1]).toEqual({ connection: PG, sql: CUSTOMERS_SQL, values })
328
- })
329
-
330
- it('业务错误(ok:false)原样透传', async () => {
331
- const template = createTemplate(
332
- [ORDERS_DATASET],
333
- [{ row: 1, col: 0, dataset: 'orders', field: 'customer' }]
334
- )
335
- const { connector } = createStubConnector({
336
- errorBySql: { [ORDERS_SQL]: { code: 'SQL_ERROR', message: '语法错误 near WHERE' } }
337
- })
338
- const result = await fetchTemplateRecords(connector, template, {})
339
- expect(result).toEqual({
340
- ok: false,
341
- error: { code: 'SQL_ERROR', message: '语法错误 near WHERE' }
342
- })
343
- })
344
-
345
- it('跳过模板未定义的绑定数据集', async () => {
346
- const template = createTemplate(
347
- [ORDERS_DATASET],
348
- [
349
- { row: 1, col: 0, dataset: 'ghost', field: 'x' },
350
- { row: 1, col: 1, dataset: 'orders', field: 'amount' }
351
- ]
352
- )
353
- const { connector, calls } = createStubConnector({ rowsBySql: { [ORDERS_SQL]: [] } })
354
- const result = await fetchTemplateRecords(connector, template, {})
355
- expect(result).toEqual({ ok: true, data: { orders: [] } })
356
- expect(calls).toHaveLength(1)
357
- })
358
-
359
- it('无绑定数据集时不取数,返回空记录集', async () => {
360
- const template = createTemplate([ORDERS_DATASET], [])
361
- const { connector, calls } = createStubConnector({})
362
- const result = await fetchTemplateRecords(connector, template, {})
363
- expect(result).toEqual({ ok: true, data: {} })
364
- expect(calls).toHaveLength(0)
365
- })
366
- })