@veltra/sheet 2.0.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 (137) hide show
  1. package/dist/core/address.d.ts +60 -0
  2. package/dist/core/address.js +142 -0
  3. package/dist/core/address.js.map +1 -0
  4. package/dist/core/cell-store.d.ts +61 -0
  5. package/dist/core/cell-store.js +135 -0
  6. package/dist/core/cell-store.js.map +1 -0
  7. package/dist/core/command/default-registry.d.ts +10 -0
  8. package/dist/core/command/default-registry.js +18 -0
  9. package/dist/core/command/default-registry.js.map +1 -0
  10. package/dist/core/command/history.d.ts +64 -0
  11. package/dist/core/command/history.js +127 -0
  12. package/dist/core/command/history.js.map +1 -0
  13. package/dist/core/command/merge-cells.d.ts +21 -0
  14. package/dist/core/command/merge-cells.js +74 -0
  15. package/dist/core/command/merge-cells.js.map +1 -0
  16. package/dist/core/command/registry.d.ts +17 -0
  17. package/dist/core/command/registry.js +26 -0
  18. package/dist/core/command/registry.js.map +1 -0
  19. package/dist/core/command/set-cell-formula.d.ts +17 -0
  20. package/dist/core/command/set-cell-formula.js +29 -0
  21. package/dist/core/command/set-cell-formula.js.map +1 -0
  22. package/dist/core/command/set-cell-value.d.ts +22 -0
  23. package/dist/core/command/set-cell-value.js +35 -0
  24. package/dist/core/command/set-cell-value.js.map +1 -0
  25. package/dist/core/command/types.d.ts +68 -0
  26. package/dist/core/events.js +29 -0
  27. package/dist/core/events.js.map +1 -0
  28. package/dist/core/formula/ast.d.ts +58 -0
  29. package/dist/core/formula/ast.js +38 -0
  30. package/dist/core/formula/ast.js.map +1 -0
  31. package/dist/core/formula/dependency-graph.d.ts +70 -0
  32. package/dist/core/formula/dependency-graph.js +269 -0
  33. package/dist/core/formula/dependency-graph.js.map +1 -0
  34. package/dist/core/formula/errors.d.ts +25 -0
  35. package/dist/core/formula/errors.js +36 -0
  36. package/dist/core/formula/errors.js.map +1 -0
  37. package/dist/core/formula/evaluator.d.ts +41 -0
  38. package/dist/core/formula/evaluator.js +154 -0
  39. package/dist/core/formula/evaluator.js.map +1 -0
  40. package/dist/core/formula/functions.d.ts +30 -0
  41. package/dist/core/formula/functions.js +209 -0
  42. package/dist/core/formula/functions.js.map +1 -0
  43. package/dist/core/formula/parser.d.ts +7 -0
  44. package/dist/core/formula/parser.js +208 -0
  45. package/dist/core/formula/parser.js.map +1 -0
  46. package/dist/core/formula/tokenizer.d.ts +37 -0
  47. package/dist/core/formula/tokenizer.js +146 -0
  48. package/dist/core/formula/tokenizer.js.map +1 -0
  49. package/dist/core/merge-manager.d.ts +64 -0
  50. package/dist/core/merge-manager.js +111 -0
  51. package/dist/core/merge-manager.js.map +1 -0
  52. package/dist/core/selection.d.ts +31 -0
  53. package/dist/core/selection.js +62 -0
  54. package/dist/core/selection.js.map +1 -0
  55. package/dist/core/sheet.d.ts +105 -0
  56. package/dist/core/sheet.js +202 -0
  57. package/dist/core/sheet.js.map +1 -0
  58. package/dist/core/workbook.d.ts +43 -0
  59. package/dist/core/workbook.js +77 -0
  60. package/dist/core/workbook.js.map +1 -0
  61. package/dist/grid/sheet-grid.d.ts +59 -0
  62. package/dist/grid/sheet-grid.js +230 -0
  63. package/dist/grid/sheet-grid.js.map +1 -0
  64. package/dist/index.d.ts +28 -0
  65. package/dist/index.js +25 -0
  66. package/dist/tools/builtin.js +72 -0
  67. package/dist/tools/builtin.js.map +1 -0
  68. package/dist/tools/context.d.ts +57 -0
  69. package/dist/tools/context.js +43 -0
  70. package/dist/tools/context.js.map +1 -0
  71. package/dist/tools/registry.d.ts +68 -0
  72. package/dist/tools/registry.js +77 -0
  73. package/dist/tools/registry.js.map +1 -0
  74. package/dist/types/index.d.ts +2 -0
  75. package/dist/types/sheet.d.ts +43 -0
  76. package/dist/vue/index.d.ts +2 -0
  77. package/dist/vue/sheet.js +185 -0
  78. package/dist/vue/sheet.js.map +1 -0
  79. package/dist/vue/sheet.vue.d.ts +24 -0
  80. package/dist/vue/style.d.ts +1 -0
  81. package/dist/vue/style.js +1 -0
  82. package/dist/vue/style2.css +93 -0
  83. package/package.json +37 -0
  84. package/src/core/__test__/address.test.ts +113 -0
  85. package/src/core/__test__/cell-store.test.ts +97 -0
  86. package/src/core/__test__/command.test.ts +377 -0
  87. package/src/core/__test__/formula-adversarial.test.ts +265 -0
  88. package/src/core/__test__/formula-engine.test.ts +470 -0
  89. package/src/core/__test__/formula-functions.test.ts +178 -0
  90. package/src/core/__test__/formula-parser.test.ts +160 -0
  91. package/src/core/__test__/history.test.ts +225 -0
  92. package/src/core/__test__/merge-manager.test.ts +106 -0
  93. package/src/core/__test__/sheet.test.ts +142 -0
  94. package/src/core/address.ts +166 -0
  95. package/src/core/cell-store.ts +173 -0
  96. package/src/core/command/default-registry.ts +15 -0
  97. package/src/core/command/history.ts +167 -0
  98. package/src/core/command/index.ts +7 -0
  99. package/src/core/command/merge-cells.ts +74 -0
  100. package/src/core/command/registry.ts +30 -0
  101. package/src/core/command/set-cell-formula.ts +29 -0
  102. package/src/core/command/set-cell-value.ts +38 -0
  103. package/src/core/command/types.ts +73 -0
  104. package/src/core/events.ts +37 -0
  105. package/src/core/formula/ast.ts +66 -0
  106. package/src/core/formula/dependency-graph.ts +329 -0
  107. package/src/core/formula/errors.ts +41 -0
  108. package/src/core/formula/evaluator.ts +219 -0
  109. package/src/core/formula/functions.ts +266 -0
  110. package/src/core/formula/index.ts +7 -0
  111. package/src/core/formula/parser.ts +198 -0
  112. package/src/core/formula/tokenizer.ts +160 -0
  113. package/src/core/merge-manager.ts +158 -0
  114. package/src/core/selection.ts +66 -0
  115. package/src/core/sheet.ts +257 -0
  116. package/src/core/workbook.ts +99 -0
  117. package/src/env.d.ts +7 -0
  118. package/src/grid/__test__/canvas-mock.ts +49 -0
  119. package/src/grid/__test__/setup.ts +3 -0
  120. package/src/grid/__test__/sheet-grid-formula.test.ts +140 -0
  121. package/src/grid/__test__/sheet-grid.test.ts +177 -0
  122. package/src/grid/sheet-grid.ts +283 -0
  123. package/src/index.ts +16 -0
  124. package/src/tools/__test__/builtin.test.ts +128 -0
  125. package/src/tools/__test__/context.test.ts +157 -0
  126. package/src/tools/__test__/registry.test.ts +102 -0
  127. package/src/tools/builtin.ts +75 -0
  128. package/src/tools/context.ts +106 -0
  129. package/src/tools/index.ts +2 -0
  130. package/src/tools/registry.ts +129 -0
  131. package/src/types/index.ts +1 -0
  132. package/src/types/sheet.ts +41 -0
  133. package/src/vue/__test__/sheet-component.test.ts +267 -0
  134. package/src/vue/index.ts +1 -0
  135. package/src/vue/sheet.vue +195 -0
  136. package/src/vue/style.scss +94 -0
  137. package/src/vue/style.ts +1 -0
@@ -0,0 +1,99 @@
1
+ import { TypedEventEmitter } from './events'
2
+ import { DependencyGraph } from './formula/dependency-graph'
3
+ import { Sheet } from './sheet'
4
+
5
+ /**
6
+ * Workbook:多 Sheet 管理。
7
+ * 所有 sheet 共享一个公式依赖图(跨表引用与增量重算的中枢)。
8
+ */
9
+
10
+ export type WorkbookEvents = {
11
+ /** 激活 sheet 切换 */
12
+ 'active-sheet-change': { sheet: Sheet; index: number }
13
+ /** sheet 增删 */
14
+ 'sheets-change': { sheets: Sheet[] }
15
+ }
16
+
17
+ export class Workbook {
18
+ private sheets: Sheet[] = []
19
+ private activeIndex = 0
20
+ private emitter = new TypedEventEmitter<WorkbookEvents>()
21
+ /** 工作簿级公式依赖图(全部 sheet 共享) */
22
+ readonly formulaGraph = new DependencyGraph()
23
+
24
+ constructor() {
25
+ this.addSheet()
26
+ }
27
+
28
+ get sheetCount(): number {
29
+ return this.sheets.length
30
+ }
31
+
32
+ get activeSheet(): Sheet {
33
+ return this.sheets[this.activeIndex]!
34
+ }
35
+
36
+ get activeSheetIndex(): number {
37
+ return this.activeIndex
38
+ }
39
+
40
+ getSheets(): Sheet[] {
41
+ return [...this.sheets]
42
+ }
43
+
44
+ getSheet(name: string): Sheet | undefined {
45
+ return this.sheets.find((sheet) => sheet.name === name)
46
+ }
47
+
48
+ /** 新增 sheet,名称缺省为 Sheet{n}(保证唯一) */
49
+ addSheet(name?: string): Sheet {
50
+ const sheet = new Sheet(name ?? this.nextDefaultName(), this.formulaGraph)
51
+ this.sheets.push(sheet)
52
+ this.emitter.emit('sheets-change', { sheets: this.getSheets() })
53
+ return sheet
54
+ }
55
+
56
+ /** 删除 sheet;至少保留一个。删除激活项时激活相邻 sheet */
57
+ removeSheet(name: string): boolean {
58
+ if (this.sheets.length <= 1) return false
59
+ const index = this.sheets.findIndex((sheet) => sheet.name === name)
60
+ if (index < 0) return false
61
+ const [removed] = this.sheets.splice(index, 1)
62
+ this.formulaGraph.unregisterSheet(removed!)
63
+ if (this.activeIndex >= this.sheets.length) {
64
+ this.activeIndex = this.sheets.length - 1
65
+ }
66
+ this.emitter.emit('sheets-change', { sheets: this.getSheets() })
67
+ this.emitter.emit('active-sheet-change', { sheet: this.activeSheet, index: this.activeIndex })
68
+ return true
69
+ }
70
+
71
+ /** 激活指定 sheet */
72
+ activateSheet(name: string): boolean {
73
+ const index = this.sheets.findIndex((sheet) => sheet.name === name)
74
+ if (index < 0 || index === this.activeIndex) return false
75
+ this.activeIndex = index
76
+ this.emitter.emit('active-sheet-change', { sheet: this.activeSheet, index })
77
+ return true
78
+ }
79
+
80
+ on<K extends keyof WorkbookEvents>(
81
+ type: K,
82
+ handler: (payload: WorkbookEvents[K]) => void
83
+ ): () => void {
84
+ return this.emitter.on(type, handler)
85
+ }
86
+
87
+ off<K extends keyof WorkbookEvents>(
88
+ type: K,
89
+ handler: (payload: WorkbookEvents[K]) => void
90
+ ): void {
91
+ this.emitter.off(type, handler)
92
+ }
93
+
94
+ private nextDefaultName(): string {
95
+ let n = this.sheets.length + 1
96
+ while (this.getSheet(`Sheet${n}`)) n++
97
+ return `Sheet${n}`
98
+ }
99
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<object, object, any>
4
+ export default component
5
+ }
6
+
7
+ declare module '*.scss' {}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * happy-dom 不实现 canvas 2d 上下文(getContext 返回 null),
3
+ * 这里用 Proxy 提供最小可用的 mock,让 @visactor/vrender 能在无头环境
4
+ * 完成挂载与绘制调用。仅用于测试(vp test setupFiles)。
5
+ */
6
+
7
+ type Context2dMock = Record<string | symbol, unknown>
8
+
9
+ function createContext2dMock(canvas: HTMLCanvasElement): Context2dMock {
10
+ const store: Context2dMock = { canvas }
11
+
12
+ return new Proxy(store, {
13
+ get(target, prop) {
14
+ if (prop in target) return target[prop]
15
+ if (prop === 'measureText') return (text: unknown) => ({ width: String(text).length * 8 })
16
+ if (prop === 'getImageData')
17
+ return () => ({ data: new Uint8ClampedArray(0), width: 0, height: 0 })
18
+ if (
19
+ prop === 'createLinearGradient' ||
20
+ prop === 'createRadialGradient' ||
21
+ prop === 'createPattern'
22
+ ) {
23
+ return () => ({ addColorStop: () => {} })
24
+ }
25
+ if (prop === 'getLineDash') return () => []
26
+ // 其余一律视为无操作绘制方法
27
+ return () => {}
28
+ },
29
+ set(target, prop, value) {
30
+ target[prop] = value
31
+ return true
32
+ }
33
+ })
34
+ }
35
+
36
+ /** 补丁 HTMLCanvasElement,使 getContext('2d') 返回可用 mock */
37
+ export function setupCanvasMock(): void {
38
+ const getContext = function (this: HTMLCanvasElement, contextId: string): unknown {
39
+ if (contextId !== '2d') return null
40
+ return createContext2dMock(this)
41
+ }
42
+
43
+ HTMLCanvasElement.prototype.getContext =
44
+ getContext as unknown as typeof HTMLCanvasElement.prototype.getContext
45
+
46
+ if (typeof HTMLCanvasElement.prototype.toDataURL !== 'function') {
47
+ HTMLCanvasElement.prototype.toDataURL = () => 'data:,'
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ import { setupCanvasMock } from './canvas-mock'
2
+
3
+ setupCanvasMock()
@@ -0,0 +1,140 @@
1
+ import type { ListTable } from '@visactor/vtable'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { Sheet } from '../../core/sheet'
5
+ import { SheetGrid } from '../sheet-grid'
6
+
7
+ function createContainer(): HTMLElement {
8
+ const el = document.createElement('div')
9
+ el.style.width = '800px'
10
+ el.style.height = '600px'
11
+ document.body.appendChild(el)
12
+ return el
13
+ }
14
+
15
+ function createGrid() {
16
+ const sheet = new Sheet()
17
+ const container = createContainer()
18
+ const grid = new SheetGrid({ container, sheet, rows: 20, cols: 6 })
19
+ return { sheet, grid, table: grid.getTable(), container }
20
+ }
21
+
22
+ /** 当前编辑器 input(容器里另有 VTable 内部 input,不能靠 querySelector 取) */
23
+ function editorInput(table: ListTable): HTMLInputElement | undefined {
24
+ const manager = (
25
+ table as unknown as {
26
+ editorManager?: { editingEditor?: { getInputElement?: () => HTMLInputElement } }
27
+ }
28
+ ).editorManager
29
+ return manager?.editingEditor?.getInputElement?.()
30
+ }
31
+
32
+ describe('SheetGrid 公式集成(happy-dom)', () => {
33
+ it('公式格显示计算值(模型写入公式 → 表格显示缓存值)', () => {
34
+ const { sheet, grid, table } = createGrid()
35
+ try {
36
+ sheet.setCellValue({ row: 0, col: 0 }, 21)
37
+ sheet.setCellFormula({ row: 0, col: 1 }, '=A1*2')
38
+ // 模型 (0,1) → 表格 (2,1)
39
+ expect(table.getCellValue(2, 1)).toBe(42)
40
+ } finally {
41
+ grid.release()
42
+ }
43
+ })
44
+
45
+ it('编辑提交 = 开头文本 → 模型存公式,表格回推计算值', () => {
46
+ const { sheet, grid, table } = createGrid()
47
+ try {
48
+ sheet.setCellValue({ row: 0, col: 0 }, 5)
49
+ // 模拟编辑提交(Playwright 无法触发双击编辑,changeCellValue 走同一提交路径)
50
+ table.changeCellValue(2, 1, '=A1*2', false, true)
51
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ f: 'A1*2', v: 10, t: 'n' })
52
+ // VTable 先把输入文本写进 record;回推后必须是计算值而非公式原文
53
+ expect(table.getCellValue(2, 1)).toBe(10)
54
+ } finally {
55
+ grid.release()
56
+ }
57
+ })
58
+
59
+ it('编辑提交引发的派生变更同步到表格(源格 + 全部依赖格)', () => {
60
+ const { sheet, grid, table } = createGrid()
61
+ try {
62
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
63
+ sheet.setCellFormula({ row: 0, col: 1 }, '=A1*2')
64
+ sheet.setCellFormula({ row: 0, col: 2 }, '=B1+1')
65
+ expect(table.getCellValue(2, 1)).toBe(2)
66
+ expect(table.getCellValue(3, 1)).toBe(3)
67
+
68
+ // 编辑 A1 → B1/C1 派生重算,两格都要同步到表格
69
+ // (编辑提交回写的是输入文本,A1 落为字符串 '10';公式按数字文本强转)
70
+ table.changeCellValue(1, 1, '10', false, true)
71
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: '10' })
72
+ expect(table.getCellValue(1, 1)).toBe('10')
73
+ expect(table.getCellValue(2, 1)).toBe(20)
74
+ expect(table.getCellValue(3, 1)).toBe(21)
75
+ } finally {
76
+ grid.release()
77
+ }
78
+ })
79
+
80
+ it('进入编辑时公式格显示原文,提交新公式后模型与显示更新', () => {
81
+ const { sheet, grid, table } = createGrid()
82
+ try {
83
+ sheet.setCellValue({ row: 0, col: 0 }, 21)
84
+ sheet.setCellFormula({ row: 0, col: 1 }, '=A1*2')
85
+
86
+ // 程序化进入编辑(等价双击):编辑器文本 = 公式原文
87
+ table.startEditCell(2, 1)
88
+ const input = editorInput(table)
89
+ expect(input).toBeDefined()
90
+ expect(input!.value).toBe('=A1*2')
91
+
92
+ // 输入新公式并提交
93
+ input!.value = '=A1*3'
94
+ table.completeEditCell()
95
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ f: 'A1*3', v: 63 })
96
+ expect(table.getCellValue(2, 1)).toBe(63)
97
+ } finally {
98
+ grid.release()
99
+ }
100
+ })
101
+
102
+ it('普通格进入编辑显示当前值', () => {
103
+ const { sheet, grid, table } = createGrid()
104
+ try {
105
+ sheet.setCellValue({ row: 0, col: 0 }, 'plain')
106
+ table.startEditCell(1, 1)
107
+ expect(editorInput(table)!.value).toBe('plain')
108
+ table.completeEditCell()
109
+ } finally {
110
+ grid.release()
111
+ }
112
+ })
113
+
114
+ it('公式错误值直接显示(#DIV/0!)', () => {
115
+ const { sheet, grid, table } = createGrid()
116
+ try {
117
+ sheet.setCellFormula({ row: 0, col: 0 }, '=1/0')
118
+ expect(table.getCellValue(1, 1)).toBe('#DIV/0!')
119
+ } finally {
120
+ grid.release()
121
+ }
122
+ })
123
+
124
+ it('undo 公式编辑后表格恢复显示', () => {
125
+ const { sheet, grid, table } = createGrid()
126
+ try {
127
+ sheet.setCellValue({ row: 0, col: 0 }, 21)
128
+ sheet.setCellFormula({ row: 0, col: 1 }, '=A1*2')
129
+ expect(table.getCellValue(2, 1)).toBe(42)
130
+
131
+ sheet.undo()
132
+ expect(table.getCellValue(2, 1)).toBeUndefined()
133
+
134
+ sheet.redo()
135
+ expect(table.getCellValue(2, 1)).toBe(42)
136
+ } finally {
137
+ grid.release()
138
+ }
139
+ })
140
+ })
@@ -0,0 +1,177 @@
1
+ import { ListTable } from '@visactor/vtable'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { Sheet } from '../../core/sheet'
5
+ import { SheetGrid } from '../sheet-grid'
6
+
7
+ function createContainer(): HTMLElement {
8
+ const el = document.createElement('div')
9
+ el.style.width = '800px'
10
+ el.style.height = '600px'
11
+ document.body.appendChild(el)
12
+ return el
13
+ }
14
+
15
+ function createGrid() {
16
+ const sheet = new Sheet()
17
+ const grid = new SheetGrid({ container: createContainer(), sheet, rows: 20, cols: 6 })
18
+ return { sheet, grid, table: grid.getTable() }
19
+ }
20
+
21
+ describe('SheetGrid(happy-dom smoke)', () => {
22
+ it('能挂载:列头 A..F + 行号列,坐标带偏移', () => {
23
+ const { grid, table } = createGrid()
24
+ try {
25
+ // (0,*) 为行号列,(*,0) 为列头;模型 (0,0) → 表格 (1,1)
26
+ expect(table.isSeriesNumber(0, 1)).toBe(true)
27
+ expect(table.isHeader(1, 0)).toBe(true)
28
+ expect(table.getCellValue(1, 0)).toBe('A')
29
+ } finally {
30
+ grid.release()
31
+ }
32
+ })
33
+
34
+ it('模型 → 表格:setCellValue 后表格可见', () => {
35
+ const { sheet, grid, table } = createGrid()
36
+ try {
37
+ sheet.setCellValue({ row: 0, col: 0 }, 'hello')
38
+ expect(table.getCellValue(1, 1)).toBe('hello')
39
+ } finally {
40
+ grid.release()
41
+ }
42
+ })
43
+
44
+ it('表格 → 模型:change_cell_value 回写 store', () => {
45
+ const { sheet, grid, table } = createGrid()
46
+ try {
47
+ table.changeCellValue(2, 3, 'world', false, true)
48
+ expect(sheet.getCellData({ row: 2, col: 1 })).toEqual({ v: 'world', t: 's' })
49
+ } finally {
50
+ grid.release()
51
+ }
52
+ })
53
+
54
+ it('合并映射:customMergeCell 反映 MergeManager 的包围盒', () => {
55
+ const { sheet, grid, table } = createGrid()
56
+ try {
57
+ sheet.mergeCells({ start: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
58
+ // 模型 B2:C3 → 表格坐标 (2..3, 2..3)
59
+ const merge = table.getCustomMerge(3, 3)
60
+ expect(merge?.range).toMatchObject({ start: { col: 2, row: 2 }, end: { col: 3, row: 3 } })
61
+ expect(table.getCustomMerge(1, 1)).toBeUndefined()
62
+ } finally {
63
+ grid.release()
64
+ }
65
+ })
66
+
67
+ it('合并区域是单一可操作单位:被覆盖格的 getCellRange 扩展为整个合并', () => {
68
+ const { sheet, grid, table } = createGrid()
69
+ try {
70
+ sheet.setCellValue({ row: 1, col: 1 }, 'anchor-value')
71
+ sheet.mergeCells({ start: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
72
+
73
+ // 点击被覆盖格(表格坐标 3,3)→ 选区/编辑范围 = 整个合并区域
74
+ const range = table.getCellRange(3, 3)
75
+ expect(range).toMatchObject({
76
+ start: { col: 2, row: 2 },
77
+ end: { col: 3, row: 3 },
78
+ isCustom: true
79
+ })
80
+
81
+ // 合并格文本 = 锚点显示值(否则渲染为空)
82
+ expect(table.getCustomMerge(3, 3)?.text).toBe('anchor-value')
83
+ expect(table.getCustomMerge(2, 2)?.text).toBe('anchor-value')
84
+ } finally {
85
+ grid.release()
86
+ }
87
+ })
88
+
89
+ it('编辑提交后合并格文本立即刷新(text 读 records,与 VTable 更新次序一致)', () => {
90
+ const { sheet, grid, table } = createGrid()
91
+ try {
92
+ sheet.setCellValue({ row: 1, col: 1 }, 'old')
93
+ sheet.mergeCells({ start: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
94
+
95
+ // 模拟编辑提交:doExit 的 isCustom 分支 → changeCellValue(锚点, 新值)
96
+ const range = table.getCellRange(3, 3)
97
+ table.changeCellValue(range.start.col, range.start.row, 'new')
98
+
99
+ // 模型已回写;且重绘发生在 change_cell_value 之前,text 必须已是新值
100
+ expect(sheet.getCellData({ row: 1, col: 1 })).toMatchObject({ v: 'new' })
101
+ expect(table.getCustomMerge(3, 3)?.text).toBe('new')
102
+ } finally {
103
+ grid.release()
104
+ }
105
+ })
106
+
107
+ it('编辑回写走命令系统:change_cell_value 后可 undo/redo', () => {
108
+ const { sheet, grid, table } = createGrid()
109
+ try {
110
+ table.changeCellValue(1, 1, 'edited', false, true)
111
+ expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'edited', t: 's' })
112
+
113
+ expect(sheet.undo()).toBe(true)
114
+ expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
115
+
116
+ expect(sheet.redo()).toBe(true)
117
+ expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'edited', t: 's' })
118
+ } finally {
119
+ grid.release()
120
+ }
121
+ })
122
+
123
+ it('拖选结束 → 模型选区同步为区域(合并工具的前提)', () => {
124
+ const { sheet, grid, table } = createGrid()
125
+ try {
126
+ // 模拟 VTable 拖选结果(表格坐标含偏移:行号列 1 + 列头行 1)
127
+ table.getSelectedCellRanges = () => [{ start: { col: 1, row: 1 }, end: { col: 3, row: 2 } }]
128
+ table.fireListeners(ListTable.EVENT_TYPE.DRAG_SELECT_END, {})
129
+ expect(sheet.getSelection().ranges[0]).toEqual({
130
+ start: { row: 0, col: 0 },
131
+ end: { row: 1, col: 2 }
132
+ })
133
+ expect(sheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
134
+ } finally {
135
+ grid.release()
136
+ }
137
+ })
138
+
139
+ it('键盘绑定:Ctrl+Z undo、Ctrl+Shift+Z / Ctrl+Y redo;编辑器 input 不拦截', () => {
140
+ const container = createContainer()
141
+ const sheet = new Sheet()
142
+ const grid = new SheetGrid({ container, sheet, rows: 20, cols: 6 })
143
+ try {
144
+ sheet.setCellValue({ row: 0, col: 0 }, 'x')
145
+
146
+ // Ctrl+Z → undo
147
+ container.dispatchEvent(
148
+ new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true })
149
+ )
150
+ expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
151
+
152
+ // Ctrl+Shift+Z → redo
153
+ container.dispatchEvent(
154
+ new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, shiftKey: true, bubbles: true })
155
+ )
156
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'x' })
157
+
158
+ // Ctrl+Z → undo;Ctrl+Y → redo
159
+ container.dispatchEvent(
160
+ new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true })
161
+ )
162
+ container.dispatchEvent(
163
+ new KeyboardEvent('keydown', { key: 'y', ctrlKey: true, bubbles: true })
164
+ )
165
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'x' })
166
+
167
+ // 事件来自编辑器 input(编辑中)→ 不拦截
168
+ const input = document.createElement('input')
169
+ container.appendChild(input)
170
+ sheet.setCellValue({ row: 0, col: 1 }, 'y')
171
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true }))
172
+ expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'y' })
173
+ } finally {
174
+ grid.release()
175
+ }
176
+ })
177
+ })