/* * 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 = {}): 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(); }); });