niuma-ui 1.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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- package/vite-plugins/silence-antlr-parse-console.ts +29 -0
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { h } from 'vue'
|
|
4
|
+
import RsTable from '../components/RsTable.vue'
|
|
5
|
+
|
|
6
|
+
vi.mock('../utils/rs-clipboard', () => ({
|
|
7
|
+
copyTextToClipboard: vi.fn().mockResolvedValue(true),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
import { copyTextToClipboard } from '../utils/rs-clipboard'
|
|
11
|
+
|
|
12
|
+
describe('RsTable', () => {
|
|
13
|
+
const columns = [
|
|
14
|
+
{ key: 'name', title: '名称', sortable: true, width: 120 },
|
|
15
|
+
{ key: 'count', title: '数量', align: 'right' as const, sortable: true, width: 80 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const data = [
|
|
19
|
+
{ id: '1', name: 'B', count: 20, status: 'running' },
|
|
20
|
+
{ id: '2', name: 'A', count: 10, status: 'stopped' },
|
|
21
|
+
{ id: '3', name: 'C', count: 5, status: 'running' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
it('renders column headers and cell values', () => {
|
|
25
|
+
const wrapper = mount(RsTable, {
|
|
26
|
+
props: { columns, data },
|
|
27
|
+
})
|
|
28
|
+
expect(wrapper.text()).toContain('名称')
|
|
29
|
+
expect(wrapper.text()).toContain('数量')
|
|
30
|
+
expect(wrapper.text()).toContain('B')
|
|
31
|
+
expect(wrapper.text()).toContain('20')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('sorts rows ascending then descending on sort icon click', async () => {
|
|
35
|
+
const wrapper = mount(RsTable, {
|
|
36
|
+
props: { columns, data },
|
|
37
|
+
})
|
|
38
|
+
const nameSort = wrapper.findAll('.rs-table__sort')[0]!
|
|
39
|
+
await nameSort.trigger('click')
|
|
40
|
+
expect(wrapper.findAll('.rs-table__row td')[0].text()).toBe('A')
|
|
41
|
+
await nameSort.trigger('click')
|
|
42
|
+
expect(wrapper.findAll('.rs-table__row td')[0].text()).toBe('C')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clears sort on third sort icon click', async () => {
|
|
46
|
+
const wrapper = mount(RsTable, {
|
|
47
|
+
props: { columns, data },
|
|
48
|
+
})
|
|
49
|
+
const nameSort = wrapper.findAll('.rs-table__sort')[0]!
|
|
50
|
+
await nameSort.trigger('click')
|
|
51
|
+
await nameSort.trigger('click')
|
|
52
|
+
await nameSort.trigger('click')
|
|
53
|
+
expect(wrapper.find('.rs-table__sort--active').exists()).toBe(false)
|
|
54
|
+
expect(wrapper.findAll('.rs-table__row td')[0].text()).toBe('B')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('supports controlled sort via v-model:sort', async () => {
|
|
58
|
+
const wrapper = mount(RsTable, {
|
|
59
|
+
props: {
|
|
60
|
+
columns,
|
|
61
|
+
data,
|
|
62
|
+
sort: { key: 'count', order: 'asc' as const },
|
|
63
|
+
'onUpdate:sort': (value: { key: string; order: 'asc' | 'desc' } | null) => wrapper.setProps({ sort: value }),
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
expect(wrapper.findAll('.rs-table__row td')[1].text()).toBe('5')
|
|
67
|
+
await wrapper.findAll('.rs-table__sort')[1]!.trigger('click')
|
|
68
|
+
expect(wrapper.emitted('update:sort')?.[0]?.[0]).toEqual({ key: 'count', order: 'desc' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('filters rows with filterText', () => {
|
|
72
|
+
const wrapper = mount(RsTable, {
|
|
73
|
+
props: { columns, data, filterText: 'a' },
|
|
74
|
+
})
|
|
75
|
+
expect(wrapper.findAll('.rs-table__row')).toHaveLength(1)
|
|
76
|
+
expect(wrapper.text()).toContain('A')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('renders group headers when groupBy is set', () => {
|
|
80
|
+
const wrapper = mount(RsTable, {
|
|
81
|
+
props: {
|
|
82
|
+
columns: [...columns, { key: 'status', title: '状态' }],
|
|
83
|
+
data,
|
|
84
|
+
groupBy: 'status',
|
|
85
|
+
groupLabel: (key: string) => `分组 ${key}`,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
expect(wrapper.findAll('.rs-table__group-row')).toHaveLength(2)
|
|
89
|
+
expect(wrapper.text()).toContain('分组 running')
|
|
90
|
+
expect(wrapper.text()).toContain('分组 stopped')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('virtual mode renders a subset of rows', () => {
|
|
94
|
+
const largeData = Array.from({ length: 200 }, (_, index) => ({
|
|
95
|
+
id: String(index),
|
|
96
|
+
name: `Row ${index}`,
|
|
97
|
+
count: index,
|
|
98
|
+
status: 'running',
|
|
99
|
+
}))
|
|
100
|
+
const wrapper = mount(RsTable, {
|
|
101
|
+
props: {
|
|
102
|
+
columns,
|
|
103
|
+
data: largeData,
|
|
104
|
+
virtual: true,
|
|
105
|
+
height: 200,
|
|
106
|
+
rowHeight: 40,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
expect(wrapper.findAll('.rs-table__row').length).toBeLessThan(largeData.length)
|
|
110
|
+
expect(wrapper.find('.rs-table__virtual-pad').exists()).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('emits loadMore when scrolled near bottom in infinite mode', async () => {
|
|
114
|
+
const wrapper = mount(RsTable, {
|
|
115
|
+
props: {
|
|
116
|
+
columns,
|
|
117
|
+
data,
|
|
118
|
+
infinite: true,
|
|
119
|
+
hasMore: true,
|
|
120
|
+
height: 120,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
const container = wrapper.find('.rs-table').element as HTMLElement
|
|
124
|
+
Object.defineProperty(container, 'scrollHeight', { value: 400, configurable: true })
|
|
125
|
+
Object.defineProperty(container, 'clientHeight', { value: 120, configurable: true })
|
|
126
|
+
container.scrollTop = 300
|
|
127
|
+
await wrapper.find('.rs-table').trigger('scroll')
|
|
128
|
+
expect(wrapper.emitted('loadMore')).toHaveLength(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('auto enables virtual scroll in infinite mode for large datasets', () => {
|
|
132
|
+
const largeData = Array.from({ length: 500 }, (_, index) => ({
|
|
133
|
+
id: String(index),
|
|
134
|
+
name: `Row ${index}`,
|
|
135
|
+
count: index,
|
|
136
|
+
status: 'running',
|
|
137
|
+
}))
|
|
138
|
+
const wrapper = mount(RsTable, {
|
|
139
|
+
props: {
|
|
140
|
+
columns,
|
|
141
|
+
data: largeData,
|
|
142
|
+
infinite: true,
|
|
143
|
+
hasMore: false,
|
|
144
|
+
height: 200,
|
|
145
|
+
rowHeight: 40,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
expect(wrapper.find('.rs-table').classes()).toContain('rs-table--virtual')
|
|
149
|
+
expect(wrapper.findAll('.rs-table__row').length).toBeLessThan(30)
|
|
150
|
+
expect(wrapper.find('.rs-table__virtual-pad').exists()).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('shows loading placeholder', () => {
|
|
154
|
+
const wrapper = mount(RsTable, {
|
|
155
|
+
props: { columns, data, loading: true },
|
|
156
|
+
})
|
|
157
|
+
expect(wrapper.find('.rs-table__empty').text()).toBe('加载中…')
|
|
158
|
+
expect(wrapper.findAll('.rs-table__row')).toHaveLength(0)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('shows default empty state', () => {
|
|
162
|
+
const wrapper = mount(RsTable, {
|
|
163
|
+
props: { columns, data: [] },
|
|
164
|
+
})
|
|
165
|
+
expect(wrapper.find('.rs-table__empty').text()).toBe('暂无数据')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('renders custom empty slot', () => {
|
|
169
|
+
const wrapper = mount(RsTable, {
|
|
170
|
+
props: { columns, data: [] },
|
|
171
|
+
slots: { empty: '暂无数据' },
|
|
172
|
+
})
|
|
173
|
+
expect(wrapper.find('.rs-table__empty').text()).toBe('暂无数据')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('applies compact and bordered modifiers', () => {
|
|
177
|
+
const wrapper = mount(RsTable, {
|
|
178
|
+
props: { columns, data, compact: true, bordered: false },
|
|
179
|
+
})
|
|
180
|
+
const root = wrapper.find('.rs-table')
|
|
181
|
+
expect(root.classes()).toContain('rs-table--compact')
|
|
182
|
+
expect(root.classes()).not.toContain('rs-table--rounded')
|
|
183
|
+
expect(root.classes()).not.toContain('rs-table--bordered')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('can enable outer rounded corners', () => {
|
|
187
|
+
const wrapper = mount(RsTable, {
|
|
188
|
+
props: { columns, data, rounded: true },
|
|
189
|
+
})
|
|
190
|
+
expect(wrapper.find('.rs-table').classes()).toContain('rs-table--rounded')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('applies configurable index column width', () => {
|
|
194
|
+
const wrapper = mount(RsTable, {
|
|
195
|
+
props: { columns, data, showIndex: true, indexWidth: 40 },
|
|
196
|
+
})
|
|
197
|
+
expect(wrapper.find('.rs-table').attributes('style')).toContain('--rs-table-index-width: 40px')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('applies configurable edit gutter width', () => {
|
|
201
|
+
const wrapper = mount(RsTable, {
|
|
202
|
+
props: {
|
|
203
|
+
columns,
|
|
204
|
+
data,
|
|
205
|
+
editable: true,
|
|
206
|
+
editGutter: true,
|
|
207
|
+
editGutterWidth: 40,
|
|
208
|
+
rowKey: 'id',
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
expect(wrapper.find('.rs-table').attributes('style')).toContain('--rs-table-gutter-width: 40px')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('uses column render function', () => {
|
|
215
|
+
const wrapper = mount(RsTable, {
|
|
216
|
+
props: {
|
|
217
|
+
columns: [{ key: 'name', title: '名称', render: (row) => `·${(row as { name: string }).name}·` }],
|
|
218
|
+
data: [{ id: '1', name: '任务' }],
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
expect(wrapper.find('td').text()).toBe('·任务·')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('renders column render VNode without stringifying', () => {
|
|
225
|
+
const wrapper = mount(RsTable, {
|
|
226
|
+
props: {
|
|
227
|
+
columns: [
|
|
228
|
+
{
|
|
229
|
+
key: 'time',
|
|
230
|
+
title: 'Time',
|
|
231
|
+
render: (row) =>
|
|
232
|
+
h('span', { class: 'time-cell', title: 'long' }, String((row as { time: number }).time)),
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
data: [{ id: '1', time: 42 }],
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
const cell = wrapper.find('.time-cell')
|
|
239
|
+
expect(cell.exists()).toBe(true)
|
|
240
|
+
expect(cell.text()).toBe('42')
|
|
241
|
+
expect(cell.attributes('title')).toBe('long')
|
|
242
|
+
expect(wrapper.find('td').text()).not.toContain('[object Object]')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('renders cell slot by column key', () => {
|
|
246
|
+
const wrapper = mount(RsTable, {
|
|
247
|
+
props: {
|
|
248
|
+
columns: [{ key: 'name', title: '名称' }],
|
|
249
|
+
data: [{ id: '1', name: '任务' }],
|
|
250
|
+
},
|
|
251
|
+
slots: { name: '<span class="custom-cell">自定义</span>' },
|
|
252
|
+
})
|
|
253
|
+
expect(wrapper.find('.custom-cell').text()).toBe('自定义')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('emits rowClick with row and index', async () => {
|
|
257
|
+
const wrapper = mount(RsTable, {
|
|
258
|
+
props: { columns, data },
|
|
259
|
+
})
|
|
260
|
+
await wrapper.find('.rs-table__row').trigger('click')
|
|
261
|
+
expect(wrapper.emitted('rowClick')?.[0]).toEqual([data[0], 0])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('updates column width when resize handle is dragged', async () => {
|
|
265
|
+
const offsetWidth = vi
|
|
266
|
+
.spyOn(HTMLElement.prototype, 'offsetWidth', 'get')
|
|
267
|
+
.mockImplementation(function (this: HTMLElement) {
|
|
268
|
+
const key = this.getAttribute('data-col-key')
|
|
269
|
+
if (key === 'name') return 120
|
|
270
|
+
if (key === 'count') return 80
|
|
271
|
+
return 0
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const wrapper = mount(RsTable, {
|
|
276
|
+
props: { columns, data, resizable: true },
|
|
277
|
+
})
|
|
278
|
+
const handle = wrapper.find('.rs-table__resize-handle')
|
|
279
|
+
await handle.trigger('mousedown', { clientX: 100 })
|
|
280
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 140, bubbles: true }))
|
|
281
|
+
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 140, bubbles: true }))
|
|
282
|
+
expect(wrapper.emitted('columnResize')?.[0]).toEqual(['name', 160])
|
|
283
|
+
} finally {
|
|
284
|
+
offsetWidth.mockRestore()
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('supports row selection with select all', async () => {
|
|
289
|
+
const wrapper = mount(RsTable, {
|
|
290
|
+
props: {
|
|
291
|
+
columns,
|
|
292
|
+
data,
|
|
293
|
+
selectable: true,
|
|
294
|
+
rowKey: 'id',
|
|
295
|
+
selectedRowKeys: [] as string[],
|
|
296
|
+
'onUpdate:selectedRowKeys': (keys: string[]) => wrapper.setProps({ selectedRowKeys: keys }),
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
const rowChecks = wrapper.findAll('.rs-table__row .rs-table__checkbox-input')
|
|
300
|
+
await rowChecks[0]!.setValue(true)
|
|
301
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1'])
|
|
302
|
+
await wrapper.find('.rs-table__head .rs-table__checkbox-input').setValue(true)
|
|
303
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1', '2', '3'])
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('renders index column and striped rows', () => {
|
|
307
|
+
const wrapper = mount(RsTable, {
|
|
308
|
+
props: { columns, data, showIndex: true, striped: true, selectable: true, rowKey: 'id' },
|
|
309
|
+
})
|
|
310
|
+
expect(wrapper.find('.rs-table__td--index').text()).toBe('1')
|
|
311
|
+
expect(wrapper.find('.rs-table__row--striped').exists()).toBe(true)
|
|
312
|
+
const stripedRow = wrapper.find('.rs-table__row--striped')
|
|
313
|
+
const selectionCell = stripedRow.find('.rs-table__td--selection')
|
|
314
|
+
expect(selectionCell.exists()).toBe(true)
|
|
315
|
+
expect(getComputedStyle(selectionCell.element).backgroundColor).toBe(
|
|
316
|
+
getComputedStyle(stripedRow.element).backgroundColor,
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('shows sort icons on sortable columns and highlights active sort', async () => {
|
|
321
|
+
const wrapper = mount(RsTable, {
|
|
322
|
+
props: { columns, data },
|
|
323
|
+
})
|
|
324
|
+
const sortIcons = wrapper.findAll('.rs-table__sort')
|
|
325
|
+
expect(sortIcons.length).toBe(2)
|
|
326
|
+
expect(wrapper.find('.rs-table__sort--active').exists()).toBe(false)
|
|
327
|
+
await sortIcons[0]!.trigger('click')
|
|
328
|
+
expect(wrapper.find('.rs-table__sort--active').exists()).toBe(true)
|
|
329
|
+
expect(wrapper.find('.rs-table__sort--active .rs-icon').exists()).toBe(true)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('hides header when showHeader is false', () => {
|
|
333
|
+
const wrapper = mount(RsTable, {
|
|
334
|
+
props: { columns, data, showHeader: false },
|
|
335
|
+
})
|
|
336
|
+
expect(wrapper.find('thead').exists()).toBe(false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('applies ellipsis wrapper and overflow styles', () => {
|
|
340
|
+
const wrapper = mount(RsTable, {
|
|
341
|
+
props: {
|
|
342
|
+
columns: [{ key: 'name', title: '名称', ellipsis: true, width: 80 }],
|
|
343
|
+
data: [{ id: '1', name: '这是一段非常非常长的文本用于测试省略号效果' }],
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
const cell = wrapper.find('.rs-table__td--ellipsis')
|
|
347
|
+
const text = wrapper.find('.rs-table__cell-tip')
|
|
348
|
+
expect(cell.exists()).toBe(true)
|
|
349
|
+
expect(text.exists()).toBe(true)
|
|
350
|
+
expect(text.text()).toContain('非常非常长')
|
|
351
|
+
expect(text.attributes('data-rs-table-tip-mode')).toBe('overflow')
|
|
352
|
+
expect(cell.attributes('style')).toContain('width: 80px')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('shows shared cell tooltip on ellipsis overflow hover', async () => {
|
|
356
|
+
vi.useFakeTimers()
|
|
357
|
+
const wrapper = mount(RsTable, {
|
|
358
|
+
attachTo: document.body,
|
|
359
|
+
props: {
|
|
360
|
+
columns: [{ key: 'name', title: '名称', ellipsis: true, width: 80 }],
|
|
361
|
+
data: [{ id: '1', name: '这是一段非常非常长的文本用于测试省略号效果' }],
|
|
362
|
+
cellTooltipDelay: 0,
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
const cell = wrapper.find('.rs-table__ellipsis-text').element as HTMLElement
|
|
366
|
+
Object.defineProperty(cell, 'scrollWidth', { value: 500, configurable: true })
|
|
367
|
+
Object.defineProperty(cell, 'clientWidth', { value: 80, configurable: true })
|
|
368
|
+
await wrapper.find('.rs-table__cell-tip').trigger('pointerover', { pointerType: 'mouse' })
|
|
369
|
+
await vi.runAllTimersAsync()
|
|
370
|
+
await wrapper.vm.$nextTick()
|
|
371
|
+
const tip = document.body.querySelector('.rs-table__shared-tip')
|
|
372
|
+
expect(tip).not.toBeNull()
|
|
373
|
+
expect(tip?.textContent).toContain('非常非常长')
|
|
374
|
+
wrapper.unmount()
|
|
375
|
+
expect(document.body.querySelector('.rs-table__shared-tip')).toBeNull()
|
|
376
|
+
vi.useRealTimers()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('shows shared tooltip for column.tooltip', async () => {
|
|
380
|
+
vi.useFakeTimers()
|
|
381
|
+
const wrapper = mount(RsTable, {
|
|
382
|
+
attachTo: document.body,
|
|
383
|
+
props: {
|
|
384
|
+
columns: [{ key: 'name', title: '名称', tooltip: () => '自定义说明' }],
|
|
385
|
+
data: [{ id: '1', name: '短文本' }],
|
|
386
|
+
cellTooltipDelay: 0,
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
await wrapper.find('.rs-table__cell-tip').trigger('pointerover', { pointerType: 'mouse' })
|
|
390
|
+
await vi.runAllTimersAsync()
|
|
391
|
+
await wrapper.vm.$nextTick()
|
|
392
|
+
const tip = document.body.querySelector('.rs-table__shared-tip')
|
|
393
|
+
expect(tip?.textContent).toContain('自定义说明')
|
|
394
|
+
wrapper.unmount()
|
|
395
|
+
vi.useRealTimers()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('shows shared header tooltip for column.headerTip', async () => {
|
|
399
|
+
vi.useFakeTimers()
|
|
400
|
+
const wrapper = mount(RsTable, {
|
|
401
|
+
attachTo: document.body,
|
|
402
|
+
props: {
|
|
403
|
+
columns: [
|
|
404
|
+
{
|
|
405
|
+
key: 'name',
|
|
406
|
+
title: '名称',
|
|
407
|
+
headerTip: '字段: name\n类型: varchar(64)',
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
data: [{ id: '1', name: 'Alice' }],
|
|
411
|
+
cellTooltipDelay: 0,
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
await wrapper.find('.rs-table__th-label--tip').trigger('pointerover', { pointerType: 'mouse' })
|
|
415
|
+
await vi.runAllTimersAsync()
|
|
416
|
+
await wrapper.vm.$nextTick()
|
|
417
|
+
const tip = document.body.querySelector('.rs-table__shared-tip--header')
|
|
418
|
+
expect(tip).not.toBeNull()
|
|
419
|
+
expect(tip?.textContent).toContain('varchar(64)')
|
|
420
|
+
wrapper.unmount()
|
|
421
|
+
vi.useRealTimers()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('highlights row on click when highlightRow is enabled', async () => {
|
|
425
|
+
const wrapper = mount(RsTable, {
|
|
426
|
+
props: {
|
|
427
|
+
columns,
|
|
428
|
+
data,
|
|
429
|
+
rowKey: 'id',
|
|
430
|
+
highlightRow: true,
|
|
431
|
+
},
|
|
432
|
+
})
|
|
433
|
+
await wrapper.findAll('.rs-table__row')[1]?.trigger('click')
|
|
434
|
+
expect(wrapper.emitted('highlightChange')?.[0]?.[0]).toBe('2')
|
|
435
|
+
expect(wrapper.find('.rs-table__row--highlighted').exists()).toBe(true)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('highlights row when clicking a data cell', async () => {
|
|
439
|
+
const wrapper = mount(RsTable, {
|
|
440
|
+
props: {
|
|
441
|
+
columns,
|
|
442
|
+
data,
|
|
443
|
+
rowKey: 'id',
|
|
444
|
+
highlightRow: true,
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
const row = wrapper.findAll('.rs-table__row')[1]
|
|
448
|
+
await row.find('.rs-table__td--data').trigger('click')
|
|
449
|
+
expect(wrapper.emitted('highlightChange')?.[0]?.[0]).toBe('2')
|
|
450
|
+
expect(wrapper.find('.rs-table__row--highlighted').exists()).toBe(true)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('emits rowDblclick when double-clicking a non-editable data cell', async () => {
|
|
454
|
+
const wrapper = mount(RsTable, {
|
|
455
|
+
props: {
|
|
456
|
+
columns,
|
|
457
|
+
data,
|
|
458
|
+
rowKey: 'id',
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
const row = wrapper.findAll('.rs-table__row')[1]
|
|
462
|
+
await row.find('.rs-table__td--data').trigger('dblclick')
|
|
463
|
+
expect(wrapper.emitted('rowDblclick')?.[0]?.[0]).toEqual(data[1])
|
|
464
|
+
expect(wrapper.emitted('rowDblclick')?.[0]?.[1]).toBe(1)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('shows highlight background on striped rows', async () => {
|
|
468
|
+
const wrapper = mount(RsTable, {
|
|
469
|
+
props: {
|
|
470
|
+
columns,
|
|
471
|
+
data,
|
|
472
|
+
rowKey: 'id',
|
|
473
|
+
striped: true,
|
|
474
|
+
highlightRow: true,
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
const stripedRow = wrapper.find('.rs-table__row--striped')
|
|
478
|
+
expect(stripedRow.exists()).toBe(true)
|
|
479
|
+
await stripedRow.find('.rs-table__td--data').trigger('click')
|
|
480
|
+
expect(stripedRow.classes()).toContain('rs-table__row--highlighted')
|
|
481
|
+
expect(wrapper.emitted('highlightChange')?.[0]?.[0]).toBe('2')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('highlights row when clicking edit gutter line number', async () => {
|
|
485
|
+
const rows = [{ id: '1', name: 'Alpha', count: 1, status: 'running' as const }]
|
|
486
|
+
const wrapper = mount(RsTable, {
|
|
487
|
+
props: {
|
|
488
|
+
columns: [{ key: 'name', title: '名称' }],
|
|
489
|
+
data: rows,
|
|
490
|
+
rowKey: 'id',
|
|
491
|
+
editable: true,
|
|
492
|
+
highlightRow: true,
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
await wrapper.find('.rs-table__td--gutter').trigger('click')
|
|
496
|
+
expect(wrapper.emitted('highlightChange')?.[0]?.[0]).toBe('1')
|
|
497
|
+
expect(wrapper.find('.rs-table__row--highlighted').exists()).toBe(true)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('supports inline cell editing commit', async () => {
|
|
501
|
+
const rows = [{ id: '1', name: 'Alpha', count: 1, status: 'running' as const }]
|
|
502
|
+
const wrapper = mount(RsTable, {
|
|
503
|
+
props: {
|
|
504
|
+
columns: [{ key: 'name', title: '名称', editable: true }],
|
|
505
|
+
data: rows,
|
|
506
|
+
rowKey: 'id',
|
|
507
|
+
editable: true,
|
|
508
|
+
},
|
|
509
|
+
})
|
|
510
|
+
expect(wrapper.find('.rs-table__td--gutter').exists()).toBe(true)
|
|
511
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
512
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
513
|
+
expect(input.exists()).toBe(true)
|
|
514
|
+
await input.setValue('Beta')
|
|
515
|
+
await input.trigger('keydown', { key: 'Enter' })
|
|
516
|
+
expect(wrapper.emitted('cellEditCommit')?.[0]?.[3]).toBe('Beta')
|
|
517
|
+
wrapper.unmount()
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('toggles boolean cells without mounting an editor', async () => {
|
|
521
|
+
const rows = [{ id: '1', name: 'Alpha', active: false }]
|
|
522
|
+
const wrapper = mount(RsTable, {
|
|
523
|
+
props: {
|
|
524
|
+
columns: [
|
|
525
|
+
{ key: 'name', title: '名称' },
|
|
526
|
+
{ key: 'active', title: '启用', editable: true, valueType: 'boolean' },
|
|
527
|
+
],
|
|
528
|
+
data: rows,
|
|
529
|
+
rowKey: 'id',
|
|
530
|
+
editable: true,
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
const check = wrapper.find('.rs-table__cell-check')
|
|
534
|
+
expect(check.exists()).toBe(true)
|
|
535
|
+
expect(wrapper.find('.rs-table-cell-editor').exists()).toBe(false)
|
|
536
|
+
await check.trigger('click')
|
|
537
|
+
expect(wrapper.emitted('cellEditCommit')?.[0]?.[3]).toBe(true)
|
|
538
|
+
expect(wrapper.find('.rs-table-cell-editor').exists()).toBe(false)
|
|
539
|
+
wrapper.unmount()
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('refreshes boolean cell after parent commits new row data (v-memo)', async () => {
|
|
543
|
+
const rows = [{ id: '1', name: 'Alpha', active: false }]
|
|
544
|
+
const wrapper = mount(RsTable, {
|
|
545
|
+
props: {
|
|
546
|
+
columns: [
|
|
547
|
+
{ key: 'name', title: '名称' },
|
|
548
|
+
{ key: 'active', title: '启用', editable: true, valueType: 'boolean' },
|
|
549
|
+
],
|
|
550
|
+
data: rows,
|
|
551
|
+
rowKey: 'id',
|
|
552
|
+
editable: true,
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
await wrapper.find('.rs-table__cell-check').trigger('click')
|
|
556
|
+
const next = (wrapper.emitted('cellEditCommit')?.[0]?.[3] as boolean) ?? true
|
|
557
|
+
await wrapper.setProps({ data: [{ id: '1', name: 'Alpha', active: next }] })
|
|
558
|
+
await wrapper.vm.$nextTick()
|
|
559
|
+
const input = wrapper.find('.rs-table__cell-check input')
|
|
560
|
+
expect((input.element as HTMLInputElement).checked).toBe(next)
|
|
561
|
+
wrapper.unmount()
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('blocks commit and shows invalid state when validator fails', async () => {
|
|
565
|
+
const rows = [{ id: '1', name: 'Alpha', count: 1 }]
|
|
566
|
+
const wrapper = mount(RsTable, {
|
|
567
|
+
props: {
|
|
568
|
+
columns: [
|
|
569
|
+
{
|
|
570
|
+
key: 'count',
|
|
571
|
+
title: '数量',
|
|
572
|
+
editable: true,
|
|
573
|
+
validator: (value) => (Number(value) >= 0 ? null : '不能为负'),
|
|
574
|
+
parser: (input) => Number(input),
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
data: rows,
|
|
578
|
+
rowKey: 'id',
|
|
579
|
+
editable: true,
|
|
580
|
+
},
|
|
581
|
+
})
|
|
582
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
583
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
584
|
+
await input.setValue('-3')
|
|
585
|
+
await wrapper.vm.$nextTick()
|
|
586
|
+
await input.trigger('blur')
|
|
587
|
+
await wrapper.vm.$nextTick()
|
|
588
|
+
expect(wrapper.emitted('cellEditCommit')).toBeUndefined()
|
|
589
|
+
expect(wrapper.emitted('cellEditInvalid')?.[0]?.[3]).toBe('不能为负')
|
|
590
|
+
expect(wrapper.find('.rs-table__td--invalid').exists()).toBe(true)
|
|
591
|
+
wrapper.unmount()
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('displays NULL placeholder for null cell values', () => {
|
|
595
|
+
const wrapper = mount(RsTable, {
|
|
596
|
+
props: {
|
|
597
|
+
columns: [{ key: 'note', title: '备注', editable: true, nullable: true }],
|
|
598
|
+
data: [{ id: '1', note: null }],
|
|
599
|
+
rowKey: 'id',
|
|
600
|
+
editable: true,
|
|
601
|
+
allowNull: true,
|
|
602
|
+
},
|
|
603
|
+
})
|
|
604
|
+
expect(wrapper.find('.rs-table__cell-body--null').text()).toContain('NULL')
|
|
605
|
+
wrapper.unmount()
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('batches commit across selected rows for the same column', async () => {
|
|
609
|
+
const rows = [
|
|
610
|
+
{ id: '1', name: 'A' },
|
|
611
|
+
{ id: '2', name: 'B' },
|
|
612
|
+
{ id: '3', name: 'C' },
|
|
613
|
+
]
|
|
614
|
+
const wrapper = mount(RsTable, {
|
|
615
|
+
props: {
|
|
616
|
+
columns: [{ key: 'name', title: '名称', editable: true }],
|
|
617
|
+
data: rows,
|
|
618
|
+
rowKey: 'id',
|
|
619
|
+
editable: true,
|
|
620
|
+
selectable: true,
|
|
621
|
+
editBatch: true,
|
|
622
|
+
selectedRowKeys: ['1', '2'],
|
|
623
|
+
},
|
|
624
|
+
})
|
|
625
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
626
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
627
|
+
await input.setValue('Z')
|
|
628
|
+
await wrapper.vm.$nextTick()
|
|
629
|
+
await input.trigger('blur')
|
|
630
|
+
await wrapper.vm.$nextTick()
|
|
631
|
+
const commits = wrapper.emitted('cellEditCommit') ?? []
|
|
632
|
+
expect(commits.length).toBe(2)
|
|
633
|
+
expect(wrapper.emitted('cellEditBatchCommit')?.[0]?.[1]).toHaveLength(2)
|
|
634
|
+
wrapper.unmount()
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('supports async validator blocking commit', async () => {
|
|
638
|
+
const rows = [{ id: '1', name: 'Alpha' }]
|
|
639
|
+
const wrapper = mount(RsTable, {
|
|
640
|
+
props: {
|
|
641
|
+
columns: [
|
|
642
|
+
{
|
|
643
|
+
key: 'name',
|
|
644
|
+
title: '名称',
|
|
645
|
+
editable: true,
|
|
646
|
+
validator: async (value) => {
|
|
647
|
+
await Promise.resolve()
|
|
648
|
+
return String(value).length < 2 ? '太短' : null
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
data: rows,
|
|
653
|
+
rowKey: 'id',
|
|
654
|
+
editable: true,
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
658
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
659
|
+
await input.setValue('x')
|
|
660
|
+
await wrapper.vm.$nextTick()
|
|
661
|
+
await input.trigger('blur')
|
|
662
|
+
await wrapper.vm.$nextTick()
|
|
663
|
+
await Promise.resolve()
|
|
664
|
+
await wrapper.vm.$nextTick()
|
|
665
|
+
expect(wrapper.emitted('cellEditCommit')).toBeUndefined()
|
|
666
|
+
expect(wrapper.emitted('cellEditInvalid')?.[0]?.[3]).toBe('太短')
|
|
667
|
+
wrapper.unmount()
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('select editor is not clearable by default', async () => {
|
|
671
|
+
const rows = [{ id: '1', kind: 'a' }]
|
|
672
|
+
const wrapper = mount(RsTable, {
|
|
673
|
+
props: {
|
|
674
|
+
columns: [
|
|
675
|
+
{
|
|
676
|
+
key: 'kind',
|
|
677
|
+
title: '类型',
|
|
678
|
+
editable: true,
|
|
679
|
+
valueType: 'select',
|
|
680
|
+
editorOptions: {
|
|
681
|
+
options: [
|
|
682
|
+
{ value: 'a', label: 'A' },
|
|
683
|
+
{ value: 'b', label: 'B' },
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
data: rows,
|
|
689
|
+
rowKey: 'id',
|
|
690
|
+
editable: true,
|
|
691
|
+
allowNull: true,
|
|
692
|
+
},
|
|
693
|
+
})
|
|
694
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('click')
|
|
695
|
+
expect(wrapper.find('.rs-table-cell-editor').exists()).toBe(true)
|
|
696
|
+
expect(wrapper.find('.rs-select__clear').exists()).toBe(false)
|
|
697
|
+
wrapper.unmount()
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('opens select editor on click and commits on change', async () => {
|
|
701
|
+
const rows = [{ id: '1', name: 'Alpha', kind: 'a' }]
|
|
702
|
+
const wrapper = mount(RsTable, {
|
|
703
|
+
props: {
|
|
704
|
+
columns: [
|
|
705
|
+
{
|
|
706
|
+
key: 'kind',
|
|
707
|
+
title: '类型',
|
|
708
|
+
editable: true,
|
|
709
|
+
valueType: 'select',
|
|
710
|
+
editorOptions: {
|
|
711
|
+
options: [
|
|
712
|
+
{ value: 'a', label: 'A' },
|
|
713
|
+
{ value: 'b', label: 'B' },
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
data: rows,
|
|
719
|
+
rowKey: 'id',
|
|
720
|
+
editable: true,
|
|
721
|
+
},
|
|
722
|
+
})
|
|
723
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('click')
|
|
724
|
+
await wrapper.vm.$nextTick()
|
|
725
|
+
expect(wrapper.find('.rs-table-cell-editor--select').exists()).toBe(true)
|
|
726
|
+
const select = wrapper.findComponent({ name: 'RsSelect' })
|
|
727
|
+
expect(select.exists()).toBe(true)
|
|
728
|
+
select.vm.$emit('update:modelValue', 'b')
|
|
729
|
+
await wrapper.vm.$nextTick()
|
|
730
|
+
expect(wrapper.emitted('cellEditCommit')?.[0]?.[3]).toBe('b')
|
|
731
|
+
wrapper.unmount()
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it('multiple select commits joined values when dropdown closes', async () => {
|
|
735
|
+
Element.prototype.scrollIntoView = vi.fn()
|
|
736
|
+
const rows = [{ id: '1', cols: 'a' }]
|
|
737
|
+
const wrapper = mount(RsTable, {
|
|
738
|
+
props: {
|
|
739
|
+
columns: [
|
|
740
|
+
{
|
|
741
|
+
key: 'cols',
|
|
742
|
+
title: '列',
|
|
743
|
+
editable: true,
|
|
744
|
+
valueType: 'select',
|
|
745
|
+
editorOptions: {
|
|
746
|
+
multiple: true,
|
|
747
|
+
clearable: true,
|
|
748
|
+
options: [
|
|
749
|
+
{ value: 'a', label: 'A' },
|
|
750
|
+
{ value: 'b', label: 'B' },
|
|
751
|
+
{ value: 'c', label: 'C' },
|
|
752
|
+
],
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
data: rows,
|
|
757
|
+
rowKey: 'id',
|
|
758
|
+
editable: true,
|
|
759
|
+
},
|
|
760
|
+
})
|
|
761
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('click')
|
|
762
|
+
await wrapper.vm.$nextTick()
|
|
763
|
+
const select = wrapper.findComponent({ name: 'RsSelect' })
|
|
764
|
+
expect(select.exists()).toBe(true)
|
|
765
|
+
select.vm.$emit('update:modelValue', ['a', 'b'])
|
|
766
|
+
await wrapper.vm.$nextTick()
|
|
767
|
+
expect(wrapper.emitted('cellEditCommit')).toBeFalsy()
|
|
768
|
+
select.vm.$emit('update:open', true)
|
|
769
|
+
await wrapper.vm.$nextTick()
|
|
770
|
+
select.vm.$emit('update:open', false)
|
|
771
|
+
await wrapper.vm.$nextTick()
|
|
772
|
+
expect(wrapper.emitted('cellEditCommit')?.[0]?.[3]).toBe('a, b')
|
|
773
|
+
wrapper.unmount()
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('shows gutter commit while actively editing a changed cell', async () => {
|
|
777
|
+
const rows = [{ id: '1', name: 'Alpha', count: 1, status: 'running' as const }]
|
|
778
|
+
const wrapper = mount(RsTable, {
|
|
779
|
+
props: {
|
|
780
|
+
columns: [{ key: 'name', title: '名称', editable: true }],
|
|
781
|
+
data: rows,
|
|
782
|
+
rowKey: 'id',
|
|
783
|
+
editable: true,
|
|
784
|
+
},
|
|
785
|
+
})
|
|
786
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
787
|
+
expect(wrapper.find('.rs-table-edit-gutter__line').exists()).toBe(true)
|
|
788
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
789
|
+
await input.setValue('Beta')
|
|
790
|
+
await wrapper.vm.$nextTick()
|
|
791
|
+
expect(wrapper.find('.rs-table-edit-gutter__commit').exists()).toBe(true)
|
|
792
|
+
wrapper.unmount()
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
it('stages row edits and commits manually in rowCommit mode', async () => {
|
|
796
|
+
const rows = [{ id: '1', name: 'Alpha', count: 1, status: 'running' as const }]
|
|
797
|
+
const wrapper = mount(RsTable, {
|
|
798
|
+
props: {
|
|
799
|
+
columns: [{ key: 'name', title: '名称', editable: true }],
|
|
800
|
+
data: rows,
|
|
801
|
+
rowKey: 'id',
|
|
802
|
+
editable: true,
|
|
803
|
+
rowCommit: true,
|
|
804
|
+
},
|
|
805
|
+
})
|
|
806
|
+
await wrapper.find('.rs-table__cell-body--editable').trigger('dblclick')
|
|
807
|
+
expect(wrapper.find('.rs-table__th--gutter').exists()).toBe(true)
|
|
808
|
+
expect(wrapper.find('.rs-table__td--gutter').exists()).toBe(true)
|
|
809
|
+
const input = wrapper.find('.rs-table-cell-editor input')
|
|
810
|
+
await input.setValue('Beta')
|
|
811
|
+
await wrapper.vm.$nextTick()
|
|
812
|
+
await input.trigger('blur')
|
|
813
|
+
await wrapper.vm.$nextTick()
|
|
814
|
+
expect(wrapper.emitted('cellEditCommit')).toBeUndefined()
|
|
815
|
+
expect(wrapper.find('.rs-table-edit-gutter__commit').exists()).toBe(true)
|
|
816
|
+
expect(wrapper.find('.rs-table-edit-gutter__commit-hint').text()).toBe('提交')
|
|
817
|
+
await wrapper.find('.rs-table-edit-gutter__commit').trigger('click')
|
|
818
|
+
const payload = wrapper.emitted('rowEditCommit')?.[0]
|
|
819
|
+
expect(payload?.[2]).toEqual([{ colKey: 'name', value: 'Beta', previous: 'Alpha' }])
|
|
820
|
+
wrapper.unmount()
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('does not show shared tooltip for non-overflow ellipsis cells', async () => {
|
|
824
|
+
vi.useFakeTimers()
|
|
825
|
+
const wrapper = mount(RsTable, {
|
|
826
|
+
attachTo: document.body,
|
|
827
|
+
props: {
|
|
828
|
+
columns: [{ key: 'name', title: '名称', ellipsis: true, width: 240 }],
|
|
829
|
+
data: [{ id: '1', name: '短文本' }],
|
|
830
|
+
cellTooltipDelay: 0,
|
|
831
|
+
},
|
|
832
|
+
})
|
|
833
|
+
await wrapper.find('.rs-table__cell-tip').trigger('pointerover', { pointerType: 'mouse' })
|
|
834
|
+
await vi.runAllTimersAsync()
|
|
835
|
+
await wrapper.vm.$nextTick()
|
|
836
|
+
expect(document.body.querySelector('.rs-table__shared-tip')).toBeNull()
|
|
837
|
+
wrapper.unmount()
|
|
838
|
+
vi.useRealTimers()
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
it('expands rows with expand slot', async () => {
|
|
842
|
+
const wrapper = mount(RsTable, {
|
|
843
|
+
props: {
|
|
844
|
+
columns,
|
|
845
|
+
data,
|
|
846
|
+
expandable: true,
|
|
847
|
+
expandedRowKeys: [] as string[],
|
|
848
|
+
rowKey: 'id',
|
|
849
|
+
'onUpdate:expandedRowKeys': (keys: string[]) => wrapper.setProps({ expandedRowKeys: keys }),
|
|
850
|
+
},
|
|
851
|
+
slots: { expand: '<div class="expand-slot">detail</div>' },
|
|
852
|
+
})
|
|
853
|
+
await wrapper.find('.rs-table__expand-btn').trigger('click')
|
|
854
|
+
expect(wrapper.props('expandedRowKeys')).toEqual(['1'])
|
|
855
|
+
await wrapper.setProps({ expandedRowKeys: ['1'] })
|
|
856
|
+
expect(wrapper.find('.expand-slot').exists()).toBe(true)
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('supports row selection without checkbox column', async () => {
|
|
860
|
+
const wrapper = mount(RsTable, {
|
|
861
|
+
props: {
|
|
862
|
+
columns,
|
|
863
|
+
data,
|
|
864
|
+
selectable: true,
|
|
865
|
+
selectionType: 'row' as const,
|
|
866
|
+
rowKey: 'id',
|
|
867
|
+
selectedRowKeys: [] as string[],
|
|
868
|
+
'onUpdate:selectedRowKeys': (keys: string[]) => wrapper.setProps({ selectedRowKeys: keys }),
|
|
869
|
+
},
|
|
870
|
+
})
|
|
871
|
+
expect(wrapper.find('.rs-table__th--selection').exists()).toBe(false)
|
|
872
|
+
const rows = wrapper.findAll('.rs-table__row')
|
|
873
|
+
await rows[0]!.trigger('click')
|
|
874
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1'])
|
|
875
|
+
rows[1]!.element.dispatchEvent(
|
|
876
|
+
new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true }),
|
|
877
|
+
)
|
|
878
|
+
await wrapper.vm.$nextTick()
|
|
879
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1', '2'])
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
it('supports shift range row selection without checkbox', async () => {
|
|
883
|
+
const wrapper = mount(RsTable, {
|
|
884
|
+
props: {
|
|
885
|
+
columns,
|
|
886
|
+
data,
|
|
887
|
+
selectable: true,
|
|
888
|
+
selectionType: 'row' as const,
|
|
889
|
+
rowKey: 'id',
|
|
890
|
+
selectedRowKeys: [] as string[],
|
|
891
|
+
'onUpdate:selectedRowKeys': (keys: string[]) => wrapper.setProps({ selectedRowKeys: keys }),
|
|
892
|
+
},
|
|
893
|
+
})
|
|
894
|
+
const rows = wrapper.findAll('.rs-table__row')
|
|
895
|
+
await rows[0]!.trigger('click')
|
|
896
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1'])
|
|
897
|
+
rows[2]!.element.dispatchEvent(
|
|
898
|
+
new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true }),
|
|
899
|
+
)
|
|
900
|
+
await wrapper.vm.$nextTick()
|
|
901
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['1', '2', '3'])
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('supports radio selection', async () => {
|
|
905
|
+
const wrapper = mount(RsTable, {
|
|
906
|
+
props: {
|
|
907
|
+
columns,
|
|
908
|
+
data,
|
|
909
|
+
selectable: true,
|
|
910
|
+
selectionType: 'radio' as const,
|
|
911
|
+
rowKey: 'id',
|
|
912
|
+
selectedRowKeys: [] as string[],
|
|
913
|
+
'onUpdate:selectedRowKeys': (keys: string[]) => wrapper.setProps({ selectedRowKeys: keys }),
|
|
914
|
+
},
|
|
915
|
+
})
|
|
916
|
+
const inputs = wrapper.findAll('.rs-table__row .rs-table__checkbox-input')
|
|
917
|
+
await inputs[0]!.setValue(true)
|
|
918
|
+
await inputs[1]!.setValue(true)
|
|
919
|
+
expect(wrapper.props('selectedRowKeys')).toEqual(['2'])
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
it('does not sort locally when remoteSort is enabled', async () => {
|
|
923
|
+
const wrapper = mount(RsTable, {
|
|
924
|
+
props: {
|
|
925
|
+
columns,
|
|
926
|
+
data,
|
|
927
|
+
remoteSort: true,
|
|
928
|
+
sort: null,
|
|
929
|
+
'onUpdate:sort': (value: { key: string; order: 'asc' | 'desc' } | null) => wrapper.setProps({ sort: value }),
|
|
930
|
+
},
|
|
931
|
+
})
|
|
932
|
+
await wrapper.findAll('.rs-table__sort')[0]!.trigger('click')
|
|
933
|
+
expect(wrapper.emitted('update:sort')?.[0]?.[0]).toEqual({ key: 'name', order: 'asc' })
|
|
934
|
+
expect(wrapper.findAll('.rs-table__row td')[0].text()).toBe('B')
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
it('renders fixed column classes and horizontal scroll', () => {
|
|
938
|
+
const wrapper = mount(RsTable, {
|
|
939
|
+
props: {
|
|
940
|
+
columns: [
|
|
941
|
+
{ key: 'name', title: '名称', fixed: 'left' as const, width: 120 },
|
|
942
|
+
{ key: 'count', title: '数量', fixed: 'right' as const, width: 80 },
|
|
943
|
+
],
|
|
944
|
+
data,
|
|
945
|
+
scrollX: 600,
|
|
946
|
+
rowKey: 'id',
|
|
947
|
+
},
|
|
948
|
+
})
|
|
949
|
+
expect(wrapper.find('.rs-table').classes()).toContain('rs-table--scroll-x')
|
|
950
|
+
expect(wrapper.find('.rs-table__cell--fixed').exists()).toBe(true)
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it('renders summary slot in footer', () => {
|
|
954
|
+
const wrapper = mount(RsTable, {
|
|
955
|
+
props: { columns, data },
|
|
956
|
+
slots: { summary: '<span class="summary-total">合计</span>' },
|
|
957
|
+
})
|
|
958
|
+
expect(wrapper.find('.rs-table__summary .summary-total').text()).toBe('合计')
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
it('uses custom column sorter', async () => {
|
|
962
|
+
const wrapper = mount(RsTable, {
|
|
963
|
+
props: {
|
|
964
|
+
columns: [{ key: 'name', title: '名称', sortable: true, sorter: (a, b) => (b as { name: string }).name.localeCompare((a as { name: string }).name) }],
|
|
965
|
+
data,
|
|
966
|
+
},
|
|
967
|
+
})
|
|
968
|
+
await wrapper.find('.rs-table__sort').trigger('click')
|
|
969
|
+
expect(wrapper.findAll('.rs-table__row td')[0].text()).toBe('C')
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('supports multi-column sort', async () => {
|
|
973
|
+
const wrapper = mount(RsTable, {
|
|
974
|
+
props: {
|
|
975
|
+
columns,
|
|
976
|
+
data,
|
|
977
|
+
multiSort: true,
|
|
978
|
+
sorts: [],
|
|
979
|
+
'onUpdate:sorts': (value: { key: string; order: 'asc' | 'desc' }[]) => wrapper.setProps({ sorts: value }),
|
|
980
|
+
},
|
|
981
|
+
})
|
|
982
|
+
const sortButtons = wrapper.findAll('.rs-table__sort')
|
|
983
|
+
await sortButtons[0]!.trigger('click')
|
|
984
|
+
await sortButtons[1]!.trigger('click')
|
|
985
|
+
expect(wrapper.emitted('update:sorts')?.[1]?.[0]).toEqual([
|
|
986
|
+
{ key: 'name', order: 'asc' },
|
|
987
|
+
{ key: 'count', order: 'asc' },
|
|
988
|
+
])
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
it('emits rowDrop when rows are reordered', async () => {
|
|
992
|
+
const wrapper = mount(RsTable, {
|
|
993
|
+
props: { columns, data, rowDraggable: true, rowKey: 'id' },
|
|
994
|
+
})
|
|
995
|
+
const handle = wrapper.find('.rs-table__row-drag-handle')
|
|
996
|
+
await handle.trigger('dragstart')
|
|
997
|
+
await wrapper.findAll('.rs-table__row')[1].trigger('drop')
|
|
998
|
+
const emitted = wrapper.emitted('rowDrop')
|
|
999
|
+
expect(emitted).toBeTruthy()
|
|
1000
|
+
expect(emitted?.[0]?.[0]).toEqual(expect.arrayContaining([expect.any(String)]))
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
it('reorders columns with columnDraggable', async () => {
|
|
1004
|
+
const wrapper = mount(RsTable, {
|
|
1005
|
+
props: {
|
|
1006
|
+
columns,
|
|
1007
|
+
data,
|
|
1008
|
+
columnDraggable: true,
|
|
1009
|
+
columnOrder: ['name', 'count'],
|
|
1010
|
+
'onUpdate:columnOrder': (value: string[]) => wrapper.setProps({ columnOrder: value }),
|
|
1011
|
+
},
|
|
1012
|
+
})
|
|
1013
|
+
const handle = wrapper.find('.rs-table__column-drag-handle')
|
|
1014
|
+
await handle.trigger('dragstart')
|
|
1015
|
+
const headers = wrapper.findAll('th')
|
|
1016
|
+
await headers[headers.length - 1].trigger('drop')
|
|
1017
|
+
expect(wrapper.emitted('update:columnOrder')).toBeTruthy()
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
it('emits rowDrop with into position in into drop mode', async () => {
|
|
1021
|
+
const onRowDrop = vi.fn()
|
|
1022
|
+
const wrapper = mount(RsTable, {
|
|
1023
|
+
props: {
|
|
1024
|
+
columns,
|
|
1025
|
+
data: [
|
|
1026
|
+
{ id: 'dir', name: 'Folder', status: 'running', count: 0, updatedAt: '' },
|
|
1027
|
+
{ id: 'file', name: 'File', status: 'running', count: 1, updatedAt: '' },
|
|
1028
|
+
],
|
|
1029
|
+
rowDraggable: true,
|
|
1030
|
+
rowDragTrigger: 'row',
|
|
1031
|
+
rowDropMode: 'into',
|
|
1032
|
+
rowDraggableWhen: (row) => (row as { id: string }).id !== 'dir',
|
|
1033
|
+
rowDropTargetWhen: (row) => (row as { id: string }).id === 'dir',
|
|
1034
|
+
rowKey: 'id',
|
|
1035
|
+
onRowDrop,
|
|
1036
|
+
},
|
|
1037
|
+
})
|
|
1038
|
+
const fileCell = wrapper.findAll('.rs-table__td--row-draggable')[0]
|
|
1039
|
+
await fileCell.trigger('dragstart')
|
|
1040
|
+
await wrapper.findAll('.rs-table__row')[0].trigger('drop')
|
|
1041
|
+
expect(onRowDrop).toHaveBeenCalled()
|
|
1042
|
+
const [dragKeys, dropKey, position] = onRowDrop.mock.calls[0] as [string[], string, string]
|
|
1043
|
+
expect(dragKeys).toEqual(['file'])
|
|
1044
|
+
expect(dropKey).toBe('dir')
|
|
1045
|
+
expect(position).toBe('into')
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('shows filter icon for filterable columns and filters rows', async () => {
|
|
1049
|
+
const filterColumns = [
|
|
1050
|
+
{ key: 'name', title: '名称', filterable: true },
|
|
1051
|
+
{ key: 'count', title: '数量' },
|
|
1052
|
+
]
|
|
1053
|
+
const wrapper = mount(RsTable, {
|
|
1054
|
+
props: {
|
|
1055
|
+
columns: filterColumns,
|
|
1056
|
+
data,
|
|
1057
|
+
columnFilters: { name: 'A' },
|
|
1058
|
+
'onUpdate:columnFilters': (value: Record<string, string>) =>
|
|
1059
|
+
wrapper.setProps({ columnFilters: value }),
|
|
1060
|
+
},
|
|
1061
|
+
})
|
|
1062
|
+
expect(wrapper.find('.rs-table__filter').exists()).toBe(true)
|
|
1063
|
+
expect(wrapper.findAll('.rs-table__row')).toHaveLength(1)
|
|
1064
|
+
expect(wrapper.find('.rs-table__row td').text()).toBe('A')
|
|
1065
|
+
|
|
1066
|
+
await wrapper.setProps({ columnFilters: {} })
|
|
1067
|
+
expect(wrapper.findAll('.rs-table__row')).toHaveLength(3)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('highlights active column filter', async () => {
|
|
1071
|
+
const wrapper = mount(RsTable, {
|
|
1072
|
+
props: {
|
|
1073
|
+
columns: [{ key: 'name', title: '名称', filterable: true }],
|
|
1074
|
+
data,
|
|
1075
|
+
columnFilters: { name: 'B' },
|
|
1076
|
+
},
|
|
1077
|
+
})
|
|
1078
|
+
expect(wrapper.find('.rs-table__filter--active').exists()).toBe(true)
|
|
1079
|
+
expect(wrapper.findAll('.rs-table__row')).toHaveLength(1)
|
|
1080
|
+
await wrapper.setProps({ columnFilters: {} })
|
|
1081
|
+
expect(wrapper.find('.rs-table__filter--active').exists()).toBe(false)
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
it('shows default copy actions in context menu on data cell', async () => {
|
|
1085
|
+
const wrapper = mount(RsTable, {
|
|
1086
|
+
props: { columns, data, rowKey: 'id' },
|
|
1087
|
+
attachTo: document.body,
|
|
1088
|
+
})
|
|
1089
|
+
await wrapper.find('.rs-table__td--data').trigger('contextmenu')
|
|
1090
|
+
await wrapper.vm.$nextTick()
|
|
1091
|
+
const labels = [...document.body.querySelectorAll('.rs-context-menu__label')].map(
|
|
1092
|
+
(node) => node.textContent?.trim(),
|
|
1093
|
+
)
|
|
1094
|
+
expect(labels).toContain('复制单元格')
|
|
1095
|
+
expect(labels).toContain('复制行')
|
|
1096
|
+
wrapper.unmount()
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
it('copies cell and row text from built-in context menu', async () => {
|
|
1100
|
+
const copyMock = vi.mocked(copyTextToClipboard)
|
|
1101
|
+
copyMock.mockClear()
|
|
1102
|
+
const wrapper = mount(RsTable, {
|
|
1103
|
+
props: { columns, data, rowKey: 'id' },
|
|
1104
|
+
attachTo: document.body,
|
|
1105
|
+
})
|
|
1106
|
+
await wrapper.find('.rs-table__td--data').trigger('contextmenu')
|
|
1107
|
+
await wrapper.vm.$nextTick()
|
|
1108
|
+
const items = document.body.querySelectorAll('.rs-context-menu__item')
|
|
1109
|
+
const copyCell = [...items].find((item) => item.textContent?.includes('复制单元格'))
|
|
1110
|
+
expect(copyCell).toBeTruthy()
|
|
1111
|
+
;(copyCell as HTMLElement).click()
|
|
1112
|
+
await wrapper.vm.$nextTick()
|
|
1113
|
+
expect(copyMock).toHaveBeenCalledWith('B')
|
|
1114
|
+
|
|
1115
|
+
copyMock.mockClear()
|
|
1116
|
+
await wrapper.find('.rs-table__td--data').trigger('contextmenu')
|
|
1117
|
+
await wrapper.vm.$nextTick()
|
|
1118
|
+
const copyRow = [...document.body.querySelectorAll('.rs-context-menu__item')].find((item) =>
|
|
1119
|
+
item.textContent?.includes('复制行'),
|
|
1120
|
+
)
|
|
1121
|
+
;(copyRow as HTMLElement).click()
|
|
1122
|
+
await wrapper.vm.$nextTick()
|
|
1123
|
+
expect(copyMock).toHaveBeenCalledWith('B\t20')
|
|
1124
|
+
wrapper.unmount()
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it('can disable built-in context menu', async () => {
|
|
1128
|
+
const wrapper = mount(RsTable, {
|
|
1129
|
+
props: { columns, data, rowKey: 'id', contextMenu: false },
|
|
1130
|
+
})
|
|
1131
|
+
await wrapper.find('.rs-table__td--data').trigger('contextmenu')
|
|
1132
|
+
expect(document.body.querySelector('.rs-context-menu__content')).toBeNull()
|
|
1133
|
+
wrapper.unmount()
|
|
1134
|
+
})
|
|
1135
|
+
})
|