@veltra/sheet 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -26
- package/dist/index.js +3 -23
- package/dist/tools/builtin.js +279 -8
- package/dist/tools/context.d.ts +54 -11
- package/dist/tools/context.js +28 -5
- package/dist/tools/download.js +68 -0
- package/dist/tools/export.worker.d.ts +16 -0
- package/dist/tools/export.worker.js +29 -0
- package/dist/tools/registry.d.ts +7 -4
- package/dist/tools/registry.js +1 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sheet.d.ts +6 -5
- package/dist/vue/formula-bar.js +393 -0
- package/dist/vue/formula-suggest-list.js +66 -0
- package/dist/vue/import-file.js +140 -0
- package/dist/vue/insert-count-menu-item.js +68 -0
- package/dist/vue/insert-image.js +79 -0
- package/dist/vue/popup-helpers.js +73 -0
- package/dist/vue/popups/border-popup.js +75 -0
- package/dist/vue/popups/border-preset-glyph.js +106 -0
- package/dist/vue/popups/export-popup.js +51 -0
- package/dist/vue/popups/fill-color-popup.js +37 -0
- package/dist/vue/popups/find-popup.js +113 -0
- package/dist/vue/popups/font-color-popup.js +37 -0
- package/dist/vue/popups/font-size-popup.js +63 -0
- package/dist/vue/popups/import.worker.d.ts +21 -0
- package/dist/vue/popups/import.worker.js +41 -0
- package/dist/vue/popups/insert-image-popup.js +38 -0
- package/dist/vue/sheet-context-menu.js +200 -0
- package/dist/vue/sheet-tabs.js +162 -0
- package/dist/vue/sheet-toolbar.js +106 -0
- package/dist/vue/sheet.js +144 -164
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +10 -0
- package/dist/vue/style2.css +564 -3
- package/dist/vue/use-find-replace.js +161 -0
- package/dist/vue/use-formula-ref-select.js +37 -0
- package/dist/vue/use-formula-suggest.js +126 -0
- package/dist/vue/use-sheet-grid.js +180 -0
- package/dist/vue/use-sheet-state.js +122 -0
- package/dist/vue/use-sheet-tabs-bar.js +90 -0
- package/dist/vue/use-tool-groups.js +42 -0
- package/dist/vue/use-tool-popup.js +123 -0
- package/dist/vue/use-toolbar-scroll.js +72 -0
- package/package.json +12 -7
- package/src/env.d.ts +11 -0
- package/src/index.ts +203 -14
- package/src/tools/__test__/builtin.test.ts +208 -66
- package/src/tools/__test__/context.test.ts +62 -2
- package/src/tools/builtin.ts +350 -6
- package/src/tools/context.ts +89 -11
- package/src/tools/download.ts +82 -0
- package/src/tools/export.worker.ts +61 -0
- package/src/tools/registry.ts +16 -1
- package/src/types/index.ts +6 -0
- package/src/types/sheet.ts +5 -3
- package/src/vue/__test__/formula-bar.test.ts +455 -0
- package/src/vue/__test__/import-file.test.ts +183 -0
- package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/vue/__test__/insert-image-popup.test.ts +89 -0
- package/src/vue/__test__/insert-image.test.ts +100 -0
- package/src/vue/__test__/sheet-component.test.ts +716 -27
- package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
- package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
- package/src/vue/formula-bar.vue +473 -0
- package/src/vue/formula-suggest-list.vue +64 -0
- package/src/vue/import-file.ts +173 -0
- package/src/vue/insert-count-menu-item.vue +69 -0
- package/src/vue/insert-image.ts +90 -0
- package/src/vue/parsing.ts +11 -0
- package/src/vue/popup-helpers.ts +50 -0
- package/src/vue/popups/border-popup.vue +93 -0
- package/src/vue/popups/border-preset-glyph.vue +111 -0
- package/src/vue/popups/export-popup.vue +52 -0
- package/src/vue/popups/fill-color-popup.vue +39 -0
- package/src/vue/popups/find-popup.vue +111 -0
- package/src/vue/popups/font-color-popup.vue +39 -0
- package/src/vue/popups/font-size-popup.vue +52 -0
- package/src/vue/popups/import.worker.ts +66 -0
- package/src/vue/popups/insert-image-popup.vue +39 -0
- package/src/vue/sheet-context-menu.ts +229 -0
- package/src/vue/sheet-tabs.vue +162 -0
- package/src/vue/sheet-toolbar.vue +91 -0
- package/src/vue/sheet.vue +161 -186
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +11 -0
- package/src/vue/use-find-replace.ts +200 -0
- package/src/vue/use-formula-ref-select.ts +34 -0
- package/src/vue/use-formula-suggest.ts +150 -0
- package/src/vue/use-sheet-grid.ts +243 -0
- package/src/vue/use-sheet-state.ts +161 -0
- package/src/vue/use-sheet-tabs-bar.ts +115 -0
- package/src/vue/use-tool-groups.ts +61 -0
- package/src/vue/use-tool-popup.ts +150 -0
- package/src/vue/use-toolbar-scroll.ts +95 -0
- package/dist/core/address.d.ts +0 -60
- package/dist/core/address.js +0 -142
- package/dist/core/address.js.map +0 -1
- package/dist/core/cell-store.d.ts +0 -61
- package/dist/core/cell-store.js +0 -135
- package/dist/core/cell-store.js.map +0 -1
- package/dist/core/command/default-registry.d.ts +0 -10
- package/dist/core/command/default-registry.js +0 -18
- package/dist/core/command/default-registry.js.map +0 -1
- package/dist/core/command/history.d.ts +0 -64
- package/dist/core/command/history.js +0 -127
- package/dist/core/command/history.js.map +0 -1
- package/dist/core/command/merge-cells.d.ts +0 -21
- package/dist/core/command/merge-cells.js +0 -74
- package/dist/core/command/merge-cells.js.map +0 -1
- package/dist/core/command/registry.d.ts +0 -17
- package/dist/core/command/registry.js +0 -26
- package/dist/core/command/registry.js.map +0 -1
- package/dist/core/command/set-cell-formula.d.ts +0 -17
- package/dist/core/command/set-cell-formula.js +0 -29
- package/dist/core/command/set-cell-formula.js.map +0 -1
- package/dist/core/command/set-cell-value.d.ts +0 -22
- package/dist/core/command/set-cell-value.js +0 -35
- package/dist/core/command/set-cell-value.js.map +0 -1
- package/dist/core/command/types.d.ts +0 -68
- package/dist/core/events.js +0 -29
- package/dist/core/events.js.map +0 -1
- package/dist/core/fill.d.ts +0 -34
- package/dist/core/fill.js +0 -320
- package/dist/core/fill.js.map +0 -1
- package/dist/core/formula/ast.d.ts +0 -58
- package/dist/core/formula/ast.js +0 -38
- package/dist/core/formula/ast.js.map +0 -1
- package/dist/core/formula/dependency-graph.d.ts +0 -70
- package/dist/core/formula/dependency-graph.js +0 -269
- package/dist/core/formula/dependency-graph.js.map +0 -1
- package/dist/core/formula/errors.d.ts +0 -25
- package/dist/core/formula/errors.js +0 -36
- package/dist/core/formula/errors.js.map +0 -1
- package/dist/core/formula/evaluator.d.ts +0 -41
- package/dist/core/formula/evaluator.js +0 -154
- package/dist/core/formula/evaluator.js.map +0 -1
- package/dist/core/formula/functions.d.ts +0 -30
- package/dist/core/formula/functions.js +0 -209
- package/dist/core/formula/functions.js.map +0 -1
- package/dist/core/formula/parser.d.ts +0 -7
- package/dist/core/formula/parser.js +0 -208
- package/dist/core/formula/parser.js.map +0 -1
- package/dist/core/formula/tokenizer.d.ts +0 -37
- package/dist/core/formula/tokenizer.js +0 -146
- package/dist/core/formula/tokenizer.js.map +0 -1
- package/dist/core/merge-manager.d.ts +0 -64
- package/dist/core/merge-manager.js +0 -111
- package/dist/core/merge-manager.js.map +0 -1
- package/dist/core/selection.d.ts +0 -31
- package/dist/core/selection.js +0 -62
- package/dist/core/selection.js.map +0 -1
- package/dist/core/sheet.d.ts +0 -116
- package/dist/core/sheet.js +0 -224
- package/dist/core/sheet.js.map +0 -1
- package/dist/core/workbook.d.ts +0 -43
- package/dist/core/workbook.js +0 -77
- package/dist/core/workbook.js.map +0 -1
- package/dist/grid/sheet-grid.d.ts +0 -81
- package/dist/grid/sheet-grid.js +0 -331
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/grid/vtable-theme.js +0 -80
- package/dist/grid/vtable-theme.js.map +0 -1
- package/dist/tools/builtin.js.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/registry.js.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/src/core/__test__/address.test.ts +0 -113
- package/src/core/__test__/cell-store.test.ts +0 -97
- package/src/core/__test__/command.test.ts +0 -377
- package/src/core/__test__/fill.test.ts +0 -140
- package/src/core/__test__/formula-adversarial.test.ts +0 -265
- package/src/core/__test__/formula-engine.test.ts +0 -470
- package/src/core/__test__/formula-functions.test.ts +0 -178
- package/src/core/__test__/formula-parser.test.ts +0 -160
- package/src/core/__test__/history.test.ts +0 -225
- package/src/core/__test__/merge-manager.test.ts +0 -106
- package/src/core/__test__/sheet.test.ts +0 -142
- package/src/core/address.ts +0 -166
- package/src/core/cell-store.ts +0 -173
- package/src/core/command/default-registry.ts +0 -15
- package/src/core/command/history.ts +0 -167
- package/src/core/command/index.ts +0 -7
- package/src/core/command/merge-cells.ts +0 -74
- package/src/core/command/registry.ts +0 -30
- package/src/core/command/set-cell-formula.ts +0 -29
- package/src/core/command/set-cell-value.ts +0 -38
- package/src/core/command/types.ts +0 -73
- package/src/core/events.ts +0 -37
- package/src/core/fill.ts +0 -314
- package/src/core/formula/ast.ts +0 -66
- package/src/core/formula/dependency-graph.ts +0 -329
- package/src/core/formula/errors.ts +0 -41
- package/src/core/formula/evaluator.ts +0 -219
- package/src/core/formula/functions.ts +0 -266
- package/src/core/formula/index.ts +0 -7
- package/src/core/formula/parser.ts +0 -198
- package/src/core/formula/tokenizer.ts +0 -160
- package/src/core/merge-manager.ts +0 -158
- package/src/core/selection.ts +0 -66
- package/src/core/sheet.ts +0 -282
- package/src/core/workbook.ts +0 -99
- package/src/grid/__test__/canvas-mock.ts +0 -49
- package/src/grid/__test__/setup.ts +0 -3
- package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
- package/src/grid/__test__/sheet-grid.test.ts +0 -275
- package/src/grid/sheet-grid.ts +0 -407
- package/src/grid/vtable-theme.ts +0 -61
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Workbook } from '@veltra/sheet-core/core/workbook'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
3
|
import { createApp, h, nextTick, ref, type App } from 'vue'
|
|
3
4
|
|
|
4
|
-
import { Workbook } from '../../core/workbook'
|
|
5
5
|
import { USheet } from '../../index'
|
|
6
6
|
import { registerTool, unregisterTool } from '../../tools/registry'
|
|
7
7
|
import type { SheetExposed } from '../../types'
|
|
@@ -38,12 +38,38 @@ function createWorkbook() {
|
|
|
38
38
|
return workbook
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
48
|
)
|
|
45
49
|
}
|
|
46
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
|
+
|
|
47
73
|
function tabs(el: HTMLElement): HTMLButtonElement[] {
|
|
48
74
|
return [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tab')]
|
|
49
75
|
}
|
|
@@ -54,22 +80,95 @@ afterEach(() => {
|
|
|
54
80
|
})
|
|
55
81
|
|
|
56
82
|
describe('USheet 组件', () => {
|
|
57
|
-
it('
|
|
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 () => {
|
|
58
96
|
const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
|
|
59
97
|
await nextTick()
|
|
60
98
|
|
|
61
|
-
const tools = [...el.querySelectorAll('.u-sheet__tool')].map(
|
|
62
|
-
button.
|
|
99
|
+
const tools = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tool')].map(
|
|
100
|
+
(button) => button.dataset.toolId
|
|
63
101
|
)
|
|
64
|
-
expect(tools).toEqual([
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
150
|
|
|
68
151
|
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2'])
|
|
69
152
|
expect(tabs(el)[0]!.classList.contains('is-active')).toBe(true)
|
|
70
153
|
|
|
71
154
|
// VTable 已挂载到 grid 容器
|
|
72
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)
|
|
73
172
|
})
|
|
74
173
|
|
|
75
174
|
it('内置 undo/redo 按钮随 history-change 置灰,点击等效快捷键', async () => {
|
|
@@ -78,24 +177,24 @@ describe('USheet 组件', () => {
|
|
|
78
177
|
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
79
178
|
await nextTick()
|
|
80
179
|
|
|
81
|
-
const undoButton = toolButton(el, '
|
|
82
|
-
const redoButton = toolButton(el, '
|
|
180
|
+
const undoButton = toolButton(el, 'undo')!
|
|
181
|
+
const redoButton = toolButton(el, 'redo')!
|
|
83
182
|
expect(undoButton.disabled).toBe(true)
|
|
84
183
|
expect(redoButton.disabled).toBe(true)
|
|
85
184
|
|
|
86
185
|
sheet.setCellValue({ row: 0, col: 0 }, 'x')
|
|
87
|
-
await
|
|
186
|
+
await flushState()
|
|
88
187
|
expect(undoButton.disabled).toBe(false)
|
|
89
188
|
expect(redoButton.disabled).toBe(true)
|
|
90
189
|
|
|
91
190
|
undoButton.click()
|
|
92
|
-
await
|
|
191
|
+
await flushState()
|
|
93
192
|
expect(sheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
|
|
94
193
|
expect(undoButton.disabled).toBe(true)
|
|
95
194
|
expect(redoButton.disabled).toBe(false)
|
|
96
195
|
|
|
97
196
|
redoButton.click()
|
|
98
|
-
await
|
|
197
|
+
await flushState()
|
|
99
198
|
expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'x', t: 's' })
|
|
100
199
|
})
|
|
101
200
|
|
|
@@ -112,8 +211,12 @@ describe('USheet 组件', () => {
|
|
|
112
211
|
await nextTick()
|
|
113
212
|
|
|
114
213
|
expect(tabs(el)[1]!.classList.contains('is-active')).toBe(true)
|
|
115
|
-
// grid
|
|
116
|
-
expect(el.querySelectorAll('.u-sheet__grid canvas')).toHaveLength(
|
|
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')
|
|
117
220
|
expect(exposed.value?.getActiveSheet().name).toBe('Sheet2')
|
|
118
221
|
// 工具上下文(与工具按钮同一门面)指向切换后的 sheet
|
|
119
222
|
expect(exposed.value?.getContext().sheetName).toBe('Sheet2')
|
|
@@ -173,15 +276,17 @@ describe('USheet 组件', () => {
|
|
|
173
276
|
it('注册表联动:运行时 register/unregister 工具即时增删按钮', async () => {
|
|
174
277
|
const { el } = mount(() => ({ workbook: createWorkbook(), rows: 10, cols: 6 }))
|
|
175
278
|
await nextTick()
|
|
176
|
-
expect(toolButton(el, '
|
|
279
|
+
expect(toolButton(el, 'temp-tool')).toBeUndefined()
|
|
177
280
|
|
|
178
281
|
registerTool({ id: 'temp-tool', title: '临时工具', onClick: () => {} })
|
|
179
282
|
await nextTick()
|
|
180
|
-
expect(toolButton(el, '
|
|
283
|
+
expect(toolButton(el, 'temp-tool')).toBeDefined()
|
|
284
|
+
// 无 icon 时回落文字按钮
|
|
285
|
+
expect(toolButton(el, 'temp-tool')!.textContent?.trim()).toBe('临时工具')
|
|
181
286
|
|
|
182
287
|
expect(unregisterTool('temp-tool')).toBe(true)
|
|
183
288
|
await nextTick()
|
|
184
|
-
expect(toolButton(el, '
|
|
289
|
+
expect(toolButton(el, 'temp-tool')).toBeUndefined()
|
|
185
290
|
})
|
|
186
291
|
|
|
187
292
|
it('无 props 默认挂载:内部自建工作簿(单 sheet),工具栏/tabs 正常', async () => {
|
|
@@ -192,6 +297,37 @@ describe('USheet 组件', () => {
|
|
|
192
297
|
expect(el.querySelector('.u-sheet__grid canvas')).not.toBeNull()
|
|
193
298
|
})
|
|
194
299
|
|
|
300
|
+
it('tab「+」按钮:添加 sheet 并自动激活新表', async () => {
|
|
301
|
+
const workbook = createWorkbook()
|
|
302
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
303
|
+
await nextTick()
|
|
304
|
+
|
|
305
|
+
const addButton = el.querySelector<HTMLButtonElement>('.u-sheet__tab-add')!
|
|
306
|
+
addButton.click()
|
|
307
|
+
await nextTick()
|
|
308
|
+
|
|
309
|
+
expect(workbook.sheetCount).toBe(3)
|
|
310
|
+
expect(workbook.activeSheet.name).toBe('Sheet3')
|
|
311
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', 'Sheet2', 'Sheet3'])
|
|
312
|
+
// 新表自动激活:active 样式落在新 tab 上
|
|
313
|
+
expect(tabs(el)[2]!.classList.contains('is-active')).toBe(true)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('renameSheet 后 tab 文本跟随更新', async () => {
|
|
317
|
+
const workbook = createWorkbook()
|
|
318
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
319
|
+
await nextTick()
|
|
320
|
+
|
|
321
|
+
expect(workbook.renameSheet('Sheet2', '数据表')).toBe(true)
|
|
322
|
+
await nextTick()
|
|
323
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
|
|
324
|
+
|
|
325
|
+
// 重名(大小写不敏感)被拒绝:tab 文本不变
|
|
326
|
+
expect(workbook.renameSheet('Sheet1', '数据表')).toBe(false)
|
|
327
|
+
await nextTick()
|
|
328
|
+
expect(tabs(el).map((tab) => tab.textContent?.trim())).toEqual(['Sheet1', '数据表'])
|
|
329
|
+
})
|
|
330
|
+
|
|
195
331
|
it('多实例并存:共享注册表,工具点击只作用于各自实例的活动 sheet', async () => {
|
|
196
332
|
const workbook1 = new Workbook()
|
|
197
333
|
const workbook2 = new Workbook()
|
|
@@ -210,8 +346,8 @@ describe('USheet 组件', () => {
|
|
|
210
346
|
await nextTick()
|
|
211
347
|
|
|
212
348
|
// 两个实例都渲染该工具(注册表全局共享)
|
|
213
|
-
const button1 = toolButton(first.el, '
|
|
214
|
-
const button2 = toolButton(second.el, '
|
|
349
|
+
const button1 = toolButton(first.el, 'mark-active')
|
|
350
|
+
const button2 = toolButton(second.el, 'mark-active')
|
|
215
351
|
expect(button1).toBeDefined()
|
|
216
352
|
expect(button2).toBeDefined()
|
|
217
353
|
|
|
@@ -237,15 +373,15 @@ describe('USheet 组件', () => {
|
|
|
237
373
|
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
238
374
|
await nextTick()
|
|
239
375
|
|
|
240
|
-
const mergeButton = toolButton(el, '
|
|
241
|
-
const unmergeButton = toolButton(el, '
|
|
242
|
-
//
|
|
376
|
+
const mergeButton = toolButton(el, 'merge')!
|
|
377
|
+
const unmergeButton = toolButton(el, 'unmerge')!
|
|
378
|
+
// 默认单格 A1:合并禁用;无合并 → 取消合并禁用
|
|
243
379
|
expect(mergeButton.disabled).toBe(true)
|
|
244
380
|
expect(unmergeButton.disabled).toBe(true)
|
|
245
381
|
|
|
246
382
|
// 拖选区域(经模型 API 模拟 grid 拖选回写)
|
|
247
383
|
sheet.selectRange({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
|
|
248
|
-
await
|
|
384
|
+
await flushState()
|
|
249
385
|
expect(mergeButton.disabled).toBe(false)
|
|
250
386
|
|
|
251
387
|
mergeButton.click()
|
|
@@ -264,4 +400,557 @@ describe('USheet 组件', () => {
|
|
|
264
400
|
sheet.undo()
|
|
265
401
|
expect(sheet.getCellInfo({ row: 1, col: 1 }).kind).toBe('merged-covered')
|
|
266
402
|
})
|
|
403
|
+
|
|
404
|
+
it('弹层工具:点击打开面板(UPalette 渲染),点击面板外关闭并提交事务', async () => {
|
|
405
|
+
const workbook = createWorkbook()
|
|
406
|
+
const sheet = workbook.activeSheet
|
|
407
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
408
|
+
await nextTick()
|
|
409
|
+
|
|
410
|
+
// 默认选区 A1:填充可用
|
|
411
|
+
const fillButton = toolButton(el, 'fill-color')!
|
|
412
|
+
expect(fillButton.disabled).toBe(false)
|
|
413
|
+
|
|
414
|
+
fillButton.click()
|
|
415
|
+
await flushPopup()
|
|
416
|
+
// 面板已渲染(含 UPalette 取色器)
|
|
417
|
+
const popupEl = document.querySelector<HTMLElement>('#pop-container .u-sheet__popup')
|
|
418
|
+
expect(popupEl).not.toBeNull()
|
|
419
|
+
expect(document.querySelector('#pop-container .u-sheet__popup .u-palette')).not.toBeNull()
|
|
420
|
+
// 面板 Teleport 到 body 级 #pop-container(不在 mount 容器内,定位由 usePop 接管)
|
|
421
|
+
expect(el.querySelector('.u-sheet__popup')).toBeNull()
|
|
422
|
+
|
|
423
|
+
// 点击面板外(grid 区)→ 关闭;空事务不入历史
|
|
424
|
+
el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
425
|
+
await nextTick()
|
|
426
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
427
|
+
expect(sheet.history.undoSize).toBe(0)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('文本工具:加粗 toggle + active 高亮;字体颜色弹层可打开', async () => {
|
|
431
|
+
const workbook = createWorkbook()
|
|
432
|
+
const sheet = workbook.activeSheet
|
|
433
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
434
|
+
await nextTick()
|
|
435
|
+
|
|
436
|
+
const boldButton = toolButton(el, 'bold')!
|
|
437
|
+
expect(boldButton.classList.contains('is-active')).toBe(false)
|
|
438
|
+
boldButton.click()
|
|
439
|
+
await flushState()
|
|
440
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.bold).toBe(true)
|
|
441
|
+
expect(boldButton.classList.contains('is-active')).toBe(true)
|
|
442
|
+
boldButton.click()
|
|
443
|
+
await flushState()
|
|
444
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
|
|
445
|
+
expect(boldButton.classList.contains('is-active')).toBe(false)
|
|
446
|
+
|
|
447
|
+
toolButton(el, 'font-color')!.click()
|
|
448
|
+
await flushPopup()
|
|
449
|
+
expect(document.querySelector('#pop-container .u-sheet__popup .u-palette')).not.toBeNull()
|
|
450
|
+
el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
451
|
+
await nextTick()
|
|
452
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('字号弹层:预设写入 + 关闭提交为一个 undo 单元', async () => {
|
|
456
|
+
const workbook = createWorkbook()
|
|
457
|
+
const sheet = workbook.activeSheet
|
|
458
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
459
|
+
await nextTick()
|
|
460
|
+
|
|
461
|
+
toolButton(el, 'font-size')!.click()
|
|
462
|
+
await flushPopup()
|
|
463
|
+
const sizeButtons = [
|
|
464
|
+
...document.querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__popup-size-item')
|
|
465
|
+
]
|
|
466
|
+
expect(sizeButtons.map((button) => button.textContent?.trim())).toContain('14')
|
|
467
|
+
sizeButtons.find((button) => button.textContent?.trim() === '14')!.click()
|
|
468
|
+
await nextTick()
|
|
469
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })?.font?.size).toBe(14)
|
|
470
|
+
|
|
471
|
+
el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
472
|
+
await nextTick()
|
|
473
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
474
|
+
expect(sheet.history.undoSize).toBe(1)
|
|
475
|
+
sheet.undo()
|
|
476
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('边框面板:预设应用 + 关闭提交为一个 undo 单元', async () => {
|
|
480
|
+
const workbook = createWorkbook()
|
|
481
|
+
const sheet = workbook.activeSheet
|
|
482
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
483
|
+
await nextTick()
|
|
484
|
+
|
|
485
|
+
sheet.selectRange({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
|
|
486
|
+
await nextTick()
|
|
487
|
+
toolButton(el, 'border')!.click()
|
|
488
|
+
await flushPopup()
|
|
489
|
+
|
|
490
|
+
const presetButtons = [...document.querySelectorAll('#pop-container .u-sheet__popup-preset')]
|
|
491
|
+
expect(presetButtons.map((button) => (button as HTMLElement).title)).toEqual([
|
|
492
|
+
'外边框',
|
|
493
|
+
'内边框',
|
|
494
|
+
'所有边框',
|
|
495
|
+
'上边框',
|
|
496
|
+
'下边框',
|
|
497
|
+
'左边框',
|
|
498
|
+
'右边框',
|
|
499
|
+
'无边框'
|
|
500
|
+
])
|
|
501
|
+
|
|
502
|
+
presetButtons.find((button) => (button as HTMLElement).title === '所有边框')!.click()
|
|
503
|
+
await nextTick()
|
|
504
|
+
// 选区内每格四边都有边框
|
|
505
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })?.border?.top).toBeDefined()
|
|
506
|
+
expect(sheet.getCellStyle({ row: 1, col: 1 })?.border?.bottom).toBeDefined()
|
|
507
|
+
expect(sheet.getCellStyle({ row: 0, col: 1 })?.border?.left).toBeDefined()
|
|
508
|
+
|
|
509
|
+
// 无边框 → 清除边框
|
|
510
|
+
presetButtons.find((button) => (button as HTMLElement).title === '无边框')!.click()
|
|
511
|
+
await nextTick()
|
|
512
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })?.border).toBeUndefined()
|
|
513
|
+
|
|
514
|
+
// 点击外部关闭 → 面板期间所有写入合并为一个 undo 单元
|
|
515
|
+
el.querySelector('.u-sheet__grid')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
516
|
+
await nextTick()
|
|
517
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
518
|
+
expect(sheet.history.undoSize).toBe(1)
|
|
519
|
+
|
|
520
|
+
sheet.undo()
|
|
521
|
+
expect(sheet.getCellStyle({ row: 0, col: 0 })).toBeUndefined()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('冻结:模型 setFrozen 驱动 VTable 冻结布局联动(工具栏入口已迁至右键菜单)', async () => {
|
|
525
|
+
const workbook = createWorkbook()
|
|
526
|
+
const sheet = workbook.activeSheet
|
|
527
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
528
|
+
mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
|
|
529
|
+
await nextTick()
|
|
530
|
+
|
|
531
|
+
sheet.setFrozen(1, 0)
|
|
532
|
+
await nextTick()
|
|
533
|
+
expect(sheet.frozen).toEqual({ rows: 1, cols: 0 })
|
|
534
|
+
// 模型 1 行 → frozenRowCount = 2:列头行 + 首行;列保持 1:行号列
|
|
535
|
+
expect(exposed.value?.getGrid()?.getTable().frozenRowCount).toBe(2)
|
|
536
|
+
expect(exposed.value?.getGrid()?.getTable().frozenColCount).toBe(1)
|
|
537
|
+
|
|
538
|
+
sheet.setFrozen(1, 1)
|
|
539
|
+
await nextTick()
|
|
540
|
+
expect(exposed.value?.getGrid()?.getTable().frozenColCount).toBe(2)
|
|
541
|
+
|
|
542
|
+
sheet.setFrozen(0, 0)
|
|
543
|
+
await nextTick()
|
|
544
|
+
expect(sheet.frozen).toEqual({ rows: 0, cols: 0 })
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('导入:点击直接拉起系统文件选择,不打开弹层、不产生历史条目', async () => {
|
|
548
|
+
const workbook = createWorkbook()
|
|
549
|
+
const sheet = workbook.activeSheet
|
|
550
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
551
|
+
await nextTick()
|
|
552
|
+
|
|
553
|
+
const click = vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(() => {})
|
|
554
|
+
try {
|
|
555
|
+
toolButton(el, 'import')!.click()
|
|
556
|
+
await nextTick()
|
|
557
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
558
|
+
const input = document.body.querySelector<HTMLInputElement>('input[type="file"]')
|
|
559
|
+
expect(input).toBeTruthy()
|
|
560
|
+
expect(input!.accept).toBe('.xlsx,.csv')
|
|
561
|
+
expect(input!.hidden).toBe(true)
|
|
562
|
+
expect(click).toHaveBeenCalled()
|
|
563
|
+
expect(sheet.history.undoSize).toBe(0)
|
|
564
|
+
} finally {
|
|
565
|
+
click.mockRestore()
|
|
566
|
+
document.body.querySelectorAll('input[type="file"]').forEach((node) => node.remove())
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('导出弹层:单按钮打开两选项;点击选项触发下载并关闭,不产生历史', async () => {
|
|
571
|
+
const workbook = createWorkbook()
|
|
572
|
+
const sheet = workbook.activeSheet
|
|
573
|
+
sheet.setCellValue({ row: 0, col: 0 }, 1)
|
|
574
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
575
|
+
await nextTick()
|
|
576
|
+
|
|
577
|
+
const originalCreate = URL.createObjectURL
|
|
578
|
+
const originalRevoke = URL.revokeObjectURL
|
|
579
|
+
const createObjectURL = vi.fn(() => 'blob:mock')
|
|
580
|
+
URL.createObjectURL = createObjectURL as typeof URL.createObjectURL
|
|
581
|
+
URL.revokeObjectURL = vi.fn() as typeof URL.revokeObjectURL
|
|
582
|
+
try {
|
|
583
|
+
toolButton(el, 'export')!.click()
|
|
584
|
+
await flushPopup()
|
|
585
|
+
const options = [
|
|
586
|
+
...document.querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__export-option')
|
|
587
|
+
]
|
|
588
|
+
expect(options.map((button) => button.textContent?.trim())).toEqual([
|
|
589
|
+
'导出 Excel (.xlsx)',
|
|
590
|
+
'导出 CSV (.csv)'
|
|
591
|
+
])
|
|
592
|
+
|
|
593
|
+
options[0]!.click()
|
|
594
|
+
await nextTick()
|
|
595
|
+
await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
|
|
596
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
597
|
+
expect(sheet.history.undoSize).toBe(1) // 预置 setCellValue;导出本身不写模型
|
|
598
|
+
|
|
599
|
+
toolButton(el, 'export')!.click()
|
|
600
|
+
await flushPopup()
|
|
601
|
+
createObjectURL.mockClear()
|
|
602
|
+
document
|
|
603
|
+
.querySelectorAll<HTMLButtonElement>('#pop-container .u-sheet__export-option')[1]!
|
|
604
|
+
.click()
|
|
605
|
+
await nextTick()
|
|
606
|
+
await vi.waitFor(() => expect(createObjectURL).toHaveBeenCalled(), { timeout: 2000 })
|
|
607
|
+
const csvBlob = createObjectURL.mock.calls[0]![0] as Blob
|
|
608
|
+
expect(csvBlob.type).toContain('text/csv')
|
|
609
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
610
|
+
} finally {
|
|
611
|
+
URL.createObjectURL = originalCreate
|
|
612
|
+
URL.revokeObjectURL = originalRevoke
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('查找条:打开 → 关键词定位命中 → Enter 下一个 / Shift+Enter 上一个 → 关闭', async () => {
|
|
617
|
+
const workbook = createWorkbook()
|
|
618
|
+
const sheet = workbook.activeSheet
|
|
619
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'hello')
|
|
620
|
+
sheet.setCellValue({ row: 0, col: 1 }, 'world')
|
|
621
|
+
sheet.setCellValue({ row: 1, col: 0 }, 'hello again')
|
|
622
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
623
|
+
await nextTick()
|
|
624
|
+
|
|
625
|
+
toolButton(el, 'find')!.click()
|
|
626
|
+
await flushPopup()
|
|
627
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).not.toBeNull()
|
|
628
|
+
expect(document.querySelector('#pop-container .u-sheet__find-input')).not.toBeNull()
|
|
629
|
+
|
|
630
|
+
// 输入关键词 → 定位第一个命中(A1)
|
|
631
|
+
const input = document.querySelector<HTMLInputElement>(
|
|
632
|
+
'#pop-container .u-sheet__find-input .u-input__native'
|
|
633
|
+
)!
|
|
634
|
+
input.value = 'hello'
|
|
635
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
636
|
+
await flushFind()
|
|
637
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
|
|
638
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
639
|
+
'1 / 2'
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
// Enter → 下一个命中(A3)
|
|
643
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
644
|
+
await nextTick()
|
|
645
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 0 })
|
|
646
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
647
|
+
'2 / 2'
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
// Enter 循环回第一个
|
|
651
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
652
|
+
await nextTick()
|
|
653
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 0, col: 0 })
|
|
654
|
+
|
|
655
|
+
// Shift+Enter → 上一个(循环回最后一个)
|
|
656
|
+
input.dispatchEvent(
|
|
657
|
+
new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true })
|
|
658
|
+
)
|
|
659
|
+
await nextTick()
|
|
660
|
+
expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 0 })
|
|
661
|
+
|
|
662
|
+
// 无命中 → 计数 0 / 0,导航禁用
|
|
663
|
+
input.value = '不存在'
|
|
664
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
665
|
+
await flushFind()
|
|
666
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
667
|
+
'0 / 0'
|
|
668
|
+
)
|
|
669
|
+
expect(
|
|
670
|
+
document.querySelector<HTMLButtonElement>('#pop-container .u-sheet__find-nav')!.disabled
|
|
671
|
+
).toBe(true)
|
|
672
|
+
|
|
673
|
+
// 关闭
|
|
674
|
+
document.querySelector<HTMLButtonElement>('#pop-container .u-sheet__find-close')!.click()
|
|
675
|
+
await nextTick()
|
|
676
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('替换:单个替换可 undo;全部替换 = 单 undo 单元,undo 一次全部还原', async () => {
|
|
680
|
+
const workbook = createWorkbook()
|
|
681
|
+
const sheet = workbook.activeSheet
|
|
682
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'foo')
|
|
683
|
+
sheet.setCellValue({ row: 0, col: 1 }, 'foo')
|
|
684
|
+
sheet.setCellValue({ row: 1, col: 0 }, 'foo')
|
|
685
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
686
|
+
await nextTick()
|
|
687
|
+
|
|
688
|
+
toolButton(el, 'find')!.click()
|
|
689
|
+
await flushPopup()
|
|
690
|
+
|
|
691
|
+
const queryInput = document.querySelector<HTMLInputElement>(
|
|
692
|
+
'#pop-container .u-sheet__find-input .u-input__native'
|
|
693
|
+
)!
|
|
694
|
+
queryInput.value = 'foo'
|
|
695
|
+
queryInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
696
|
+
await flushFind()
|
|
697
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
698
|
+
'1 / 3'
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
// 全部替换:3 格一次写入
|
|
702
|
+
const replaceInputs = [
|
|
703
|
+
...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
|
|
704
|
+
]
|
|
705
|
+
replaceInputs[1]!.value = 'bar'
|
|
706
|
+
replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
707
|
+
await nextTick()
|
|
708
|
+
document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
|
|
709
|
+
await nextTick()
|
|
710
|
+
|
|
711
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'bar' })
|
|
712
|
+
expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'bar' })
|
|
713
|
+
expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'bar' })
|
|
714
|
+
// 全部替换 = 单 undo 单元(3 次 setCellValue 初始 + 1 次替换)
|
|
715
|
+
expect(sheet.history.undoSize).toBe(4)
|
|
716
|
+
|
|
717
|
+
// undo 一次 → 全部还原
|
|
718
|
+
sheet.undo()
|
|
719
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'foo' })
|
|
720
|
+
expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'foo' })
|
|
721
|
+
expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'foo' })
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it('全部替换跳过公式格(不覆盖公式原文)', async () => {
|
|
725
|
+
const workbook = createWorkbook()
|
|
726
|
+
const sheet = workbook.activeSheet
|
|
727
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'foo')
|
|
728
|
+
sheet.setCellFormula({ row: 1, col: 0 }, '=A1&"!"') // 公式格显示值 foo!
|
|
729
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
730
|
+
await nextTick()
|
|
731
|
+
|
|
732
|
+
toolButton(el, 'find')!.click()
|
|
733
|
+
await flushPopup()
|
|
734
|
+
|
|
735
|
+
const queryInput = document.querySelector<HTMLInputElement>(
|
|
736
|
+
'#pop-container .u-sheet__find-input .u-input__native'
|
|
737
|
+
)!
|
|
738
|
+
queryInput.value = 'foo'
|
|
739
|
+
queryInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
740
|
+
await flushFind()
|
|
741
|
+
// 普通格 + 公式格显示值都命中
|
|
742
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
743
|
+
'1 / 2'
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
const replaceInputs = [
|
|
747
|
+
...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
|
|
748
|
+
]
|
|
749
|
+
replaceInputs[1]!.value = 'bar'
|
|
750
|
+
replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
751
|
+
await nextTick()
|
|
752
|
+
document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
|
|
753
|
+
await nextTick()
|
|
754
|
+
|
|
755
|
+
// 普通格被替换,公式格保留公式(显示值也因依赖 A1 变化为 bar!)
|
|
756
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'bar' })
|
|
757
|
+
expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ f: 'A1&"!"' })
|
|
758
|
+
expect(sheet.getCellData({ row: 1, col: 0 })?.v).toBe('bar!')
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('防抖窗口内全部替换:按输入框当前关键词补扫,不用旧缓存的命中集', async () => {
|
|
762
|
+
const workbook = createWorkbook()
|
|
763
|
+
const sheet = workbook.activeSheet
|
|
764
|
+
sheet.setCellValue({ row: 0, col: 0 }, 'foo')
|
|
765
|
+
sheet.setCellValue({ row: 0, col: 1 }, 'f')
|
|
766
|
+
sheet.setCellValue({ row: 1, col: 0 }, 'f')
|
|
767
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
768
|
+
await nextTick()
|
|
769
|
+
|
|
770
|
+
toolButton(el, 'find')!.click()
|
|
771
|
+
await flushPopup()
|
|
772
|
+
|
|
773
|
+
const queryInput = document.querySelector<HTMLInputElement>(
|
|
774
|
+
'#pop-container .u-sheet__find-input .u-input__native'
|
|
775
|
+
)!
|
|
776
|
+
queryInput.value = 'foo'
|
|
777
|
+
queryInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
778
|
+
await flushFind()
|
|
779
|
+
expect(document.querySelector('#pop-container .u-sheet__find-count')!.textContent?.trim()).toBe(
|
|
780
|
+
'1 / 1'
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
// 快速改输 'f'(不等 300ms 防抖触发)→ 立刻全部替换。
|
|
784
|
+
// 'f' 命中全部 3 格(foo 含 f);若误用 'foo' 的旧缓存只会改写 A1
|
|
785
|
+
queryInput.value = 'f'
|
|
786
|
+
queryInput.dispatchEvent(new Event('input', { bubbles: true }))
|
|
787
|
+
await nextTick()
|
|
788
|
+
|
|
789
|
+
const replaceInputs = [
|
|
790
|
+
...document.querySelectorAll('#pop-container .u-sheet__find-input .u-input__native')
|
|
791
|
+
]
|
|
792
|
+
replaceInputs[1]!.value = 'x'
|
|
793
|
+
replaceInputs[1]!.dispatchEvent(new Event('input', { bubbles: true }))
|
|
794
|
+
await nextTick()
|
|
795
|
+
document.querySelectorAll('#pop-container .u-sheet__find-btn')[1]!.click()
|
|
796
|
+
await nextTick()
|
|
797
|
+
|
|
798
|
+
expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'x' })
|
|
799
|
+
expect(sheet.getCellData({ row: 0, col: 1 })).toMatchObject({ v: 'x' })
|
|
800
|
+
expect(sheet.getCellData({ row: 1, col: 0 })).toMatchObject({ v: 'x' })
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('Ctrl+F:容器外按键不劫持浏览器查找;查找条聚焦时(弹层在 root 外)可 toggle 关闭', async () => {
|
|
804
|
+
const workbook = createWorkbook()
|
|
805
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
806
|
+
await nextTick()
|
|
807
|
+
|
|
808
|
+
// 容器外(body)按 Ctrl+F:不弹查找条、不 preventDefault
|
|
809
|
+
const outside = new KeyboardEvent('keydown', {
|
|
810
|
+
key: 'f',
|
|
811
|
+
ctrlKey: true,
|
|
812
|
+
bubbles: true,
|
|
813
|
+
cancelable: true
|
|
814
|
+
})
|
|
815
|
+
document.body.dispatchEvent(outside)
|
|
816
|
+
await flushPopup()
|
|
817
|
+
expect(outside.defaultPrevented).toBe(false)
|
|
818
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
819
|
+
|
|
820
|
+
// 打开查找条,焦点进入弹层输入框(Teleport 到 #pop-container,不在实例 root 内)
|
|
821
|
+
toolButton(el, 'find')!.click()
|
|
822
|
+
await flushPopup()
|
|
823
|
+
const input = document.querySelector<HTMLInputElement>(
|
|
824
|
+
'#pop-container .u-sheet__find-input .u-input__native'
|
|
825
|
+
)!
|
|
826
|
+
input.focus()
|
|
827
|
+
|
|
828
|
+
// 弹层内再按 Ctrl+F:toggle 关闭查找条,且拦截浏览器原生查找
|
|
829
|
+
const toggle = new KeyboardEvent('keydown', {
|
|
830
|
+
key: 'f',
|
|
831
|
+
ctrlKey: true,
|
|
832
|
+
bubbles: true,
|
|
833
|
+
cancelable: true
|
|
834
|
+
})
|
|
835
|
+
input.dispatchEvent(toggle)
|
|
836
|
+
await nextTick()
|
|
837
|
+
expect(toggle.defaultPrevented).toBe(true)
|
|
838
|
+
expect(document.querySelector('#pop-container .u-sheet__popup')).toBeNull()
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
describe('tabs 栏', () => {
|
|
843
|
+
it('少量 sheet:「+」在视口外且紧随 viewport(nav 隐藏,不占位)', async () => {
|
|
844
|
+
const workbook = createWorkbook()
|
|
845
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
846
|
+
await nextTick()
|
|
847
|
+
|
|
848
|
+
const tabsBar = el.querySelector<HTMLElement>('.u-sheet__tabs')!
|
|
849
|
+
const viewport = el.querySelector<HTMLElement>('.u-sheet__tabs-viewport')!
|
|
850
|
+
const addButton = el.querySelector<HTMLButtonElement>('.u-sheet__tab-add')!
|
|
851
|
+
const navButtons = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tabs-nav')]
|
|
852
|
+
|
|
853
|
+
// 「+」仍是 tabs 直接子级,不在滚动 list 内
|
|
854
|
+
expect(tabsBar.contains(addButton)).toBe(true)
|
|
855
|
+
expect(viewport.contains(addButton)).toBe(false)
|
|
856
|
+
expect(el.querySelector('.u-sheet__tabs-list .u-sheet__tab-add')).toBeNull()
|
|
857
|
+
|
|
858
|
+
// 未溢出:nav 隐藏;DOM 顺序 viewport → next-nav →「+」,布局上「+」紧跟 tab 区
|
|
859
|
+
expect(navButtons).toHaveLength(2)
|
|
860
|
+
expect(navButtons[0]!.style.display).toBe('none')
|
|
861
|
+
expect(navButtons[1]!.style.display).toBe('none')
|
|
862
|
+
expect(viewport.nextElementSibling).toBe(navButtons[1]!)
|
|
863
|
+
expect(navButtons[1]!.nextElementSibling).toBe(addButton)
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
it('多 sheet 溢出:viewport 可横滚,nav 出现;next/prev 改变 scrollLeft;活动 tab 滚入视野', async () => {
|
|
867
|
+
const workbook = new Workbook()
|
|
868
|
+
for (let i = 2; i <= 16; i++) workbook.addSheet(`Sheet${i}`)
|
|
869
|
+
const { el } = mount(() => ({ workbook, rows: 10, cols: 6 }))
|
|
870
|
+
await nextTick()
|
|
871
|
+
|
|
872
|
+
const viewport = el.querySelector<HTMLElement>('.u-sheet__tabs-viewport')!
|
|
873
|
+
const list = el.querySelector<HTMLElement>('.u-sheet__tabs-list')!
|
|
874
|
+
expect(viewport).not.toBeNull()
|
|
875
|
+
expect(list).not.toBeNull()
|
|
876
|
+
expect(list.children.length).toBe(16)
|
|
877
|
+
// 「+」钉在视口外(tabs 直接子级,不在 list 内)
|
|
878
|
+
expect(el.querySelector('.u-sheet__tabs > .u-sheet__tab-add')).not.toBeNull()
|
|
879
|
+
expect(list.querySelector('.u-sheet__tab-add')).toBeNull()
|
|
880
|
+
|
|
881
|
+
// happy-dom 无真实布局:伪造溢出几何,触发导航显隐
|
|
882
|
+
let scrollLeft = 0
|
|
883
|
+
Object.defineProperty(viewport, 'clientWidth', { configurable: true, get: () => 100 })
|
|
884
|
+
Object.defineProperty(viewport, 'scrollWidth', { configurable: true, get: () => 800 })
|
|
885
|
+
Object.defineProperty(viewport, 'scrollLeft', {
|
|
886
|
+
configurable: true,
|
|
887
|
+
get: () => scrollLeft,
|
|
888
|
+
set: (value: number) => {
|
|
889
|
+
scrollLeft = value
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
viewport.scrollTo = ((options?: ScrollToOptions | number) => {
|
|
893
|
+
if (typeof options === 'number') {
|
|
894
|
+
scrollLeft = options
|
|
895
|
+
} else if (options && typeof options.left === 'number') {
|
|
896
|
+
scrollLeft = options.left
|
|
897
|
+
}
|
|
898
|
+
viewport.dispatchEvent(new Event('scroll'))
|
|
899
|
+
}) as typeof viewport.scrollTo
|
|
900
|
+
viewport.getBoundingClientRect = () =>
|
|
901
|
+
({
|
|
902
|
+
left: 0,
|
|
903
|
+
right: 100,
|
|
904
|
+
top: 0,
|
|
905
|
+
bottom: 24,
|
|
906
|
+
width: 100,
|
|
907
|
+
height: 24,
|
|
908
|
+
x: 0,
|
|
909
|
+
y: 0,
|
|
910
|
+
toJSON: () => ({})
|
|
911
|
+
}) as DOMRect
|
|
912
|
+
|
|
913
|
+
viewport.dispatchEvent(new Event('scroll'))
|
|
914
|
+
await nextTick()
|
|
915
|
+
|
|
916
|
+
expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth)
|
|
917
|
+
const navButtons = [...el.querySelectorAll<HTMLButtonElement>('.u-sheet__tabs-nav')]
|
|
918
|
+
expect(navButtons).toHaveLength(2)
|
|
919
|
+
// v-show:溢出时显示;初始 scrollLeft=0 → prev 禁用、next 可用
|
|
920
|
+
expect(navButtons[0]!.style.display).not.toBe('none')
|
|
921
|
+
expect(navButtons[1]!.style.display).not.toBe('none')
|
|
922
|
+
expect(navButtons[0]!.disabled).toBe(true)
|
|
923
|
+
expect(navButtons[1]!.disabled).toBe(false)
|
|
924
|
+
|
|
925
|
+
navButtons[1]!.click()
|
|
926
|
+
await nextTick()
|
|
927
|
+
expect(scrollLeft).toBeGreaterThan(0)
|
|
928
|
+
expect(navButtons[0]!.disabled).toBe(false)
|
|
929
|
+
|
|
930
|
+
const beforePrev = scrollLeft
|
|
931
|
+
navButtons[0]!.click()
|
|
932
|
+
await nextTick()
|
|
933
|
+
expect(scrollLeft).toBeLessThan(beforePrev)
|
|
934
|
+
|
|
935
|
+
// 切换到末尾 sheet:活动 tab 在视口右侧外 → ensureActiveVisible 滚入
|
|
936
|
+
const lastTab = tabs(el)[15]!
|
|
937
|
+
lastTab.getBoundingClientRect = () =>
|
|
938
|
+
({
|
|
939
|
+
left: 700,
|
|
940
|
+
right: 780,
|
|
941
|
+
top: 0,
|
|
942
|
+
bottom: 24,
|
|
943
|
+
width: 80,
|
|
944
|
+
height: 24,
|
|
945
|
+
x: 700,
|
|
946
|
+
y: 0,
|
|
947
|
+
toJSON: () => ({})
|
|
948
|
+
}) as DOMRect
|
|
949
|
+
scrollLeft = 0
|
|
950
|
+
lastTab.click()
|
|
951
|
+
await nextTick()
|
|
952
|
+
await nextTick()
|
|
953
|
+
expect(workbook.activeSheet.name).toBe('Sheet16')
|
|
954
|
+
expect(scrollLeft).toBeGreaterThan(0)
|
|
955
|
+
})
|
|
267
956
|
})
|