tmagic-editor/packages/form/tests/unit/utils/formStateProxy.spec.ts
roymondchen a7999f50d5 feat(form): 用 FormContext 与 provide/inject 替代 extendState 钩子
统一表单业务上下文的注入方式,通过读穿 Proxy 保持 mForm 回调签名不变,并移除 extendFormState/extendState 等分散的扩展钩子。
2026-09-01 14:48:42 +08:00

182 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, expect, test } from 'vitest';
import { reactive, ref } from 'vue';
import type { FormState } from '@form/index';
import { createFormStateProxy, mergeFormContexts } from '@form/utils/formStateProxy';
const makeCore = (extra: Record<string, any> = {}): FormState =>
reactive({
config: [],
initValues: {},
lastValues: {},
isCompare: false,
values: {},
lastValuesProcessed: {},
$emit: () => undefined,
keyProp: '__key',
setField: () => undefined,
getField: () => undefined,
deleteField: () => undefined,
$messageBox: {} as any,
$message: {} as any,
...extra,
}) as FormState;
describe('createFormStateProxy', () => {
test('核心字段优先于 context', () => {
const formState = createFormStateProxy(makeCore({ keyProp: 'id' }), () => ({ keyProp: 'hacked', extra: 1 }));
expect(formState.keyProp).toBe('id');
expect((formState as any).extra).toBe(1);
});
test('Object.entries 能枚举到扩展字段', () => {
const formState = createFormStateProxy(makeCore(), () => ({ username: 'alice' }));
const keys = Object.keys(formState);
expect(keys).toContain('username');
expect(keys).toContain('values');
expect(Object.entries(formState).some(([k, v]) => k === 'username' && v === 'alice')).toBe(true);
});
test('set 写入 core读取优先于 context', () => {
const core = makeCore();
const formState = createFormStateProxy(core, () => ({ stage: 'from-context' }));
(formState as any).stage = 'assigned';
expect((formState as any).stage).toBe('assigned');
expect((core as any).stage).toBe('assigned');
});
test('has 在 core 或 context 命中时为 true', () => {
const formState = createFormStateProxy(makeCore(), () => ({ username: 'alice' }));
expect('values' in formState).toBe(true);
expect('username' in formState).toBe(true);
expect('missing' in formState).toBe(false);
});
test('symbol key 只走 core四个 trap 语义一致', () => {
const key = Symbol('vue-raw');
const core = makeCore();
(core as any)[key] = 'from-core';
const onlyInContext = Symbol('ctx-only');
const formState = createFormStateProxy(core, () => ({ [key]: 'from-context', [onlyInContext]: 1 }));
expect((formState as any)[key]).toBe('from-core');
// context 上的 symbol 不应被 has / ownKeys / getOwnPropertyDescriptor 报告为存在,
// 否则会与 get 返回 undefined 自相矛盾
expect(onlyInContext in formState).toBe(false);
expect(Object.getOwnPropertySymbols(formState)).not.toContain(onlyInContext);
expect(Object.getOwnPropertyDescriptor(formState, onlyInContext)).toBeUndefined();
expect((formState as any)[onlyInContext]).toBeUndefined();
});
test('core 上存在但值为 undefined 的字段不读穿 context', () => {
const formState = createFormStateProxy(makeCore({ parentValues: undefined }), () => ({ parentValues: { a: 1 } }));
expect(formState.parentValues).toBeUndefined();
});
test('getContext 返回 undefined 时读扩展字段不抛错', () => {
const formState = createFormStateProxy(makeCore(), () => undefined as any);
expect((formState as any).username).toBeUndefined();
expect('username' in formState).toBe(false);
});
test('getOwnPropertyDescriptor 对 core/context 都不存在的 key 返回 undefined', () => {
const formState = createFormStateProxy(makeCore(), () => ({ username: 'alice' }));
expect(Object.getOwnPropertyDescriptor(formState, 'nope')).toBeUndefined();
expect(Object.getOwnPropertyDescriptor(formState, 'username')?.enumerable).toBe(true);
});
test('getContext 可以是 Ref', () => {
const ctx = ref({ username: 'alice' });
const formState = createFormStateProxy(makeCore(), ctx);
expect((formState as any).username).toBe('alice');
ctx.value = { username: 'bob' };
expect((formState as any).username).toBe('bob');
});
});
describe('mergeFormContexts', () => {
test('靠后的层优先', () => {
const merged = mergeFormContexts({ a: 1, b: 1 }, { b: 2, c: 2 }, { c: 3 }) as any;
expect(merged.a).toBe(1);
expect(merged.b).toBe(2);
expect(merged.c).toBe(3);
});
test('accessor 保持读时求值,不在合并时被展开', () => {
let count = 0;
const layer = Object.defineProperties(
{},
{
stage: {
enumerable: true,
get: () => {
count += 1;
return count;
},
},
},
);
const merged = mergeFormContexts(layer) as any;
expect(count).toBe(0);
const multi = mergeFormContexts({ other: 1 }, layer) as any;
expect(count).toBe(0);
expect(multi.stage).toBe(1);
expect(multi.stage).toBe(2);
expect(merged.stage).toBe(3);
});
test('枚举语义覆盖所有层,且不重复', () => {
const merged = mergeFormContexts({ a: 1, b: 1 }, { b: 2, c: 2 });
expect(Object.keys(merged).sort()).toEqual(['a', 'b', 'c']);
expect(Object.entries(merged).sort()).toEqual([
['a', 1],
['b', 2],
['c', 2],
]);
});
test('忽略 undefined / null 层;零层与单层不额外包 Proxy', () => {
const only = { a: 1 };
expect(mergeFormContexts(undefined, null)).toEqual({});
expect(mergeFormContexts(undefined, only, null)).toBe(only);
// 空层数组每次返回同一个共享空对象
expect(mergeFormContexts()).toBe(mergeFormContexts(undefined));
});
test('has 与 getOwnPropertyDescriptor 与取值一致', () => {
const merged = mergeFormContexts({ a: 1 }, { b: 2 });
expect('a' in merged).toBe(true);
expect('b' in merged).toBe(true);
expect('missing' in merged).toBe(false);
expect(Object.getOwnPropertyDescriptor(merged, 'b')?.value).toBe(2);
expect(Object.getOwnPropertyDescriptor(merged, 'missing')).toBeUndefined();
});
});