@veltra/sheet 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/dist/components/report/binding-badge.js +76 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +15 -0
  4. package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
  5. package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
  6. package/dist/components/report/designer/float-panel.js +385 -0
  7. package/dist/components/report/designer/role.js +17 -0
  8. package/dist/components/report/designer/rule-preview.js +33 -0
  9. package/dist/components/report/designer/rule-row.js +269 -0
  10. package/dist/components/report/designer/rules-dialog.js +135 -0
  11. package/dist/components/report/designer/topology-overlay.js +122 -0
  12. package/dist/components/report/designer/topology.js +64 -0
  13. package/dist/components/report/designer/use-grid-overlay.js +75 -0
  14. package/dist/components/report/designer-hub.js +177 -0
  15. package/dist/components/report/field-panel-helpers.js +33 -0
  16. package/dist/components/report/field-panel.js +132 -0
  17. package/dist/components/report/filter-bar.js +74 -0
  18. package/dist/components/report/hub-connection-form.js +145 -0
  19. package/dist/components/report/hub-dataset-editor.js +394 -0
  20. package/dist/components/report/report-designer.js +343 -0
  21. package/dist/components/report/report-viewer.js +111 -0
  22. package/dist/components/report/style.d.ts +17 -0
  23. package/dist/components/report/style.js +18 -0
  24. package/dist/components/report/style2.css +869 -0
  25. package/dist/components/report/use-report-designer.js +360 -0
  26. package/dist/components/report/use-report-viewer.js +50 -0
  27. package/dist/components/sheet/formula-bar.js +393 -0
  28. package/dist/components/sheet/formula-suggest-list.js +66 -0
  29. package/dist/components/sheet/import-file.js +140 -0
  30. package/dist/components/sheet/insert-count-menu-item.js +68 -0
  31. package/dist/components/sheet/insert-image.js +79 -0
  32. package/dist/components/sheet/popup-helpers.js +73 -0
  33. package/dist/components/sheet/popups/border-popup.js +75 -0
  34. package/dist/components/sheet/popups/border-preset-glyph.js +106 -0
  35. package/dist/components/sheet/popups/export-popup.js +51 -0
  36. package/dist/components/sheet/popups/fill-color-popup.js +37 -0
  37. package/dist/components/sheet/popups/find-popup.js +113 -0
  38. package/dist/components/sheet/popups/font-color-popup.js +37 -0
  39. package/dist/components/sheet/popups/font-size-popup.js +63 -0
  40. package/dist/components/sheet/popups/import.worker.d.ts +21 -0
  41. package/dist/components/sheet/popups/import.worker.js +41 -0
  42. package/dist/components/sheet/popups/insert-image-popup.js +38 -0
  43. package/dist/components/sheet/sheet-context-menu.js +200 -0
  44. package/dist/components/sheet/sheet-tabs.js +162 -0
  45. package/dist/components/sheet/sheet-toolbar.js +106 -0
  46. package/dist/components/sheet/sheet.js +198 -0
  47. package/dist/components/sheet/style.d.ts +11 -0
  48. package/dist/components/sheet/style.js +12 -0
  49. package/dist/components/sheet/style2.css +654 -0
  50. package/dist/components/sheet/use-find-replace.js +161 -0
  51. package/dist/components/sheet/use-formula-ref-select.js +37 -0
  52. package/dist/components/sheet/use-formula-suggest.js +126 -0
  53. package/dist/components/sheet/use-sheet-grid.js +188 -0
  54. package/dist/components/sheet/use-sheet-state.js +122 -0
  55. package/dist/components/sheet/use-sheet-tabs-bar.js +90 -0
  56. package/dist/components/sheet/use-tool-groups.js +42 -0
  57. package/dist/components/sheet/use-tool-popup.js +123 -0
  58. package/dist/components/sheet/use-toolbar-scroll.js +72 -0
  59. package/dist/index.d.ts +18 -26
  60. package/dist/index.js +14 -24
  61. package/dist/report/binding.d.ts +45 -0
  62. package/dist/report/binding.js +175 -0
  63. package/dist/report/connector.d.ts +66 -0
  64. package/dist/report/connector.js +159 -0
  65. package/dist/report/export-xlsx.d.ts +19 -0
  66. package/dist/report/export-xlsx.js +95 -0
  67. package/dist/report/filter-bar.d.ts +10 -0
  68. package/dist/report/filter-bar.js +31 -0
  69. package/dist/report/index.d.ts +12 -0
  70. package/dist/report/params.d.ts +13 -0
  71. package/dist/report/params.js +61 -0
  72. package/dist/report/render/aggregate.js +157 -0
  73. package/dist/report/render/builder.d.ts +12 -0
  74. package/dist/report/render/builder.js +139 -0
  75. package/dist/report/render/coordinate.d.ts +25 -0
  76. package/dist/report/render/coordinate.js +733 -0
  77. package/dist/report/render/index.d.ts +14 -0
  78. package/dist/report/render/index.js +21 -0
  79. package/dist/report/render/style-resolver.js +50 -0
  80. package/dist/report/render/template-index.d.ts +24 -0
  81. package/dist/report/render/template-index.js +137 -0
  82. package/dist/report/rules/evaluate.d.ts +25 -0
  83. package/dist/report/rules/evaluate.js +82 -0
  84. package/dist/report/rules/index.d.ts +2 -0
  85. package/dist/report/template.d.ts +59 -0
  86. package/dist/report/template.js +80 -0
  87. package/dist/report/types.d.ts +85 -0
  88. package/dist/tools/builtin.js +279 -8
  89. package/dist/tools/context.d.ts +54 -11
  90. package/dist/tools/context.js +28 -5
  91. package/dist/tools/download.js +68 -0
  92. package/dist/tools/export.worker.d.ts +16 -0
  93. package/dist/tools/export.worker.js +29 -0
  94. package/dist/tools/registry.d.ts +7 -4
  95. package/dist/tools/registry.js +1 -3
  96. package/dist/types/index.d.ts +3 -1
  97. package/dist/types/report-designer.d.ts +36 -0
  98. package/dist/types/report-viewer.d.ts +38 -0
  99. package/dist/types/sheet.d.ts +22 -5
  100. package/package.json +10 -7
  101. package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
  102. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  103. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  104. package/src/components/report/__test__/report-designer.test.ts +388 -0
  105. package/src/components/report/__test__/report-viewer.test.ts +334 -0
  106. package/src/components/report/__test__/topology.test.ts +143 -0
  107. package/src/components/report/__test__/use-report-designer.test.ts +565 -0
  108. package/src/components/report/binding-badge.ts +73 -0
  109. package/src/components/report/designer/cell-coords.ts +94 -0
  110. package/src/components/report/designer/col-widths.ts +33 -0
  111. package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
  112. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  113. package/src/components/report/designer/float-panel.vue +387 -0
  114. package/src/components/report/designer/role.ts +32 -0
  115. package/src/components/report/designer/rule-preview.vue +40 -0
  116. package/src/components/report/designer/rule-row.vue +252 -0
  117. package/src/components/report/designer/rules-dialog.vue +126 -0
  118. package/src/components/report/designer/topology-overlay.vue +127 -0
  119. package/src/components/report/designer/topology.ts +124 -0
  120. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  121. package/src/components/report/designer-hub.vue +198 -0
  122. package/src/components/report/field-panel-helpers.ts +44 -0
  123. package/src/components/report/field-panel.vue +136 -0
  124. package/src/components/report/filter-bar.vue +77 -0
  125. package/src/components/report/hub-connection-form.vue +124 -0
  126. package/src/components/report/hub-dataset-editor.vue +369 -0
  127. package/src/components/report/index.ts +2 -0
  128. package/src/components/report/report-designer.vue +356 -0
  129. package/src/components/report/report-viewer.vue +128 -0
  130. package/src/components/report/style-designer.scss +583 -0
  131. package/src/components/report/style-hub.scss +292 -0
  132. package/src/components/report/style-viewer.scss +104 -0
  133. package/src/components/report/style.scss +3 -0
  134. package/src/components/report/style.ts +20 -0
  135. package/src/components/report/use-report-designer.ts +547 -0
  136. package/src/components/report/use-report-viewer.ts +79 -0
  137. package/src/components/sheet/__test__/formula-bar.test.ts +474 -0
  138. package/src/components/sheet/__test__/import-file.test.ts +183 -0
  139. package/src/components/sheet/__test__/insert-count-menu-item.test.ts +123 -0
  140. package/src/components/sheet/__test__/insert-image-popup.test.ts +89 -0
  141. package/src/components/sheet/__test__/insert-image.test.ts +100 -0
  142. package/src/components/sheet/__test__/sheet-component.test.ts +993 -0
  143. package/src/components/sheet/__test__/sheet-context-menu.test.ts +206 -0
  144. package/src/components/sheet/__test__/use-formula-ref-select.test.ts +35 -0
  145. package/src/components/sheet/__test__/use-formula-suggest.test.ts +83 -0
  146. package/src/components/sheet/formula-bar.vue +473 -0
  147. package/src/components/sheet/formula-suggest-list.vue +64 -0
  148. package/src/components/sheet/import-file.ts +173 -0
  149. package/src/components/sheet/insert-count-menu-item.vue +69 -0
  150. package/src/components/sheet/insert-image.ts +90 -0
  151. package/src/components/sheet/parsing.ts +11 -0
  152. package/src/components/sheet/popup-helpers.ts +50 -0
  153. package/src/components/sheet/popups/border-popup.vue +93 -0
  154. package/src/components/sheet/popups/border-preset-glyph.vue +111 -0
  155. package/src/components/sheet/popups/export-popup.vue +52 -0
  156. package/src/components/sheet/popups/fill-color-popup.vue +39 -0
  157. package/src/components/sheet/popups/find-popup.vue +111 -0
  158. package/src/components/sheet/popups/font-color-popup.vue +39 -0
  159. package/src/components/sheet/popups/font-size-popup.vue +52 -0
  160. package/src/components/sheet/popups/import.worker.ts +66 -0
  161. package/src/components/sheet/popups/insert-image-popup.vue +39 -0
  162. package/src/components/sheet/sheet-context-menu.ts +229 -0
  163. package/src/components/sheet/sheet-tabs.vue +162 -0
  164. package/src/components/sheet/sheet-toolbar.vue +91 -0
  165. package/src/components/sheet/sheet.vue +211 -0
  166. package/src/components/sheet/style.scss +737 -0
  167. package/src/components/sheet/style.ts +13 -0
  168. package/src/components/sheet/use-find-replace.ts +200 -0
  169. package/src/components/sheet/use-formula-ref-select.ts +34 -0
  170. package/src/components/sheet/use-formula-suggest.ts +150 -0
  171. package/src/components/sheet/use-sheet-grid.ts +247 -0
  172. package/src/components/sheet/use-sheet-state.ts +161 -0
  173. package/src/components/sheet/use-sheet-tabs-bar.ts +115 -0
  174. package/src/components/sheet/use-tool-groups.ts +61 -0
  175. package/src/components/sheet/use-tool-popup.ts +150 -0
  176. package/src/components/sheet/use-toolbar-scroll.ts +95 -0
  177. package/src/env.d.ts +11 -0
  178. package/src/index.ts +39 -14
  179. package/src/report/__test__/binding.test.ts +218 -0
  180. package/src/report/__test__/connector.test.ts +218 -0
  181. package/src/report/__test__/export-xlsx.test.ts +118 -0
  182. package/src/report/__test__/filter-bar.test.ts +58 -0
  183. package/src/report/__test__/params.test.ts +156 -0
  184. package/src/report/__test__/render.test.ts +728 -0
  185. package/src/report/__test__/rules.test.ts +131 -0
  186. package/src/report/__test__/template.test.ts +355 -0
  187. package/src/report/binding.ts +243 -0
  188. package/src/report/connector.ts +205 -0
  189. package/src/report/export-xlsx.ts +118 -0
  190. package/src/report/filter-bar.ts +36 -0
  191. package/src/report/index.ts +13 -0
  192. package/src/report/params.ts +96 -0
  193. package/src/report/render/__test__/aggregate.test.ts +16 -0
  194. package/src/report/render/__test__/coordinate.test.ts +405 -0
  195. package/src/report/render/aggregate.ts +240 -0
  196. package/src/report/render/builder.ts +211 -0
  197. package/src/report/render/coordinate.ts +1239 -0
  198. package/src/report/render/index.ts +30 -0
  199. package/src/report/render/style-resolver.ts +68 -0
  200. package/src/report/render/template-index.ts +200 -0
  201. package/src/report/rules/evaluate.ts +113 -0
  202. package/src/report/rules/index.ts +7 -0
  203. package/src/report/template.ts +141 -0
  204. package/src/report/types.ts +96 -0
  205. package/src/tools/__test__/builtin.test.ts +208 -66
  206. package/src/tools/__test__/context.test.ts +62 -2
  207. package/src/tools/builtin.ts +350 -6
  208. package/src/tools/context.ts +89 -11
  209. package/src/tools/download.ts +82 -0
  210. package/src/tools/export.worker.ts +61 -0
  211. package/src/tools/registry.ts +16 -1
  212. package/src/types/index.ts +2 -0
  213. package/src/types/report-designer.ts +36 -0
  214. package/src/types/report-viewer.ts +37 -0
  215. package/src/types/sheet.ts +26 -3
  216. package/dist/core/address.d.ts +0 -60
  217. package/dist/core/address.js +0 -142
  218. package/dist/core/address.js.map +0 -1
  219. package/dist/core/cell-store.d.ts +0 -61
  220. package/dist/core/cell-store.js +0 -135
  221. package/dist/core/cell-store.js.map +0 -1
  222. package/dist/core/command/default-registry.d.ts +0 -10
  223. package/dist/core/command/default-registry.js +0 -18
  224. package/dist/core/command/default-registry.js.map +0 -1
  225. package/dist/core/command/history.d.ts +0 -64
  226. package/dist/core/command/history.js +0 -127
  227. package/dist/core/command/history.js.map +0 -1
  228. package/dist/core/command/merge-cells.d.ts +0 -21
  229. package/dist/core/command/merge-cells.js +0 -74
  230. package/dist/core/command/merge-cells.js.map +0 -1
  231. package/dist/core/command/registry.d.ts +0 -17
  232. package/dist/core/command/registry.js +0 -26
  233. package/dist/core/command/registry.js.map +0 -1
  234. package/dist/core/command/set-cell-formula.d.ts +0 -17
  235. package/dist/core/command/set-cell-formula.js +0 -29
  236. package/dist/core/command/set-cell-formula.js.map +0 -1
  237. package/dist/core/command/set-cell-value.d.ts +0 -22
  238. package/dist/core/command/set-cell-value.js +0 -35
  239. package/dist/core/command/set-cell-value.js.map +0 -1
  240. package/dist/core/command/types.d.ts +0 -68
  241. package/dist/core/events.js +0 -29
  242. package/dist/core/events.js.map +0 -1
  243. package/dist/core/fill.d.ts +0 -34
  244. package/dist/core/fill.js +0 -320
  245. package/dist/core/fill.js.map +0 -1
  246. package/dist/core/formula/ast.d.ts +0 -58
  247. package/dist/core/formula/ast.js +0 -38
  248. package/dist/core/formula/ast.js.map +0 -1
  249. package/dist/core/formula/dependency-graph.d.ts +0 -70
  250. package/dist/core/formula/dependency-graph.js +0 -269
  251. package/dist/core/formula/dependency-graph.js.map +0 -1
  252. package/dist/core/formula/errors.d.ts +0 -25
  253. package/dist/core/formula/errors.js +0 -36
  254. package/dist/core/formula/errors.js.map +0 -1
  255. package/dist/core/formula/evaluator.d.ts +0 -41
  256. package/dist/core/formula/evaluator.js +0 -154
  257. package/dist/core/formula/evaluator.js.map +0 -1
  258. package/dist/core/formula/functions.d.ts +0 -30
  259. package/dist/core/formula/functions.js +0 -209
  260. package/dist/core/formula/functions.js.map +0 -1
  261. package/dist/core/formula/parser.d.ts +0 -7
  262. package/dist/core/formula/parser.js +0 -208
  263. package/dist/core/formula/parser.js.map +0 -1
  264. package/dist/core/formula/tokenizer.d.ts +0 -37
  265. package/dist/core/formula/tokenizer.js +0 -146
  266. package/dist/core/formula/tokenizer.js.map +0 -1
  267. package/dist/core/merge-manager.d.ts +0 -64
  268. package/dist/core/merge-manager.js +0 -111
  269. package/dist/core/merge-manager.js.map +0 -1
  270. package/dist/core/selection.d.ts +0 -31
  271. package/dist/core/selection.js +0 -62
  272. package/dist/core/selection.js.map +0 -1
  273. package/dist/core/sheet.d.ts +0 -116
  274. package/dist/core/sheet.js +0 -224
  275. package/dist/core/sheet.js.map +0 -1
  276. package/dist/core/workbook.d.ts +0 -43
  277. package/dist/core/workbook.js +0 -77
  278. package/dist/core/workbook.js.map +0 -1
  279. package/dist/grid/sheet-grid.d.ts +0 -81
  280. package/dist/grid/sheet-grid.js +0 -331
  281. package/dist/grid/sheet-grid.js.map +0 -1
  282. package/dist/grid/vtable-theme.js +0 -80
  283. package/dist/grid/vtable-theme.js.map +0 -1
  284. package/dist/tools/builtin.js.map +0 -1
  285. package/dist/tools/context.js.map +0 -1
  286. package/dist/tools/registry.js.map +0 -1
  287. package/dist/vue/index.d.ts +0 -2
  288. package/dist/vue/sheet.js +0 -211
  289. package/dist/vue/sheet.js.map +0 -1
  290. package/dist/vue/sheet.vue.d.ts +0 -24
  291. package/dist/vue/style.d.ts +0 -1
  292. package/dist/vue/style.js +0 -2
  293. package/dist/vue/style2.css +0 -93
  294. package/src/core/__test__/address.test.ts +0 -113
  295. package/src/core/__test__/cell-store.test.ts +0 -97
  296. package/src/core/__test__/command.test.ts +0 -377
  297. package/src/core/__test__/fill.test.ts +0 -140
  298. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  299. package/src/core/__test__/formula-engine.test.ts +0 -470
  300. package/src/core/__test__/formula-functions.test.ts +0 -178
  301. package/src/core/__test__/formula-parser.test.ts +0 -160
  302. package/src/core/__test__/history.test.ts +0 -225
  303. package/src/core/__test__/merge-manager.test.ts +0 -106
  304. package/src/core/__test__/sheet.test.ts +0 -142
  305. package/src/core/address.ts +0 -166
  306. package/src/core/cell-store.ts +0 -173
  307. package/src/core/command/default-registry.ts +0 -15
  308. package/src/core/command/history.ts +0 -167
  309. package/src/core/command/index.ts +0 -7
  310. package/src/core/command/merge-cells.ts +0 -74
  311. package/src/core/command/registry.ts +0 -30
  312. package/src/core/command/set-cell-formula.ts +0 -29
  313. package/src/core/command/set-cell-value.ts +0 -38
  314. package/src/core/command/types.ts +0 -73
  315. package/src/core/events.ts +0 -37
  316. package/src/core/fill.ts +0 -314
  317. package/src/core/formula/ast.ts +0 -66
  318. package/src/core/formula/dependency-graph.ts +0 -329
  319. package/src/core/formula/errors.ts +0 -41
  320. package/src/core/formula/evaluator.ts +0 -219
  321. package/src/core/formula/functions.ts +0 -266
  322. package/src/core/formula/index.ts +0 -7
  323. package/src/core/formula/parser.ts +0 -198
  324. package/src/core/formula/tokenizer.ts +0 -160
  325. package/src/core/merge-manager.ts +0 -158
  326. package/src/core/selection.ts +0 -66
  327. package/src/core/sheet.ts +0 -282
  328. package/src/core/workbook.ts +0 -99
  329. package/src/grid/__test__/canvas-mock.ts +0 -49
  330. package/src/grid/__test__/setup.ts +0 -3
  331. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  332. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  333. package/src/grid/sheet-grid.ts +0 -407
  334. package/src/grid/vtable-theme.ts +0 -61
  335. package/src/vue/__test__/sheet-component.test.ts +0 -267
  336. package/src/vue/sheet.vue +0 -235
  337. package/src/vue/style.scss +0 -94
  338. package/src/vue/style.ts +0 -2
  339. /package/src/{vue → components/sheet}/index.ts +0 -0
@@ -0,0 +1,474 @@
1
+ import { parseRange } from '@veltra/sheet-core/core/address'
2
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { createApp, h, nextTick, type App } from 'vue'
5
+
6
+ import { USheet } from '../../../index'
7
+ import { createSheetContext } from '../../../tools/context'
8
+ import type { SheetExposed } from '../../../types'
9
+ import UFormulaBar from '../formula-bar.vue'
10
+
11
+ const apps: App[] = []
12
+ const containers: HTMLElement[] = []
13
+
14
+ function mount(
15
+ props: () => Record<string, unknown> = () => ({}),
16
+ exposedRef?: { value: SheetExposed | undefined }
17
+ ) {
18
+ const el = document.createElement('div')
19
+ el.style.width = '800px'
20
+ el.style.height = '600px'
21
+ document.body.appendChild(el)
22
+ containers.push(el)
23
+ const app = createApp({
24
+ render: () =>
25
+ h(USheet, {
26
+ ...props(),
27
+ ref: (value: unknown) => {
28
+ if (exposedRef) exposedRef.value = value as SheetExposed | undefined
29
+ }
30
+ })
31
+ })
32
+ app.mount(el)
33
+ apps.push(app)
34
+ return { app, el }
35
+ }
36
+
37
+ function mountFormulaBar(sheet: Workbook['activeSheet']) {
38
+ const el = document.createElement('div')
39
+ document.body.appendChild(el)
40
+ containers.push(el)
41
+ const exposed: { value: InstanceType<typeof UFormulaBar> | undefined } = { value: undefined }
42
+ const app = createApp({
43
+ render: () =>
44
+ h(UFormulaBar, {
45
+ sheet,
46
+ context: createSheetContext(sheet),
47
+ ref: (value: unknown) => {
48
+ exposed.value = value as InstanceType<typeof UFormulaBar> | undefined
49
+ }
50
+ })
51
+ })
52
+ app.mount(el)
53
+ apps.push(app)
54
+ return { el, exposed }
55
+ }
56
+
57
+ function createWorkbook() {
58
+ const workbook = new Workbook()
59
+ workbook.addSheet('Sheet2')
60
+ return workbook
61
+ }
62
+
63
+ afterEach(() => {
64
+ while (apps.length) apps.pop()!.unmount()
65
+ while (containers.length) containers.pop()!.remove()
66
+ })
67
+
68
+ function nameBox(el: HTMLElement): HTMLInputElement {
69
+ return el.querySelector<HTMLInputElement>('.u-sheet__name-box')!
70
+ }
71
+
72
+ function fxInput(el: HTMLElement): HTMLTextAreaElement {
73
+ return el.querySelector<HTMLTextAreaElement>('.u-sheet__fx-input')!
74
+ }
75
+
76
+ /** 名称框输入并按键(默认 Enter) */
77
+ function typeName(el: HTMLElement, text: string, key = 'Enter'): void {
78
+ const input = nameBox(el)
79
+ input.value = text
80
+ input.dispatchEvent(new Event('input', { bubbles: true }))
81
+ if (key) input.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
82
+ }
83
+
84
+ /** fx 输入栏聚焦 → 输入 → 按键(key = null 时不按键,保持编辑态) */
85
+ function typeFx(el: HTMLElement, text: string, key: string | null = null): void {
86
+ const input = fxInput(el)
87
+ input.dispatchEvent(new FocusEvent('focus'))
88
+ input.value = text
89
+ input.dispatchEvent(new Event('input', { bubbles: true }))
90
+ if (key) input.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
91
+ }
92
+
93
+ describe('USheet 公式栏(名称框 + fx 输入栏)', () => {
94
+ it('选区变化 → 名称框 / 输入栏内容正确(普通值 / 公式原文 / 空 / 合并格锚点)', async () => {
95
+ const workbook = new Workbook()
96
+ const sheet = workbook.activeSheet
97
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
98
+ await nextTick()
99
+
100
+ // 默认选区 A1:名称框显示 A1,fx 输入栏可用
101
+ expect(nameBox(el).value).toBe('A1')
102
+ expect(fxInput(el).disabled).toBe(false)
103
+ expect(fxInput(el).value).toBe('')
104
+
105
+ // 普通值 → 原始值文本
106
+ sheet.setCellValue({ row: 0, col: 0 }, 'hello')
107
+ await nextTick()
108
+ expect(fxInput(el).value).toBe('hello')
109
+
110
+ // 公式格 → '=' + f 原文
111
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
112
+ await nextTick()
113
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
114
+
115
+ // 区域选区 → A1:B2 序列化;活动格(区域起点)为空 → 输入栏空
116
+ sheet.selectRange({ start: { row: 1, col: 1 }, end: { row: 3, col: 3 } })
117
+ await nextTick()
118
+ expect(nameBox(el).value).toBe('B2:D4')
119
+ expect(fxInput(el).value).toBe('')
120
+
121
+ // 合并格锚点:点被覆盖格 → 名称框显示锚点地址(B2)
122
+ sheet.mergeCells({ start: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
123
+ sheet.selectCell({ row: 2, col: 2 })
124
+ await nextTick()
125
+ expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 1 })
126
+ expect(nameBox(el).value).toBe('B2')
127
+ })
128
+
129
+ it('输入 =SUM(A1:A2) 提交 → 模型公式与计算值正确;普通文本原样存储;Esc 不改模型', async () => {
130
+ const workbook = new Workbook()
131
+ const sheet = workbook.activeSheet
132
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
133
+ sheet.setCellValue({ row: 1, col: 0 }, 2)
134
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
135
+ await nextTick()
136
+ sheet.selectCell({ row: 2, col: 0 })
137
+ await nextTick()
138
+
139
+ // 公式提交:'=' 前缀自动公式路径;计算值正确;提交后显示公式原文;选区保持
140
+ typeFx(el, '=SUM(A1:A2)', 'Enter')
141
+ await nextTick()
142
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1:A2)', v: 3, t: 'n' })
143
+ expect(sheet.getDisplayValue({ row: 2, col: 0 })).toBe(3)
144
+ expect(fxInput(el).value).toBe('=SUM(A1:A2)')
145
+ expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
146
+
147
+ // 普通文本原样存储(首尾空格不 trim)
148
+ typeFx(el, ' hello ', 'Enter')
149
+ await nextTick()
150
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: ' hello ', t: 's' })
151
+
152
+ // Esc 取消:不改模型,输入栏还原为模型内容
153
+ typeFx(el, 'zzz', 'Escape')
154
+ await nextTick()
155
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: ' hello ' })
156
+ expect(fxInput(el).value).toBe(' hello ')
157
+
158
+ // 失焦提交(Excel 语义:点击网格等场景自然提交)
159
+ typeFx(el, 'blur-submit')
160
+ fxInput(el).dispatchEvent(new FocusEvent('blur'))
161
+ await nextTick()
162
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: 'blur-submit' })
163
+ })
164
+
165
+ it('名称框输入 B3 / B3:D5 跳转正确;非法输入被拒绝', async () => {
166
+ const workbook = new Workbook()
167
+ const sheet = workbook.activeSheet
168
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
169
+ await nextTick()
170
+
171
+ // 单格地址 → selectCell 跳转
172
+ typeName(el, 'B3')
173
+ await nextTick()
174
+ expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 1 })
175
+ expect(nameBox(el).value).toBe('B3')
176
+
177
+ // 区域 → selectRange 跳转
178
+ typeName(el, 'B3:D5')
179
+ await nextTick()
180
+ expect(sheet.getSelection().ranges[0]).toEqual({
181
+ start: { row: 2, col: 1 },
182
+ end: { row: 4, col: 3 }
183
+ })
184
+ expect(nameBox(el).value).toBe('B3:D5')
185
+
186
+ // 非法:不跳转、不写入,名称框还原为当前选区地址
187
+ typeName(el, 'INVALID!')
188
+ await nextTick()
189
+ expect(sheet.getSelection().ranges[0]).toEqual({
190
+ start: { row: 2, col: 1 },
191
+ end: { row: 4, col: 3 }
192
+ })
193
+ expect(nameBox(el).value).toBe('B3:D5')
194
+
195
+ // Esc:还原显示
196
+ typeName(el, 'X99', 'Escape')
197
+ await nextTick()
198
+ expect(sheet.getSelection().ranges[0]).toEqual({
199
+ start: { row: 2, col: 1 },
200
+ end: { row: 4, col: 3 }
201
+ })
202
+ expect(nameBox(el).value).toBe('B3:D5')
203
+ })
204
+
205
+ it('公式栏编辑期间网格事件不打断输入;提交后网格渲染计算值', async () => {
206
+ const workbook = new Workbook()
207
+ const sheet = workbook.activeSheet
208
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
209
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
210
+ sheet.setCellValue({ row: 1, col: 0 }, 2)
211
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
212
+ await nextTick()
213
+ sheet.selectCell({ row: 2, col: 0 })
214
+ await nextTick()
215
+
216
+ // 聚焦进入编辑态并输入(不按键)
217
+ typeFx(el, '=SUM(A1:A2)')
218
+ // 网格侧选区变化(编辑态锁:输入内容不被覆盖)
219
+ sheet.selectCell({ row: 5, col: 5 })
220
+ await nextTick()
221
+ expect(fxInput(el).value).toBe('=SUM(A1:A2)')
222
+
223
+ // 提交 → 写进入编辑时的格(A3),而非当前选区格(F6)
224
+ fxInput(el).dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
225
+ await nextTick()
226
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1:A2)', v: 3 })
227
+
228
+ // 提交后网格渲染计算值(模型 A3 → 表格坐标 (1,3))
229
+ expect(exposed.value?.getGrid()?.getTable().getCellValue(1, 3)).toBe(3)
230
+ })
231
+
232
+ it('网格侧编辑提交(change_cell_value)后公式栏同步显示', async () => {
233
+ const workbook = new Workbook()
234
+ const sheet = workbook.activeSheet
235
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
236
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
237
+ await nextTick()
238
+ sheet.selectCell({ row: 0, col: 0 })
239
+ await nextTick()
240
+
241
+ // 模拟网格双击编辑提交(与 grid 测试同一路径:change_cell_value → 模型回写)
242
+ exposed.value?.getGrid()?.getTable().changeCellValue(1, 1, 'grid-edited', false, true)
243
+ await nextTick()
244
+ expect(fxInput(el).value).toBe('grid-edited')
245
+ })
246
+
247
+ it('网格编辑器打开(startEditCell)→ 公式栏镜像显示公式原文,提交后退出镜像', async () => {
248
+ const workbook = new Workbook()
249
+ const sheet = workbook.activeSheet
250
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
251
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
252
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
253
+ await nextTick()
254
+ sheet.selectCell({ row: 0, col: 0 })
255
+ await nextTick()
256
+
257
+ // 程序化进入网格编辑(表格坐标 (1,1) = 模型 A1)
258
+ exposed.value?.getGrid()?.getTable().startEditCell(1, 1)
259
+ await nextTick()
260
+ expect(fxInput(el).readOnly).toBe(true)
261
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
262
+
263
+ // 提交 → 编辑器 onEnd → 公式栏退出镜像(显示模型内容)
264
+ exposed.value?.getGrid()?.getTable().completeEditCell()
265
+ await nextTick()
266
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ f: 'SUM(1,2)' })
267
+ expect(fxInput(el).readOnly).toBe(false)
268
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
269
+ })
270
+
271
+ it('tab 切换:重绑订阅并刷新(新 sheet 的选区 / 单元格内容回显)', async () => {
272
+ const workbook = createWorkbook()
273
+ const sheet1 = workbook.activeSheet
274
+ const sheet2 = workbook.getSheet('Sheet2')!
275
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
276
+ await nextTick()
277
+ // Sheet1 默认选区 A1
278
+ expect(nameBox(el).value).toBe('A1')
279
+
280
+ // Sheet2 的选区不影响公式栏(订阅的是 Sheet1)
281
+ sheet2.selectCell({ row: 1, col: 1 })
282
+ await nextTick()
283
+ expect(nameBox(el).value).toBe('A1')
284
+
285
+ workbook.activateSheet('Sheet2')
286
+ await nextTick()
287
+ expect(nameBox(el).value).toBe('B2')
288
+
289
+ sheet2.setCellValue({ row: 1, col: 1 }, 's2')
290
+ await nextTick()
291
+ expect(fxInput(el).value).toBe('s2')
292
+
293
+ // 切回 Sheet1:公式栏跟随
294
+ workbook.activateSheet('Sheet1')
295
+ await nextTick()
296
+ expect(nameBox(el).value).toBe('A1')
297
+ })
298
+
299
+ it('fx 编辑中切换 tab:编辑态重置,草稿不写入任何 sheet', async () => {
300
+ const workbook = createWorkbook()
301
+ const sheet1 = workbook.activeSheet
302
+ const sheet2 = workbook.getSheet('Sheet2')!
303
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
304
+ await nextTick()
305
+ sheet1.selectCell({ row: 0, col: 0 })
306
+ await nextTick()
307
+
308
+ // 进入 fx 编辑态并输入草稿
309
+ const input = fxInput(el)
310
+ input.dispatchEvent(new Event('focus', { bubbles: true }))
311
+ input.value = 'draft'
312
+ input.dispatchEvent(new Event('input', { bubbles: true }))
313
+ await nextTick()
314
+
315
+ // 切 tab → 编辑态重置(editing=false、editAddr=null)
316
+ workbook.activateSheet('Sheet2')
317
+ await nextTick()
318
+ // fx 显示新 sheet 内容(空),不再显示草稿
319
+ expect(input.value).toBe('')
320
+ // Enter 提交:编辑态已重置 → 不写入
321
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
322
+ await nextTick()
323
+ expect(sheet2.getCellData({ row: 0, col: 0 })).toBeUndefined()
324
+ expect(sheet1.getCellData({ row: 0, col: 0 })).toBeUndefined()
325
+ })
326
+
327
+ it('showFormulaBar=false 隐藏公式栏', async () => {
328
+ const { el } = mount(() => ({ showFormulaBar: false }))
329
+ await nextTick()
330
+ expect(el.querySelector('.u-sheet__formula-bar')).toBeNull()
331
+ })
332
+ })
333
+
334
+ /** 设置 fx 文本并同步光标到末尾(触发补全 / 引用选择上下文) */
335
+ function setFxText(el: HTMLElement, text: string): HTMLTextAreaElement {
336
+ const input = fxInput(el)
337
+ input.dispatchEvent(new FocusEvent('focus'))
338
+ input.value = text
339
+ input.dispatchEvent(new Event('input', { bubbles: true }))
340
+ input.setSelectionRange(text.length, text.length)
341
+ input.dispatchEvent(new Event('keyup', { bubbles: true }))
342
+ return input
343
+ }
344
+
345
+ function suggestList(el: HTMLElement): HTMLElement | null {
346
+ return el.querySelector('.u-sheet__fx-suggest')
347
+ }
348
+
349
+ describe('USheet 公式栏:函数补全与引用选择', () => {
350
+ it('输入 = 弹出候选含 SUM;SU 过滤到 SUM;↓+Enter 替换为 =SUM() 且光标在括号内', async () => {
351
+ const workbook = new Workbook()
352
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
353
+ await nextTick()
354
+
355
+ setFxText(el, '=')
356
+ await nextTick()
357
+ expect(suggestList(el)).not.toBeNull()
358
+ const emptyPrefixNames = [...el.querySelectorAll('.u-sheet__fx-suggest-item')].map(
359
+ (n) => n.querySelector('.u-sheet__fx-suggest-sig')!.textContent
360
+ )
361
+ expect(emptyPrefixNames.length).toBeGreaterThan(0)
362
+ expect(emptyPrefixNames.some((t) => t?.startsWith('SUM('))).toBe(true)
363
+
364
+ setFxText(el, '=SU')
365
+ await nextTick()
366
+ const items = [...el.querySelectorAll('.u-sheet__fx-suggest-item')].map(
367
+ (n) => n.querySelector('.u-sheet__fx-suggest-sig')!.textContent
368
+ )
369
+ expect(items.some((t) => t?.startsWith('SUM('))).toBe(true)
370
+
371
+ const input = fxInput(el)
372
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
373
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
374
+ await nextTick()
375
+ expect(input.value).toBe('=SUM()')
376
+ expect(input.selectionStart).toBe(5)
377
+ expect(suggestList(el)).toBeNull()
378
+ })
379
+
380
+ it('Esc 分层:先关候选再取消编辑;候选开着时 Enter 确认而非提交', async () => {
381
+ const workbook = new Workbook()
382
+ const sheet = workbook.activeSheet
383
+ sheet.setCellValue({ row: 0, col: 0 }, 'keep')
384
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
385
+ await nextTick()
386
+
387
+ const input = setFxText(el, '=S')
388
+ await nextTick()
389
+ expect(suggestList(el)).not.toBeNull()
390
+
391
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
392
+ await nextTick()
393
+ expect(suggestList(el)).toBeNull()
394
+ expect(input.value).toBe('=S') // 仍在编辑
395
+
396
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
397
+ await nextTick()
398
+ expect(input.value).toBe('keep') // 取消还原
399
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'keep' })
400
+ })
401
+
402
+ it('引用插入文本与光标位置;blur 抑制;非引用选择失焦仍提交', async () => {
403
+ const workbook = new Workbook()
404
+ const sheet = workbook.activeSheet
405
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
406
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
407
+ await nextTick()
408
+ sheet.selectCell({ row: 2, col: 0 })
409
+ await nextTick()
410
+
411
+ const input = setFxText(el, '=SUM(')
412
+ await nextTick()
413
+
414
+ // pointerdown on grid → blur 不提交(引用选择);监听绑定在实例容器(LRU 缓存)
415
+ const gridEl = el.querySelector('.u-sheet__grid-instance')!
416
+ gridEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
417
+ input.dispatchEvent(new FocusEvent('blur'))
418
+ await nextTick()
419
+ expect(sheet.getCellData({ row: 2, col: 0 })).toBeUndefined()
420
+ expect(input.value).toBe('=SUM(')
421
+
422
+ // 网格选区拦截 → handleRefSelect 路径(VTable 时序见 sheet-core selection-debug)
423
+ const { el: barEl, exposed: barExposed } = mountFormulaBar(sheet)
424
+ await nextTick()
425
+ setFxText(barEl, '=SUM(')
426
+ await nextTick()
427
+ barExposed.value!.handleRefSelect(parseRange('A1:B2'))
428
+ await nextTick()
429
+ const barInput = fxInput(barEl)
430
+ expect(barInput.value).toBe('=SUM(A1:B2')
431
+ expect(barInput.selectionStart).toBe('=SUM(A1:B2'.length)
432
+
433
+ // 非引用选择场景失焦提交
434
+ setFxText(el, 'plain')
435
+ await nextTick()
436
+ fxInput(el).dispatchEvent(new FocusEvent('blur'))
437
+ await nextTick()
438
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: 'plain' })
439
+ })
440
+
441
+ it('网格镜像期不弹补全', async () => {
442
+ const workbook = new Workbook()
443
+ const sheet = workbook.activeSheet
444
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
445
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
446
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
447
+ await nextTick()
448
+ sheet.selectCell({ row: 0, col: 0 })
449
+ await nextTick()
450
+
451
+ exposed.value?.getGrid()?.getTable().startEditCell(1, 1)
452
+ await nextTick()
453
+ expect(fxInput(el).readOnly).toBe(true)
454
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
455
+ expect(suggestList(el)).toBeNull()
456
+
457
+ exposed.value?.getGrid()?.getTable().completeEditCell()
458
+ await nextTick()
459
+ })
460
+
461
+ it('连续引用选择:=A1+ 后再插入 B1', async () => {
462
+ const sheet = new Workbook().activeSheet
463
+ const { el, exposed } = mountFormulaBar(sheet)
464
+ await nextTick()
465
+ sheet.selectCell({ row: 2, col: 0 })
466
+ await nextTick()
467
+
468
+ setFxText(el, '=A1+')
469
+ await nextTick()
470
+ exposed.value!.handleRefSelect(parseRange('B1'))
471
+ await nextTick()
472
+ expect(fxInput(el).value).toBe('=A1+B1')
473
+ })
474
+ })
@@ -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
+ })