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,172 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { h } from 'vue'
4
+ import RsBreadcrumb from '../components/RsBreadcrumb.vue'
5
+ import RsConfigProvider from '../components/RsConfigProvider.vue'
6
+
7
+ const threeLevelItems = [
8
+ { label: '首页', href: '/' },
9
+ { label: '应用', href: '/apps' },
10
+ { label: '详情' },
11
+ ]
12
+
13
+ describe('RsBreadcrumb', () => {
14
+ it('renders nav with breadcrumb aria-label', () => {
15
+ const wrapper = mount(RsBreadcrumb, {
16
+ props: { items: threeLevelItems },
17
+ })
18
+ expect(wrapper.find('nav.rs-breadcrumb').attributes('aria-label')).toBe('面包屑')
19
+ })
20
+
21
+ it('renders all items', () => {
22
+ const wrapper = mount(RsBreadcrumb, {
23
+ props: { items: threeLevelItems },
24
+ })
25
+ expect(wrapper.text()).toContain('首页')
26
+ expect(wrapper.text()).toContain('应用')
27
+ expect(wrapper.text()).toContain('详情')
28
+ })
29
+
30
+ it('renders single item without separator', () => {
31
+ const wrapper = mount(RsBreadcrumb, {
32
+ props: { items: [{ label: '仅一项' }] },
33
+ })
34
+ expect(wrapper.findAll('.rs-breadcrumb__item')).toHaveLength(1)
35
+ expect(wrapper.findAll('.rs-breadcrumb__sep')).toHaveLength(0)
36
+ expect(wrapper.text()).toBe('仅一项')
37
+ })
38
+
39
+ it('renders empty list when items is empty', () => {
40
+ const wrapper = mount(RsBreadcrumb, {
41
+ props: { items: [] },
42
+ })
43
+ expect(wrapper.findAll('.rs-breadcrumb__item')).toHaveLength(0)
44
+ expect(wrapper.find('.rs-breadcrumb__list').exists()).toBe(true)
45
+ })
46
+
47
+ it('renders href items as anchor with correct href', () => {
48
+ const wrapper = mount(RsBreadcrumb, {
49
+ props: {
50
+ items: [
51
+ { label: '首页', href: '/' },
52
+ { label: '当前页' },
53
+ ],
54
+ },
55
+ })
56
+ const anchors = wrapper.findAll('a.rs-breadcrumb__link')
57
+ expect(anchors).toHaveLength(1)
58
+ expect(anchors[0]?.text()).toBe('首页')
59
+ expect(anchors[0]?.attributes('href')).toBe('/')
60
+ })
61
+
62
+ it('renders plain items as span without href', () => {
63
+ const wrapper = mount(RsBreadcrumb, {
64
+ props: { items: [{ label: 'A' }, { label: 'B' }] },
65
+ })
66
+ const spans = wrapper.findAll('span.rs-breadcrumb__link')
67
+ expect(spans).toHaveLength(2)
68
+ expect(spans[0]?.text()).toBe('A')
69
+ expect(spans[1]?.text()).toBe('B')
70
+ })
71
+
72
+ it('renders anchor when only to is provided', () => {
73
+ const wrapper = mount(RsBreadcrumb, {
74
+ props: {
75
+ items: [{ label: '路由页', to: '/route' }, { label: '当前' }],
76
+ },
77
+ })
78
+ expect(wrapper.findAll('a.rs-breadcrumb__link')).toHaveLength(1)
79
+ expect(wrapper.findAll('span.rs-breadcrumb__link')).toHaveLength(1)
80
+ })
81
+
82
+ it('marks last item as current page', () => {
83
+ const wrapper = mount(RsBreadcrumb, {
84
+ props: { items: [{ label: 'A' }, { label: 'B' }] },
85
+ })
86
+ const links = wrapper.findAll('.rs-breadcrumb__link')
87
+ expect(links.at(-1)?.attributes('aria-current')).toBe('page')
88
+ })
89
+
90
+ it('does not set aria-current on non-last items', () => {
91
+ const wrapper = mount(RsBreadcrumb, {
92
+ props: { items: threeLevelItems },
93
+ })
94
+ const links = wrapper.findAll('.rs-breadcrumb__link')
95
+ expect(links[0]?.attributes('aria-current')).toBeUndefined()
96
+ expect(links[1]?.attributes('aria-current')).toBeUndefined()
97
+ expect(links[2]?.attributes('aria-current')).toBe('page')
98
+ })
99
+
100
+ it('applies current class only to last item', () => {
101
+ const wrapper = mount(RsBreadcrumb, {
102
+ props: { items: threeLevelItems },
103
+ })
104
+ const links = wrapper.findAll('.rs-breadcrumb__link')
105
+ expect(links[0]?.classes()).not.toContain('rs-breadcrumb__link--current')
106
+ expect(links[1]?.classes()).not.toContain('rs-breadcrumb__link--current')
107
+ expect(links[2]?.classes()).toContain('rs-breadcrumb__link--current')
108
+ })
109
+
110
+ it('renders separators between items but not after last', () => {
111
+ const wrapper = mount(RsBreadcrumb, {
112
+ props: { items: threeLevelItems },
113
+ })
114
+ expect(wrapper.findAll('.rs-breadcrumb__sep')).toHaveLength(2)
115
+ expect(wrapper.findAll('.rs-breadcrumb__item')).toHaveLength(3)
116
+ })
117
+
118
+ it('renders separator icon with accessible label', () => {
119
+ const wrapper = mount(RsBreadcrumb, {
120
+ props: { items: [{ label: 'A' }, { label: 'B' }] },
121
+ })
122
+ const sep = wrapper.find('.rs-breadcrumb__sep')
123
+ expect(sep.exists()).toBe(true)
124
+ expect(sep.attributes('aria-label')).toBe('分隔符')
125
+ })
126
+
127
+ it('renders i18n labels in en-US locale', () => {
128
+ const wrapper = mount(RsConfigProvider, {
129
+ props: { locale: 'en-US' },
130
+ slots: {
131
+ default: () =>
132
+ h(RsBreadcrumb, {
133
+ items: [{ label: 'Home', href: '/' }, { label: 'Docs' }],
134
+ }),
135
+ },
136
+ })
137
+ expect(wrapper.find('nav.rs-breadcrumb').attributes('aria-label')).toBe('Breadcrumb')
138
+ expect(wrapper.find('.rs-breadcrumb__sep').attributes('aria-label')).toBe('Separator')
139
+ })
140
+
141
+ it('uses href as rendered anchor href when both to and href exist', () => {
142
+ const wrapper = mount(RsBreadcrumb, {
143
+ props: {
144
+ items: [
145
+ { label: 'Entry', to: '/route-target', href: '/href-target' },
146
+ { label: 'Current' },
147
+ ],
148
+ },
149
+ })
150
+ const anchor = wrapper.find('a.rs-breadcrumb__link')
151
+ expect(anchor.exists()).toBe(true)
152
+ expect(anchor.attributes('href')).toBe('/href-target')
153
+ })
154
+
155
+ it('updates current item and separators after items prop changes', async () => {
156
+ const wrapper = mount(RsBreadcrumb, {
157
+ props: { items: [{ label: 'Root' }, { label: 'Leaf' }] },
158
+ })
159
+ expect(wrapper.findAll('.rs-breadcrumb__sep')).toHaveLength(1)
160
+ expect(wrapper.findAll('.rs-breadcrumb__link')[1]?.text()).toBe('Leaf')
161
+
162
+ await wrapper.setProps({
163
+ items: [{ label: 'Root' }, { label: 'Branch' }, { label: 'Leaf' }],
164
+ })
165
+
166
+ const links = wrapper.findAll('.rs-breadcrumb__link')
167
+ expect(wrapper.findAll('.rs-breadcrumb__sep')).toHaveLength(2)
168
+ expect(links[1]?.attributes('aria-current')).toBeUndefined()
169
+ expect(links[2]?.attributes('aria-current')).toBe('page')
170
+ expect(links[2]?.text()).toBe('Leaf')
171
+ })
172
+ })
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsButton from '../components/RsButton.vue'
4
+
5
+ describe('RsButton', () => {
6
+ it('renders slot', () => {
7
+ const wrapper = mount(RsButton, {
8
+ slots: { default: '确定' },
9
+ })
10
+ expect(wrapper.text()).toContain('确定')
11
+ })
12
+
13
+ it('applies size class', () => {
14
+ const wrapper = mount(RsButton, {
15
+ props: { size: 'lg' },
16
+ })
17
+ expect(wrapper.classes()).toContain('rs-btn--lg')
18
+ })
19
+
20
+ it('applies default variant class', () => {
21
+ const wrapper = mount(RsButton, {
22
+ props: { variant: 'default' },
23
+ })
24
+ expect(wrapper.classes()).toContain('rs-btn--default')
25
+ })
26
+
27
+ it('applies variant class', () => {
28
+ const wrapper = mount(RsButton, {
29
+ props: { variant: 'ghost' },
30
+ })
31
+ expect(wrapper.classes()).toContain('rs-btn--ghost')
32
+ })
33
+
34
+ it('renders prefix icon', () => {
35
+ const wrapper = mount(RsButton, {
36
+ props: { icon: 'plus' },
37
+ slots: { default: '新建' },
38
+ })
39
+ expect(wrapper.find('.rs-btn__icon').exists()).toBe(true)
40
+ expect(wrapper.text()).toContain('新建')
41
+ })
42
+
43
+ it('iconOnly with tooltip sets aria-label', () => {
44
+ const wrapper = mount(RsButton, {
45
+ props: { icon: 'search', iconOnly: true, tooltip: '搜索' },
46
+ })
47
+ expect(wrapper.classes()).toContain('rs-btn--icon-only')
48
+ expect(wrapper.find('.rs-btn__tooltip').text()).toBe('搜索')
49
+ expect(wrapper.attributes('aria-label')).toBe('搜索')
50
+ })
51
+
52
+ it('iconOnly uses slot text in floating tooltip when tooltip omitted', () => {
53
+ const wrapper = mount(RsButton, {
54
+ props: { icon: 'message-square', iconOnly: true },
55
+ slots: { default: '消息' },
56
+ })
57
+ expect(wrapper.find('.rs-btn__label').exists()).toBe(false)
58
+ expect(wrapper.find('.rs-btn__tooltip').text()).toBe('消息')
59
+ })
60
+
61
+ it('reveal-label mode keeps label in dom with reveal class', () => {
62
+ const wrapper = mount(RsButton, {
63
+ props: { icon: 'plus', revealLabel: true },
64
+ slots: { default: '新建对话' },
65
+ })
66
+ expect(wrapper.classes()).toContain('rs-btn--reveal-label')
67
+ expect(wrapper.find('.rs-btn__label--reveal').exists()).toBe(true)
68
+ expect(wrapper.text()).toContain('新建对话')
69
+ })
70
+
71
+ it('shows tooltip when tooltip prop is set with label', () => {
72
+ const wrapper = mount(RsButton, {
73
+ props: { icon: 'plus', tooltip: 'Ctrl+N' },
74
+ slots: { default: '新建' },
75
+ })
76
+ expect(wrapper.find('.rs-btn__tooltip').text()).toBe('Ctrl+N')
77
+ })
78
+
79
+ it('loading: applies class, keeps enabled, shows inline spinner and label', async () => {
80
+ const wrapper = mount(RsButton, {
81
+ props: { loading: true, variant: 'secondary' },
82
+ slots: { default: '测试连接' },
83
+ })
84
+ const btn = wrapper.find('button')
85
+ expect(btn.classes()).toContain('rs-btn--loading')
86
+ expect(btn.attributes('disabled')).toBeUndefined()
87
+ expect(btn.attributes('aria-busy')).toBe('true')
88
+ expect(btn.find('.rs-btn__spinner-ring').exists()).toBe(true)
89
+ expect(btn.text()).toContain('测试连接')
90
+ })
91
+
92
+ it('loading: locks min-width to avoid layout shift', async () => {
93
+ const wrapper = mount(RsButton, {
94
+ props: { loading: false, variant: 'default' },
95
+ slots: { default: '测试连接' },
96
+ attachTo: document.body,
97
+ })
98
+ const btn = wrapper.find('button').element as HTMLButtonElement
99
+ Object.defineProperty(btn, 'offsetWidth', { configurable: true, value: 120 })
100
+ await wrapper.setProps({ loading: true })
101
+ await wrapper.vm.$nextTick()
102
+ expect(btn.style.minWidth).toBe('120px')
103
+ await wrapper.setProps({ loading: false })
104
+ await wrapper.vm.$nextTick()
105
+ expect(btn.style.minWidth).toBe('')
106
+ wrapper.unmount()
107
+ })
108
+ })
@@ -0,0 +1,82 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+ import RsCalendarGrid from '../components/RsCalendarGrid.vue'
4
+
5
+ describe('RsCalendarGrid', () => {
6
+ it('renders month navigation and a 7×6 date grid', () => {
7
+ const wrapper = mount(RsCalendarGrid, {
8
+ props: { viewYear: 2025, viewMonth: 6 },
9
+ })
10
+ expect(wrapper.find('.rs-calendar-grid__table').exists()).toBe(true)
11
+ expect(wrapper.findAll('thead th')).toHaveLength(7)
12
+ expect(wrapper.findAll('.rs-calendar-grid__cell')).toHaveLength(42)
13
+ expect(wrapper.find('.rs-calendar-grid__title').text()).toContain('2025')
14
+ })
15
+
16
+ it('emits select when a cell is clicked', async () => {
17
+ const wrapper = mount(RsCalendarGrid, {
18
+ props: { viewYear: 2025, viewMonth: 6 },
19
+ })
20
+ const enabled = wrapper
21
+ .findAll('.rs-calendar-grid__cell')
22
+ .find((btn) => !btn.attributes('disabled'))
23
+ await enabled?.trigger('click')
24
+ expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({
25
+ year: expect.any(Number),
26
+ month: expect.any(Number),
27
+ day: expect.any(Number),
28
+ })
29
+ })
30
+
31
+ it('marks the selected date with selected class', () => {
32
+ const wrapper = mount(RsCalendarGrid, {
33
+ props: {
34
+ viewYear: 2025,
35
+ viewMonth: 6,
36
+ selected: { year: 2025, month: 6, day: 16 },
37
+ },
38
+ })
39
+ expect(wrapper.find('.rs-calendar-grid__cell--selected').exists()).toBe(true)
40
+ })
41
+
42
+ it('shows outside-month cells', () => {
43
+ const wrapper = mount(RsCalendarGrid, {
44
+ props: { viewYear: 2025, viewMonth: 6 },
45
+ })
46
+ expect(wrapper.findAll('.rs-calendar-grid__cell--outside').length).toBeGreaterThan(0)
47
+ })
48
+
49
+ it('disables dates listed in disabledDates', () => {
50
+ const wrapper = mount(RsCalendarGrid, {
51
+ props: {
52
+ viewYear: 2025,
53
+ viewMonth: 6,
54
+ disabledDates: ['2025-06-01', '2025-06-15', '2025-06-30'],
55
+ },
56
+ })
57
+ const disabledCount = wrapper
58
+ .findAll('.rs-calendar-grid__cell')
59
+ .filter((btn) => btn.attributes('disabled') !== undefined).length
60
+ expect(disabledCount).toBeGreaterThanOrEqual(3)
61
+ })
62
+
63
+ it('updates view month via navigation', async () => {
64
+ const wrapper = mount(RsCalendarGrid, {
65
+ props: { viewYear: 2025, viewMonth: 6 },
66
+ })
67
+ await wrapper.findAll('.rs-calendar-grid__nav')[1].trigger('click')
68
+ expect(wrapper.emitted('update:viewMonth')?.[0]).toEqual([7])
69
+ })
70
+
71
+ it('highlights range between start and end', () => {
72
+ const wrapper = mount(RsCalendarGrid, {
73
+ props: {
74
+ viewYear: 2025,
75
+ viewMonth: 6,
76
+ rangeStart: { year: 2025, month: 6, day: 10 },
77
+ rangeEnd: { year: 2025, month: 6, day: 12 },
78
+ },
79
+ })
80
+ expect(wrapper.findAll('.rs-calendar-grid__cell--in-range').length).toBeGreaterThan(0)
81
+ })
82
+ })
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsCard from '../components/RsCard.vue'
4
+
5
+ describe('RsCard', () => {
6
+ it('renders default slot in body', () => {
7
+ const wrapper = mount(RsCard, {
8
+ slots: { default: '<p>正文</p>' },
9
+ })
10
+ expect(wrapper.find('.rs-card__body').html()).toContain('正文')
11
+ })
12
+
13
+ it('renders as section by default', () => {
14
+ const wrapper = mount(RsCard, { slots: { default: '内容' } })
15
+ expect(wrapper.element.tagName).toBe('SECTION')
16
+ expect(wrapper.classes()).toContain('rs-card')
17
+ })
18
+
19
+ it('renders custom as element', () => {
20
+ const wrapper = mount(RsCard, {
21
+ props: { as: 'article' },
22
+ slots: { default: '内容' },
23
+ })
24
+ expect(wrapper.element.tagName).toBe('ARTICLE')
25
+ })
26
+
27
+ it('renders title and description in header', () => {
28
+ const wrapper = mount(RsCard, {
29
+ props: { title: '标题', description: '描述文字' },
30
+ slots: { default: '正文' },
31
+ })
32
+ expect(wrapper.find('.rs-card__title').text()).toBe('标题')
33
+ expect(wrapper.find('.rs-card__description').text()).toBe('描述文字')
34
+ expect(wrapper.find('header.rs-card__header').exists()).toBe(true)
35
+ })
36
+
37
+ it('omits header when no title, description, or header slot', () => {
38
+ const wrapper = mount(RsCard, { slots: { default: '仅正文' } })
39
+ expect(wrapper.find('header').exists()).toBe(false)
40
+ })
41
+
42
+ it('renders header slot instead of default heading', () => {
43
+ const wrapper = mount(RsCard, {
44
+ slots: {
45
+ header: '<div class="custom-h">自定义</div>',
46
+ default: '正文',
47
+ },
48
+ })
49
+ expect(wrapper.find('.custom-h').text()).toBe('自定义')
50
+ expect(wrapper.find('.rs-card__title').exists()).toBe(false)
51
+ })
52
+
53
+ it('renders actions slot in header', () => {
54
+ const wrapper = mount(RsCard, {
55
+ props: { title: '卡片' },
56
+ slots: {
57
+ actions: '<button type="button">操作</button>',
58
+ default: '正文',
59
+ },
60
+ })
61
+ expect(wrapper.find('.rs-card__actions button').text()).toBe('操作')
62
+ })
63
+
64
+ it('applies padded body class by default', () => {
65
+ const wrapper = mount(RsCard, { slots: { default: '内容' } })
66
+ expect(wrapper.find('.rs-card__body').classes()).toContain('rs-card__body--padded')
67
+ })
68
+
69
+ it('omits padded class when padding is false', () => {
70
+ const wrapper = mount(RsCard, {
71
+ props: { padding: false },
72
+ slots: { default: '内容' },
73
+ })
74
+ expect(wrapper.find('.rs-card__body').classes()).not.toContain('rs-card__body--padded')
75
+ })
76
+
77
+ it('applies elevated class when elevated is true', () => {
78
+ const wrapper = mount(RsCard, {
79
+ props: { elevated: true },
80
+ slots: { default: '内容' },
81
+ })
82
+ expect(wrapper.classes()).toContain('rs-card--elevated')
83
+ })
84
+
85
+ it('applies variant class', () => {
86
+ const wrapper = mount(RsCard, {
87
+ props: { variant: 'plain' },
88
+ slots: { default: '内容' },
89
+ })
90
+ expect(wrapper.classes()).toContain('rs-card--plain')
91
+ })
92
+
93
+ it('defaults to grouped variant', () => {
94
+ const wrapper = mount(RsCard, { slots: { default: '内容' } })
95
+ expect(wrapper.classes()).toContain('rs-card--grouped')
96
+ })
97
+ })
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsCheckbox from '../components/RsCheckbox.vue'
4
+
5
+ describe('RsCheckbox', () => {
6
+ it('toggles model on change', async () => {
7
+ const wrapper = mount(RsCheckbox, { props: { modelValue: false } })
8
+ await wrapper.find('input').setValue(true)
9
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true])
10
+ expect(wrapper.emitted('change')?.[0]).toEqual([true])
11
+ })
12
+
13
+ it('applies checked and size classes', async () => {
14
+ const wrapper = mount(RsCheckbox, {
15
+ props: { modelValue: true, size: 'sm' },
16
+ })
17
+ expect(wrapper.find('.rs-checkbox').classes()).toContain('rs-checkbox--checked')
18
+ expect(wrapper.find('.rs-checkbox').classes()).toContain('rs-checkbox--sm')
19
+ })
20
+
21
+ it('supports indeterminate visual state', () => {
22
+ const wrapper = mount(RsCheckbox, {
23
+ props: { modelValue: false, indeterminate: true },
24
+ })
25
+ expect(wrapper.find('.rs-checkbox').classes()).toContain('rs-checkbox--indeterminate')
26
+ expect(wrapper.find('input').attributes('aria-checked')).toBe('mixed')
27
+ })
28
+
29
+ it('does not emit when disabled', async () => {
30
+ const wrapper = mount(RsCheckbox, {
31
+ props: { modelValue: false, disabled: true },
32
+ })
33
+ await wrapper.find('input').setValue(true)
34
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
35
+ })
36
+ })
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsCodeEditor from '../components/RsCodeEditor.vue'
4
+ import {
5
+ resolveCodeEditorLanguage,
6
+ resolveCodeEditorSize,
7
+ resolveCodeEditorTheme,
8
+ } from '../components/code-editor-utils'
9
+
10
+ describe('RsCodeEditor', () => {
11
+ it('renders CodeMirror surface', async () => {
12
+ const wrapper = mount(RsCodeEditor, {
13
+ props: { modelValue: 'hello' },
14
+ attachTo: document.body,
15
+ })
16
+ await new Promise((r) => setTimeout(r, 50))
17
+ expect(wrapper.find('.rs-code-editor__surface').exists()).toBe(true)
18
+ wrapper.unmount()
19
+ })
20
+
21
+ it('shows resolved language label in toolbar', () => {
22
+ const wrapper = mount(RsCodeEditor, {
23
+ props: { language: 'typescript' },
24
+ })
25
+ expect(wrapper.find('.rs-code-editor__toolbar').text()).toContain('TypeScript')
26
+ })
27
+
28
+ it('applies theme class', () => {
29
+ const wrapper = mount(RsCodeEditor, {
30
+ props: { theme: 'dark' },
31
+ })
32
+ expect(wrapper.classes()).toContain('rs-code-editor--dark')
33
+ })
34
+
35
+ it('sets height from number prop', () => {
36
+ const wrapper = mount(RsCodeEditor, {
37
+ props: { height: 200 },
38
+ })
39
+ expect(wrapper.attributes('style')).toContain('height: 200px')
40
+ })
41
+
42
+ it('supports embedded square chrome without consumer deep styles', () => {
43
+ const wrapper = mount(RsCodeEditor, {
44
+ props: {
45
+ embedded: true,
46
+ rounded: false,
47
+ foldGutter: false,
48
+ gutterWidth: 40,
49
+ showToolbar: false,
50
+ },
51
+ })
52
+ expect(wrapper.classes()).toContain('rs-code-editor--embedded')
53
+ expect(wrapper.classes()).toContain('rs-code-editor--square')
54
+ expect(wrapper.classes()).toContain('rs-code-editor--no-fold')
55
+ expect(wrapper.classes()).toContain('rs-code-editor--gutter-fixed')
56
+ expect(wrapper.attributes('style')).toContain('--rs-code-editor-gutter-width: 40px')
57
+ })
58
+
59
+ it('keeps rounded corners by default', () => {
60
+ const wrapper = mount(RsCodeEditor, {
61
+ props: { modelValue: 'select 1' },
62
+ })
63
+ expect(wrapper.classes()).not.toContain('rs-code-editor--square')
64
+ expect(wrapper.classes()).not.toContain('rs-code-editor--embedded')
65
+ })
66
+
67
+ it('renders diagnostics with severity classes', () => {
68
+ const wrapper = mount(RsCodeEditor, {
69
+ props: {
70
+ diagnostics: [
71
+ { message: 'Syntax error', line: 1, column: 3, severity: 'error' },
72
+ { message: 'Unused var', severity: 'warning' },
73
+ ],
74
+ },
75
+ })
76
+ const items = wrapper.findAll('.rs-code-editor__diagnostics li')
77
+ expect(items).toHaveLength(2)
78
+ expect(items[0].text()).toContain('1:3')
79
+ expect(items[0].text()).toContain('Syntax error')
80
+ expect(items[0].classes()).toContain('rs-code-editor__diagnostic--error')
81
+ expect(items[1].classes()).toContain('rs-code-editor__diagnostic--warning')
82
+ })
83
+
84
+ it('renders toolbar slot', () => {
85
+ const wrapper = mount(RsCodeEditor, {
86
+ slots: { toolbar: '<button type="button" class="save-btn">Save</button>' },
87
+ })
88
+ expect(wrapper.find('.save-btn').exists()).toBe(true)
89
+ })
90
+ })
91
+
92
+ describe('code-editor-utils', () => {
93
+ it('resolves supported languages', () => {
94
+ expect(resolveCodeEditorLanguage('json')).toBe('json')
95
+ expect(resolveCodeEditorLanguage('go')).toBe('go')
96
+ expect(resolveCodeEditorLanguage('xml')).toBe('xml')
97
+ expect(resolveCodeEditorLanguage('unknown')).toBe('plaintext')
98
+ })
99
+
100
+ it('resolves theme', () => {
101
+ document.documentElement.dataset.rsTheme = 'light'
102
+ expect(resolveCodeEditorTheme('auto')).toBe('light')
103
+ expect(resolveCodeEditorTheme('dark')).toBe('dark')
104
+ })
105
+
106
+ it('resolves editor height', () => {
107
+ expect(resolveCodeEditorSize()).toBe('20rem')
108
+ expect(resolveCodeEditorSize(240)).toBe('240px')
109
+ expect(resolveCodeEditorSize('16rem')).toBe('16rem')
110
+ })
111
+ })