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,328 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import { h } from 'vue'
4
+ import RsTabs from '../components/RsTabs.vue'
5
+ import RsConfigProvider from '../components/RsConfigProvider.vue'
6
+ import { getNextTabAfterClose, isTabClosable, isTabRenamable, reorderTabItems, resolveVisibleTabValues } from '../components/tabs-utils'
7
+
8
+ const items = [
9
+ { value: 'a', label: '标签 A' },
10
+ { value: 'b', label: '标签 B', icon: 'settings' as const },
11
+ { value: 'c', label: '标签 C', badge: 9 },
12
+ ]
13
+
14
+ function mountTabs(
15
+ props: Record<string, unknown> = {},
16
+ slots?: Record<string, () => ReturnType<typeof h>>,
17
+ ) {
18
+ return mount(RsTabs, {
19
+ props: {
20
+ items,
21
+ modelValue: 'a',
22
+ ...props,
23
+ },
24
+ slots: {
25
+ a: () => h('p', { class: 'panel-a' }, '内容 A'),
26
+ b: () => h('p', { class: 'panel-b' }, '内容 B'),
27
+ c: () => h('p', { class: 'panel-c' }, '内容 C'),
28
+ ...slots,
29
+ },
30
+ })
31
+ }
32
+
33
+ describe('tabs-utils', () => {
34
+ it('isTabClosable respects per-item override and global default', () => {
35
+ expect(isTabClosable({ value: 'a', label: 'A' }, true)).toBe(true)
36
+ expect(isTabClosable({ value: 'a', label: 'A', closable: false }, true)).toBe(false)
37
+ expect(isTabClosable({ value: 'a', label: 'A', closable: true }, false)).toBe(true)
38
+ })
39
+
40
+ it('getNextTabAfterClose prefers right neighbor then left', () => {
41
+ const items = [
42
+ { value: 'a', label: 'A' },
43
+ { value: 'b', label: 'B' },
44
+ { value: 'c', label: 'C' },
45
+ ]
46
+ expect(getNextTabAfterClose(items, 'b', 'b')).toBe('c')
47
+ expect(getNextTabAfterClose(items, 'c', 'c')).toBe('b')
48
+ expect(getNextTabAfterClose(items, 'a', 'b')).toBe('b')
49
+ })
50
+
51
+ it('reorderTabItems moves drag item before drop target', () => {
52
+ const items = [
53
+ { value: 'a', label: 'A' },
54
+ { value: 'b', label: 'B' },
55
+ { value: 'c', label: 'C' },
56
+ ]
57
+ const next = reorderTabItems(items, 'c', 'a')
58
+ expect(next.map((item) => item.value)).toEqual(['c', 'a', 'b'])
59
+ })
60
+
61
+ it('isTabRenamable respects per-item override', () => {
62
+ expect(isTabRenamable({ value: 'a', label: 'A' }, true)).toBe(true)
63
+ expect(isTabRenamable({ value: 'a', label: 'A', renamable: false }, true)).toBe(false)
64
+ })
65
+
66
+ it('resolveVisibleTabValues keeps active tab visible when keepActiveVisible is true', () => {
67
+ const items = [
68
+ { value: 'a', label: 'A' },
69
+ { value: 'b', label: 'B' },
70
+ { value: 'c', label: 'C' },
71
+ ]
72
+ const widths = new Map([
73
+ ['a', 80],
74
+ ['b', 80],
75
+ ['c', 80],
76
+ ])
77
+ const visible = resolveVisibleTabValues(items, widths, 100, 'c', 0, {
78
+ keepActiveVisible: true,
79
+ })
80
+ expect(visible.has('c')).toBe(true)
81
+ })
82
+
83
+ it('resolveVisibleTabValues hides active tab from bar in dropdown mode', () => {
84
+ const items = [
85
+ { value: 'a', label: 'A' },
86
+ { value: 'b', label: 'B' },
87
+ { value: 'c', label: 'C' },
88
+ ]
89
+ const widths = new Map([
90
+ ['a', 80],
91
+ ['b', 80],
92
+ ['c', 80],
93
+ ])
94
+ const visible = resolveVisibleTabValues(items, widths, 100, 'c', 0, {
95
+ keepActiveVisible: false,
96
+ })
97
+ expect(visible.has('c')).toBe(false)
98
+ expect(visible.has('a')).toBe(true)
99
+ })
100
+ })
101
+
102
+ describe('RsTabs', () => {
103
+ it('renders root with tabs aria-label', () => {
104
+ const wrapper = mountTabs()
105
+ expect(wrapper.find('.rs-tabs').attributes('aria-label')).toBe('标签页')
106
+ })
107
+
108
+ it('uses en-US aria-label when locale is en-US', () => {
109
+ const wrapper = mount(RsConfigProvider, {
110
+ props: { locale: 'en-US' },
111
+ slots: {
112
+ default: () =>
113
+ h(RsTabs, {
114
+ items,
115
+ modelValue: 'a',
116
+ }),
117
+ },
118
+ })
119
+ expect(wrapper.find('.rs-tabs').attributes('aria-label')).toBe('Tabs')
120
+ })
121
+
122
+ it('renders all tab labels', () => {
123
+ const wrapper = mountTabs()
124
+ expect(wrapper.text()).toContain('标签 A')
125
+ expect(wrapper.text()).toContain('标签 B')
126
+ expect(wrapper.text()).toContain('标签 C')
127
+ })
128
+
129
+ it('renders icon and badge', () => {
130
+ const wrapper = mountTabs()
131
+ expect(wrapper.find('.rs-icon').exists()).toBe(true)
132
+ expect(wrapper.find('.rs-tabs__badge').text()).toBe('9')
133
+ })
134
+
135
+ it('marks active trigger with data-state active', () => {
136
+ const wrapper = mountTabs({ modelValue: 'b' })
137
+ const triggers = wrapper.findAll('.rs-tabs__trigger')
138
+ expect(triggers[0]?.attributes('data-state')).toBe('inactive')
139
+ expect(triggers[1]?.attributes('data-state')).toBe('active')
140
+ })
141
+
142
+ it('emits update:modelValue when another tab is clicked', async () => {
143
+ const wrapper = mountTabs()
144
+ await wrapper.findAll('.rs-tabs__trigger')[1]?.trigger('mousedown', { button: 0 })
145
+ await flushPromises()
146
+ expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['b'])
147
+ })
148
+
149
+ it('does not emit update:modelValue when disabled tab is clicked', async () => {
150
+ const wrapper = mount(RsTabs, {
151
+ props: {
152
+ items: [
153
+ { value: 'a', label: 'A' },
154
+ { value: 'b', label: 'B', disabled: true },
155
+ ],
156
+ modelValue: 'a',
157
+ },
158
+ slots: {
159
+ a: () => h('p', 'A'),
160
+ b: () => h('p', 'B'),
161
+ },
162
+ })
163
+ await wrapper.findAll('.rs-tabs__trigger')[1]?.trigger('mousedown', { button: 0 })
164
+ await flushPromises()
165
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
166
+ })
167
+
168
+ it('shows active panel content', () => {
169
+ const wrapper = mountTabs({ modelValue: 'a' })
170
+ expect(wrapper.find('.panel-a').exists()).toBe(true)
171
+ expect(wrapper.find('.panel-b').exists()).toBe(false)
172
+ })
173
+
174
+ it('hides panels when panelless is true', () => {
175
+ const wrapper = mountTabs({ panelless: true })
176
+ expect(wrapper.find('.rs-tabs__panel').exists()).toBe(false)
177
+ expect(wrapper.find('.panel-a').exists()).toBe(false)
178
+ })
179
+
180
+ it('wraps list and panels in a shared body container', () => {
181
+ const wrapper = mountTabs()
182
+ expect(wrapper.find('.rs-tabs__body').exists()).toBe(true)
183
+ expect(wrapper.find('.rs-tabs__panel-inner').exists()).toBe(true)
184
+ })
185
+
186
+ it('uses line variant by default', () => {
187
+ const wrapper = mountTabs()
188
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--line')
189
+ })
190
+
191
+ it('applies segmented variant class', () => {
192
+ const wrapper = mountTabs({ variant: 'segmented' })
193
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--segmented')
194
+ })
195
+
196
+ it('applies sm size class on root', () => {
197
+ const wrapper = mountTabs({ size: 'sm' })
198
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--sm')
199
+ })
200
+
201
+ it('applies md size class by default', () => {
202
+ const wrapper = mountTabs()
203
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--md')
204
+ })
205
+
206
+ it('renders close buttons when closable is true', () => {
207
+ const wrapper = mountTabs({ closable: true })
208
+ expect(wrapper.findAll('.rs-tabs__close')).toHaveLength(items.length)
209
+ })
210
+
211
+ it('hides close button when item sets closable false', () => {
212
+ const wrapper = mount(RsTabs, {
213
+ props: {
214
+ items: [
215
+ { value: 'a', label: 'A', closable: false },
216
+ { value: 'b', label: 'B' },
217
+ ],
218
+ modelValue: 'a',
219
+ closable: true,
220
+ },
221
+ slots: {
222
+ a: () => h('p', 'A'),
223
+ b: () => h('p', 'B'),
224
+ },
225
+ })
226
+ expect(wrapper.findAll('.rs-tabs__close')).toHaveLength(1)
227
+ })
228
+
229
+ it('emits close and switches active tab when closing current tab', async () => {
230
+ const wrapper = mountTabs({ modelValue: 'b', closable: true })
231
+ await wrapper.findAll('.rs-tabs__close')[1]?.trigger('click')
232
+ await flushPromises()
233
+ expect(wrapper.emitted('close')?.[0]).toEqual(['b'])
234
+ expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['c'])
235
+ })
236
+
237
+ it('does not emit close when item is disabled', async () => {
238
+ const wrapper = mount(RsTabs, {
239
+ props: {
240
+ items: [{ value: 'a', label: 'A', disabled: true }],
241
+ modelValue: 'a',
242
+ closable: true,
243
+ },
244
+ slots: { a: () => h('p', 'A') },
245
+ })
246
+ await wrapper.find('.rs-tabs__close').trigger('click')
247
+ expect(wrapper.emitted('close')).toBeUndefined()
248
+ })
249
+
250
+ it('renders add button and emits add when addable', async () => {
251
+ const wrapper = mountTabs({ addable: true })
252
+ expect(wrapper.find('.rs-tabs__add').exists()).toBe(true)
253
+ await wrapper.find('.rs-tabs__add').trigger('click')
254
+ expect(wrapper.emitted('add')?.length).toBe(1)
255
+ })
256
+
257
+ it('hides add button when maxCount is reached', () => {
258
+ const wrapper = mountTabs({ addable: true, maxCount: items.length })
259
+ expect(wrapper.find('.rs-tabs__add').exists()).toBe(false)
260
+ })
261
+
262
+ it('uses en-US labels for close and add buttons', () => {
263
+ const wrapper = mount(RsConfigProvider, {
264
+ props: { locale: 'en-US' },
265
+ slots: {
266
+ default: () =>
267
+ h(RsTabs, {
268
+ items: [{ value: 'a', label: 'Docs' }],
269
+ modelValue: 'a',
270
+ closable: true,
271
+ addable: true,
272
+ }, {
273
+ a: () => h('p', 'A'),
274
+ }),
275
+ },
276
+ })
277
+ expect(wrapper.find('.rs-tabs__close').attributes('aria-label')).toBe('Close Docs')
278
+ expect(wrapper.find('.rs-tabs__add').attributes('aria-label')).toBe('Add tab')
279
+ })
280
+
281
+ it('applies card variant class', () => {
282
+ const wrapper = mountTabs({ variant: 'card' })
283
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--card')
284
+ })
285
+
286
+ it('renders drag handle when draggable', () => {
287
+ const wrapper = mountTabs({ draggable: true })
288
+ expect(wrapper.find('.rs-tabs__drag-handle').exists()).toBe(true)
289
+ })
290
+
291
+ it('emits reorder on drop', async () => {
292
+ const wrapper = mountTabs({ draggable: true })
293
+ const triggers = wrapper.findAll('.rs-tabs__trigger')
294
+ await wrapper.findAll('.rs-tabs__drag-handle')[0]?.trigger('dragstart', {
295
+ dataTransfer: { setData: () => {}, effectAllowed: 'move' },
296
+ })
297
+ await triggers[2]?.trigger('drop', { preventDefault: () => {} })
298
+ expect(wrapper.emitted('reorder')?.[0]).toEqual(['a', 'c'])
299
+ })
300
+
301
+ it('enters rename mode on label double click when renamable', async () => {
302
+ const wrapper = mountTabs({ renamable: true })
303
+ await wrapper.find('.rs-tabs__label').trigger('dblclick')
304
+ await flushPromises()
305
+ expect(wrapper.find('.rs-tabs__rename-input').exists()).toBe(true)
306
+ })
307
+
308
+ it('emits rename on input blur', async () => {
309
+ const wrapper = mountTabs({ renamable: true })
310
+ await wrapper.find('.rs-tabs__label').trigger('dblclick')
311
+ await flushPromises()
312
+ const input = wrapper.find('.rs-tabs__rename-input')
313
+ await input.setValue('新标签 A')
314
+ await input.trigger('blur')
315
+ await flushPromises()
316
+ expect(wrapper.emitted('rename')?.[0]).toEqual(['a', '新标签 A'])
317
+ })
318
+
319
+ it('applies scrollable class when overflow is scroll', () => {
320
+ const wrapper = mountTabs({ overflow: 'scroll' })
321
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--scrollable')
322
+ })
323
+
324
+ it('applies dropdown overflow class when overflow is dropdown', () => {
325
+ const wrapper = mountTabs({ overflow: 'dropdown' })
326
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--dropdown-overflow')
327
+ })
328
+ })
@@ -0,0 +1,218 @@
1
+ import { nextTick } from 'vue'
2
+ import { mount } from '@vue/test-utils'
3
+ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import RsTerminal from '../components/RsTerminal.vue'
5
+ import { buildAnsiColorDemo, normalizeTerminalHexColor, resolveTerminalTheme } from '../components/terminal-utils'
6
+ import { readDocumentTheme } from '../components/code-editor-utils'
7
+
8
+ vi.mock('../utils/rs-clipboard', () => ({
9
+ beginClipboardPrefetch: vi.fn(),
10
+ copyTextToClipboard: vi.fn().mockResolvedValue(true),
11
+ readClipboardText: vi.fn().mockResolvedValue('pasted'),
12
+ }))
13
+
14
+ const fitMock = vi.fn()
15
+ const writeMock = vi.fn()
16
+ const clearMock = vi.fn()
17
+ const focusMock = vi.fn()
18
+ const disposeMock = vi.fn()
19
+ const selectAllMock = vi.fn()
20
+ const clearSelectionMock = vi.fn()
21
+ const hasSelectionMock = vi.fn(() => false)
22
+ const getSelectionMock = vi.fn(() => '')
23
+ const onDataHandlers: Array<(data: string) => void> = []
24
+ const onSelectionHandlers: Array<() => void> = []
25
+ const onRenderHandlers: Array<() => void> = []
26
+ const onScrollHandlers: Array<() => void> = []
27
+ let pasteMock = vi.fn()
28
+ let keyEventHandler: ((event: KeyboardEvent) => boolean) | null = null
29
+
30
+ const bufferMock = {
31
+ baseY: 0,
32
+ cursorY: 0,
33
+ viewportY: 0,
34
+ length: 1,
35
+ }
36
+
37
+ vi.mock('@xterm/addon-fit', () => ({
38
+ FitAddon: class {
39
+ fit = fitMock
40
+ proposeDimensions = () => ({ cols: 80, rows: 24 })
41
+ },
42
+ }))
43
+
44
+ vi.mock('xterm', () => ({
45
+ Terminal: class {
46
+ cols = 80
47
+ rows = 24
48
+ options: Record<string, unknown> = {}
49
+ buffer = {
50
+ active: bufferMock,
51
+ }
52
+ loadAddon() {
53
+ return undefined
54
+ }
55
+ open() {
56
+ return undefined
57
+ }
58
+ onData(handler: (data: string) => void) {
59
+ onDataHandlers.push(handler)
60
+ }
61
+ onSelectionChange(handler: () => void) {
62
+ onSelectionHandlers.push(handler)
63
+ }
64
+ onRender(handler: () => void) {
65
+ onRenderHandlers.push(handler)
66
+ return { dispose: vi.fn() }
67
+ }
68
+ onScroll(handler: () => void) {
69
+ onScrollHandlers.push(handler)
70
+ return { dispose: vi.fn() }
71
+ }
72
+ registerMarker() {
73
+ return { dispose: vi.fn() }
74
+ }
75
+ registerDecoration() {
76
+ return { dispose: vi.fn() }
77
+ }
78
+ attachCustomKeyEventHandler(handler: (event: KeyboardEvent) => boolean) {
79
+ keyEventHandler = handler
80
+ }
81
+ hasSelection = hasSelectionMock
82
+ getSelection = getSelectionMock
83
+ selectAll = selectAllMock
84
+ clearSelection = clearSelectionMock
85
+ paste = pasteMock
86
+ element = document.createElement('div')
87
+ write = writeMock
88
+ clear = clearMock
89
+ focus = focusMock
90
+ dispose = disposeMock
91
+ },
92
+ }))
93
+
94
+ beforeAll(() => {
95
+ class ResizeObserverMock {
96
+ observe() {
97
+ return undefined
98
+ }
99
+ disconnect() {
100
+ return undefined
101
+ }
102
+ }
103
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock)
104
+ })
105
+
106
+ describe('terminal-utils', () => {
107
+ it('builds ansi color demo text', () => {
108
+ const demo = buildAnsiColorDemo()
109
+ expect(demo).toContain('ANSI 16 colors')
110
+ expect(demo).toContain('\x1b[31m')
111
+ })
112
+
113
+ it('resolves document theme', () => {
114
+ document.documentElement.dataset.rsTheme = 'light'
115
+ expect(resolveTerminalTheme('auto')).toBe('light')
116
+ document.documentElement.dataset.rsTheme = 'dark'
117
+ expect(resolveTerminalTheme('auto')).toBe('dark')
118
+ })
119
+
120
+ it('normalizes terminal hex colors', () => {
121
+ expect(normalizeTerminalHexColor('#181818')).toBe('#181818')
122
+ expect(normalizeTerminalHexColor('#abc')).toBe('#aabbcc')
123
+ expect(normalizeTerminalHexColor('rgb(246, 246, 246)')).toBe('#f6f6f6')
124
+ expect(normalizeTerminalHexColor('rgb(248 248 248)')).toBe('#f8f8f8')
125
+ expect(normalizeTerminalHexColor('rgb(0 0 0)')).toBe('#000000')
126
+ })
127
+
128
+ it('detects tui refresh sequences', async () => {
129
+ const { containsTuiRefreshSequence } = await import('../components/terminal-utils')
130
+ expect(containsTuiRefreshSequence('\x1b[2J')).toBe(true)
131
+ expect(containsTuiRefreshSequence('\x1b[1;1H')).toBe(true)
132
+ expect(containsTuiRefreshSequence('plain log line')).toBe(false)
133
+ })
134
+
135
+ it('detects full screen clear sequences', async () => {
136
+ const { containsFullScreenClear, startsWithCursorHome } = await import('../components/terminal-utils')
137
+ expect(containsFullScreenClear('\x1b[2J')).toBe(true)
138
+ expect(containsFullScreenClear('\x1b[2;0H')).toBe(false)
139
+ expect(startsWithCursorHome('\x1b[Hsummary')).toBe(true)
140
+ expect(startsWithCursorHome(' \x1b[1;1H')).toBe(true)
141
+ expect(startsWithCursorHome('process line')).toBe(false)
142
+ })
143
+ })
144
+
145
+ describe('RsTerminal', () => {
146
+ beforeEach(() => {
147
+ pasteMock.mockClear()
148
+ selectAllMock.mockClear()
149
+ keyEventHandler = null
150
+ })
151
+
152
+ it('renders overlay text', () => {
153
+ const wrapper = mount(RsTerminal, {
154
+ props: {
155
+ overlay: '连接中...',
156
+ },
157
+ })
158
+ expect(wrapper.find('.rs-terminal__overlay').text()).toContain('连接中...')
159
+ })
160
+
161
+ it('emits terminal input data', async () => {
162
+ const wrapper = mount(RsTerminal)
163
+ await nextTick()
164
+ onDataHandlers.at(-1)?.('ls\r')
165
+ expect(wrapper.emitted('data')?.[0]).toEqual(['ls\r'])
166
+ })
167
+
168
+ it('exposes terminal helpers', async () => {
169
+ const wrapper = mount(RsTerminal)
170
+ await nextTick()
171
+ wrapper.vm.write('pwd\r\n')
172
+ wrapper.vm.clear()
173
+ wrapper.vm.focus()
174
+ wrapper.vm.selectAll()
175
+ expect(writeMock).toHaveBeenCalledWith('pwd\r\n')
176
+ expect(clearMock).toHaveBeenCalled()
177
+ expect(focusMock).toHaveBeenCalled()
178
+ expect(selectAllMock).toHaveBeenCalled()
179
+ })
180
+
181
+ it('handles select-all shortcut', async () => {
182
+ mount(RsTerminal, { props: { shortcuts: true } })
183
+ await nextTick()
184
+ const handled = keyEventHandler?.({
185
+ type: 'keydown',
186
+ metaKey: true,
187
+ ctrlKey: false,
188
+ key: 'a',
189
+ preventDefault: vi.fn(),
190
+ stopPropagation: vi.fn(),
191
+ } as unknown as KeyboardEvent)
192
+ expect(handled).toBe(false)
193
+ expect(selectAllMock).toHaveBeenCalledTimes(1)
194
+ })
195
+
196
+ it('pastes once on ctrl+v keydown and ignores keyup', async () => {
197
+ mount(RsTerminal, { props: { shortcuts: true } })
198
+ await nextTick()
199
+ const preventDefault = vi.fn()
200
+ const stopPropagation = vi.fn()
201
+ keyEventHandler?.({
202
+ type: 'keydown',
203
+ ctrlKey: true,
204
+ key: 'v',
205
+ preventDefault,
206
+ stopPropagation,
207
+ } as unknown as KeyboardEvent)
208
+ keyEventHandler?.({
209
+ type: 'keyup',
210
+ ctrlKey: true,
211
+ key: 'v',
212
+ } as unknown as KeyboardEvent)
213
+ await nextTick()
214
+ expect(pasteMock).toHaveBeenCalledTimes(1)
215
+ expect(preventDefault).toHaveBeenCalled()
216
+ expect(stopPropagation).toHaveBeenCalled()
217
+ })
218
+ })
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import RsTimePicker from '../components/RsTimePicker.vue'
4
+
5
+ describe('RsTimePicker', () => {
6
+ it('renders popover trigger with placeholder', () => {
7
+ const wrapper = mount(RsTimePicker, { props: { modelValue: '' } })
8
+ expect(wrapper.find('.rs-time-picker__trigger').exists()).toBe(true)
9
+ expect(wrapper.find('.rs-time-picker__value').text()).toContain('选择时间')
10
+ expect(wrapper.classes()).toContain('rs-field')
11
+ })
12
+
13
+ it('shows preset value on trigger when model is set', () => {
14
+ const wrapper = mount(RsTimePicker, {
15
+ props: { modelValue: '14:30' },
16
+ })
17
+ expect(wrapper.find('.rs-time-picker__trigger--placeholder').exists()).toBe(false)
18
+ expect(wrapper.find('.rs-time-picker__value').text()).toBe('14:30')
19
+ })
20
+
21
+ it('renders label, hint, and required marker', () => {
22
+ const wrapper = mount(RsTimePicker, {
23
+ props: {
24
+ modelValue: '',
25
+ label: '签到时间',
26
+ hint: '请选择时间',
27
+ required: true,
28
+ },
29
+ })
30
+ expect(wrapper.find('.rs-field__label').text()).toContain('签到时间')
31
+ expect(wrapper.find('.rs-field__required').exists()).toBe(true)
32
+ expect(wrapper.find('.rs-field__hint').text()).toBe('请选择时间')
33
+ })
34
+
35
+ it('applies left label layout class', () => {
36
+ const wrapper = mount(RsTimePicker, {
37
+ props: { modelValue: '', labelPosition: 'left' },
38
+ })
39
+ expect(wrapper.classes()).toContain('rs-field--label-left')
40
+ })
41
+
42
+ it('uses single trigger for range mode', () => {
43
+ const wrapper = mount(RsTimePicker, {
44
+ props: { modelValue: { start: '', end: '' }, range: true },
45
+ })
46
+ expect(wrapper.findAll('.rs-time-picker__trigger')).toHaveLength(1)
47
+ expect(wrapper.find('.rs-time-picker__value').text()).toContain('选择时间范围')
48
+ })
49
+
50
+ it('shows range label when range model is set', () => {
51
+ const wrapper = mount(RsTimePicker, {
52
+ props: {
53
+ modelValue: { start: '09:00', end: '18:00' },
54
+ range: true,
55
+ },
56
+ })
57
+ expect(wrapper.find('.rs-time-picker__value').text()).toContain('09:00')
58
+ expect(wrapper.find('.rs-time-picker__value').text()).toContain('18:00')
59
+ })
60
+
61
+ it('opens panel with scroll columns', async () => {
62
+ const wrapper = mount(RsTimePicker, {
63
+ props: { modelValue: '', open: true },
64
+ attachTo: document.body,
65
+ })
66
+ await flushPromises()
67
+ expect(document.body.querySelector('.rs-time-columns')).not.toBeNull()
68
+ wrapper.unmount()
69
+ })
70
+
71
+ it('disables trigger when disabled', () => {
72
+ const wrapper = mount(RsTimePicker, {
73
+ props: { modelValue: '10:00', disabled: true },
74
+ })
75
+ expect(wrapper.find('.rs-time-picker__trigger').attributes('disabled')).toBeDefined()
76
+ })
77
+ })