mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-05 01:32:10 +08:00
refactor(form): 使用 getter 访问 props 字段并补充单元测试
- formState 中与 props 对应的字段改用 getter,避免 props 与 formState 之间的同步中间态 - 完善 extendState 同步段的响应式追踪说明注释 - 新增 Form.extra.spec.ts 覆盖 isCompare 模式与 config 变化场景
This commit is contained in:
parent
fbbd05e291
commit
08011efd6d
@ -92,14 +92,45 @@ const fields = new Map<string, any>();
|
|||||||
|
|
||||||
const requestFuc = getConfig('request') as Function;
|
const requestFuc = getConfig('request') as Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formState 实现说明:
|
||||||
|
*
|
||||||
|
* 1. 与 props 直接对应的字段(config / initValues / lastValues / isCompare / parentValues /
|
||||||
|
* keyProp / popperClass)使用「访问器(getter)」定义,每次读取都会回到 `props.xxx`
|
||||||
|
* 取最新值,不存在「props 变了但 formState 还没同步过来」的中间态。
|
||||||
|
*
|
||||||
|
* 2. `values` / `lastValuesProcessed` 是 ref,Vue 的 `reactive` 会自动解包,因此每次
|
||||||
|
* 访问 `formState.values` / `formState.lastValuesProcessed` 也都是当前 ref 值。
|
||||||
|
*
|
||||||
|
* 3. `extendState` 注入的字段在下方的 `watchEffect` 中合并到 `formState`:
|
||||||
|
* - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的
|
||||||
|
* set,触发依赖通知;`extendState` 同步段读到的响应式数据变化时会自动重跑,
|
||||||
|
* 把最新值刷进 formState。
|
||||||
|
* - accessor 描述符(`{ get stage() { return ... } }`)按原样写入,调用方可以控制
|
||||||
|
* 读时求值,每次读取都会重新执行 getter。
|
||||||
|
*/
|
||||||
const formState: FormState = reactive<FormState>({
|
const formState: FormState = reactive<FormState>({
|
||||||
keyProp: props.keyProp,
|
get keyProp() {
|
||||||
popperClass: props.popperClass,
|
return props.keyProp;
|
||||||
config: props.config,
|
},
|
||||||
initValues: props.initValues,
|
get popperClass() {
|
||||||
isCompare: props.isCompare,
|
return props.popperClass;
|
||||||
lastValues: props.lastValues,
|
},
|
||||||
parentValues: props.parentValues,
|
get config() {
|
||||||
|
return props.config;
|
||||||
|
},
|
||||||
|
get initValues() {
|
||||||
|
return props.initValues;
|
||||||
|
},
|
||||||
|
get isCompare() {
|
||||||
|
return props.isCompare;
|
||||||
|
},
|
||||||
|
get lastValues() {
|
||||||
|
return props.lastValues;
|
||||||
|
},
|
||||||
|
get parentValues() {
|
||||||
|
return props.parentValues;
|
||||||
|
},
|
||||||
values,
|
values,
|
||||||
lastValuesProcessed,
|
lastValuesProcessed,
|
||||||
$emit: emit as (_event: string, ..._args: any[]) => void,
|
$emit: emit as (_event: string, ..._args: any[]) => void,
|
||||||
@ -119,20 +150,52 @@ const formState: FormState = reactive<FormState>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(async () => {
|
/**
|
||||||
formState.initValues = props.initValues;
|
* `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
|
||||||
formState.lastValues = props.lastValues;
|
* 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
|
||||||
formState.isCompare = props.isCompare;
|
*
|
||||||
formState.config = props.config;
|
* extendState: (formState) => ({
|
||||||
formState.keyProp = props.keyProp;
|
* username: store.username, // 同步读 store,会被跟踪
|
||||||
formState.popperClass = props.popperClass;
|
* env: store.env,
|
||||||
formState.parentValues = props.parentValues;
|
* })
|
||||||
|
*
|
||||||
|
* 当 `store.username` 变化时,整个 effect 重跑,新值会被刷进 `formState`。
|
||||||
|
*
|
||||||
|
* prop 派生字段(initValues / config / ...)已经在上方用 getter 定义,
|
||||||
|
* 这里不再重复同步;因此 `props.initValues` 这类高频变化也不会再触发
|
||||||
|
* `extendState` 重跑(旧版的性能问题修复点)。
|
||||||
|
*
|
||||||
|
* 实现细节:
|
||||||
|
* - data 描述符:通过 `formState[key] = value` 走 reactive proxy 的 set,
|
||||||
|
* 触发依赖通知;与旧版「逐项赋值」语义完全等价。
|
||||||
|
* - accessor 描述符(`{ get stage() {...} }`)按原样写入 formState,调用方
|
||||||
|
* 可以自行控制读时求值;强制 `configurable: true` 以便下一次重跑可再 define。
|
||||||
|
*/
|
||||||
|
watchEffect(async (onCleanup) => {
|
||||||
|
const { extendState } = props;
|
||||||
|
if (typeof extendState !== 'function') return;
|
||||||
|
|
||||||
if (typeof props.extendState === 'function') {
|
let stale = false;
|
||||||
const state = (await props.extendState(formState)) || {};
|
onCleanup(() => {
|
||||||
Object.entries(state).forEach(([key, value]) => {
|
stale = true;
|
||||||
formState[key] = value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let state: Record<string, any> = {};
|
||||||
|
try {
|
||||||
|
state = (await extendState(formState)) || {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MForm] extendState failed:', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stale) return;
|
||||||
|
|
||||||
|
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
||||||
|
if ('value' in descriptor) {
|
||||||
|
(formState as any)[key] = (state as any)[key];
|
||||||
|
} else {
|
||||||
|
descriptor.configurable = true;
|
||||||
|
Object.defineProperty(formState, key, descriptor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
515
packages/form/tests/unit/Form.extra.spec.ts
Normal file
515
packages/form/tests/unit/Form.extra.spec.ts
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
/*
|
||||||
|
* 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 { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
import MagicForm, { MForm } from '@form/index';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
|
||||||
|
const mountForm = (props: Record<string, any> = {}, options: Record<string, any> = {}) =>
|
||||||
|
mount(MForm, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus as any, MagicForm as any],
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initValues: {},
|
||||||
|
config: [],
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— 默认 props', () => {
|
||||||
|
test('未传任何 props 时使用默认值,渲染不报错', async () => {
|
||||||
|
const wrapper = mountForm();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.find('.m-form').exists()).toBe(true);
|
||||||
|
expect(wrapper.vm.values).toEqual({});
|
||||||
|
expect(wrapper.vm.changeRecords).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('height/labelWidth 透传到样式与子表单', async () => {
|
||||||
|
const wrapper = mountForm({ height: '300px', labelWidth: '120px' });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const formEl = wrapper.find('.m-form').element as HTMLElement;
|
||||||
|
expect(formEl.getAttribute('style') || '').toContain('height: 300px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— formState getter 行为', () => {
|
||||||
|
test('formState 的 keyProp / popperClass / config / initValues / isCompare / lastValues / parentValues 始终回读最新 props', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
keyProp: 'id',
|
||||||
|
popperClass: 'pop-a',
|
||||||
|
isCompare: false,
|
||||||
|
initValues: { a: 1 },
|
||||||
|
lastValues: { a: 0 },
|
||||||
|
parentValues: { x: 1 },
|
||||||
|
config: [{ text: 'a', name: 'a' }],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const fs1: any = wrapper.vm.formState;
|
||||||
|
expect(fs1.keyProp).toBe('id');
|
||||||
|
expect(fs1.popperClass).toBe('pop-a');
|
||||||
|
expect(fs1.isCompare).toBe(false);
|
||||||
|
expect(fs1.initValues).toEqual({ a: 1 });
|
||||||
|
expect(fs1.lastValues).toEqual({ a: 0 });
|
||||||
|
expect(fs1.parentValues).toEqual({ x: 1 });
|
||||||
|
expect(Array.isArray(fs1.config)).toBe(true);
|
||||||
|
|
||||||
|
// 修改 props,formState 上的 getter 应直接反映新值(无中间态)
|
||||||
|
await wrapper.setProps({
|
||||||
|
keyProp: 'uuid',
|
||||||
|
popperClass: 'pop-b',
|
||||||
|
isCompare: true,
|
||||||
|
parentValues: { x: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fs2: any = wrapper.vm.formState;
|
||||||
|
expect(fs2.keyProp).toBe('uuid');
|
||||||
|
expect(fs2.popperClass).toBe('pop-b');
|
||||||
|
expect(fs2.isCompare).toBe(true);
|
||||||
|
expect(fs2.parentValues).toEqual({ x: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('values / lastValuesProcessed 在 formState 上自动解包为 ref 当前值', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
isCompare: true,
|
||||||
|
initValues: { a: '1' },
|
||||||
|
lastValues: { a: '2' },
|
||||||
|
config: [{ text: 'a', type: 'text', name: 'a' }],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect((wrapper.vm.formState as any).values).toEqual({ a: '1' });
|
||||||
|
expect((wrapper.vm.formState as any).lastValuesProcessed).toEqual({ a: '2' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— extendState', () => {
|
||||||
|
test('extendState 抛错时被 catch,不影响表单渲染', async () => {
|
||||||
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const extendState = vi.fn(async () => {
|
||||||
|
throw new Error('boom');
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mountForm({
|
||||||
|
extendState,
|
||||||
|
config: [{ text: 'text', name: 'text', type: 'text' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(extendState).toHaveBeenCalled();
|
||||||
|
expect(errorSpy).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('.m-form').exists()).toBe(true);
|
||||||
|
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extendState 返回的普通字段被合并到 formState', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
extendState: async () => ({ extra: 'hello', count: 42 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect((wrapper.vm.formState as any).extra).toBe('hello');
|
||||||
|
expect((wrapper.vm.formState as any).count).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extendState 返回的 accessor 描述符按原样定义并支持读时求值', async () => {
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
const wrapper = mountForm({
|
||||||
|
extendState: () =>
|
||||||
|
Object.defineProperties(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
stage: {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
counter += 1;
|
||||||
|
return `stage-${counter}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const v1 = (wrapper.vm.formState as any).stage;
|
||||||
|
const v2 = (wrapper.vm.formState as any).stage;
|
||||||
|
|
||||||
|
expect(v1).not.toEqual(v2);
|
||||||
|
expect(v1).toMatch(/^stage-/);
|
||||||
|
expect(v2).toMatch(/^stage-/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extendState 同步段读到的响应式数据变化时会重跑', async () => {
|
||||||
|
const username = ref('alice');
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const wrapper = mountForm({
|
||||||
|
// 同步读取 ref,会被 watchEffect 跟踪
|
||||||
|
extendState: (_state: any) => {
|
||||||
|
calls.push(username.value);
|
||||||
|
return { username: username.value };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
expect((wrapper.vm.formState as any).username).toBe('alice');
|
||||||
|
|
||||||
|
username.value = 'bob';
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect((wrapper.vm.formState as any).username).toBe('bob');
|
||||||
|
// 至少跑了两次(初始 + 响应变化)
|
||||||
|
expect(calls.length).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未传 extendState 时 watchEffect 早退,不抛错', async () => {
|
||||||
|
const wrapper = mountForm({});
|
||||||
|
await nextTick();
|
||||||
|
expect(wrapper.find('.m-form').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— resetForm / changeRecords', () => {
|
||||||
|
test('resetForm 会清空 changeRecords', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
wrapper.find('input').setValue('hi');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.changeRecords.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
wrapper.vm.resetForm();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.changeRecords).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— submitForm 实例方法', () => {
|
||||||
|
test('校验通过返回 cloneDeep 后的 values', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
initValues: { text: 'hi' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const result = await wrapper.vm.submitForm();
|
||||||
|
expect(result).toEqual({ text: 'hi' });
|
||||||
|
// 默认 cloneDeep,应该不是同一引用
|
||||||
|
expect(result).not.toBe(wrapper.vm.values);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('native=true 直接返回原 values 引用', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
initValues: { text: 'hi' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const result = await wrapper.vm.submitForm(true);
|
||||||
|
expect(result).toBe(wrapper.vm.values);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('校验失败时 emit error 并抛出汇总后的错误(错误信息中包含字段 text)', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [
|
||||||
|
{
|
||||||
|
text: '名称',
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initValues: { name: '' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// 替换 useTemplateRef 暴露的 validate(写入 $.exposed 才能影响内部 setup 中的 tMagicFormRef.value)
|
||||||
|
const tmForm = wrapper.findComponent({ name: 'TMForm' });
|
||||||
|
expect(tmForm.exists()).toBe(true);
|
||||||
|
|
||||||
|
const invalidFields = {
|
||||||
|
name: [{ field: 'name', message: '必填' }],
|
||||||
|
};
|
||||||
|
const { exposed } = (tmForm.vm as any).$;
|
||||||
|
exposed.validate = vi.fn().mockRejectedValue(invalidFields);
|
||||||
|
|
||||||
|
let caught: Error | null = null;
|
||||||
|
try {
|
||||||
|
await wrapper.vm.submitForm();
|
||||||
|
} catch (e: any) {
|
||||||
|
caught = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(caught).toBeInstanceOf(Error);
|
||||||
|
expect(caught!.message).toContain('名称');
|
||||||
|
expect(caught!.message).toContain('必填');
|
||||||
|
expect(wrapper.emitted('error')).toBeTruthy();
|
||||||
|
expect(wrapper.emitted('error')![0][0]).toEqual(invalidFields);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('校验返回非 true(tdesign 风格)时也走错误分支', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: '账号', type: 'text', name: 'account' }],
|
||||||
|
initValues: { account: '' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const tmForm = wrapper.findComponent({ name: 'TMForm' });
|
||||||
|
const invalidFields = {
|
||||||
|
account: [{ field: 'account', message: '不能为空' }],
|
||||||
|
};
|
||||||
|
const { exposed } = (tmForm.vm as any).$;
|
||||||
|
exposed.validate = vi.fn().mockResolvedValue(invalidFields);
|
||||||
|
|
||||||
|
let caught: Error | null = null;
|
||||||
|
try {
|
||||||
|
await wrapper.vm.submitForm();
|
||||||
|
} catch (e: any) {
|
||||||
|
caught = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(caught).toBeInstanceOf(Error);
|
||||||
|
expect(caught!.message).toContain('账号');
|
||||||
|
expect(caught!.message).toContain('不能为空');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('校验失败但 invalidFields 中字段无对应 text 时回退使用 field/prop 名', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'a', type: 'text', name: 'a' }],
|
||||||
|
initValues: { a: '' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const tmForm = wrapper.findComponent({ name: 'TMForm' });
|
||||||
|
const { exposed } = (tmForm.vm as any).$;
|
||||||
|
exposed.validate = vi.fn().mockRejectedValue({
|
||||||
|
unknown: [{ field: '', message: '出错' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
let caught: Error | null = null;
|
||||||
|
try {
|
||||||
|
await wrapper.vm.submitForm();
|
||||||
|
} catch (e: any) {
|
||||||
|
caught = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(caught).toBeInstanceOf(Error);
|
||||||
|
// field 为空 -> 用 prop(unknown)
|
||||||
|
expect(caught!.message).toContain('unknown');
|
||||||
|
expect(caught!.message).toContain('出错');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— getTextByName', () => {
|
||||||
|
let wrapper: ReturnType<typeof mountForm>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = mountForm({
|
||||||
|
config: [
|
||||||
|
{ text: '名称', type: 'text', name: 'name' },
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
items: [
|
||||||
|
{ text: '内层名称', type: 'text', name: 'inner' },
|
||||||
|
{
|
||||||
|
name: 'nested',
|
||||||
|
items: [{ text: '深层', type: 'text', name: 'deep' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 无 name 的容器,items 应能继续被搜索
|
||||||
|
{
|
||||||
|
items: [{ text: '无名容器内字段', type: 'text', name: 'plain' }],
|
||||||
|
},
|
||||||
|
// text 非字符串
|
||||||
|
{ text: { foo: 'bar' } as any, type: 'text', name: 'nonString' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('单层名匹配', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('name')).toBe('名称');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('点分隔多层路径匹配', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('object.inner')).toBe('内层名称');
|
||||||
|
expect(wrapper.vm.getTextByName('object.nested.deep')).toBe('深层');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('无 name 容器的 items 也能被搜索到', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('plain')).toBe('无名容器内字段');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('找不到时返回 undefined', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('not.exist')).toBeUndefined();
|
||||||
|
expect(wrapper.vm.getTextByName('object.unknown')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('text 非字符串时返回 undefined', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('nonString')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('参数非法时返回 undefined', () => {
|
||||||
|
expect(wrapper.vm.getTextByName('')).toBeUndefined();
|
||||||
|
// @ts-expect-error 故意传非数组
|
||||||
|
expect(wrapper.vm.getTextByName('name', null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— preventSubmitDefault', () => {
|
||||||
|
test('preventSubmitDefault=true 时 submit 事件 preventDefault 被调用', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
preventSubmitDefault: true,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const formEl = wrapper.find('.m-form').element as HTMLFormElement;
|
||||||
|
const evt = new Event('submit', { cancelable: true, bubbles: true });
|
||||||
|
const spy = vi.spyOn(evt, 'preventDefault');
|
||||||
|
formEl.dispatchEvent(evt);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preventSubmitDefault=false(默认)时不调用 preventDefault', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const formEl = wrapper.find('.m-form').element as HTMLFormElement;
|
||||||
|
const evt = new Event('submit', { cancelable: true, bubbles: true });
|
||||||
|
const spy = vi.spyOn(evt, 'preventDefault');
|
||||||
|
formEl.dispatchEvent(evt);
|
||||||
|
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— isCompare 模式', () => {
|
||||||
|
test('isCompare=true 时 lastValuesProcessed 会被初始化', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
isCompare: true,
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
initValues: { text: 'a' },
|
||||||
|
lastValues: { text: 'b' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.values.text).toBe('a');
|
||||||
|
expect(wrapper.vm.lastValuesProcessed.text).toBe('b');
|
||||||
|
expect(wrapper.vm.initialized).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isCompare=false 时 lastValuesProcessed 不会被填充', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
isCompare: false,
|
||||||
|
config: [{ text: 'text', type: 'text', name: 'text' }],
|
||||||
|
initValues: { text: 'a' },
|
||||||
|
lastValues: { text: 'b' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.values.text).toBe('a');
|
||||||
|
expect(wrapper.vm.lastValuesProcessed).toEqual({});
|
||||||
|
expect(wrapper.vm.initialized).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form.vue —— config 变化', () => {
|
||||||
|
test('config 引用变化会重新初始化(initialized 短暂置 false 后回 true)', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'a', type: 'text', name: 'a' }],
|
||||||
|
initValues: { a: '1' },
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.initialized).toBe(true);
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
config: [{ text: 'b', type: 'text', name: 'b' }],
|
||||||
|
initValues: { b: '2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第一次 microtask 后还在重建
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.initialized).toBe(true);
|
||||||
|
expect(wrapper.vm.values).toHaveProperty('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config 变化会清空 changeRecords', async () => {
|
||||||
|
const wrapper = mountForm({
|
||||||
|
config: [{ text: 'a', type: 'text', name: 'a' }],
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
wrapper.find('input').setValue('xx');
|
||||||
|
await nextTick();
|
||||||
|
expect(wrapper.vm.changeRecords.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
config: [{ text: 'b', type: 'text', name: 'b' }],
|
||||||
|
initValues: {},
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.changeRecords).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user