diff --git a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts index 317fd02a..f69ae50a 100644 --- a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts +++ b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts @@ -217,7 +217,7 @@ describe('editorTypeMatchRules', () => { expect(run('data-source-field-select', 'text-value', { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined(); expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined(); expect(firstLine(run('data-source-field-select', { a: 1 }, { name: 'f', fieldConfig: { type: 'text' } }))).toBe( - '[object Object] 类型应为字符串', + '{"a":1} 类型应为字符串', ); expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'number' } })).toBeUndefined(); // fieldConfig 未声明 type 时不做类型校验 diff --git a/packages/form/src/utils/typeMatch.ts b/packages/form/src/utils/typeMatch.ts index 0736dd4f..99deb4dd 100644 --- a/packages/form/src/utils/typeMatch.ts +++ b/packages/form/src/utils/typeMatch.ts @@ -20,6 +20,7 @@ import { readonly } from 'vue'; import dayjs from 'dayjs'; // dayjs 没有 exports 映射,原生 Node ESM 不会补扩展名,深路径必须写全 .js import customParseFormat from 'dayjs/plugin/customParseFormat.js'; +import { isEqual } from 'lodash-es'; import { appendValidateSuggestion } from '@tmagic/design/headless'; import { getValueByKeyPath, toLine } from '@tmagic/utils'; @@ -167,6 +168,14 @@ export const stringifyExampleValue = (value: any): string => { return String(value); }; +/** 错误主文案里的非法取值:对象/数组用 JSON,避免变成 `[object Object]`。 */ +const formatErrorValue = (value: any): string => { + if (value !== null && typeof value === 'object') { + return stringifyExampleValue(value); + } + return String(value); +}; + /** 参考建议中可选值全量展示上限;超出时仅举例 EXAMPLE_SUGGESTION_COUNT 个并标明总数。 */ export const MAX_SUGGESTION_OPTIONS = 20; const EXAMPLE_SUGGESTION_COUNT = 5; @@ -363,7 +372,20 @@ const resolveOptions = (mForm: FormState | undefined, props: any): any[] => { return resolved; }; -const includesOptionValue = (optionValues: any[], value: any) => optionValues.some((item) => Object.is(item, value)); +/** + * option.value 可能是对象,表单取值又常被 Vue reactive 包成 Proxy。 + * 引用比较会把内容相同的选项判成不在可选项中,因此按内容比较。 + * 配置了 `valueKey` 时只比该字段(与 Select 的对象值比对一致)。 + */ +const isSameOptionValue = (left: any, right: any, valueKey?: string) => { + if (valueKey && left !== null && right !== null && typeof left === 'object' && typeof right === 'object') { + return isEqual(left[valueKey], right[valueKey]); + } + return isEqual(left, right); +}; + +const includesOptionValue = (optionValues: any[], value: any, valueKey?: string) => + optionValues.some((item) => isSameOptionValue(item, value, valueKey)); const collectCascaderLeafValues = (options: CascaderOption[], result: any[] = []) => { options.forEach((option) => { @@ -381,10 +403,11 @@ const isValidCascaderPath = (options: CascaderOption[], path: any[]): boolean => let currentOptions = options; for (let i = 0; i < path.length; i++) { - const node = currentOptions.find((option) => Object.is(option.value, path[i])); - if (!node) return false; + // 同层可能有多份内容相同的 value,不能只走 find 到的第一项 + const matches = currentOptions.filter((option) => isSameOptionValue(option.value, path[i])); + if (!matches.length) return false; if (i === path.length - 1) return true; - currentOptions = node.children || []; + currentOptions = matches.flatMap((option) => option.children || []); } return false; @@ -450,7 +473,7 @@ const validateSelectValue = ( if (!Array.isArray(value)) { return defaultMessage( message, - `${value} 类型应为数组`, + `${formatErrorValue(value)} 类型应为数组`, optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true), ); } @@ -460,7 +483,7 @@ const validateSelectValue = ( if (typeof value === 'object') { return defaultMessage( message, - `${value} 类型不合法`, + `${formatErrorValue(value)} 类型不合法`, optionExampleSuggestion(optionValues, '"文本内容" 或 123', false), ); } @@ -471,18 +494,18 @@ const validateSelectValue = ( if (!Array.isArray(value)) { return defaultMessage( message, - `${value} 类型应为数组`, + `${formatErrorValue(value)} 类型应为数组`, optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true), ); } - if (value.some((item) => !includesOptionValue(optionValues, item))) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues)); + if (value.some((item) => !includesOptionValue(optionValues, item, config.valueKey))) { + return defaultMessage(message, `${formatErrorValue(value)} 不在可选项中`, optionSuggestion(optionValues)); } return undefined; } - if (!includesOptionValue(optionValues, value)) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues)); + if (!includesOptionValue(optionValues, value, config.valueKey)) { + return defaultMessage(message, `${formatErrorValue(value)} 不在可选项中`, optionSuggestion(optionValues)); } return undefined; }; @@ -514,16 +537,16 @@ const validateCascaderValue = ( if (typeof value !== 'string' && !Array.isArray(value)) { return defaultMessage( message, - `${value} 类型应为字符串或数组`, + `${formatErrorValue(value)} 类型应为字符串或数组`, cascaderExample('"选项1,选项2" 或 ["选项1", "选项2"]'), ); } } else if (multiple) { if (!Array.isArray(value)) { - return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); + return defaultMessage(message, `${formatErrorValue(value)} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); } } else if (emitPath && !Array.isArray(value)) { - return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); + return defaultMessage(message, `${formatErrorValue(value)} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); } if (config.remote) { @@ -538,7 +561,7 @@ const validateCascaderValue = ( if (multiple) { if (!Array.isArray(normalizedValue)) { - return defaultMessage(message, `${value} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); + return defaultMessage(message, `${formatErrorValue(value)} 类型应为数组`, cascaderExample('["选项1", "选项2"]')); } const invalid = normalizedValue.some((item) => { @@ -549,20 +572,32 @@ const validateCascaderValue = ( }); if (invalid) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options))); + return defaultMessage( + message, + `${formatErrorValue(value)} 不在可选项中`, + optionSuggestion(collectCascaderLeafValues(options)), + ); } return undefined; } if (emitPath) { if (!Array.isArray(normalizedValue) || !isValidCascaderPath(options, normalizedValue)) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options))); + return defaultMessage( + message, + `${formatErrorValue(value)} 不在可选项中`, + optionSuggestion(collectCascaderLeafValues(options)), + ); } return undefined; } if (!includesOptionValue(collectCascaderLeafValues(options), normalizedValue)) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options))); + return defaultMessage( + message, + `${formatErrorValue(value)} 不在可选项中`, + optionSuggestion(collectCascaderLeafValues(options)), + ); } return undefined; }; @@ -585,7 +620,7 @@ const validateBuiltinTypeMatch = ( if (typeof value !== 'number' || Number.isNaN(value)) { return defaultMessage( message, - `${value} 类型应为数字`, + `${formatErrorValue(value)} 类型应为数字`, typeExampleSuggestion(mForm, props, '123', isNumberValue), ); } @@ -605,7 +640,7 @@ const validateBuiltinTypeMatch = ( if (typeof value !== 'string') { return defaultMessage( message, - `${value} 类型应为字符串`, + `${formatErrorValue(value)} 类型应为字符串`, typeExampleSuggestion(mForm, props, '"文本内容"', isStringValue), ); } @@ -624,7 +659,7 @@ const validateBuiltinTypeMatch = ( if (typeof value !== 'number' || Number.isNaN(value)) { return defaultMessage( message, - `${value} 类型应为数字`, + `${formatErrorValue(value)} 类型应为数字`, typeExampleSuggestion(mForm, props, '123', isNumberValue), ); } @@ -639,7 +674,7 @@ const validateBuiltinTypeMatch = ( ) { return defaultMessage( message, - `${value} 类型应为长度为 2 的数字数组`, + `${formatErrorValue(value)} 类型应为长度为 2 的数字数组`, typeExampleSuggestion(mForm, props, '[0, 100]', isNumberRangeValue), ); } @@ -649,7 +684,11 @@ const validateBuiltinTypeMatch = ( if (fieldType === 'switch' || fieldType === 'checkbox') { const { activeValue, inactiveValue } = resolveToggleValues(config); if (!Object.is(value, activeValue) && !Object.is(value, inactiveValue)) { - return defaultMessage(message, `${value} 不在合法开关值中`, toggleSuggestion(activeValue, inactiveValue)); + return defaultMessage( + message, + `${formatErrorValue(value)} 不在合法开关值中`, + toggleSuggestion(activeValue, inactiveValue), + ); } return undefined; } @@ -667,7 +706,7 @@ const validateBuiltinTypeMatch = ( } if (!includesOptionValue(optionValues, value)) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues)); + return defaultMessage(message, `${formatErrorValue(value)} 不在可选项中`, optionSuggestion(optionValues)); } return undefined; } @@ -682,12 +721,12 @@ const validateBuiltinTypeMatch = ( if (!Array.isArray(value)) { return defaultMessage( message, - `${value} 类型应为数组`, + `${formatErrorValue(value)} 类型应为数组`, optionExampleSuggestion(optionValues, '["选项1", "选项2"]', true), ); } if (value.some((item) => !includesOptionValue(optionValues, item))) { - return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues)); + return defaultMessage(message, `${formatErrorValue(value)} 不在可选项中`, optionSuggestion(optionValues)); } return undefined; } @@ -709,8 +748,8 @@ const validateBuiltinTypeMatch = ( return defaultMessage( message, isTimestampValueFormat(valueFormat) - ? `${value} 类型应为长度为 2 的时间戳数字数组` - : `${value} 格式应为长度为 2 的 ${valueFormat} 数组`, + ? `${formatErrorValue(value)} 类型应为长度为 2 的时间戳数字数组` + : `${formatErrorValue(value)} 格式应为长度为 2 的 ${valueFormat} 数组`, dateRangeSuggestion(valueFormat), ); } @@ -721,7 +760,7 @@ const validateBuiltinTypeMatch = ( if (!Array.isArray(value) || value.some((item) => !isObjectValue(item))) { return defaultMessage( message, - `${value} 类型应为对象数组`, + `${formatErrorValue(value)} 类型应为对象数组`, typeExampleSuggestion(mForm, props, '[{}]', isObjectArrayValue), ); } diff --git a/packages/form/tests/unit/utils/typeMatch.spec.ts b/packages/form/tests/unit/utils/typeMatch.spec.ts index 22035859..7aacc9cc 100644 --- a/packages/form/tests/unit/utils/typeMatch.spec.ts +++ b/packages/form/tests/unit/utils/typeMatch.spec.ts @@ -17,6 +17,7 @@ */ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { reactive } from 'vue'; import { setDesignConfig } from '@tmagic/design'; import { getDesignConfig } from '@tmagic/design/headless'; @@ -102,7 +103,7 @@ describe('validateTypeMatch', () => { 'NaN 类型应为字符串\n\n请参考以下示例值:"文本内容"', ); expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'text' }))).toBe( - '[object Object] 类型应为字符串\n\n请参考以下示例值:"文本内容"', + '{"a":1} 类型应为字符串\n\n请参考以下示例值:"文本内容"', ); expect(validateTypeMatch(true, mForm, propsOf({ type: 'text' }), '自定义错误')).toBe('自定义错误'); }); @@ -199,6 +200,112 @@ describe('validateTypeMatch', () => { expect(validateTypeMatch(3, mForm, propsOf(config))).toBe('3 不在可选项中\n\n请使用以下某一个值:1;2'); }); + test('select 对象 option.value 按内容比较,兼容 Vue Proxy', () => { + const share = { componentType: 'share', modType: 42, name: '分享' }; + const pop = { componentType: 'pop', name: '弹窗' }; + const config = { + type: 'select', + options: [ + { text: '弹窗', value: pop }, + { text: '分享', value: share }, + ], + }; + + expect(validateTypeMatch({ ...share }, mForm, propsOf(config))).toBeUndefined(); + expect(validateTypeMatch(reactive({ ...share }), mForm, propsOf(config))).toBeUndefined(); + expect( + validateTypeMatch( + { ...share }, + mForm, + propsOf({ ...config, options: [{ text: '分享', value: reactive(share) }] }), + ), + ).toBeUndefined(); + expect(validateTypeMatch({ ...share, modType: 99 }, mForm, propsOf(config))).toContain( + `${JSON.stringify({ ...share, modType: 99 })} 不在可选项中`, + ); + expect(validateTypeMatch({ ...share, name: 'Share' }, mForm, propsOf(config))).toContain( + `${JSON.stringify({ ...share, name: 'Share' })} 不在可选项中`, + ); + + const byValueKey = { ...config, valueKey: 'modType' }; + expect(validateTypeMatch({ ...share, name: 'Share' }, mForm, propsOf(byValueKey))).toBeUndefined(); + expect(validateTypeMatch(reactive({ modType: 42 }), mForm, propsOf(byValueKey))).toBeUndefined(); + expect(validateTypeMatch({ ...share, modType: 99 }, mForm, propsOf(byValueKey))).toContain( + `${JSON.stringify({ ...share, modType: 99 })} 不在可选项中`, + ); + + const multipleConfig = { ...config, multiple: true }; + expect(validateTypeMatch([reactive({ ...share }), { ...pop }], mForm, propsOf(multipleConfig))).toBeUndefined(); + expect(validateTypeMatch([reactive({ ...share, modType: 99 })], mForm, propsOf(multipleConfig))).toMatch( + /不在可选项中/, + ); + expect( + validateTypeMatch( + [reactive({ componentType: 'share' }), { componentType: 'pop' }], + mForm, + propsOf({ ...multipleConfig, valueKey: 'componentType' }), + ), + ).toBeUndefined(); + expect( + validateTypeMatch([{ componentType: 'copy' }], mForm, propsOf({ ...multipleConfig, valueKey: 'componentType' })), + ).toMatch(/不在可选项中/); + + const checkboxGroup = { + type: 'checkbox-group', + options: [{ text: '分享', value: share }], + }; + expect(validateTypeMatch([reactive({ ...share })], mForm, propsOf(checkboxGroup))).toBeUndefined(); + }); + + test('cascader 对象 value 按内容比较,兼容 Vue Proxy', () => { + const options = [ + { + value: { id: 'zhejiang' }, + label: 'Zhejiang', + children: [{ value: { id: 'hangzhou' }, label: 'Hangzhou' }], + }, + ]; + expect( + validateTypeMatch( + [reactive({ id: 'zhejiang' }), { id: 'hangzhou' }], + mForm, + propsOf({ type: 'cascader', options }), + ), + ).toBeUndefined(); + expect( + validateTypeMatch( + [{ id: 'zhejiang' }, { id: 'hangzhou' }], + mForm, + propsOf({ + type: 'cascader', + options: [ + { + value: reactive({ id: 'zhejiang' }), + label: 'Zhejiang', + children: [{ value: reactive({ id: 'hangzhou' }), label: 'Hangzhou' }], + }, + ], + }), + ), + ).toBeUndefined(); + expect( + validateTypeMatch([{ id: 'zhejiang' }, { id: 'ningbo' }], mForm, propsOf({ type: 'cascader', options })), + ).toMatch(/不在可选项中/); + expect( + validateTypeMatch(reactive({ id: 'hangzhou' }), mForm, propsOf({ type: 'cascader', options, emitPath: false })), + ).toBeUndefined(); + }); + + test('cascader 同层内容相同的 value 会尝试所有匹配节点', () => { + const options = [ + { value: { id: 1 }, children: [{ value: 'a' }] }, + { value: { id: 1 }, children: [{ value: 'b' }] }, + ]; + expect(validateTypeMatch([{ id: 1 }, 'a'], mForm, propsOf({ type: 'cascader', options }))).toBeUndefined(); + expect(validateTypeMatch([{ id: 1 }, 'b'], mForm, propsOf({ type: 'cascader', options }))).toBeUndefined(); + expect(validateTypeMatch([{ id: 1 }, 'c'], mForm, propsOf({ type: 'cascader', options }))).toMatch(/不在可选项中/); + }); + test('可选项超过 20 个时建议仅举例前 5 个并标明总数', () => { const values = Array.from({ length: 21 }, (_, i) => i + 1); const config = { @@ -232,7 +339,7 @@ describe('validateTypeMatch', () => { }; expect(validateTypeMatch(['a'], mForm, propsOf(config))).toBeUndefined(); expect(validateTypeMatch(['a', 'c'], mForm, propsOf(config))).toBe( - 'a,c 不在可选项中\n\n请使用以下某一个值:"a";"b"', + '["a","c"] 不在可选项中\n\n请使用以下某一个值:"a";"b"', ); // multiple 类型不匹配时,示例值基于真实 options(前 2 个值组成的数组) expect(validateTypeMatch('a', mForm, propsOf(config))).toBe('a 类型应为数组\n\n请参考以下示例值:["a","b"]'); @@ -323,7 +430,7 @@ describe('validateTypeMatch', () => { validateTypeMatch(['a'], mForm, propsOf({ type: 'checkbox-group', options: checkboxOptions })), ).toBeUndefined(); expect(validateTypeMatch(['b'], mForm, propsOf({ type: 'checkbox-group', options: checkboxOptions }))).toBe( - 'b 不在可选项中\n\n请使用以下某一个值:"a"', + '["b"] 不在可选项中\n\n请使用以下某一个值:"a"', ); const cascaderOptions = () => [ @@ -338,7 +445,7 @@ describe('validateTypeMatch', () => { ).toBeUndefined(); expect( validateTypeMatch(['zhejiang', 'ningbo'], mForm, propsOf({ type: 'cascader', options: cascaderOptions })), - ).toBe('zhejiang,ningbo 不在可选项中\n\n请使用以下某一个值:"hangzhou"'); + ).toBe('["zhejiang","ningbo"] 不在可选项中\n\n请使用以下某一个值:"hangzhou"'); }); test('select allowCreate / remote 不做枚举', () => { @@ -360,9 +467,7 @@ describe('validateTypeMatch', () => { { text: 'B', value: 'b' }, ], }; - expect(validateTypeMatch({ a: 1 }, mForm, propsOf(config))).toBe( - '[object Object] 类型不合法\n\n请参考以下示例值:"a"', - ); + expect(validateTypeMatch({ a: 1 }, mForm, propsOf(config))).toBe('{"a":1} 类型不合法\n\n请参考以下示例值:"a"'); // allowCreate + multiple + options:传非数组,示例取前 2 个真实 option 值组成数组 expect(validateTypeMatch('a', mForm, propsOf({ ...config, multiple: true }))).toBe( 'a 类型应为数组\n\n请参考以下示例值:["a","b"]', @@ -389,7 +494,7 @@ describe('validateTypeMatch', () => { }; expect(validateTypeMatch(['a', 'b'], mForm, propsOf(checkboxGroup))).toBeUndefined(); expect(validateTypeMatch(['c'], mForm, propsOf(checkboxGroup))).toBe( - 'c 不在可选项中\n\n请使用以下某一个值:"a";"b"', + '["c"] 不在可选项中\n\n请使用以下某一个值:"a";"b"', ); // type 为驼峰形式 radioGroup,应通过 toLine 归一化后按 radio-group 规则校验 @@ -422,7 +527,7 @@ describe('validateTypeMatch', () => { expect(validateTypeMatch(['zhejiang', 'hangzhou'], mForm, propsOf({ type: 'cascader', options }))).toBeUndefined(); expect(validateTypeMatch(['zhejiang', 'ningbo'], mForm, propsOf({ type: 'cascader', options }))).toBe( - 'zhejiang,ningbo 不在可选项中\n\n请使用以下某一个值:"hangzhou"', + '["zhejiang","ningbo"] 不在可选项中\n\n请使用以下某一个值:"hangzhou"', ); expect( validateTypeMatch('zhejiang/hangzhou', mForm, propsOf({ type: 'cascader', options, valueSeparator: '/' })), @@ -435,17 +540,17 @@ describe('validateTypeMatch', () => { test('number-range / daterange / table', () => { expect(validateTypeMatch([1, 2], mForm, propsOf({ type: 'number-range' }))).toBeUndefined(); expect(validateTypeMatch([1], mForm, propsOf({ type: 'number-range' }))).toBe( - '1 类型应为长度为 2 的数字数组\n\n请参考以下示例值:[0, 100]', + '[1] 类型应为长度为 2 的数字数组\n\n请参考以下示例值:[0, 100]', ); expect( validateTypeMatch(['2020/01/01 00:00:00', '2020/01/02 00:00:00'], mForm, propsOf({ type: 'daterange' })), ).toBeUndefined(); expect(validateTypeMatch(['2020-01-01', '2020-01-02'], mForm, propsOf({ type: 'daterange' }))).toMatch( - /^2020-01-01,2020-01-02 格式应为长度为 2 的 YYYY\/MM\/DD HH:mm:ss 数组\n\n请参考以下示例值:\["\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}", "\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}"\]$/, + /^\["2020-01-01","2020-01-02"\] 格式应为长度为 2 的 YYYY\/MM\/DD HH:mm:ss 数组\n\n请参考以下示例值:\["\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}", "\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}"\]$/, ); expect(validateTypeMatch(['a'], mForm, propsOf({ type: 'daterange' }))).toMatch( - /^a 格式应为长度为 2 的 YYYY\/MM\/DD HH:mm:ss 数组\n\n请参考以下示例值:\["\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}", "\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}"\]$/, + /^\["a"\] 格式应为长度为 2 的 YYYY\/MM\/DD HH:mm:ss 数组\n\n请参考以下示例值:\["\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}", "\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}"\]$/, ); expect(validateTypeMatch([1, 2], mForm, propsOf({ type: 'daterange', valueFormat: 'timestamp' }))).toBeUndefined(); expect(validateTypeMatch('x', mForm, propsOf({ type: 'daterange', names: ['a', 'b'] }))).toBeUndefined(); @@ -453,18 +558,18 @@ describe('validateTypeMatch', () => { expect(validateTypeMatch([{ id: 1 }], mForm, propsOf({ type: 'table' }))).toBeUndefined(); // table 无 options,类型不匹配示例回退到通用对象数组示例 expect(validateTypeMatch({}, mForm, propsOf({ type: 'group-list' }))).toBe( - '[object Object] 类型应为对象数组\n\n请参考以下示例值:[{}]', + '{} 类型应为对象数组\n\n请参考以下示例值:[{}]', ); // table / group-list 元素必须为对象,字符串数组不合法 expect(validateTypeMatch(['a', 'b'], mForm, propsOf({ type: 'table' }))).toBe( - 'a,b 类型应为对象数组\n\n请参考以下示例值:[{}]', + '["a","b"] 类型应为对象数组\n\n请参考以下示例值:[{}]', ); expect(validateTypeMatch([1], mForm, propsOf({ type: 'group-list' }))).toBe( - '1 类型应为对象数组\n\n请参考以下示例值:[{}]', + '[1] 类型应为对象数组\n\n请参考以下示例值:[{}]', ); // 数组中混入非对象元素也不合法 expect(validateTypeMatch([{ id: 1 }, 'x'], mForm, propsOf({ type: 'grouplist' }))).toBe( - '[object Object],x 类型应为对象数组\n\n请参考以下示例值:[{}]', + '[{"id":1},"x"] 类型应为对象数组\n\n请参考以下示例值:[{}]', ); }); @@ -501,7 +606,7 @@ describe('validateTypeMatch', () => { test('timerange 按 valueFormat 校验', () => { expect(validateTypeMatch(['12:00:00', '13:00:00'], mForm, propsOf({ type: 'timerange' }))).toBeUndefined(); expect(validateTypeMatch(['bad'], mForm, propsOf({ type: 'timerange' }))).toMatch( - /^bad 格式应为长度为 2 的 HH:mm:ss 数组\n\n请参考以下示例值:\["\d{2}:\d{2}:\d{2}", "\d{2}:\d{2}:\d{2}"\]$/, + /^\["bad"\] 格式应为长度为 2 的 HH:mm:ss 数组\n\n请参考以下示例值:\["\d{2}:\d{2}:\d{2}", "\d{2}:\d{2}:\d{2}"\]$/, ); }); @@ -534,7 +639,7 @@ describe('validateTypeMatch', () => { ).toBeUndefined(); expect( validateTypeMatch(['ningbo'], mForm, propsOf({ type: 'cascader', options, multiple: true, emitPath: false })), - ).toBe('ningbo 不在可选项中\n\n请使用以下某一个值:"hangzhou"'); + ).toBe('["ningbo"] 不在可选项中\n\n请使用以下某一个值:"hangzhou"'); }); test('cascader valueSeparator 时数组值', () => { @@ -686,7 +791,7 @@ describe('validateTypeMatch', () => { '1 类型应为数字\n\n请参考以下示例值:123', ); expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'text', defaultValue: async () => '示例' }))).toBe( - '[object Object] 类型应为字符串\n\n请参考以下示例值:"文本内容"', + '{"a":1} 类型应为字符串\n\n请参考以下示例值:"文本内容"', ); }); });