tmagic-editor/packages/form/tests/unit/submitForm.spec.ts
roymondchen 638c3e9f3c feat(form): 新增 submitForm 命令式提交函数
提供脱离组件树以函数方式完成一次表单校验/提交的能力,类似 ElMessage 用法:
传入 config/initValues 等 props 后内部临时挂载 Form 实例,
初始化完成即调用 submitForm,校验通过 resolve 表单值、失败 reject,
最后自动卸载,并支持 appContext 继承、timeout 与 native 透传。

同步补充单元测试、API 文档及侧边栏入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 15:55:28 +08:00

206 lines
5.9 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 { afterEach, beforeAll, describe, expect, test, vi } from 'vitest';
import { type AppContext, createApp, defineComponent, h } from 'vue';
import MagicForm, { submitForm } from '@form/index';
import ElementPlus from 'element-plus';
let appContext: AppContext;
beforeAll(() => {
// 构造一个父级 app把 element-plus 与 m-form 插件装上,
// 之后通过 appContext 传给 submitForm 复用全局注册
const parentApp = createApp(defineComponent({ render: () => h('div') }));
parentApp.use(ElementPlus);
parentApp.use(MagicForm);
appContext = parentApp._context;
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('submitForm', () => {
test('校验通过时 resolve 表单值,并自动清理 DOM', async () => {
const values = await submitForm({
config: [
{
type: 'text',
name: 'text',
text: 'text',
},
],
initValues: { text: 'hello' },
appContext,
});
expect(values).toEqual({ text: 'hello' });
expect(document.body.querySelector('.m-form')).toBeNull();
});
test('native=true 时返回原始(未 clone的 values', async () => {
const initValues = { text: 'origin' };
const values = await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues,
native: true,
appContext,
});
expect(values).toEqual({ text: 'origin' });
});
test('支持 extendState 扩展状态', async () => {
const extendState = vi.fn(async () => ({ extra: 'value' }));
await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'foo' },
extendState,
appContext,
});
expect(extendState).toHaveBeenCalled();
});
test('在嵌套 items 配置下也能正确 resolve', async () => {
const values = await submitForm({
config: [
{ type: 'text', name: 'name', text: 'name' },
{
name: 'object',
items: [{ type: 'text', name: 'nested', text: 'nested' }],
},
],
initValues: {
name: 'a',
object: { nested: 'b' },
},
appContext,
});
expect(values).toEqual({
name: 'a',
object: { nested: 'b' },
});
});
test('多次连续调用不会相互干扰', async () => {
const [v1, v2] = await Promise.all([
submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'first' },
appContext,
}),
submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'second' },
appContext,
}),
]);
expect(v1).toEqual({ text: 'first' });
expect(v2).toEqual({ text: 'second' });
expect(document.body.querySelector('.m-form')).toBeNull();
});
test('多次串行调用后 document.body 不留下任何节点', async () => {
const baseChildCount = document.body.children.length;
for (let i = 0; i < 5; i++) {
await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: `value-${i}` },
appContext,
});
}
// 反复调用后body 下不应残留任何挂载容器
expect(document.body.children.length).toBe(baseChildCount);
expect(document.body.querySelector('.m-form')).toBeNull();
});
test('调用过程中临时容器会被附加到 body 上,结束后被移除', async () => {
const baseChildCount = document.body.children.length;
const pending = submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'in-flight' },
appContext,
});
// 此时容器应已加入 body
expect(document.body.children.length).toBe(baseChildCount + 1);
await pending;
expect(document.body.children.length).toBe(baseChildCount);
});
test('未注入 DOM 环境时document 不可用)以错误 reject', async () => {
const originalDocument = globalThis.document;
// 模拟纯 Node 环境
delete (globalThis as any).document;
let caught: any = null;
try {
await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'no-dom' },
appContext,
});
} catch (e) {
caught = e;
} finally {
(globalThis as any).document = originalDocument;
}
expect(caught).toBeInstanceOf(Error);
});
test('timeout > 0 时会注册定时器timeout <= 0 时不注册', async () => {
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'with-timeout' },
timeout: 5000,
appContext,
});
const calledWithTimeout = setTimeoutSpy.mock.calls.some(([, delay]) => delay === 5000);
expect(calledWithTimeout).toBe(true);
setTimeoutSpy.mockClear();
await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'no-timeout' },
timeout: 0,
appContext,
});
const calledWithZero = setTimeoutSpy.mock.calls.some(([, delay]) => delay === 0);
expect(calledWithZero).toBe(false);
setTimeoutSpy.mockRestore();
});
});