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,160 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { h } from 'vue'
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import RsConfigProvider from '../components/RsConfigProvider.vue'
|
|
5
|
+
import RsUpload from '../components/RsUpload.vue'
|
|
6
|
+
import {
|
|
7
|
+
formatFileSize,
|
|
8
|
+
isFileAccepted,
|
|
9
|
+
mergeUploadFiles,
|
|
10
|
+
removeUploadFileAt,
|
|
11
|
+
validateUploadFiles,
|
|
12
|
+
} from '../components/upload-utils'
|
|
13
|
+
|
|
14
|
+
function createFile(name: string, content: string, type = 'text/plain'): File {
|
|
15
|
+
return new File([content], name, { type })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function selectFiles(wrapper: ReturnType<typeof mount>, files: File[]): Promise<void> {
|
|
19
|
+
const input = wrapper.find('input[type="file"]')
|
|
20
|
+
Object.defineProperty(input.element, 'files', {
|
|
21
|
+
value: files,
|
|
22
|
+
configurable: true,
|
|
23
|
+
})
|
|
24
|
+
await input.trigger('change')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('upload-utils', () => {
|
|
28
|
+
it('formats file sizes', () => {
|
|
29
|
+
expect(formatFileSize(512)).toBe('512 B')
|
|
30
|
+
expect(formatFileSize(1024)).toBe('1.0 KB')
|
|
31
|
+
expect(formatFileSize(1024 * 1024)).toBe('1.0 MB')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('matches accept rules by extension, mime type, and wildcard', () => {
|
|
35
|
+
const txt = createFile('a.txt', 'x')
|
|
36
|
+
const png = createFile('b.png', 'x', 'image/png')
|
|
37
|
+
expect(isFileAccepted(txt, '.txt')).toBe(true)
|
|
38
|
+
expect(isFileAccepted(txt, 'image/*')).toBe(false)
|
|
39
|
+
expect(isFileAccepted(png, 'image/*')).toBe(true)
|
|
40
|
+
expect(isFileAccepted(png, 'text/plain')).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('validates accept, maxSize, and maxCount', () => {
|
|
44
|
+
const small = createFile('small.txt', 'hi')
|
|
45
|
+
const large = createFile('large.txt', 'x'.repeat(200))
|
|
46
|
+
const invalidType = createFile('bad.pdf', 'x', 'application/pdf')
|
|
47
|
+
|
|
48
|
+
expect(validateUploadFiles([invalidType], { accept: '.txt' }).rejected[0]?.reason).toBe('accept')
|
|
49
|
+
expect(validateUploadFiles([large], { maxSize: 100 }).rejected[0]?.reason).toBe('maxSize')
|
|
50
|
+
expect(
|
|
51
|
+
validateUploadFiles([small, createFile('extra.txt', 'y')], { maxCount: 1 }).rejected[0]?.reason,
|
|
52
|
+
).toBe('maxCount')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('merges and trims files by maxCount', () => {
|
|
56
|
+
const first = createFile('a.txt', 'a')
|
|
57
|
+
const second = createFile('b.txt', 'b')
|
|
58
|
+
expect(mergeUploadFiles([first], [second], 1)).toHaveLength(1)
|
|
59
|
+
expect(mergeUploadFiles([first], [second])).toHaveLength(2)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('removes file at index', () => {
|
|
63
|
+
const files = [createFile('a.txt', 'a'), createFile('b.txt', 'b')]
|
|
64
|
+
expect(removeUploadFileAt(files, 0).map((file) => file.name)).toEqual(['b.txt'])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('RsUpload', () => {
|
|
69
|
+
it('renders default zh-CN label', () => {
|
|
70
|
+
const wrapper = mount(RsUpload)
|
|
71
|
+
expect(wrapper.find('.rs-upload__label').text()).toBe('点击或拖拽文件到此处上传')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('renders custom label and hint', () => {
|
|
75
|
+
const wrapper = mount(RsUpload, {
|
|
76
|
+
props: { label: '上传附件', hint: '仅 PDF' },
|
|
77
|
+
})
|
|
78
|
+
expect(wrapper.find('.rs-upload__label').text()).toBe('上传附件')
|
|
79
|
+
expect(wrapper.find('.rs-upload__hint').text()).toBe('仅 PDF')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('renders default browse hint when hint is omitted', () => {
|
|
83
|
+
const wrapper = mount(RsUpload)
|
|
84
|
+
expect(wrapper.find('.rs-upload__hint').text()).toBe('或点击浏览本地文件')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('uses en-US label inside RsConfigProvider', () => {
|
|
88
|
+
const wrapper = mount(RsConfigProvider, {
|
|
89
|
+
props: { locale: 'en-US' },
|
|
90
|
+
slots: {
|
|
91
|
+
default: () => h(RsUpload),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
expect(wrapper.find('.rs-upload__label').text()).toBe('Click or drag files here to upload')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('accepts files and updates model', async () => {
|
|
98
|
+
const wrapper = mount(RsUpload, {
|
|
99
|
+
props: { modelValue: [] },
|
|
100
|
+
})
|
|
101
|
+
const file = createFile('demo.txt', 'hello')
|
|
102
|
+
await selectFiles(wrapper, [file])
|
|
103
|
+
expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toEqual([file])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('shows file list with formatted size', async () => {
|
|
107
|
+
const file = createFile('demo.txt', 'hello')
|
|
108
|
+
const wrapper = mount(RsUpload, {
|
|
109
|
+
props: { modelValue: [file] },
|
|
110
|
+
})
|
|
111
|
+
expect(wrapper.find('.rs-upload__file-name').text()).toBe('demo.txt')
|
|
112
|
+
expect(wrapper.find('.rs-upload__file-size').text()).toBe(formatFileSize(file.size))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('removes file from list', async () => {
|
|
116
|
+
const first = createFile('a.txt', 'a')
|
|
117
|
+
const second = createFile('b.txt', 'b')
|
|
118
|
+
const wrapper = mount(RsUpload, {
|
|
119
|
+
props: { modelValue: [first, second] },
|
|
120
|
+
})
|
|
121
|
+
await wrapper.find('.rs-upload__file-remove').trigger('click')
|
|
122
|
+
expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toEqual([second])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('emits reject for invalid files', async () => {
|
|
126
|
+
const wrapper = mount(RsUpload, {
|
|
127
|
+
props: { modelValue: [], accept: '.txt' },
|
|
128
|
+
})
|
|
129
|
+
const invalid = createFile('image.png', 'x', 'image/png')
|
|
130
|
+
await selectFiles(wrapper, [invalid])
|
|
131
|
+
const reject = wrapper.emitted('reject')?.[0]?.[0] as Array<{ reason: string }>
|
|
132
|
+
expect(reject?.[0]?.reason).toBe('accept')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('disables input when disabled', () => {
|
|
136
|
+
const wrapper = mount(RsUpload, { props: { disabled: true } })
|
|
137
|
+
expect(wrapper.find('input').attributes('disabled')).toBeDefined()
|
|
138
|
+
expect(wrapper.find('.rs-upload__dropzone').classes()).toContain('rs-upload__dropzone--disabled')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('disables input when maxCount is reached', () => {
|
|
142
|
+
const wrapper = mount(RsUpload, {
|
|
143
|
+
props: {
|
|
144
|
+
modelValue: [createFile('a.txt', 'a'), createFile('b.txt', 'b')],
|
|
145
|
+
maxCount: 2,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
expect(wrapper.find('input').attributes('disabled')).toBeDefined()
|
|
149
|
+
expect(wrapper.find('.rs-upload__dropzone').classes()).toContain('rs-upload__dropzone--disabled')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('binds accept and multiple attributes', () => {
|
|
153
|
+
const wrapper = mount(RsUpload, {
|
|
154
|
+
props: { accept: 'image/*', multiple: true },
|
|
155
|
+
})
|
|
156
|
+
const input = wrapper.find('input')
|
|
157
|
+
expect(input.attributes('accept')).toBe('image/*')
|
|
158
|
+
expect(input.attributes('multiple')).toBeDefined()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import RsVirtualList from '../components/RsVirtualList.vue'
|
|
4
|
+
import { resolveItemSize, resolveVirtualListHeight } from '../components/virtual-list-utils'
|
|
5
|
+
|
|
6
|
+
describe('virtual-list-utils', () => {
|
|
7
|
+
it('resolves item size from number or function', () => {
|
|
8
|
+
expect(resolveItemSize(32)).toBe(32)
|
|
9
|
+
expect(resolveItemSize((index) => index * 10 + 20, 2)).toBe(40)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('resolves list height', () => {
|
|
13
|
+
expect(resolveVirtualListHeight(240)).toBe('240px')
|
|
14
|
+
expect(resolveVirtualListHeight('50%')).toBe('50%')
|
|
15
|
+
expect(resolveVirtualListHeight(0)).toBeUndefined()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('RsVirtualList', () => {
|
|
20
|
+
const items = Array.from({ length: 500 }, (_, index) => `Row ${index}`)
|
|
21
|
+
|
|
22
|
+
it('renders subset of items for large lists', () => {
|
|
23
|
+
const wrapper = mount(RsVirtualList, {
|
|
24
|
+
props: { items, height: 200, itemSize: 32 },
|
|
25
|
+
slots: {
|
|
26
|
+
default: `<div class="item">{{ item }}</div>`,
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
expect(wrapper.findAll('.item').length).toBeLessThan(items.length)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('passes index to default slot', () => {
|
|
33
|
+
const wrapper = mount(RsVirtualList, {
|
|
34
|
+
props: { items: ['A', 'B', 'C'], height: 96, itemSize: 32 },
|
|
35
|
+
slots: {
|
|
36
|
+
default: `<div class="cell">{{ index }}-{{ item }}</div>`,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
expect(wrapper.text()).toContain('0-A')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('marks active row with activeIndex', () => {
|
|
43
|
+
const wrapper = mount(RsVirtualList, {
|
|
44
|
+
props: { items: ['A', 'B', 'C', 'D'], height: 128, itemSize: 32, activeIndex: 2 },
|
|
45
|
+
slots: {
|
|
46
|
+
default: `<div class="cell">{{ index }}</div>`,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
expect(wrapper.find('.rs-virtual-list__item--active').exists()).toBe(true)
|
|
50
|
+
expect(wrapper.find('.rs-virtual-list__item--active').text()).toBe('2')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('scrolls active row into view on mount', async () => {
|
|
54
|
+
const items = Array.from({ length: 100 }, (_, index) => `Row ${index}`)
|
|
55
|
+
const wrapper = mount(RsVirtualList, {
|
|
56
|
+
props: { items, height: 200, itemSize: 40, activeIndex: 50 },
|
|
57
|
+
slots: {
|
|
58
|
+
default: `<div class="cell">{{ index }}</div>`,
|
|
59
|
+
},
|
|
60
|
+
attachTo: document.body,
|
|
61
|
+
})
|
|
62
|
+
const listEl = wrapper.find('.rs-virtual-list').element as HTMLElement
|
|
63
|
+
Object.defineProperty(listEl, 'clientHeight', { configurable: true, value: 200 })
|
|
64
|
+
await wrapper.vm.$nextTick()
|
|
65
|
+
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
66
|
+
expect(listEl.scrollTop).toBeGreaterThan(0)
|
|
67
|
+
wrapper.unmount()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('applies radius none as CSS variable', () => {
|
|
71
|
+
const wrapper = mount(RsVirtualList, {
|
|
72
|
+
props: { items: ['A'], height: 64, itemSize: 32, radius: 'none' },
|
|
73
|
+
})
|
|
74
|
+
const el = wrapper.find('.rs-virtual-list').element as HTMLElement
|
|
75
|
+
expect(el.style.getPropertyValue('--rs-virtual-list-radius')).toBe('0')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
formatDateDisplay,
|
|
4
|
+
formatDateTimeDisplay,
|
|
5
|
+
formatDateTimeValue,
|
|
6
|
+
formatDateValue,
|
|
7
|
+
parseDateTimeValue,
|
|
8
|
+
parseDateValue,
|
|
9
|
+
} from '../components/date-picker-utils'
|
|
10
|
+
import { RS_DATE_FORMAT, RS_DATETIME_FORMAT } from '../lib/rs-dayjs'
|
|
11
|
+
|
|
12
|
+
describe('date-picker-utils (dayjs)', () => {
|
|
13
|
+
it('uses fixed date format constant', () => {
|
|
14
|
+
expect(RS_DATE_FORMAT).toBe('YYYY-MM-DD')
|
|
15
|
+
expect(RS_DATETIME_FORMAT).toBe('YYYY-MM-DD HH:mm:ss')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('formats and parses date values', () => {
|
|
19
|
+
expect(formatDateValue('2025-06-16')).toBe('2025-06-16')
|
|
20
|
+
expect(formatDateDisplay('2025-06-16')).toBe('2025-06-16')
|
|
21
|
+
expect(parseDateValue('2025-06-16')).toEqual({ year: 2025, month: 6, day: 16 })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('formats and parses datetime values', () => {
|
|
25
|
+
expect(formatDateTimeValue('2025-06-16 14:30:00')).toBe('2025-06-16 14:30:00')
|
|
26
|
+
expect(formatDateTimeDisplay('2025-06-16 14:30:00')).toBe('2025-06-16 14:30:00')
|
|
27
|
+
expect(parseDateTimeValue('2025-06-16 14:30:00')).toEqual({
|
|
28
|
+
year: 2025,
|
|
29
|
+
month: 6,
|
|
30
|
+
day: 16,
|
|
31
|
+
hour: 14,
|
|
32
|
+
minute: 30,
|
|
33
|
+
second: 0,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('parses ISO-like datetime and normalizes to canonical form', () => {
|
|
38
|
+
expect(parseDateTimeValue('2025-06-16T14:30:00')).toEqual({
|
|
39
|
+
year: 2025,
|
|
40
|
+
month: 6,
|
|
41
|
+
day: 16,
|
|
42
|
+
hour: 14,
|
|
43
|
+
minute: 30,
|
|
44
|
+
second: 0,
|
|
45
|
+
})
|
|
46
|
+
expect(parseDateTimeValue('2025-06-16T14:30')).toEqual({
|
|
47
|
+
year: 2025,
|
|
48
|
+
month: 6,
|
|
49
|
+
day: 16,
|
|
50
|
+
hour: 14,
|
|
51
|
+
minute: 30,
|
|
52
|
+
second: 0,
|
|
53
|
+
})
|
|
54
|
+
expect(parseDateTimeValue('2025-06-16 14:30')).toEqual({
|
|
55
|
+
year: 2025,
|
|
56
|
+
month: 6,
|
|
57
|
+
day: 16,
|
|
58
|
+
hour: 14,
|
|
59
|
+
minute: 30,
|
|
60
|
+
second: 0,
|
|
61
|
+
})
|
|
62
|
+
expect(formatDateTimeValue('2025-06-16T14:30:00')).toBe('2025-06-16 14:30:00')
|
|
63
|
+
expect(formatDateTimeValue('2026-07-01T14:49:43Z')).toBe('2026-07-01 14:49:43')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('rejects unrecognizable datetime strings', () => {
|
|
67
|
+
expect(parseDateTimeValue('not-a-date')).toBeNull()
|
|
68
|
+
expect(formatDateTimeValue('not-a-date')).toBe('not-a-date')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { dialogViewportSize, readDialogViewportInsets } from '../components/dialog-viewport'
|
|
3
|
+
|
|
4
|
+
describe('dialog-viewport', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.documentElement.style.removeProperty('--rs-dialog-inset-top')
|
|
7
|
+
document.documentElement.style.removeProperty('--rs-dialog-inset-bottom')
|
|
8
|
+
document.documentElement.style.removeProperty('--rs-dialog-inset-x')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('falls back to viewport gap when insets are unset', () => {
|
|
12
|
+
const insets = readDialogViewportInsets()
|
|
13
|
+
expect(insets.top).toBe(16)
|
|
14
|
+
expect(insets.bottom).toBe(16)
|
|
15
|
+
expect(insets.left).toBe(16)
|
|
16
|
+
expect(insets.right).toBe(16)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('reads inset tokens from document root', () => {
|
|
20
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-top', '40px')
|
|
21
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-bottom', '26px')
|
|
22
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-x', '12px')
|
|
23
|
+
const insets = readDialogViewportInsets()
|
|
24
|
+
expect(insets.top).toBe(40)
|
|
25
|
+
expect(insets.bottom).toBe(26)
|
|
26
|
+
expect(insets.left).toBe(12)
|
|
27
|
+
expect(insets.right).toBe(12)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('subtracts insets from viewport size', () => {
|
|
31
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-top', '40px')
|
|
32
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-bottom', '26px')
|
|
33
|
+
document.documentElement.style.setProperty('--rs-dialog-inset-x', '10px')
|
|
34
|
+
const size = dialogViewportSize()
|
|
35
|
+
expect(size.width).toBe(window.innerWidth - 20)
|
|
36
|
+
expect(size.height).toBe(window.innerHeight - 66)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
formatIsoUtcToLocal,
|
|
4
|
+
looksLikeIsoDateTimeWithTz,
|
|
5
|
+
parseLocalDateTimeToUtcIso,
|
|
6
|
+
} from '../lib/iso-local-datetime'
|
|
7
|
+
|
|
8
|
+
function localParts(isoForDate: string): { date: string; time: string; ms: string } {
|
|
9
|
+
const d = new Date(isoForDate)
|
|
10
|
+
const pad = (n: number, w = 2) => String(n).padStart(w, '0')
|
|
11
|
+
return {
|
|
12
|
+
date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
|
|
13
|
+
time: `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`,
|
|
14
|
+
ms: pad(d.getMilliseconds(), 3),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('iso-local-datetime', () => {
|
|
19
|
+
it('formats Z to local with second / millisecond precision', () => {
|
|
20
|
+
const sec = '2026-06-25T08:06:36Z'
|
|
21
|
+
const { date, time } = localParts(sec)
|
|
22
|
+
expect(formatIsoUtcToLocal(sec)).toBe(`${date} ${time}`)
|
|
23
|
+
|
|
24
|
+
const ms = '2026-06-25T08:06:36.2263624Z'
|
|
25
|
+
const p = localParts('2026-06-25T08:06:36.226Z')
|
|
26
|
+
expect(formatIsoUtcToLocal(ms)).toBe(`${p.date} ${p.time}.${p.ms}`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('preserves original ISO when local second unchanged (picker strips ms)', () => {
|
|
30
|
+
const prev = '2026-06-25T08:06:36.2263624Z'
|
|
31
|
+
const local = formatIsoUtcToLocal(prev)!
|
|
32
|
+
const withoutMs = local.replace(/\.\d+$/, '')
|
|
33
|
+
expect(parseLocalDateTimeToUtcIso(withoutMs, prev)).toBe(prev)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('writes new UTC ISO when local time changes', () => {
|
|
37
|
+
const prev = '2026-06-25T08:06:36.226Z'
|
|
38
|
+
const prevLocal = formatIsoUtcToLocal(prev)!
|
|
39
|
+
// +1 second on local wall clock(无毫秒 → 写回秒精度)
|
|
40
|
+
const d = new Date(prev)
|
|
41
|
+
d.setSeconds(d.getSeconds() + 1)
|
|
42
|
+
d.setMilliseconds(0)
|
|
43
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
44
|
+
const nextLocal =
|
|
45
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
46
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
47
|
+
expect(nextLocal).not.toBe(prevLocal.replace(/\.\d+$/, ''))
|
|
48
|
+
const written = parseLocalDateTimeToUtcIso(nextLocal, prev)
|
|
49
|
+
expect(written).toMatch(/Z$/i)
|
|
50
|
+
expect(written).not.toBe(prev)
|
|
51
|
+
expect(new Date(written!).getTime()).toBe(d.getTime())
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('rejects naive datetime for looksLike / format', () => {
|
|
55
|
+
expect(looksLikeIsoDateTimeWithTz('2026-06-25T08:06:36')).toBe(false)
|
|
56
|
+
expect(formatIsoUtcToLocal('2026-06-25T08:06:36')).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
__resetClipboardStateForTests,
|
|
4
|
+
beginClipboardPrefetch,
|
|
5
|
+
copyTextToClipboard,
|
|
6
|
+
copyTextWithExecCommand,
|
|
7
|
+
readClipboardText,
|
|
8
|
+
writeClipboardText,
|
|
9
|
+
} from '../utils/rs-clipboard'
|
|
10
|
+
|
|
11
|
+
function stubNavigator(clipboard: Record<string, unknown>): void {
|
|
12
|
+
vi.stubGlobal('navigator', {
|
|
13
|
+
clipboard,
|
|
14
|
+
permissions: {
|
|
15
|
+
query: vi.fn().mockResolvedValue({ state: 'granted', onchange: null }),
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('rs-clipboard', () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
__resetClipboardStateForTests()
|
|
23
|
+
vi.restoreAllMocks()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('uses prefetched text when menu opens before paste', async () => {
|
|
27
|
+
const readText = vi.fn().mockResolvedValue('from-windows')
|
|
28
|
+
vi.stubGlobal('navigator', {
|
|
29
|
+
clipboard: { readText, writeText: vi.fn() },
|
|
30
|
+
})
|
|
31
|
+
await beginClipboardPrefetch()
|
|
32
|
+
expect(readText).toHaveBeenCalledTimes(1)
|
|
33
|
+
await expect(readClipboardText()).resolves.toBe('from-windows')
|
|
34
|
+
expect(readText).toHaveBeenCalledTimes(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('waits for in-flight prefetch before paste', async () => {
|
|
38
|
+
let resolveRead: (value: string) => void = () => undefined
|
|
39
|
+
const readText = vi.fn(
|
|
40
|
+
() =>
|
|
41
|
+
new Promise<string>((resolve) => {
|
|
42
|
+
resolveRead = resolve
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
vi.stubGlobal('navigator', {
|
|
46
|
+
clipboard: { readText, writeText: vi.fn() },
|
|
47
|
+
})
|
|
48
|
+
const pending = beginClipboardPrefetch()
|
|
49
|
+
await Promise.resolve()
|
|
50
|
+
const readPromise = readClipboardText()
|
|
51
|
+
resolveRead('late-text')
|
|
52
|
+
await pending
|
|
53
|
+
await expect(readPromise).resolves.toBe('late-text')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('writes clipboard text via API', async () => {
|
|
57
|
+
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
58
|
+
vi.stubGlobal('navigator', { clipboard: { readText: vi.fn(), writeText } })
|
|
59
|
+
await expect(writeClipboardText('hello')).resolves.toBe(true)
|
|
60
|
+
expect(writeText).toHaveBeenCalledWith('hello')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('falls back to execCommand when writeText fails', async () => {
|
|
64
|
+
const writeText = vi.fn().mockRejectedValue(new Error('denied'))
|
|
65
|
+
const execCommand = vi.fn().mockReturnValue(true)
|
|
66
|
+
vi.stubGlobal('navigator', { clipboard: { readText: vi.fn(), writeText } })
|
|
67
|
+
vi.stubGlobal('document', {
|
|
68
|
+
body: { appendChild: vi.fn(), removeChild: vi.fn() },
|
|
69
|
+
createElement: () => ({
|
|
70
|
+
value: '',
|
|
71
|
+
style: {},
|
|
72
|
+
setAttribute: vi.fn(),
|
|
73
|
+
select: vi.fn(),
|
|
74
|
+
remove: vi.fn(),
|
|
75
|
+
}),
|
|
76
|
+
execCommand,
|
|
77
|
+
})
|
|
78
|
+
await expect(copyTextToClipboard('fallback')).resolves.toBe(true)
|
|
79
|
+
expect(execCommand).toHaveBeenCalledWith('copy')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('falls back to shell bridge when clipboard read fails', async () => {
|
|
83
|
+
const readText = vi.fn().mockRejectedValue(new DOMException('denied', 'NotAllowedError'))
|
|
84
|
+
const cefQuery = vi.fn(({ onSuccess }: { onSuccess: (r: string) => void }) => {
|
|
85
|
+
onSuccess(JSON.stringify({ text: 'from-shell' }))
|
|
86
|
+
})
|
|
87
|
+
vi.stubGlobal('cefQuery', cefQuery)
|
|
88
|
+
vi.stubGlobal('navigator', {
|
|
89
|
+
clipboard: { readText, writeText: vi.fn() },
|
|
90
|
+
})
|
|
91
|
+
await beginClipboardPrefetch()
|
|
92
|
+
await expect(readClipboardText()).resolves.toBe('from-shell')
|
|
93
|
+
expect(cefQuery).toHaveBeenCalled()
|
|
94
|
+
expect(readText).toHaveBeenCalledTimes(1)
|
|
95
|
+
await expect(readClipboardText()).resolves.toBe('from-shell')
|
|
96
|
+
expect(readText).toHaveBeenCalledTimes(1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('writes via shell bridge only when API and execCommand fail', async () => {
|
|
100
|
+
const writeText = vi.fn().mockRejectedValue(new Error('denied'))
|
|
101
|
+
const execCommand = vi.fn().mockReturnValue(false)
|
|
102
|
+
const cefQuery = vi.fn(({ request, onSuccess }: { request: string; onSuccess: (r: string) => void }) => {
|
|
103
|
+
const payload = JSON.parse(request) as { method: string }
|
|
104
|
+
expect(payload.method).toBe('shell.clipboard.writeText')
|
|
105
|
+
onSuccess(JSON.stringify({ written: true }))
|
|
106
|
+
})
|
|
107
|
+
vi.stubGlobal('cefQuery', cefQuery)
|
|
108
|
+
vi.stubGlobal('navigator', { clipboard: { readText: vi.fn(), writeText } })
|
|
109
|
+
vi.stubGlobal('document', {
|
|
110
|
+
body: { appendChild: vi.fn(), removeChild: vi.fn() },
|
|
111
|
+
createElement: () => ({
|
|
112
|
+
value: '',
|
|
113
|
+
style: {},
|
|
114
|
+
setAttribute: vi.fn(),
|
|
115
|
+
select: vi.fn(),
|
|
116
|
+
remove: vi.fn(),
|
|
117
|
+
}),
|
|
118
|
+
execCommand,
|
|
119
|
+
})
|
|
120
|
+
await expect(copyTextToClipboard('cef-copy')).resolves.toBe(true)
|
|
121
|
+
expect(writeText).toHaveBeenCalled()
|
|
122
|
+
expect(execCommand).toHaveBeenCalledWith('copy')
|
|
123
|
+
expect(cefQuery).toHaveBeenCalled()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('copyTextWithExecCommand uses textarea trick', () => {
|
|
127
|
+
const execCommand = vi.fn().mockReturnValue(true)
|
|
128
|
+
vi.stubGlobal('document', {
|
|
129
|
+
body: { appendChild: vi.fn(), removeChild: vi.fn() },
|
|
130
|
+
createElement: () => ({
|
|
131
|
+
value: '',
|
|
132
|
+
style: {},
|
|
133
|
+
setAttribute: vi.fn(),
|
|
134
|
+
select: vi.fn(),
|
|
135
|
+
remove: vi.fn(),
|
|
136
|
+
}),
|
|
137
|
+
execCommand,
|
|
138
|
+
})
|
|
139
|
+
expect(copyTextWithExecCommand('line')).toBe(true)
|
|
140
|
+
expect(execCommand).toHaveBeenCalledWith('copy')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** jsdom 不实现 ResizeObserver,此处提供最小 mock 供组件初始化使用。 */
|
|
2
|
+
class ResizeObserverMock {
|
|
3
|
+
observe() {}
|
|
4
|
+
unobserve() {}
|
|
5
|
+
disconnect() {}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(globalThis, 'ResizeObserver', {
|
|
9
|
+
writable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
value: ResizeObserverMock,
|
|
12
|
+
})
|