@veltra/sheet 2.0.1 → 2.1.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 (211) hide show
  1. package/dist/index.d.ts +4 -26
  2. package/dist/index.js +3 -23
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -164
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +10 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -7
  46. package/src/env.d.ts +11 -0
  47. package/src/index.ts +203 -14
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +161 -186
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +11 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/fill.d.ts +0 -34
  125. package/dist/core/fill.js +0 -320
  126. package/dist/core/fill.js.map +0 -1
  127. package/dist/core/formula/ast.d.ts +0 -58
  128. package/dist/core/formula/ast.js +0 -38
  129. package/dist/core/formula/ast.js.map +0 -1
  130. package/dist/core/formula/dependency-graph.d.ts +0 -70
  131. package/dist/core/formula/dependency-graph.js +0 -269
  132. package/dist/core/formula/dependency-graph.js.map +0 -1
  133. package/dist/core/formula/errors.d.ts +0 -25
  134. package/dist/core/formula/errors.js +0 -36
  135. package/dist/core/formula/errors.js.map +0 -1
  136. package/dist/core/formula/evaluator.d.ts +0 -41
  137. package/dist/core/formula/evaluator.js +0 -154
  138. package/dist/core/formula/evaluator.js.map +0 -1
  139. package/dist/core/formula/functions.d.ts +0 -30
  140. package/dist/core/formula/functions.js +0 -209
  141. package/dist/core/formula/functions.js.map +0 -1
  142. package/dist/core/formula/parser.d.ts +0 -7
  143. package/dist/core/formula/parser.js +0 -208
  144. package/dist/core/formula/parser.js.map +0 -1
  145. package/dist/core/formula/tokenizer.d.ts +0 -37
  146. package/dist/core/formula/tokenizer.js +0 -146
  147. package/dist/core/formula/tokenizer.js.map +0 -1
  148. package/dist/core/merge-manager.d.ts +0 -64
  149. package/dist/core/merge-manager.js +0 -111
  150. package/dist/core/merge-manager.js.map +0 -1
  151. package/dist/core/selection.d.ts +0 -31
  152. package/dist/core/selection.js +0 -62
  153. package/dist/core/selection.js.map +0 -1
  154. package/dist/core/sheet.d.ts +0 -116
  155. package/dist/core/sheet.js +0 -224
  156. package/dist/core/sheet.js.map +0 -1
  157. package/dist/core/workbook.d.ts +0 -43
  158. package/dist/core/workbook.js +0 -77
  159. package/dist/core/workbook.js.map +0 -1
  160. package/dist/grid/sheet-grid.d.ts +0 -81
  161. package/dist/grid/sheet-grid.js +0 -331
  162. package/dist/grid/sheet-grid.js.map +0 -1
  163. package/dist/grid/vtable-theme.js +0 -80
  164. package/dist/grid/vtable-theme.js.map +0 -1
  165. package/dist/tools/builtin.js.map +0 -1
  166. package/dist/tools/context.js.map +0 -1
  167. package/dist/tools/registry.js.map +0 -1
  168. package/dist/vue/index.d.ts +0 -2
  169. package/dist/vue/sheet.js.map +0 -1
  170. package/dist/vue/sheet.vue.d.ts +0 -24
  171. package/src/core/__test__/address.test.ts +0 -113
  172. package/src/core/__test__/cell-store.test.ts +0 -97
  173. package/src/core/__test__/command.test.ts +0 -377
  174. package/src/core/__test__/fill.test.ts +0 -140
  175. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  176. package/src/core/__test__/formula-engine.test.ts +0 -470
  177. package/src/core/__test__/formula-functions.test.ts +0 -178
  178. package/src/core/__test__/formula-parser.test.ts +0 -160
  179. package/src/core/__test__/history.test.ts +0 -225
  180. package/src/core/__test__/merge-manager.test.ts +0 -106
  181. package/src/core/__test__/sheet.test.ts +0 -142
  182. package/src/core/address.ts +0 -166
  183. package/src/core/cell-store.ts +0 -173
  184. package/src/core/command/default-registry.ts +0 -15
  185. package/src/core/command/history.ts +0 -167
  186. package/src/core/command/index.ts +0 -7
  187. package/src/core/command/merge-cells.ts +0 -74
  188. package/src/core/command/registry.ts +0 -30
  189. package/src/core/command/set-cell-formula.ts +0 -29
  190. package/src/core/command/set-cell-value.ts +0 -38
  191. package/src/core/command/types.ts +0 -73
  192. package/src/core/events.ts +0 -37
  193. package/src/core/fill.ts +0 -314
  194. package/src/core/formula/ast.ts +0 -66
  195. package/src/core/formula/dependency-graph.ts +0 -329
  196. package/src/core/formula/errors.ts +0 -41
  197. package/src/core/formula/evaluator.ts +0 -219
  198. package/src/core/formula/functions.ts +0 -266
  199. package/src/core/formula/index.ts +0 -7
  200. package/src/core/formula/parser.ts +0 -198
  201. package/src/core/formula/tokenizer.ts +0 -160
  202. package/src/core/merge-manager.ts +0 -158
  203. package/src/core/selection.ts +0 -66
  204. package/src/core/sheet.ts +0 -282
  205. package/src/core/workbook.ts +0 -99
  206. package/src/grid/__test__/canvas-mock.ts +0 -49
  207. package/src/grid/__test__/setup.ts +0 -3
  208. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  209. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  210. package/src/grid/sheet-grid.ts +0 -407
  211. package/src/grid/vtable-theme.ts +0 -61
@@ -1,9 +1,12 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { createRange } from '@veltra/sheet-core/core/address'
2
2
 
3
3
  import '../builtin'
4
- import { createRange } from '../../core/address'
5
- import { Sheet } from '../../core/sheet'
4
+ import { Sheet } from '@veltra/sheet-core/core/sheet'
5
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
6
8
  import { createSheetContext } from '../context'
9
+ import { exportSheetCsvFile, exportWorkbookFile } from '../download'
7
10
  import { defaultToolRegistry, type SheetTool } from '../registry'
8
11
 
9
12
  function mustGet(id: string): SheetTool {
@@ -17,90 +20,167 @@ function isDisabled(tool: SheetTool, sheet: Sheet): boolean {
17
20
  }
18
21
 
19
22
  describe('内置工具(dogfood 扩展机制)', () => {
20
- it('注册布局:history 组(undo/redo)在前,cell 组(merge/unmerge)在后', () => {
23
+ it('注册布局:history / cell / text / edit / insert / file 六组;structure/freeze 已移除', () => {
21
24
  const groups = defaultToolRegistry.getGroups()
22
- expect(groups.map((group) => group.name)).toEqual(['history', 'cell'])
25
+ expect(groups.map((group) => group.name)).toEqual([
26
+ 'history',
27
+ 'cell',
28
+ 'text',
29
+ 'edit',
30
+ 'insert',
31
+ 'file'
32
+ ])
23
33
  expect(groups[0]!.tools.map((tool) => tool.id)).toEqual(['undo', 'redo'])
24
- expect(groups[1]!.tools.map((tool) => tool.id)).toEqual(['merge', 'unmerge'])
25
- })
26
-
27
- it('undo/redo 的 disabled 随 history 状态联动', () => {
28
- const sheet = new Sheet()
29
- const ctx = createSheetContext(sheet)
30
- const undo = mustGet('undo')
31
- const redo = mustGet('redo')
32
-
33
- // 空历史:两者禁用
34
- expect(undo.disabled?.(ctx)).toBe(true)
35
- expect(redo.disabled?.(ctx)).toBe(true)
36
-
37
- sheet.setCellValue({ row: 0, col: 0 }, 1)
38
- expect(undo.disabled?.(ctx)).toBe(false)
39
- expect(redo.disabled?.(ctx)).toBe(true)
34
+ expect(groups[1]!.tools.map((tool) => tool.id)).toEqual([
35
+ 'border',
36
+ 'fill-color',
37
+ 'merge',
38
+ 'unmerge'
39
+ ])
40
+ expect(groups[2]!.tools.map((tool) => tool.id)).toEqual([
41
+ 'bold',
42
+ 'italic',
43
+ 'underline',
44
+ 'strikethrough',
45
+ 'font-color',
46
+ 'font-size',
47
+ 'align-left',
48
+ 'align-center',
49
+ 'align-right',
50
+ 'valign-top',
51
+ 'valign-middle',
52
+ 'valign-bottom',
53
+ 'wrap-text'
54
+ ])
55
+ expect(groups[3]!.tools.map((tool) => tool.id)).toEqual(['find'])
56
+ expect(groups[4]!.tools.map((tool) => tool.id)).toEqual(['insert-image'])
57
+ expect(groups[5]!.tools.map((tool) => tool.id)).toEqual(['import', 'export'])
40
58
 
41
- undo.onClick(ctx)
42
- expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
43
- expect(undo.disabled?.(ctx)).toBe(true)
44
- expect(redo.disabled?.(ctx)).toBe(false)
59
+ for (const id of [
60
+ 'insert-rows',
61
+ 'insert-cols',
62
+ 'delete-rows',
63
+ 'delete-cols',
64
+ 'freeze',
65
+ 'freeze-row',
66
+ 'freeze-col',
67
+ 'unfreeze',
68
+ 'export-xlsx',
69
+ 'export-csv'
70
+ ]) {
71
+ expect(defaultToolRegistry.has(id)).toBe(false)
72
+ }
73
+ })
45
74
 
46
- redo.onClick(ctx)
47
- expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 1, t: 'n' })
75
+ it('全部保留工具带 icon', () => {
76
+ for (const group of defaultToolRegistry.getGroups()) {
77
+ for (const tool of group.tools) {
78
+ expect(tool.icon, `${tool.id} 缺少 icon`).toBeTruthy()
79
+ }
80
+ }
48
81
  })
49
82
 
50
- it('merge:无选区/单格禁用,拖选区域可用,合并已有合并区域时禁用', () => {
83
+ it('导入导出工具:import 直接选文件、export 为弹层型;导出下载辅助不依赖选区', async () => {
51
84
  const sheet = new Sheet()
52
85
  const ctx = createSheetContext(sheet)
53
- const merge = mustGet('merge')
86
+ const exportTool = mustGet('export')
87
+ const importTool = mustGet('import')
54
88
 
55
- // 无选区
56
- expect(merge.disabled?.(ctx)).toBe(true)
57
-
58
- // 单格选区
59
- ctx.selectCell({ row: 0, col: 0 })
60
- expect(merge.disabled?.(ctx)).toBe(true)
89
+ expect(importTool.popup).toBeUndefined()
90
+ expect(exportTool.popup).toBe('export')
91
+ // 无 workbook 的上下文:导出辅助空操作(不抛错)
92
+ expect(() => exportWorkbookFile(ctx)).not.toThrow()
93
+ expect(() => exportSheetCsvFile(ctx)).not.toThrow()
94
+ })
61
95
 
62
- // 区域选区 → 可用
63
- const range = createRange({ row: 0, col: 0 }, { row: 1, col: 1 })
64
- ctx.selectRange(range)
65
- expect(merge.disabled?.(ctx)).toBe(false)
96
+ it('导出 xlsx / csv:生成 Blob 下载(workbook 传入时)', async () => {
97
+ const workbook = new Workbook()
98
+ workbook.activeSheet.setCellValue({ row: 0, col: 0 }, 42)
99
+ const ctx = createSheetContext(workbook.activeSheet, workbook)
66
100
 
67
- merge.onClick(ctx)
68
- expect(sheet.merges.getMergeAt({ row: 0, col: 0 })).toEqual(range)
101
+ // stub 浏览器下载 API(happy-dom 无 createObjectURL)
102
+ const originalCreate = URL.createObjectURL
103
+ const originalRevoke = URL.revokeObjectURL
104
+ const createObjectURL = vi.fn(() => 'blob:mock')
105
+ const revokeObjectURL = vi.fn()
106
+ URL.createObjectURL = createObjectURL as typeof URL.createObjectURL
107
+ URL.revokeObjectURL = revokeObjectURL as typeof URL.revokeObjectURL
108
+ try {
109
+ exportWorkbookFile(ctx)
110
+ await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
111
+ const blob = createObjectURL.mock.calls[0]![0] as Blob
112
+ expect(blob.type).toContain('spreadsheetml')
69
113
 
70
- // 选区恰好等于既有合并区域 → 禁用(避免空操作历史条目)
71
- expect(merge.disabled?.(ctx)).toBe(true)
114
+ createObjectURL.mockClear()
115
+ exportSheetCsvFile(ctx)
116
+ await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
117
+ const csvBlob = createObjectURL.mock.calls[0]![0] as Blob
118
+ expect(csvBlob.type).toContain('text/csv')
119
+ } finally {
120
+ URL.createObjectURL = originalCreate
121
+ URL.revokeObjectURL = originalRevoke
122
+ }
123
+ })
72
124
 
73
- // 合并可 undo
74
- ctx.undo()
75
- expect(sheet.merges.getMergeAt({ row: 0, col: 0 })).toBeUndefined()
125
+ it('导出 xlsx:sheet 名含 Excel 非法字符时 reject(hucre 1.0 写入校验,错误传播给调用方)', async () => {
126
+ const workbook = new Workbook()
127
+ workbook.renameSheet('Sheet1', 'a:b')
128
+ const ctx = createSheetContext(workbook.activeSheet, workbook)
129
+ await expect(exportWorkbookFile(ctx)).rejects.toThrow(/Sheet name/)
76
130
  })
77
131
 
78
- it('unmerge:活动格不在合并内禁用,在合并内可用且可 undo', () => {
132
+ it('样式工具:弹层型声明 + 无选区禁用;填充/边框面板不直接写(onClick 为空)', () => {
79
133
  const sheet = new Sheet()
80
134
  const ctx = createSheetContext(sheet)
81
- const unmerge = mustGet('unmerge')
135
+ const fill = mustGet('fill-color')
136
+ const border = mustGet('border')
82
137
 
83
- const range = createRange({ row: 0, col: 0 }, { row: 1, col: 2 })
84
- sheet.mergeCells(range)
85
- sheet.history.clear()
138
+ expect(fill.popup).toBe('fill-color')
139
+ expect(border.popup).toBe('border')
140
+ // 默认 A1 有选区 → 可用
141
+ expect(fill.disabled?.(ctx)).toBe(false)
142
+ sheet.selection.clear()
143
+ expect(fill.disabled?.(ctx)).toBe(true)
144
+ expect(border.disabled?.(ctx)).toBe(true)
145
+ })
86
146
 
87
- // 无选区
88
- expect(unmerge.disabled?.(ctx)).toBe(true)
147
+ it('合并:单格禁用;选区恰等于既有合并时禁用;可合并时执行', () => {
148
+ const sheet = new Sheet()
149
+ const merge = mustGet('merge')
150
+ const unmerge = mustGet('unmerge')
89
151
 
90
- // 普通格
91
- ctx.selectCell({ row: 5, col: 5 })
92
- expect(unmerge.disabled?.(ctx)).toBe(true)
152
+ // 默认 A1 单格 → 合并禁用、取消合并禁用
153
+ expect(isDisabled(merge, sheet)).toBe(true)
154
+ expect(isDisabled(unmerge, sheet)).toBe(true)
93
155
 
94
- // 被覆盖格 → 解析锚点后在合并内
95
- ctx.selectCell({ row: 1, col: 2 })
96
- expect(ctx.getSelection().activeCell).toEqual({ row: 0, col: 0 })
97
- expect(unmerge.disabled?.(ctx)).toBe(false)
156
+ sheet.selectRange(createRange({ row: 0, col: 0 }, { row: 1, col: 1 }))
157
+ expect(isDisabled(merge, sheet)).toBe(false)
158
+ merge.onClick(createSheetContext(sheet))
159
+ expect(sheet.getCellInfo({ row: 0, col: 0 }).mergeRange).toEqual(
160
+ createRange({ row: 0, col: 0 }, { row: 1, col: 1 })
161
+ )
162
+ // 选区恰等于合并 → 合并禁用;取消合并可用
163
+ expect(isDisabled(merge, sheet)).toBe(true)
164
+ expect(isDisabled(unmerge, sheet)).toBe(false)
165
+ unmerge.onClick(createSheetContext(sheet))
166
+ expect(sheet.getCellInfo({ row: 0, col: 0 }).mergeRange).toBeUndefined()
167
+ })
98
168
 
99
- unmerge.onClick(ctx)
100
- expect(sheet.merges.getMergeAt({ row: 0, col: 0 })).toBeUndefined()
169
+ it('undo/redo:disabled 读 canUndo/canRedo', () => {
170
+ const sheet = new Sheet()
171
+ const ctx = createSheetContext(sheet)
172
+ const undo = mustGet('undo')
173
+ const redo = mustGet('redo')
101
174
 
102
- ctx.undo()
103
- expect(sheet.merges.getMergeAt({ row: 0, col: 0 })).toEqual(range)
175
+ expect(undo.disabled?.(ctx)).toBe(true)
176
+ expect(redo.disabled?.(ctx)).toBe(true)
177
+ sheet.setCellValue({ row: 0, col: 0 }, 'x')
178
+ expect(undo.disabled?.(ctx)).toBe(false)
179
+ undo.onClick(ctx)
180
+ expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
181
+ expect(redo.disabled?.(ctx)).toBe(false)
182
+ redo.onClick(ctx)
183
+ expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'x', t: 's' })
104
184
  })
105
185
 
106
186
  it('visible 随选区联动(自定义工具)', () => {
@@ -119,10 +199,72 @@ describe('内置工具(dogfood 扩展机制)', () => {
119
199
  onClick: () => {}
120
200
  }
121
201
 
122
- expect(tool.visible?.(ctx)).toBe(false)
123
- ctx.selectCell({ row: 1, col: 1 })
202
+ // 默认 A1 落在合并内 → 可见;移出后不可见;再点覆盖格仍可见
124
203
  expect(tool.visible?.(ctx)).toBe(true)
125
204
  ctx.selectCell({ row: 3, col: 3 })
126
205
  expect(tool.visible?.(ctx)).toBe(false)
206
+ ctx.selectCell({ row: 1, col: 1 })
207
+ expect(tool.visible?.(ctx)).toBe(true)
208
+ })
209
+
210
+ it('查找工具:弹层型声明(popup: find),组 edit', () => {
211
+ const find = mustGet('find')
212
+ expect(find.popup).toBe('find')
213
+ expect(find.group).toBe('edit')
214
+ expect(find.disabled).toBeUndefined()
215
+ })
216
+
217
+ it('插入图片工具:弹层型声明(popup: insert-image),组 insert;无选区禁用', () => {
218
+ const sheet = new Sheet()
219
+ const ctx = createSheetContext(sheet)
220
+ const tool = mustGet('insert-image')
221
+ expect(tool.popup).toBe('insert-image')
222
+ expect(tool.group).toBe('insert')
223
+ expect(tool.disabled?.(ctx)).toBe(false)
224
+ sheet.selection.clear()
225
+ expect(tool.disabled?.(ctx)).toBe(true)
226
+ })
227
+
228
+ it('文本工具:toggle 以活动格为基准统一翻转;active 高亮;对齐互斥取反', () => {
229
+ const sheet = new Sheet()
230
+ const ctx = createSheetContext(sheet)
231
+ const bold = mustGet('bold')
232
+ const alignCenter = mustGet('align-center')
233
+ const wrap = mustGet('wrap-text')
234
+
235
+ expect(mustGet('font-color').popup).toBe('font-color')
236
+ expect(mustGet('font-size').popup).toBe('font-size')
237
+
238
+ // 默认 A1:active 假;点 B → 加粗 + active
239
+ expect(bold.active?.(ctx)).toBe(false)
240
+ bold.onClick(ctx)
241
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.bold).toBe(true)
242
+ expect(bold.active?.(ctx)).toBe(true)
243
+ // 再点 → 取消
244
+ bold.onClick(ctx)
245
+ expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
246
+ expect(bold.active?.(ctx)).toBe(false)
247
+
248
+ // 混合选区:活动格未加粗 → 全加粗
249
+ sheet.setCellStyle(createRange({ row: 0, col: 0 }, { row: 0, col: 0 }), {
250
+ font: { bold: true }
251
+ })
252
+ sheet.setCellValue({ row: 0, col: 1 }, 'x')
253
+ ctx.selectRange(createRange({ row: 0, col: 0 }, { row: 0, col: 1 }), { row: 0, col: 1 })
254
+ expect(bold.active?.(ctx)).toBe(false) // 活动格 B1 未加粗
255
+ bold.onClick(ctx)
256
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.bold).toBe(true)
257
+ expect(sheet.getCellStyle({ row: 0, col: 1 })?.font?.bold).toBe(true)
258
+
259
+ // 对齐:点居中 → 再点清除
260
+ alignCenter.onClick(ctx)
261
+ expect(sheet.getCellStyle({ row: 0, col: 1 })?.align?.horizontal).toBe('center')
262
+ expect(alignCenter.active?.(ctx)).toBe(true)
263
+ alignCenter.onClick(ctx)
264
+ expect(sheet.getCellStyle({ row: 0, col: 1 })?.align?.horizontal).toBeUndefined()
265
+
266
+ wrap.onClick(ctx)
267
+ expect(sheet.getCellStyle({ row: 0, col: 1 })?.align?.wrap).toBe(true)
268
+ expect(wrap.active?.(ctx)).toBe(true)
127
269
  })
128
270
  })
@@ -1,7 +1,7 @@
1
+ import { createRange } from '@veltra/sheet-core/core/address'
2
+ import { Sheet } from '@veltra/sheet-core/core/sheet'
1
3
  import { describe, expect, it, vi } from 'vitest'
2
4
 
3
- import { createRange } from '../../core/address'
4
- import { Sheet } from '../../core/sheet'
5
5
  import { createSheetContext } from '../context'
6
6
  import type { SheetTool } from '../registry'
7
7
 
@@ -91,6 +91,31 @@ describe('SheetContext', () => {
91
91
  expect(sheet.getCellData({ row: 1, col: 1 })).toBeUndefined()
92
92
  })
93
93
 
94
+ it('样式门面:setCellStyle / clearCellStyle / getCellStyle,经命令可 undo', () => {
95
+ const sheet = new Sheet()
96
+ const ctx = createSheetContext(sheet)
97
+
98
+ const range = createRange({ row: 0, col: 0 }, { row: 1, col: 1 })
99
+ ctx.setCellStyle(range, { fill: { color: '#FF0000' } })
100
+ expect(ctx.getCellStyle({ row: 0, col: 0 })).toEqual({ fill: { color: '#FF0000' } })
101
+ expect(sheet.stylePool.size).toBe(1)
102
+
103
+ // 部分合并:只设边框保留填充
104
+ ctx.setCellStyle(range, { border: { top: { style: 'thin', width: 1, color: '#000000' } } })
105
+ expect(ctx.getCellStyle({ row: 1, col: 1 })).toEqual({
106
+ fill: { color: '#FF0000' },
107
+ border: { top: { style: 'thin', width: 1, color: '#000000' } }
108
+ })
109
+
110
+ ctx.clearCellStyle(range)
111
+ expect(ctx.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
112
+
113
+ // 一次 undo 回退到带边框填充状态
114
+ expect(ctx.undo()).toBe(true)
115
+ expect(ctx.getCellStyle({ row: 0, col: 0 })?.border).toBeDefined()
116
+ expect(ctx.getCellStyle({ row: 0, col: 0 })?.fill?.color).toBe('#FF0000')
117
+ })
118
+
94
119
  it('动态解析器:tab 切换后同一上下文指向新的活动 sheet', () => {
95
120
  const sheet1 = new Sheet('Sheet1')
96
121
  const sheet2 = new Sheet('Sheet2')
@@ -154,4 +179,39 @@ describe('SheetContext', () => {
154
179
  expect(ctx.undo()).toBe(true)
155
180
  expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'keep', t: 's' })
156
181
  })
182
+
183
+ it('图片门面:insertImage / removeImage / updateImage / getImages / onImageChange,经命令可 undo', () => {
184
+ const sheet = new Sheet()
185
+ const ctx = createSheetContext(sheet)
186
+ const onImage = vi.fn()
187
+ const off = ctx.onImageChange(onImage)
188
+
189
+ const data = new Uint8Array([1, 2, 3])
190
+ const id = ctx.insertImage({ data, type: 'png', anchor: { from: { row: 1, col: 2 } } })
191
+ expect(id).toBeTruthy()
192
+ expect(ctx.getImages()).toHaveLength(1)
193
+ expect(ctx.getImages()[0]?.anchor.from).toEqual({ row: 1, col: 2 })
194
+ expect(onImage).toHaveBeenCalledWith({ id })
195
+
196
+ ctx.updateImage(id, { anchor: { from: { row: 3, col: 4 } } })
197
+ expect(ctx.getImages()[0]?.anchor.from).toEqual({ row: 3, col: 4 })
198
+ expect(onImage).toHaveBeenCalledWith({ id })
199
+
200
+ ctx.removeImage(id)
201
+ expect(ctx.getImages()).toHaveLength(0)
202
+ expect(onImage).toHaveBeenCalledWith({ id })
203
+
204
+ expect(ctx.undo()).toBe(true)
205
+ expect(ctx.getImages()).toHaveLength(1)
206
+ expect(ctx.getImages()[0]?.anchor.from).toEqual({ row: 3, col: 4 })
207
+
208
+ expect(ctx.undo()).toBe(true)
209
+ expect(ctx.getImages()[0]?.anchor.from).toEqual({ row: 1, col: 2 })
210
+
211
+ off()
212
+ onImage.mockClear()
213
+ ctx.removeImage(id)
214
+ expect(ctx.getImages()).toHaveLength(0)
215
+ expect(onImage).not.toHaveBeenCalled()
216
+ })
157
217
  })