@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
@@ -0,0 +1,183 @@
1
+ import type { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+ import { ref } from 'vue'
5
+
6
+ import { IMPORT_FILE_ACCEPT, importFromFile, pickAndImportFile } from '../import-file'
7
+
8
+ const mocks = vi.hoisted(() => {
9
+ const message = vi.fn(() => ({ close: vi.fn(), id: 'loading', onClosed: Promise.resolve() }))
10
+ for (const type of ['success', 'warn', 'info', 'error', 'default']) {
11
+ message[type] = vi.fn()
12
+ }
13
+ return {
14
+ message,
15
+ messageConfirm: { danger: vi.fn() },
16
+ importCsv: vi.fn(),
17
+ importXlsx: vi.fn(() =>
18
+ Promise.resolve({
19
+ getSheets: () => [{ name: 'S1', snapshot: () => EMPTY_SNAPSHOT }],
20
+ activeSheetIndex: 0
21
+ })
22
+ ),
23
+ replaceWorkbookWithSnapshots: vi.fn()
24
+ }
25
+ })
26
+
27
+ /** 空表快照(快照数组路径的源数据) */
28
+ const EMPTY_SNAPSHOT = {
29
+ cells: [],
30
+ styles: [],
31
+ merges: [],
32
+ frozen: { rows: 0, cols: 0 },
33
+ rows: 0,
34
+ cols: 0
35
+ }
36
+
37
+ vi.mock('@veltra/desktop', () => ({ message: mocks.message, messageConfirm: mocks.messageConfirm }))
38
+
39
+ vi.mock('@veltra/sheet-core/core/io/import', () => ({
40
+ importCsv: mocks.importCsv,
41
+ importXlsx: mocks.importXlsx,
42
+ replaceWorkbookWithSnapshots: mocks.replaceWorkbookWithSnapshots
43
+ }))
44
+
45
+ function makeOptions(
46
+ overrides: {
47
+ onWorkbookReplaced?: () => void
48
+ onCsvImported?: () => void
49
+ parsing?: ReturnType<typeof ref<boolean>>
50
+ } = {}
51
+ ) {
52
+ const workbook = new Workbook()
53
+ const activeSheet = workbook.activeSheet as Sheet
54
+ return {
55
+ workbook,
56
+ activeSheet,
57
+ parsing: overrides.parsing ?? ref(false),
58
+ parseProgress: ref({ done: 0, total: 0 }),
59
+ onCsvImported: overrides.onCsvImported,
60
+ onWorkbookReplaced: overrides.onWorkbookReplaced
61
+ }
62
+ }
63
+
64
+ /** 触发 xlsx 导入并等异步链(arrayBuffer → importXlsx → danger)完成 */
65
+ async function triggerXlsxAndAwait(
66
+ options: ReturnType<typeof makeOptions>,
67
+ name = 'test.xlsx'
68
+ ): Promise<void> {
69
+ const file = {
70
+ name,
71
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
72
+ text: () => Promise.resolve('')
73
+ } as File
74
+ importFromFile(file, options)
75
+ await Promise.resolve()
76
+ await Promise.resolve()
77
+ await Promise.resolve()
78
+ }
79
+
80
+ /** 取 messageConfirm.danger 注册的 onClosed 回调 */
81
+ function getOnClosed(): (action: string) => void {
82
+ const opts = mocks.messageConfirm.danger.mock.calls[0]![1] as {
83
+ onClosed: (action: string) => void
84
+ }
85
+ return opts.onClosed
86
+ }
87
+
88
+ afterEach(() => {
89
+ vi.clearAllMocks()
90
+ document.body.querySelectorAll('input[type="file"]').forEach((el) => el.remove())
91
+ })
92
+
93
+ describe('importFromFile 导入确认兜底', () => {
94
+ it('确认后:loading 提示 → replaceWorkbookWithSnapshots → 成功提示', async () => {
95
+ const replaced = vi.fn()
96
+ const parsingRef = ref(false)
97
+ const options = makeOptions({ onWorkbookReplaced: replaced, parsing: parsingRef })
98
+ await triggerXlsxAndAwait(options)
99
+ const onClosed = getOnClosed()
100
+ await onClosed('confirm')
101
+
102
+ expect(parsingRef.value).toBe(false)
103
+ expect(mocks.message).toHaveBeenCalledWith(
104
+ expect.objectContaining({ message: '正在导入…', duration: 0 })
105
+ )
106
+ expect(mocks.replaceWorkbookWithSnapshots).toHaveBeenCalledTimes(1)
107
+ expect(mocks.replaceWorkbookWithSnapshots).toHaveBeenCalledWith(
108
+ expect.anything(),
109
+ [{ name: 'S1', snapshot: EMPTY_SNAPSHOT }],
110
+ 0
111
+ )
112
+ expect(replaced).toHaveBeenCalledTimes(1)
113
+ await new Promise((resolve) =>
114
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
115
+ )
116
+ expect(mocks.message.success).toHaveBeenCalledWith('导入完成')
117
+ })
118
+
119
+ it('replaceWorkbookWithSnapshots 抛错:报错提示 + 不误报成功 + 不通知宿主', async () => {
120
+ const replaced = vi.fn()
121
+ const options = makeOptions({ onWorkbookReplaced: replaced, parsing: ref(false) })
122
+ await triggerXlsxAndAwait(options)
123
+ const loadingClose = vi.fn()
124
+ mocks.message.mockReturnValueOnce({
125
+ close: loadingClose,
126
+ id: 'loading',
127
+ onClosed: Promise.resolve()
128
+ })
129
+ mocks.replaceWorkbookWithSnapshots.mockImplementationOnce(() => {
130
+ throw new Error('内存不足 boom')
131
+ })
132
+
133
+ const onClosed = getOnClosed()
134
+ await onClosed('confirm')
135
+
136
+ expect(mocks.replaceWorkbookWithSnapshots).toHaveBeenCalledTimes(1)
137
+ expect(replaced).not.toHaveBeenCalled()
138
+ expect(mocks.message.error).toHaveBeenCalledWith('导入失败:内存不足 boom')
139
+ expect(mocks.message.success).not.toHaveBeenCalled()
140
+ expect(loadingClose).toHaveBeenCalledTimes(1)
141
+ })
142
+
143
+ it('取消(非 confirm):不触发导入', async () => {
144
+ const options = makeOptions({ parsing: ref(false) })
145
+ await triggerXlsxAndAwait(options)
146
+ const onClosed = getOnClosed()
147
+ await onClosed('cancel')
148
+ expect(mocks.replaceWorkbookWithSnapshots).not.toHaveBeenCalled()
149
+ expect(mocks.message.success).not.toHaveBeenCalled()
150
+ expect(mocks.message.error).not.toHaveBeenCalled()
151
+ })
152
+
153
+ it('csv:写入活动表并回调 onCsvImported', async () => {
154
+ const csvImported = vi.fn()
155
+ const options = makeOptions({ onCsvImported: csvImported })
156
+ const file = {
157
+ name: 'data.csv',
158
+ text: () => Promise.resolve('a,b\n1,2'),
159
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0))
160
+ } as File
161
+ importFromFile(file, options)
162
+ await Promise.resolve()
163
+ await Promise.resolve()
164
+ expect(mocks.importCsv).toHaveBeenCalledWith('a,b\n1,2', options.activeSheet)
165
+ expect(csvImported).toHaveBeenCalledTimes(1)
166
+ expect(mocks.message.success).toHaveBeenCalledWith(
167
+ expect.stringContaining('已从 data.csv 导入')
168
+ )
169
+ })
170
+ })
171
+
172
+ describe('pickAndImportFile', () => {
173
+ it('创建隐藏 file input(同一 accept)并 click', () => {
174
+ const click = vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(() => {})
175
+ pickAndImportFile(makeOptions())
176
+ const input = document.body.querySelector<HTMLInputElement>('input[type="file"]')
177
+ expect(input).toBeTruthy()
178
+ expect(input!.accept).toBe(IMPORT_FILE_ACCEPT)
179
+ expect(input!.hidden).toBe(true)
180
+ expect(click).toHaveBeenCalled()
181
+ click.mockRestore()
182
+ })
183
+ })
@@ -0,0 +1,123 @@
1
+ import { ContextmenuRootDIKey } from '@veltra/desktop'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { createApp, h, nextTick } from 'vue'
4
+
5
+ import InsertCountMenuItem from '../insert-count-menu-item.vue'
6
+
7
+ function mountItem(options?: { defaultValue?: number }) {
8
+ const host = document.createElement('div')
9
+ document.body.appendChild(host)
10
+ const onConfirm = vi.fn()
11
+ const onItemClickEnd = vi.fn()
12
+
13
+ const app = createApp({
14
+ setup() {
15
+ return () =>
16
+ h(InsertCountMenuItem, {
17
+ prefix: '在上方插入',
18
+ suffix: '行',
19
+ defaultValue: options?.defaultValue ?? 2,
20
+ min: 1,
21
+ max: 1000,
22
+ onConfirm
23
+ })
24
+ }
25
+ })
26
+ app.provide(ContextmenuRootDIKey, {
27
+ cls: { b: '', e: () => '', m: () => '' } as never,
28
+ onItemClickStart: () => {},
29
+ onItemClickEnd
30
+ })
31
+ app.mount(host)
32
+
33
+ return {
34
+ host,
35
+ onConfirm,
36
+ onItemClickEnd,
37
+ unmount() {
38
+ app.unmount()
39
+ host.remove()
40
+ }
41
+ }
42
+ }
43
+
44
+ describe('InsertCountMenuItem', () => {
45
+ it('无确认按钮', async () => {
46
+ const { host, unmount } = mountItem()
47
+ try {
48
+ await nextTick()
49
+ expect(host.querySelector('button')?.textContent).not.toBe('确认')
50
+ expect(
51
+ Array.from(host.querySelectorAll('button')).some((b) => b.textContent === '确认')
52
+ ).toBe(false)
53
+ } finally {
54
+ unmount()
55
+ }
56
+ })
57
+
58
+ it('点击菜单项本体按当前值执行并关闭', async () => {
59
+ const { host, onConfirm, onItemClickEnd, unmount } = mountItem({ defaultValue: 3 })
60
+ try {
61
+ await nextTick()
62
+ host.querySelector<HTMLElement>('.u-sheet__insert-count-menu')!.click()
63
+ expect(onConfirm).toHaveBeenCalledWith(3)
64
+ expect(onItemClickEnd).toHaveBeenCalledOnce()
65
+ } finally {
66
+ unmount()
67
+ }
68
+ })
69
+
70
+ it('点击输入框区域不执行', async () => {
71
+ const { host, onConfirm, onItemClickEnd, unmount } = mountItem({ defaultValue: 3 })
72
+ try {
73
+ await nextTick()
74
+ host.querySelector<HTMLElement>('.u-sheet__insert-count-input')!.click()
75
+ expect(onConfirm).not.toHaveBeenCalled()
76
+ expect(onItemClickEnd).not.toHaveBeenCalled()
77
+ } finally {
78
+ unmount()
79
+ }
80
+ })
81
+
82
+ it('UNumberInput clearable=false(无清除钮)', async () => {
83
+ const { host, unmount } = mountItem({ defaultValue: 3 })
84
+ try {
85
+ await nextTick()
86
+ expect(host.querySelector('.u-number-input__clear')).toBeNull()
87
+ } finally {
88
+ unmount()
89
+ }
90
+ })
91
+
92
+ it('输入框内 Enter 提交(穿透 UNumberInput keydown.stop)', async () => {
93
+ const { host, onConfirm, onItemClickEnd, unmount } = mountItem({ defaultValue: 4 })
94
+ try {
95
+ await nextTick()
96
+ const input = host.querySelector<HTMLInputElement>('input')
97
+ expect(input).toBeTruthy()
98
+ input!.dispatchEvent(
99
+ new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
100
+ )
101
+ expect(onConfirm).toHaveBeenCalledWith(4)
102
+ expect(onItemClickEnd).toHaveBeenCalledOnce()
103
+ } finally {
104
+ unmount()
105
+ }
106
+ })
107
+
108
+ it('输入框内 Esc 关闭且不提交', async () => {
109
+ const { host, onConfirm, onItemClickEnd, unmount } = mountItem()
110
+ try {
111
+ await nextTick()
112
+ const input = host.querySelector<HTMLInputElement>('input')
113
+ expect(input).toBeTruthy()
114
+ input!.dispatchEvent(
115
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
116
+ )
117
+ expect(onConfirm).not.toHaveBeenCalled()
118
+ expect(onItemClickEnd).toHaveBeenCalledOnce()
119
+ } finally {
120
+ unmount()
121
+ }
122
+ })
123
+ })
@@ -0,0 +1,89 @@
1
+ import { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { createApp, h, type App } from 'vue'
4
+
5
+ import { createSheetContext } from '../../tools/context'
6
+ import USheetInsertImagePopup from '../popups/insert-image-popup.vue'
7
+
8
+ const mocks = vi.hoisted(() => {
9
+ const message = Object.assign(vi.fn(), {
10
+ success: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ error: vi.fn(),
14
+ default: vi.fn()
15
+ })
16
+ return { message }
17
+ })
18
+
19
+ vi.mock('@veltra/desktop', async () => {
20
+ const { defineComponent, h } = await import('vue')
21
+ return {
22
+ message: mocks.message,
23
+ UFilePicker: defineComponent({
24
+ name: 'UFilePickerStub',
25
+ props: ['accept'],
26
+ emits: ['pick'],
27
+ setup(props, { emit }) {
28
+ return () =>
29
+ h(
30
+ 'button',
31
+ {
32
+ class: 'picker-stub-btn',
33
+ 'data-accept': props.accept as string,
34
+ onClick: () =>
35
+ emit('pick', [
36
+ new File([new Uint8Array([9, 8, 7])], 'pic.png', { type: 'image/png' })
37
+ ])
38
+ },
39
+ 'pick'
40
+ )
41
+ }
42
+ })
43
+ }
44
+ })
45
+
46
+ const apps: App[] = []
47
+ const containers: HTMLElement[] = []
48
+
49
+ function mountPopup(onClose?: () => void) {
50
+ const sheet = new Sheet()
51
+ sheet.selectCell({ row: 1, col: 1 })
52
+ const context = createSheetContext(sheet)
53
+ const el = document.createElement('div')
54
+ document.body.appendChild(el)
55
+ containers.push(el)
56
+ const app = createApp({
57
+ setup() {
58
+ return () => h(USheetInsertImagePopup, { context, onClose })
59
+ }
60
+ })
61
+ app.mount(el)
62
+ apps.push(app)
63
+ return { el, sheet, context }
64
+ }
65
+
66
+ afterEach(() => {
67
+ apps.splice(0).forEach((app) => app.unmount())
68
+ containers.splice(0).forEach((el) => el.remove())
69
+ vi.clearAllMocks()
70
+ })
71
+
72
+ describe('USheetInsertImagePopup', () => {
73
+ it('accept 限定扩展名;pick 后关闭并调用 insertImage', async () => {
74
+ const closed = vi.fn()
75
+ const { el, sheet } = mountPopup(closed)
76
+ const btn = el.querySelector<HTMLButtonElement>('.picker-stub-btn')!
77
+ expect(btn.getAttribute('data-accept')).toBe('.png,.jpg,.jpeg,.gif,.svg,.webp')
78
+
79
+ btn.click()
80
+ await Promise.resolve()
81
+ await Promise.resolve()
82
+
83
+ expect(closed).toHaveBeenCalledTimes(1)
84
+ expect(sheet.getImages()).toHaveLength(1)
85
+ expect(sheet.getImages()[0]?.anchor.from).toEqual({ row: 1, col: 1 })
86
+ expect(sheet.getImages()[0]?.type).toBe('png')
87
+ expect([...sheet.getImages()[0]!.data]).toEqual([9, 8, 7])
88
+ })
89
+ })
@@ -0,0 +1,100 @@
1
+ import { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { createSheetContext } from '../../tools/context'
5
+ import {
6
+ INSERT_IMAGE_ACCEPT,
7
+ insertImageFromFile,
8
+ pickAndInsertImage,
9
+ resolveImageType
10
+ } from '../insert-image'
11
+
12
+ const mocks = vi.hoisted(() => {
13
+ const message = Object.assign(vi.fn(), {
14
+ success: vi.fn(),
15
+ warn: vi.fn(),
16
+ info: vi.fn(),
17
+ error: vi.fn(),
18
+ default: vi.fn()
19
+ })
20
+ return { message }
21
+ })
22
+
23
+ vi.mock('@veltra/desktop', () => ({ message: mocks.message }))
24
+
25
+ function makeFile(name: string, type: string, bytes: number[] = [0x89, 0x50]): File {
26
+ return new File([new Uint8Array(bytes)], name, { type })
27
+ }
28
+
29
+ afterEach(() => {
30
+ vi.clearAllMocks()
31
+ document.body.querySelectorAll('input[type="file"]').forEach((el) => el.remove())
32
+ })
33
+
34
+ describe('resolveImageType', () => {
35
+ it('按 MIME 映射 png/jpeg/gif/svg/webp', () => {
36
+ expect(resolveImageType(makeFile('a.png', 'image/png'))).toBe('png')
37
+ expect(resolveImageType(makeFile('a.jpg', 'image/jpeg'))).toBe('jpeg')
38
+ expect(resolveImageType(makeFile('a.gif', 'image/gif'))).toBe('gif')
39
+ expect(resolveImageType(makeFile('a.svg', 'image/svg+xml'))).toBe('svg')
40
+ expect(resolveImageType(makeFile('a.webp', 'image/webp'))).toBe('webp')
41
+ })
42
+
43
+ it('MIME 为空时回落扩展名;不支持则 undefined', () => {
44
+ expect(resolveImageType(makeFile('logo.PNG', ''))).toBe('png')
45
+ expect(resolveImageType(makeFile('photo.jpg', ''))).toBe('jpeg')
46
+ expect(resolveImageType(makeFile('x.bmp', 'image/bmp'))).toBeUndefined()
47
+ expect(resolveImageType(makeFile('x.txt', 'text/plain'))).toBeUndefined()
48
+ })
49
+ })
50
+
51
+ describe('insertImageFromFile', () => {
52
+ it('支持格式 → 锚定活动格并经 ctx.insertImage 写入', async () => {
53
+ const sheet = new Sheet()
54
+ const ctx = createSheetContext(sheet)
55
+ ctx.selectCell({ row: 2, col: 3 })
56
+ const bytes = [1, 2, 3, 4]
57
+ const id = await insertImageFromFile(ctx, makeFile('logo.png', 'image/png', bytes))
58
+ expect(id).toBeTruthy()
59
+ const images = ctx.getImages()
60
+ expect(images).toHaveLength(1)
61
+ expect(images[0]?.type).toBe('png')
62
+ expect(images[0]?.anchor.from).toEqual({ row: 2, col: 3 })
63
+ expect([...images[0]!.data]).toEqual(bytes)
64
+ expect(mocks.message.error).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it('不支持格式 → message.error,不写入', async () => {
68
+ const sheet = new Sheet()
69
+ const ctx = createSheetContext(sheet)
70
+ const id = await insertImageFromFile(ctx, makeFile('x.bmp', 'image/bmp'))
71
+ expect(id).toBeUndefined()
72
+ expect(ctx.getImages()).toHaveLength(0)
73
+ expect(mocks.message.error).toHaveBeenCalledWith(expect.stringContaining('不支持的图片格式'))
74
+ })
75
+
76
+ it('无活动格 → message.error,不写入', async () => {
77
+ const sheet = new Sheet()
78
+ const ctx = createSheetContext(sheet)
79
+ sheet.selection.clear()
80
+ const id = await insertImageFromFile(ctx, makeFile('a.png', 'image/png'))
81
+ expect(id).toBeUndefined()
82
+ expect(ctx.getImages()).toHaveLength(0)
83
+ expect(mocks.message.error).toHaveBeenCalledWith('请先选择单元格')
84
+ })
85
+ })
86
+
87
+ describe('pickAndInsertImage', () => {
88
+ it('创建隐藏 file input(同一 accept)并 click', () => {
89
+ const sheet = new Sheet()
90
+ const ctx = createSheetContext(sheet)
91
+ const click = vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(() => {})
92
+ pickAndInsertImage(ctx)
93
+ const input = document.body.querySelector<HTMLInputElement>('input[type="file"]')
94
+ expect(input).toBeTruthy()
95
+ expect(input!.accept).toBe(INSERT_IMAGE_ACCEPT)
96
+ expect(input!.hidden).toBe(true)
97
+ expect(click).toHaveBeenCalled()
98
+ click.mockRestore()
99
+ })
100
+ })