diff --git a/packages/components/form/__tests__/form-item.spec.tsx b/packages/components/form/__tests__/form-item.spec.tsx new file mode 100644 index 0000000000..e3b58e4782 --- /dev/null +++ b/packages/components/form/__tests__/form-item.spec.tsx @@ -0,0 +1,109 @@ +import { ref, reactive, nextTick } from 'vue' +import { mount } from '@vue/test-utils' +import { rAF } from '@element-plus/test-utils/tick' +import Input from '@element-plus/components/input' +import FormItem from '../src/form-item.vue' +import DynamicFormItem from '../mocks/mock-data' + +import type { VueWrapper } from '@vue/test-utils' +import type { InputInstance } from '@element-plus/components/input' + +type FormItemInstance = InstanceType + +describe('ElFormItem', () => { + let wrapper: VueWrapper> + const formItemRef = ref() + const inputRef = ref() + const model = reactive({ + email: '', + }) + + const createComponent = () => { + wrapper = mount(DynamicFormItem, { + props: { + model, + }, + slots: { + default: () => ( + + + + ), + }, + }) + } + + beforeAll(() => jest.spyOn(console, 'warn').mockImplementation()) + afterAll(() => (console.warn as any as jest.SpyInstance).mockRestore()) + afterEach(() => { + formItemRef.value = undefined + inputRef.value = undefined + model.email = '' + }) + + describe('When initialized', () => { + it('should throw when no form on top', () => { + const warnHandler = jest.fn() + try { + mount(FormItem, { + global: { + config: { + warnHandler, + }, + }, + }) + } catch (e) { + expect(e).toBeInstanceOf(Error) + } + expect(warnHandler).toHaveBeenCalled() + }) + }) + + describe('when validation dispatches', () => { + beforeEach(() => { + createComponent() + }) + + afterEach(() => { + wrapper.unmount() + }) + + describe('it successes', () => { + it('should be able to validate successfully without callback', async () => { + const emailInput = formItemRef.value! + model.email = 'test' + await nextTick() + await rAF() + expect(emailInput.validate('')).resolves.toBe(true) + }) + + it('should be able to validate successfully with callback', async () => { + const emailInput = formItemRef.value! + model.email = 'test' + await nextTick() + await rAF() + const callback = jest.fn() + expect(emailInput.validate('', callback)).resolves.toBe(true) + await rAF() + expect(callback).toHaveBeenCalledWith(true) + }) + }) + + describe('it fails', () => { + it('should be able to validate without callback', async () => { + const emailInput = formItemRef.value! + expect(emailInput.validate('')).rejects.toHaveProperty('email') + expect(console.warn).toHaveBeenCalled() + }) + + it('should be able to validate with callback without throwing rejection', async () => { + const emailInput = formItemRef.value! + const callback = jest.fn() + expect(console.warn).toHaveBeenCalled() + expect(emailInput.validate('', callback)).resolves.toBe(false) + await rAF() + expect(callback).toHaveBeenCalled() + }) + }) + }) +}) diff --git a/packages/components/form/__tests__/form.spec.tsx b/packages/components/form/__tests__/form.spec.tsx index e7675feef7..080330ba6f 100644 --- a/packages/components/form/__tests__/form.spec.tsx +++ b/packages/components/form/__tests__/form.spec.tsx @@ -2,17 +2,21 @@ import { nextTick, reactive, ref } from 'vue' import { mount } from '@vue/test-utils' import { rAF } from '@element-plus/test-utils/tick' import installStyle from '@element-plus/test-utils/style-plugin' -import Checkbox from '@element-plus/components/checkbox/src/checkbox.vue' -import CheckboxGroup from '@element-plus/components/checkbox/src/checkbox-group.vue' +import { + ElCheckboxGroup as CheckboxGroup, + ElCheckbox as Checkbox, +} from '@element-plus/components/checkbox' import Input from '@element-plus/components/input' import Form from '../src/form.vue' import FormItem from '../src/form-item.vue' import DynamicDomainForm, { formatDomainError } from '../mocks/mock-data' import type { VueWrapper } from '@vue/test-utils' +import type { ValidateFieldsError } from 'async-validator' import type { FormRules } from '@element-plus/tokens' -import type { FormInstance } from '../src/form' -import type { FormItemInstance } from '../src/form-item' + +type FormInstance = InstanceType +type FormItemInstance = InstanceType const findStyle = (wrapper: VueWrapper, selector: string) => wrapper.find(selector).element.style @@ -262,13 +266,13 @@ describe('Form', () => { }) const form = wrapper.findComponent(Form).vm as FormInstance form - .validate(async (valid) => { + .validate(async (valid: boolean) => { expect(valid).toBe(false) await nextTick() expect(wrapper.find('.el-form-item__error').exists()).toBe(false) done() }) - .catch((e) => { + .catch((e: ValidateFieldsError) => { expect(e).toBeDefined() }) }) @@ -531,11 +535,12 @@ describe('Form', () => { const onSuccess = jest.fn() const onError = jest.fn() let wrapper: VueWrapper> - const createComponent = () => { + const createComponent = (onSubmit?: jest.MockedFunction) => { wrapper = mount(DynamicDomainForm, { props: { onSuccess, onError, + onSubmit, }, }) } @@ -580,5 +585,15 @@ describe('Form', () => { await rAF() expect(onError).toHaveBeenLastCalledWith(formatDomainError(1)) }) + + it('should not throw error when callback passed in', async () => { + const onSubmit = jest.fn() + createComponent(onSubmit) + + await findSubmitButton().trigger('click') + await rAF() + expect(onError).not.toHaveBeenCalled() + expect(onSubmit).toHaveBeenCalled() + }) }) }) diff --git a/packages/components/form/index.ts b/packages/components/form/index.ts index 1c0045f389..b7a279c8fc 100644 --- a/packages/components/form/index.ts +++ b/packages/components/form/index.ts @@ -11,3 +11,6 @@ export const ElFormItem = withNoopInstall(FormItem) export * from './src/form' export * from './src/form-item' export * from './src/types' + +export type FormInstance = InstanceType +export type FormItemInstance = InstanceType diff --git a/packages/components/form/mocks/mock-data.tsx b/packages/components/form/mocks/mock-data.tsx index 8fc80cdb2a..14ee848825 100644 --- a/packages/components/form/mocks/mock-data.tsx +++ b/packages/components/form/mocks/mock-data.tsx @@ -1,11 +1,9 @@ -import { defineComponent, ref } from 'vue' +import { defineComponent, ref, toRef } from 'vue' import Input from '@element-plus/components/input' import Button from '@element-plus/components/button' import Form from '../src/form.vue' import FormItem from '../src/form-item.vue' -import type { FormInstance } from '../src/form' - interface DomainItem { key: number value: string @@ -15,8 +13,11 @@ const DynamicDomainForm = defineComponent({ props: { onSuccess: Function, onError: Function, + onSubmit: Function, + model: Object, }, - setup(props) { + setup(props, { slots }) { + const propsModel = toRef(props, 'model') const model = ref({ domains: [ { @@ -26,7 +27,7 @@ const DynamicDomainForm = defineComponent({ ], }) - const formRef = ref() + const formRef = ref>() const removeDomain = (item: DomainItem) => { const index = model.value.domains.indexOf(item) @@ -45,7 +46,11 @@ const DynamicDomainForm = defineComponent({ const submitForm = async () => { if (!formRef.value) return try { - await formRef.value.validate() + const validate = props.onSubmit + ? formRef.value.validate(props.onSubmit as any) + : formRef.value.validate() + + await validate props.onSuccess?.() } catch (e) { props.onError?.(e) @@ -53,7 +58,10 @@ const DynamicDomainForm = defineComponent({ } return () => ( -
+ {model.value.domains.map((domain, index) => { return ( ) })} + {slots.default?.()}