niuma-ui 1.0.0 → 1.0.2
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 +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- package/src/components/RsTableHeaderRow.vue +0 -30
|
@@ -1,42 +1,72 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import { h } from 'vue'
|
|
2
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
3
|
+
import { h, nextTick } from 'vue'
|
|
4
4
|
import RsMenu from '../components/RsMenu.vue'
|
|
5
5
|
import RsConfigProvider from '../components/RsConfigProvider.vue'
|
|
6
|
-
import type { RsMenuItemGroup } from '../components/menu-utils'
|
|
6
|
+
import type { RsMenuItem, RsMenuItemGroup } from '../components/menu-utils'
|
|
7
7
|
|
|
8
8
|
describe('RsMenu', () => {
|
|
9
|
-
const flatItems = [
|
|
10
|
-
{ key: 'dashboard', label: '仪表盘', icon: 'layout-dashboard'
|
|
11
|
-
{ key: 'chat', label: '对话', icon: 'message-square'
|
|
12
|
-
{ key: 'settings', label: '设置', icon: 'settings'
|
|
9
|
+
const flatItems: RsMenuItem[] = [
|
|
10
|
+
{ key: 'dashboard', label: '仪表盘', icon: 'layout-dashboard' },
|
|
11
|
+
{ key: 'chat', label: '对话', icon: 'message-square' },
|
|
12
|
+
{ key: 'settings', label: '设置', icon: 'settings' },
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
const groupedItems: RsMenuItemGroup[] = [
|
|
16
16
|
{
|
|
17
17
|
label: '工作区',
|
|
18
18
|
children: [
|
|
19
|
-
{ key: 'projects', label: '项目', icon: 'folder'
|
|
20
|
-
{ key: 'docs', label: '文档', icon: 'search'
|
|
19
|
+
{ key: 'projects', label: '项目', icon: 'folder' },
|
|
20
|
+
{ key: 'docs', label: '文档', icon: 'search' },
|
|
21
21
|
],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
label: '系统',
|
|
25
|
-
children: [{ key: 'settings', label: '设置', icon: 'settings'
|
|
25
|
+
children: [{ key: 'settings', label: '设置', icon: 'settings' }],
|
|
26
26
|
},
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
const nestedItems = [
|
|
29
|
+
const nestedItems: RsMenuItem[] = [
|
|
30
30
|
{
|
|
31
31
|
key: 'workspace',
|
|
32
32
|
label: '工作区',
|
|
33
|
-
icon: 'folder'
|
|
33
|
+
icon: 'folder',
|
|
34
34
|
children: [
|
|
35
35
|
{ key: 'projects', label: '项目' },
|
|
36
36
|
{ key: 'archived', label: '归档', disabled: true },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
|
-
{ key: 'settings', label: '设置', icon: 'settings'
|
|
39
|
+
{ key: 'settings', label: '设置', icon: 'settings' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const deepNestedItems: RsMenuItem[] = [
|
|
43
|
+
{
|
|
44
|
+
key: 'workspace',
|
|
45
|
+
label: '工作区',
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
key: 'team',
|
|
49
|
+
label: '团队',
|
|
50
|
+
children: [
|
|
51
|
+
{ key: 'members', label: '成员' },
|
|
52
|
+
{ key: 'permissions', label: '权限' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const groupedNestedItems: RsMenuItemGroup[] = [
|
|
60
|
+
{
|
|
61
|
+
label: '产品',
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
key: 'product-line',
|
|
65
|
+
label: '产品线',
|
|
66
|
+
children: [{ key: 'saas-billing', label: '计费' }],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
40
70
|
]
|
|
41
71
|
|
|
42
72
|
it('renders nav with menu aria-label', () => {
|
|
@@ -60,6 +90,82 @@ describe('RsMenu', () => {
|
|
|
60
90
|
expect(wrapper.find('nav.rs-menu').attributes('aria-label')).toBe('Navigation menu')
|
|
61
91
|
})
|
|
62
92
|
|
|
93
|
+
it('applies vertical mode class by default', () => {
|
|
94
|
+
const wrapper = mount(RsMenu, {
|
|
95
|
+
props: { items: flatItems, modelValue: 'dashboard' },
|
|
96
|
+
})
|
|
97
|
+
expect(wrapper.classes()).toContain('rs-menu--vertical')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('applies horizontal mode class', () => {
|
|
101
|
+
const wrapper = mount(RsMenu, {
|
|
102
|
+
props: { items: flatItems, modelValue: 'dashboard', mode: 'horizontal' },
|
|
103
|
+
})
|
|
104
|
+
expect(wrapper.classes()).toContain('rs-menu--horizontal')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('marks active leaf with aria-current and active class', () => {
|
|
108
|
+
const wrapper = mount(RsMenu, {
|
|
109
|
+
props: { items: flatItems, modelValue: 'chat' },
|
|
110
|
+
})
|
|
111
|
+
const active = wrapper.find('.rs-menu__item--active')
|
|
112
|
+
expect(active.exists()).toBe(true)
|
|
113
|
+
expect(active.attributes('aria-current')).toBe('page')
|
|
114
|
+
expect(active.text()).toContain('对话')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not highlight parent by default when nested leaf is active', () => {
|
|
118
|
+
const wrapper = mount(RsMenu, {
|
|
119
|
+
props: {
|
|
120
|
+
items: nestedItems,
|
|
121
|
+
modelValue: 'projects',
|
|
122
|
+
openKeys: ['workspace'],
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
const actives = wrapper.findAll('.rs-menu__item--active')
|
|
126
|
+
expect(actives).toHaveLength(1)
|
|
127
|
+
expect(actives[0]!.text()).toContain('项目')
|
|
128
|
+
expect(actives[0]!.attributes('aria-current')).toBe('page')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('highlights ancestor submenu triggers when highlightParent is true', () => {
|
|
132
|
+
const wrapper = mount(RsMenu, {
|
|
133
|
+
props: {
|
|
134
|
+
items: nestedItems,
|
|
135
|
+
modelValue: 'projects',
|
|
136
|
+
openKeys: ['workspace'],
|
|
137
|
+
highlightParent: true,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
const leaf = wrapper.findAll('.rs-menu__item--active').find((n) => n.text().includes('项目'))
|
|
141
|
+
expect(leaf).toBeDefined()
|
|
142
|
+
expect(leaf?.attributes('aria-current')).toBe('page')
|
|
143
|
+
const parent = wrapper
|
|
144
|
+
.findAll('.rs-menu__item--active-parent')
|
|
145
|
+
.find((n) => n.text().includes('工作区'))
|
|
146
|
+
expect(parent).toBeDefined()
|
|
147
|
+
expect(parent?.attributes('aria-current')).toBeUndefined()
|
|
148
|
+
expect(parent!.classes()).not.toContain('rs-menu__item--active')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('highlights collapsed parent icon when highlightParent is true', () => {
|
|
152
|
+
const wrapper = mount(RsMenu, {
|
|
153
|
+
props: {
|
|
154
|
+
items: nestedItems,
|
|
155
|
+
modelValue: 'projects',
|
|
156
|
+
openKeys: [],
|
|
157
|
+
collapsed: true,
|
|
158
|
+
highlightParent: true,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
const parent = wrapper
|
|
162
|
+
.findAll('.rs-menu__item--submenu-trigger')
|
|
163
|
+
.find((btn) => btn.attributes('aria-label') === '工作区')
|
|
164
|
+
expect(parent).toBeDefined()
|
|
165
|
+
expect(parent!.classes()).toContain('rs-menu__item--active-parent')
|
|
166
|
+
expect(parent!.classes()).not.toContain('rs-menu__item--active')
|
|
167
|
+
})
|
|
168
|
+
|
|
63
169
|
it('emits select and updates model when item clicked', async () => {
|
|
64
170
|
const wrapper = mount(RsMenu, {
|
|
65
171
|
props: { items: flatItems, modelValue: 'dashboard' },
|
|
@@ -78,6 +184,10 @@ describe('RsMenu', () => {
|
|
|
78
184
|
props: { items: groupedItems, modelValue: 'projects' },
|
|
79
185
|
})
|
|
80
186
|
expect(wrapper.find('.rs-menu__group-label').text()).toBe('工作区')
|
|
187
|
+
expect(wrapper.findAll('.rs-menu__group-label').map((n) => n.text())).toEqual([
|
|
188
|
+
'工作区',
|
|
189
|
+
'系统',
|
|
190
|
+
])
|
|
81
191
|
})
|
|
82
192
|
|
|
83
193
|
it('auto-expands parent submenu for active nested item', () => {
|
|
@@ -91,6 +201,76 @@ describe('RsMenu', () => {
|
|
|
91
201
|
expect(wrapper.emitted('update:openKeys')?.pop()?.[0]).toContain('workspace')
|
|
92
202
|
})
|
|
93
203
|
|
|
204
|
+
it('auto-expands all ancestor keys for deeply nested active item', () => {
|
|
205
|
+
const wrapper = mount(RsMenu, {
|
|
206
|
+
props: {
|
|
207
|
+
items: deepNestedItems,
|
|
208
|
+
modelValue: 'permissions',
|
|
209
|
+
openKeys: [],
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
const keys = wrapper.emitted('update:openKeys')?.pop()?.[0] as string[]
|
|
213
|
+
expect(keys).toEqual(expect.arrayContaining(['workspace', 'team']))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('auto-expands ancestors inside grouped nested items', () => {
|
|
217
|
+
const wrapper = mount(RsMenu, {
|
|
218
|
+
props: {
|
|
219
|
+
items: groupedNestedItems,
|
|
220
|
+
modelValue: 'saas-billing',
|
|
221
|
+
openKeys: [],
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
expect(wrapper.emitted('update:openKeys')?.pop()?.[0]).toContain('product-line')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('toggles openKeys when submenu trigger is clicked', async () => {
|
|
228
|
+
const wrapper = mount(RsMenu, {
|
|
229
|
+
props: {
|
|
230
|
+
items: nestedItems,
|
|
231
|
+
modelValue: 'settings',
|
|
232
|
+
openKeys: [],
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
const trigger = wrapper.find('.rs-menu__item--submenu-trigger')
|
|
236
|
+
expect(trigger.exists()).toBe(true)
|
|
237
|
+
await trigger.trigger('click')
|
|
238
|
+
await nextTick()
|
|
239
|
+
expect(wrapper.emitted('update:openKeys')?.pop()?.[0]).toContain('workspace')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('closes submenu when open trigger is clicked again', async () => {
|
|
243
|
+
const wrapper = mount(RsMenu, {
|
|
244
|
+
props: {
|
|
245
|
+
items: nestedItems,
|
|
246
|
+
modelValue: 'settings',
|
|
247
|
+
openKeys: ['workspace'],
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
const trigger = wrapper.find('.rs-menu__item--submenu-trigger')
|
|
251
|
+
await trigger.trigger('click')
|
|
252
|
+
await nextTick()
|
|
253
|
+
const keys = wrapper.emitted('update:openKeys')?.pop()?.[0] as string[]
|
|
254
|
+
expect(keys).not.toContain('workspace')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('selects nested leaf under open submenu', async () => {
|
|
258
|
+
const wrapper = mount(RsMenu, {
|
|
259
|
+
props: {
|
|
260
|
+
items: nestedItems,
|
|
261
|
+
modelValue: 'settings',
|
|
262
|
+
openKeys: ['workspace'],
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
const projects = wrapper
|
|
266
|
+
.findAll('.rs-menu__item')
|
|
267
|
+
.find((btn) => btn.text().includes('项目'))
|
|
268
|
+
expect(projects).toBeDefined()
|
|
269
|
+
await projects!.trigger('click')
|
|
270
|
+
expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['projects'])
|
|
271
|
+
expect(wrapper.emitted('select')?.[0]).toEqual(['projects'])
|
|
272
|
+
})
|
|
273
|
+
|
|
94
274
|
it('does not emit select for disabled item', async () => {
|
|
95
275
|
const wrapper = mount(RsMenu, {
|
|
96
276
|
props: {
|
|
@@ -105,7 +285,76 @@ describe('RsMenu', () => {
|
|
|
105
285
|
expect(wrapper.emitted('select')).toBeUndefined()
|
|
106
286
|
})
|
|
107
287
|
|
|
108
|
-
it('
|
|
288
|
+
it('renders nested submenu as popup list in horizontal mode', () => {
|
|
289
|
+
const wrapper = mount(RsMenu, {
|
|
290
|
+
props: {
|
|
291
|
+
items: nestedItems,
|
|
292
|
+
modelValue: 'projects',
|
|
293
|
+
openKeys: ['workspace'],
|
|
294
|
+
mode: 'horizontal',
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
expect(wrapper.classes()).toContain('rs-menu--horizontal')
|
|
298
|
+
expect(wrapper.find('.rs-menu__list--nested').exists()).toBe(true)
|
|
299
|
+
expect(wrapper.find('.rs-menu__entry--submenu').exists()).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('applies collapsed class and hides labels', () => {
|
|
303
|
+
const wrapper = mount(RsMenu, {
|
|
304
|
+
props: {
|
|
305
|
+
items: flatItems,
|
|
306
|
+
modelValue: 'dashboard',
|
|
307
|
+
collapsed: true,
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
expect(wrapper.classes()).toContain('rs-menu--collapsed')
|
|
311
|
+
expect(wrapper.find('.rs-menu__label').exists()).toBe(false)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('hides group labels when collapsed', () => {
|
|
315
|
+
const wrapper = mount(RsMenu, {
|
|
316
|
+
props: {
|
|
317
|
+
items: groupedItems,
|
|
318
|
+
modelValue: 'projects',
|
|
319
|
+
collapsed: true,
|
|
320
|
+
},
|
|
321
|
+
})
|
|
322
|
+
expect(wrapper.find('.rs-menu__group-label').exists()).toBe(false)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('sets title and aria-label on items when collapsed', () => {
|
|
326
|
+
const wrapper = mount(RsMenu, {
|
|
327
|
+
props: {
|
|
328
|
+
items: flatItems,
|
|
329
|
+
modelValue: 'dashboard',
|
|
330
|
+
collapsed: true,
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
const settings = wrapper
|
|
334
|
+
.findAll('.rs-menu__item')
|
|
335
|
+
.find((btn) => btn.attributes('title') === '设置')
|
|
336
|
+
expect(settings).toBeDefined()
|
|
337
|
+
expect(settings!.attributes('aria-label')).toBe('设置')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('still selects leaf items when collapsed', async () => {
|
|
341
|
+
const wrapper = mount(RsMenu, {
|
|
342
|
+
props: {
|
|
343
|
+
items: flatItems,
|
|
344
|
+
modelValue: 'dashboard',
|
|
345
|
+
collapsed: true,
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
const chat = wrapper
|
|
349
|
+
.findAll('.rs-menu__item')
|
|
350
|
+
.find((btn) => btn.attributes('title') === '对话')
|
|
351
|
+
expect(chat).toBeDefined()
|
|
352
|
+
await chat!.trigger('click')
|
|
353
|
+
expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['chat'])
|
|
354
|
+
expect(wrapper.emitted('select')?.[0]).toEqual(['chat'])
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('does not render inline nested submenu list when collapsed', () => {
|
|
109
358
|
const wrapper = mount(RsMenu, {
|
|
110
359
|
props: {
|
|
111
360
|
items: nestedItems,
|
|
@@ -113,7 +362,150 @@ describe('RsMenu', () => {
|
|
|
113
362
|
openKeys: ['workspace'],
|
|
114
363
|
collapsed: true,
|
|
115
364
|
},
|
|
365
|
+
attachTo: document.body,
|
|
116
366
|
})
|
|
117
367
|
expect(wrapper.find('.rs-menu__list--nested').exists()).toBe(false)
|
|
368
|
+
expect(wrapper.find('.rs-menu__item--submenu-trigger').exists()).toBe(true)
|
|
369
|
+
expect(document.body.querySelector('.rs-menu__flyout')).toBeNull()
|
|
370
|
+
wrapper.unmount()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('shows flyout submenu on hover when collapsed vertically', async () => {
|
|
374
|
+
const wrapper = mount(RsMenu, {
|
|
375
|
+
props: {
|
|
376
|
+
items: nestedItems,
|
|
377
|
+
modelValue: 'settings',
|
|
378
|
+
openKeys: [],
|
|
379
|
+
collapsed: true,
|
|
380
|
+
},
|
|
381
|
+
attachTo: document.body,
|
|
382
|
+
})
|
|
383
|
+
const parent = wrapper
|
|
384
|
+
.findAll('.rs-menu__item')
|
|
385
|
+
.find((btn) => btn.attributes('aria-label') === '工作区')
|
|
386
|
+
expect(parent).toBeDefined()
|
|
387
|
+
|
|
388
|
+
await parent!.trigger('mouseenter')
|
|
389
|
+
await nextTick()
|
|
390
|
+
await flushPromises()
|
|
391
|
+
|
|
392
|
+
const flyout = document.body.querySelector('.rs-menu__flyout')
|
|
393
|
+
expect(flyout).not.toBeNull()
|
|
394
|
+
expect(flyout?.textContent).toContain('工作区')
|
|
395
|
+
expect(flyout?.textContent).toContain('项目')
|
|
396
|
+
expect(wrapper.emitted('update:openKeys')).toBeUndefined()
|
|
397
|
+
// 折叠态父级点击不选中父 key,仅用于打开浮层
|
|
398
|
+
await parent!.trigger('click')
|
|
399
|
+
await nextTick()
|
|
400
|
+
expect(wrapper.emitted('select')).toBeUndefined()
|
|
401
|
+
wrapper.unmount()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('selects nested leaf from collapsed flyout', async () => {
|
|
405
|
+
const wrapper = mount(RsMenu, {
|
|
406
|
+
props: {
|
|
407
|
+
items: nestedItems,
|
|
408
|
+
modelValue: 'settings',
|
|
409
|
+
openKeys: [],
|
|
410
|
+
collapsed: true,
|
|
411
|
+
},
|
|
412
|
+
attachTo: document.body,
|
|
413
|
+
})
|
|
414
|
+
const parent = wrapper
|
|
415
|
+
.findAll('.rs-menu__item')
|
|
416
|
+
.find((btn) => btn.attributes('aria-label') === '工作区')
|
|
417
|
+
expect(parent).toBeDefined()
|
|
418
|
+
await parent!.trigger('mouseenter')
|
|
419
|
+
await nextTick()
|
|
420
|
+
await flushPromises()
|
|
421
|
+
|
|
422
|
+
const projects = Array.from(
|
|
423
|
+
document.body.querySelectorAll('.rs-menu__flyout .rs-menu__item'),
|
|
424
|
+
).find((el) => el.textContent?.includes('项目')) as HTMLButtonElement | undefined
|
|
425
|
+
expect(projects).toBeDefined()
|
|
426
|
+
projects!.click()
|
|
427
|
+
await nextTick()
|
|
428
|
+
await flushPromises()
|
|
429
|
+
|
|
430
|
+
expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['projects'])
|
|
431
|
+
expect(wrapper.emitted('select')?.[0]).toEqual(['projects'])
|
|
432
|
+
expect(document.body.querySelector('.rs-menu__flyout')).toBeNull()
|
|
433
|
+
wrapper.unmount()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('renders icons on collapsed triggers and flyout children', async () => {
|
|
437
|
+
const iconNestedItems: RsMenuItem[] = [
|
|
438
|
+
{
|
|
439
|
+
key: 'workspace',
|
|
440
|
+
label: '工作区',
|
|
441
|
+
icon: 'folder',
|
|
442
|
+
children: [
|
|
443
|
+
{ key: 'projects', label: '项目', icon: 'box' },
|
|
444
|
+
{ key: 'docs', label: '文档', icon: 'file-text' },
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
{ key: 'settings', label: '设置', icon: 'settings' },
|
|
448
|
+
]
|
|
449
|
+
const wrapper = mount(RsMenu, {
|
|
450
|
+
props: {
|
|
451
|
+
items: iconNestedItems,
|
|
452
|
+
modelValue: 'settings',
|
|
453
|
+
openKeys: [],
|
|
454
|
+
collapsed: true,
|
|
455
|
+
},
|
|
456
|
+
attachTo: document.body,
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const triggers = wrapper.findAll('.rs-menu__item')
|
|
460
|
+
expect(triggers.every((btn) => btn.find('.rs-menu__icon').exists())).toBe(true)
|
|
461
|
+
|
|
462
|
+
const parent = triggers.find((btn) => btn.attributes('aria-label') === '工作区')
|
|
463
|
+
expect(parent).toBeDefined()
|
|
464
|
+
await parent!.trigger('mouseenter')
|
|
465
|
+
await nextTick()
|
|
466
|
+
await flushPromises()
|
|
467
|
+
|
|
468
|
+
const flyout = document.body.querySelector('.rs-menu__flyout')
|
|
469
|
+
expect(flyout).not.toBeNull()
|
|
470
|
+
expect(flyout!.querySelectorAll('.rs-menu__icon').length).toBeGreaterThanOrEqual(2)
|
|
471
|
+
expect(flyout!.textContent).toContain('项目')
|
|
472
|
+
expect(flyout!.textContent).toContain('文档')
|
|
473
|
+
wrapper.unmount()
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('keeps horizontal submenu expandable even when collapsed prop is true', async () => {
|
|
477
|
+
const wrapper = mount(RsMenu, {
|
|
478
|
+
props: {
|
|
479
|
+
items: nestedItems,
|
|
480
|
+
modelValue: 'settings',
|
|
481
|
+
openKeys: [],
|
|
482
|
+
mode: 'horizontal',
|
|
483
|
+
collapsed: true,
|
|
484
|
+
},
|
|
485
|
+
})
|
|
486
|
+
// 水平模式忽略 collapsed:仍显示文案,不挂 rs-menu--collapsed
|
|
487
|
+
expect(wrapper.classes()).not.toContain('rs-menu--collapsed')
|
|
488
|
+
expect(wrapper.text()).toContain('工作区')
|
|
489
|
+
const trigger = wrapper.find('.rs-menu__item--submenu-trigger')
|
|
490
|
+
expect(trigger.exists()).toBe(true)
|
|
491
|
+
await trigger.trigger('click')
|
|
492
|
+
await nextTick()
|
|
493
|
+
expect(wrapper.emitted('update:openKeys')?.pop()?.[0]).toContain('workspace')
|
|
494
|
+
// 子菜单内容可见,而非空气泡
|
|
495
|
+
expect(wrapper.find('.rs-menu__list--nested').exists()).toBe(true)
|
|
496
|
+
expect(wrapper.text()).toContain('项目')
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('does not hide horizontal labels when collapsed is true', () => {
|
|
500
|
+
const wrapper = mount(RsMenu, {
|
|
501
|
+
props: {
|
|
502
|
+
items: nestedItems,
|
|
503
|
+
modelValue: 'settings',
|
|
504
|
+
mode: 'horizontal',
|
|
505
|
+
collapsed: true,
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
expect(wrapper.find('.rs-menu__label').exists()).toBe(true)
|
|
509
|
+
expect(wrapper.find('.rs-menu__submenu-arrow').exists()).toBe(true)
|
|
118
510
|
})
|
|
119
511
|
})
|
|
@@ -131,7 +131,13 @@ describe('RsPagination', () => {
|
|
|
131
131
|
|
|
132
132
|
it('jumps to page when confirm is clicked', async () => {
|
|
133
133
|
const wrapper = mount(RsPagination, {
|
|
134
|
-
props: {
|
|
134
|
+
props: {
|
|
135
|
+
total: 100,
|
|
136
|
+
page: 1,
|
|
137
|
+
pageSize: 10,
|
|
138
|
+
showQuickJumper: true,
|
|
139
|
+
showJumpConfirm: true,
|
|
140
|
+
},
|
|
135
141
|
})
|
|
136
142
|
const input = wrapper.find('.rs-pagination__jumper-input input')
|
|
137
143
|
await input.setValue('7')
|
|
@@ -143,9 +149,68 @@ describe('RsPagination', () => {
|
|
|
143
149
|
expect(wrapper.emitted('update:page')?.pop()).toEqual([7])
|
|
144
150
|
})
|
|
145
151
|
|
|
152
|
+
it('hides jump confirm button by default', () => {
|
|
153
|
+
const wrapper = mount(RsPagination, {
|
|
154
|
+
props: {
|
|
155
|
+
total: 100,
|
|
156
|
+
page: 1,
|
|
157
|
+
pageSize: 10,
|
|
158
|
+
showQuickJumper: true,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
const jumperBtn = wrapper.findAll('button').find((btn) => btn.text() === '确定')
|
|
162
|
+
expect(jumperBtn).toBeUndefined()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('hides jump confirm button when showJumpConfirm is false', () => {
|
|
166
|
+
const wrapper = mount(RsPagination, {
|
|
167
|
+
props: {
|
|
168
|
+
total: 100,
|
|
169
|
+
page: 1,
|
|
170
|
+
pageSize: 10,
|
|
171
|
+
showQuickJumper: true,
|
|
172
|
+
showJumpConfirm: false,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
const jumperBtn = wrapper.findAll('button').find((btn) => btn.text() === '确定')
|
|
176
|
+
expect(jumperBtn).toBeUndefined()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('jumps to page on enter when confirm is hidden', async () => {
|
|
180
|
+
const wrapper = mount(RsPagination, {
|
|
181
|
+
props: {
|
|
182
|
+
total: 100,
|
|
183
|
+
page: 1,
|
|
184
|
+
pageSize: 10,
|
|
185
|
+
showQuickJumper: true,
|
|
186
|
+
showJumpConfirm: false,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
const input = wrapper.find('.rs-pagination__jumper-input input')
|
|
190
|
+
await input.setValue('4')
|
|
191
|
+
// RsInput 对 Enter 发 pressEnter;需带 key 才能命中 onKeydown
|
|
192
|
+
await input.trigger('keydown', { key: 'Enter' })
|
|
193
|
+
expect(wrapper.emitted('update:page')?.pop()).toEqual([4])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('applies size class for sm / md / lg', () => {
|
|
197
|
+
for (const size of ['sm', 'md', 'lg'] as const) {
|
|
198
|
+
const wrapper = mount(RsPagination, {
|
|
199
|
+
props: { total: 40, page: 1, pageSize: 10, size },
|
|
200
|
+
})
|
|
201
|
+
expect(wrapper.find('nav').classes()).toContain(`rs-pagination--${size}`)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
146
205
|
it('clamps jump target to valid page range', async () => {
|
|
147
206
|
const wrapper = mount(RsPagination, {
|
|
148
|
-
props: {
|
|
207
|
+
props: {
|
|
208
|
+
total: 100,
|
|
209
|
+
page: 1,
|
|
210
|
+
pageSize: 10,
|
|
211
|
+
showQuickJumper: true,
|
|
212
|
+
showJumpConfirm: true,
|
|
213
|
+
},
|
|
149
214
|
})
|
|
150
215
|
const input = wrapper.find('.rs-pagination__jumper-input input')
|
|
151
216
|
await input.setValue('99')
|
|
@@ -4,7 +4,12 @@ import { defineComponent, h, ref } from 'vue'
|
|
|
4
4
|
import RsConfigProvider from '../components/RsConfigProvider.vue'
|
|
5
5
|
import RsForm from '../components/RsForm.vue'
|
|
6
6
|
import RsSelect from '../components/RsSelect.vue'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
filterSelectOptions,
|
|
9
|
+
fromComboboxValue,
|
|
10
|
+
toComboboxValue,
|
|
11
|
+
RS_SELECT_EMPTY_VALUE,
|
|
12
|
+
} from '../components/select-utils'
|
|
8
13
|
|
|
9
14
|
describe('RsSelect', () => {
|
|
10
15
|
const options = [
|
|
@@ -91,6 +96,35 @@ describe('RsSelect', () => {
|
|
|
91
96
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([''])
|
|
92
97
|
})
|
|
93
98
|
|
|
99
|
+
it('maps empty option value through sentinel without crashing', async () => {
|
|
100
|
+
const withEmpty = [
|
|
101
|
+
{ label: '空串项', value: '' },
|
|
102
|
+
{ label: 'Claude', value: 'claude' },
|
|
103
|
+
]
|
|
104
|
+
const wrapper = mount(RsSelect, {
|
|
105
|
+
props: { options: withEmpty, modelValue: '', clearable: true },
|
|
106
|
+
attachTo: document.body,
|
|
107
|
+
})
|
|
108
|
+
expect(wrapper.find('.rs-select__placeholder').exists()).toBe(true)
|
|
109
|
+
await wrapper.find('.rs-select__trigger').trigger('click')
|
|
110
|
+
await flushPromises()
|
|
111
|
+
expect(document.body.textContent).toContain('空串项')
|
|
112
|
+
wrapper.unmount()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('selecting empty sentinel emits empty string', async () => {
|
|
116
|
+
const withEmpty = [
|
|
117
|
+
{ label: '空串项', value: '' },
|
|
118
|
+
{ label: 'Claude', value: 'claude' },
|
|
119
|
+
]
|
|
120
|
+
const wrapper = mount(RsSelect, {
|
|
121
|
+
props: { options: withEmpty, modelValue: 'claude', clearable: true },
|
|
122
|
+
})
|
|
123
|
+
const root = wrapper.getComponent({ name: 'ComboboxRoot' })
|
|
124
|
+
await root.vm.$emit('update:modelValue', '__rs_select_empty__')
|
|
125
|
+
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([''])
|
|
126
|
+
})
|
|
127
|
+
|
|
94
128
|
it('supports multiple mode tags', () => {
|
|
95
129
|
const wrapper = mount(RsSelect, {
|
|
96
130
|
props: {
|
|
@@ -266,6 +300,13 @@ describe('select-utils', () => {
|
|
|
266
300
|
const contains = (text: string, search: string) =>
|
|
267
301
|
text.toLowerCase().includes(search.toLowerCase())
|
|
268
302
|
|
|
303
|
+
it('maps empty string to combobox sentinel and back', () => {
|
|
304
|
+
expect(toComboboxValue('')).toBe(RS_SELECT_EMPTY_VALUE)
|
|
305
|
+
expect(toComboboxValue('claude')).toBe('claude')
|
|
306
|
+
expect(fromComboboxValue(RS_SELECT_EMPTY_VALUE)).toBe('')
|
|
307
|
+
expect(fromComboboxValue('claude')).toBe('claude')
|
|
308
|
+
})
|
|
309
|
+
|
|
269
310
|
it('filterSelectOptions filters flat and grouped options', () => {
|
|
270
311
|
const grouped = [
|
|
271
312
|
{
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { defineComponent, nextTick } from 'vue'
|
|
4
|
+
import RsSwitch from '../components/RsSwitch.vue'
|
|
5
|
+
|
|
6
|
+
describe('RsSwitch', () => {
|
|
7
|
+
it('toggles v-model on click', async () => {
|
|
8
|
+
const Host = defineComponent({
|
|
9
|
+
components: { RsSwitch },
|
|
10
|
+
data: () => ({ on: false }),
|
|
11
|
+
template: '<RsSwitch v-model="on" />',
|
|
12
|
+
})
|
|
13
|
+
const wrapper = mount(Host)
|
|
14
|
+
|
|
15
|
+
await wrapper.find('.rs-switch__root').trigger('click')
|
|
16
|
+
await nextTick()
|
|
17
|
+
expect(wrapper.find('.rs-switch--checked').exists()).toBe(true)
|
|
18
|
+
expect((wrapper.vm as { on: boolean }).on).toBe(true)
|
|
19
|
+
|
|
20
|
+
await wrapper.find('.rs-switch__root').trigger('click')
|
|
21
|
+
await nextTick()
|
|
22
|
+
expect(wrapper.find('.rs-switch--checked').exists()).toBe(false)
|
|
23
|
+
expect((wrapper.vm as { on: boolean }).on).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('emits change when toggled', async () => {
|
|
27
|
+
const wrapper = mount(RsSwitch, {
|
|
28
|
+
props: { modelValue: false },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await wrapper.find('.rs-switch__root').trigger('click')
|
|
32
|
+
expect(wrapper.emitted('change')?.[0]).toEqual([true])
|
|
33
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does not toggle when disabled', async () => {
|
|
37
|
+
const wrapper = mount(RsSwitch, {
|
|
38
|
+
props: { modelValue: false, disabled: true },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await wrapper.find('.rs-switch__root').trigger('click')
|
|
42
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
})
|