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,99 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsEmpty from '../components/RsEmpty.vue'
4
+
5
+ describe('RsEmpty', () => {
6
+ it('renders required description', () => {
7
+ const wrapper = mount(RsEmpty, {
8
+ props: { description: '暂无数据' },
9
+ })
10
+ expect(wrapper.find('.rs-empty__description').text()).toBe('暂无数据')
11
+ })
12
+
13
+ it('renders title when provided', () => {
14
+ const wrapper = mount(RsEmpty, {
15
+ props: { title: '还没有项目', description: '创建第一个项目。' },
16
+ })
17
+ expect(wrapper.find('.rs-empty__title').text()).toBe('还没有项目')
18
+ })
19
+
20
+ it('omits title element when title is not provided', () => {
21
+ const wrapper = mount(RsEmpty, {
22
+ props: { description: '暂无数据' },
23
+ })
24
+ expect(wrapper.find('.rs-empty__title').exists()).toBe(false)
25
+ })
26
+
27
+ it('applies description offset class when title is present', () => {
28
+ const wrapper = mount(RsEmpty, {
29
+ props: { title: '标题', description: '描述' },
30
+ })
31
+ expect(wrapper.find('.rs-empty__description').classes()).toContain('rs-empty__description--offset')
32
+ })
33
+
34
+ it('does not apply description offset class without title', () => {
35
+ const wrapper = mount(RsEmpty, {
36
+ props: { description: '描述' },
37
+ })
38
+ expect(wrapper.find('.rs-empty__description').classes()).not.toContain('rs-empty__description--offset')
39
+ })
40
+
41
+ it('renders icon slot', () => {
42
+ const wrapper = mount(RsEmpty, {
43
+ props: { description: '空状态' },
44
+ slots: { icon: '<span class="custom-icon">icon</span>' },
45
+ })
46
+ expect(wrapper.find('.rs-empty__icon .custom-icon').text()).toBe('icon')
47
+ })
48
+
49
+ it('renders default slot for actions', () => {
50
+ const wrapper = mount(RsEmpty, {
51
+ props: { description: '空状态' },
52
+ slots: { default: '<button type="button" class="action-btn">新建</button>' },
53
+ })
54
+ expect(wrapper.find('.action-btn').text()).toBe('新建')
55
+ })
56
+
57
+ it('renders root container with base class', () => {
58
+ const wrapper = mount(RsEmpty, {
59
+ props: { description: '空状态' },
60
+ })
61
+ expect(wrapper.classes()).toContain('rs-empty')
62
+ expect(wrapper.classes()).not.toContain('rs-empty--fill')
63
+ expect(wrapper.find('.rs-empty__text').exists()).toBe(true)
64
+ expect(wrapper.find('.rs-empty__icon').exists()).toBe(true)
65
+ })
66
+
67
+ it('applies fill class for flush square layout', () => {
68
+ const wrapper = mount(RsEmpty, {
69
+ props: { description: '空状态', fill: true },
70
+ })
71
+ expect(wrapper.classes()).toContain('rs-empty--fill')
72
+ })
73
+
74
+ it('applies icon radius via CSS variable', () => {
75
+ const wrapper = mount(RsEmpty, {
76
+ props: { description: '空状态', iconRadius: 'none' },
77
+ })
78
+ const style = wrapper.attributes('style') ?? ''
79
+ expect(style).toContain('--rs-empty-icon-radius')
80
+ expect(style).toMatch(/--rs-empty-icon-radius:\s*0/)
81
+ })
82
+
83
+ it('applies outer radius via CSS variable', () => {
84
+ const wrapper = mount(RsEmpty, {
85
+ props: { description: '空状态', radius: 'sm' },
86
+ })
87
+ const style = wrapper.attributes('style') ?? ''
88
+ expect(style).toContain('--rs-empty-radius')
89
+ expect(style).toContain('--rs-radius-sm')
90
+ })
91
+
92
+ it('forces outer radius to 0 when fill is true', () => {
93
+ const wrapper = mount(RsEmpty, {
94
+ props: { description: '空状态', fill: true, radius: 'lg' },
95
+ })
96
+ const style = wrapper.attributes('style') ?? ''
97
+ expect(style).toMatch(/--rs-empty-radius:\s*0/)
98
+ })
99
+ })
@@ -0,0 +1,173 @@
1
+ import { defineComponent, ref } from 'vue'
2
+ import { afterEach, describe, expect, it } from 'vitest'
3
+ import { flushPromises, mount } from '@vue/test-utils'
4
+ import RsForm from '../components/RsForm.vue'
5
+ import RsInput from '../components/RsInput.vue'
6
+ import RsSelect from '../components/RsSelect.vue'
7
+
8
+ const roleOptions = [
9
+ { label: '成员', value: 'member' },
10
+ { label: '管理员', value: 'admin' },
11
+ ]
12
+
13
+ describe('RsForm', () => {
14
+ afterEach(() => {
15
+ document.body.innerHTML = ''
16
+ })
17
+
18
+ it('renders form element with base class', () => {
19
+ const wrapper = mount(RsForm, {
20
+ slots: { default: '<p class="slot">内容</p>' },
21
+ })
22
+ expect(wrapper.element.tagName).toBe('FORM')
23
+ expect(wrapper.classes()).toContain('rs-form')
24
+ expect(wrapper.find('.slot').text()).toBe('内容')
25
+ })
26
+
27
+ it('applies gap, maxWidth, and labelPosition classes', () => {
28
+ const wrapper = mount(RsForm, {
29
+ props: { gap: 'lg', maxWidth: 'sm', labelPosition: 'left' },
30
+ })
31
+ expect(wrapper.classes()).toContain('rs-form--gap-lg')
32
+ expect(wrapper.classes()).toContain('rs-form--max-sm')
33
+ expect(wrapper.classes()).toContain('rs-form--label-left')
34
+ })
35
+
36
+ it('validate returns valid when all fields pass', async () => {
37
+ const Host = defineComponent({
38
+ components: { RsForm, RsInput },
39
+ setup() {
40
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
41
+ const email = ref('user@example.com')
42
+ return { formRef, email }
43
+ },
44
+ template: `
45
+ <RsForm ref="formRef">
46
+ <RsInput v-model="email" rule="email" required />
47
+ </RsForm>
48
+ `,
49
+ })
50
+ const wrapper = mount(Host)
51
+ await flushPromises()
52
+ const result = await wrapper.vm.formRef!.validate()
53
+ expect(result.valid).toBe(true)
54
+ wrapper.unmount()
55
+ })
56
+
57
+ it('validate returns invalid when a field fails', async () => {
58
+ const Host = defineComponent({
59
+ components: { RsForm, RsInput },
60
+ setup() {
61
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
62
+ const email = ref('not-an-email')
63
+ return { formRef, email }
64
+ },
65
+ template: `
66
+ <RsForm ref="formRef">
67
+ <RsInput v-model="email" rule="email" required />
68
+ </RsForm>
69
+ `,
70
+ })
71
+ const wrapper = mount(Host)
72
+ await flushPromises()
73
+ const result = await wrapper.vm.formRef!.validate()
74
+ expect(result.valid).toBe(false)
75
+ wrapper.unmount()
76
+ })
77
+
78
+ it('resetFields restores initial values', async () => {
79
+ const Host = defineComponent({
80
+ components: { RsForm, RsInput },
81
+ setup() {
82
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
83
+ const name = ref('初始值')
84
+ return { formRef, name }
85
+ },
86
+ template: `
87
+ <RsForm ref="formRef">
88
+ <RsInput v-model="name" />
89
+ </RsForm>
90
+ `,
91
+ })
92
+ const wrapper = mount(Host)
93
+ await flushPromises()
94
+ wrapper.vm.name = '已修改'
95
+ await flushPromises()
96
+ wrapper.vm.formRef!.resetFields()
97
+ await flushPromises()
98
+ expect(wrapper.vm.name).toBe('初始值')
99
+ wrapper.unmount()
100
+ })
101
+
102
+ it('clearValidation clears input error state', async () => {
103
+ const Host = defineComponent({
104
+ components: { RsForm, RsInput },
105
+ setup() {
106
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
107
+ const email = ref('bad')
108
+ return { formRef, email }
109
+ },
110
+ template: `
111
+ <RsForm ref="formRef">
112
+ <RsInput v-model="email" rule="email" required />
113
+ </RsForm>
114
+ `,
115
+ })
116
+ const wrapper = mount(Host)
117
+ await flushPromises()
118
+ await wrapper.vm.formRef!.validate()
119
+ await flushPromises()
120
+ expect(wrapper.find('.rs-input-group--invalid').exists()).toBe(true)
121
+ wrapper.vm.formRef!.clearValidation()
122
+ await flushPromises()
123
+ expect(wrapper.find('.rs-input-group--invalid').exists()).toBe(false)
124
+ wrapper.unmount()
125
+ })
126
+
127
+ it('propagates disabled to child inputs', async () => {
128
+ const wrapper = mount(RsForm, {
129
+ props: { disabled: true },
130
+ slots: { default: '<RsInput model-value="只读" />' },
131
+ global: { components: { RsInput } },
132
+ })
133
+ await flushPromises()
134
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined()
135
+ wrapper.unmount()
136
+ })
137
+
138
+ it('propagates size to child inputs without explicit size', async () => {
139
+ const wrapper = mount(RsForm, {
140
+ props: { size: 'sm' },
141
+ slots: { default: '<RsInput model-value="" />' },
142
+ global: { components: { RsInput } },
143
+ })
144
+ await flushPromises()
145
+ expect(wrapper.find('.rs-input-group--sm').exists()).toBe(true)
146
+ wrapper.unmount()
147
+ })
148
+
149
+ it('validates required select field', async () => {
150
+ const Host = defineComponent({
151
+ components: { RsForm, RsSelect },
152
+ setup() {
153
+ const formRef = ref<InstanceType<typeof RsForm> | null>(null)
154
+ const role = ref('')
155
+ return { formRef, role, roleOptions }
156
+ },
157
+ template: `
158
+ <RsForm ref="formRef">
159
+ <RsSelect v-model="role" :options="roleOptions" required />
160
+ </RsForm>
161
+ `,
162
+ })
163
+ const wrapper = mount(Host, { attachTo: document.body })
164
+ await flushPromises()
165
+ const invalid = await wrapper.vm.formRef!.validate()
166
+ expect(invalid.valid).toBe(false)
167
+ wrapper.vm.role = 'member'
168
+ await flushPromises()
169
+ const valid = await wrapper.vm.formRef!.validate()
170
+ expect(valid.valid).toBe(true)
171
+ wrapper.unmount()
172
+ })
173
+ })
@@ -0,0 +1,244 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import RsIcon from '../components/RsIcon.vue'
4
+ import {
5
+ isRsIconName,
6
+ lucideIconCount,
7
+ resolveLucideIcon,
8
+ rsCommonIconNames,
9
+ } from '../icons/registry'
10
+
11
+ describe('RsIcon', () => {
12
+ it('loads lucide icon module', () => {
13
+ const icon = resolveLucideIcon('house')
14
+ expect(icon).toBeTruthy()
15
+ })
16
+
17
+ it('loads custom api icon', () => {
18
+ const icon = resolveLucideIcon('api')
19
+ expect(icon).toBeTruthy()
20
+ const wrapper = mount(RsIcon, { props: { name: 'api' } })
21
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
22
+ expect(wrapper.find('.api-icon__label').exists()).toBe(true)
23
+ expect(wrapper.find('.api-icon__label').text()).toBe('API')
24
+ expect(isRsIconName('api')).toBe(true)
25
+ })
26
+
27
+ it('loads custom ftp icon', () => {
28
+ const icon = resolveLucideIcon('ftp')
29
+ expect(icon).toBeTruthy()
30
+ const wrapper = mount(RsIcon, { props: { name: 'ftp' } })
31
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
32
+ expect(wrapper.find('.ftp-icon__folder').exists()).toBe(true)
33
+ expect(wrapper.find('.ftp-icon__label').exists()).toBe(true)
34
+ expect(wrapper.find('.ftp-icon__label').text()).toBe('FTP')
35
+ expect(isRsIconName('ftp')).toBe(true)
36
+ })
37
+
38
+ it('loads custom mongodb icon', () => {
39
+ const icon = resolveLucideIcon('mongodb')
40
+ expect(icon).toBeTruthy()
41
+ const wrapper = mount(RsIcon, { props: { name: 'mongodb' } })
42
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
43
+ expect(wrapper.find('.mongodb-icon__mark').exists()).toBe(true)
44
+ expect(isRsIconName('mongodb')).toBe(true)
45
+ })
46
+
47
+ it('loads custom vastbase icon', () => {
48
+ const icon = resolveLucideIcon('vastbase')
49
+ expect(icon).toBeTruthy()
50
+ const wrapper = mount(RsIcon, { props: { name: 'vastbase' } })
51
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
52
+ expect(wrapper.find('.vastbase-icon__mark').exists()).toBe(true)
53
+ expect(wrapper.findAll('.vastbase-icon__mark')).toHaveLength(6)
54
+ expect(isRsIconName('vastbase')).toBe(true)
55
+ })
56
+
57
+ it('loads custom mysql icon', () => {
58
+ const icon = resolveLucideIcon('mysql')
59
+ expect(icon).toBeTruthy()
60
+ const wrapper = mount(RsIcon, { props: { name: 'mysql' } })
61
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
62
+ expect(wrapper.find('.mysql-icon__mark').exists()).toBe(true)
63
+ expect(isRsIconName('mysql')).toBe(true)
64
+ })
65
+
66
+ it('loads custom dameng icon', () => {
67
+ const icon = resolveLucideIcon('dameng')
68
+ expect(icon).toBeTruthy()
69
+ const wrapper = mount(RsIcon, { props: { name: 'dameng' } })
70
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
71
+ expect(wrapper.find('.dameng-icon__oval').exists()).toBe(true)
72
+ expect(wrapper.findAll('.dameng-icon__wave')).toHaveLength(3)
73
+ expect(isRsIconName('dameng')).toBe(true)
74
+ })
75
+
76
+ it('loads custom oracle icon', () => {
77
+ const icon = resolveLucideIcon('oracle')
78
+ expect(icon).toBeTruthy()
79
+ const wrapper = mount(RsIcon, { props: { name: 'oracle' } })
80
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
81
+ expect(wrapper.find('.oracle-icon__mark').exists()).toBe(true)
82
+ expect(isRsIconName('oracle')).toBe(true)
83
+ })
84
+
85
+ it('loads custom clickhouse icon', () => {
86
+ const icon = resolveLucideIcon('clickhouse')
87
+ expect(icon).toBeTruthy()
88
+ const wrapper = mount(RsIcon, { props: { name: 'clickhouse' } })
89
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
90
+ expect(wrapper.find('.clickhouse-icon__mark').exists()).toBe(true)
91
+ expect(isRsIconName('clickhouse')).toBe(true)
92
+ })
93
+
94
+ it('loads custom kingbase icon', () => {
95
+ const icon = resolveLucideIcon('kingbase')
96
+ expect(icon).toBeTruthy()
97
+ const wrapper = mount(RsIcon, { props: { name: 'kingbase' } })
98
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
99
+ expect(wrapper.find('.kingbase-icon__mark').exists()).toBe(true)
100
+ expect(isRsIconName('kingbase')).toBe(true)
101
+ })
102
+
103
+ it('loads custom sqlserver icon', () => {
104
+ const icon = resolveLucideIcon('sqlserver')
105
+ expect(icon).toBeTruthy()
106
+ const wrapper = mount(RsIcon, { props: { name: 'sqlserver' } })
107
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
108
+ expect(wrapper.find('.sqlserver-icon__mark').exists()).toBe(true)
109
+ expect(isRsIconName('sqlserver')).toBe(true)
110
+ })
111
+
112
+ it('loads arbitrary lucide icon by name', () => {
113
+ const icon = resolveLucideIcon('trash-2')
114
+ expect(icon).toBeTruthy()
115
+ })
116
+
117
+ it('renders svg for known icon', () => {
118
+ const wrapper = mount(RsIcon, { props: { name: 'house' } })
119
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
120
+ expect(wrapper.find('path').exists()).toBe(true)
121
+ })
122
+
123
+ it('renders lucide icon by kebab-case name', () => {
124
+ const wrapper = mount(RsIcon, { props: { name: 'trash-2' } })
125
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
126
+ })
127
+
128
+ it('renders all common icons', () => {
129
+ for (const name of rsCommonIconNames) {
130
+ const wrapper = mount(RsIcon, { props: { name } })
131
+ expect(wrapper.find('svg.rs-icon').exists(), `icon failed: ${name}`).toBe(true)
132
+ }
133
+ })
134
+
135
+ it('exposes lucide library size', () => {
136
+ expect(lucideIconCount).toBeGreaterThan(100)
137
+ expect(isRsIconName('trash-2')).toBe(true)
138
+ expect(isRsIconName('not-a-real-icon')).toBe(false)
139
+ })
140
+
141
+ it('renders multiple paths for complex icons', () => {
142
+ const wrapper = mount(RsIcon, { props: { name: 'loader' } })
143
+ expect(wrapper.findAll('path').length).toBeGreaterThan(1)
144
+ })
145
+
146
+ it('renders nothing for unknown icon', () => {
147
+ const wrapper = mount(RsIcon, { props: { name: 'not-a-real-icon' } })
148
+ expect(wrapper.find('svg').exists()).toBe(false)
149
+ })
150
+
151
+ it('applies numeric size', () => {
152
+ const wrapper = mount(RsIcon, { props: { name: 'house', size: 24 } })
153
+ expect(wrapper.find('svg').attributes('width')).toBe('24')
154
+ expect(wrapper.find('svg').attributes('height')).toBe('24')
155
+ })
156
+
157
+ it.each(['sm', 'md', 'lg'] as const)('applies %s size preset', (size) => {
158
+ const wrapper = mount(RsIcon, { props: { name: 'house', size } })
159
+ const expected = { sm: '14', md: '16', lg: '20' }[size]
160
+ expect(wrapper.find('svg').attributes('width')).toBe(expected)
161
+ })
162
+
163
+ it('applies css length size', () => {
164
+ const wrapper = mount(RsIcon, { props: { name: 'house', size: '1.5rem' } })
165
+ expect(wrapper.find('svg').attributes('style')).toContain('width: 1.5rem')
166
+ })
167
+
168
+ it('applies custom stroke width', () => {
169
+ const wrapper = mount(RsIcon, { props: { name: 'search', strokeWidth: 2.5 } })
170
+ expect(wrapper.find('svg').attributes('stroke-width')).toBe('2.5')
171
+ })
172
+
173
+ it('applies color style', () => {
174
+ const wrapper = mount(RsIcon, { props: { name: 'check', color: 'var(--rs-success)' } })
175
+ expect(wrapper.find('svg').attributes('style')).toContain('color: var(--rs-success)')
176
+ })
177
+
178
+ it('applies accent color to custom ftp icon', () => {
179
+ const wrapper = mount(RsIcon, { props: { name: 'ftp', color: '#34C759' } })
180
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-ftp-accent: #34C759')
181
+ })
182
+
183
+ it('applies accent color to custom mongodb icon', () => {
184
+ const wrapper = mount(RsIcon, { props: { name: 'mongodb', color: '#34C759' } })
185
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-mongodb-accent: #34C759')
186
+ })
187
+
188
+ it('applies accent color to custom vastbase icon', () => {
189
+ const wrapper = mount(RsIcon, { props: { name: 'vastbase', color: '#34C759' } })
190
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-vastbase-accent: #34C759')
191
+ })
192
+
193
+ it('applies accent color to custom mysql icon', () => {
194
+ const wrapper = mount(RsIcon, { props: { name: 'mysql', color: '#34C759' } })
195
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-mysql-accent: #34C759')
196
+ })
197
+
198
+ it('applies accent color to custom oracle icon', () => {
199
+ const wrapper = mount(RsIcon, { props: { name: 'oracle', color: '#34C759' } })
200
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-oracle-accent: #34C759')
201
+ })
202
+
203
+ it('applies accent color to custom sqlserver icon', () => {
204
+ const wrapper = mount(RsIcon, { props: { name: 'sqlserver', color: '#34C759' } })
205
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-sqlserver-accent: #34C759')
206
+ })
207
+
208
+ it('applies rotate transform', () => {
209
+ const wrapper = mount(RsIcon, { props: { name: 'chevron-right', rotate: 90 } })
210
+ expect(wrapper.find('svg').attributes('style')).toContain('rotate(90deg)')
211
+ })
212
+
213
+ it('applies horizontal flip', () => {
214
+ const wrapper = mount(RsIcon, { props: { name: 'chevron-right', flip: 'horizontal' } })
215
+ expect(wrapper.find('svg').attributes('style')).toContain('scaleX(-1)')
216
+ })
217
+
218
+ it('applies spin class', () => {
219
+ const wrapper = mount(RsIcon, { props: { name: 'loader', spin: true } })
220
+ expect(wrapper.find('svg').classes()).toContain('rs-icon--spin')
221
+ })
222
+
223
+ it('sets semantic attributes when label is provided', () => {
224
+ const wrapper = mount(RsIcon, { props: { name: 'settings', label: '设置' } })
225
+ expect(wrapper.find('svg').attributes('aria-label')).toBe('设置')
226
+ expect(wrapper.find('svg').attributes('role')).toBe('img')
227
+ expect(wrapper.find('svg').attributes('aria-hidden')).toBeUndefined()
228
+ })
229
+
230
+ it('hides decorative icon from assistive tech', () => {
231
+ const wrapper = mount(RsIcon, { props: { name: 'plus' } })
232
+ expect(wrapper.find('svg').attributes('aria-hidden')).toBe('true')
233
+ expect(wrapper.find('svg').attributes('role')).toBeUndefined()
234
+ })
235
+
236
+ it('merges external class onto svg', () => {
237
+ const wrapper = mount(RsIcon, {
238
+ props: { name: 'house' },
239
+ attrs: { class: 'custom-icon' },
240
+ })
241
+ expect(wrapper.find('svg').classes()).toContain('rs-icon')
242
+ expect(wrapper.find('svg').classes()).toContain('custom-icon')
243
+ })
244
+ })