From 29be0139e7341982772e47af71ffc576366a2b2a Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 16 May 2021 21:10:05 +0800 Subject: [PATCH 01/12] 1.20.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41c40d11..cb6ff975 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-field-form", - "version": "1.20.0", + "version": "1.20.1", "description": "React Form Component", "typings": "es/index.d.ts", "engines": { From c6f548f3aedfec39814ea2db318e685b1e928ec4 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 31 May 2021 11:23:12 +0800 Subject: [PATCH 02/12] feat: Support onError event --- docs/examples/validate.tsx | 262 +++++++++++++++++++------------------ src/Field.tsx | 41 +++--- 2 files changed, 158 insertions(+), 145 deletions(-) diff --git a/docs/examples/validate.tsx b/docs/examples/validate.tsx index ceaf7226..1721e2dc 100644 --- a/docs/examples/validate.tsx +++ b/docs/examples/validate.tsx @@ -33,139 +33,141 @@ const FieldState = ({ form, name }) => { ); }; -export default class Demo extends React.Component { - onFinish = values => { +export default () => { + const onFinish = (values: object) => { console.log('Finish:', values); }; - render() { - return ( -
-

Validate Form

-
- {(values, form) => { - const usernameError = form.getFieldError('username'); - const passwordError = form.getFieldError('password'); - const password2Error = form.getFieldError('password2'); - const errors = form.getFieldsError(); - if (errors) { - console.log('Render with Errors:', values, form.getFieldsError()); - } - - return ( - - - { - console.log('Username change:', value); - }} - /> - - - {usernameError} - - ({ - validator(_, __, callback) { - if (context.isFieldTouched('password2')) { - context.validateFields(['password2']); - callback(); - return; - } + const onNameError = (errors: string[]) => { + console.log('🐞 Name Error Change:', errors); + }; + + return ( +
+

Validate Form

+ + {(values, form) => { + const usernameError = form.getFieldError('username'); + const passwordError = form.getFieldError('password'); + const password2Error = form.getFieldError('password2'); + const errors = form.getFieldsError(); + if (errors) { + console.log('Render with Errors:', values, form.getFieldsError()); + } + + return ( + + + { + console.log('Username change:', value); + }} + /> + + + {usernameError} + + ({ + validator(_, __, callback) { + if (context.isFieldTouched('password2')) { + context.validateFields(['password2']); callback(); - }, - }), - ]} - > - - - - {passwordError} - - ({ - validator(rule, value, callback) { - const { password } = context.getFieldsValue(true); - if (password !== value) { - callback('Not Same as password1!!!'); + return; + } + callback(); + }, + }), + ]} + > + + + + {passwordError} + + ({ + validator(rule, value, callback) { + const { password } = context.getFieldsValue(true); + if (password !== value) { + callback('Not Same as password1!!!'); + } + callback(); + }, + }), + ]} + > + + + + {password2Error} + + + {(control, meta) => ( +
+ Use Meta: + + + {meta.errors} +
+ )} +
+ + { + if (Number(value).toString() === value) { + callback(); } - callback(); - }, - }), - ]} - > - - - - {password2Error} - - - {(control, meta) => ( -
- Use Meta: - - - {meta.errors} -
- )} -
- - { - if (Number(value).toString() === value) { - callback(); - } - callback('Integer number only!'); - }, 1000); - }, - validateTrigger: 'onChange', + callback('Integer number only!'); + }, 1000); }, - ]} - > - {(control, meta) => ( -
- Multiple `validateTrigger`: -
    -
  • Required check on submit
  • -
  • Number check on change
  • -
- - - {meta.errors} -
- )} -
- -
- - - - -
- ); - }} - -
- ); - } -} + validateTrigger: 'onChange', + }, + ]} + > + {(control, meta) => ( +
+ Multiple `validateTrigger`: +
    +
  • Required check on submit
  • +
  • Number check on change
  • +
+ + + {meta.errors} +
+ )} +
+ +
+ + + + +
+ ); + }} + +
+ ); +}; diff --git a/src/Field.tsx b/src/Field.tsx index c2d211c8..65c96c44 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -1,7 +1,7 @@ import toChildrenArray from 'rc-util/lib/Children/toArray'; import warning from 'rc-util/lib/warning'; import * as React from 'react'; -import { +import type { FieldEntity, FormInstance, InternalNamePath, @@ -24,8 +24,11 @@ import { defaultGetValueFromEvent, getNamePath, getValue, + isSimilar, } from './utils/valueUtil'; +const EMPTY_ERRORS: string[] = []; + export type ShouldUpdate = | boolean | ((prevValues: Values, nextValues: Values, info: { source?: string }) => boolean); @@ -44,6 +47,7 @@ function requireUpdate( return prevValue !== nextValue; } +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style interface ChildProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any [name: string]: any; @@ -72,6 +76,7 @@ export interface InternalFieldProps { messageVariables?: Record; initialValue?: any; onReset?: () => void; + onError?: (errors: string[]) => void; preserve?: boolean; /** @private Passed by Form.List props. Do not use since it will break by path check. */ @@ -128,7 +133,7 @@ class Field extends React.Component implements F private prevValidating: boolean; - private errors: string[] = []; + private errors: string[] = EMPTY_ERRORS; // ============================== Subscriptions ============================== constructor(props: InternalFieldProps) { @@ -185,14 +190,12 @@ class Field extends React.Component implements F public getRules = (): RuleObject[] => { const { rules = [], fieldContext } = this.props; - return rules.map( - (rule: Rule): RuleObject => { - if (typeof rule === 'function') { - return rule(fieldContext); - } - return rule; - }, - ); + return rules.map((rule: Rule): RuleObject => { + if (typeof rule === 'function') { + return rule(fieldContext); + } + return rule; + }); }; public reRender() { @@ -227,7 +230,7 @@ class Field extends React.Component implements F this.touched = true; this.dirty = true; this.validatePromise = null; - this.errors = []; + this.errors = EMPTY_ERRORS; } switch (info.type) { @@ -237,7 +240,7 @@ class Field extends React.Component implements F this.touched = false; this.dirty = false; this.validatePromise = null; - this.errors = []; + this.errors = EMPTY_ERRORS; if (onReset) { onReset(); @@ -321,6 +324,8 @@ class Field extends React.Component implements F }; public validateRules = (options?: ValidateOptions): Promise => { + const prevErrors = this.errors; + // We should fixed namePath & value to avoid developer change then by form function const namePath = this.getNamePath(); const currentValue = this.getValue(); @@ -331,7 +336,7 @@ class Field extends React.Component implements F return []; } - const { validateFirst = false, messageVariables } = this.props; + const { validateFirst = false, messageVariables, onError } = this.props; const { triggerName } = (options || {}) as ValidateOptions; let filteredRules = this.getRules(); @@ -357,10 +362,16 @@ class Field extends React.Component implements F promise .catch(e => e) - .then((errors: string[] = []) => { + .then((errors: string[] = EMPTY_ERRORS) => { if (this.validatePromise === rootPromise) { this.validatePromise = null; this.errors = errors; + + // Trigger error if changed + if (!isSimilar(prevErrors, errors)) { + onError?.(errors); + } + this.reRender(); } }); @@ -370,7 +381,7 @@ class Field extends React.Component implements F this.validatePromise = rootPromise; this.dirty = true; - this.errors = []; + this.errors = EMPTY_ERRORS; // Force trigger re-render since we need sync renderProps with new meta this.reRender(); From 1ee767c266f5d1d2593088b7a3ed1508db67cfef Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 31 May 2021 11:47:13 +0800 Subject: [PATCH 03/12] fix: Every update errors should trigger event --- docs/examples/validate-perf.tsx | 6 ++++++ src/Field.tsx | 30 +++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/examples/validate-perf.tsx b/docs/examples/validate-perf.tsx index bd9ea48a..f07cdc76 100644 --- a/docs/examples/validate-perf.tsx +++ b/docs/examples/validate-perf.tsx @@ -34,6 +34,10 @@ export default class Demo extends React.Component { console.log('Failed:', errorInfo); }; + public onPasswordError = (errors: string[]) => { + console.log('🐞 Password Error:', errors); + }; + public render() { return (
@@ -50,6 +54,7 @@ export default class Demo extends React.Component { name="password" messageVariables={{ displayName: '密码' }} rules={[{ required: true }]} + onError={this.onPasswordError} > @@ -118,6 +123,7 @@ export default class Demo extends React.Component { > Reset +
); diff --git a/src/Field.tsx b/src/Field.tsx index 65c96c44..c5b2d43d 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -214,6 +214,15 @@ class Field extends React.Component implements F })); }; + /** Update `this.error`. If `onError` provided, trigger it */ + public updateError(prevErrors: string[], nextErrors: string[]) { + const { onError } = this.props; + if (onError && !isSimilar(prevErrors, nextErrors)) { + onError(nextErrors); + } + this.errors = nextErrors; + } + // ========================= Field Entity Interfaces ========================= // Trigger by store update. Check if need update the component public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => { @@ -225,12 +234,14 @@ class Field extends React.Component implements F const namePathMatch = namePathList && containsNamePath(namePathList, namePath); + const prevErrors = this.errors; + // `setFieldsValue` is a quick access to update related status if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue) { this.touched = true; this.dirty = true; this.validatePromise = null; - this.errors = EMPTY_ERRORS; + this.updateError(prevErrors, EMPTY_ERRORS); } switch (info.type) { @@ -240,11 +251,9 @@ class Field extends React.Component implements F this.touched = false; this.dirty = false; this.validatePromise = null; - this.errors = EMPTY_ERRORS; + this.updateError(prevErrors, EMPTY_ERRORS); - if (onReset) { - onReset(); - } + onReset?.(); this.refresh(); return; @@ -261,7 +270,7 @@ class Field extends React.Component implements F this.validatePromise = data.validating ? Promise.resolve([]) : null; } if ('errors' in data) { - this.errors = data.errors || []; + this.updateError(prevErrors, data.errors || EMPTY_ERRORS); } this.dirty = true; @@ -336,7 +345,7 @@ class Field extends React.Component implements F return []; } - const { validateFirst = false, messageVariables, onError } = this.props; + const { validateFirst = false, messageVariables } = this.props; const { triggerName } = (options || {}) as ValidateOptions; let filteredRules = this.getRules(); @@ -365,12 +374,7 @@ class Field extends React.Component implements F .then((errors: string[] = EMPTY_ERRORS) => { if (this.validatePromise === rootPromise) { this.validatePromise = null; - this.errors = errors; - - // Trigger error if changed - if (!isSimilar(prevErrors, errors)) { - onError?.(errors); - } + this.updateError(prevErrors, errors); this.reRender(); } From da301bb1bf7a5d13c6f18a984c1a51024df011dd Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 31 May 2021 11:53:09 +0800 Subject: [PATCH 04/12] test: Test driven of onError --- tests/index.test.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/index.test.js b/tests/index.test.js index 419fd0c5..81f587d1 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -101,6 +101,7 @@ describe('Form.Basic', () => { it(name, async () => { let form; const onReset = jest.fn(); + const onError = jest.fn(); const wrapper = mount(
@@ -109,7 +110,12 @@ describe('Form.Basic', () => { form = instance; }} > - + @@ -120,12 +126,14 @@ describe('Form.Basic', () => { expect(form.getFieldValue('username')).toEqual('Bamboo'); expect(form.getFieldError('username')).toEqual([]); expect(form.isFieldTouched('username')).toBeTruthy(); - + expect(onError).not.toHaveBeenCalled(); expect(onReset).not.toHaveBeenCalled(); + form.resetFields(...args); expect(form.getFieldValue('username')).toEqual(undefined); expect(form.getFieldError('username')).toEqual([]); expect(form.isFieldTouched('username')).toBeFalsy(); + expect(onError).not.toHaveBeenCalled(); expect(onReset).toHaveBeenCalled(); onReset.mockRestore(); @@ -133,12 +141,14 @@ describe('Form.Basic', () => { expect(form.getFieldValue('username')).toEqual(''); expect(form.getFieldError('username')).toEqual(["'username' is required"]); expect(form.isFieldTouched('username')).toBeTruthy(); - + expect(onError).toHaveBeenCalledWith(["'username' is required"]); expect(onReset).not.toHaveBeenCalled(); + form.resetFields(...args); expect(form.getFieldValue('username')).toEqual(undefined); expect(form.getFieldError('username')).toEqual([]); expect(form.isFieldTouched('username')).toBeFalsy(); + expect(onError).toHaveBeenCalledWith([]); expect(onReset).toHaveBeenCalled(); }); } From 0175ee06ed03fac3aad997fd20ce83f3901632a9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 15:48:15 +0800 Subject: [PATCH 05/12] feat: All message support variable --- src/utils/validateUtil.ts | 83 +++++++++++---------------------------- 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 1c702aae..1ce75034 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -1,15 +1,7 @@ import RawAsyncValidator from 'async-validator'; import * as React from 'react'; import warning from 'rc-util/lib/warning'; -import type { - InternalNamePath, - ValidateOptions, - ValidateMessages, - RuleObject, - StoreValue, -} from '../interface'; -import { setValues } from './valueUtil'; -import { defaultValidateMessages } from './messages'; +import type { InternalNamePath, ValidateOptions, RuleObject, StoreValue } from '../interface'; // Remove incorrect original ts define const AsyncValidator: any = RawAsyncValidator; @@ -25,46 +17,6 @@ function replaceMessage(template: string, kv: Record): string { }); } -/** - * We use `async-validator` to validate rules. So have to hot replace the message with validator. - * { required: '${name} is required' } => { required: () => 'field is required' } - */ -function convertMessages( - messages: ValidateMessages, - name: string, - rule: RuleObject, - messageVariables?: Record, -): ValidateMessages { - const kv = { - ...(rule as Record), - name, - enum: (rule.enum || []).join(', '), - }; - - const replaceFunc = (template: string, additionalKV?: Record) => () => - replaceMessage(template, { ...kv, ...additionalKV }); - - /* eslint-disable no-param-reassign */ - function fillTemplate(source: ValidateMessages, target: ValidateMessages = {}) { - Object.keys(source).forEach(ruleName => { - const value = source[ruleName]; - if (typeof value === 'string') { - target[ruleName] = replaceFunc(value, messageVariables); - } else if (value && typeof value === 'object') { - target[ruleName] = {}; - fillTemplate(value, target[ruleName]); - } else { - target[ruleName] = value; - } - }); - - return target; - } - /* eslint-enable */ - - return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages; -} - async function validateRule( name: string, value: StoreValue, @@ -84,13 +36,8 @@ async function validateRule( [name]: [cloneRule], }); - const messages: ValidateMessages = convertMessages( - options.validateMessages, - name, - cloneRule, - messageVariables, - ); - validator.messages(messages); + const { validateMessages } = options; + validator.messages(validateMessages); let result = []; @@ -106,7 +53,7 @@ async function validateRule( ); } else { console.error(errObj); - result = [(messages.default as () => string)()]; + result = [(validateMessages.default as () => string)()]; } } @@ -120,7 +67,22 @@ async function validateRule( return subResults.reduce((prev, errors) => [...prev, ...errors], []); } - return result; + // Replace message with variables + const kv = { + ...(rule as Record), + name, + enum: (rule.enum || []).join(', '), + ...messageVariables, + }; + + const fillVariableResult = result.map(error => { + if (typeof error === 'string') { + return replaceMessage(error, kv); + } + return error; + }); + + return fillVariableResult; } /** @@ -211,9 +173,8 @@ export function validateRules( validateRule(name, value, rule, options, messageVariables), ); - summaryPromise = (validateFirst - ? finishOnFirstFailed(rulePromises) - : finishOnAllFailed(rulePromises) + summaryPromise = ( + validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises) ).then((errors: string[]): string[] | Promise => { if (!errors.length) { return []; From 4a6b7916fb7043aee6859b380182f389b30ee923 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 16:04:26 +0800 Subject: [PATCH 06/12] fix: Fill template logic --- src/utils/validateUtil.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 1ce75034..4b80212a 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -2,6 +2,8 @@ import RawAsyncValidator from 'async-validator'; import * as React from 'react'; import warning from 'rc-util/lib/warning'; import type { InternalNamePath, ValidateOptions, RuleObject, StoreValue } from '../interface'; +import { defaultValidateMessages } from './messages'; +import { setValues } from './valueUtil'; // Remove incorrect original ts define const AsyncValidator: any = RawAsyncValidator; @@ -36,8 +38,8 @@ async function validateRule( [name]: [cloneRule], }); - const { validateMessages } = options; - validator.messages(validateMessages); + const messages = setValues({}, defaultValidateMessages, options.validateMessages); + validator.messages(messages); let result = []; @@ -53,7 +55,7 @@ async function validateRule( ); } else { console.error(errObj); - result = [(validateMessages.default as () => string)()]; + result = [messages.default]; } } From f3a372b262a9ae1c9865d63843544f32b49476dc Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 17:00:17 +0800 Subject: [PATCH 07/12] feat: support warning --- docs/examples/components/LabelField.tsx | 12 ++++-- docs/examples/validate-perf.tsx | 12 +++++- src/Field.tsx | 51 +++++++++++++++++++------ src/interface.ts | 16 +++++--- src/utils/validateUtil.ts | 39 +++++++++++-------- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/docs/examples/components/LabelField.tsx b/docs/examples/components/LabelField.tsx index 2790fa9e..2608f2bd 100644 --- a/docs/examples/components/LabelField.tsx +++ b/docs/examples/components/LabelField.tsx @@ -1,11 +1,16 @@ import * as React from 'react'; import Form from 'rc-field-form'; -import { FieldProps } from '@/Field'; +import type { FieldProps } from '@/Field'; const { Field } = Form; -const Error = ({ children }) => ( -
    +interface ErrorProps { + warning?: boolean; + children?: React.ReactNode[]; +} + +const Error = ({ children, warning }: ErrorProps) => ( +
      {children.map((error: React.ReactNode, index: number) => (
    • {error}
    • ))} @@ -55,6 +60,7 @@ const LabelField: React.FunctionComponent = ({ {meta.errors} + {meta.warnings}
); }} diff --git a/docs/examples/validate-perf.tsx b/docs/examples/validate-perf.tsx index f07cdc76..ca146bd7 100644 --- a/docs/examples/validate-perf.tsx +++ b/docs/examples/validate-perf.tsx @@ -53,7 +53,17 @@ export default class Demo extends React.Component { { + if (value.length < 6) { + throw new Error('δ½ ηš„ ${displayName} ε€ͺηŸ­δΊ†β€¦β€¦'); + } + }, + }, + ]} onError={this.onPasswordError} > diff --git a/src/Field.tsx b/src/Field.tsx index c5b2d43d..b07293f6 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -15,6 +15,7 @@ import type { RuleObject, StoreValue, EventArgs, + RuleError, } from './interface'; import FieldContext, { HOOK_MARK } from './FieldContext'; import { toArray } from './utils/typeUtil'; @@ -27,7 +28,7 @@ import { isSimilar, } from './utils/valueUtil'; -const EMPTY_ERRORS: string[] = []; +const EMPTY_ERRORS: any[] = []; export type ShouldUpdate = | boolean @@ -76,7 +77,7 @@ export interface InternalFieldProps { messageVariables?: Record; initialValue?: any; onReset?: () => void; - onError?: (errors: string[]) => void; + onError?: (errors: string[], warnings: string[]) => void; preserve?: boolean; /** @private Passed by Form.List props. Do not use since it will break by path check. */ @@ -134,6 +135,7 @@ class Field extends React.Component implements F private prevValidating: boolean; private errors: string[] = EMPTY_ERRORS; + private warnings: string[] = EMPTY_ERRORS; // ============================== Subscriptions ============================== constructor(props: InternalFieldProps) { @@ -215,12 +217,18 @@ class Field extends React.Component implements F }; /** Update `this.error`. If `onError` provided, trigger it */ - public updateError(prevErrors: string[], nextErrors: string[]) { + public updateError( + prevErrors: string[], + nextErrors: string[], + prevWarnings: string[], + nextWarnings: string[], + ) { const { onError } = this.props; - if (onError && !isSimilar(prevErrors, nextErrors)) { - onError(nextErrors); + if (onError && (!isSimilar(prevErrors, nextErrors) || !isSimilar(prevWarnings, nextWarnings))) { + onError(nextErrors, nextWarnings); } this.errors = nextErrors; + this.warnings = nextWarnings; } // ========================= Field Entity Interfaces ========================= @@ -235,13 +243,14 @@ class Field extends React.Component implements F const namePathMatch = namePathList && containsNamePath(namePathList, namePath); const prevErrors = this.errors; + const prevWarnings = this.warnings; // `setFieldsValue` is a quick access to update related status if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue) { this.touched = true; this.dirty = true; this.validatePromise = null; - this.updateError(prevErrors, EMPTY_ERRORS); + this.updateError(prevErrors, EMPTY_ERRORS, prevWarnings, EMPTY_ERRORS); } switch (info.type) { @@ -251,7 +260,7 @@ class Field extends React.Component implements F this.touched = false; this.dirty = false; this.validatePromise = null; - this.updateError(prevErrors, EMPTY_ERRORS); + this.updateError(prevErrors, EMPTY_ERRORS, prevWarnings, EMPTY_ERRORS); onReset?.(); @@ -269,8 +278,13 @@ class Field extends React.Component implements F if ('validating' in data && !('originRCField' in data)) { this.validatePromise = data.validating ? Promise.resolve([]) : null; } - if ('errors' in data) { - this.updateError(prevErrors, data.errors || EMPTY_ERRORS); + + const hasError = 'errors' in data; + const hasWarning = 'warnings' in data; + if (hasError || hasWarning) { + const nextErrors = hasError ? data.errors || EMPTY_ERRORS : prevErrors; + const nextWarnings = hasWarning ? data.warnings || EMPTY_ERRORS : prevWarnings; + this.updateError(prevErrors, nextErrors, prevWarnings, nextWarnings); } this.dirty = true; @@ -334,6 +348,7 @@ class Field extends React.Component implements F public validateRules = (options?: ValidateOptions): Promise => { const prevErrors = this.errors; + const prevWarnings = this.warnings; // We should fixed namePath & value to avoid developer change then by form function const namePath = this.getNamePath(); @@ -371,10 +386,22 @@ class Field extends React.Component implements F promise .catch(e => e) - .then((errors: string[] = EMPTY_ERRORS) => { + .then((ruleErrors: RuleError[] = EMPTY_ERRORS) => { if (this.validatePromise === rootPromise) { this.validatePromise = null; - this.updateError(prevErrors, errors); + + // Get errors & warnings + const nextErrors: string[] = []; + const nextWarnings: string[] = []; + ruleErrors.forEach(({ rule: { warningOnly }, errors = EMPTY_ERRORS }) => { + if (warningOnly) { + nextWarnings.push(...errors); + } else { + nextErrors.push(...errors); + } + }); + + this.updateError(prevErrors, nextErrors, prevWarnings, nextWarnings); this.reRender(); } @@ -386,6 +413,7 @@ class Field extends React.Component implements F this.validatePromise = rootPromise; this.dirty = true; this.errors = EMPTY_ERRORS; + this.warnings = EMPTY_ERRORS; // Force trigger re-render since we need sync renderProps with new meta this.reRender(); @@ -416,6 +444,7 @@ class Field extends React.Component implements F touched: this.isFieldTouched(), validating: this.prevValidating, errors: this.errors, + warnings: this.warnings, name: this.getNamePath(), }; diff --git a/src/interface.ts b/src/interface.ts index 3fdbf4a1..1d9e243b 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,18 +1,17 @@ -import { ReactElement } from 'react'; -import { ReducerAction } from './useForm'; +import type { ReactElement } from 'react'; +import type { ReducerAction } from './useForm'; export type InternalNamePath = (string | number)[]; export type NamePath = string | number | InternalNamePath; export type StoreValue = any; -export interface Store { - [name: string]: StoreValue; -} +export type Store = Record; export interface Meta { touched: boolean; validating: boolean; errors: string[]; + warnings: string[]; name: InternalNamePath; } @@ -51,11 +50,13 @@ type Validator = ( export type RuleRender = (form: FormInstance) => RuleObject; export interface ValidatorRule { + warningOnly?: boolean; message?: string | ReactElement; validator: Validator; } interface BaseRule { + warningOnly?: boolean; enum?: StoreValue[]; len?: number; max?: number; @@ -117,6 +118,11 @@ export interface FieldError { errors: string[]; } +export interface RuleError { + errors: string[]; + rule: RuleObject; +} + export interface ValidateOptions { triggerName?: string; validateMessages?: ValidateMessages; diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 4b80212a..c4dcd7cf 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -1,7 +1,13 @@ import RawAsyncValidator from 'async-validator'; import * as React from 'react'; import warning from 'rc-util/lib/warning'; -import type { InternalNamePath, ValidateOptions, RuleObject, StoreValue } from '../interface'; +import type { + InternalNamePath, + ValidateOptions, + RuleObject, + StoreValue, + RuleError, +} from '../interface'; import { defaultValidateMessages } from './messages'; import { setValues } from './valueUtil'; @@ -152,16 +158,17 @@ export function validateRules( }; }); - let summaryPromise: Promise; + let summaryPromise: Promise; if (validateFirst === true) { // >>>>> Validate by serialization summaryPromise = new Promise(async (resolve, reject) => { /* eslint-disable no-await-in-loop */ for (let i = 0; i < filledRules.length; i += 1) { - const errors = await validateRule(name, value, filledRules[i], options, messageVariables); + const rule = filledRules[i]; + const errors = await validateRule(name, value, rule, options, messageVariables); if (errors.length) { - reject(errors); + reject([{ errors, rule }]); return; } } @@ -171,18 +178,18 @@ export function validateRules( }); } else { // >>>>> Validate by parallel - const rulePromises = filledRules.map(rule => - validateRule(name, value, rule, options, messageVariables), + const rulePromises: Promise[] = filledRules.map(rule => + validateRule(name, value, rule, options, messageVariables).then(errors => ({ errors, rule })), ); summaryPromise = ( validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises) - ).then((errors: string[]): string[] | Promise => { + ).then((errors: RuleError[]): RuleError[] | Promise => { if (!errors.length) { return []; } - return Promise.reject(errors); + return Promise.reject(errors); }); } @@ -192,22 +199,24 @@ export function validateRules( return summaryPromise; } -async function finishOnAllFailed(rulePromises: Promise[]): Promise { - return Promise.all(rulePromises).then((errorsList: string[][]): string[] | Promise => { - const errors: string[] = [].concat(...errorsList); +async function finishOnAllFailed(rulePromises: Promise[]): Promise { + return Promise.all(rulePromises).then((errorsList: RuleError[]): + | RuleError[] + | Promise => { + const errors: RuleError[] = [].concat(...errorsList); return errors; }); } -async function finishOnFirstFailed(rulePromises: Promise[]): Promise { +async function finishOnFirstFailed(rulePromises: Promise[]): Promise { let count = 0; return new Promise(resolve => { rulePromises.forEach(promise => { - promise.then(errors => { - if (errors.length) { - resolve(errors); + promise.then(ruleError => { + if (ruleError.errors.length) { + resolve([ruleError]); } count += 1; From ce3236ef94e8bdc51a35e9d8f5ee40ae2bfcfa55 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 17:46:48 +0800 Subject: [PATCH 08/12] test: Test driven --- src/utils/validateUtil.ts | 115 +++++++++++++++++++------------- tests/common/InfoField.tsx | 13 ++-- tests/common/index.ts | 19 +++++- tests/validate-warning.test.tsx | 63 +++++++++++++++++ 4 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 tests/validate-warning.test.tsx diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index c4dcd7cf..f2758429 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -108,56 +108,77 @@ export function validateRules( const name = namePath.join('.'); // Fill rule with context - const filledRules: RuleObject[] = rules.map(currentRule => { - const originValidatorFunc = currentRule.validator; - - if (!originValidatorFunc) { - return currentRule; - } - return { - ...currentRule, - validator(rule: RuleObject, val: StoreValue, callback: (error?: string) => void) { - let hasPromise = false; - - // Wrap callback only accept when promise not provided - const wrappedCallback = (...args: string[]) => { - // Wait a tick to make sure return type is a promise - Promise.resolve().then(() => { - warning( - !hasPromise, - 'Your validator function has already return a promise. `callback` will be ignored.', - ); - - if (!hasPromise) { - callback(...args); - } - }); + const filledRules: RuleObject[] = rules + .map((currentRule, ruleIndex) => { + const originValidatorFunc = currentRule.validator; + const cloneRule = { + ...currentRule, + ruleIndex, + }; + + // Replace validator if needed + if (originValidatorFunc) { + cloneRule.validator = ( + rule: RuleObject, + val: StoreValue, + callback: (error?: string) => void, + ) => { + let hasPromise = false; + + // Wrap callback only accept when promise not provided + const wrappedCallback = (...args: string[]) => { + // Wait a tick to make sure return type is a promise + Promise.resolve().then(() => { + warning( + !hasPromise, + 'Your validator function has already return a promise. `callback` will be ignored.', + ); + + if (!hasPromise) { + callback(...args); + } + }); + }; + + // Get promise + const promise = originValidatorFunc(rule, val, wrappedCallback); + hasPromise = + promise && typeof promise.then === 'function' && typeof promise.catch === 'function'; + + /** + * 1. Use promise as the first priority. + * 2. If promise not exist, use callback with warning instead + */ + warning(hasPromise, '`callback` is deprecated. Please return a promise instead.'); + + if (hasPromise) { + (promise as Promise) + .then(() => { + callback(); + }) + .catch(err => { + callback(err || ' '); + }); + } }; + } - // Get promise - const promise = originValidatorFunc(rule, val, wrappedCallback); - hasPromise = - promise && typeof promise.then === 'function' && typeof promise.catch === 'function'; - - /** - * 1. Use promise as the first priority. - * 2. If promise not exist, use callback with warning instead - */ - warning(hasPromise, '`callback` is deprecated. Please return a promise instead.'); - - if (hasPromise) { - (promise as Promise) - .then(() => { - callback(); - }) - .catch(err => { - callback(err || ' '); - }); - } - }, - }; - }); + return cloneRule; + }) + .sort(({ warningOnly: w1, ruleIndex: i1 }, { warningOnly: w2, ruleIndex: i2 }) => { + if (!!w1 === !!w2) { + // Let keep origin order + return i1 - i2; + } + + if (w1) { + return 1; + } + + return -1; + }); + // Do validate rules let summaryPromise: Promise; if (validateFirst === true) { diff --git a/tests/common/InfoField.tsx b/tests/common/InfoField.tsx index 54613067..ecf75ecd 100644 --- a/tests/common/InfoField.tsx +++ b/tests/common/InfoField.tsx @@ -1,9 +1,9 @@ -import React, { ReactElement } from 'react'; +import React from 'react'; import { Field } from '../../src'; -import { FieldProps } from '../../src/Field'; +import type { FieldProps } from '../../src/Field'; interface InfoFieldProps extends FieldProps { - children?: ReactElement; + children?: React.ReactElement; } export const Input = ({ value = '', ...props }) => ; @@ -14,7 +14,7 @@ export const Input = ({ value = '', ...props }) => = ({ children, ...props }) => ( {(control, info) => { - const { errors, validating } = info; + const { errors, warnings, validating } = info; return (
@@ -24,6 +24,11 @@ const InfoField: React.FC = ({ children, ...props }) => (
  • {error}
  • ))} +
      + {warnings.map((warning, index) => ( +
    • {warning}
    • + ))} +
    {validating && }
    ); diff --git a/tests/common/index.ts b/tests/common/index.ts index 4d9b3cdd..54dc9073 100644 --- a/tests/common/index.ts +++ b/tests/common/index.ts @@ -1,6 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { act } from 'react-dom/test-utils'; +import type { ReactWrapper } from 'enzyme'; import timeout from './timeout'; import { Field } from '../../src'; import { getNamePath, matchNamePath } from '../../src/utils/valueUtil'; @@ -13,7 +14,12 @@ export async function changeValue(wrapper, value) { wrapper.update(); } -export function matchError(wrapper, error) { +export function matchError( + wrapper: ReactWrapper, + error?: boolean | string, + warning?: boolean | string, +) { + // Error if (error) { expect(wrapper.find('.errors li').length).toBeTruthy(); } else { @@ -23,6 +29,17 @@ export function matchError(wrapper, error) { if (error && typeof error !== 'boolean') { expect(wrapper.find('.errors li').text()).toBe(error); } + + // Warning + if (warning) { + expect(wrapper.find('.warnings li').length).toBeTruthy(); + } else { + expect(wrapper.find('.warnings li').length).toBeFalsy(); + } + + if (warning && typeof warning !== 'boolean') { + expect(wrapper.find('.warnings li').text()).toBe(warning); + } } export function getField(wrapper, index: string | number = 0) { diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx new file mode 100644 index 00000000..39b9fa8e --- /dev/null +++ b/tests/validate-warning.test.tsx @@ -0,0 +1,63 @@ +/* eslint-disable no-template-curly-in-string */ +import React from 'react'; +import { mount } from 'enzyme'; +import Form from '../src'; +import InfoField, { Input } from './common/InfoField'; +import { changeValue, matchError } from './common'; + +describe('Form.WarningValidate', () => { + it('required', async () => { + const wrapper = mount( +
    + + + +
    , + ); + + await changeValue(wrapper, ''); + matchError(wrapper, false, "'name' is required"); + }); + + describe('validateFirst should not block error', () => { + function testValidateFirst(name: string, validateFirst: boolean | 'parallel') { + it(name, async () => { + const wrapper = mount( +
    + + + +
    , + ); + + await changeValue(wrapper, 'bamboo'); + matchError(wrapper, "'name' is not a valid url", false); + }); + } + + testValidateFirst('default', true); + testValidateFirst('parallel', 'parallel'); + }); +}); +/* eslint-enable no-template-curly-in-string */ From efa74e9fc1e52c2b81d609c8967f5e27b6e72a03 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 18:40:04 +0800 Subject: [PATCH 09/12] chore: export getFieldWarning --- src/Field.tsx | 2 ++ src/interface.ts | 3 +++ src/useForm.ts | 53 +++++++++++++++++++++++++++--------------------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index b07293f6..900c2ec0 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -429,6 +429,8 @@ class Field extends React.Component implements F public getErrors = () => this.errors; + public getWarnings = () => this.warnings; + public isListField = () => this.props.isListField; public isList = () => this.props.isList; diff --git a/src/interface.ts b/src/interface.ts index 1d9e243b..01978842 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -105,6 +105,7 @@ export interface FieldEntity { getMeta: () => Meta; getNamePath: () => InternalNamePath; getErrors: () => string[]; + getWarnings: () => string[]; props: { name?: NamePath; rules?: Rule[]; @@ -116,6 +117,7 @@ export interface FieldEntity { export interface FieldError { name: InternalNamePath; errors: string[]; + warnings: string[]; } export interface RuleError { @@ -216,6 +218,7 @@ export interface FormInstance { getFieldsValue(nameList: NamePath[] | true, filterFunc?: (meta: Meta) => boolean): any; getFieldError: (name: NamePath) => string[]; getFieldsError: (nameList?: NamePath[]) => FieldError[]; + getFieldWarning: (name: NamePath) => string[]; isFieldsTouched(nameList?: NamePath[], allFieldsTouched?: boolean): boolean; isFieldsTouched(allFieldsTouched?: boolean): boolean; isFieldTouched: (name: NamePath) => boolean; diff --git a/src/useForm.ts b/src/useForm.ts index cb620f8f..c7a9d727 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -80,6 +80,7 @@ export class FormStore { getFieldValue: this.getFieldValue, getFieldsValue: this.getFieldsValue, getFieldError: this.getFieldError, + getFieldWarning: this.getFieldWarning, getFieldsError: this.getFieldsError, isFieldsTouched: this.isFieldsTouched, isFieldTouched: this.isFieldTouched, @@ -249,12 +250,14 @@ export class FormStore { return { name: entity.getNamePath(), errors: entity.getErrors(), + warnings: entity.getWarnings(), }; } return { name: getNamePath(nameList[index]), errors: [], + warnings: [], }; }); }; @@ -267,6 +270,14 @@ export class FormStore { return fieldError.errors; }; + private getFieldWarning = (name: NamePath): string[] => { + this.warningUnhooked(); + + const namePath = getNamePath(name); + const fieldError = this.getFieldsError([namePath])[0]; + return fieldError.warnings; + }; + private isFieldsTouched = (...args) => { this.warningUnhooked(); @@ -484,23 +495,21 @@ export class FormStore { private getFields = (): InternalFieldData[] => { const entities = this.getFieldEntities(true); - const fields = entities.map( - (field: FieldEntity): InternalFieldData => { - const namePath = field.getNamePath(); - const meta = field.getMeta(); - const fieldData = { - ...meta, - name: namePath, - value: this.getFieldValue(namePath), - }; + const fields = entities.map((field: FieldEntity): InternalFieldData => { + const namePath = field.getNamePath(); + const meta = field.getMeta(); + const fieldData = { + ...meta, + name: namePath, + value: this.getFieldValue(namePath), + }; - Object.defineProperty(fieldData, 'originRCField', { - value: true, - }); + Object.defineProperty(fieldData, 'originRCField', { + value: true, + }); - return fieldData; - }, - ); + return fieldData; + }); return fields; }; @@ -802,14 +811,12 @@ export class FormStore { }); const returnPromise: Promise = summaryPromise - .then( - (): Promise => { - if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(namePathList)); - } - return Promise.reject([]); - }, - ) + .then((): Promise => { + if (this.lastValidatePromise === summaryPromise) { + return Promise.resolve(this.getFieldsValue(namePathList)); + } + return Promise.reject([]); + }) .catch((results: { name: InternalNamePath; errors: string[] }[]) => { const errorList = results.filter(result => result && result.errors.length); return Promise.reject({ From 6f401e502688cc257e4a6595a2a59382799674ee Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 19:14:19 +0800 Subject: [PATCH 10/12] fix: summary validate logic --- src/Field.tsx | 2 +- src/FieldContext.ts | 3 ++- src/interface.ts | 2 +- src/useForm.ts | 38 ++++++++++++++++++++++++++++---------- src/utils/asyncUtil.ts | 2 +- tests/context.test.js | 9 ++++++++- tests/index.test.js | 11 +++++++---- tests/validate.test.tsx | 8 ++++++-- 8 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 900c2ec0..3074c357 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -346,7 +346,7 @@ class Field extends React.Component implements F } }; - public validateRules = (options?: ValidateOptions): Promise => { + public validateRules = (options?: ValidateOptions): Promise => { const prevErrors = this.errors; const prevWarnings = this.warnings; diff --git a/src/FieldContext.ts b/src/FieldContext.ts index 6ddc191f..5429aaa9 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import warning from 'rc-util/lib/warning'; -import { InternalFormInstance } from './interface'; +import type { InternalFormInstance } from './interface'; export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS'; @@ -13,6 +13,7 @@ const Context = React.createContext({ getFieldValue: warningFunc, getFieldsValue: warningFunc, getFieldError: warningFunc, + getFieldWarning: warningFunc, getFieldsError: warningFunc, isFieldsTouched: warningFunc, isFieldTouched: warningFunc, diff --git a/src/interface.ts b/src/interface.ts index 01978842..45dcff6e 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -101,7 +101,7 @@ export interface FieldEntity { isListField: () => boolean; isList: () => boolean; isPreserve: () => boolean; - validateRules: (options?: ValidateOptions) => Promise; + validateRules: (options?: ValidateOptions) => Promise; getMeta: () => Meta; getNamePath: () => InternalNamePath; getErrors: () => string[]; diff --git a/src/useForm.ts b/src/useForm.ts index c7a9d727..b6037a48 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -20,6 +20,7 @@ import type { Meta, InternalFieldData, ValuedNotifyInfo, + RuleError, } from './interface'; import { HOOK_MARK } from './FieldContext'; import { allPromiseFinish } from './utils/asyncUtil'; @@ -740,10 +741,7 @@ export class FormStore { : []; // Collect result in promise list - const promiseList: Promise<{ - name: InternalNamePath; - errors: string[]; - }>[] = []; + const promiseList: Promise[] = []; this.getFieldEntities(true).forEach((field: FieldEntity) => { // Add field if not provide `nameList` @@ -785,13 +783,33 @@ export class FormStore { // Wrap promise with field promiseList.push( promise - .then(() => ({ name: fieldNamePath, errors: [] })) - .catch(errors => - Promise.reject({ + .then(() => ({ name: fieldNamePath, errors: [], warnings: [] })) + .catch((ruleErrors: RuleError[]) => { + const mergedErrors: string[] = []; + const mergedWarnings: string[] = []; + + ruleErrors.forEach(({ rule: { warningOnly }, errors }) => { + if (warningOnly) { + mergedWarnings.push(...errors); + } else { + mergedErrors.push(...errors); + } + }); + + if (mergedErrors.length) { + return Promise.reject({ + name: fieldNamePath, + errors: mergedErrors, + warnings: mergedWarnings, + }); + } + + return { name: fieldNamePath, - errors, - }), - ), + errors: mergedErrors, + warnings: mergedWarnings, + }; + }), ); } }); diff --git a/src/utils/asyncUtil.ts b/src/utils/asyncUtil.ts index 3a977087..51593b5f 100644 --- a/src/utils/asyncUtil.ts +++ b/src/utils/asyncUtil.ts @@ -1,4 +1,4 @@ -import { FieldError } from '../interface'; +import type { FieldError } from '../interface'; export function allPromiseFinish(promiseList: Promise[]): Promise { let hasError = false; diff --git a/tests/context.test.js b/tests/context.test.js index 0cdc0f52..d4a5c193 100644 --- a/tests/context.test.js +++ b/tests/context.test.js @@ -35,7 +35,14 @@ describe('Form.Context', () => { 'form1', expect.objectContaining({ changedFields: [ - { errors: [], name: ['username'], touched: true, validating: false, value: 'Light' }, + { + errors: [], + warnings: [], + name: ['username'], + touched: true, + validating: false, + value: 'Light', + }, ], forms: { form1: expect.objectContaining({}), diff --git a/tests/index.test.js b/tests/index.test.js index 81f587d1..030ba010 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -135,20 +135,23 @@ describe('Form.Basic', () => { expect(form.isFieldTouched('username')).toBeFalsy(); expect(onError).not.toHaveBeenCalled(); expect(onReset).toHaveBeenCalled(); + onError.mockRestore(); onReset.mockRestore(); await changeValue(getField(wrapper, 'username'), ''); expect(form.getFieldValue('username')).toEqual(''); expect(form.getFieldError('username')).toEqual(["'username' is required"]); expect(form.isFieldTouched('username')).toBeTruthy(); - expect(onError).toHaveBeenCalledWith(["'username' is required"]); + expect(onError).toHaveBeenCalledWith(["'username' is required"], []); expect(onReset).not.toHaveBeenCalled(); + onError.mockRestore(); + onReset.mockRestore(); form.resetFields(...args); expect(form.getFieldValue('username')).toEqual(undefined); expect(form.getFieldError('username')).toEqual([]); expect(form.isFieldTouched('username')).toBeFalsy(); - expect(onError).toHaveBeenCalledWith([]); + expect(onError).toHaveBeenCalledWith([], []); expect(onReset).toHaveBeenCalled(); }); } @@ -287,7 +290,7 @@ describe('Form.Basic', () => { matchError(wrapper, "'user' is required"); expect(onFinish).not.toHaveBeenCalled(); expect(onFinishFailed).toHaveBeenCalledWith({ - errorFields: [{ name: ['user'], errors: ["'user' is required"] }], + errorFields: [{ name: ['user'], errors: ["'user' is required"], warnings: [] }], outOfDate: false, values: {}, }); @@ -643,7 +646,7 @@ describe('Form.Basic', () => { expect( form.getFieldsValue(null, meta => { - expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'name']); + expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'warnings', 'name']); return false; }), ).toEqual({}); diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index 0d9bd7ee..76ca9a89 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -29,6 +29,7 @@ describe('Form.Validate', () => { { name: ['username'], errors: ["'username' is required"], + warnings: [], }, ]); @@ -37,10 +38,12 @@ describe('Form.Validate', () => { { name: ['username'], errors: ["'username' is required"], + warnings: [], }, { name: ['not-exist'], errors: [], + warnings: [], }, ]); }); @@ -412,7 +415,7 @@ describe('Form.Validate', () => { }) .then(() => { expect(failed).toBeTruthy(); - resolve(); + resolve(''); }); }); }); @@ -465,7 +468,7 @@ describe('Form.Validate', () => { validator: () => new Promise(resolve => { if (canEnd) { - resolve(); + resolve(''); } }), }, @@ -483,6 +486,7 @@ describe('Form.Validate', () => { { name: ['username'], errors: ["'username' is required"], + warnings: [], }, ]); expect(onFinish).not.toHaveBeenCalled(); From a71262253445c2a3feab7b2e7c3b7300db2fc85b Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 21:50:30 +0800 Subject: [PATCH 11/12] test: Update coverage --- src/utils/validateUtil.ts | 5 +--- tests/validate-warning.test.tsx | 47 ++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index f2758429..79dccc1a 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -206,10 +206,7 @@ export function validateRules( summaryPromise = ( validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises) ).then((errors: RuleError[]): RuleError[] | Promise => { - if (!errors.length) { - return []; - } - + // Always change to rejection for Field to catch return Promise.reject(errors); }); } diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx index 39b9fa8e..c64184cd 100644 --- a/tests/validate-warning.test.tsx +++ b/tests/validate-warning.test.tsx @@ -4,6 +4,7 @@ import { mount } from 'enzyme'; import Form from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError } from './common'; +import type { Rule } from '@/interface'; describe('Form.WarningValidate', () => { it('required', async () => { @@ -28,23 +29,36 @@ describe('Form.WarningValidate', () => { }); describe('validateFirst should not block error', () => { - function testValidateFirst(name: string, validateFirst: boolean | 'parallel') { + function testValidateFirst( + name: string, + validateFirst: boolean | 'parallel', + additionalRule?: Rule, + errorMessage?: string, + ) { it(name, async () => { + const rules = [ + additionalRule, + { + type: 'string', + len: 10, + warningOnly: true, + }, + { + type: 'url', + }, + { + type: 'string', + len: 20, + warningOnly: true, + }, + ]; + const wrapper = mount(
    r) as any} > @@ -52,11 +66,20 @@ describe('Form.WarningValidate', () => { ); await changeValue(wrapper, 'bamboo'); - matchError(wrapper, "'name' is not a valid url", false); + matchError(wrapper, errorMessage || "'name' is not a valid url", false); }); } testValidateFirst('default', true); + testValidateFirst( + 'default', + true, + { + type: 'string', + len: 3, + }, + "'name' must be exactly 3 characters", + ); testValidateFirst('parallel', 'parallel'); }); }); From 2f75e5d9caca286cd8a38b5a09874219e6aac2e9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 1 Jun 2021 22:06:54 +0800 Subject: [PATCH 12/12] test: Update coverage --- tests/validate-warning.test.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx index c64184cd..447153c3 100644 --- a/tests/validate-warning.test.tsx +++ b/tests/validate-warning.test.tsx @@ -4,12 +4,18 @@ import { mount } from 'enzyme'; import Form from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError } from './common'; -import type { Rule } from '@/interface'; +import type { FormInstance, Rule } from '../src/interface'; describe('Form.WarningValidate', () => { it('required', async () => { + let form: FormInstance; + const wrapper = mount( - + { + form = f; + }} + > { await changeValue(wrapper, ''); matchError(wrapper, false, "'name' is required"); + expect(form.getFieldWarning('name')).toEqual(["'name' is required"]); }); describe('validateFirst should not block error', () => {