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.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsMarkdown from '../components/RsMarkdown.vue'
4
+ import {
5
+ escapeHtml,
6
+ isSafeHref,
7
+ isSafeImageSrc,
8
+ renderMarkdown,
9
+ resolveMarkdownHeight,
10
+ resolveMarkdownMode,
11
+ } from '../components/markdown-utils'
12
+
13
+ describe('markdown-utils', () => {
14
+ it('escapes HTML special characters', () => {
15
+ expect(escapeHtml(`<a href="x">'"&`)).toBe('&lt;a href=&quot;x&quot;&gt;&#39;&quot;&amp;')
16
+ })
17
+
18
+ it('allows safe href / image protocols', () => {
19
+ expect(isSafeHref('https://example.com')).toBe(true)
20
+ expect(isSafeHref('/docs')).toBe(true)
21
+ expect(isSafeHref('#section')).toBe(true)
22
+ expect(isSafeHref('mailto:a@b.com')).toBe(true)
23
+ expect(isSafeHref('javascript:alert(1)')).toBe(false)
24
+ expect(isSafeImageSrc('https://cdn.example.com/a.png')).toBe(true)
25
+ expect(isSafeImageSrc('data:image/png;base64,xx')).toBe(true)
26
+ expect(isSafeImageSrc('javascript:alert(1)')).toBe(false)
27
+ })
28
+
29
+ it('renders GFM headings, lists and code safely', () => {
30
+ const html = renderMarkdown('# Hello\n\n- item\n\n```js\nconst a = 1\n```')
31
+ expect(html).toContain('<h1')
32
+ expect(html).toContain('<li>')
33
+ expect(html).toContain('rs-markdown__codeblock')
34
+ expect(html).toContain('const a = 1')
35
+ expect(html).not.toContain('<script')
36
+ })
37
+
38
+ it('strips javascript links and keeps link text', () => {
39
+ const html = renderMarkdown('[x](javascript:alert(1))')
40
+ expect(html).toContain('x')
41
+ expect(html).not.toContain('javascript:')
42
+ })
43
+
44
+ it('opens http links in a new tab with noopener', () => {
45
+ const html = renderMarkdown('[docs](https://example.com)')
46
+ expect(html).toContain('href="https://example.com"')
47
+ expect(html).toContain('target="_blank"')
48
+ expect(html).toContain('rel="noopener noreferrer"')
49
+ })
50
+
51
+ it('sanitizes raw HTML injection', () => {
52
+ const html = renderMarkdown('Hello <img src=x onerror=alert(1)>')
53
+ expect(html.toLowerCase()).not.toContain('onerror')
54
+ })
55
+
56
+ it('resolves height and readonly mode', () => {
57
+ expect(resolveMarkdownHeight()).toBe('16rem')
58
+ expect(resolveMarkdownHeight(240)).toBe('240px')
59
+ expect(resolveMarkdownHeight('12rem')).toBe('12rem')
60
+ expect(resolveMarkdownMode('edit', true)).toBe('preview')
61
+ expect(resolveMarkdownMode('split', false)).toBe('split')
62
+ expect(resolveMarkdownMode(undefined, false)).toBe('edit')
63
+ })
64
+ })
65
+
66
+ describe('RsMarkdown', () => {
67
+ it('renders mode toggle and editor by default', async () => {
68
+ const wrapper = mount(RsMarkdown, {
69
+ props: { modelValue: '# hi', height: 200 },
70
+ attachTo: document.body,
71
+ })
72
+ await new Promise((r) => setTimeout(r, 50))
73
+ expect(wrapper.find('.rs-markdown__toolbar').exists()).toBe(true)
74
+ expect(wrapper.find('.rs-markdown__editor').exists()).toBe(true)
75
+ expect(wrapper.classes()).toContain('rs-markdown--edit')
76
+ wrapper.unmount()
77
+ })
78
+
79
+ it('switches to preview and renders HTML', async () => {
80
+ const wrapper = mount(RsMarkdown, {
81
+ props: {
82
+ modelValue: '## Title\n\nHello',
83
+ mode: 'preview',
84
+ height: 180,
85
+ },
86
+ })
87
+ expect(wrapper.classes()).toContain('rs-markdown--preview')
88
+ expect(wrapper.find('.rs-markdown__prose').html()).toContain('<h2')
89
+ expect(wrapper.find('.rs-markdown__prose').text()).toContain('Hello')
90
+ })
91
+
92
+ it('supports split mode with editor and preview', async () => {
93
+ const wrapper = mount(RsMarkdown, {
94
+ props: {
95
+ modelValue: '**bold**',
96
+ mode: 'split',
97
+ height: 220,
98
+ },
99
+ attachTo: document.body,
100
+ })
101
+ await new Promise((r) => setTimeout(r, 50))
102
+ expect(wrapper.classes()).toContain('rs-markdown--split')
103
+ expect(wrapper.find('.rs-markdown__editor').exists()).toBe(true)
104
+ expect(wrapper.find('.rs-markdown__preview').exists()).toBe(true)
105
+ wrapper.unmount()
106
+ })
107
+
108
+ it('forces preview and hides toggle when readonly', () => {
109
+ const wrapper = mount(RsMarkdown, {
110
+ props: {
111
+ modelValue: 'note',
112
+ mode: 'edit',
113
+ readonly: true,
114
+ },
115
+ })
116
+ expect(wrapper.classes()).toContain('rs-markdown--preview')
117
+ expect(wrapper.find('.rs-markdown__toolbar').exists()).toBe(false)
118
+ expect(wrapper.find('.rs-markdown__editor').exists()).toBe(false)
119
+ })
120
+
121
+ it('shows empty state when preview has no content', () => {
122
+ const wrapper = mount(RsMarkdown, {
123
+ props: {
124
+ modelValue: ' ',
125
+ mode: 'preview',
126
+ },
127
+ })
128
+ expect(wrapper.find('.rs-markdown__empty').exists()).toBe(true)
129
+ })
130
+
131
+ it('emits mode updates from toolbar', async () => {
132
+ const wrapper = mount(RsMarkdown, {
133
+ props: {
134
+ modelValue: 'x',
135
+ mode: 'edit',
136
+ },
137
+ })
138
+ const buttons = wrapper.findAll('.rs-markdown__toolbar button')
139
+ expect(buttons.length).toBeGreaterThanOrEqual(2)
140
+ await buttons[1]!.trigger('click')
141
+ expect(wrapper.emitted('update:mode')?.[0]).toEqual(['preview'])
142
+ })
143
+ })
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { h } from 'vue'
4
+ import RsMenu from '../components/RsMenu.vue'
5
+ import RsConfigProvider from '../components/RsConfigProvider.vue'
6
+ import type { RsMenuItemGroup } from '../components/menu-utils'
7
+
8
+ describe('RsMenu', () => {
9
+ const flatItems = [
10
+ { key: 'dashboard', label: '仪表盘', icon: 'layout-dashboard' as const },
11
+ { key: 'chat', label: '对话', icon: 'message-square' as const },
12
+ { key: 'settings', label: '设置', icon: 'settings' as const },
13
+ ]
14
+
15
+ const groupedItems: RsMenuItemGroup[] = [
16
+ {
17
+ label: '工作区',
18
+ children: [
19
+ { key: 'projects', label: '项目', icon: 'folder' as const },
20
+ { key: 'docs', label: '文档', icon: 'search' as const },
21
+ ],
22
+ },
23
+ {
24
+ label: '系统',
25
+ children: [{ key: 'settings', label: '设置', icon: 'settings' as const }],
26
+ },
27
+ ]
28
+
29
+ const nestedItems = [
30
+ {
31
+ key: 'workspace',
32
+ label: '工作区',
33
+ icon: 'folder' as const,
34
+ children: [
35
+ { key: 'projects', label: '项目' },
36
+ { key: 'archived', label: '归档', disabled: true },
37
+ ],
38
+ },
39
+ { key: 'settings', label: '设置', icon: 'settings' as const },
40
+ ]
41
+
42
+ it('renders nav with menu aria-label', () => {
43
+ const wrapper = mount(RsMenu, {
44
+ props: { items: flatItems, modelValue: 'dashboard' },
45
+ })
46
+ expect(wrapper.find('nav.rs-menu').attributes('aria-label')).toBe('导航菜单')
47
+ })
48
+
49
+ it('uses en-US aria-label when locale is en-US', () => {
50
+ const wrapper = mount(RsConfigProvider, {
51
+ props: { locale: 'en-US' },
52
+ slots: {
53
+ default: () =>
54
+ h(RsMenu, {
55
+ items: flatItems,
56
+ modelValue: 'dashboard',
57
+ }),
58
+ },
59
+ })
60
+ expect(wrapper.find('nav.rs-menu').attributes('aria-label')).toBe('Navigation menu')
61
+ })
62
+
63
+ it('emits select and updates model when item clicked', async () => {
64
+ const wrapper = mount(RsMenu, {
65
+ props: { items: flatItems, modelValue: 'dashboard' },
66
+ })
67
+ const buttons = wrapper.findAll('.rs-menu__item')
68
+ const targetButton = buttons[1]
69
+ expect(targetButton).toBeDefined()
70
+ if (!targetButton) return
71
+ await targetButton.trigger('click')
72
+ expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['chat'])
73
+ expect(wrapper.emitted('select')?.[0]).toEqual(['chat'])
74
+ })
75
+
76
+ it('renders group labels', () => {
77
+ const wrapper = mount(RsMenu, {
78
+ props: { items: groupedItems, modelValue: 'projects' },
79
+ })
80
+ expect(wrapper.find('.rs-menu__group-label').text()).toBe('工作区')
81
+ })
82
+
83
+ it('auto-expands parent submenu for active nested item', () => {
84
+ const wrapper = mount(RsMenu, {
85
+ props: {
86
+ items: nestedItems,
87
+ modelValue: 'projects',
88
+ openKeys: [],
89
+ },
90
+ })
91
+ expect(wrapper.emitted('update:openKeys')?.pop()?.[0]).toContain('workspace')
92
+ })
93
+
94
+ it('does not emit select for disabled item', async () => {
95
+ const wrapper = mount(RsMenu, {
96
+ props: {
97
+ items: nestedItems,
98
+ modelValue: 'projects',
99
+ openKeys: ['workspace'],
100
+ },
101
+ })
102
+ const disabled = wrapper.find('.rs-menu__item:disabled')
103
+ expect(disabled.exists()).toBe(true)
104
+ await disabled.trigger('click')
105
+ expect(wrapper.emitted('select')).toBeUndefined()
106
+ })
107
+
108
+ it('does not render nested submenu list when collapsed', () => {
109
+ const wrapper = mount(RsMenu, {
110
+ props: {
111
+ items: nestedItems,
112
+ modelValue: 'settings',
113
+ openKeys: ['workspace'],
114
+ collapsed: true,
115
+ },
116
+ })
117
+ expect(wrapper.find('.rs-menu__list--nested').exists()).toBe(false)
118
+ })
119
+ })
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getPaginationRange, slicePageData } from '../components/pagination-utils'
3
+ import { formatFileSize, validateUploadFiles } from '../components/upload-utils'
4
+
5
+ describe('new Rs UI utilities', () => {
6
+ it('creates compact pagination ranges', () => {
7
+ expect(getPaginationRange(5, 10)).toEqual([1, 'ellipsis', 4, 5, 6, 'ellipsis', 10])
8
+ expect(slicePageData([1, 2, 3, 4], 2, 2)).toEqual([3, 4])
9
+ })
10
+
11
+ it('validates uploads', () => {
12
+ const file = new File(['hello'], 'hello.txt', { type: 'text/plain' })
13
+ const result = validateUploadFiles([file], { accept: '.txt', maxSize: 100 })
14
+ expect(result.accepted).toHaveLength(1)
15
+ expect(formatFileSize(1024)).toBe('1.0 KB')
16
+ })
17
+ })
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { h } from 'vue'
4
+ import RsPagination from '../components/RsPagination.vue'
5
+ import RsConfigProvider from '../components/RsConfigProvider.vue'
6
+ import RsSelect from '../components/RsSelect.vue'
7
+
8
+ describe('RsPagination', () => {
9
+ it('renders nav with pagination aria-label', () => {
10
+ const wrapper = mount(RsPagination, {
11
+ props: { total: 100, page: 1, pageSize: 10 },
12
+ })
13
+ expect(wrapper.find('nav.rs-pagination').attributes('aria-label')).toBe('分页')
14
+ })
15
+
16
+ it('uses en-US aria-label when locale is en-US', () => {
17
+ const wrapper = mount(RsConfigProvider, {
18
+ props: { locale: 'en-US' },
19
+ slots: {
20
+ default: () =>
21
+ h(RsPagination, {
22
+ total: 100,
23
+ page: 1,
24
+ pageSize: 10,
25
+ }),
26
+ },
27
+ })
28
+ expect(wrapper.find('nav.rs-pagination').attributes('aria-label')).toBe('Pagination')
29
+ })
30
+
31
+ it('renders summary text with total', () => {
32
+ const wrapper = mount(RsPagination, {
33
+ props: { total: 86, page: 1, pageSize: 20 },
34
+ })
35
+ expect(wrapper.find('.rs-pagination__summary').text()).toBe('共 86 条')
36
+ })
37
+
38
+ it('hides summary when showSummary is false', () => {
39
+ const wrapper = mount(RsPagination, {
40
+ props: { total: 86, page: 1, showSummary: false },
41
+ })
42
+ expect(wrapper.find('.rs-pagination__summary').exists()).toBe(false)
43
+ })
44
+
45
+ it('emits update:page when next is clicked', async () => {
46
+ const wrapper = mount(RsPagination, {
47
+ props: { total: 100, page: 1, pageSize: 10 },
48
+ })
49
+ const buttons = wrapper.findAll('button')
50
+ await buttons.at(-1)?.trigger('click')
51
+ expect(wrapper.emitted('update:page')?.[0]).toEqual([2])
52
+ })
53
+
54
+ it('emits update:page when a page number is clicked', async () => {
55
+ const wrapper = mount(RsPagination, {
56
+ props: { total: 50, page: 1, pageSize: 10 },
57
+ })
58
+ const pageButton = wrapper.findAll('button').find((btn) => btn.text() === '3')
59
+ expect(pageButton).toBeDefined()
60
+ await pageButton?.trigger('click')
61
+ expect(wrapper.emitted('update:page')?.pop()).toEqual([3])
62
+ })
63
+
64
+ it('disables prev on first page', () => {
65
+ const wrapper = mount(RsPagination, {
66
+ props: { total: 100, page: 1, pageSize: 10 },
67
+ })
68
+ expect(wrapper.findAll('button')[0].attributes('disabled')).toBeDefined()
69
+ })
70
+
71
+ it('disables next on last page', () => {
72
+ const wrapper = mount(RsPagination, {
73
+ props: { total: 100, page: 10, pageSize: 10 },
74
+ })
75
+ const buttons = wrapper.findAll('button')
76
+ expect(buttons.at(-1)?.attributes('disabled')).toBeDefined()
77
+ })
78
+
79
+ it('renders ellipsis for large page counts', () => {
80
+ const wrapper = mount(RsPagination, {
81
+ props: { total: 500, page: 12, pageSize: 20 },
82
+ })
83
+ expect(wrapper.find('.rs-pagination__ellipsis').exists()).toBe(true)
84
+ })
85
+
86
+ it('renders RsSelect for page size when showPageSize is true', () => {
87
+ const wrapper = mount(RsPagination, {
88
+ props: { total: 100, page: 1, pageSize: 20, showPageSize: true },
89
+ })
90
+ expect(wrapper.find('.rs-pagination__size .rs-select').exists()).toBe(true)
91
+ })
92
+
93
+ it('emits update:pageSize when page size changes', async () => {
94
+ const wrapper = mount(RsPagination, {
95
+ props: { total: 100, page: 1, pageSize: 20, showPageSize: true },
96
+ })
97
+ await wrapper.findComponent(RsSelect).setValue('50')
98
+ expect(wrapper.emitted('update:pageSize')?.pop()).toEqual([50])
99
+ })
100
+
101
+ it('clamps page when page size increases', async () => {
102
+ const wrapper = mount(RsPagination, {
103
+ props: { total: 100, page: 5, pageSize: 10, showPageSize: true },
104
+ })
105
+ await wrapper.findComponent(RsSelect).setValue('50')
106
+ expect(wrapper.emitted('update:page')?.pop()).toEqual([2])
107
+ })
108
+
109
+ it('does not emit update:page when disabled', async () => {
110
+ const wrapper = mount(RsPagination, {
111
+ props: { total: 100, page: 1, pageSize: 10, disabled: true },
112
+ })
113
+ await wrapper.findAll('button').at(-1)?.trigger('click')
114
+ expect(wrapper.emitted('update:page')).toBeUndefined()
115
+ })
116
+
117
+ it('renders quick jumper when showQuickJumper is true', () => {
118
+ const wrapper = mount(RsPagination, {
119
+ props: { total: 100, page: 1, pageSize: 10, showQuickJumper: true },
120
+ })
121
+ expect(wrapper.find('.rs-pagination__jumper').exists()).toBe(true)
122
+ expect(wrapper.find('.rs-pagination__jumper-input input').exists()).toBe(true)
123
+ })
124
+
125
+ it('hides quick jumper when only one page exists', () => {
126
+ const wrapper = mount(RsPagination, {
127
+ props: { total: 8, page: 1, pageSize: 10, showQuickJumper: true },
128
+ })
129
+ expect(wrapper.find('.rs-pagination__jumper').exists()).toBe(false)
130
+ })
131
+
132
+ it('jumps to page when confirm is clicked', async () => {
133
+ const wrapper = mount(RsPagination, {
134
+ props: { total: 100, page: 1, pageSize: 10, showQuickJumper: true },
135
+ })
136
+ const input = wrapper.find('.rs-pagination__jumper-input input')
137
+ await input.setValue('7')
138
+ const jumperBtn = wrapper
139
+ .findAll('button')
140
+ .find((btn) => btn.text() === '确定')
141
+ expect(jumperBtn).toBeDefined()
142
+ await jumperBtn?.trigger('click')
143
+ expect(wrapper.emitted('update:page')?.pop()).toEqual([7])
144
+ })
145
+
146
+ it('clamps jump target to valid page range', async () => {
147
+ const wrapper = mount(RsPagination, {
148
+ props: { total: 100, page: 1, pageSize: 10, showQuickJumper: true },
149
+ })
150
+ const input = wrapper.find('.rs-pagination__jumper-input input')
151
+ await input.setValue('99')
152
+ const jumperBtn = wrapper
153
+ .findAll('button')
154
+ .find((btn) => btn.text() === '确定')
155
+ await jumperBtn?.trigger('click')
156
+ expect(wrapper.emitted('update:page')?.pop()).toEqual([10])
157
+ })
158
+ })
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import RsPopover from '../components/RsPopover.vue'
4
+
5
+ describe('RsPopover', () => {
6
+ it('opens content on trigger click', async () => {
7
+ const wrapper = mount(RsPopover, {
8
+ slots: {
9
+ default: '<button type="button" class="trigger">Open</button>',
10
+ content: '<p class="panel">Popover body</p>',
11
+ },
12
+ attachTo: document.body,
13
+ })
14
+ expect(document.body.querySelector('.rs-popover__content')).toBeNull()
15
+ await wrapper.find('.trigger').trigger('click')
16
+ await flushPromises()
17
+ expect(document.body.querySelector('.rs-popover__content')).not.toBeNull()
18
+ expect(document.body.textContent).toContain('Popover body')
19
+ wrapper.unmount()
20
+ })
21
+
22
+ it('supports v-model:open', async () => {
23
+ const wrapper = mount(RsPopover, {
24
+ props: { open: true },
25
+ slots: {
26
+ default: '<button type="button" class="trigger">Open</button>',
27
+ content: '<p class="panel">Controlled</p>',
28
+ },
29
+ attachTo: document.body,
30
+ })
31
+ await flushPromises()
32
+ expect(document.body.querySelector('.rs-popover__content')).not.toBeNull()
33
+ await wrapper.setProps({ open: false })
34
+ await flushPromises()
35
+ expect(document.body.querySelector('.rs-popover__content')).toBeNull()
36
+ wrapper.unmount()
37
+ })
38
+
39
+ it('applies width modifier class', async () => {
40
+ const wrapper = mount(RsPopover, {
41
+ props: { open: true, width: 'lg' },
42
+ slots: {
43
+ default: '<button type="button" class="trigger">Open</button>',
44
+ content: '<p>Wide panel</p>',
45
+ },
46
+ attachTo: document.body,
47
+ })
48
+ await flushPromises()
49
+ expect(document.body.querySelector('.rs-popover__content--lg')).not.toBeNull()
50
+ wrapper.unmount()
51
+ })
52
+
53
+ it('lazy-mounts content when closed by default', async () => {
54
+ const wrapper = mount(RsPopover, {
55
+ slots: {
56
+ default: '<button type="button" class="trigger">Open</button>',
57
+ content: '<p class="marker">Lazy content</p>',
58
+ },
59
+ attachTo: document.body,
60
+ })
61
+ expect(document.body.querySelector('.marker')).toBeNull()
62
+ wrapper.unmount()
63
+ })
64
+
65
+ it('unmounts content after close with default lazyMount', async () => {
66
+ const wrapper = mount(RsPopover, {
67
+ slots: {
68
+ default: '<button type="button" class="trigger">Open</button>',
69
+ content: '<p class="marker">Toggle content</p>',
70
+ },
71
+ attachTo: document.body,
72
+ })
73
+ await wrapper.find('.trigger').trigger('click')
74
+ await flushPromises()
75
+ expect(document.body.querySelector('.marker')).not.toBeNull()
76
+ await wrapper.find('.trigger').trigger('click')
77
+ await flushPromises()
78
+ expect(document.body.querySelector('.marker')).toBeNull()
79
+ wrapper.unmount()
80
+ })
81
+
82
+ it('keeps content mounted when lazyMount is false and closed', async () => {
83
+ const wrapper = mount(RsPopover, {
84
+ props: { lazyMount: false },
85
+ slots: {
86
+ default: '<button type="button" class="trigger">Open</button>',
87
+ content: '<p class="marker">Eager content</p>',
88
+ },
89
+ attachTo: document.body,
90
+ })
91
+ await flushPromises()
92
+ expect(document.body.querySelector('.marker')).not.toBeNull()
93
+ wrapper.unmount()
94
+ })
95
+
96
+ it('keeps content mounted when forceMount is true and closed', async () => {
97
+ const wrapper = mount(RsPopover, {
98
+ props: { forceMount: true },
99
+ slots: {
100
+ default: '<button type="button" class="trigger">Open</button>',
101
+ content: '<p class="marker">Forced content</p>',
102
+ },
103
+ attachTo: document.body,
104
+ })
105
+ await flushPromises()
106
+ expect(document.body.querySelector('.marker')).not.toBeNull()
107
+ wrapper.unmount()
108
+ })
109
+
110
+ it('retains eager content in DOM after close when lazyMount is false', async () => {
111
+ const wrapper = mount(RsPopover, {
112
+ props: { lazyMount: false, open: true },
113
+ slots: {
114
+ default: '<button type="button" class="trigger">Open</button>',
115
+ content: '<p class="marker">Persistent content</p>',
116
+ },
117
+ attachTo: document.body,
118
+ })
119
+ await flushPromises()
120
+ expect(document.body.querySelector('.marker')).not.toBeNull()
121
+ await wrapper.setProps({ open: false })
122
+ await flushPromises()
123
+ expect(document.body.querySelector('.marker')).not.toBeNull()
124
+ wrapper.unmount()
125
+ })
126
+ })
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsScrollbar from '../components/RsScrollbar.vue'
4
+ import { ScrollAreaRoot, ScrollAreaScrollbar } from '../components/reka'
5
+
6
+ describe('RsScrollbar', () => {
7
+ it('renders slot content', () => {
8
+ const wrapper = mount(RsScrollbar, {
9
+ slots: {
10
+ default: '<div class="slot-content">滚动内容</div>',
11
+ },
12
+ })
13
+
14
+ expect(wrapper.find('.slot-content').exists()).toBe(true)
15
+ expect(wrapper.find('.rs-scrollbar').exists()).toBe(true)
16
+ })
17
+
18
+ it('applies root sizing props', () => {
19
+ const wrapper = mount(RsScrollbar, {
20
+ props: {
21
+ height: '10rem',
22
+ maxHeight: '16rem',
23
+ minHeight: '6rem',
24
+ },
25
+ })
26
+
27
+ const style = wrapper.get('.rs-scrollbar').attributes('style')
28
+ expect(style).toContain('height: 10rem;')
29
+ expect(style).toContain('max-height: 16rem;')
30
+ expect(style).toContain('min-height: 6rem;')
31
+ })
32
+
33
+ it('renders only vertical bar when orientation is vertical', () => {
34
+ const wrapper = mount(RsScrollbar, {
35
+ props: { orientation: 'vertical' },
36
+ })
37
+ const bars = wrapper.findAllComponents(ScrollAreaScrollbar)
38
+ expect(bars).toHaveLength(1)
39
+ expect(bars[0]?.props('orientation')).toBe('vertical')
40
+ })
41
+
42
+ it('forwards scroll behavior props to ScrollAreaRoot', () => {
43
+ const wrapper = mount(RsScrollbar, {
44
+ props: {
45
+ type: 'always',
46
+ scrollHideDelay: 400,
47
+ },
48
+ })
49
+ const root = wrapper.getComponent(ScrollAreaRoot)
50
+ expect(root.props('type')).toBe('always')
51
+ expect(root.props('scrollHideDelay')).toBe(400)
52
+ })
53
+ })