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.
- package/CHANGELOG.md +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- 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
|
-
|
|
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
|
-
|
|
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('
|
|
347
|
+
it('showHeader=false 时表头视觉隐藏但仍保留 th', () => {
|
|
333
348
|
const wrapper = mount(RsTable, {
|
|
334
349
|
props: { columns, data, showHeader: false },
|
|
335
350
|
})
|
|
336
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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('
|
|
329
|
+
it('makes movable tabs draggable without a handle', () => {
|
|
287
330
|
const wrapper = mountTabs({ draggable: true })
|
|
288
|
-
expect(wrapper.find('.rs-
|
|
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
|
|
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
|
})
|
|
@@ -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 },
|