niuma-ui 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -0,0 +1,83 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { nextTick } from 'vue'
3
+ import { describe, expect, it } from 'vitest'
4
+ import RsTable from '../components/RsTable.vue'
5
+
6
+ describe('RsTable a11y baseline', () => {
7
+ it('壳层 region + table grid 语义与行列计数', () => {
8
+ const wrapper = mount(RsTable, {
9
+ props: {
10
+ columns: [
11
+ { key: 'name', title: 'Name' },
12
+ { key: 'age', title: 'Age' },
13
+ ],
14
+ data: [
15
+ { id: '1', name: 'a', age: 1 },
16
+ { id: '2', name: 'b', age: 2 },
17
+ ],
18
+ rowKey: 'id',
19
+ ariaLabel: '用户列表',
20
+ },
21
+ })
22
+ const shell = wrapper.find('.rs-table-shell')
23
+ expect(shell.attributes('role')).toBe('region')
24
+ expect(shell.attributes('aria-label')).toBe('用户列表')
25
+ expect(shell.attributes('tabindex')).toBeUndefined()
26
+
27
+ const table = wrapper.find('table.rs-table__table')
28
+ expect(table.attributes('role')).toBe('grid')
29
+ expect(table.attributes('tabindex')).toBe('0')
30
+ expect(table.attributes('aria-label')).toBe('用户列表')
31
+ expect(table.attributes('aria-rowcount')).toBe('2')
32
+ expect(table.attributes('aria-colcount')).toBe('2')
33
+ expect(wrapper.find('tr.rs-table__row').attributes('role')).toBe('row')
34
+ expect(wrapper.find('td.rs-table__td--data').attributes('role')).toBe('gridcell')
35
+ wrapper.unmount()
36
+ })
37
+
38
+ it('loading 时 aria-busy', () => {
39
+ const wrapper = mount(RsTable, {
40
+ props: {
41
+ columns: [{ key: 'name', title: 'Name' }],
42
+ data: [],
43
+ loading: true,
44
+ },
45
+ })
46
+ expect(wrapper.find('.rs-table-shell').attributes('aria-busy')).toBe('true')
47
+ wrapper.unmount()
48
+ })
49
+
50
+ it('只读表方向键漫游 focus 格', async () => {
51
+ const wrapper = mount(RsTable, {
52
+ props: {
53
+ columns: [
54
+ { key: 'name', title: 'Name' },
55
+ { key: 'age', title: 'Age' },
56
+ ],
57
+ data: [
58
+ { id: '1', name: 'a', age: 1 },
59
+ { id: '2', name: 'b', age: 2 },
60
+ ],
61
+ rowKey: 'id',
62
+ ariaLabel: '键盘表',
63
+ },
64
+ attachTo: document.body,
65
+ })
66
+ const table = wrapper.find('table.rs-table__table')
67
+ await table.trigger('keydown', { key: 'ArrowDown' })
68
+ await nextTick()
69
+ expect(wrapper.find('.rs-table__td--focused').exists()).toBe(true)
70
+ expect(wrapper.find('.rs-table__td--focused').attributes('data-col-key')).toBe('name')
71
+
72
+ await table.trigger('keydown', { key: 'ArrowRight' })
73
+ await nextTick()
74
+ expect(wrapper.find('.rs-table__td--focused').attributes('data-col-key')).toBe('age')
75
+
76
+ await table.trigger('keydown', { key: 'ArrowDown' })
77
+ await nextTick()
78
+ const focusedRow = wrapper.find('.rs-table__td--focused').element.closest('tr')
79
+ expect(focusedRow?.getAttribute('data-row-key')).toBe('2')
80
+
81
+ wrapper.unmount()
82
+ })
83
+ })
@@ -0,0 +1,87 @@
1
+ /**
2
+ * DOM 结构基线(轻量「视觉回归」):锁定关键 a11y/壳层骨架,不引 Playwright。
3
+ * 只采集 tag + role + 结构类(rs-table / rs-table__*),忽略 --size/--theme 修饰,降低抖动。
4
+ */
5
+
6
+ import { mount } from '@vue/test-utils'
7
+ import { describe, expect, it } from 'vitest'
8
+ import RsTable from '../components/RsTable.vue'
9
+
10
+ function structuralClass(el: Element): string {
11
+ return [...el.classList]
12
+ .filter(
13
+ (c) =>
14
+ c === 'rs-table-shell' ||
15
+ c === 'rs-table' ||
16
+ c.startsWith('rs-table__'),
17
+ )
18
+ .sort()
19
+ .join('.')
20
+ }
21
+
22
+ function structureFingerprint(root: Element): string {
23
+ const parts: string[] = []
24
+ const walk = (el: Element, depth: number) => {
25
+ if (depth > 5) return
26
+ const tag = el.tagName.toLowerCase()
27
+ // 跳过纯展示文案节点,基线只关心结构
28
+ if (tag === 'span' && !el.getAttribute('role') && !structuralClass(el)) {
29
+ return
30
+ }
31
+ const role = el.getAttribute('role')
32
+ const cls = structuralClass(el)
33
+ const bits = [tag]
34
+ if (role) bits.push(`role=${role}`)
35
+ if (cls) bits.push(cls)
36
+ parts.push(`${' '.repeat(depth)}${bits.join(' ')}`)
37
+ for (const child of Array.from(el.children).slice(0, 16)) {
38
+ walk(child, depth + 1)
39
+ }
40
+ }
41
+ walk(root, 0)
42
+ return parts.join('\n')
43
+ }
44
+
45
+ describe('RsTable DOM 结构基线', () => {
46
+ it('只读表壳层 / grid / 行格骨架稳定', () => {
47
+ const wrapper = mount(RsTable, {
48
+ props: {
49
+ columns: [
50
+ { key: 'name', title: 'Name' },
51
+ { key: 'age', title: 'Age' },
52
+ ],
53
+ data: [
54
+ { id: '1', name: 'a', age: 1 },
55
+ { id: '2', name: 'b', age: 2 },
56
+ ],
57
+ rowKey: 'id',
58
+ ariaLabel: '基线表',
59
+ contextMenu: false,
60
+ cellTooltip: false,
61
+ },
62
+ })
63
+ const shell = wrapper.find('.rs-table-shell').element
64
+ const fp = structureFingerprint(shell)
65
+ expect(fp).toContain('div role=region rs-table-shell')
66
+ expect(fp).toContain('table role=grid rs-table__table')
67
+ expect(fp).toContain('tr role=row rs-table__row')
68
+ expect(fp).toContain('td role=gridcell')
69
+ expect(fp).toMatchInlineSnapshot(`
70
+ "div role=region rs-table-shell
71
+ div rs-table
72
+ table role=grid rs-table__table
73
+ thead rs-table__head
74
+ tr role=row
75
+ th role=columnheader rs-table__cell--left.rs-table__th
76
+ th role=columnheader rs-table__cell--left.rs-table__th
77
+ tbody
78
+ tr role=row rs-table__row
79
+ td role=gridcell rs-table__cell--left.rs-table__td.rs-table__td--data
80
+ td role=gridcell rs-table__cell--left.rs-table__td.rs-table__td--data
81
+ tr role=row rs-table__row
82
+ td role=gridcell rs-table__cell--left.rs-table__td.rs-table__td--data
83
+ td role=gridcell rs-table__cell--left.rs-table__td.rs-table__td--data"
84
+ `)
85
+ wrapper.unmount()
86
+ })
87
+ })
@@ -1,6 +1,7 @@
1
1
  import { mount } from '@vue/test-utils'
2
2
  import { describe, expect, it } from 'vitest'
3
3
  import RsTable from '../components/RsTable.vue'
4
+ import { assertWithinBudget, RS_TABLE_PERF_BUDGETS } from './perf-budgets'
4
5
 
5
6
  function makeRows(count: number) {
6
7
  return Array.from({ length: count }, (_, i) => ({
@@ -50,8 +51,7 @@ describe('RsTable performance baselines', () => {
50
51
  })
51
52
  const elapsed = performance.now() - started
52
53
  expect(wrapper.find('.rs-table').exists()).toBe(true)
53
- // jsdom 下偏松:主要防止回归爆炸(本机通常远低于此)
54
- expect(elapsed).toBeLessThan(8000)
54
+ assertWithinBudget(elapsed, RS_TABLE_PERF_BUDGETS.mountReadonly3k, 'mountReadonly3k')
55
55
  wrapper.unmount()
56
56
  })
57
57
 
@@ -76,12 +76,31 @@ describe('RsTable performance baselines', () => {
76
76
  },
77
77
  })
78
78
  const editableElapsed = performance.now() - editableStart
79
- // 编辑关闭时不挂编辑器,成本应接近只读(允许一定抖动)
80
- expect(editableElapsed).toBeLessThan(readonlyElapsed * 3 + 2000)
79
+ const limit =
80
+ readonlyElapsed * RS_TABLE_PERF_BUDGETS.editableVsReadonlyRatio +
81
+ RS_TABLE_PERF_BUDGETS.editableVsReadonlySlackMs
82
+ assertWithinBudget(editableElapsed, limit, 'editableVsReadonly')
81
83
  expect(editableWrapper.find('.rs-table-cell-editor').exists()).toBe(false)
82
84
  editableWrapper.unmount()
83
85
  })
84
86
 
87
+ it('dual tables mount within budget (isolation + ViewContext)', () => {
88
+ const data = makeRows(1500)
89
+ const started = performance.now()
90
+ const a = mount(RsTable, {
91
+ props: { columns, data, rowKey: 'id', virtual: true, height: 320, ariaLabel: 'A' },
92
+ })
93
+ const b = mount(RsTable, {
94
+ props: { columns, data, rowKey: 'id', virtual: true, height: 320, ariaLabel: 'B' },
95
+ })
96
+ const elapsed = performance.now() - started
97
+ expect(a.find('.rs-table-shell').attributes('aria-label')).toBe('A')
98
+ expect(b.find('.rs-table-shell').attributes('aria-label')).toBe('B')
99
+ assertWithinBudget(elapsed, RS_TABLE_PERF_BUDGETS.mountDual1k5, 'mountDual1k5')
100
+ a.unmount()
101
+ b.unmount()
102
+ })
103
+
85
104
  it('opening one editor keeps editor count at 1', async () => {
86
105
  const data = makeRows(500)
87
106
  const wrapper = mount(RsTable, {
@@ -1,4 +1,4 @@
1
- import { mount } from '@vue/test-utils'
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
  import { h } from 'vue'
4
4
  import RsTable from '../components/RsTable.vue'
@@ -163,6 +163,7 @@ describe('RsTable', () => {
163
163
  props: { columns, data: [] },
164
164
  })
165
165
  expect(wrapper.find('.rs-table__empty').text()).toBe('暂无数据')
166
+ expect(wrapper.find('.rs-table__empty-content').exists()).toBe(true)
166
167
  })
167
168
 
168
169
  it('renders custom empty slot', () => {
@@ -173,6 +174,20 @@ describe('RsTable', () => {
173
174
  expect(wrapper.find('.rs-table__empty').text()).toBe('暂无数据')
174
175
  })
175
176
 
177
+ it('wraps empty and loading placeholders in sticky viewport content', () => {
178
+ const empty = mount(RsTable, {
179
+ props: { columns, data: [] },
180
+ })
181
+ expect(empty.find('.rs-table__empty-content').exists()).toBe(true)
182
+ empty.unmount()
183
+
184
+ const loading = mount(RsTable, {
185
+ props: { columns, data, loading: true },
186
+ })
187
+ expect(loading.find('.rs-table__empty-content').text()).toBe('加载中…')
188
+ loading.unmount()
189
+ })
190
+
176
191
  it('applies compact and bordered modifiers', () => {
177
192
  const wrapper = mount(RsTable, {
178
193
  props: { columns, data, compact: true, bordered: false },
@@ -329,11 +344,16 @@ describe('RsTable', () => {
329
344
  expect(wrapper.find('.rs-table__sort--active .rs-icon').exists()).toBe(true)
330
345
  })
331
346
 
332
- it('hides header when showHeader is false', () => {
347
+ it('showHeader=false 时表头视觉隐藏但仍保留 th', () => {
333
348
  const wrapper = mount(RsTable, {
334
349
  props: { columns, data, showHeader: false },
335
350
  })
336
- expect(wrapper.find('thead').exists()).toBe(false)
351
+ const head = wrapper.find('thead.rs-table__head--sr-only')
352
+ expect(head.exists()).toBe(true)
353
+ expect(head.findAll('th').length).toBeGreaterThan(0)
354
+ expect(wrapper.find('thead.rs-table__head:not(.rs-table__head--sr-only)').exists()).toBe(
355
+ false,
356
+ )
337
357
  })
338
358
 
339
359
  it('applies ellipsis wrapper and overflow styles', () => {
@@ -856,6 +876,41 @@ describe('RsTable', () => {
856
876
  expect(wrapper.find('.expand-slot').exists()).toBe(true)
857
877
  })
858
878
 
879
+ it('renders tree rows with indent toggle on expand column', async () => {
880
+ const treeData = [
881
+ {
882
+ id: 'p',
883
+ name: 'Parent',
884
+ count: 1,
885
+ children: [
886
+ { id: 'c1', name: 'Child', count: 2 },
887
+ ],
888
+ },
889
+ ]
890
+ const wrapper = mount(RsTable, {
891
+ props: {
892
+ columns,
893
+ data: treeData,
894
+ rowKey: 'id',
895
+ treeConfig: {
896
+ childrenField: 'children',
897
+ expandColumnKey: 'name',
898
+ indent: 16,
899
+ },
900
+ expandedRowKeys: [] as string[],
901
+ 'onUpdate:expandedRowKeys': (keys: string[]) => wrapper.setProps({ expandedRowKeys: keys }),
902
+ },
903
+ })
904
+ expect(wrapper.find('.rs-table--tree').exists()).toBe(true)
905
+ expect(wrapper.findAll('.rs-table__row')).toHaveLength(1)
906
+ expect(wrapper.find('.rs-table__tree-toggle').exists()).toBe(true)
907
+ await wrapper.find('.rs-table__tree-toggle').trigger('click')
908
+ expect(wrapper.props('expandedRowKeys')).toEqual(['p'])
909
+ await wrapper.setProps({ expandedRowKeys: ['p'] })
910
+ expect(wrapper.findAll('.rs-table__row')).toHaveLength(2)
911
+ expect(wrapper.text()).toContain('Child')
912
+ })
913
+
859
914
  it('supports row selection without checkbox column', async () => {
860
915
  const wrapper = mount(RsTable, {
861
916
  props: {
@@ -1087,7 +1142,7 @@ describe('RsTable', () => {
1087
1142
  attachTo: document.body,
1088
1143
  })
1089
1144
  await wrapper.find('.rs-table__td--data').trigger('contextmenu')
1090
- await wrapper.vm.$nextTick()
1145
+ await flushPromises()
1091
1146
  const labels = [...document.body.querySelectorAll('.rs-context-menu__label')].map(
1092
1147
  (node) => node.textContent?.trim(),
1093
1148
  )
@@ -1104,22 +1159,22 @@ describe('RsTable', () => {
1104
1159
  attachTo: document.body,
1105
1160
  })
1106
1161
  await wrapper.find('.rs-table__td--data').trigger('contextmenu')
1107
- await wrapper.vm.$nextTick()
1162
+ await flushPromises()
1108
1163
  const items = document.body.querySelectorAll('.rs-context-menu__item')
1109
1164
  const copyCell = [...items].find((item) => item.textContent?.includes('复制单元格'))
1110
1165
  expect(copyCell).toBeTruthy()
1111
1166
  ;(copyCell as HTMLElement).click()
1112
- await wrapper.vm.$nextTick()
1167
+ await flushPromises()
1113
1168
  expect(copyMock).toHaveBeenCalledWith('B')
1114
1169
 
1115
1170
  copyMock.mockClear()
1116
1171
  await wrapper.find('.rs-table__td--data').trigger('contextmenu')
1117
- await wrapper.vm.$nextTick()
1172
+ await flushPromises()
1118
1173
  const copyRow = [...document.body.querySelectorAll('.rs-context-menu__item')].find((item) =>
1119
1174
  item.textContent?.includes('复制行'),
1120
1175
  )
1121
1176
  ;(copyRow as HTMLElement).click()
1122
- await wrapper.vm.$nextTick()
1177
+ await flushPromises()
1123
1178
  expect(copyMock).toHaveBeenCalledWith('B\t20')
1124
1179
  wrapper.unmount()
1125
1180
  })
@@ -0,0 +1,71 @@
1
+ import { defineComponent, h } from 'vue'
2
+ import { renderToString } from '@vue/server-renderer'
3
+ import { describe, expect, it } from 'vitest'
4
+ import RsTable from '../components/RsTable.vue'
5
+ import { createChartSeriesTableFeature } from '../components/table/table-features'
6
+ import { useRsTableHeadless } from '../composables/useRsTableHeadless'
7
+ import type { RsTableRowData } from '../components/table-utils'
8
+
9
+ type Row = RsTableRowData & { id: string; name: string; status: string }
10
+
11
+ const columns = [
12
+ { key: 'name', title: 'Name' },
13
+ { key: 'status', title: 'Status' },
14
+ ]
15
+
16
+ const rows: Row[] = [
17
+ { id: '1', name: 'a', status: 'ok' },
18
+ { id: '2', name: 'b', status: 'wait' },
19
+ ]
20
+
21
+ describe('RsTable SSR', () => {
22
+ it('renderToString 产出 grid 语义与行数据(轻量只读)', async () => {
23
+ const App = defineComponent({
24
+ setup() {
25
+ return () =>
26
+ h(RsTable, {
27
+ columns,
28
+ data: rows,
29
+ rowKey: 'id',
30
+ virtual: false,
31
+ editable: false,
32
+ contextMenu: false,
33
+ cellTooltip: false,
34
+ ariaLabel: 'SSR 表',
35
+ })
36
+ },
37
+ })
38
+ const html = await renderToString(h(App))
39
+ expect(html).toContain('role="region"')
40
+ expect(html).toContain('role="grid"')
41
+ expect(html).toContain('aria-label="SSR 表"')
42
+ expect(html).toContain('role="gridcell"')
43
+ expect(html).toContain('>a<')
44
+ expect(html).toContain('>b<')
45
+ })
46
+
47
+ it('useRsTableHeadless 在无 document 依赖下可算 viewRows + series', async () => {
48
+ let seriesIds: string[] = []
49
+ const Host = defineComponent({
50
+ setup() {
51
+ const { api } = useRsTableHeadless<Row>({
52
+ columns: () => columns,
53
+ data: () => rows,
54
+ rowKey: 'id',
55
+ features: () => [
56
+ createChartSeriesTableFeature({
57
+ seriesDefs: [{ id: 'by-status', kind: 'pie', categoryField: 'status' }],
58
+ onSeries: (series) => {
59
+ seriesIds = series.map((s) => s.id)
60
+ },
61
+ }),
62
+ ],
63
+ })
64
+ return () => h('div', { 'data-count': String(api.getViewRows().length) })
65
+ },
66
+ })
67
+ const html = await renderToString(h(Host))
68
+ expect(html).toContain('data-count="2"')
69
+ expect(seriesIds).toEqual(['by-status'])
70
+ })
71
+ })
@@ -0,0 +1,45 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+ import RsTable from '../components/RsTable.vue'
4
+
5
+ describe('RsTable summary', () => {
6
+ it('renders builtin summary row from column.summary', async () => {
7
+ const wrapper = mount(RsTable, {
8
+ props: {
9
+ showSummary: true,
10
+ summaryMode: 'client',
11
+ columns: [
12
+ { key: 'name', title: '名称' },
13
+ {
14
+ key: 'amount',
15
+ title: '金额',
16
+ summary: { type: 'sum' as const },
17
+ },
18
+ ],
19
+ data: [
20
+ { id: 1, name: 'A', amount: 10 },
21
+ { id: 2, name: 'B', amount: 5 },
22
+ ],
23
+ rowKey: 'id',
24
+ },
25
+ })
26
+ expect(wrapper.find('.rs-table__summary-row').exists()).toBe(true)
27
+ expect(wrapper.find('.rs-table__summary-row').text()).toContain('15')
28
+ wrapper.unmount()
29
+ })
30
+
31
+ it('prefers summary slot over builtin row', async () => {
32
+ const wrapper = mount(RsTable, {
33
+ props: {
34
+ showSummary: true,
35
+ columns: [{ key: 'name', title: '名称', summary: { type: 'count' as const } }],
36
+ data: [{ id: 1, name: 'A' }],
37
+ rowKey: 'id',
38
+ },
39
+ slots: { summary: () => 'custom-summary' },
40
+ })
41
+ expect(wrapper.text()).toContain('custom-summary')
42
+ expect(wrapper.find('.rs-table__summary-row').exists()).toBe(false)
43
+ wrapper.unmount()
44
+ })
45
+ })
@@ -3,7 +3,16 @@ import { flushPromises, mount } from '@vue/test-utils'
3
3
  import { h } from 'vue'
4
4
  import RsTabs from '../components/RsTabs.vue'
5
5
  import RsConfigProvider from '../components/RsConfigProvider.vue'
6
- import { getNextTabAfterClose, isTabClosable, isTabRenamable, reorderTabItems, resolveVisibleTabValues } from '../components/tabs-utils'
6
+ import {
7
+ getNextTabAfterBatchClose,
8
+ getNextTabAfterClose,
9
+ isTabClosable,
10
+ isTabFixed,
11
+ isTabRenamable,
12
+ reorderTabItems,
13
+ resolveTabsToClose,
14
+ resolveVisibleTabValues,
15
+ } from '../components/tabs-utils'
7
16
 
8
17
  const items = [
9
18
  { value: 'a', label: '标签 A' },
@@ -37,6 +46,34 @@ describe('tabs-utils', () => {
37
46
  expect(isTabClosable({ value: 'a', label: 'A', closable: true }, false)).toBe(true)
38
47
  })
39
48
 
49
+ it('isTabFixed and fixed forbids closable', () => {
50
+ expect(isTabFixed({ value: 'a', label: 'A', fixed: true })).toBe(true)
51
+ expect(isTabClosable({ value: 'a', label: 'A', fixed: true, closable: true }, true)).toBe(false)
52
+ })
53
+
54
+ it('resolveTabsToClose skips fixed tabs for batch actions', () => {
55
+ const items = [
56
+ { value: 'home', label: 'Home', fixed: true },
57
+ { value: 'a', label: 'A' },
58
+ { value: 'b', label: 'B' },
59
+ { value: 'c', label: 'C' },
60
+ ]
61
+ expect(resolveTabsToClose(items, 'others', 'b', true)).toEqual(['a', 'c'])
62
+ expect(resolveTabsToClose(items, 'left', 'b', true)).toEqual(['a'])
63
+ expect(resolveTabsToClose(items, 'right', 'b', true)).toEqual(['c'])
64
+ expect(resolveTabsToClose(items, 'all', 'b', true)).toEqual(['a', 'b', 'c'])
65
+ })
66
+
67
+ it('getNextTabAfterBatchClose keeps active when still present', () => {
68
+ const remaining = [
69
+ { value: 'home', label: 'Home' },
70
+ { value: 'b', label: 'B' },
71
+ ]
72
+ expect(getNextTabAfterBatchClose(remaining, 'b', 'b')).toBe('b')
73
+ expect(getNextTabAfterBatchClose(remaining, 'a', 'b')).toBe('b')
74
+ expect(getNextTabAfterBatchClose([], 'a')).toBeUndefined()
75
+ })
76
+
40
77
  it('getNextTabAfterClose prefers right neighbor then left', () => {
41
78
  const items = [
42
79
  { value: 'a', label: 'A' },
@@ -177,6 +214,12 @@ describe('RsTabs', () => {
177
214
  expect(wrapper.find('.panel-a').exists()).toBe(false)
178
215
  })
179
216
 
217
+ it('applies contentGap class for title-to-content spacing', () => {
218
+ const wrapper = mountTabs({ borderless: true, contentGap: 'xl' })
219
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--content-gap-xl')
220
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--borderless')
221
+ })
222
+
180
223
  it('wraps list and panels in a shared body container', () => {
181
224
  const wrapper = mountTabs()
182
225
  expect(wrapper.find('.rs-tabs__body').exists()).toBe(true)
@@ -283,15 +326,18 @@ describe('RsTabs', () => {
283
326
  expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--card')
284
327
  })
285
328
 
286
- it('renders drag handle when draggable', () => {
329
+ it('makes movable tabs draggable without a handle', () => {
287
330
  const wrapper = mountTabs({ draggable: true })
288
- expect(wrapper.find('.rs-tabs__drag-handle').exists()).toBe(true)
331
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--draggable')
332
+ expect(wrapper.find('.rs-tabs__drag-handle').exists()).toBe(false)
333
+ expect(wrapper.findAll('.rs-tabs__trigger--movable')).toHaveLength(items.length)
334
+ expect(wrapper.find('.rs-tabs__trigger--movable').attributes('draggable')).toBe('true')
289
335
  })
290
336
 
291
337
  it('emits reorder on drop', async () => {
292
338
  const wrapper = mountTabs({ draggable: true })
293
339
  const triggers = wrapper.findAll('.rs-tabs__trigger')
294
- await wrapper.findAll('.rs-tabs__drag-handle')[0]?.trigger('dragstart', {
340
+ await triggers[0]?.trigger('dragstart', {
295
341
  dataTransfer: { setData: () => {}, effectAllowed: 'move' },
296
342
  })
297
343
  await triggers[2]?.trigger('drop', { preventDefault: () => {} })
@@ -325,4 +371,56 @@ describe('RsTabs', () => {
325
371
  const wrapper = mountTabs({ overflow: 'dropdown' })
326
372
  expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--dropdown-overflow')
327
373
  })
374
+
375
+ it('hides close button and shows pin for fixed tabs', () => {
376
+ const wrapper = mount(RsTabs, {
377
+ props: {
378
+ items: [
379
+ { value: 'home', label: 'Home', fixed: true },
380
+ { value: 'a', label: 'A' },
381
+ ],
382
+ modelValue: 'home',
383
+ closable: true,
384
+ },
385
+ slots: {
386
+ home: () => h('p', 'Home'),
387
+ a: () => h('p', 'A'),
388
+ },
389
+ })
390
+ expect(wrapper.find('.rs-tabs__trigger--fixed').exists()).toBe(true)
391
+ expect(wrapper.find('.rs-tabs__pin').exists()).toBe(true)
392
+ expect(wrapper.findAll('.rs-tabs__close')).toHaveLength(1)
393
+ })
394
+
395
+ it('renders extra slot actions', () => {
396
+ const wrapper = mount(RsTabs, {
397
+ props: {
398
+ items,
399
+ modelValue: 'a',
400
+ panelless: true,
401
+ },
402
+ slots: {
403
+ extra: () => h('button', { class: 'extra-action' }, '刷新'),
404
+ },
405
+ })
406
+ expect(wrapper.find('.rs-tabs__extra .extra-action').exists()).toBe(true)
407
+ expect(wrapper.find('.rs-tabs').classes()).toContain('rs-tabs--has-extra')
408
+ })
409
+
410
+ it('blocks tab switch when beforeLeave returns false', async () => {
411
+ const wrapper = mountTabs({
412
+ beforeLeave: () => false,
413
+ })
414
+ await wrapper.findAll('.rs-tabs__trigger')[1]?.trigger('mousedown', { button: 0 })
415
+ await flushPromises()
416
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
417
+ })
418
+
419
+ it('emits closeBatch for middle-click close', async () => {
420
+ const wrapper = mountTabs({ modelValue: 'b', closable: true })
421
+ await wrapper.findAll('.rs-tabs__trigger')[1]?.trigger('auxclick', { button: 1 })
422
+ await flushPromises()
423
+ expect(wrapper.emitted('close')?.[0]).toEqual(['b'])
424
+ expect(wrapper.emitted('update:modelValue')?.pop()).toEqual(['c'])
425
+ })
328
426
  })
@@ -41,7 +41,7 @@ vi.mock('@xterm/addon-fit', () => ({
41
41
  },
42
42
  }))
43
43
 
44
- vi.mock('xterm', () => ({
44
+ vi.mock('@xterm/xterm', () => ({
45
45
  Terminal: class {
46
46
  cols = 80
47
47
  rows = 24
@@ -68,6 +68,17 @@ describe('RsTimePicker', () => {
68
68
  wrapper.unmount()
69
69
  })
70
70
 
71
+ it('applies size class on root and columns panel', async () => {
72
+ const wrapper = mount(RsTimePicker, {
73
+ props: { modelValue: '08:00', size: 'sm', open: true },
74
+ attachTo: document.body,
75
+ })
76
+ await flushPromises()
77
+ expect(wrapper.classes()).toContain('rs-time-picker--sm')
78
+ expect(document.body.querySelector('.rs-time-columns--sm')).not.toBeNull()
79
+ wrapper.unmount()
80
+ })
81
+
71
82
  it('disables trigger when disabled', () => {
72
83
  const wrapper = mount(RsTimePicker, {
73
84
  props: { modelValue: '10:00', disabled: true },