@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
@@ -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
+ })