niuma-ui 1.0.1 → 1.1.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 +65 -1
- package/package.json +10 -2
- 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__/RsDatePicker.spec.ts +21 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +178 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +250 -6
- package/src/__tests__/RsFormItem.spec.ts +478 -0
- package/src/__tests__/RsFormList.spec.ts +109 -0
- package/src/__tests__/RsInput.spec.ts +108 -1
- package/src/__tests__/RsInputNumber.spec.ts +33 -0
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +465 -2
- package/src/__tests__/RsSwitch.spec.ts +74 -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__/RsTimePicker.spec.ts +20 -0
- package/src/__tests__/RsTooltip.spec.ts +43 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/css-token.spec.ts +51 -0
- package/src/__tests__/date-picker-utils.spec.ts +49 -0
- package/src/__tests__/dialog-utils.spec.ts +43 -0
- package/src/__tests__/form-path.spec.ts +46 -0
- package/src/__tests__/form-rules.spec.ts +139 -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 +27 -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/RsAvatar.vue +1 -1
- package/src/components/RsBadge.vue +1 -1
- package/src/components/RsBreadcrumb.vue +1 -1
- package/src/components/RsButton.vue +147 -23
- package/src/components/RsCalendarGrid.vue +3 -3
- package/src/components/RsCard.vue +204 -20
- package/src/components/RsCodeBlock.vue +4 -4
- package/src/components/RsCodeEditor.vue +8 -8
- package/src/components/RsConfirmDialog.vue +217 -30
- package/src/components/RsContextMenu.vue +57 -17
- package/src/components/RsDatePicker.vue +316 -39
- package/src/components/RsDateTimePicker.vue +27 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +374 -52
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +559 -34
- package/src/components/RsDropdown.vue +34 -3
- package/src/components/RsDynamicTags.vue +531 -0
- package/src/components/RsEmpty.vue +1 -1
- package/src/components/RsForm.vue +164 -8
- package/src/components/RsFormItem.vue +423 -0
- package/src/components/RsFormItemControl.vue +21 -0
- package/src/components/RsFormList.vue +117 -0
- package/src/components/RsInput.vue +491 -100
- package/src/components/RsInputNumber.vue +77 -27
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLink.vue +1 -1
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMarkdown.vue +5 -5
- package/src/components/RsMenu.vue +74 -16
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsMonacoEditor.vue +9 -3
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsProseEditor.vue +3 -3
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +372 -642
- package/src/components/RsSidebarGroup.vue +1 -1
- package/src/components/RsSplitPane.vue +90 -1
- package/src/components/RsStatCard.vue +2 -2
- package/src/components/RsSteps.vue +2 -2
- package/src/components/RsSwitch.vue +192 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +422 -90
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +58 -19
- package/src/components/RsTimePicker.vue +72 -9
- package/src/components/RsTimePickerColumns.vue +137 -78
- package/src/components/RsToaster.vue +64 -24
- package/src/components/RsTooltip.vue +10 -1
- package/src/components/RsTooltipProvider.vue +11 -1
- package/src/components/RsTree.vue +45 -27
- package/src/components/RsUpload.vue +147 -14
- 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 +195 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +182 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-item-control.ts +55 -0
- package/src/components/form-path.ts +92 -0
- package/src/components/form-rules.ts +311 -0
- package/src/components/form-utils.ts +145 -3
- 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 +285 -6
- 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/RsTableHeaderFilter.vue +1 -1
- 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 +85 -7
- 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/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/use-rs-select.ts +436 -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/useRsConfig.ts +10 -3
- package/src/composables/useRsI18n.ts +16 -5
- 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/index.ts +378 -10
- package/src/locale/interpolate.ts +32 -0
- package/src/locale/messages.ts +40 -0
- package/src/styles.css +365 -56
- package/src/theme/brand.example.css +24 -2
- package/src/theme/css-token.ts +107 -0
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +68 -0
- package/src/components/RsTableColgroup.vue +0 -17
- package/src/components/RsTableHeaderRow.vue +0 -30
|
@@ -69,6 +69,23 @@ describe('RsDialog', () => {
|
|
|
69
69
|
wrapper.unmount()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('centers window layout when width is a viewport percentage', async () => {
|
|
73
|
+
const wrapper = mount(RsDialog, {
|
|
74
|
+
props: { open: true, title: '百分比宽度', width: '90%' },
|
|
75
|
+
attachTo: document.body,
|
|
76
|
+
})
|
|
77
|
+
await flushPromises()
|
|
78
|
+
const content = document.body.querySelector('.rs-dialog__content') as HTMLElement
|
|
79
|
+
const width = Number.parseFloat(content.style.width)
|
|
80
|
+
const left = Number.parseFloat(content.style.left)
|
|
81
|
+
const available = window.innerWidth - 32
|
|
82
|
+
const expectedWidth = Math.min(available, Math.max(320, Math.round(available * 0.9)))
|
|
83
|
+
expect(width).toBe(expectedWidth)
|
|
84
|
+
expect(left).toBeCloseTo(16 + (available - expectedWidth) / 2, 1)
|
|
85
|
+
expect(content.style.maxWidth).toBe('')
|
|
86
|
+
wrapper.unmount()
|
|
87
|
+
})
|
|
88
|
+
|
|
72
89
|
it('applies window layout with resize handles by default', async () => {
|
|
73
90
|
const wrapper = mount(RsDialog, {
|
|
74
91
|
props: { open: true, title: '测试', width: 'sm' },
|
|
@@ -275,9 +292,14 @@ describe('RsDialog', () => {
|
|
|
275
292
|
header.dispatchEvent(
|
|
276
293
|
new PointerEvent('pointerdown', { clientX: 120, clientY: 80, bubbles: true, cancelable: true }),
|
|
277
294
|
)
|
|
295
|
+
expect(document.body.style.cursor).toBe('move')
|
|
278
296
|
window.dispatchEvent(
|
|
279
297
|
new PointerEvent('pointermove', { clientX: 180, clientY: 120, bubbles: true, cancelable: true }),
|
|
280
298
|
)
|
|
299
|
+
// 拖拽过程中应即时位移,不能等到 pointerup
|
|
300
|
+
expect(content.style.left).not.toBe(startLeft)
|
|
301
|
+
expect(content.style.top).not.toBe(startTop)
|
|
302
|
+
|
|
281
303
|
window.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true }))
|
|
282
304
|
await flushPromises()
|
|
283
305
|
|
|
@@ -361,6 +383,50 @@ describe('RsDialog', () => {
|
|
|
361
383
|
await flushPromises()
|
|
362
384
|
expect(target.querySelector('.rs-dialog__content')).not.toBeNull()
|
|
363
385
|
wrapper.unmount()
|
|
386
|
+
target.remove()
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('re-teleports to body while fullscreen, then restores custom target', async () => {
|
|
390
|
+
const target = document.createElement('div')
|
|
391
|
+
target.id = 'rs-dialog-fullscreen-target'
|
|
392
|
+
document.body.appendChild(target)
|
|
393
|
+
|
|
394
|
+
const wrapper = mount(RsDialog, {
|
|
395
|
+
props: {
|
|
396
|
+
open: true,
|
|
397
|
+
title: '全屏挂载',
|
|
398
|
+
layout: 'window',
|
|
399
|
+
fullscreenable: true,
|
|
400
|
+
teleportTo: '#rs-dialog-fullscreen-target',
|
|
401
|
+
},
|
|
402
|
+
attachTo: document.body,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await flushPromises()
|
|
406
|
+
expect(target.querySelector('.rs-dialog__content')).not.toBeNull()
|
|
407
|
+
|
|
408
|
+
const enterFullscreenBtn = target.querySelectorAll('.rs-dialog__actions button')[0] as HTMLElement
|
|
409
|
+
await enterFullscreenBtn.click()
|
|
410
|
+
await flushPromises()
|
|
411
|
+
|
|
412
|
+
expect(target.querySelector('.rs-dialog__content')).toBeNull()
|
|
413
|
+
const bodyContent = document.body.querySelector('.rs-dialog__content') as HTMLElement
|
|
414
|
+
expect(bodyContent).not.toBeNull()
|
|
415
|
+
expect(bodyContent.classList.contains('rs-dialog__content--fullscreen')).toBe(true)
|
|
416
|
+
expect(target.contains(bodyContent)).toBe(false)
|
|
417
|
+
|
|
418
|
+
const exitFullscreenBtn = document.body.querySelectorAll(
|
|
419
|
+
'.rs-dialog__actions button',
|
|
420
|
+
)[0] as HTMLElement
|
|
421
|
+
await exitFullscreenBtn.click()
|
|
422
|
+
await flushPromises()
|
|
423
|
+
expect(target.querySelector('.rs-dialog__content')).not.toBeNull()
|
|
424
|
+
expect(
|
|
425
|
+
target.querySelector('.rs-dialog__content')?.classList.contains('rs-dialog__content--fullscreen'),
|
|
426
|
+
).toBe(false)
|
|
427
|
+
|
|
428
|
+
wrapper.unmount()
|
|
429
|
+
target.remove()
|
|
364
430
|
})
|
|
365
431
|
|
|
366
432
|
it('passes modal=false to DialogRoot for non-modal usage', async () => {
|
|
@@ -401,4 +467,116 @@ describe('RsDialog', () => {
|
|
|
401
467
|
expect(wrapper.props('open')).toBe(true)
|
|
402
468
|
wrapper.unmount()
|
|
403
469
|
})
|
|
470
|
+
|
|
471
|
+
it('beforeClose returning false keeps dialog open', async () => {
|
|
472
|
+
const Host = defineComponent({
|
|
473
|
+
components: { RsDialog },
|
|
474
|
+
setup() {
|
|
475
|
+
const open = ref(true)
|
|
476
|
+
const beforeClose = () => false
|
|
477
|
+
return { open, beforeClose }
|
|
478
|
+
},
|
|
479
|
+
template:
|
|
480
|
+
'<RsDialog v-model:open="open" title="拦截关闭" :fullscreenable="false" :before-close="beforeClose" />',
|
|
481
|
+
})
|
|
482
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
483
|
+
await flushPromises()
|
|
484
|
+
const closeBtn = document.body.querySelector('.rs-dialog__actions button') as HTMLElement
|
|
485
|
+
await closeBtn.click()
|
|
486
|
+
await flushPromises()
|
|
487
|
+
expect(wrapper.findComponent(RsDialog).props('open')).toBe(true)
|
|
488
|
+
wrapper.unmount()
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('renders builtin footer when showFooter is true and no footer slot', async () => {
|
|
492
|
+
const wrapper = mount(RsDialog, {
|
|
493
|
+
props: {
|
|
494
|
+
open: true,
|
|
495
|
+
title: '内置页脚',
|
|
496
|
+
showFooter: true,
|
|
497
|
+
layout: 'confirm',
|
|
498
|
+
resizable: false,
|
|
499
|
+
fullscreenable: false,
|
|
500
|
+
},
|
|
501
|
+
attachTo: document.body,
|
|
502
|
+
})
|
|
503
|
+
await flushPromises()
|
|
504
|
+
const footer = document.body.querySelector('.rs-dialog__footer')
|
|
505
|
+
expect(footer).not.toBeNull()
|
|
506
|
+
expect(footer?.querySelectorAll('button').length).toBe(2)
|
|
507
|
+
wrapper.unmount()
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('does not render builtin footer by default', async () => {
|
|
511
|
+
const wrapper = mount(RsDialog, {
|
|
512
|
+
props: {
|
|
513
|
+
open: true,
|
|
514
|
+
title: '无页脚',
|
|
515
|
+
layout: 'confirm',
|
|
516
|
+
resizable: false,
|
|
517
|
+
fullscreenable: false,
|
|
518
|
+
},
|
|
519
|
+
attachTo: document.body,
|
|
520
|
+
})
|
|
521
|
+
await flushPromises()
|
|
522
|
+
expect(document.body.querySelector('.rs-dialog__footer')).toBeNull()
|
|
523
|
+
wrapper.unmount()
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('applies custom numeric width on confirm layout', async () => {
|
|
527
|
+
const wrapper = mount(RsDialog, {
|
|
528
|
+
props: {
|
|
529
|
+
open: true,
|
|
530
|
+
title: '自定义宽度',
|
|
531
|
+
layout: 'confirm',
|
|
532
|
+
width: 520,
|
|
533
|
+
resizable: false,
|
|
534
|
+
fullscreenable: false,
|
|
535
|
+
},
|
|
536
|
+
attachTo: document.body,
|
|
537
|
+
})
|
|
538
|
+
await flushPromises()
|
|
539
|
+
const content = document.body.querySelector('.rs-dialog__content') as HTMLElement
|
|
540
|
+
expect(content.classList.contains('rs-dialog__content--custom-width')).toBe(true)
|
|
541
|
+
expect(content.style.maxWidth).toBe('520px')
|
|
542
|
+
wrapper.unmount()
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('title slot overrides title prop text', async () => {
|
|
546
|
+
const wrapper = mount(RsDialog, {
|
|
547
|
+
props: { open: true, title: '属性标题', fullscreenable: false },
|
|
548
|
+
slots: { title: '<span class="slot-title">插槽标题</span>' },
|
|
549
|
+
attachTo: document.body,
|
|
550
|
+
})
|
|
551
|
+
await flushPromises()
|
|
552
|
+
expect(document.body.querySelector('.slot-title')?.textContent).toBe('插槽标题')
|
|
553
|
+
wrapper.unmount()
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('emits afterClose when closed via close button', async () => {
|
|
557
|
+
const Host = defineComponent({
|
|
558
|
+
components: { RsDialog },
|
|
559
|
+
setup() {
|
|
560
|
+
const open = ref(true)
|
|
561
|
+
const reason = ref('')
|
|
562
|
+
return { open, reason }
|
|
563
|
+
},
|
|
564
|
+
template: `
|
|
565
|
+
<RsDialog
|
|
566
|
+
v-model:open="open"
|
|
567
|
+
title="关闭事件"
|
|
568
|
+
:fullscreenable="false"
|
|
569
|
+
@after-close="reason = $event"
|
|
570
|
+
/>
|
|
571
|
+
`,
|
|
572
|
+
})
|
|
573
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
574
|
+
await flushPromises()
|
|
575
|
+
const closeBtn = document.body.querySelector('.rs-dialog__actions button') as HTMLElement
|
|
576
|
+
await closeBtn.click()
|
|
577
|
+
await flushPromises()
|
|
578
|
+
expect(wrapper.vm.reason).toBe('close')
|
|
579
|
+
expect(wrapper.findComponent(RsDialog).props('open')).toBe(false)
|
|
580
|
+
wrapper.unmount()
|
|
581
|
+
})
|
|
404
582
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, h, ref } from 'vue'
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
4
4
|
import RsConfigProvider from '../components/RsConfigProvider.vue'
|
|
5
5
|
import RsDrawer from '../components/RsDrawer.vue'
|
|
@@ -51,6 +51,17 @@ describe('RsDrawer', () => {
|
|
|
51
51
|
wrapper.unmount()
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
it('applies custom width via CSS variable', async () => {
|
|
55
|
+
const wrapper = mount(RsDrawer, {
|
|
56
|
+
props: { open: true, title: '自定义宽', width: 420 },
|
|
57
|
+
attachTo: document.body,
|
|
58
|
+
})
|
|
59
|
+
await flushPromises()
|
|
60
|
+
const content = document.body.querySelector('.rs-drawer__content') as HTMLElement
|
|
61
|
+
expect(content.style.getPropertyValue('--rs-drawer-panel-size')).toBe('420px')
|
|
62
|
+
wrapper.unmount()
|
|
63
|
+
})
|
|
64
|
+
|
|
54
65
|
it('renders footer slot', async () => {
|
|
55
66
|
const wrapper = mount(RsDrawer, {
|
|
56
67
|
props: { open: true, title: '保存' },
|
|
@@ -62,7 +73,7 @@ describe('RsDrawer', () => {
|
|
|
62
73
|
wrapper.unmount()
|
|
63
74
|
})
|
|
64
75
|
|
|
65
|
-
it('
|
|
76
|
+
it('provides visually hidden DialogTitle when using header slot', async () => {
|
|
66
77
|
const wrapper = mount(RsDrawer, {
|
|
67
78
|
props: { open: true },
|
|
68
79
|
slots: {
|
|
@@ -72,8 +83,9 @@ describe('RsDrawer', () => {
|
|
|
72
83
|
attachTo: document.body,
|
|
73
84
|
})
|
|
74
85
|
await flushPromises()
|
|
86
|
+
const hiddenTitle = document.body.querySelector('.rs-drawer__title--sr-only')
|
|
87
|
+
expect(hiddenTitle).not.toBeNull()
|
|
75
88
|
expect(document.body.querySelector('.custom-header')?.textContent).toBe('自定义标题')
|
|
76
|
-
expect(document.body.querySelector('.rs-drawer__title')).toBeNull()
|
|
77
89
|
wrapper.unmount()
|
|
78
90
|
})
|
|
79
91
|
|
|
@@ -95,6 +107,44 @@ describe('RsDrawer', () => {
|
|
|
95
107
|
wrapper.unmount()
|
|
96
108
|
})
|
|
97
109
|
|
|
110
|
+
it('beforeClose returning false keeps drawer open', async () => {
|
|
111
|
+
const Host = defineComponent({
|
|
112
|
+
components: { RsDrawer },
|
|
113
|
+
setup() {
|
|
114
|
+
const open = ref(true)
|
|
115
|
+
const beforeClose = () => false
|
|
116
|
+
return { open, beforeClose }
|
|
117
|
+
},
|
|
118
|
+
template: '<RsDrawer v-model:open="open" title="拦截" :before-close="beforeClose" />',
|
|
119
|
+
})
|
|
120
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
121
|
+
await flushPromises()
|
|
122
|
+
const closeBtn = document.body.querySelector('.rs-drawer__header button') as HTMLElement
|
|
123
|
+
await closeBtn.click()
|
|
124
|
+
await flushPromises()
|
|
125
|
+
expect(wrapper.findComponent(RsDrawer).props('open')).toBe(true)
|
|
126
|
+
wrapper.unmount()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('emits afterClose when closed via close button', async () => {
|
|
130
|
+
const onAfterClose = vi.fn()
|
|
131
|
+
const Host = defineComponent({
|
|
132
|
+
components: { RsDrawer },
|
|
133
|
+
setup() {
|
|
134
|
+
const open = ref(true)
|
|
135
|
+
return { open, onAfterClose }
|
|
136
|
+
},
|
|
137
|
+
template: '<RsDrawer v-model:open="open" title="关闭" @after-close="onAfterClose" />',
|
|
138
|
+
})
|
|
139
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
140
|
+
await flushPromises()
|
|
141
|
+
const closeBtn = document.body.querySelector('.rs-drawer__header button') as HTMLElement
|
|
142
|
+
await closeBtn.click()
|
|
143
|
+
await flushPromises()
|
|
144
|
+
expect(onAfterClose).toHaveBeenCalledWith('close')
|
|
145
|
+
wrapper.unmount()
|
|
146
|
+
})
|
|
147
|
+
|
|
98
148
|
it('hides overlay when showOverlay is false', async () => {
|
|
99
149
|
const wrapper = mount(RsDrawer, {
|
|
100
150
|
props: { open: true, title: '无遮罩', showOverlay: false },
|
|
@@ -105,6 +155,49 @@ describe('RsDrawer', () => {
|
|
|
105
155
|
wrapper.unmount()
|
|
106
156
|
})
|
|
107
157
|
|
|
158
|
+
it('does not close on outside when closeOnOverlayClick is false', async () => {
|
|
159
|
+
const Host = defineComponent({
|
|
160
|
+
components: { RsDrawer },
|
|
161
|
+
setup() {
|
|
162
|
+
const open = ref(true)
|
|
163
|
+
return { open }
|
|
164
|
+
},
|
|
165
|
+
template:
|
|
166
|
+
'<RsDrawer v-model:open="open" title="非点外关" :show-overlay="false" :close-on-overlay-click="false" />',
|
|
167
|
+
})
|
|
168
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
169
|
+
await flushPromises()
|
|
170
|
+
document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
|
171
|
+
await flushPromises()
|
|
172
|
+
expect(wrapper.findComponent(RsDrawer).props('open')).toBe(true)
|
|
173
|
+
wrapper.unmount()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('teleports into a container and uses contained positioning', async () => {
|
|
177
|
+
const host = document.createElement('div')
|
|
178
|
+
host.id = 'drawer-host'
|
|
179
|
+
host.style.position = 'relative'
|
|
180
|
+
document.body.appendChild(host)
|
|
181
|
+
|
|
182
|
+
const wrapper = mount(RsDrawer, {
|
|
183
|
+
props: {
|
|
184
|
+
open: true,
|
|
185
|
+
title: '容器内抽屉',
|
|
186
|
+
teleportTo: '#drawer-host',
|
|
187
|
+
},
|
|
188
|
+
attachTo: document.body,
|
|
189
|
+
})
|
|
190
|
+
await flushPromises()
|
|
191
|
+
|
|
192
|
+
const content = host.querySelector('.rs-drawer__content') as HTMLElement | null
|
|
193
|
+
expect(content).not.toBeNull()
|
|
194
|
+
expect(content?.classList.contains('rs-drawer__content--contained')).toBe(true)
|
|
195
|
+
expect(document.body.querySelector(':scope > .rs-drawer__content')).toBeNull()
|
|
196
|
+
|
|
197
|
+
wrapper.unmount()
|
|
198
|
+
host.remove()
|
|
199
|
+
})
|
|
200
|
+
|
|
108
201
|
it('hides close button when showClose is false', async () => {
|
|
109
202
|
const wrapper = mount(RsDrawer, {
|
|
110
203
|
props: { open: true, title: '无关闭', showClose: false },
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { defineComponent, nextTick } from 'vue'
|
|
4
|
+
import RsDynamicTags from '../components/RsDynamicTags.vue'
|
|
5
|
+
|
|
6
|
+
describe('RsDynamicTags', () => {
|
|
7
|
+
it('opens input from trigger and creates tag on Enter', async () => {
|
|
8
|
+
const Host = defineComponent({
|
|
9
|
+
components: { RsDynamicTags },
|
|
10
|
+
data: () => ({ tags: [] as string[] }),
|
|
11
|
+
template: '<RsDynamicTags v-model="tags" />',
|
|
12
|
+
})
|
|
13
|
+
const wrapper = mount(Host)
|
|
14
|
+
|
|
15
|
+
await wrapper.find('.rs-dynamic-tags__trigger').trigger('click')
|
|
16
|
+
await nextTick()
|
|
17
|
+
const input = wrapper.find('.rs-dynamic-tags__input')
|
|
18
|
+
expect(input.exists()).toBe(true)
|
|
19
|
+
|
|
20
|
+
await input.setValue('alpha')
|
|
21
|
+
await input.trigger('keydown', { key: 'Enter' })
|
|
22
|
+
await nextTick()
|
|
23
|
+
expect((wrapper.vm as { tags: string[] }).tags).toEqual(['alpha'])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('rejects duplicate tags', async () => {
|
|
27
|
+
const wrapper = mount(RsDynamicTags, {
|
|
28
|
+
props: {
|
|
29
|
+
modelValue: ['a'],
|
|
30
|
+
inputMode: 'always',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const input = wrapper.find('.rs-dynamic-tags__input')
|
|
35
|
+
await input.setValue('a')
|
|
36
|
+
await input.trigger('keydown', { key: 'Enter' })
|
|
37
|
+
await nextTick()
|
|
38
|
+
expect(wrapper.emitted('reject')?.[0]).toEqual(['duplicate', 'a'])
|
|
39
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('hides trigger when max reached', async () => {
|
|
43
|
+
const wrapper = mount(RsDynamicTags, {
|
|
44
|
+
props: {
|
|
45
|
+
modelValue: ['a', 'b'],
|
|
46
|
+
max: 2,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
expect(wrapper.find('.rs-dynamic-tags__trigger').exists()).toBe(false)
|
|
50
|
+
expect(wrapper.find('.rs-dynamic-tags__input').exists()).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, ref } from 'vue'
|
|
1
|
+
import { defineComponent, h, ref } from 'vue'
|
|
2
2
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
3
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
4
4
|
import RsForm from '../components/RsForm.vue'
|
|
@@ -33,6 +33,88 @@ describe('RsForm', () => {
|
|
|
33
33
|
expect(wrapper.classes()).toContain('rs-form--label-left')
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
it('defaults labelPosition class to top', () => {
|
|
37
|
+
const wrapper = mount(RsForm)
|
|
38
|
+
expect(wrapper.classes()).toContain('rs-form--label-top')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('propagates labelPosition left to child inputs', async () => {
|
|
42
|
+
const wrapper = mount(RsForm, {
|
|
43
|
+
props: { labelPosition: 'left', labelWidth: '5rem' },
|
|
44
|
+
slots: { default: '<RsInput model-value="" label="名称" />' },
|
|
45
|
+
global: { components: { RsInput } },
|
|
46
|
+
})
|
|
47
|
+
await flushPromises()
|
|
48
|
+
const field = wrapper.find('.rs-field')
|
|
49
|
+
expect(field.classes()).toContain('rs-field--label-left')
|
|
50
|
+
expect(field.attributes('style') ?? '').toContain('--rs-field-label-width: 5rem')
|
|
51
|
+
wrapper.unmount()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('propagates labelAlign end only when labelPosition is left', async () => {
|
|
55
|
+
const leftAlign = mount(RsForm, {
|
|
56
|
+
props: { labelPosition: 'left', labelAlign: 'end', labelWidth: '6rem' },
|
|
57
|
+
slots: { default: '<RsInput model-value="" label="邮箱" />' },
|
|
58
|
+
global: { components: { RsInput } },
|
|
59
|
+
})
|
|
60
|
+
await flushPromises()
|
|
61
|
+
expect(leftAlign.find('.rs-field').classes()).toContain('rs-field--label-align-end')
|
|
62
|
+
leftAlign.unmount()
|
|
63
|
+
|
|
64
|
+
const topAlign = mount(RsForm, {
|
|
65
|
+
props: { labelPosition: 'top', labelAlign: 'end' },
|
|
66
|
+
slots: { default: '<RsInput model-value="" label="邮箱" />' },
|
|
67
|
+
global: { components: { RsInput } },
|
|
68
|
+
})
|
|
69
|
+
await flushPromises()
|
|
70
|
+
expect(topAlign.find('.rs-field').classes()).toContain('rs-field--label-top')
|
|
71
|
+
expect(topAlign.find('.rs-field').classes()).not.toContain('rs-field--label-align-end')
|
|
72
|
+
topAlign.unmount()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('allows field labelPosition to override Form', async () => {
|
|
76
|
+
const Host = defineComponent({
|
|
77
|
+
components: { RsForm, RsInput },
|
|
78
|
+
template: `
|
|
79
|
+
<RsForm label-position="left" label-width="5rem">
|
|
80
|
+
<RsInput model-value="" label="继承" />
|
|
81
|
+
<RsInput model-value="" label="覆盖" label-position="top" />
|
|
82
|
+
</RsForm>
|
|
83
|
+
`,
|
|
84
|
+
})
|
|
85
|
+
const wrapper = mount(Host)
|
|
86
|
+
await flushPromises()
|
|
87
|
+
const fields = wrapper.findAll('.rs-field')
|
|
88
|
+
expect(fields[0]!.classes()).toContain('rs-field--label-left')
|
|
89
|
+
expect(fields[1]!.classes()).toContain('rs-field--label-top')
|
|
90
|
+
wrapper.unmount()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('updates child label layout when Form labelPosition changes', async () => {
|
|
94
|
+
const Host = defineComponent({
|
|
95
|
+
components: { RsForm, RsInput },
|
|
96
|
+
setup() {
|
|
97
|
+
const labelPosition = ref<'top' | 'left'>('top')
|
|
98
|
+
return { labelPosition }
|
|
99
|
+
},
|
|
100
|
+
template: `
|
|
101
|
+
<RsForm :label-position="labelPosition" label-width="5rem">
|
|
102
|
+
<RsInput model-value="" label="名称" />
|
|
103
|
+
</RsForm>
|
|
104
|
+
`,
|
|
105
|
+
})
|
|
106
|
+
const wrapper = mount(Host)
|
|
107
|
+
await flushPromises()
|
|
108
|
+
expect(wrapper.find('.rs-form').classes()).toContain('rs-form--label-top')
|
|
109
|
+
expect(wrapper.find('.rs-field').classes()).toContain('rs-field--label-top')
|
|
110
|
+
|
|
111
|
+
wrapper.vm.labelPosition = 'left'
|
|
112
|
+
await flushPromises()
|
|
113
|
+
expect(wrapper.find('.rs-form').classes()).toContain('rs-form--label-left')
|
|
114
|
+
expect(wrapper.find('.rs-field').classes()).toContain('rs-field--label-left')
|
|
115
|
+
wrapper.unmount()
|
|
116
|
+
})
|
|
117
|
+
|
|
36
118
|
it('validate returns valid when all fields pass', async () => {
|
|
37
119
|
const Host = defineComponent({
|
|
38
120
|
components: { RsForm, RsInput },
|
|
@@ -43,7 +125,7 @@ describe('RsForm', () => {
|
|
|
43
125
|
},
|
|
44
126
|
template: `
|
|
45
127
|
<RsForm ref="formRef">
|
|
46
|
-
<RsInput v-model="email" rule="email" required />
|
|
128
|
+
<RsInput v-model="email" name="email" rule="email" required />
|
|
47
129
|
</RsForm>
|
|
48
130
|
`,
|
|
49
131
|
})
|
|
@@ -64,7 +146,7 @@ describe('RsForm', () => {
|
|
|
64
146
|
},
|
|
65
147
|
template: `
|
|
66
148
|
<RsForm ref="formRef">
|
|
67
|
-
<RsInput v-model="email" rule="email" required />
|
|
149
|
+
<RsInput v-model="email" name="email" rule="email" required />
|
|
68
150
|
</RsForm>
|
|
69
151
|
`,
|
|
70
152
|
})
|
|
@@ -85,7 +167,7 @@ describe('RsForm', () => {
|
|
|
85
167
|
},
|
|
86
168
|
template: `
|
|
87
169
|
<RsForm ref="formRef">
|
|
88
|
-
<RsInput v-model="name" />
|
|
170
|
+
<RsInput v-model="name" name="name" />
|
|
89
171
|
</RsForm>
|
|
90
172
|
`,
|
|
91
173
|
})
|
|
@@ -109,7 +191,7 @@ describe('RsForm', () => {
|
|
|
109
191
|
},
|
|
110
192
|
template: `
|
|
111
193
|
<RsForm ref="formRef">
|
|
112
|
-
<RsInput v-model="email" rule="email" required />
|
|
194
|
+
<RsInput v-model="email" name="email" rule="email" required />
|
|
113
195
|
</RsForm>
|
|
114
196
|
`,
|
|
115
197
|
})
|
|
@@ -156,7 +238,7 @@ describe('RsForm', () => {
|
|
|
156
238
|
},
|
|
157
239
|
template: `
|
|
158
240
|
<RsForm ref="formRef">
|
|
159
|
-
<RsSelect v-model="role" :options="roleOptions" required />
|
|
241
|
+
<RsSelect v-model="role" name="role" :options="roleOptions" required />
|
|
160
242
|
</RsForm>
|
|
161
243
|
`,
|
|
162
244
|
})
|
|
@@ -170,4 +252,166 @@ describe('RsForm', () => {
|
|
|
170
252
|
expect(valid.valid).toBe(true)
|
|
171
253
|
wrapper.unmount()
|
|
172
254
|
})
|
|
255
|
+
|
|
256
|
+
it('supports centralized rules by field name', async () => {
|
|
257
|
+
const Host = defineComponent({
|
|
258
|
+
components: { RsForm, RsInput },
|
|
259
|
+
setup() {
|
|
260
|
+
const formRef = ref<InstanceType<typeof RsForm> | null>(null)
|
|
261
|
+
const userId = ref('')
|
|
262
|
+
const rules = {
|
|
263
|
+
userId: [
|
|
264
|
+
{ required: true, message: '必填' },
|
|
265
|
+
{ min: 3, message: '至少 3 位' },
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
return { formRef, userId, rules }
|
|
269
|
+
},
|
|
270
|
+
template: `
|
|
271
|
+
<RsForm ref="formRef" :rules="rules">
|
|
272
|
+
<RsInput v-model="userId" name="userId" />
|
|
273
|
+
</RsForm>
|
|
274
|
+
`,
|
|
275
|
+
})
|
|
276
|
+
const wrapper = mount(Host)
|
|
277
|
+
await flushPromises()
|
|
278
|
+
|
|
279
|
+
const empty = await wrapper.vm.formRef!.validate()
|
|
280
|
+
expect(empty.valid).toBe(false)
|
|
281
|
+
expect(empty.errors.userId).toBe('必填')
|
|
282
|
+
|
|
283
|
+
wrapper.vm.userId = 'ab'
|
|
284
|
+
await flushPromises()
|
|
285
|
+
const short = await wrapper.vm.formRef!.validate()
|
|
286
|
+
expect(short.valid).toBe(false)
|
|
287
|
+
expect(short.errors.userId).toBe('至少 3 位')
|
|
288
|
+
|
|
289
|
+
wrapper.vm.userId = 'abc'
|
|
290
|
+
await flushPromises()
|
|
291
|
+
const ok = await wrapper.vm.formRef!.validate()
|
|
292
|
+
expect(ok.valid).toBe(true)
|
|
293
|
+
expect(ok.errors).toEqual({})
|
|
294
|
+
wrapper.unmount()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('validateField only checks one named field', async () => {
|
|
298
|
+
const Host = defineComponent({
|
|
299
|
+
components: { RsForm, RsInput },
|
|
300
|
+
setup() {
|
|
301
|
+
const formRef = ref<InstanceType<typeof RsForm> | null>(null)
|
|
302
|
+
const userId = ref('')
|
|
303
|
+
const email = ref('bad')
|
|
304
|
+
const rules = {
|
|
305
|
+
userId: [{ required: true, message: 'user required' }],
|
|
306
|
+
email: [{ type: 'email' as const, message: 'bad email' }],
|
|
307
|
+
}
|
|
308
|
+
return { formRef, userId, email, rules }
|
|
309
|
+
},
|
|
310
|
+
template: `
|
|
311
|
+
<RsForm ref="formRef" :rules="rules">
|
|
312
|
+
<RsInput v-model="userId" name="userId" />
|
|
313
|
+
<RsInput v-model="email" name="email" />
|
|
314
|
+
</RsForm>
|
|
315
|
+
`,
|
|
316
|
+
})
|
|
317
|
+
const wrapper = mount(Host)
|
|
318
|
+
await flushPromises()
|
|
319
|
+
const result = await wrapper.vm.formRef!.validateField('userId')
|
|
320
|
+
expect(result.valid).toBe(false)
|
|
321
|
+
expect(result.message).toBe('user required')
|
|
322
|
+
expect(result.name).toBe('userId')
|
|
323
|
+
wrapper.unmount()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('supports async validator in rules', async () => {
|
|
327
|
+
const Host = defineComponent({
|
|
328
|
+
components: { RsForm, RsInput },
|
|
329
|
+
setup() {
|
|
330
|
+
const formRef = ref<InstanceType<typeof RsForm> | null>(null)
|
|
331
|
+
const username = ref('admin')
|
|
332
|
+
const rules = {
|
|
333
|
+
username: [
|
|
334
|
+
{
|
|
335
|
+
async validator(value: unknown) {
|
|
336
|
+
await Promise.resolve()
|
|
337
|
+
if (String(value) === 'admin') return '占用'
|
|
338
|
+
return true
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
}
|
|
343
|
+
return { formRef, username, rules }
|
|
344
|
+
},
|
|
345
|
+
template: `
|
|
346
|
+
<RsForm ref="formRef" :rules="rules">
|
|
347
|
+
<RsInput v-model="username" name="username" />
|
|
348
|
+
</RsForm>
|
|
349
|
+
`,
|
|
350
|
+
})
|
|
351
|
+
const wrapper = mount(Host)
|
|
352
|
+
await flushPromises()
|
|
353
|
+
const result = await wrapper.vm.formRef!.validate()
|
|
354
|
+
expect(result.valid).toBe(false)
|
|
355
|
+
expect(result.errors.username).toBe('占用')
|
|
356
|
+
wrapper.unmount()
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('renders custom Form #error slot content', async () => {
|
|
360
|
+
const Host = defineComponent({
|
|
361
|
+
components: { RsForm, RsInput },
|
|
362
|
+
setup() {
|
|
363
|
+
const formRef = ref<InstanceType<typeof RsForm> | null>(null)
|
|
364
|
+
const name = ref('')
|
|
365
|
+
const rules = {
|
|
366
|
+
name: [{ required: true, message: '必填昵称' }],
|
|
367
|
+
}
|
|
368
|
+
return { formRef, name, rules }
|
|
369
|
+
},
|
|
370
|
+
template: `
|
|
371
|
+
<RsForm ref="formRef" :rules="rules">
|
|
372
|
+
<template #error="{ message }">
|
|
373
|
+
<span class="custom-form-error">CUSTOM:{{ message }}</span>
|
|
374
|
+
</template>
|
|
375
|
+
<RsInput v-model="name" name="name" />
|
|
376
|
+
</RsForm>
|
|
377
|
+
`,
|
|
378
|
+
})
|
|
379
|
+
const wrapper = mount(Host)
|
|
380
|
+
await flushPromises()
|
|
381
|
+
await wrapper.vm.formRef!.validate()
|
|
382
|
+
await flushPromises()
|
|
383
|
+
expect(wrapper.find('.custom-form-error').text()).toBe('CUSTOM:必填昵称')
|
|
384
|
+
wrapper.unmount()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('field #error slot overrides Form errorRender', async () => {
|
|
388
|
+
const Host = defineComponent({
|
|
389
|
+
components: { RsForm, RsInput },
|
|
390
|
+
setup() {
|
|
391
|
+
const formRef = ref<InstanceType<typeof RsForm> | null>(null)
|
|
392
|
+
const name = ref('')
|
|
393
|
+
const rules = {
|
|
394
|
+
name: [{ required: true, message: '必填' }],
|
|
395
|
+
}
|
|
396
|
+
const errorRender = () => h('span', { class: 'from-form' }, 'FORM')
|
|
397
|
+
return { formRef, name, rules, errorRender }
|
|
398
|
+
},
|
|
399
|
+
template: `
|
|
400
|
+
<RsForm ref="formRef" :rules="rules" :error-render="errorRender">
|
|
401
|
+
<RsInput v-model="name" name="name">
|
|
402
|
+
<template #error="{ message }">
|
|
403
|
+
<span class="from-field">FIELD:{{ message }}</span>
|
|
404
|
+
</template>
|
|
405
|
+
</RsInput>
|
|
406
|
+
</RsForm>
|
|
407
|
+
`,
|
|
408
|
+
})
|
|
409
|
+
const wrapper = mount(Host)
|
|
410
|
+
await flushPromises()
|
|
411
|
+
await wrapper.vm.formRef!.validate()
|
|
412
|
+
await flushPromises()
|
|
413
|
+
expect(wrapper.find('.from-field').text()).toBe('FIELD:必填')
|
|
414
|
+
expect(wrapper.find('.from-form').exists()).toBe(false)
|
|
415
|
+
wrapper.unmount()
|
|
416
|
+
})
|
|
173
417
|
})
|