@veltra/sheet 2.8.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/sheet/formula-bar.js +1 -1
- package/dist/components/sheet/import-file.js +1 -1
- package/dist/components/sheet/insert-image.js +1 -1
- package/dist/components/sheet/popup-helpers.js +1 -1
- package/dist/components/sheet/popups/border-popup.js +1 -2
- package/dist/components/sheet/popups/functions-popup.js +3 -3
- package/dist/components/sheet/popups/import.worker.d.ts +1 -1
- package/dist/components/sheet/popups/import.worker.js +1 -1
- package/dist/components/sheet/sheet-context-menu.js +2 -2
- package/dist/components/sheet/sheet.js +12 -2
- package/dist/components/sheet/use-find-replace.js +1 -2
- package/dist/components/sheet/use-formula-suggest.js +5 -4
- package/dist/components/sheet/use-sheet-grid.js +4 -2
- package/dist/components/sheet/use-sheet-state.js +1 -1
- package/dist/components/sheet/use-tool-popup.js +13 -2
- package/dist/index.d.ts +2 -2
- package/dist/tools/builtin.js +1 -1
- package/dist/tools/context.d.ts +1 -11
- package/dist/tools/download.js +3 -3
- package/dist/tools/export.worker.d.ts +1 -1
- package/dist/tools/export.worker.js +1 -1
- package/dist/tools/registry.js +1 -1
- package/dist/types/sheet.d.ts +15 -7
- package/package.json +2 -3
- package/src/components/sheet/__test__/canvas-mock.ts +49 -0
- package/src/components/sheet/__test__/find-shortcut-anchor.test.ts +117 -0
- package/src/components/sheet/__test__/formula-bar.test.ts +11 -9
- package/src/components/sheet/__test__/formula-capability.test.ts +160 -0
- package/src/components/sheet/__test__/functions-popup.test.ts +7 -7
- package/src/components/sheet/__test__/grid-setup.ts +3 -0
- package/src/components/sheet/__test__/grid-test-utils.ts +47 -0
- package/src/components/sheet/__test__/import-file.test.ts +11 -7
- package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
- package/src/components/sheet/__test__/insert-image.test.ts +1 -1
- package/src/components/sheet/__test__/sheet-component.test.ts +9 -19
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +2 -2
- package/src/components/sheet/__test__/sheet-grow-on-scroll.test.ts +105 -0
- package/src/components/sheet/__test__/sheet-scrollbar.test.ts +137 -0
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +1 -1
- package/src/components/sheet/formula-bar.vue +4 -4
- package/src/components/sheet/import-file.ts +3 -7
- package/src/components/sheet/insert-image.ts +2 -2
- package/src/components/sheet/popup-helpers.ts +3 -3
- package/src/components/sheet/popups/border-popup.vue +4 -4
- package/src/components/sheet/popups/export-popup.vue +1 -1
- package/src/components/sheet/popups/find-popup.vue +1 -1
- package/src/components/sheet/popups/functions-popup.vue +3 -9
- package/src/components/sheet/popups/import.worker.ts +3 -3
- package/src/components/sheet/sheet-context-menu.ts +4 -4
- package/src/components/sheet/sheet-tabs.vue +2 -2
- package/src/components/sheet/sheet.vue +12 -3
- package/src/components/sheet/use-find-replace.ts +3 -7
- package/src/components/sheet/use-formula-suggest.ts +6 -8
- package/src/components/sheet/use-sheet-grid.ts +6 -4
- package/src/components/sheet/use-sheet-state.ts +2 -2
- package/src/components/sheet/use-tool-popup.ts +22 -2
- package/src/index.ts +3 -1
- package/src/tools/__test__/apply-style.test.ts +2 -2
- package/src/tools/__test__/builtin.test.ts +4 -4
- package/src/tools/__test__/context.test.ts +2 -2
- package/src/tools/apply-style.ts +3 -3
- package/src/tools/builtin.ts +2 -2
- package/src/tools/context.ts +11 -11
- package/src/tools/download.ts +4 -4
- package/src/tools/export.worker.ts +4 -7
- package/src/tools/registry.ts +1 -1
- package/src/types/sheet.ts +19 -8
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 公式能力可达性(spec 第 4 项 / 下游 meta 报表合计场景):
|
|
3
|
+
* 写公式(setCellFormula / setCellValue '=' 前缀)、读公式(getCellData().f)、
|
|
4
|
+
* 求值(getDisplayValue)、自定义函数注册(infinitable 主入口)、
|
|
5
|
+
* 数据填充后自动重算——全部经 USheet 门面路径(getContext() 的 SheetContext)。
|
|
6
|
+
*/
|
|
7
|
+
import { registerFormulaFunction } from 'infinitable'
|
|
8
|
+
import { Workbook } from 'infinitable/sheet'
|
|
9
|
+
import { afterEach, describe, expect, it } from 'vite-plus/test'
|
|
10
|
+
import { createApp, h, nextTick, type App } from 'vue'
|
|
11
|
+
|
|
12
|
+
import { USheet } from '../../../index'
|
|
13
|
+
import type { SheetContext } from '../../../tools/context'
|
|
14
|
+
import type { SheetExposed } from '../../../types'
|
|
15
|
+
import USheetFunctionsPopup from '../popups/functions-popup.vue'
|
|
16
|
+
import { filterFormulaSuggestions } from '../use-formula-suggest'
|
|
17
|
+
|
|
18
|
+
const apps: App[] = []
|
|
19
|
+
const containers: HTMLElement[] = []
|
|
20
|
+
|
|
21
|
+
/** 挂载 USheet(真实组件 + 内部自建工作簿),返回活动门面 SheetContext */
|
|
22
|
+
function mountSheet(): { getContext: () => SheetContext } {
|
|
23
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
24
|
+
const el = document.createElement('div')
|
|
25
|
+
el.style.width = '800px'
|
|
26
|
+
el.style.height = '600px'
|
|
27
|
+
document.body.appendChild(el)
|
|
28
|
+
containers.push(el)
|
|
29
|
+
const app = createApp({
|
|
30
|
+
render: () =>
|
|
31
|
+
h(USheet, {
|
|
32
|
+
workbook: new Workbook(),
|
|
33
|
+
rows: 10,
|
|
34
|
+
cols: 6,
|
|
35
|
+
ref: (value: unknown) => {
|
|
36
|
+
exposed.value = value as SheetExposed | undefined
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
app.mount(el)
|
|
41
|
+
apps.push(app)
|
|
42
|
+
return { getContext: () => exposed.value!.getContext() }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 直接挂载函数弹框(与工具栏「函数」按钮 / fx 按钮同一组件) */
|
|
46
|
+
function mountFunctionsPopup(): HTMLElement {
|
|
47
|
+
const el = document.createElement('div')
|
|
48
|
+
document.body.appendChild(el)
|
|
49
|
+
containers.push(el)
|
|
50
|
+
const app = createApp({ render: () => h(USheetFunctionsPopup) })
|
|
51
|
+
app.mount(el)
|
|
52
|
+
apps.push(app)
|
|
53
|
+
return el
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 点击左侧分类导航项(等一帧渲染),与 functions-popup.test.ts 同一交互 */
|
|
57
|
+
async function clickCategory(el: HTMLElement, category: string): Promise<void> {
|
|
58
|
+
const item = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__functions-nav-item')].find(
|
|
59
|
+
(button) => button.textContent?.trim() === category
|
|
60
|
+
)
|
|
61
|
+
item!.click()
|
|
62
|
+
await nextTick()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 宿主自定义函数(消费方经 `infinitable` 主入口注册,@veltra/sheet 不转售):
|
|
67
|
+
* 数值参数与区域数值求和——报表「合计」类函数的最小形态。
|
|
68
|
+
*/
|
|
69
|
+
function registerCustomTotal(): void {
|
|
70
|
+
registerFormulaFunction('VELTRATOTAL', {
|
|
71
|
+
meta: {
|
|
72
|
+
params: [{ name: 'number1' }, { name: 'number2', optional: true }, { name: '...' }],
|
|
73
|
+
description: '测试用自定义求和(公式能力可达性)',
|
|
74
|
+
category: '数学'
|
|
75
|
+
},
|
|
76
|
+
impl(args) {
|
|
77
|
+
let total = 0
|
|
78
|
+
for (const arg of args) {
|
|
79
|
+
if (Array.isArray(arg)) {
|
|
80
|
+
for (const item of arg) if (typeof item === 'number') total += item
|
|
81
|
+
} else if (typeof arg === 'number') {
|
|
82
|
+
total += arg
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return total
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
while (apps.length) apps.pop()!.unmount()
|
|
92
|
+
while (containers.length) containers.pop()!.remove()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('公式能力可达性(USheet 门面路径)', () => {
|
|
96
|
+
it('setCellFormula 写入 SUM:getDisplayValue 得计算值;getCellData().f 读回公式(不含 = 前缀)', async () => {
|
|
97
|
+
const { getContext } = mountSheet()
|
|
98
|
+
await nextTick()
|
|
99
|
+
const ctx = getContext()
|
|
100
|
+
|
|
101
|
+
ctx.setCellValue({ row: 0, col: 1 }, 10)
|
|
102
|
+
ctx.setCellValue({ row: 1, col: 1 }, 20)
|
|
103
|
+
ctx.setCellValue({ row: 2, col: 1 }, 30)
|
|
104
|
+
ctx.setCellFormula({ row: 0, col: 0 }, '=SUM(B1:B3)')
|
|
105
|
+
|
|
106
|
+
expect(ctx.getDisplayValue({ row: 0, col: 0 })).toBe(60)
|
|
107
|
+
expect(ctx.getCellData({ row: 0, col: 0 })?.f).toBe('SUM(B1:B3)')
|
|
108
|
+
|
|
109
|
+
// setCellValue 的 '=' 前缀同样进公式通道(报表配置的另一种写法)
|
|
110
|
+
ctx.setCellValue({ row: 1, col: 0 }, '=SUM(B1:B2)')
|
|
111
|
+
expect(ctx.getDisplayValue({ row: 1, col: 0 })).toBe(30)
|
|
112
|
+
expect(ctx.getCellData({ row: 1, col: 0 })?.f).toBe('SUM(B1:B2)')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('批量 setCells 填充被引用区域后公式自动重算(命令执行后引擎增量重算,无需手动刷新)', async () => {
|
|
116
|
+
const { getContext } = mountSheet()
|
|
117
|
+
await nextTick()
|
|
118
|
+
const ctx = getContext()
|
|
119
|
+
|
|
120
|
+
// 先写公式:被引用区域为空 → SUM 空区域为 0
|
|
121
|
+
ctx.setCellFormula({ row: 0, col: 0 }, '=SUM(B1:B3)')
|
|
122
|
+
expect(ctx.getDisplayValue({ row: 0, col: 0 })).toBe(0)
|
|
123
|
+
|
|
124
|
+
// 报表数据填充:一次 setCells = 一个 undo 单元,填充后合计自动得出
|
|
125
|
+
ctx.setCells([
|
|
126
|
+
{ addr: { row: 0, col: 1 }, data: { v: 10, t: 'n' } },
|
|
127
|
+
{ addr: { row: 1, col: 1 }, data: { v: 20, t: 'n' } },
|
|
128
|
+
{ addr: { row: 2, col: 1 }, data: { v: 30, t: 'n' } }
|
|
129
|
+
])
|
|
130
|
+
expect(ctx.getDisplayValue({ row: 0, col: 0 })).toBe(60)
|
|
131
|
+
|
|
132
|
+
// 修改被引用单元格 → 依赖格重算
|
|
133
|
+
ctx.setCellValue({ row: 1, col: 1 }, 25)
|
|
134
|
+
expect(ctx.getDisplayValue({ row: 0, col: 0 })).toBe(65)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('registerFormulaFunction(infinitable 主入口)注册的自定义函数:fx 补全与函数弹框可达、公式求值可用', async () => {
|
|
138
|
+
registerCustomTotal()
|
|
139
|
+
|
|
140
|
+
// fx 输入栏补全列表按前缀命中自定义函数
|
|
141
|
+
expect(filterFormulaSuggestions('VELTRATOTA').map((item) => item.name)).toEqual(['VELTRATOTAL'])
|
|
142
|
+
|
|
143
|
+
// 函数弹框「全部」列出其签名(与内置函数同一列表)
|
|
144
|
+
const popup = mountFunctionsPopup()
|
|
145
|
+
await clickCategory(popup, '全部')
|
|
146
|
+
const signatures = [...popup.querySelectorAll('.u-sheet__functions-signature')].map(
|
|
147
|
+
(node) => node.textContent
|
|
148
|
+
)
|
|
149
|
+
expect(signatures).toContain('VELTRATOTAL(number1, [number2], ...)')
|
|
150
|
+
|
|
151
|
+
// 求值:区域参数 + 标量参数混合(args 收到 [数组, 数值])
|
|
152
|
+
const { getContext } = mountSheet()
|
|
153
|
+
await nextTick()
|
|
154
|
+
const ctx = getContext()
|
|
155
|
+
ctx.setCellValue({ row: 0, col: 1 }, 10)
|
|
156
|
+
ctx.setCellValue({ row: 1, col: 1 }, 20)
|
|
157
|
+
ctx.setCellFormula({ row: 0, col: 0 }, '=VELTRATOTAL(B1:B2, 5)')
|
|
158
|
+
expect(ctx.getDisplayValue({ row: 0, col: 0 })).toBe(35)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
listFormulaFunctions,
|
|
3
|
-
registerFormulaFunction
|
|
4
|
-
} from '@veltra/sheet-core/core/formula/functions.js'
|
|
1
|
+
import { listFormulaFunctions, registerFormulaFunction } from 'infinitable'
|
|
5
2
|
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
6
3
|
import { createApp, h, nextTick, type App } from 'vue'
|
|
7
4
|
|
|
@@ -97,7 +94,7 @@ describe('USheetFunctionsPopup', () => {
|
|
|
97
94
|
|
|
98
95
|
it('「全部」列出全部已注册函数;分类项只列声明该分类的函数', async () => {
|
|
99
96
|
registerFormulaFunction('MYTESTFN', {
|
|
100
|
-
meta: { params: ['value'], description: '测试未分类自定义函数' },
|
|
97
|
+
meta: { params: [{ name: 'value' }], description: '测试未分类自定义函数' },
|
|
101
98
|
impl: () => 1
|
|
102
99
|
})
|
|
103
100
|
const { el } = mountPopup()
|
|
@@ -110,9 +107,12 @@ describe('USheetFunctionsPopup', () => {
|
|
|
110
107
|
await clickCategory(el, '数学')
|
|
111
108
|
const expected = listFormulaFunctions().filter((fn) => fn.category === '数学')
|
|
112
109
|
expect(items(el).length).toBe(expected.length)
|
|
113
|
-
expect(signatures(el)).toContain('SUM(number1, number2, ...)')
|
|
114
110
|
expect(signatures(el)).not.toContain('MYTESTFN(value)')
|
|
115
111
|
|
|
112
|
+
// SUM 在官方注册表归「常用」分类
|
|
113
|
+
await clickCategory(el, '常用')
|
|
114
|
+
expect(signatures(el)).toContain('SUM(number1, [number2], ...)')
|
|
115
|
+
|
|
116
116
|
// 「常用」也不含未分类自定义函数
|
|
117
117
|
await clickCategory(el, '常用')
|
|
118
118
|
expect(signatures(el)).not.toContain('MYTESTFN(value)')
|
|
@@ -120,7 +120,7 @@ describe('USheetFunctionsPopup', () => {
|
|
|
120
120
|
|
|
121
121
|
it('搜索跨分类(含未分类函数):名称 / 描述大小写不敏感;无匹配显空态', async () => {
|
|
122
122
|
registerFormulaFunction('MYTESTFN', {
|
|
123
|
-
meta: { params: ['value'], description: '测试未分类自定义函数' },
|
|
123
|
+
meta: { params: [{ name: 'value' }], description: '测试未分类自定义函数' },
|
|
124
124
|
impl: () => 1
|
|
125
125
|
})
|
|
126
126
|
const { el } = mountPopup()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SheetGrid 测试几何与事件工具。几何口径与官方 `infinitable/sheet` 的
|
|
3
|
+
* grid-theme 取值一致:行号列宽 46、列头高 28、列宽 80、行高 28。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ROW_HEADER_WIDTH = 46
|
|
7
|
+
const HEADER_HEIGHT = 28
|
|
8
|
+
const DEFAULT_COL_WIDTH = 80
|
|
9
|
+
const DEFAULT_ROW_HEIGHT = 28
|
|
10
|
+
|
|
11
|
+
/** 数据格 (col, row) 的层坐标(+5 落在格内而非边线上) */
|
|
12
|
+
export const cellX = (col: number) => ROW_HEADER_WIDTH + col * DEFAULT_COL_WIDTH + 5
|
|
13
|
+
export const cellY = (row: number) => HEADER_HEIGHT + row * DEFAULT_ROW_HEIGHT + 5
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 引擎指针事件派发目标:body 层画布。引擎事件源在 canvas 档是挂载容器、
|
|
17
|
+
* 原生档(scrollbar.mode: 'native')是滚动容器内的 sticky 视口——body 画布
|
|
18
|
+
* 在两档下都是其后代,从它派发(冒泡)对两种形态都到达引擎监听。
|
|
19
|
+
*/
|
|
20
|
+
export function gridEventTarget(root: HTMLElement): HTMLElement {
|
|
21
|
+
return root.querySelector<HTMLElement>(".u-sheet__grid-instance canvas[data-layer-kind='body']")!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 向容器派发 DOM 指针类事件(引擎 EventSystem 归一化为场景事件) */
|
|
25
|
+
export function fire(
|
|
26
|
+
container: HTMLElement,
|
|
27
|
+
type: string,
|
|
28
|
+
init: {
|
|
29
|
+
clientX?: number
|
|
30
|
+
clientY?: number
|
|
31
|
+
button?: number
|
|
32
|
+
bubbles?: boolean
|
|
33
|
+
ctrlKey?: boolean
|
|
34
|
+
shiftKey?: boolean
|
|
35
|
+
} = {}
|
|
36
|
+
): void {
|
|
37
|
+
const event = new MouseEvent(type, {
|
|
38
|
+
bubbles: init.bubbles ?? true,
|
|
39
|
+
cancelable: true,
|
|
40
|
+
clientX: init.clientX ?? 0,
|
|
41
|
+
clientY: init.clientY ?? 0,
|
|
42
|
+
button: init.button ?? 0,
|
|
43
|
+
ctrlKey: init.ctrlKey ?? false,
|
|
44
|
+
shiftKey: init.shiftKey ?? false
|
|
45
|
+
})
|
|
46
|
+
container.dispatchEvent(event)
|
|
47
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Sheet } from '
|
|
2
|
-
import { Workbook } from '
|
|
1
|
+
import type { Sheet } from 'infinitable/sheet'
|
|
2
|
+
import { Workbook } from 'infinitable/sheet'
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
4
4
|
import { ref } from 'vue'
|
|
5
5
|
|
|
@@ -36,11 +36,15 @@ const EMPTY_SNAPSHOT = {
|
|
|
36
36
|
|
|
37
37
|
vi.mock('@veltra/desktop', () => ({ message: mocks.message, messageConfirm: mocks.messageConfirm }))
|
|
38
38
|
|
|
39
|
-
vi.mock('
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
vi.mock('infinitable/sheet', async (importActual) => {
|
|
40
|
+
const actual = await importActual<typeof import('infinitable/sheet')>()
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
importCsv: mocks.importCsv,
|
|
44
|
+
importXlsx: mocks.importXlsx,
|
|
45
|
+
replaceWorkbookWithSnapshots: mocks.replaceWorkbookWithSnapshots
|
|
46
|
+
}
|
|
47
|
+
})
|
|
44
48
|
|
|
45
49
|
function makeOptions(
|
|
46
50
|
overrides: {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createRange } from '
|
|
2
|
-
import { Workbook } from '
|
|
1
|
+
import { createRange } from 'infinitable/sheet'
|
|
2
|
+
import { Workbook } from 'infinitable/sheet'
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
4
4
|
import { createApp, h, nextTick, ref, type App, type Component } from 'vue'
|
|
5
5
|
|
|
6
|
-
// 引擎指针事件模拟与画布几何(cellX/cellY):setup 已跨包引用同目录 setup.ts
|
|
7
|
-
import { cellX, cellY, fire } from '../../../../../sheet-core/src/grid/__test__/grid-test-utils'
|
|
8
6
|
import { USheet } from '../../../index'
|
|
9
7
|
import { registerTool, unregisterTool } from '../../../tools/registry'
|
|
10
8
|
import type { SheetExposed } from '../../../types'
|
|
11
9
|
import { buildColHeaderMenus, buildRowHeaderMenus } from '../sheet-context-menu'
|
|
10
|
+
// 引擎指针事件模拟与画布几何(cellX/cellY):setup 已跨包引用同目录 setup.ts
|
|
11
|
+
import { cellX, cellY, fire, gridEventTarget } from './grid-test-utils'
|
|
12
12
|
|
|
13
13
|
const apps: App[] = []
|
|
14
14
|
const containers: HTMLElement[] = []
|
|
@@ -84,18 +84,8 @@ afterEach(() => {
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
describe('USheet 组件', () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
mount(() => ({}), exposedOn)
|
|
90
|
-
await nextTick()
|
|
91
|
-
// 引擎缺省 true(undefined 即开启),无需显式下发
|
|
92
|
-
expect(exposedOn.value!.getGrid()!.getTable().options.scrollbar).toBeUndefined()
|
|
93
|
-
|
|
94
|
-
const exposedOff: { value: SheetExposed | undefined } = { value: undefined }
|
|
95
|
-
mount(() => ({ scrollbar: false }), exposedOff)
|
|
96
|
-
await nextTick()
|
|
97
|
-
expect(exposedOff.value!.getGrid()!.getTable().options.scrollbar).toBe(false)
|
|
98
|
-
})
|
|
87
|
+
// scrollbar 形态与语义(缺省原生档 / false / true / 对象透传 / watch 重建)见
|
|
88
|
+
// 同目录 sheet-scrollbar.test.ts
|
|
99
89
|
|
|
100
90
|
it('挂载后默认选区 A1:名称框显示 A1、fx 输入栏可用', async () => {
|
|
101
91
|
const workbook = createWorkbook()
|
|
@@ -1214,7 +1204,7 @@ describe('USheet colResize(readonly 列宽拖拽)', () => {
|
|
|
1214
1204
|
await nextTick()
|
|
1215
1205
|
|
|
1216
1206
|
// 第 0 列右缘(46 + 80 = 126,±4 手柄区)列头带内拖拽:宽 = 起始 80 + 指针位移 42
|
|
1217
|
-
const instance = el
|
|
1207
|
+
const instance = gridEventTarget(el)
|
|
1218
1208
|
fire(instance, 'pointerdown', { clientX: 124, clientY: 10 })
|
|
1219
1209
|
fire(instance, 'pointermove', { clientX: 166, clientY: 10 })
|
|
1220
1210
|
fire(instance, 'pointerup', { clientX: 166, clientY: 10 })
|
|
@@ -1235,7 +1225,7 @@ describe('USheet colResize(readonly 列宽拖拽)', () => {
|
|
|
1235
1225
|
}))
|
|
1236
1226
|
await nextTick()
|
|
1237
1227
|
|
|
1238
|
-
const instance = el
|
|
1228
|
+
const instance = gridEventTarget(el)
|
|
1239
1229
|
fire(instance, 'pointerdown', { clientX: 124, clientY: 10 })
|
|
1240
1230
|
fire(instance, 'pointermove', { clientX: 166, clientY: 10 })
|
|
1241
1231
|
fire(instance, 'pointerup', { clientX: 166, clientY: 10 })
|
|
@@ -1277,7 +1267,7 @@ describe('fx 选区锚点', () => {
|
|
|
1277
1267
|
fxInput.value = '=SUM('
|
|
1278
1268
|
fxInput.setSelectionRange(5, 5)
|
|
1279
1269
|
fxInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1280
|
-
const instance = el
|
|
1270
|
+
const instance = gridEventTarget(el)
|
|
1281
1271
|
fire(instance, 'pointerdown', { clientX: cellX(1), clientY: cellY(1) })
|
|
1282
1272
|
fire(instance, 'pointermove', { clientX: cellX(3), clientY: cellY(3) })
|
|
1283
1273
|
fire(instance, 'pointerup', { clientX: cellX(3), clientY: cellY(3) })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createRange } from '
|
|
2
|
-
import { Sheet } from '
|
|
1
|
+
import { createRange } from 'infinitable/sheet'
|
|
2
|
+
import { Sheet } from 'infinitable/sheet'
|
|
3
3
|
import { describe, expect, it } from 'vite-plus/test'
|
|
4
4
|
|
|
5
5
|
import { createSheetContext } from '../../../tools/context'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Workbook } from 'infinitable/sheet'
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vite-plus/test'
|
|
3
|
+
import { createApp, h, nextTick, ref, type App } from 'vue'
|
|
4
|
+
|
|
5
|
+
import { USheet } from '../../../index'
|
|
6
|
+
import type { SheetExposed } from '../../../types'
|
|
7
|
+
|
|
8
|
+
const apps: App[] = []
|
|
9
|
+
const containers: HTMLElement[] = []
|
|
10
|
+
|
|
11
|
+
function mount(props: () => Record<string, unknown>): {
|
|
12
|
+
exposed: { value: SheetExposed | undefined }
|
|
13
|
+
el: HTMLElement
|
|
14
|
+
} {
|
|
15
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
el.style.width = '800px'
|
|
18
|
+
el.style.height = '600px'
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
containers.push(el)
|
|
21
|
+
const app = createApp({
|
|
22
|
+
render: () =>
|
|
23
|
+
h(USheet, {
|
|
24
|
+
...props(),
|
|
25
|
+
ref: (value: unknown) => {
|
|
26
|
+
exposed.value = value as SheetExposed | undefined
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
app.mount(el)
|
|
31
|
+
apps.push(app)
|
|
32
|
+
return { exposed, el }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 滚动帧经 requestAnimationFrame 派发(引擎 host.requestFrame):
|
|
37
|
+
* 等待 n 帧确保 onScrollFrame → growOnNearEdge 已执行
|
|
38
|
+
*/
|
|
39
|
+
async function nextFrames(count: number): Promise<void> {
|
|
40
|
+
for (let i = 0; i < count; i++) {
|
|
41
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 滚动到水平最末端(引擎按内容宽度钳制到 maxLeft,可视缓冲触到当前列数) */
|
|
46
|
+
function scrollToLeftEnd(exposed: { value: SheetExposed | undefined }): void {
|
|
47
|
+
exposed.value!.getGrid()!.getTable().setScrollLeft(Number.MAX_SAFE_INTEGER)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
while (apps.length) apps.pop()!.unmount()
|
|
52
|
+
while (containers.length) containers.pop()!.remove()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('USheet growOnScroll(近边增长开关透传)', () => {
|
|
56
|
+
it('缺省:不传保持引擎缺省(true),滚动触界后模型列数扩容', async () => {
|
|
57
|
+
const { exposed } = mount(() => ({ workbook: new Workbook(), rows: 10, cols: 26 }))
|
|
58
|
+
await nextTick()
|
|
59
|
+
|
|
60
|
+
const sheet = exposed.value!.getActiveSheet()
|
|
61
|
+
expect(sheet.cols).toBe(26)
|
|
62
|
+
|
|
63
|
+
scrollToLeftEnd(exposed)
|
|
64
|
+
await nextFrames(3)
|
|
65
|
+
// 引擎 growOnNearEdge:可视缓冲触到 26 列 → 模型与引擎同步扩容
|
|
66
|
+
expect(sheet.cols).toBeGreaterThan(26)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('growOnScroll: false:固定尺寸网格,滚动触界不扩容', async () => {
|
|
70
|
+
const { exposed } = mount(() => ({
|
|
71
|
+
workbook: new Workbook(),
|
|
72
|
+
rows: 10,
|
|
73
|
+
cols: 26,
|
|
74
|
+
growOnScroll: false
|
|
75
|
+
}))
|
|
76
|
+
await nextTick()
|
|
77
|
+
|
|
78
|
+
const sheet = exposed.value!.getActiveSheet()
|
|
79
|
+
scrollToLeftEnd(exposed)
|
|
80
|
+
await nextFrames(3)
|
|
81
|
+
expect(sheet.cols).toBe(26)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('watch 重建:切换 growOnScroll 触发网格重建', async () => {
|
|
85
|
+
const state = ref<boolean | undefined>(false)
|
|
86
|
+
const { exposed } = mount(() => ({
|
|
87
|
+
workbook: new Workbook(),
|
|
88
|
+
rows: 10,
|
|
89
|
+
cols: 26,
|
|
90
|
+
growOnScroll: state.value
|
|
91
|
+
}))
|
|
92
|
+
await nextTick()
|
|
93
|
+
|
|
94
|
+
const gridFixed = exposed.value!.getGrid()
|
|
95
|
+
state.value = undefined
|
|
96
|
+
await nextTick()
|
|
97
|
+
const gridGrow = exposed.value!.getGrid()
|
|
98
|
+
expect(gridGrow).not.toBe(gridFixed)
|
|
99
|
+
|
|
100
|
+
// 重建后的实例恢复引擎缺省(近边增长生效)
|
|
101
|
+
scrollToLeftEnd(exposed)
|
|
102
|
+
await nextFrames(3)
|
|
103
|
+
expect(exposed.value!.getActiveSheet().cols).toBeGreaterThan(26)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Workbook } from 'infinitable/sheet'
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vite-plus/test'
|
|
3
|
+
import { createApp, h, nextTick, ref, type App } from 'vue'
|
|
4
|
+
|
|
5
|
+
import { USheet } from '../../../index'
|
|
6
|
+
import type { SheetExposed } from '../../../types'
|
|
7
|
+
|
|
8
|
+
const apps: App[] = []
|
|
9
|
+
const containers: HTMLElement[] = []
|
|
10
|
+
|
|
11
|
+
function mount(props: () => Record<string, unknown>): {
|
|
12
|
+
exposed: { value: SheetExposed | undefined }
|
|
13
|
+
el: HTMLElement
|
|
14
|
+
} {
|
|
15
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
el.style.width = '800px'
|
|
18
|
+
el.style.height = '600px'
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
containers.push(el)
|
|
21
|
+
const app = createApp({
|
|
22
|
+
render: () =>
|
|
23
|
+
h(USheet, {
|
|
24
|
+
...props(),
|
|
25
|
+
ref: (value: unknown) => {
|
|
26
|
+
exposed.value = value as SheetExposed | undefined
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
app.mount(el)
|
|
31
|
+
apps.push(app)
|
|
32
|
+
return { exposed, el }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 引擎装配的原生滚动容器(wrapper 带 data-native-scroll 标记,见引擎 NativeScrollbarHost) */
|
|
36
|
+
function nativeScrollHost(el: HTMLElement): HTMLElement | null {
|
|
37
|
+
return el.querySelector<HTMLElement>('.u-sheet__grid-instance [data-native-scroll]')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
while (apps.length) apps.pop()!.unmount()
|
|
42
|
+
while (containers.length) containers.pop()!.remove()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('USheet scrollbar(缺省原生档,全形态透传)', () => {
|
|
46
|
+
it('缺省:原生档配置下发引擎,usesNativeScrollbar 为 true,grid 区装配原生滚动容器', async () => {
|
|
47
|
+
const { exposed, el } = mount(() => ({ workbook: new Workbook(), rows: 10, cols: 6 }))
|
|
48
|
+
await nextTick()
|
|
49
|
+
|
|
50
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
51
|
+
expect(table.options.scrollbar).toEqual({ mode: 'native' })
|
|
52
|
+
expect(table.usesNativeScrollbar).toBe(true)
|
|
53
|
+
// DOM 断言:引擎在挂载容器内装配真实 overflow 滚动容器(scrollbar-gutter: stable)
|
|
54
|
+
expect(nativeScrollHost(el)).not.toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('scrollbar: false:整体关闭语义不变,无原生滚动容器', async () => {
|
|
58
|
+
const { exposed, el } = mount(() => ({
|
|
59
|
+
workbook: new Workbook(),
|
|
60
|
+
rows: 10,
|
|
61
|
+
cols: 6,
|
|
62
|
+
scrollbar: false
|
|
63
|
+
}))
|
|
64
|
+
await nextTick()
|
|
65
|
+
|
|
66
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
67
|
+
expect(table.options.scrollbar).toBe(false)
|
|
68
|
+
expect(table.usesNativeScrollbar).toBe(false)
|
|
69
|
+
expect(nativeScrollHost(el)).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('scrollbar: true:旧语义(画布常驻滚动条)透传,不装配原生容器', async () => {
|
|
73
|
+
const { exposed, el } = mount(() => ({
|
|
74
|
+
workbook: new Workbook(),
|
|
75
|
+
rows: 10,
|
|
76
|
+
cols: 6,
|
|
77
|
+
scrollbar: true
|
|
78
|
+
}))
|
|
79
|
+
await nextTick()
|
|
80
|
+
|
|
81
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
82
|
+
expect(table.options.scrollbar).toBe(true)
|
|
83
|
+
expect(table.usesNativeScrollbar).toBe(false)
|
|
84
|
+
expect(nativeScrollHost(el)).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('对象形态按引用透传(mode: canvas 显式回画布档)', async () => {
|
|
88
|
+
const canvasOption = { mode: 'canvas', visibility: 'always' } as const
|
|
89
|
+
const { exposed, el } = mount(() => ({
|
|
90
|
+
workbook: new Workbook(),
|
|
91
|
+
rows: 10,
|
|
92
|
+
cols: 6,
|
|
93
|
+
scrollbar: canvasOption
|
|
94
|
+
}))
|
|
95
|
+
await nextTick()
|
|
96
|
+
|
|
97
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
98
|
+
// 引用透传:引擎收到宿主的同一对象(构造期选项,宿主保持稳定引用即可)
|
|
99
|
+
expect(table.options.scrollbar).toBe(canvasOption)
|
|
100
|
+
expect(table.usesNativeScrollbar).toBe(false)
|
|
101
|
+
expect(nativeScrollHost(el)).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('watch 重建:对象引用更替与 boolean 切换都触发网格重建,新配置生效', async () => {
|
|
105
|
+
const state = ref<boolean | Record<string, unknown>>({ mode: 'native' })
|
|
106
|
+
const { exposed, el } = mount(() => ({
|
|
107
|
+
workbook: new Workbook(),
|
|
108
|
+
rows: 10,
|
|
109
|
+
cols: 6,
|
|
110
|
+
scrollbar: state.value
|
|
111
|
+
}))
|
|
112
|
+
await nextTick()
|
|
113
|
+
|
|
114
|
+
const gridNative = exposed.value!.getGrid()
|
|
115
|
+
expect(gridNative!.getTable().usesNativeScrollbar).toBe(true)
|
|
116
|
+
|
|
117
|
+
// 对象引用更替(native → canvas 常驻):构造期选项变化 → 重建
|
|
118
|
+
state.value = { mode: 'canvas', visibility: 'always' }
|
|
119
|
+
await nextTick()
|
|
120
|
+
const gridCanvas = exposed.value!.getGrid()
|
|
121
|
+
expect(gridCanvas).not.toBe(gridNative)
|
|
122
|
+
expect(gridCanvas!.getTable().options.scrollbar).toEqual({
|
|
123
|
+
mode: 'canvas',
|
|
124
|
+
visibility: 'always'
|
|
125
|
+
})
|
|
126
|
+
expect(gridCanvas!.getTable().usesNativeScrollbar).toBe(false)
|
|
127
|
+
expect(nativeScrollHost(el)).toBeNull()
|
|
128
|
+
|
|
129
|
+
// boolean 切换(canvas → false):同样重建,整体关闭
|
|
130
|
+
state.value = false
|
|
131
|
+
await nextTick()
|
|
132
|
+
const gridOff = exposed.value!.getGrid()
|
|
133
|
+
expect(gridOff).not.toBe(gridCanvas)
|
|
134
|
+
expect(gridOff!.getTable().options.scrollbar).toBe(false)
|
|
135
|
+
expect(gridOff!.getTable().usesNativeScrollbar).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -95,16 +95,16 @@
|
|
|
95
95
|
|
|
96
96
|
<script lang="ts" setup>
|
|
97
97
|
import { message, UDropdown } from '@veltra/desktop'
|
|
98
|
+
import { bem } from '@veltra/utils'
|
|
98
99
|
import {
|
|
99
100
|
formatAddress,
|
|
100
101
|
formatRange,
|
|
101
102
|
parseRange,
|
|
102
103
|
type CellAddress,
|
|
103
104
|
type CellRange
|
|
104
|
-
} from '
|
|
105
|
-
import type { SelectionState } from '
|
|
106
|
-
import type { Sheet } from '
|
|
107
|
-
import { bem } from '@veltra/utils'
|
|
105
|
+
} from 'infinitable/sheet'
|
|
106
|
+
import type { SelectionState } from 'infinitable/sheet'
|
|
107
|
+
import type { Sheet } from 'infinitable/sheet'
|
|
108
108
|
import {
|
|
109
109
|
computed,
|
|
110
110
|
nextTick,
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { message, messageConfirm } from '@veltra/desktop'
|
|
2
|
-
import {
|
|
3
|
-
importCsv,
|
|
4
|
-
importXlsx,
|
|
5
|
-
replaceWorkbookWithSnapshots
|
|
6
|
-
} from '@veltra/sheet-core/core/io/import.js'
|
|
7
|
-
import type { Sheet, SheetSnapshot } from '@veltra/sheet-core/core/sheet.js'
|
|
8
|
-
import type { Workbook } from '@veltra/sheet-core/core/workbook.js'
|
|
9
2
|
import { nextFrame } from '@veltra/utils'
|
|
3
|
+
import { importCsv, importXlsx, replaceWorkbookWithSnapshots } from 'infinitable/sheet'
|
|
4
|
+
import type { Sheet, SheetSnapshot } from 'infinitable/sheet'
|
|
5
|
+
import type { Workbook } from 'infinitable/sheet'
|
|
10
6
|
import type { Ref } from 'vue'
|
|
11
7
|
|
|
12
8
|
import type { ImportWorkerResponse } from './popups/import.worker'
|