@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,993 @@
1
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { createApp, h, nextTick, ref, type App } from 'vue'
4
+
5
+ import { USheet } from '../../../index'
6
+ import { registerTool, unregisterTool } from '../../../tools/registry'
7
+ import type { SheetExposed } from '../../../types'
8
+
9
+ const apps: App[] = []
10
+ const containers: HTMLElement[] = []
11
+
12
+ function mount(
13
+ props: () => Record<string, unknown> = () => ({}),
14
+ exposedRef?: { value: SheetExposed | undefined }
15
+ ) {
16
+ const el = document.createElement('div')
17
+ el.style.width = '800px'
18
+ el.style.height = '600px'
19
+ document.body.appendChild(el)
20
+ containers.push(el)
21
+ const app = createApp({
22
+ render: () =>
23
+ h(USheet, {
24
+ ...props(),
25
+ ref: (value: unknown) => {
26
+ if (exposedRef) exposedRef.value = value as SheetExposed | undefined
27
+ }
28
+ })
29
+ })
30
+ app.mount(el)
31
+ apps.push(app)
32
+ return { app, el }
33
+ }
34
+
35
+ function createWorkbook() {
36
+ const workbook = new Workbook()
37
+ workbook.addSheet('Sheet2')
38
+ return workbook
39
+ }
40
+
41
+ /** 按 data-tool-id / title 文本 / 原生 title 定位工具按钮(图标化后 textContent 可能为空) */
42
+ function toolButton(el: HTMLElement, idOrTitle: string): HTMLButtonElement | undefined {
43
+ return [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tool')].find(
44
+ (button) =>
45
+ button.dataset.toolId === idOrTitle ||
46
+ button.textContent?.includes(idOrTitle) ||
47
+ button.title.includes(idOrTitle)
48
+ )
49
+ }
50
+
51
+ /** 等待弹层打开:openPopup 走 setTimeout 宏任务(避免真实浏览器中同次 click 冒泡关闭面板) */
52
+ async function flushPopup(): Promise<void> {
53
+ await nextTick()
54
+ await new Promise((resolve) => setTimeout(resolve, 0))
55
+ await nextTick()
56
+ }
57
+
58
+ /** 等待查找防抖(use-find-replace 300ms 停键才全扫)+ 微任务 */
59
+ async function flushFind(): Promise<void> {
60
+ await new Promise((resolve) => setTimeout(resolve, 320))
61
+ await nextTick()
62
+ }
63
+
64
+ /**
65
+ * 等待工具栏状态落定:stateTick bump 走微任务合并(#25),一次 nextTick 只等到
66
+ * bump 微任务执行(Vue 渲染在其后的微任务),需要两个周期才能拿到新渲染结果。
67
+ */
68
+ async function flushState(): Promise<void> {
69
+ await nextTick()
70
+ await nextTick()
71
+ }
72
+
73
+ function tabs(el: HTMLElement): HTMLButtonElement[] {
74
+ return [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tab')]
75
+ }
76
+
77
+ afterEach(() => {
78
+ while (apps.length) apps.pop()!.unmount()
79
+ while (containers.length) containers.pop()!.remove()
80
+ })
81
+
82
+ describe('USheet 组件', () => {
83
+ it('挂载后默认选区 A1:名称框显示 A1、fx 输入栏可用', async () => {
84
+ const workbook = createWorkbook()
85
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
86
+ await nextTick()
87
+
88
+ const nameBox = el.querySelector<HTMLInputElement>('.u-sheet__name-box')!
89
+ const fxInput = el.querySelector<HTMLTextAreaElement>('.u-sheet__fx-input')!
90
+ expect(nameBox.value).toBe('A1')
91
+ expect(fxInput.disabled).toBe(false)
92
+ expect(workbook.activeSheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
93
+ })
94
+
95
+ it('挂载:工具栏图标化分组(history|cell|text|edit|insert|file),tabs 与 grid 挂载', async () => {
96
+ const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
97
+ await nextTick()
98
+
99
+ const tools = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tool')].map(
100
+ (button) => button.dataset.toolId
101
+ )
102
+ expect(tools).toEqual([
103
+ 'undo',
104
+ 'redo',
105
+ 'border',
106
+ 'fill-color',
107
+ 'merge',
108
+ 'unmerge',
109
+ 'bold',
110
+ 'italic',
111
+ 'underline',
112
+ 'strikethrough',
113
+ 'font-color',
114
+ 'font-size',
115
+ 'align-left',
116
+ 'align-center',
117
+ 'align-right',
118
+ 'valign-top',
119
+ 'valign-middle',
120
+ 'valign-bottom',
121
+ 'wrap-text',
122
+ 'find',
123
+ 'insert-image',
124
+ 'import',
125
+ 'export'
126
+ ])
127
+ // 六组之间五个分隔符
128
+ expect(el.querySelectorAll('.u-sheet__toolbar-divider')).toHaveLength(5)
129
+ // 图标化:每个内置工具有 icon,按钮无可见文字
130
+ for (const button of el.querySelectorAll<HTMLButtonElement>('.u-sheet__tool')) {
131
+ expect(button.querySelector('.u-sheet__tool-icon')).not.toBeNull()
132
+ expect(button.textContent?.trim()).toBe('')
133
+ expect(button.title.length).toBeGreaterThan(0)
134
+ }
135
+ // 已移除的工具不再出现
136
+ for (const id of [
137
+ 'insert-rows',
138
+ 'insert-cols',
139
+ 'delete-rows',
140
+ 'delete-cols',
141
+ 'freeze',
142
+ 'freeze-row',
143
+ 'freeze-col',
144
+ 'unfreeze',
145
+ 'export-xlsx',
146
+ 'export-csv'
147
+ ]) {
148
+ expect(toolButton(el, id)).toBeUndefined()
149
+ }
150
+
151
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
152
+ expect(tabs(el)[0]!.classList.contains('is-active')).toBe(true)
153
+
154
+ // VTable 已挂载到 grid 容器
155
+ expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
156
+
157
+ // 单行滚动结构:工具按钮全部位于滚动视口内的 list 容器
158
+ const scroll = el.querySelector('.u-sheet__toolbar-scroll')
159
+ const list = el.querySelector('.u-sheet__toolbar-list')
160
+ expect(scroll).not.toBeNull()
161
+ expect(list).not.toBeNull()
162
+ expect(scroll!.querySelectorAll('.u-sheet__tool').length).toBeGreaterThan(0)
163
+ expect(list!.querySelectorAll('.u-sheet__tool').length).toBeGreaterThan(0)
164
+ })
165
+
166
+ it('工具栏无溢出时不渲染导航箭头(happy-dom 无布局 → 判定不溢出)', async () => {
167
+ const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
168
+ await nextTick()
169
+
170
+ // happy-dom 下 clientWidth/scrollWidth 均为 0 → overflowing = false → 箭头不渲染
171
+ expect(el.querySelectorAll('.u-sheet__toolbar-nav')).toHaveLength(0)
172
+ })
173
+
174
+ it('内置 undo/redo 按钮随 history-change 置灰,点击等效快捷键', async () => {
175
+ const workbook = createWorkbook()
176
+ const sheet = workbook.activeSheet
177
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
178
+ await nextTick()
179
+
180
+ const undoButton = toolButton(el, 'undo')!
181
+ const redoButton = toolButton(el, 'redo')!
182
+ expect(undoButton.disabled).toBe(true)
183
+ expect(redoButton.disabled).toBe(true)
184
+
185
+ sheet.setCellValue({ row: 0, col: 0 }, 'x')
186
+ await flushState()
187
+ expect(undoButton.disabled).toBe(false)
188
+ expect(redoButton.disabled).toBe(true)
189
+
190
+ undoButton.click()
191
+ await flushState()
192
+ expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
193
+ expect(undoButton.disabled).toBe(true)
194
+ expect(redoButton.disabled).toBe(false)
195
+
196
+ redoButton.click()
197
+ await flushState()
198
+ expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'x', t: 's' })
199
+ })
200
+
201
+ it('tab 切换:active 样式移动,工具上下文指向当前 sheet', async () => {
202
+ const workbook = createWorkbook()
203
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
204
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
205
+ await nextTick()
206
+
207
+ expect(exposed.value?.getActiveSheet().name).toBe('Sheet1')
208
+ expect(exposed.value?.getContext().sheetName).toBe('Sheet1')
209
+
210
+ tabs(el)[1]!.click()
211
+ await nextTick()
212
+
213
+ expect(tabs(el)[1]!.classList.contains('is-active')).toBe(true)
214
+ // grid 实例缓存:切换后旧实例保留(LRU,visibility:hidden),容器内有 2 个 canvas
215
+ expect(el.querySelectorAll('.u-sheet__grid canvas')).toHaveLength(2)
216
+ // 激活实例可见、旧实例隐藏
217
+ const instances = el.querySelectorAll<HTMLElement>('.u-sheet__grid-instance')
218
+ expect(instances[0]!.style.visibility).toBe('hidden')
219
+ expect(instances[1]!.style.visibility).toBe('visible')
220
+ expect(exposed.value?.getActiveSheet().name).toBe('Sheet2')
221
+ // 工具上下文(与工具按钮同一门面)指向切换后的 sheet
222
+ expect(exposed.value?.getContext().sheetName).toBe('Sheet2')
223
+
224
+ // grid 已重建到 Sheet2:写入 Sheet2 应在表格可见
225
+ workbook.getSheet('Sheet2')!.setCellValue({ row: 0, col: 0 }, 's2')
226
+ await nextTick()
227
+ expect(exposed.value?.getGrid()?.getTable().getCellValue(1, 1)).toBe('s2')
228
+ })
229
+
230
+ it('props 变更响应:showToolbar/showTabs 显隐,rows 变化重建 grid', async () => {
231
+ const workbook = createWorkbook()
232
+ const state = ref({ rows: 10, showToolbar: true, showTabs: true })
233
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
234
+ const { el } = mount(
235
+ () => ({
236
+ workbook,
237
+ rows: state.value.rows,
238
+ cols: 6,
239
+ showToolbar: state.value.showToolbar,
240
+ showTabs: state.value.showTabs
241
+ }),
242
+ exposed
243
+ )
244
+ await nextTick()
245
+
246
+ const gridBefore = exposed.value?.getGrid()
247
+ expect(gridBefore).toBeDefined()
248
+
249
+ state.value = { ...state.value, showToolbar: false, showTabs: false }
250
+ await nextTick()
251
+ expect(el.querySelector('.u-sheet__toolbar')).toBeNull()
252
+ expect(el.querySelector('.u-sheet__tabs')).toBeNull()
253
+
254
+ state.value = { ...state.value, rows: 20 }
255
+ await nextTick()
256
+ const gridAfter = exposed.value?.getGrid()
257
+ expect(gridAfter).toBeDefined()
258
+ expect(gridAfter).not.toBe(gridBefore)
259
+ })
260
+
261
+ it('resolveCellStyle prop 传入 SheetGrid 视口渲染', async () => {
262
+ const workbook = createWorkbook()
263
+ const sheet = workbook.activeSheet
264
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
265
+ mount(
266
+ () => ({
267
+ workbook,
268
+ rows: 10,
269
+ cols: 6,
270
+ resolveCellStyle: (
271
+ addr: { row: number; col: number },
272
+ baseStyle?: { fill?: { color: string } }
273
+ ) => {
274
+ if (addr.row === 1 && addr.col === 1) {
275
+ return { ...baseStyle, fill: { color: '#FFE0E0' } }
276
+ }
277
+ return baseStyle
278
+ }
279
+ }),
280
+ exposed
281
+ )
282
+ await nextTick()
283
+
284
+ const addr = { row: 1, col: 1 }
285
+ sheet.setCellStyle({ start: addr, end: addr }, { fill: { color: '#CCCCCC' } })
286
+ await nextTick()
287
+
288
+ const table = exposed.value?.getGrid()?.getTable()
289
+ expect(table).toBeDefined()
290
+ const column = table!.getBodyColumnDefine(2, 2) as {
291
+ style?: (arg: { row: number; col: number; table: unknown }) => Record<string, unknown>
292
+ }
293
+ const rendered = column.style!({ row: 2, col: 2, table })
294
+ expect(rendered.bgColor).toBe('#FFE0E0')
295
+ expect(sheet.getCellStyle(addr)?.fill?.color).toBe('#CCCCCC')
296
+ })
297
+
298
+ it('workbook prop 变更:tabs 与 grid 切到新工作簿', async () => {
299
+ const workbook1 = createWorkbook()
300
+ const workbook2 = new Workbook()
301
+ workbook2.addSheet('数据表')
302
+ const current = ref(workbook1)
303
+ const { el } = mount(() => ({ workbook: current.value, rows: 10, cols: 6 }))
304
+ await nextTick()
305
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
306
+
307
+ current.value = workbook2
308
+ await nextTick()
309
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
310
+ expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
311
+ })
312
+
313
+ it('注册表联动:运行时 register/unregister 工具即时增删按钮', async () => {
314
+ const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
315
+ await nextTick()
316
+ expect(toolButton(el, 'temp-tool')).toBeUndefined()
317
+
318
+ registerTool({ id: 'temp-tool', title: '临时工具', onClick: () => {} })
319
+ await nextTick()
320
+ expect(toolButton(el, 'temp-tool')).toBeDefined()
321
+ // 无 icon 时回落文字按钮
322
+ expect(toolButton(el, 'temp-tool')!.textContent?.trim()).toBe('临时工具')
323
+
324
+ expect(unregisterTool('temp-tool')).toBe(true)
325
+ await nextTick()
326
+ expect(toolButton(el, 'temp-tool')).toBeUndefined()
327
+ })
328
+
329
+ it('无 props 默认挂载:内部自建工作簿(单 sheet),工具栏/tabs 正常', async () => {
330
+ const { el } = mount()
331
+ await nextTick()
332
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1'])
333
+ expect(el.querySelectorAll('.u-sheet__tool').length).toBeGreaterThan(0)
334
+ expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
335
+ })
336
+
337
+ it('tab「+」按钮:添加 sheet 并自动激活新表', async () => {
338
+ const workbook = createWorkbook()
339
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
340
+ await nextTick()
341
+
342
+ const addButton = el.querySelector<HTMLButtonElement>('.u-sheet__tab-add')!
343
+ addButton.click()
344
+ await nextTick()
345
+
346
+ expect(workbook.sheetCount).toBe(3)
347
+ expect(workbook.activeSheet.name).toBe('Sheet3')
348
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2', 'Sheet3'])
349
+ // 新表自动激活:active 样式落在新 tab 上
350
+ expect(tabs(el)[2]!.classList.contains('is-active')).toBe(true)
351
+ })
352
+
353
+ it('renameSheet 后 tab 文本跟随更新', async () => {
354
+ const workbook = createWorkbook()
355
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
356
+ await nextTick()
357
+
358
+ expect(workbook.renameSheet('Sheet2', '数据表')).toBe(true)
359
+ await nextTick()
360
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
361
+
362
+ // 重名(大小写不敏感)被拒绝:tab 文本不变
363
+ expect(workbook.renameSheet('Sheet1', '数据表')).toBe(false)
364
+ await nextTick()
365
+ expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
366
+ })
367
+
368
+ it('多实例并存:共享注册表,工具点击只作用于各自实例的活动 sheet', async () => {
369
+ const workbook1 = new Workbook()
370
+ const workbook2 = new Workbook()
371
+ const first = mount(() => ({ workbook: workbook1, rows: 10, cols: 6 }))
372
+ const second = mount(() => ({ workbook: workbook2, rows: 10, cols: 6 }))
373
+ await nextTick()
374
+
375
+ registerTool({
376
+ id: 'mark-active',
377
+ title: '标记当前格',
378
+ onClick: (ctx) => {
379
+ const active = ctx.getSelection().activeCell ?? { row: 0, col: 0 }
380
+ ctx.setCellValue(active, 'marked')
381
+ }
382
+ })
383
+ await nextTick()
384
+
385
+ // 两个实例都渲染该工具(注册表全局共享)
386
+ const button1 = toolButton(first.el, 'mark-active')
387
+ const button2 = toolButton(second.el, 'mark-active')
388
+ expect(button1).toBeDefined()
389
+ expect(button2).toBeDefined()
390
+
391
+ // 点击实例 2 的工具 → 只写实例 2 的 sheet(各自 SheetContext 绑定各自工作簿)
392
+ workbook2.activeSheet.selectCell({ row: 2, col: 1 })
393
+ await nextTick()
394
+ button2!.click()
395
+ await nextTick()
396
+ expect(workbook2.activeSheet.getCellData({ row: 2, col: 1 })).toEqual({ v: 'marked', t: 's' })
397
+ expect(workbook1.activeSheet.getCellData({ row: 2, col: 1 })).toBeUndefined()
398
+
399
+ // 该写入经命令系统:实例 2 的 undo 可用且可撤销
400
+ expect(workbook2.activeSheet.canUndo).toBe(true)
401
+ workbook2.activeSheet.undo()
402
+ expect(workbook2.activeSheet.getCellData({ row: 2, col: 1 })).toBeUndefined()
403
+
404
+ unregisterTool('mark-active')
405
+ })
406
+
407
+ it('合并/取消合并按钮随选区联动,点击生效且可 undo', async () => {
408
+ const workbook = new Workbook()
409
+ const sheet = workbook.activeSheet
410
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
411
+ await nextTick()
412
+
413
+ const mergeButton = toolButton(el, 'merge')!
414
+ const unmergeButton = toolButton(el, 'unmerge')!
415
+ // 默认单格 A1:合并禁用;无合并 → 取消合并禁用
416
+ expect(mergeButton.disabled).toBe(true)
417
+ expect(unmergeButton.disabled).toBe(true)
418
+
419
+ // 拖选区域(经模型 API 模拟 grid 拖选回写)
420
+ sheet.selectRange({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
421
+ await flushState()
422
+ expect(mergeButton.disabled).toBe(false)
423
+
424
+ mergeButton.click()
425
+ await nextTick()
426
+ expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('merged-covered')
427
+
428
+ // 选中合并锚点 → 取消合并可用
429
+ sheet.selectCell({ row: 0, col: 0 })
430
+ await nextTick()
431
+ expect(unmergeButton.disabled).toBe(false)
432
+
433
+ unmergeButton.click()
434
+ await nextTick()
435
+ expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('normal')
436
+
437
+ sheet.undo()
438
+ expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('merged-covered')
439
+ })
440
+
441
+ it('弹层工具:点击打开面板(UPalette 渲染),点击面板外关闭并提交事务', async () => {
442
+ const workbook = createWorkbook()
443
+ const sheet = workbook.activeSheet
444
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
445
+ await nextTick()
446
+
447
+ // 默认选区 A1:填充可用
448
+ const fillButton = toolButton(el, 'fill-color')!
449
+ expect(fillButton.disabled).toBe(false)
450
+
451
+ fillButton.click()
452
+ await flushPopup()
453
+ // 面板已渲染(含 UPalette 取色器)
454
+ const popupEl = document.querySelector<HTMLElement>('#pop-container .u-sheet__popup')
455
+ expect(popupEl).not.toBeNull()
456
+ expect(document.querySelector('#pop-container .u-sheet__popup .u-palette')).not.toBeNull()
457
+ // 面板 Teleport 到 body 级 #pop-container(不在 mount 容器内,定位由 usePop 接管)
458
+ expect(el.querySelector('.u-sheet__popup')).toBeNull()
459
+
460
+ // 点击面板外(grid 区)→ 关闭;空事务不入历史
461
+ el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
462
+ await nextTick()
463
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
464
+ expect(sheet.history.undoSize).toBe(0)
465
+ })
466
+
467
+ it('文本工具:加粗 toggle + active 高亮;字体颜色弹层可打开', async () => {
468
+ const workbook = createWorkbook()
469
+ const sheet = workbook.activeSheet
470
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
471
+ await nextTick()
472
+
473
+ const boldButton = toolButton(el, 'bold')!
474
+ expect(boldButton.classList.contains('is-active')).toBe(false)
475
+ boldButton.click()
476
+ await flushState()
477
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.bold).toBe(true)
478
+ expect(boldButton.classList.contains('is-active')).toBe(true)
479
+ boldButton.click()
480
+ await flushState()
481
+ expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
482
+ expect(boldButton.classList.contains('is-active')).toBe(false)
483
+
484
+ toolButton(el, 'font-color')!.click()
485
+ await flushPopup()
486
+ expect(document.querySelector('#pop-container .u-sheet__popup .u-palette')).not.toBeNull()
487
+ el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
488
+ await nextTick()
489
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
490
+ })
491
+
492
+ it('字号弹层:预设写入 + 关闭提交为一个 undo 单元', async () => {
493
+ const workbook = createWorkbook()
494
+ const sheet = workbook.activeSheet
495
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
496
+ await nextTick()
497
+
498
+ toolButton(el, 'font-size')!.click()
499
+ await flushPopup()
500
+ const sizeButtons = [
501
+ ...document.querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__popup-size-item')
502
+ ]
503
+ expect(sizeButtons.map((button) => button.textContent?.trim())).toContain('14')
504
+ sizeButtons.find((button) => button.textContent?.trim() === '14')!.click()
505
+ await nextTick()
506
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.size).toBe(14)
507
+
508
+ el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
509
+ await nextTick()
510
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
511
+ expect(sheet.history.undoSize).toBe(1)
512
+ sheet.undo()
513
+ expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
514
+ })
515
+
516
+ it('边框面板:预设应用 + 关闭提交为一个 undo 单元', async () => {
517
+ const workbook = createWorkbook()
518
+ const sheet = workbook.activeSheet
519
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
520
+ await nextTick()
521
+
522
+ sheet.selectRange({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
523
+ await nextTick()
524
+ toolButton(el, 'border')!.click()
525
+ await flushPopup()
526
+
527
+ const presetButtons = [...document.querySelectorAll('#pop-container .u-sheet__popup-preset')]
528
+ expect(presetButtons.map((button) => (button as HTMLElement).title)).toEqual([
529
+ '外边框',
530
+ '内边框',
531
+ '所有边框',
532
+ '上边框',
533
+ '下边框',
534
+ '左边框',
535
+ '右边框',
536
+ '无边框'
537
+ ])
538
+
539
+ presetButtons.find((button) => (button as HTMLElement).title === '所有边框')!.click()
540
+ await nextTick()
541
+ // 选区内每格四边都有边框
542
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.border?.top).toBeDefined()
543
+ expect(sheet.getCellStyle({ row: 1, col: 1 })?.border?.bottom).toBeDefined()
544
+ expect(sheet.getCellStyle({ row: 0, col: 1 })?.border?.left).toBeDefined()
545
+
546
+ // 无边框 → 清除边框
547
+ presetButtons.find((button) => (button as HTMLElement).title === '无边框')!.click()
548
+ await nextTick()
549
+ expect(sheet.getCellStyle({ row: 0, col: 0 })?.border).toBeUndefined()
550
+
551
+ // 点击外部关闭 → 面板期间所有写入合并为一个 undo 单元
552
+ el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
553
+ await nextTick()
554
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
555
+ expect(sheet.history.undoSize).toBe(1)
556
+
557
+ sheet.undo()
558
+ expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
559
+ })
560
+
561
+ it('冻结:模型 setFrozen 驱动 VTable 冻结布局联动(工具栏入口已迁至右键菜单)', async () => {
562
+ const workbook = createWorkbook()
563
+ const sheet = workbook.activeSheet
564
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
565
+ mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
566
+ await nextTick()
567
+
568
+ sheet.setFrozen(1, 0)
569
+ await nextTick()
570
+ expect(sheet.frozen).toEqual({ rows: 1, cols: 0 })
571
+ // 模型 1 行 → frozenRowCount = 2:列头行 + 首行;列保持 1:行号列
572
+ expect(exposed.value?.getGrid()?.getTable().frozenRowCount).toBe(2)
573
+ expect(exposed.value?.getGrid()?.getTable().frozenColCount).toBe(1)
574
+
575
+ sheet.setFrozen(1, 1)
576
+ await nextTick()
577
+ expect(exposed.value?.getGrid()?.getTable().frozenColCount).toBe(2)
578
+
579
+ sheet.setFrozen(0, 0)
580
+ await nextTick()
581
+ expect(sheet.frozen).toEqual({ rows: 0, cols: 0 })
582
+ })
583
+
584
+ it('导入:点击直接拉起系统文件选择,不打开弹层、不产生历史条目', async () => {
585
+ const workbook = createWorkbook()
586
+ const sheet = workbook.activeSheet
587
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
588
+ await nextTick()
589
+
590
+ const click = vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(() => {})
591
+ try {
592
+ toolButton(el, 'import')!.click()
593
+ await nextTick()
594
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
595
+ const input = document.body.querySelector<HTMLInputElement>('input[type="file"]')
596
+ expect(input).toBeTruthy()
597
+ expect(input!.accept).toBe('.xlsx,.csv')
598
+ expect(input!.hidden).toBe(true)
599
+ expect(click).toHaveBeenCalled()
600
+ expect(sheet.history.undoSize).toBe(0)
601
+ } finally {
602
+ click.mockRestore()
603
+ document.body.querySelectorAll('input[type="file"]').forEach((node) => node.remove())
604
+ }
605
+ })
606
+
607
+ it('导出弹层:单按钮打开两选项;点击选项触发下载并关闭,不产生历史', async () => {
608
+ const workbook = createWorkbook()
609
+ const sheet = workbook.activeSheet
610
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
611
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
612
+ await nextTick()
613
+
614
+ const originalCreate = URL.createObjectURL
615
+ const originalRevoke = URL.revokeObjectURL
616
+ const createObjectURL = vi.fn(() => 'blob:mock')
617
+ URL.createObjectURL = createObjectURL as typeof URL.createObjectURL
618
+ URL.revokeObjectURL = vi.fn() as typeof URL.revokeObjectURL
619
+ try {
620
+ toolButton(el, 'export')!.click()
621
+ await flushPopup()
622
+ const options = [
623
+ ...document.querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__export-option')
624
+ ]
625
+ expect(options.map((button) => button.textContent?.trim())).toEqual([
626
+ '导出 Excel (.xlsx)',
627
+ '导出 CSV (.csv)'
628
+ ])
629
+
630
+ options[0]!.click()
631
+ await nextTick()
632
+ await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
633
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
634
+ expect(sheet.history.undoSize).toBe(1) // 预置 setCellValue;导出本身不写模型
635
+
636
+ toolButton(el, 'export')!.click()
637
+ await flushPopup()
638
+ createObjectURL.mockClear()
639
+ document
640
+ .querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__export-option')[1]!
641
+ .click()
642
+ await nextTick()
643
+ await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
644
+ const csvBlob = createObjectURL.mock.calls[0]![0] as Blob
645
+ expect(csvBlob.type).toContain('text/csv')
646
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
647
+ } finally {
648
+ URL.createObjectURL = originalCreate
649
+ URL.revokeObjectURL = originalRevoke
650
+ }
651
+ })
652
+
653
+ it('查找条:打开 → 关键词定位命中 → Enter 下一个 / Shift+Enter 上一个 → 关闭', async () => {
654
+ const workbook = createWorkbook()
655
+ const sheet = workbook.activeSheet
656
+ sheet.setCellValue({ row: 0, col: 0 }, 'hello')
657
+ sheet.setCellValue({ row: 0, col: 1 }, 'world')
658
+ sheet.setCellValue({ row: 1, col: 0 }, 'hello again')
659
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
660
+ await nextTick()
661
+
662
+ toolButton(el, 'find')!.click()
663
+ await flushPopup()
664
+ expect(document.querySelector('#pop-container .u-sheet__popup')).not.toBeNull()
665
+ expect(document.querySelector('#pop-container .u-sheet__find-input')).not.toBeNull()
666
+
667
+ // 输入关键词 → 定位第一个命中(A1)
668
+ const input = document.querySelector<HTMLInputElement>(
669
+ '#pop-container .u-sheet__find-input .u-input__native'
670
+ )!
671
+ input.value = 'hello'
672
+ input.dispatchEvent(new Event('input', { bubbles: true }))
673
+ await flushFind()
674
+ expect(sheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
675
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
676
+ '1 / 2'
677
+ )
678
+
679
+ // Enter → 下一个命中(A3)
680
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
681
+ await nextTick()
682
+ expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 0 })
683
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
684
+ '2 / 2'
685
+ )
686
+
687
+ // Enter 循环回第一个
688
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
689
+ await nextTick()
690
+ expect(sheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
691
+
692
+ // Shift+Enter → 上一个(循环回最后一个)
693
+ input.dispatchEvent(
694
+ new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true })
695
+ )
696
+ await nextTick()
697
+ expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 0 })
698
+
699
+ // 无命中 → 计数 0 / 0,导航禁用
700
+ input.value = '不存在'
701
+ input.dispatchEvent(new Event('input', { bubbles: true }))
702
+ await flushFind()
703
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
704
+ '0 / 0'
705
+ )
706
+ expect(
707
+ document.querySelector<HTMLButtonElement>('#pop-container .u-sheet__find-nav')!.disabled
708
+ ).toBe(true)
709
+
710
+ // 关闭
711
+ document.querySelector<HTMLButtonElement>('#pop-container .u-sheet__find-close')!.click()
712
+ await nextTick()
713
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
714
+ })
715
+
716
+ it('替换:单个替换可 undo;全部替换 = 单 undo 单元,undo 一次全部还原', async () => {
717
+ const workbook = createWorkbook()
718
+ const sheet = workbook.activeSheet
719
+ sheet.setCellValue({ row: 0, col: 0 }, 'foo')
720
+ sheet.setCellValue({ row: 0, col: 1 }, 'foo')
721
+ sheet.setCellValue({ row: 1, col: 0 }, 'foo')
722
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
723
+ await nextTick()
724
+
725
+ toolButton(el, 'find')!.click()
726
+ await flushPopup()
727
+
728
+ const queryInput = document.querySelector<HTMLInputElement>(
729
+ '#pop-container .u-sheet__find-input .u-input__native'
730
+ )!
731
+ queryInput.value = 'foo'
732
+ queryInput.dispatchEvent(new Event('input', { bubbles: true }))
733
+ await flushFind()
734
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
735
+ '1 / 3'
736
+ )
737
+
738
+ // 全部替换:3 格一次写入
739
+ const replaceInputs = [
740
+ ...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
741
+ ]
742
+ replaceInputs[1]!.value = 'bar'
743
+ replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
744
+ await nextTick()
745
+ document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
746
+ await nextTick()
747
+
748
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'bar' })
749
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'bar' })
750
+ expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'bar' })
751
+ // 全部替换 = 单 undo 单元(3 次 setCellValue 初始 + 1 次替换)
752
+ expect(sheet.history.undoSize).toBe(4)
753
+
754
+ // undo 一次 → 全部还原
755
+ sheet.undo()
756
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'foo' })
757
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'foo' })
758
+ expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'foo' })
759
+ })
760
+
761
+ it('全部替换跳过公式格(不覆盖公式原文)', async () => {
762
+ const workbook = createWorkbook()
763
+ const sheet = workbook.activeSheet
764
+ sheet.setCellValue({ row: 0, col: 0 }, 'foo')
765
+ sheet.setCellFormula({ row: 1, col: 0 }, '=A1&"!"') // 公式格显示值 foo!
766
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
767
+ await nextTick()
768
+
769
+ toolButton(el, 'find')!.click()
770
+ await flushPopup()
771
+
772
+ const queryInput = document.querySelector<HTMLInputElement>(
773
+ '#pop-container .u-sheet__find-input .u-input__native'
774
+ )!
775
+ queryInput.value = 'foo'
776
+ queryInput.dispatchEvent(new Event('input', { bubbles: true }))
777
+ await flushFind()
778
+ // 普通格 + 公式格显示值都命中
779
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
780
+ '1 / 2'
781
+ )
782
+
783
+ const replaceInputs = [
784
+ ...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
785
+ ]
786
+ replaceInputs[1]!.value = 'bar'
787
+ replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
788
+ await nextTick()
789
+ document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
790
+ await nextTick()
791
+
792
+ // 普通格被替换,公式格保留公式(显示值也因依赖 A1 变化为 bar!)
793
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'bar' })
794
+ expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ f: 'A1&"!"' })
795
+ expect(sheet.getCellData({ row: 1, col: 0 })?.v).toBe('bar!')
796
+ })
797
+
798
+ it('防抖窗口内全部替换:按输入框当前关键词补扫,不用旧缓存的命中集', async () => {
799
+ const workbook = createWorkbook()
800
+ const sheet = workbook.activeSheet
801
+ sheet.setCellValue({ row: 0, col: 0 }, 'foo')
802
+ sheet.setCellValue({ row: 0, col: 1 }, 'f')
803
+ sheet.setCellValue({ row: 1, col: 0 }, 'f')
804
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
805
+ await nextTick()
806
+
807
+ toolButton(el, 'find')!.click()
808
+ await flushPopup()
809
+
810
+ const queryInput = document.querySelector<HTMLInputElement>(
811
+ '#pop-container .u-sheet__find-input .u-input__native'
812
+ )!
813
+ queryInput.value = 'foo'
814
+ queryInput.dispatchEvent(new Event('input', { bubbles: true }))
815
+ await flushFind()
816
+ expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
817
+ '1 / 1'
818
+ )
819
+
820
+ // 快速改输 'f'(不等 300ms 防抖触发)→ 立刻全部替换。
821
+ // 'f' 命中全部 3 格(foo 含 f);若误用 'foo' 的旧缓存只会改写 A1
822
+ queryInput.value = 'f'
823
+ queryInput.dispatchEvent(new Event('input', { bubbles: true }))
824
+ await nextTick()
825
+
826
+ const replaceInputs = [
827
+ ...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
828
+ ]
829
+ replaceInputs[1]!.value = 'x'
830
+ replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
831
+ await nextTick()
832
+ document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
833
+ await nextTick()
834
+
835
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'x' })
836
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'x' })
837
+ expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'x' })
838
+ })
839
+
840
+ it('Ctrl+F:容器外按键不劫持浏览器查找;查找条聚焦时(弹层在 root 外)可 toggle 关闭', async () => {
841
+ const workbook = createWorkbook()
842
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
843
+ await nextTick()
844
+
845
+ // 容器外(body)按 Ctrl+F:不弹查找条、不 preventDefault
846
+ const outside = new KeyboardEvent('keydown', {
847
+ key: 'f',
848
+ ctrlKey: true,
849
+ bubbles: true,
850
+ cancelable: true
851
+ })
852
+ document.body.dispatchEvent(outside)
853
+ await flushPopup()
854
+ expect(outside.defaultPrevented).toBe(false)
855
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
856
+
857
+ // 打开查找条,焦点进入弹层输入框(Teleport 到 #pop-container,不在实例 root 内)
858
+ toolButton(el, 'find')!.click()
859
+ await flushPopup()
860
+ const input = document.querySelector<HTMLInputElement>(
861
+ '#pop-container .u-sheet__find-input .u-input__native'
862
+ )!
863
+ input.focus()
864
+
865
+ // 弹层内再按 Ctrl+F:toggle 关闭查找条,且拦截浏览器原生查找
866
+ const toggle = new KeyboardEvent('keydown', {
867
+ key: 'f',
868
+ ctrlKey: true,
869
+ bubbles: true,
870
+ cancelable: true
871
+ })
872
+ input.dispatchEvent(toggle)
873
+ await nextTick()
874
+ expect(toggle.defaultPrevented).toBe(true)
875
+ expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
876
+ })
877
+ })
878
+
879
+ describe('tabs 栏', () => {
880
+ it('少量 sheet:「+」在视口外且紧随 viewport(nav 隐藏,不占位)', async () => {
881
+ const workbook = createWorkbook()
882
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
883
+ await nextTick()
884
+
885
+ const tabsBar = el.querySelector<HTMLElement>('.u-sheet__tabs')!
886
+ const viewport = el.querySelector<HTMLElement>('.u-sheet__tabs-viewport')!
887
+ const addButton = el.querySelector<HTMLButtonElement>('.u-sheet__tab-add')!
888
+ const navButtons = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tabs-nav')]
889
+
890
+ // 「+」仍是 tabs 直接子级,不在滚动 list 内
891
+ expect(tabsBar.contains(addButton)).toBe(true)
892
+ expect(viewport.contains(addButton)).toBe(false)
893
+ expect(el.querySelector('.u-sheet__tabs-list .u-sheet__tab-add')).toBeNull()
894
+
895
+ // 未溢出:nav 隐藏;DOM 顺序 viewport → next-nav →「+」,布局上「+」紧跟 tab 区
896
+ expect(navButtons).toHaveLength(2)
897
+ expect(navButtons[0]!.style.display).toBe('none')
898
+ expect(navButtons[1]!.style.display).toBe('none')
899
+ expect(viewport.nextElementSibling).toBe(navButtons[1]!)
900
+ expect(navButtons[1]!.nextElementSibling).toBe(addButton)
901
+ })
902
+
903
+ it('多 sheet 溢出:viewport 可横滚,nav 出现;next/prev 改变 scrollLeft;活动 tab 滚入视野', async () => {
904
+ const workbook = new Workbook()
905
+ for (let i = 2; i <= 16; i++) workbook.addSheet(`Sheet${i}`)
906
+ const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
907
+ await nextTick()
908
+
909
+ const viewport = el.querySelector<HTMLElement>('.u-sheet__tabs-viewport')!
910
+ const list = el.querySelector<HTMLElement>('.u-sheet__tabs-list')!
911
+ expect(viewport).not.toBeNull()
912
+ expect(list).not.toBeNull()
913
+ expect(list.children.length).toBe(16)
914
+ // 「+」钉在视口外(tabs 直接子级,不在 list 内)
915
+ expect(el.querySelector('.u-sheet__tabs > .u-sheet__tab-add')).not.toBeNull()
916
+ expect(list.querySelector('.u-sheet__tab-add')).toBeNull()
917
+
918
+ // happy-dom 无真实布局:伪造溢出几何,触发导航显隐
919
+ let scrollLeft = 0
920
+ Object.defineProperty(viewport, 'clientWidth', { configurable: true, get: () => 100 })
921
+ Object.defineProperty(viewport, 'scrollWidth', { configurable: true, get: () => 800 })
922
+ Object.defineProperty(viewport, 'scrollLeft', {
923
+ configurable: true,
924
+ get: () => scrollLeft,
925
+ set: (value: number) => {
926
+ scrollLeft = value
927
+ }
928
+ })
929
+ viewport.scrollTo = ((options?: ScrollToOptions | number) => {
930
+ if (typeof options === 'number') {
931
+ scrollLeft = options
932
+ } else if (options && typeof options.left === 'number') {
933
+ scrollLeft = options.left
934
+ }
935
+ viewport.dispatchEvent(new Event('scroll'))
936
+ }) as typeof viewport.scrollTo
937
+ viewport.getBoundingClientRect = () =>
938
+ ({
939
+ left: 0,
940
+ right: 100,
941
+ top: 0,
942
+ bottom: 24,
943
+ width: 100,
944
+ height: 24,
945
+ x: 0,
946
+ y: 0,
947
+ toJSON: () => ({})
948
+ }) as DOMRect
949
+
950
+ viewport.dispatchEvent(new Event('scroll'))
951
+ await nextTick()
952
+
953
+ expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth)
954
+ const navButtons = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tabs-nav')]
955
+ expect(navButtons).toHaveLength(2)
956
+ // v-show:溢出时显示;初始 scrollLeft=0 → prev 禁用、next 可用
957
+ expect(navButtons[0]!.style.display).not.toBe('none')
958
+ expect(navButtons[1]!.style.display).not.toBe('none')
959
+ expect(navButtons[0]!.disabled).toBe(true)
960
+ expect(navButtons[1]!.disabled).toBe(false)
961
+
962
+ navButtons[1]!.click()
963
+ await nextTick()
964
+ expect(scrollLeft).toBeGreaterThan(0)
965
+ expect(navButtons[0]!.disabled).toBe(false)
966
+
967
+ const beforePrev = scrollLeft
968
+ navButtons[0]!.click()
969
+ await nextTick()
970
+ expect(scrollLeft).toBeLessThan(beforePrev)
971
+
972
+ // 切换到末尾 sheet:活动 tab 在视口右侧外 → ensureActiveVisible 滚入
973
+ const lastTab = tabs(el)[15]!
974
+ lastTab.getBoundingClientRect = () =>
975
+ ({
976
+ left: 700,
977
+ right: 780,
978
+ top: 0,
979
+ bottom: 24,
980
+ width: 80,
981
+ height: 24,
982
+ x: 700,
983
+ y: 0,
984
+ toJSON: () => ({})
985
+ }) as DOMRect
986
+ scrollLeft = 0
987
+ lastTab.click()
988
+ await nextTick()
989
+ await nextTick()
990
+ expect(workbook.activeSheet.name).toBe('Sheet16')
991
+ expect(scrollLeft).toBeGreaterThan(0)
992
+ })
993
+ })