@veltra/sheet 2.0.0 → 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 (203) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.js +3 -22
  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 -138
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +11 -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 -6
  46. package/src/env.d.ts +14 -0
  47. package/src/index.ts +203 -13
  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 +163 -148
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +12 -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/formula/ast.d.ts +0 -58
  125. package/dist/core/formula/ast.js +0 -38
  126. package/dist/core/formula/ast.js.map +0 -1
  127. package/dist/core/formula/dependency-graph.d.ts +0 -70
  128. package/dist/core/formula/dependency-graph.js +0 -269
  129. package/dist/core/formula/dependency-graph.js.map +0 -1
  130. package/dist/core/formula/errors.d.ts +0 -25
  131. package/dist/core/formula/errors.js +0 -36
  132. package/dist/core/formula/errors.js.map +0 -1
  133. package/dist/core/formula/evaluator.d.ts +0 -41
  134. package/dist/core/formula/evaluator.js +0 -154
  135. package/dist/core/formula/evaluator.js.map +0 -1
  136. package/dist/core/formula/functions.d.ts +0 -30
  137. package/dist/core/formula/functions.js +0 -209
  138. package/dist/core/formula/functions.js.map +0 -1
  139. package/dist/core/formula/parser.d.ts +0 -7
  140. package/dist/core/formula/parser.js +0 -208
  141. package/dist/core/formula/parser.js.map +0 -1
  142. package/dist/core/formula/tokenizer.d.ts +0 -37
  143. package/dist/core/formula/tokenizer.js +0 -146
  144. package/dist/core/formula/tokenizer.js.map +0 -1
  145. package/dist/core/merge-manager.d.ts +0 -64
  146. package/dist/core/merge-manager.js +0 -111
  147. package/dist/core/merge-manager.js.map +0 -1
  148. package/dist/core/selection.d.ts +0 -31
  149. package/dist/core/selection.js +0 -62
  150. package/dist/core/selection.js.map +0 -1
  151. package/dist/core/sheet.d.ts +0 -105
  152. package/dist/core/sheet.js +0 -202
  153. package/dist/core/sheet.js.map +0 -1
  154. package/dist/core/workbook.d.ts +0 -43
  155. package/dist/core/workbook.js +0 -77
  156. package/dist/core/workbook.js.map +0 -1
  157. package/dist/grid/sheet-grid.d.ts +0 -59
  158. package/dist/grid/sheet-grid.js +0 -230
  159. package/dist/grid/sheet-grid.js.map +0 -1
  160. package/dist/tools/builtin.js.map +0 -1
  161. package/dist/tools/context.js.map +0 -1
  162. package/dist/tools/registry.js.map +0 -1
  163. package/dist/vue/index.d.ts +0 -2
  164. package/dist/vue/sheet.js.map +0 -1
  165. package/dist/vue/sheet.vue.d.ts +0 -24
  166. package/src/core/__test__/address.test.ts +0 -113
  167. package/src/core/__test__/cell-store.test.ts +0 -97
  168. package/src/core/__test__/command.test.ts +0 -377
  169. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  170. package/src/core/__test__/formula-engine.test.ts +0 -470
  171. package/src/core/__test__/formula-functions.test.ts +0 -178
  172. package/src/core/__test__/formula-parser.test.ts +0 -160
  173. package/src/core/__test__/history.test.ts +0 -225
  174. package/src/core/__test__/merge-manager.test.ts +0 -106
  175. package/src/core/__test__/sheet.test.ts +0 -142
  176. package/src/core/address.ts +0 -166
  177. package/src/core/cell-store.ts +0 -173
  178. package/src/core/command/default-registry.ts +0 -15
  179. package/src/core/command/history.ts +0 -167
  180. package/src/core/command/index.ts +0 -7
  181. package/src/core/command/merge-cells.ts +0 -74
  182. package/src/core/command/registry.ts +0 -30
  183. package/src/core/command/set-cell-formula.ts +0 -29
  184. package/src/core/command/set-cell-value.ts +0 -38
  185. package/src/core/command/types.ts +0 -73
  186. package/src/core/events.ts +0 -37
  187. package/src/core/formula/ast.ts +0 -66
  188. package/src/core/formula/dependency-graph.ts +0 -329
  189. package/src/core/formula/errors.ts +0 -41
  190. package/src/core/formula/evaluator.ts +0 -219
  191. package/src/core/formula/functions.ts +0 -266
  192. package/src/core/formula/index.ts +0 -7
  193. package/src/core/formula/parser.ts +0 -198
  194. package/src/core/formula/tokenizer.ts +0 -160
  195. package/src/core/merge-manager.ts +0 -158
  196. package/src/core/selection.ts +0 -66
  197. package/src/core/sheet.ts +0 -257
  198. package/src/core/workbook.ts +0 -99
  199. package/src/grid/__test__/canvas-mock.ts +0 -49
  200. package/src/grid/__test__/setup.ts +0 -3
  201. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  202. package/src/grid/__test__/sheet-grid.test.ts +0 -177
  203. package/src/grid/sheet-grid.ts +0 -283
@@ -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
  })