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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- package/vite-plugins/silence-antlr-parse-console.ts +29 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
applySplitResize,
|
|
4
|
+
collapseSplitPane,
|
|
5
|
+
expandSplitPane,
|
|
6
|
+
isSplitPaneCollapsed,
|
|
7
|
+
normalizeSplitSizes,
|
|
8
|
+
resolveSplitConstraints,
|
|
9
|
+
splitSizesEqual,
|
|
10
|
+
type RsSplitPaneItem,
|
|
11
|
+
} from '../components/split-pane-utils'
|
|
12
|
+
|
|
13
|
+
const sum = (values: number[]) => values.reduce((acc, value) => acc + value, 0)
|
|
14
|
+
|
|
15
|
+
describe('resolveSplitConstraints', () => {
|
|
16
|
+
it('applies defaults and clamps ranges', () => {
|
|
17
|
+
const result = resolveSplitConstraints([{ key: 'a' }])
|
|
18
|
+
expect(result[0]).toEqual({ min: 0, max: 100, collapsible: false, collapsedSize: 0 })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('orders swapped min/max and caps collapsedSize at min', () => {
|
|
22
|
+
const result = resolveSplitConstraints([
|
|
23
|
+
{ key: 'a', min: 60, max: 20, collapsible: true, collapsedSize: 40 },
|
|
24
|
+
])
|
|
25
|
+
expect(result[0]).toEqual({ min: 20, max: 60, collapsible: true, collapsedSize: 20 })
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('normalizeSplitSizes', () => {
|
|
30
|
+
it('distributes remaining space to unspecified panes', () => {
|
|
31
|
+
const panes: RsSplitPaneItem[] = [{ key: 'a', size: 40 }, { key: 'b' }, { key: 'c' }]
|
|
32
|
+
const sizes = normalizeSplitSizes(panes)
|
|
33
|
+
expect(sum(sizes)).toBeCloseTo(100)
|
|
34
|
+
expect(sizes[0]).toBeCloseTo(40)
|
|
35
|
+
expect(sizes[1]).toBeCloseTo(30)
|
|
36
|
+
expect(sizes[2]).toBeCloseTo(30)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('rescales values that do not sum to 100', () => {
|
|
40
|
+
const sizes = normalizeSplitSizes([{ key: 'a', size: 1 }, { key: 'b', size: 3 }])
|
|
41
|
+
expect(sizes).toEqual([25, 75])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('prefers provided (controlled) sizes over pane defaults', () => {
|
|
45
|
+
const panes: RsSplitPaneItem[] = [{ key: 'a', size: 20 }, { key: 'b', size: 80 }]
|
|
46
|
+
const sizes = normalizeSplitSizes(panes, [50, 50])
|
|
47
|
+
expect(sizes).toEqual([50, 50])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('falls back to equal split when all sizes are zero', () => {
|
|
51
|
+
const sizes = normalizeSplitSizes([{ key: 'a', size: 0 }, { key: 'b', size: 0 }])
|
|
52
|
+
expect(sizes).toEqual([50, 50])
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('applySplitResize', () => {
|
|
57
|
+
const constraints = resolveSplitConstraints([{ key: 'a' }, { key: 'b' }])
|
|
58
|
+
|
|
59
|
+
it('moves space between adjacent panes and preserves total', () => {
|
|
60
|
+
const next = applySplitResize([50, 50], constraints, 0, 10)
|
|
61
|
+
expect(next).toEqual([60, 40])
|
|
62
|
+
expect(sum(next)).toBe(100)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('is immutable', () => {
|
|
66
|
+
const input = [50, 50]
|
|
67
|
+
const next = applySplitResize(input, constraints, 0, 10)
|
|
68
|
+
expect(input).toEqual([50, 50])
|
|
69
|
+
expect(next).not.toBe(input)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('respects min/max of both neighbors', () => {
|
|
73
|
+
const bounded = resolveSplitConstraints([
|
|
74
|
+
{ key: 'a', min: 30 },
|
|
75
|
+
{ key: 'b', min: 30 },
|
|
76
|
+
])
|
|
77
|
+
expect(applySplitResize([50, 50], bounded, 0, -40)).toEqual([30, 70])
|
|
78
|
+
expect(applySplitResize([50, 50], bounded, 0, 40)).toEqual([70, 30])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('ignores dividers that are out of range', () => {
|
|
82
|
+
expect(applySplitResize([50, 50], constraints, 1, 10)).toEqual([50, 50])
|
|
83
|
+
expect(applySplitResize([50, 50], constraints, -1, 10)).toEqual([50, 50])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('snaps a collapsible pane to collapsedSize past the halfway threshold', () => {
|
|
87
|
+
const collapsible = resolveSplitConstraints([
|
|
88
|
+
{ key: 'a', min: 20, collapsible: true, collapsedSize: 0 },
|
|
89
|
+
{ key: 'b' },
|
|
90
|
+
])
|
|
91
|
+
// 50 - 45 = 5 < (20+0)/2 = 10 → 折叠到 0
|
|
92
|
+
expect(applySplitResize([50, 50], collapsible, 0, -45)).toEqual([0, 100])
|
|
93
|
+
// 50 - 38 = 12 > 10 → 夹到 min 20
|
|
94
|
+
expect(applySplitResize([50, 50], collapsible, 0, -38)).toEqual([20, 80])
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('collapseSplitPane / expandSplitPane', () => {
|
|
99
|
+
const constraints = resolveSplitConstraints([
|
|
100
|
+
{ key: 'a', min: 20, collapsible: true, collapsedSize: 0 },
|
|
101
|
+
{ key: 'b' },
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
it('collapses to collapsedSize and gives space to the neighbor', () => {
|
|
105
|
+
expect(collapseSplitPane([40, 60], constraints, 0)).toEqual([0, 100])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('expands back to min by default', () => {
|
|
109
|
+
expect(expandSplitPane([0, 100], constraints, 0)).toEqual([20, 80])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('expands to an explicit size clamped within bounds', () => {
|
|
113
|
+
expect(expandSplitPane([0, 100], constraints, 0, 35)).toEqual([35, 65])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('isSplitPaneCollapsed', () => {
|
|
118
|
+
const [collapsible, plain] = resolveSplitConstraints([
|
|
119
|
+
{ key: 'a', collapsible: true, collapsedSize: 0 },
|
|
120
|
+
{ key: 'b' },
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
it('is true only for collapsible panes at collapsedSize', () => {
|
|
124
|
+
expect(isSplitPaneCollapsed(0, collapsible)).toBe(true)
|
|
125
|
+
expect(isSplitPaneCollapsed(10, collapsible)).toBe(false)
|
|
126
|
+
expect(isSplitPaneCollapsed(0, plain)).toBe(false)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('splitSizesEqual', () => {
|
|
131
|
+
it('compares with tolerance', () => {
|
|
132
|
+
expect(splitSizesEqual([50, 50], [50.005, 49.995])).toBe(true)
|
|
133
|
+
expect(splitSizesEqual([50, 50], [60, 40])).toBe(false)
|
|
134
|
+
expect(splitSizesEqual([50], [50, 50])).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
resolveDragRowKeys,
|
|
4
|
+
resolveTableRowDropPosition,
|
|
5
|
+
} from '../components/table-drag'
|
|
6
|
+
|
|
7
|
+
describe('table-drag', () => {
|
|
8
|
+
it('resolves reorder drop position from pointer', () => {
|
|
9
|
+
const element = {
|
|
10
|
+
getBoundingClientRect: () => ({ top: 0, height: 100 }),
|
|
11
|
+
} as HTMLElement
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
resolveTableRowDropPosition({ clientY: 20 } as MouseEvent, element, 'reorder'),
|
|
15
|
+
).toBe('before')
|
|
16
|
+
expect(
|
|
17
|
+
resolveTableRowDropPosition({ clientY: 80 } as MouseEvent, element, 'reorder'),
|
|
18
|
+
).toBe('after')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('uses into position for folder drop mode', () => {
|
|
22
|
+
const element = {
|
|
23
|
+
getBoundingClientRect: () => ({ top: 0, height: 100 }),
|
|
24
|
+
} as HTMLElement
|
|
25
|
+
|
|
26
|
+
expect(
|
|
27
|
+
resolveTableRowDropPosition({ clientY: 20 } as MouseEvent, element, 'into'),
|
|
28
|
+
).toBe('into')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('drags all selected rows when dragging a selected row', () => {
|
|
32
|
+
expect(resolveDragRowKeys('b', ['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
|
|
33
|
+
expect(resolveDragRowKeys('d', ['a', 'b', 'c'])).toEqual(['d'])
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { h, isVNode } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
RS_TABLE_NULL_DRAFT,
|
|
5
|
+
booleanToEditText,
|
|
6
|
+
coerceBoolean,
|
|
7
|
+
isBooleanToggleColumn,
|
|
8
|
+
isNullDraft,
|
|
9
|
+
navigateEditableCell,
|
|
10
|
+
parseCellEditInput,
|
|
11
|
+
parseClipboardGrid,
|
|
12
|
+
plainTextFromCellDisplay,
|
|
13
|
+
resolveColumnCommitOn,
|
|
14
|
+
resolveColumnDisplayContent,
|
|
15
|
+
resolveColumnEditTrigger,
|
|
16
|
+
resolveColumnRawValue,
|
|
17
|
+
stripNumberDecorations,
|
|
18
|
+
resolveCellEditText,
|
|
19
|
+
usesOverlayEditor,
|
|
20
|
+
validateCellValue,
|
|
21
|
+
} from '../components/table/table-edit-utils'
|
|
22
|
+
|
|
23
|
+
describe('table-edit-utils editors', () => {
|
|
24
|
+
it('normalizes ISO datetime for date picker binding', () => {
|
|
25
|
+
expect(
|
|
26
|
+
resolveCellEditText(
|
|
27
|
+
{ updatedAt: '2026-06-15T10:30:00' },
|
|
28
|
+
{ key: 'updatedAt', title: '更新时间', valueType: 'datetime' },
|
|
29
|
+
0,
|
|
30
|
+
),
|
|
31
|
+
).toBe('2026-06-15 10:30:00')
|
|
32
|
+
expect(
|
|
33
|
+
resolveCellEditText(
|
|
34
|
+
{ birthday: '2024-01-15' },
|
|
35
|
+
{ key: 'birthday', title: '生日', valueType: 'date' },
|
|
36
|
+
0,
|
|
37
|
+
),
|
|
38
|
+
).toBe('2024-01-15')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('edits UTC ISO as local and writes back without losing ms when unchanged', () => {
|
|
42
|
+
const raw = '2026-06-25T08:06:36.2263624Z'
|
|
43
|
+
const col = { key: 'addTime', title: 'addTime', valueType: 'datetime' as const }
|
|
44
|
+
const draft = resolveCellEditText({ addTime: raw }, col, 0)
|
|
45
|
+
const d = new Date('2026-06-25T08:06:36.226Z')
|
|
46
|
+
const pad = (n: number, w = 2) => String(n).padStart(w, '0')
|
|
47
|
+
expect(draft).toBe(
|
|
48
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
49
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`,
|
|
50
|
+
)
|
|
51
|
+
// 选择器通常去掉毫秒:秒级未变应还原原始串
|
|
52
|
+
const withoutMs = draft.replace(/\.\d+$/, '')
|
|
53
|
+
expect(parseCellEditInput(withoutMs, { addTime: raw }, col, 0)).toBe(raw)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('stringifies object cell values for edit draft', () => {
|
|
57
|
+
const binary = { $binary: '566h55CG5ZGYMDAx' }
|
|
58
|
+
expect(
|
|
59
|
+
resolveCellEditText(
|
|
60
|
+
{ userName: binary },
|
|
61
|
+
{ key: 'userName', title: 'userName' },
|
|
62
|
+
0,
|
|
63
|
+
),
|
|
64
|
+
).toBe(JSON.stringify(binary))
|
|
65
|
+
expect(
|
|
66
|
+
resolveCellEditText(
|
|
67
|
+
{ userName: binary },
|
|
68
|
+
{
|
|
69
|
+
key: 'userName',
|
|
70
|
+
title: 'userName',
|
|
71
|
+
formatter: (v) => (typeof v === 'object' ? JSON.stringify(v) : String(v)),
|
|
72
|
+
},
|
|
73
|
+
0,
|
|
74
|
+
),
|
|
75
|
+
).toBe(JSON.stringify(binary))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('detects boolean toggle columns', () => {
|
|
79
|
+
expect(isBooleanToggleColumn({ key: 'a', title: 'A', valueType: 'boolean' })).toBe(true)
|
|
80
|
+
expect(isBooleanToggleColumn({ key: 'a', title: 'A', valueType: 'text' })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('uses change commit and click trigger for overlay editors', () => {
|
|
84
|
+
expect(usesOverlayEditor('select')).toBe(true)
|
|
85
|
+
expect(usesOverlayEditor('date')).toBe(true)
|
|
86
|
+
expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'select' }, false)).toBe(
|
|
87
|
+
'change',
|
|
88
|
+
)
|
|
89
|
+
expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'date' }, false)).toBe('enter')
|
|
90
|
+
expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'datetime' }, false)).toBe(
|
|
91
|
+
'enter',
|
|
92
|
+
)
|
|
93
|
+
expect(resolveColumnEditTrigger({ key: 'k', title: 'K', valueType: 'select' }, 'dblclick')).toBe(
|
|
94
|
+
'click',
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('parses boolean edit text', () => {
|
|
99
|
+
expect(coerceBoolean(true)).toBe(true)
|
|
100
|
+
expect(coerceBoolean('YES')).toBe(true)
|
|
101
|
+
expect(booleanToEditText(false)).toBe('false')
|
|
102
|
+
expect(
|
|
103
|
+
parseCellEditInput('true', { a: false }, { key: 'a', title: 'A', valueType: 'boolean' }, 0),
|
|
104
|
+
).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('parses explicit NULL draft and emptyAsNull', () => {
|
|
108
|
+
expect(isNullDraft(RS_TABLE_NULL_DRAFT)).toBe(true)
|
|
109
|
+
expect(
|
|
110
|
+
parseCellEditInput(RS_TABLE_NULL_DRAFT, { a: 'x' }, { key: 'a', title: 'A' }, 0),
|
|
111
|
+
).toBeNull()
|
|
112
|
+
expect(
|
|
113
|
+
parseCellEditInput('', { a: 'x' }, { key: 'a', title: 'A', emptyAsNull: true, nullable: true }, 0),
|
|
114
|
+
).toBeNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('supports async validators', async () => {
|
|
118
|
+
const { validateCellValueAsync, listBatchColumnTargets } = await import(
|
|
119
|
+
'../components/table/table-edit-utils'
|
|
120
|
+
)
|
|
121
|
+
await expect(
|
|
122
|
+
validateCellValueAsync(
|
|
123
|
+
-1,
|
|
124
|
+
{ n: 0 },
|
|
125
|
+
{
|
|
126
|
+
key: 'n',
|
|
127
|
+
title: 'N',
|
|
128
|
+
validator: async (value) => (Number(value) >= 0 ? null : '异步不能为负'),
|
|
129
|
+
},
|
|
130
|
+
0,
|
|
131
|
+
),
|
|
132
|
+
).resolves.toBe('异步不能为负')
|
|
133
|
+
|
|
134
|
+
const targets = listBatchColumnTargets({
|
|
135
|
+
rows: [
|
|
136
|
+
{ row: { id: '1' }, rowIndex: 0, rowKey: '1' },
|
|
137
|
+
{ row: { id: '2' }, rowIndex: 1, rowKey: '2' },
|
|
138
|
+
{ row: { id: '3' }, rowIndex: 2, rowKey: '3' },
|
|
139
|
+
],
|
|
140
|
+
column: { key: 'name', title: '名称', editable: true },
|
|
141
|
+
tableEditable: true,
|
|
142
|
+
selectedKeys: ['1', '3'],
|
|
143
|
+
anchorRowKey: '1',
|
|
144
|
+
})
|
|
145
|
+
expect(targets.map((item) => item.rowKey)).toEqual(['1', '3'])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('strips number decorations and validates', () => {
|
|
149
|
+
expect(stripNumberDecorations('1,234.5')).toBe('1234.5')
|
|
150
|
+
expect(
|
|
151
|
+
parseCellEditInput('1,200', { n: 0 }, { key: 'n', title: 'N', valueType: 'number' }, 0),
|
|
152
|
+
).toBe(1200)
|
|
153
|
+
expect(
|
|
154
|
+
validateCellValue(-1, { n: 0 }, {
|
|
155
|
+
key: 'n',
|
|
156
|
+
title: 'N',
|
|
157
|
+
validator: (value) => (Number(value) >= 0 ? null : '不能为负'),
|
|
158
|
+
}, 0),
|
|
159
|
+
).toBe('不能为负')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('parses clipboard TSV grid', () => {
|
|
163
|
+
expect(parseClipboardGrid('a\tb\nc\td')).toEqual([
|
|
164
|
+
['a', 'b'],
|
|
165
|
+
['c', 'd'],
|
|
166
|
+
])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('navigates editable cells', () => {
|
|
170
|
+
const cells = [
|
|
171
|
+
{ rowKey: '1', colKey: 'a', rowIndex: 0 },
|
|
172
|
+
{ rowKey: '1', colKey: 'b', rowIndex: 0 },
|
|
173
|
+
{ rowKey: '2', colKey: 'a', rowIndex: 1 },
|
|
174
|
+
{ rowKey: '2', colKey: 'b', rowIndex: 1 },
|
|
175
|
+
]
|
|
176
|
+
expect(navigateEditableCell(cells, '1', 'a', 'next', 2)?.colKey).toBe('b')
|
|
177
|
+
expect(navigateEditableCell(cells, '1', 'b', 'next', 2)?.rowKey).toBe('2')
|
|
178
|
+
expect(navigateEditableCell(cells, '1', 'a', 'down', 2)?.rowKey).toBe('2')
|
|
179
|
+
expect(navigateEditableCell(cells, '2', 'b', 'prev', 2)?.colKey).toBe('a')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('keeps raw value separate from render display (incl. VNode)', () => {
|
|
183
|
+
const row = { time: 42 }
|
|
184
|
+
const column = {
|
|
185
|
+
key: 'time',
|
|
186
|
+
title: 'Time',
|
|
187
|
+
render: (r: { time: number }) => h('span', { class: 'x' }, String(r.time)),
|
|
188
|
+
}
|
|
189
|
+
expect(resolveColumnRawValue(row, column, 0)).toBe(42)
|
|
190
|
+
const display = resolveColumnDisplayContent(row, column, 0)
|
|
191
|
+
expect(plainTextFromCellDisplay(display)).toBe('42')
|
|
192
|
+
expect(isVNode(display)).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
buildTableEntries,
|
|
4
|
+
clampColumnWidth,
|
|
5
|
+
filterTableRows,
|
|
6
|
+
filterTableRowsByColumnFilters,
|
|
7
|
+
isColumnFilterActive,
|
|
8
|
+
fixedCellStyle,
|
|
9
|
+
groupTableRows,
|
|
10
|
+
injectExpandRows,
|
|
11
|
+
isNearScrollBottom,
|
|
12
|
+
resolveFixedColumnStyles,
|
|
13
|
+
resolveSelectableRowKeys,
|
|
14
|
+
resolveSelectAllState,
|
|
15
|
+
resolveTableVirtualEnabled,
|
|
16
|
+
selectRowKeys,
|
|
17
|
+
selectRowKeysByClick,
|
|
18
|
+
sliceVirtualColumns,
|
|
19
|
+
sliceVirtualTableEntries,
|
|
20
|
+
sortTableRows,
|
|
21
|
+
toggleMultiSortState,
|
|
22
|
+
toggleRowSelection,
|
|
23
|
+
toggleSelectAll,
|
|
24
|
+
toggleSortState,
|
|
25
|
+
reorderColumnKeys,
|
|
26
|
+
reorderTableRows,
|
|
27
|
+
resolveColumnOrder,
|
|
28
|
+
resolveOrderedColumns,
|
|
29
|
+
sortTableRowsMulti,
|
|
30
|
+
} from '../components/table-utils'
|
|
31
|
+
|
|
32
|
+
describe('table-utils', () => {
|
|
33
|
+
const columns = [
|
|
34
|
+
{ key: 'name', title: '名称', sortable: true },
|
|
35
|
+
{ key: 'status', title: '状态' },
|
|
36
|
+
{ key: 'count', title: '数量', sortable: true },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const rows = [
|
|
40
|
+
{ id: '1', name: 'Alpha', status: 'running', count: 12 },
|
|
41
|
+
{ id: '2', name: 'Beta', status: 'stopped', count: 7 },
|
|
42
|
+
{ id: '3', name: 'Gamma', status: 'running', count: 23 },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
it('filters rows by query across columns', () => {
|
|
46
|
+
expect(filterTableRows(rows, 'alp', columns).map((row) => row.name)).toEqual(['Alpha'])
|
|
47
|
+
expect(filterTableRows(rows, 'running', columns, ['status'])).toHaveLength(2)
|
|
48
|
+
expect(filterTableRows(rows, ' ', columns)).toHaveLength(3)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('filters rows by per-column filters', () => {
|
|
52
|
+
expect(
|
|
53
|
+
filterTableRowsByColumnFilters(rows, columns, { name: 'beta' }).map((row) => row.name),
|
|
54
|
+
).toEqual(['Beta'])
|
|
55
|
+
expect(filterTableRowsByColumnFilters(rows, columns, { status: 'running' })).toHaveLength(2)
|
|
56
|
+
expect(
|
|
57
|
+
filterTableRowsByColumnFilters(rows, columns, { name: 'alp', status: 'running' }).map((row) => row.name),
|
|
58
|
+
).toEqual(['Alpha'])
|
|
59
|
+
expect(isColumnFilterActive({ name: 'x' }, 'name')).toBe(true)
|
|
60
|
+
expect(isColumnFilterActive({ name: ' ' }, 'name')).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('applies column filters in buildTableEntries', () => {
|
|
64
|
+
const entries = buildTableEntries(rows, columns, { columnFilters: { name: 'gamma' } })
|
|
65
|
+
expect(entries).toHaveLength(1)
|
|
66
|
+
expect(entries[0]?.type).toBe('row')
|
|
67
|
+
if (entries[0]?.type === 'row') expect(entries[0].row.name).toBe('Gamma')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('toggles sort state', () => {
|
|
71
|
+
expect(toggleSortState(null, 'name')).toEqual({ key: 'name', order: 'asc' })
|
|
72
|
+
expect(toggleSortState({ key: 'name', order: 'asc' }, 'name')).toEqual({ key: 'name', order: 'desc' })
|
|
73
|
+
expect(toggleSortState({ key: 'name', order: 'desc' }, 'name')).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('sorts rows by numeric and string columns', () => {
|
|
77
|
+
expect(sortTableRows(rows, columns, { key: 'name', order: 'asc' }).map((row) => row.name)).toEqual([
|
|
78
|
+
'Alpha',
|
|
79
|
+
'Beta',
|
|
80
|
+
'Gamma',
|
|
81
|
+
])
|
|
82
|
+
expect(sortTableRows(rows, columns, { key: 'count', order: 'desc' }).map((row) => row.count)).toEqual([23, 12, 7])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('groups rows and inserts group headers', () => {
|
|
86
|
+
const sorted = [...rows].sort((left, right) => left.status.localeCompare(right.status))
|
|
87
|
+
const grouped = groupTableRows(sorted, 'status', (key) => `状态:${key}`)
|
|
88
|
+
expect(grouped.filter((entry) => entry.type === 'group').map((entry) => entry.label)).toEqual([
|
|
89
|
+
'状态:running',
|
|
90
|
+
'状态:stopped',
|
|
91
|
+
])
|
|
92
|
+
expect(grouped.filter((entry) => entry.type === 'row')).toHaveLength(3)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('builds entries with filter, sort and group pipeline', () => {
|
|
96
|
+
const entries = buildTableEntries(rows, columns, {
|
|
97
|
+
filterText: 'running',
|
|
98
|
+
filterKeys: ['status'],
|
|
99
|
+
sort: { key: 'count', order: 'desc' },
|
|
100
|
+
groupBy: 'status',
|
|
101
|
+
groupLabel: (key) => key,
|
|
102
|
+
})
|
|
103
|
+
expect(entries.some((entry) => entry.type === 'group' && entry.key === 'running')).toBe(true)
|
|
104
|
+
const dataRows = entries.filter((entry) => entry.type === 'row')
|
|
105
|
+
expect(dataRows).toHaveLength(2)
|
|
106
|
+
expect(dataRows[0].type === 'row' && dataRows[0].row.name).toBe('Gamma')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('slices virtual entries with padding', () => {
|
|
110
|
+
const entries = buildTableEntries(
|
|
111
|
+
Array.from({ length: 100 }, (_, index) => ({
|
|
112
|
+
id: String(index),
|
|
113
|
+
name: `Row ${index}`,
|
|
114
|
+
status: 'running',
|
|
115
|
+
count: index,
|
|
116
|
+
})),
|
|
117
|
+
columns,
|
|
118
|
+
)
|
|
119
|
+
const slice = sliceVirtualTableEntries(entries, 400, 200, 40, 36, 2)
|
|
120
|
+
expect(slice.entries.length).toBeLessThan(entries.length)
|
|
121
|
+
expect(slice.paddingTop).toBeGreaterThan(0)
|
|
122
|
+
expect(slice.paddingBottom).toBeGreaterThan(0)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('clamps column width', () => {
|
|
126
|
+
expect(clampColumnWidth(10)).toBe(48)
|
|
127
|
+
expect(clampColumnWidth(900)).toBe(640)
|
|
128
|
+
expect(clampColumnWidth(120)).toBe(120)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('resolves virtual scroll for infinite mode', () => {
|
|
132
|
+
expect(resolveTableVirtualEnabled({ infinite: true })).toBe(true)
|
|
133
|
+
expect(resolveTableVirtualEnabled({ infinite: true, virtualOnInfinite: false })).toBe(false)
|
|
134
|
+
expect(resolveTableVirtualEnabled({ virtual: true })).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('detects near scroll bottom', () => {
|
|
138
|
+
expect(isNearScrollBottom(300, 400, 120, 80)).toBe(true)
|
|
139
|
+
expect(isNearScrollBottom(100, 400, 120, 80)).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('resolves row selection helpers', () => {
|
|
143
|
+
const rows = [
|
|
144
|
+
{ id: '1', name: 'A' },
|
|
145
|
+
{ id: '2', name: 'B', disabled: true },
|
|
146
|
+
{ id: '3', name: 'C' },
|
|
147
|
+
]
|
|
148
|
+
const keys = resolveSelectableRowKeys(rows, 'id')
|
|
149
|
+
expect(keys).toEqual(['1', '3'])
|
|
150
|
+
expect(resolveSelectAllState(['1'], keys)).toBe('indeterminate')
|
|
151
|
+
expect(toggleSelectAll([], keys, true)).toEqual(['1', '3'])
|
|
152
|
+
expect(toggleRowSelection(['1', '3'], '1')).toEqual(['3'])
|
|
153
|
+
expect(
|
|
154
|
+
selectRowKeysByClick(['1'], '3', {
|
|
155
|
+
toggle: false,
|
|
156
|
+
range: false,
|
|
157
|
+
orderedKeys: keys,
|
|
158
|
+
anchorKey: '1',
|
|
159
|
+
}),
|
|
160
|
+
).toEqual(['3'])
|
|
161
|
+
expect(
|
|
162
|
+
selectRowKeysByClick(['1'], '3', {
|
|
163
|
+
toggle: true,
|
|
164
|
+
range: false,
|
|
165
|
+
orderedKeys: keys,
|
|
166
|
+
anchorKey: '1',
|
|
167
|
+
}),
|
|
168
|
+
).toEqual(['1', '3'])
|
|
169
|
+
expect(
|
|
170
|
+
selectRowKeysByClick(['1'], '3', {
|
|
171
|
+
toggle: false,
|
|
172
|
+
range: true,
|
|
173
|
+
orderedKeys: ['1', '2', '3'],
|
|
174
|
+
anchorKey: '1',
|
|
175
|
+
}),
|
|
176
|
+
).toEqual(['1', '2', '3'])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('injects expand rows after expanded keys', () => {
|
|
180
|
+
const entries = buildTableEntries(
|
|
181
|
+
[{ id: '1', name: 'A' }],
|
|
182
|
+
[{ key: 'name', title: '名称' }],
|
|
183
|
+
)
|
|
184
|
+
const expanded = injectExpandRows(entries, new Set(['1']), 'id')
|
|
185
|
+
expect(expanded).toHaveLength(2)
|
|
186
|
+
expect(expanded[1]?.type).toBe('expand')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('resolves fixed column offsets', () => {
|
|
190
|
+
const styles = resolveFixedColumnStyles(
|
|
191
|
+
[
|
|
192
|
+
{ key: 'name', title: '名称', fixed: 'left', width: 100 },
|
|
193
|
+
{ key: 'action', title: '操作', fixed: 'right', width: 80 },
|
|
194
|
+
],
|
|
195
|
+
{},
|
|
196
|
+
{ expandable: true },
|
|
197
|
+
)
|
|
198
|
+
expect(styles.get('name')?.left).toBe(40)
|
|
199
|
+
expect(styles.get('action')?.right).toBe(0)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('applies header top sticky for fixed cells', () => {
|
|
203
|
+
expect(fixedCellStyle({ fixed: 'left', left: 0 }, { header: true })).toEqual({
|
|
204
|
+
position: 'sticky',
|
|
205
|
+
zIndex: '4',
|
|
206
|
+
top: '0',
|
|
207
|
+
left: '0px',
|
|
208
|
+
})
|
|
209
|
+
expect(fixedCellStyle({ fixed: 'right', right: 12 })).toEqual({
|
|
210
|
+
position: 'sticky',
|
|
211
|
+
zIndex: '2',
|
|
212
|
+
right: '12px',
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('slices visible columns for horizontal virtualization', () => {
|
|
217
|
+
const cols = Array.from({ length: 20 }, (_, i) => ({
|
|
218
|
+
key: `c${i}`,
|
|
219
|
+
title: `C${i}`,
|
|
220
|
+
width: 100,
|
|
221
|
+
}))
|
|
222
|
+
const sliced = sliceVirtualColumns(cols, {
|
|
223
|
+
scrollLeft: 500,
|
|
224
|
+
viewportWidth: 300,
|
|
225
|
+
getWidth: (c) => (typeof c.width === 'number' ? c.width : 100),
|
|
226
|
+
overscan: 1,
|
|
227
|
+
})
|
|
228
|
+
// 500→Start near col 5; viewport 300px ≈ 3 cols + overscan
|
|
229
|
+
expect(sliced.startIndex).toBeGreaterThanOrEqual(3)
|
|
230
|
+
expect(sliced.endIndex).toBeLessThanOrEqual(12)
|
|
231
|
+
expect(sliced.paddingLeft).toBe(sliced.startIndex * 100)
|
|
232
|
+
expect(sliced.paddingLeft + sliced.columns.reduce((n, c) => n + 100, 0) + sliced.paddingRight).toBe(
|
|
233
|
+
2000,
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('selects single row in radio mode', () => {
|
|
238
|
+
expect(selectRowKeys([], 'a', 'radio')).toEqual(['a'])
|
|
239
|
+
expect(selectRowKeys(['a'], 'b', 'radio')).toEqual(['b'])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('toggles multi-column sort state', () => {
|
|
243
|
+
expect(toggleMultiSortState([], 'name')).toEqual([{ key: 'name', order: 'asc' }])
|
|
244
|
+
expect(toggleMultiSortState([{ key: 'name', order: 'asc' }], 'count')).toEqual([
|
|
245
|
+
{ key: 'name', order: 'asc' },
|
|
246
|
+
{ key: 'count', order: 'asc' },
|
|
247
|
+
])
|
|
248
|
+
expect(toggleMultiSortState([{ key: 'name', order: 'desc' }], 'name')).toEqual([])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('sorts rows by multiple columns', () => {
|
|
252
|
+
const mixed = [
|
|
253
|
+
{ id: '1', name: 'A', status: 'running', count: 10 },
|
|
254
|
+
{ id: '2', name: 'A', status: 'stopped', count: 5 },
|
|
255
|
+
{ id: '3', name: 'B', status: 'running', count: 8 },
|
|
256
|
+
]
|
|
257
|
+
const sorted = sortTableRowsMulti(mixed, columns, [
|
|
258
|
+
{ key: 'name', order: 'asc' },
|
|
259
|
+
{ key: 'count', order: 'desc' },
|
|
260
|
+
])
|
|
261
|
+
expect(sorted.map((row) => row.id)).toEqual(['1', '2', '3'])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('reorders columns and rows', () => {
|
|
265
|
+
expect(reorderColumnKeys(['a', 'b', 'c'], 'c', 'a')).toEqual(['c', 'a', 'b'])
|
|
266
|
+
expect(resolveOrderedColumns(columns, ['count', 'name']).map((column) => column.key)).toEqual([
|
|
267
|
+
'count',
|
|
268
|
+
'name',
|
|
269
|
+
'status',
|
|
270
|
+
])
|
|
271
|
+
expect(resolveColumnOrder(columns, ['status', 'name'])).toEqual(['status', 'name', 'count'])
|
|
272
|
+
expect(reorderTableRows(['a', 'b', 'c'], 2, 0, 'before')).toEqual(['c', 'a', 'b'])
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('builds entries with multi sort', () => {
|
|
276
|
+
const entries = buildTableEntries(rows, columns, {
|
|
277
|
+
multiSort: true,
|
|
278
|
+
sorts: [
|
|
279
|
+
{ key: 'status', order: 'asc' },
|
|
280
|
+
{ key: 'count', order: 'desc' },
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
const dataRows = entries.filter((entry) => entry.type === 'row')
|
|
284
|
+
expect(dataRows[0].type === 'row' && dataRows[0].row.name).toBe('Gamma')
|
|
285
|
+
})
|
|
286
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createRsConfigState } from '../composables/useRsConfig'
|
|
3
|
+
import { applyTheme } from '../theme/apply'
|
|
4
|
+
|
|
5
|
+
describe('useRsConfig / theme', () => {
|
|
6
|
+
it('translates by locale', () => {
|
|
7
|
+
const zh = createRsConfigState('dark', 'zh-CN')
|
|
8
|
+
const en = createRsConfigState('dark', 'en-US')
|
|
9
|
+
expect(zh.t('select.placeholder')).toBe('请选择')
|
|
10
|
+
expect(en.t('select.placeholder')).toBe('Select')
|
|
11
|
+
expect(zh.t('input.required')).toBe('此项为必填')
|
|
12
|
+
expect(en.t('input.required')).toBe('This field is required')
|
|
13
|
+
expect(en.t('breadcrumb.label')).toBe('Breadcrumb')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('applyTheme sets data-rs-theme on element', () => {
|
|
17
|
+
applyTheme('light')
|
|
18
|
+
expect(document.documentElement.dataset.rsTheme).toBe('light')
|
|
19
|
+
applyTheme('dark')
|
|
20
|
+
expect(document.documentElement.dataset.rsTheme).toBe('dark')
|
|
21
|
+
})
|
|
22
|
+
})
|