@veltra/sheet 2.4.0 → 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.
- package/dist/index.d.ts +2 -19
- package/dist/index.js +2 -15
- package/dist/types/index.d.ts +1 -3
- package/package.json +2 -3
- package/src/index.ts +1 -27
- package/src/types/index.ts +0 -2
- package/dist/components/report/binding-badge.js +0 -118
- package/dist/components/report/designer/cell-coords.js +0 -46
- package/dist/components/report/designer/col-widths.js +0 -29
- package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
- package/dist/components/report/designer/drill-dialog.js +0 -258
- package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
- package/dist/components/report/designer/float-panel.js +0 -434
- package/dist/components/report/designer/role.js +0 -17
- package/dist/components/report/designer/rule-preview.js +0 -33
- package/dist/components/report/designer/rule-row.js +0 -253
- package/dist/components/report/designer/rules-dialog.js +0 -118
- package/dist/components/report/designer/topology-overlay.js +0 -122
- package/dist/components/report/designer/topology.js +0 -64
- package/dist/components/report/designer/use-grid-overlay.js +0 -75
- package/dist/components/report/designer-hub.js +0 -177
- package/dist/components/report/field-panel-helpers.js +0 -33
- package/dist/components/report/field-panel.js +0 -132
- package/dist/components/report/filter-bar.js +0 -74
- package/dist/components/report/hub-connection-form.js +0 -145
- package/dist/components/report/hub-dataset-editor.js +0 -409
- package/dist/components/report/preview-grid-size.js +0 -32
- package/dist/components/report/print.js +0 -39
- package/dist/components/report/report-designer.js +0 -428
- package/dist/components/report/report-viewer.js +0 -253
- package/dist/components/report/style.d.ts +0 -17
- package/dist/components/report/style.js +0 -18
- package/dist/components/report/style2.css +0 -965
- package/dist/components/report/use-report-designer.js +0 -387
- package/dist/components/report/use-report-viewer.js +0 -140
- package/dist/report/binding.d.ts +0 -83
- package/dist/report/binding.js +0 -334
- package/dist/report/connector.d.ts +0 -66
- package/dist/report/connector.js +0 -159
- package/dist/report/drill.d.ts +0 -48
- package/dist/report/drill.js +0 -78
- package/dist/report/export-xlsx.d.ts +0 -13
- package/dist/report/export-xlsx.js +0 -13
- package/dist/report/filter-bar.d.ts +0 -10
- package/dist/report/filter-bar.js +0 -31
- package/dist/report/index.d.ts +0 -14
- package/dist/report/params.d.ts +0 -13
- package/dist/report/params.js +0 -61
- package/dist/report/print.d.ts +0 -23
- package/dist/report/print.js +0 -197
- package/dist/report/render/aggregate.js +0 -193
- package/dist/report/render/builder.d.ts +0 -12
- package/dist/report/render/builder.js +0 -139
- package/dist/report/render/coordinate.d.ts +0 -25
- package/dist/report/render/coordinate.js +0 -733
- package/dist/report/render/index.d.ts +0 -14
- package/dist/report/render/index.js +0 -21
- package/dist/report/render/style-resolver.js +0 -50
- package/dist/report/render/template-index.d.ts +0 -24
- package/dist/report/render/template-index.js +0 -137
- package/dist/report/rules/evaluate.d.ts +0 -25
- package/dist/report/rules/evaluate.js +0 -82
- package/dist/report/rules/index.d.ts +0 -2
- package/dist/report/template.d.ts +0 -63
- package/dist/report/template.js +0 -83
- package/dist/report/types.d.ts +0 -103
- package/dist/types/report-designer.d.ts +0 -51
- package/dist/types/report-viewer.d.ts +0 -57
- package/src/components/report/__test__/binding-badge.test.ts +0 -92
- package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
- package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
- package/src/components/report/__test__/float-panel-position.test.ts +0 -45
- package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
- package/src/components/report/__test__/report-designer.test.ts +0 -559
- package/src/components/report/__test__/report-drill.test.ts +0 -692
- package/src/components/report/__test__/report-viewer.test.ts +0 -400
- package/src/components/report/__test__/topology.test.ts +0 -143
- package/src/components/report/__test__/use-report-designer.test.ts +0 -804
- package/src/components/report/binding-badge.ts +0 -122
- package/src/components/report/designer/cell-coords.ts +0 -95
- package/src/components/report/designer/col-widths.ts +0 -56
- package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
- package/src/components/report/designer/drill-dialog.vue +0 -245
- package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
- package/src/components/report/designer/float-panel.vue +0 -444
- package/src/components/report/designer/role.ts +0 -32
- package/src/components/report/designer/rule-preview.vue +0 -40
- package/src/components/report/designer/rule-row.vue +0 -252
- package/src/components/report/designer/rules-dialog.vue +0 -110
- package/src/components/report/designer/topology-overlay.vue +0 -128
- package/src/components/report/designer/topology.ts +0 -124
- package/src/components/report/designer/use-grid-overlay.ts +0 -114
- package/src/components/report/designer-hub.vue +0 -198
- package/src/components/report/field-panel-helpers.ts +0 -44
- package/src/components/report/field-panel.vue +0 -136
- package/src/components/report/filter-bar.vue +0 -77
- package/src/components/report/hub-connection-form.vue +0 -124
- package/src/components/report/hub-dataset-editor.vue +0 -387
- package/src/components/report/index.ts +0 -2
- package/src/components/report/preview-grid-size.ts +0 -33
- package/src/components/report/print.ts +0 -44
- package/src/components/report/report-designer.vue +0 -454
- package/src/components/report/report-viewer.vue +0 -289
- package/src/components/report/style-designer.scss +0 -653
- package/src/components/report/style-hub.scss +0 -292
- package/src/components/report/style-viewer.scss +0 -144
- package/src/components/report/style.scss +0 -3
- package/src/components/report/style.ts +0 -20
- package/src/components/report/use-report-designer.ts +0 -603
- package/src/components/report/use-report-viewer.ts +0 -227
- package/src/report/__test__/binding.test.ts +0 -320
- package/src/report/__test__/connector.test.ts +0 -239
- package/src/report/__test__/drill.test.ts +0 -194
- package/src/report/__test__/export-xlsx.test.ts +0 -96
- package/src/report/__test__/filter-bar.test.ts +0 -58
- package/src/report/__test__/params.test.ts +0 -156
- package/src/report/__test__/print.test.ts +0 -170
- package/src/report/__test__/render.test.ts +0 -856
- package/src/report/__test__/rules.test.ts +0 -131
- package/src/report/__test__/template.test.ts +0 -366
- package/src/report/binding.ts +0 -474
- package/src/report/connector.ts +0 -206
- package/src/report/drill.ts +0 -112
- package/src/report/export-xlsx.ts +0 -15
- package/src/report/filter-bar.ts +0 -36
- package/src/report/index.ts +0 -16
- package/src/report/params.ts +0 -96
- package/src/report/print.ts +0 -234
- package/src/report/render/__test__/aggregate.test.ts +0 -16
- package/src/report/render/__test__/coordinate.test.ts +0 -405
- package/src/report/render/aggregate.ts +0 -289
- package/src/report/render/builder.ts +0 -211
- package/src/report/render/coordinate.ts +0 -1239
- package/src/report/render/index.ts +0 -30
- package/src/report/render/style-resolver.ts +0 -68
- package/src/report/render/template-index.ts +0 -200
- package/src/report/rules/evaluate.ts +0 -113
- package/src/report/rules/index.ts +0 -7
- package/src/report/template.ts +0 -145
- package/src/report/types.ts +0 -116
- package/src/types/report-designer.ts +0 -55
- 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
|
-
})
|