@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.
Files changed (67) hide show
  1. package/dist/components/sheet/formula-bar.js +1 -1
  2. package/dist/components/sheet/import-file.js +1 -1
  3. package/dist/components/sheet/insert-image.js +1 -1
  4. package/dist/components/sheet/popup-helpers.js +1 -1
  5. package/dist/components/sheet/popups/border-popup.js +1 -2
  6. package/dist/components/sheet/popups/functions-popup.js +3 -3
  7. package/dist/components/sheet/popups/import.worker.d.ts +1 -1
  8. package/dist/components/sheet/popups/import.worker.js +1 -1
  9. package/dist/components/sheet/sheet-context-menu.js +2 -2
  10. package/dist/components/sheet/sheet.js +12 -2
  11. package/dist/components/sheet/use-find-replace.js +1 -2
  12. package/dist/components/sheet/use-formula-suggest.js +5 -4
  13. package/dist/components/sheet/use-sheet-grid.js +4 -2
  14. package/dist/components/sheet/use-sheet-state.js +1 -1
  15. package/dist/components/sheet/use-tool-popup.js +13 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/tools/builtin.js +1 -1
  18. package/dist/tools/context.d.ts +1 -11
  19. package/dist/tools/download.js +3 -3
  20. package/dist/tools/export.worker.d.ts +1 -1
  21. package/dist/tools/export.worker.js +1 -1
  22. package/dist/tools/registry.js +1 -1
  23. package/dist/types/sheet.d.ts +15 -7
  24. package/package.json +2 -3
  25. package/src/components/sheet/__test__/canvas-mock.ts +49 -0
  26. package/src/components/sheet/__test__/find-shortcut-anchor.test.ts +117 -0
  27. package/src/components/sheet/__test__/formula-bar.test.ts +11 -9
  28. package/src/components/sheet/__test__/formula-capability.test.ts +160 -0
  29. package/src/components/sheet/__test__/functions-popup.test.ts +7 -7
  30. package/src/components/sheet/__test__/grid-setup.ts +3 -0
  31. package/src/components/sheet/__test__/grid-test-utils.ts +47 -0
  32. package/src/components/sheet/__test__/import-file.test.ts +11 -7
  33. package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
  34. package/src/components/sheet/__test__/insert-image.test.ts +1 -1
  35. package/src/components/sheet/__test__/sheet-component.test.ts +9 -19
  36. package/src/components/sheet/__test__/sheet-context-menu.test.ts +2 -2
  37. package/src/components/sheet/__test__/sheet-grow-on-scroll.test.ts +105 -0
  38. package/src/components/sheet/__test__/sheet-scrollbar.test.ts +137 -0
  39. package/src/components/sheet/__test__/use-formula-suggest.test.ts +1 -1
  40. package/src/components/sheet/formula-bar.vue +4 -4
  41. package/src/components/sheet/import-file.ts +3 -7
  42. package/src/components/sheet/insert-image.ts +2 -2
  43. package/src/components/sheet/popup-helpers.ts +3 -3
  44. package/src/components/sheet/popups/border-popup.vue +4 -4
  45. package/src/components/sheet/popups/export-popup.vue +1 -1
  46. package/src/components/sheet/popups/find-popup.vue +1 -1
  47. package/src/components/sheet/popups/functions-popup.vue +3 -9
  48. package/src/components/sheet/popups/import.worker.ts +3 -3
  49. package/src/components/sheet/sheet-context-menu.ts +4 -4
  50. package/src/components/sheet/sheet-tabs.vue +2 -2
  51. package/src/components/sheet/sheet.vue +12 -3
  52. package/src/components/sheet/use-find-replace.ts +3 -7
  53. package/src/components/sheet/use-formula-suggest.ts +6 -8
  54. package/src/components/sheet/use-sheet-grid.ts +6 -4
  55. package/src/components/sheet/use-sheet-state.ts +2 -2
  56. package/src/components/sheet/use-tool-popup.ts +22 -2
  57. package/src/index.ts +3 -1
  58. package/src/tools/__test__/apply-style.test.ts +2 -2
  59. package/src/tools/__test__/builtin.test.ts +4 -4
  60. package/src/tools/__test__/context.test.ts +2 -2
  61. package/src/tools/apply-style.ts +3 -3
  62. package/src/tools/builtin.ts +2 -2
  63. package/src/tools/context.ts +11 -11
  64. package/src/tools/download.ts +4 -4
  65. package/src/tools/export.worker.ts +4 -7
  66. package/src/tools/registry.ts +1 -1
  67. 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,3 @@
1
+ import { setupCanvasMock } from './canvas-mock'
2
+
3
+ setupCanvasMock()
@@ -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 '@veltra/sheet-core/core/sheet.js'
2
- import { Workbook } from '@veltra/sheet-core/core/workbook.js'
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('@veltra/sheet-core/core/io/import.js', () => ({
40
- importCsv: mocks.importCsv,
41
- importXlsx: mocks.importXlsx,
42
- replaceWorkbookWithSnapshots: mocks.replaceWorkbookWithSnapshots
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,4 +1,4 @@
1
- import { Sheet } from '@veltra/sheet-core/core/sheet.js'
1
+ import { Sheet } from 'infinitable/sheet'
2
2
  import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
3
3
  import { createApp, h, type App } from 'vue'
4
4
 
@@ -1,4 +1,4 @@
1
- import { Sheet } from '@veltra/sheet-core/core/sheet.js'
1
+ import { Sheet } from 'infinitable/sheet'
2
2
  import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
3
3
 
4
4
  import { createSheetContext } from '../../../tools/context'
@@ -1,14 +1,14 @@
1
- import { createRange } from '@veltra/sheet-core/core/address.js'
2
- import { Workbook } from '@veltra/sheet-core/core/workbook.js'
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
- it('scrollbar 透传引擎内建滚动条(缺省开启,false 关闭并触发重建)', async () => {
88
- const exposedOn: { value: SheetExposed | undefined } = { value: undefined }
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.querySelector<HTMLElement>('.u-sheet__grid-instance')!
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.querySelector<HTMLElement>('.u-sheet__grid-instance')!
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.querySelector<HTMLElement>('.u-sheet__grid-instance')!
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 '@veltra/sheet-core/core/address.js'
2
- import { Sheet } from '@veltra/sheet-core/core/sheet.js'
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
+ })
@@ -1,4 +1,4 @@
1
- import { registerFormulaFunction } from '@veltra/sheet-core/core/formula/functions.js'
1
+ import { registerFormulaFunction } from 'infinitable'
2
2
  import { describe, expect, it } from 'vite-plus/test'
3
3
 
4
4
  import {
@@ -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 '@veltra/sheet-core/core/address.js'
105
- import type { SelectionState } from '@veltra/sheet-core/core/selection.js'
106
- import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
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'