mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-23 14:38:26 +00:00
refactor(form): 抽离表单校验逻辑至 validateForm 并复用于 props 面板
- 将 editor 的 validatePropsForm 逻辑迁移到 @tmagic/form 的 validateForm - FormPanel 源码保存改为复用 validateForm 静默校验 - 补充 form 校验与 applyExtendState 相关单元测试
This commit is contained in:
parent
fe7815cf3d
commit
d0a22315c0
@ -49,14 +49,13 @@ import { Document as DocumentIcon } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
|
||||
import { MForm } from '@tmagic/form';
|
||||
import { MForm, validateForm } from '@tmagic/form';
|
||||
import { filterXSS } from '@tmagic/utils';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
||||
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { validatePropsForm } from '@editor/utils/props';
|
||||
|
||||
import CodeEditor from '../CodeEditor.vue';
|
||||
|
||||
@ -162,14 +161,19 @@ const saveCode = async (values: any) => {
|
||||
// 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
|
||||
// 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
|
||||
try {
|
||||
const error = await validatePropsForm({
|
||||
const error = await validateForm({
|
||||
config: props.config,
|
||||
values: newValues,
|
||||
appContext: internalInstance?.appContext ?? null,
|
||||
services,
|
||||
stage: stage.value,
|
||||
typeMatchValid: true,
|
||||
extendState: props.extendState,
|
||||
initValues: newValues,
|
||||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||||
appContext: internalInstance?.appContext ? { ...internalInstance?.appContext, provides: { services } } : null,
|
||||
extendState: (state) => {
|
||||
if (configFormRef.value?.formState) {
|
||||
return { ...(configFormRef.value?.formState || {}) };
|
||||
}
|
||||
return props.extendState?.(state) || {};
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@ -16,9 +16,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { AppContext } from 'vue';
|
||||
|
||||
import {
|
||||
HookType,
|
||||
NODE_CONDS_KEY,
|
||||
@ -27,18 +24,7 @@ import {
|
||||
NODE_DISABLE_DATA_SOURCE_KEY,
|
||||
} from '@tmagic/core';
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type {
|
||||
ChildConfig,
|
||||
DisplayCondsConfig,
|
||||
FormConfig,
|
||||
FormState,
|
||||
FormValue,
|
||||
TabConfig,
|
||||
TabPaneConfig,
|
||||
} from '@tmagic/form';
|
||||
import { validateForm } from '@tmagic/form';
|
||||
|
||||
import type { Services } from '@editor/type';
|
||||
import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
|
||||
export const arrayOptions = [
|
||||
{ text: '包含', value: 'include' },
|
||||
@ -403,86 +389,3 @@ export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// #region ValidatePropsFormOptions
|
||||
/**
|
||||
* validatePropsForm 参数
|
||||
*/
|
||||
export interface ValidatePropsFormOptions {
|
||||
/** 组件属性表单配置 */
|
||||
config: FormConfig;
|
||||
/** 待校验的表单值 */
|
||||
values: FormValue;
|
||||
/**
|
||||
* 当前组件实例的 appContext(通常为 `getCurrentInstance()?.appContext`)。
|
||||
* 会与 services 一并合入临时 MForm 的 appContext,使编辑器字段组件(DataSourceInput 等)能正常 inject。
|
||||
*/
|
||||
appContext?: AppContext | null;
|
||||
/** 编辑器服务集合,注入到临时表单的 formState */
|
||||
services?: Services;
|
||||
/** stage 实例,注入到临时表单的 formState */
|
||||
stage?: any;
|
||||
/** 外部扩展的 formState */
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/**
|
||||
* 调试模式,默认 `true`:以弹层形式可见地渲染表单,点击「确定」才触发校验。
|
||||
* 置为 `false` 时静默挂载后自动校验。
|
||||
*/
|
||||
debug?: boolean;
|
||||
typeMatchValid?: boolean;
|
||||
}
|
||||
// #endregion ValidatePropsFormOptions
|
||||
|
||||
/**
|
||||
* 对一份「组件属性表单配置 + 值」做一次独立的校验,**不复用也不污染页面上正在展示的表单**。
|
||||
*
|
||||
* 内部基于 `@tmagic/form` 的 `validateForm` 另建一个独立的 MForm 实例完成校验,并统一处理
|
||||
* 编辑器场景所需的上下文注入:将当前组件实例的 provides 合入 appContext,并向 formState 注入
|
||||
* stage / services 及外部扩展状态,保证校验规则依赖的上下文可用。
|
||||
*
|
||||
* 常用于源码编辑器保存后,对最新配置做一次校验,并将校验结果(错误信息)随提交一并抛给上层记录,
|
||||
* 使源码保存的错误状态与表单编辑保持一致。
|
||||
*
|
||||
* @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
|
||||
* 仅在初始化超时或挂载失败等异常情况下才会 reject。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const error = await validatePropsForm({
|
||||
* config,
|
||||
* values,
|
||||
* appContext: getCurrentInstance()?.appContext,
|
||||
* services,
|
||||
* stage: editorService.get('stage'),
|
||||
* extendState,
|
||||
* });
|
||||
* if (error) {
|
||||
* // 配置不合法,error 为错误文案
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const validatePropsForm = ({
|
||||
config,
|
||||
values,
|
||||
appContext = null,
|
||||
services,
|
||||
stage,
|
||||
extendState,
|
||||
debug,
|
||||
typeMatchValid,
|
||||
}: ValidatePropsFormOptions): Promise<string> =>
|
||||
validateForm({
|
||||
config,
|
||||
debug,
|
||||
typeMatchValid,
|
||||
initValues: values,
|
||||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||||
appContext: appContext ? { ...appContext, provides: { services } } : null,
|
||||
// 与页面表单保持一致:注入 stage/services 及外部扩展状态,保证校验规则依赖的上下文可用
|
||||
extendState: async (state) => ({
|
||||
...((await extendState?.(state)) || {}),
|
||||
stage,
|
||||
services,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
*
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { NODE_CONDS_RESULT_KEY } from '@tmagic/core';
|
||||
import { validateForm } from '@tmagic/form';
|
||||
|
||||
import {
|
||||
advancedTabConfig,
|
||||
@ -20,7 +19,6 @@ import {
|
||||
numberOptions,
|
||||
removeStyleDisplayConfig,
|
||||
styleTabConfig,
|
||||
validatePropsForm,
|
||||
} from '@editor/utils/props';
|
||||
|
||||
vi.mock('@tmagic/design', () => ({
|
||||
@ -172,59 +170,6 @@ describe('fillConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePropsForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(validateForm).mockReset();
|
||||
});
|
||||
|
||||
test('合入 appContext.provides、注入 stage/services 并合并外部 extendState,debug 默认 undefined', async () => {
|
||||
vi.mocked(validateForm).mockResolvedValue('err-text');
|
||||
|
||||
const services = { uiService: {} } as any;
|
||||
const stage = { id: 'stage' };
|
||||
const appContext = { app: {}, provides: { foo: 1 } } as any;
|
||||
|
||||
const result = await validatePropsForm({
|
||||
config: [],
|
||||
values: { a: 1 },
|
||||
appContext,
|
||||
services,
|
||||
stage,
|
||||
extendState: async () => ({ extra: true }),
|
||||
});
|
||||
|
||||
// 直接返回 validateForm 的结果
|
||||
expect(result).toBe('err-text');
|
||||
|
||||
const arg = vi.mocked(validateForm).mock.calls[0][0];
|
||||
expect(arg.config).toEqual([]);
|
||||
expect(arg.debug).toBeUndefined();
|
||||
expect(arg.initValues).toEqual({ a: 1 });
|
||||
// appContext 保留原字段,但 provides 被替换为 { services }
|
||||
expect(arg.appContext).toEqual({ app: {}, provides: { services } });
|
||||
|
||||
// extendState 合并外部返回并注入 stage/services
|
||||
const state = await arg.extendState!({} as any);
|
||||
expect(state).toEqual({ extra: true, stage, services });
|
||||
});
|
||||
|
||||
test('appContext 缺省为 null,extendState 缺省时仍注入 stage/services,可覆盖 debug', async () => {
|
||||
vi.mocked(validateForm).mockResolvedValue('');
|
||||
|
||||
const services = {} as any;
|
||||
const stage = {};
|
||||
|
||||
await validatePropsForm({ config: [], values: {}, services, stage, debug: false });
|
||||
|
||||
const arg = vi.mocked(validateForm).mock.calls[0][0];
|
||||
expect(arg.appContext).toBeNull();
|
||||
expect(arg.debug).toBe(false);
|
||||
|
||||
const state = await arg.extendState!({} as any);
|
||||
expect(state).toEqual({ stage, services });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStyleDisplayConfig', () => {
|
||||
test('将 tab 内「样式」pane 的 display 置为 true', () => {
|
||||
const config = [
|
||||
|
||||
@ -261,6 +261,18 @@ const formState: FormState = reactive<FormState>({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
|
||||
*
|
||||
* 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
|
||||
* 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
|
||||
*
|
||||
* 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
|
||||
* `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
|
||||
* 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
|
||||
*/
|
||||
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(formState));
|
||||
|
||||
/**
|
||||
* `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
|
||||
* 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
|
||||
@ -299,7 +311,7 @@ watchEffect(async (onCleanup) => {
|
||||
}
|
||||
if (stale) return;
|
||||
|
||||
applyExtendState(formState, state);
|
||||
applyExtendState(formState, state, reservedStateKeys);
|
||||
});
|
||||
|
||||
provide('mForm', formState);
|
||||
|
||||
@ -204,10 +204,14 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
||||
console.error('[MForm] extendState failed:', e);
|
||||
return;
|
||||
}
|
||||
// formState 的内置 key 快照:在 extendState 合并前捕获,
|
||||
// 供 applyExtendState 禁止 extendState 覆盖这些已有字段(只能新增),
|
||||
// 与 Form.vue 中 reservedStateKeys 的语义保持一致。
|
||||
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(form.formState));
|
||||
// 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
|
||||
// (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
|
||||
const apply = (state: Record<string, any> | null | undefined) =>
|
||||
applyExtendState(form.formState, state);
|
||||
applyExtendState(form.formState, state, reservedStateKeys);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
|
||||
} else {
|
||||
|
||||
@ -449,16 +449,27 @@ export const sortChange = (data: any[], { prop, order }: SortProp) => {
|
||||
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
||||
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
||||
*
|
||||
* 注意:formState 上由 props 派生的字段(keyProp / popperClass / config / initValues /
|
||||
* isCompare / lastValues / parentValues)是只读 getter(无 setter),extendState 若以
|
||||
* 普通字段形式返回同名 key,直接赋值会让 proxy 的 set trap 失败并抛出
|
||||
* `TypeError: 'set' on proxy: trap returned falsish`,这里统一跳过并告警;
|
||||
* 如确需覆盖,可在 extendState 中以 get 访问器形式返回。
|
||||
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
||||
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
||||
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
||||
* post 等),命中这些 key 时统一跳过并告警。
|
||||
*
|
||||
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
||||
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
||||
* `TypeError: 'set' on proxy: trap returned falsish`。
|
||||
*/
|
||||
export const applyExtendState = (formState: FormState, state: Record<string, any> | null | undefined): void => {
|
||||
export const applyExtendState = (
|
||||
formState: FormState,
|
||||
state: Record<string, any> | null | undefined,
|
||||
reservedKeys?: Set<string | symbol>,
|
||||
): void => {
|
||||
if (!state) return;
|
||||
|
||||
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
||||
if (reservedKeys?.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!('value' in descriptor)) {
|
||||
descriptor.configurable = true;
|
||||
Object.defineProperty(formState, key, descriptor);
|
||||
|
||||
@ -189,9 +189,7 @@ describe('Form.vue —— extendState', () => {
|
||||
expect(v2).toMatch(/^stage-/);
|
||||
});
|
||||
|
||||
test('extendState 以普通字段返回 props 派生的只读字段时跳过并告警,不抛错', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
test('extendState 以普通字段返回内置保留字段时静默跳过,其余字段正常合并', async () => {
|
||||
const wrapper = mountForm({
|
||||
keyProp: 'id',
|
||||
extendState: () => ({ keyProp: 'custom', config: [], extra: 'ok' }),
|
||||
@ -201,18 +199,15 @@ describe('Form.vue —— extendState', () => {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
// 只读派生字段保持 props 值,未被覆盖
|
||||
// 内置保留字段(keyProp / config)未被 extendState 覆盖
|
||||
expect((wrapper.vm.formState as any).keyProp).toBe('id');
|
||||
expect(Array.isArray((wrapper.vm.formState as any).config)).toBe(true);
|
||||
// 其他字段正常合并
|
||||
// 非保留字段正常合并
|
||||
expect((wrapper.vm.formState as any).extra).toBe('ok');
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(wrapper.find('.m-form').exists()).toBe(true);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('extendState 以 get 访问器返回只读字段同名 key 时允许显式覆盖', async () => {
|
||||
test('extendState 以 get 访问器返回内置保留字段同名 key 时仍被拦截,无法覆盖', async () => {
|
||||
const wrapper = mountForm({
|
||||
keyProp: 'id',
|
||||
extendState: () =>
|
||||
@ -228,7 +223,8 @@ describe('Form.vue —— extendState', () => {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect((wrapper.vm.formState as any).keyProp).toBe('custom-key');
|
||||
// keyProp 属于内置保留字段,即使以访问器形式返回也不允许覆盖
|
||||
expect((wrapper.vm.formState as any).keyProp).toBe('id');
|
||||
});
|
||||
|
||||
test('extendState 同步段读到的响应式数据变化时会重跑', async () => {
|
||||
|
||||
@ -90,9 +90,7 @@ describe('submitForm', () => {
|
||||
expect(extendState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('extendState 返回 keyProp 等只读派生字段时不抛错且正常 resolve', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
test('extendState 返回 keyProp 等内置保留字段时静默跳过且正常 resolve', async () => {
|
||||
const values = await submitForm({
|
||||
config: [{ type: 'text', name: 'text', text: 'text' }],
|
||||
initValues: { text: 'foo' },
|
||||
@ -100,10 +98,8 @@ describe('submitForm', () => {
|
||||
appContext,
|
||||
});
|
||||
|
||||
// keyProp 属于内置保留字段,被静默跳过,不污染最终 values
|
||||
expect(values).toEqual({ text: 'foo' });
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('在嵌套 items 配置下也能正确 resolve', async () => {
|
||||
|
||||
@ -16,9 +16,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import type { FormState } from '@form/index';
|
||||
import {
|
||||
applyExtendState,
|
||||
createObjectProp,
|
||||
createValues,
|
||||
datetimeFormatter,
|
||||
@ -1014,3 +1015,130 @@ describe('createObjectProp', () => {
|
||||
expect(createObjectProp('a.b.c', 'newKey', 'c')).toBe('a.b.newKey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyExtendState', () => {
|
||||
test('state 为 null 或 undefined 时不修改 formState', () => {
|
||||
const formState = { a: 1 } as any;
|
||||
|
||||
applyExtendState(formState, null);
|
||||
applyExtendState(formState, undefined);
|
||||
|
||||
expect(formState).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test('普通字段(data descriptor)以新增字段形式合并进 formState', () => {
|
||||
const formState = {} as any;
|
||||
|
||||
applyExtendState(formState, { foo: 'bar', num: 1 });
|
||||
|
||||
expect(formState.foo).toBe('bar');
|
||||
expect(formState.num).toBe(1);
|
||||
});
|
||||
|
||||
test('accessor descriptor 按原样 define 且读时求值', () => {
|
||||
const formState = {} as any;
|
||||
let count = 0;
|
||||
const state = {};
|
||||
Object.defineProperty(state, 'stage', {
|
||||
get() {
|
||||
count += 1;
|
||||
return 'stageValue';
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
applyExtendState(formState, state);
|
||||
|
||||
// 读时才求值
|
||||
expect(count).toBe(0);
|
||||
expect(formState.stage).toBe('stageValue');
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test('accessor descriptor 强制 configurable=true,可再次 define 不报错', () => {
|
||||
const formState = {} as any;
|
||||
const state = {};
|
||||
// 原始描述符 configurable 为 false
|
||||
Object.defineProperty(state, 'stage', {
|
||||
get: () => 'v',
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
applyExtendState(formState, state);
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(formState, 'stage');
|
||||
expect(descriptor?.configurable).toBe(true);
|
||||
// 再次合并(重新 define)不应抛出
|
||||
expect(() => applyExtendState(formState, state)).not.toThrow();
|
||||
});
|
||||
|
||||
test('reservedKeys 命中的 key 被跳过,不覆盖已有内置字段', () => {
|
||||
const formState = { values: { origin: true } } as any;
|
||||
const reservedKeys = new Set<string | symbol>(['values']);
|
||||
|
||||
applyExtendState(formState, { values: { changed: true }, extra: 1 }, reservedKeys);
|
||||
|
||||
// reserved key 未被覆盖
|
||||
expect(formState.values).toEqual({ origin: true });
|
||||
// 非 reserved 的新字段正常新增
|
||||
expect(formState.extra).toBe(1);
|
||||
});
|
||||
|
||||
test('reservedKeys 对 accessor 形式的同名 key 同样跳过', () => {
|
||||
const formState = { keyProp: 'origin' } as any;
|
||||
const reservedKeys = new Set<string | symbol>(['keyProp']);
|
||||
const state = {};
|
||||
Object.defineProperty(state, 'keyProp', {
|
||||
get: () => 'override',
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
applyExtendState(formState, state, reservedKeys);
|
||||
|
||||
expect(formState.keyProp).toBe('origin');
|
||||
});
|
||||
|
||||
test('未传 reservedKeys 时,只读 getter 字段被跳过并告警', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
const formState = {} as any;
|
||||
Object.defineProperty(formState, 'keyProp', {
|
||||
get: () => 'readonly',
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
applyExtendState(formState, { keyProp: 'newValue' });
|
||||
|
||||
expect(formState.keyProp).toBe('readonly');
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('未传 reservedKeys 时,存在 setter 的字段可被赋值', () => {
|
||||
let inner = 'old';
|
||||
const formState = {} as any;
|
||||
Object.defineProperty(formState, 'writable', {
|
||||
get: () => inner,
|
||||
set: (v: string) => {
|
||||
inner = v;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
applyExtendState(formState, { writable: 'new' });
|
||||
|
||||
expect(formState.writable).toBe('new');
|
||||
});
|
||||
|
||||
test('未传 reservedKeys 时,普通可写字段正常覆盖赋值', () => {
|
||||
const formState = { editable: 'old' } as any;
|
||||
|
||||
applyExtendState(formState, { editable: 'new' });
|
||||
|
||||
expect(formState.editable).toBe('new');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user