mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-15 08:28:41 +00:00
182 lines
6.5 KiB
TypeScript
182 lines
6.5 KiB
TypeScript
/*
|
||
* 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();
|
||
});
|
||
});
|