表单字段逐项报错并可交给陶泥儿处理
Project CI / Backend tests (pull_request) Failing after 22s
Project CI / Native shell tests (pull_request) Failing after 4m6s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m30s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m17s
Project CI / AI game creator shell web tests (pull_request) Failing after 35s
Project CI / Frontend tests (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m2s

- xhsMinitoolFields.ts 的 validateXhsMinitoolForm 一次返回全部出错字段

- xhsMinitoolInstruction.ts 新增字段红字指令,示意里的字数上限改用 XHS_MINITOOL_TEXT_MAX_CHARS

- useXhsMinitoolExport.ts 的 fieldError 改为 fieldErrors,新增 handFieldErrorsToAgent:交接后清红字与 dirty,让自动重读能采纳 agent 的修正

- FormCard.tsx 每个出错字段各挂红框与红字,标题下新增「交给陶泥儿」入口

- ArtifactsPane.tsx 接上新字段与新动作

- 补 xhsMinitoolFields / xhsMinitoolExport 测试覆盖多字段报错与交接
This commit is contained in:
2026-10-06 22:56:51 +08:00
parent ac0b08b2ac
commit 157b4eb861
7 changed files with 249 additions and 63 deletions
@@ -45,6 +45,7 @@ import {
import {
buildXhsMinitoolAdaptationInstruction,
buildXhsMinitoolErrorFixInstruction,
buildXhsMinitoolFormErrorInstruction,
buildXhsMinitoolFormFillInstruction,
} from './xhsMinitoolInstruction';
@@ -86,9 +87,7 @@ export function useXhsMinitoolExport({
}) {
const [form, setForm] = useState<XHSMiniToolExportForm | null>(null);
const [hasScript, setHasScript] = useState(false);
const [fieldError, setFieldError] = useState<XhsMinitoolFieldError | null>(
null,
);
const [fieldErrors, setFieldErrors] = useState<XhsMinitoolFieldError[]>([]);
const [conflict, setConflict] = useState<SaveConflict | null>(null);
const [failure, setFailure] = useState<XhsMinitoolCardFailure | null>(null);
/**
@@ -153,8 +152,8 @@ export function useXhsMinitoolExport({
}, [form]);
useEffect(() => {
adoptableRef.current = !dirty && !conflict && !fieldError;
}, [dirty, conflict, fieldError]);
adoptableRef.current = !dirty && !conflict && fieldErrors.length === 0;
}, [dirty, conflict, fieldErrors]);
/**
* 换项目时把整套状态与本地判据清回初值。
@@ -169,7 +168,7 @@ export function useXhsMinitoolExport({
projectPathRef.current = projectPath;
setForm(null);
setHasScript(false);
setFieldError(null);
setFieldErrors([]);
setConflict(null);
setFailure(null);
setDismissedFailure('');
@@ -262,7 +261,7 @@ export function useXhsMinitoolExport({
return content.hasScript;
}
setForm(content.form);
setFieldError(null);
setFieldErrors([]);
setConflict(null);
setFailure(null);
try {
@@ -337,11 +336,11 @@ export function useXhsMinitoolExport({
if (!projectPath) return false;
const invoke = resolveTauriInvoke();
if (!invoke) return false;
// 首个字段错误即返回:红字挂到对应输入框,等用户改完下一次输入再存。`dirty` 归零,同一份
// 不合法的输入不会反复排队。
// 一次把**所有**出错字段都挂出来:用户能同时看到每个字段各自的问题,不必改一个才知道下一个。
// `dirty` 归零,同一份不合法的输入不会反复排队。
const invalid = validateXhsMinitoolForm(next);
if (invalid) {
setFieldError(invalid);
if (invalid.length > 0) {
setFieldErrors(invalid);
setDirty(false);
return false;
}
@@ -357,7 +356,7 @@ export function useXhsMinitoolExport({
// 保存命令的返回值就是当前表单的新指纹:这份表单的基线已经配套,不必再补读。
baselineRef.current = nextBaseline;
hashPendingRef.current = false;
setFieldError(null);
setFieldErrors([]);
setConflict(null);
setFailure(null);
return true;
@@ -389,12 +388,14 @@ export function useXhsMinitoolExport({
);
useEffect(() => {
if (!enabled || !form || !dirty || conflict || fieldError) return;
if (!enabled || !form || !dirty || conflict || fieldErrors.length > 0) {
return;
}
const timer = setTimeout(() => {
void persist(form, baselineRef.current);
}, AUTOSAVE_DELAY_MS);
return () => clearTimeout(timer);
}, [enabled, form, dirty, conflict, fieldError, persist]);
}, [enabled, form, dirty, conflict, fieldErrors, persist]);
/** 改一个字段。改哪个字段就清哪个字段的旧报错——别让已经改好的红字继续挂着。 */
const updateField = useCallback(
@@ -404,8 +405,8 @@ export function useXhsMinitoolExport({
setForm((previous) =>
previous ? { ...previous, [field]: value } : previous,
);
setFieldError((previous) =>
previous && previous.field === field ? null : previous,
setFieldErrors((previous) =>
previous.filter((error) => error.field !== field),
);
setDirty(true);
},
@@ -482,12 +483,12 @@ export function useXhsMinitoolExport({
return;
}
// 先 flush 自动保存:用户刚敲完就点打包时,注册表里可能还是上一份。
if (dirty && form && !conflict && !fieldError) {
if (dirty && form && !conflict && fieldErrors.length === 0) {
const flushed = await persist(form, baselineRef.current);
if (!flushed) return;
}
// 没解决的冲突 / 字段红字:盘上的内容和用户看到的不一致,先别打包。
if (conflict || fieldError) return;
if (conflict || fieldErrors.length > 0) return;
await buildOnce();
} finally {
// 旧项目那一次打包的收尾不解新项目的锁。
@@ -499,7 +500,7 @@ export function useXhsMinitoolExport({
dirty,
form,
conflict,
fieldError,
fieldErrors,
persist,
buildOnce,
]);
@@ -542,11 +543,26 @@ export function useXhsMinitoolExport({
const fillForm = useCallback(async (): Promise<void> => {
if (!projectPath || !form) return;
// 冲突或字段红字挂着时不重复撞一次保存(自动保存此时也是停的),直接把现场交给 agent。
if (dirty && !conflict && !fieldError) {
if (dirty && !conflict && fieldErrors.length === 0) {
await persist(form, baselineRef.current);
}
await sendToAgent(buildXhsMinitoolFormFillInstruction());
}, [projectPath, form, dirty, conflict, fieldError, persist, sendToAgent]);
}, [projectPath, form, dirty, conflict, fieldErrors, persist, sendToAgent]);
/**
* 把手上的字段限制问题交给 agent:指令带上界面本地校验给用户的那些原话,agent 直接改注册表把值改成
* 合规的。
*
* 交出去之后清掉本地红字与 `dirty`:红字留着会让自动重读一直不肯采纳 agent 写回来的新值
* (`adoptableRef` 要求手上没有待处理现场),用户就会卡在「点了按钮却什么都没变」。用户那份不合法
* 的输入随指令一起带过去,agent 不必回界面猜。
*/
const handFieldErrorsToAgent = useCallback((): void => {
if (!projectPath || !form || fieldErrors.length === 0) return;
setFieldErrors([]);
setDirty(false);
void sendToAgent(buildXhsMinitoolFormErrorInstruction(form, fieldErrors));
}, [projectPath, form, fieldErrors, sendToAgent]);
/**
* 一键适配(bootstrap):把首次适配指令发给同一个项目会话。发出即收起阻断提示,别让弹窗再拦住用户。
@@ -595,7 +611,7 @@ export function useXhsMinitoolExport({
return {
form,
hasScript,
fieldError,
fieldErrors,
conflict,
failure: visibleFailure,
loading,
@@ -609,6 +625,7 @@ export function useXhsMinitoolExport({
adapt,
repair,
fillForm,
handFieldErrorsToAgent,
dismissAdaptPrompt,
dismissFailure,
};
@@ -9,7 +9,7 @@ import type { XHSMiniToolExportForm } from '../generated/XHSMiniToolExportForm';
/** 表单里的三个字段,与 [`XHSMiniToolExportForm`] 的键一一对应。 */
export type XhsMinitoolField = 'name' | 'introduction' | 'iconPath';
/** 字段红字:一次只挂一个出错字段,用户改完再报下一个。 */
/** 字段红字:一个出错字段一条,界面按 `field` 挂到对应输入框下面。 */
export type XhsMinitoolFieldError = {
field: XhsMinitoolField;
reason: string;
@@ -63,18 +63,18 @@ export const XHS_MINITOOL_FORM_FIELDS: readonly XhsMinitoolFieldMeta[] = [
*
* 校验放在界面这一层是刻意的:用户最终面对的就是界面,这份表单也只是一份草稿,不值得让宿主再挡
* 一次。文件是不是真的存在、是不是符号链接、扩展名与大小都不在这里判——那些是「真的去读它」时
* 才成立的事实,由预览 / 下载那条路各自的路径门禁负责。首个错误即返回,与表单的提交顺序一致。
* 才成立的事实,由预览 / 下载那条路各自的路径门禁负责。
*
* 返回**全部**出错字段(按表单顺序),让界面一次把每个字段的红字都挂上;调用方只需判空数组。
*/
export function validateXhsMinitoolForm(
form: XHSMiniToolExportForm,
): XhsMinitoolFieldError | null {
return (
validateText('name', '名称', form.name) ??
validateText('introduction', '简介', form.introduction) ??
(form.iconPath.trim()
? null
: { field: 'iconPath', reason: 'icon 不能为空' })
);
): XhsMinitoolFieldError[] {
return [
validateText('name', '名称', form.name),
validateText('introduction', '简介', form.introduction),
form.iconPath.trim() ? null : { field: 'iconPath', reason: '图标不能为空' },
].filter((error): error is XhsMinitoolFieldError => error !== null);
}
function validateText(
@@ -9,8 +9,14 @@
* `tests/xhsMinitoolContract.test.ts` 对着 Rust 权威 `src-tauri/src/export/draft/xhs_minitool/layout.rs`
* 逐条钉住。
*/
import type { XHSMiniToolExportForm } from '../generated/XHSMiniToolExportForm';
import type { XHSMiniToolExportRegistry } from '../generated/XHSMiniToolExportRegistry';
import type { XhsMinitoolCardFailure } from './xhsMinitoolFailure';
import {
XHS_MINITOOL_FORM_FIELDS,
XHS_MINITOOL_TEXT_MAX_CHARS,
type XhsMinitoolFieldError,
} from './xhsMinitoolFields';
/** 目标名,与 `model.ts` 的 `TARGET_META` 人话一致。 */
export const XHS_MINITOOL_TARGET_LABEL = '小红书小工具';
@@ -37,8 +43,8 @@ export const XHS_MINITOOL_PACKAGE_SKILL_NAME = 'vite-export-xhs-minitool';
*/
const XHS_MINITOOL_REGISTRY_SHAPE_HINT: XHSMiniToolExportRegistry = {
form: {
name: '最多 14 个字',
introduction: '最多 14 个字',
name: `最多 ${XHS_MINITOOL_TEXT_MAX_CHARS} 个字`,
introduction: `最多 ${XHS_MINITOOL_TEXT_MAX_CHARS} 个字`,
iconPath: 'png、jpg、jpeg;大小不超过 5 M;推荐比例为1:1',
},
};
@@ -94,3 +100,36 @@ ${JSON.stringify(XHS_MINITOOL_REGISTRY_SHAPE_HINT, null, 2)}
* 对于图标示例图等字段可以看看目前是否已经有合适的, 没有的话可以(征询用户意见后)生图/留给用户自己选择
`;
}
/**
* 「字段不合限制」指令:界面本地校验挡下表单后,让 agent 直接改注册表把值改成合规的。
*
* 与「帮我填」同一条链(落点都是那份文件、宿主自动重读带回界面),区别是这条把**界面上那些原话**
* 带上:用户已经被限制挡下,agent 不必回界面猜他卡在哪。当前值也照抄进去——不合法的值没落盘,
* agent 光读文件看不到。
*/
export function buildXhsMinitoolFormErrorInstruction(
form: XHSMiniToolExportForm,
errors: readonly XhsMinitoolFieldError[],
): string {
const lines = errors
.map((error) => {
const label =
XHS_MINITOOL_FORM_FIELDS.find((meta) => meta.field === error.field)
?.label ?? error.field;
return ` - ${label}(${error.field}):${error.reason};当前值 ${JSON.stringify(
form[error.field],
)}`;
})
.join('\n');
return `
帮我处理用于发布 ${XHS_MINITOOL_TARGET_LABEL} 的表单 ${XHS_MINITOOL_REGISTRY_RELATIVE_PATH} 里不合限制的字段。
* 被限制挡下的字段:
${lines}
* 请按限制改成合适的值, 直接修改文件, 改完不必重述.
* 结构和限制:
${JSON.stringify(XHS_MINITOOL_REGISTRY_SHAPE_HINT, null, 2)}
`;
}
@@ -135,11 +135,12 @@ export function ArtifactsPane({
formSlot = (
<FormCard
form={form}
fieldError={minitool.fieldError}
fieldErrors={minitool.fieldErrors}
projectPath={projectPath}
onChange={minitool.updateField}
onDownloadIcon={downloadIcon}
onAskAgentToFill={() => void minitool.fillForm()}
onAskAgentAboutErrors={minitool.handFieldErrorsToAgent}
/>
);
} else if (!minitool.readSettled) {
@@ -16,8 +16,11 @@ import { IconPreview } from './IconPreview';
* 小红书上传表单。名称与介绍可复制(它们是要粘贴到上传页的文字),图标只给「下载」——复制一条
* 项目内路径没有意义。
*
* 表单只收集值:校验在界面这一层(保存前,见 `validateXhsMinitoolForm`),红字是本地校验的
* 结果;宿主那侧只按原样读写草稿。
* 表单只收集值:校验在界面这一层(保存前,见 `validateXhsMinitoolForm`),**每个**出错字段各自挂
* 红框与红字;宿主那侧只按原样读写草稿。
*
* 有字段被限制挡下时,标题下多一行说明 +「交给陶泥儿」:用户自己改不动就让 agent 直接改注册表,
* 指令带上这些红字的原话。
* 文案一律说「上传到小红书要填什么」,不暴露脚本名、注册表与 zip 这些实现词。
*
* 版式对齐发布面板(`GameDistributionPublishPanel`):标签在上、输入独占整行、字段之间留够呼吸,
@@ -26,18 +29,20 @@ import { IconPreview } from './IconPreview';
*/
export function FormCard({
form,
fieldError,
fieldErrors,
projectPath,
onChange,
onDownloadIcon,
onAskAgentToFill,
onAskAgentAboutErrors,
}: {
form: XHSMiniToolExportForm;
fieldError: XhsMinitoolFieldError | null;
fieldErrors: XhsMinitoolFieldError[];
projectPath: string | null;
onChange: (field: keyof XHSMiniToolExportForm, value: string) => void;
onDownloadIcon: () => void;
onAskAgentToFill: () => void;
onAskAgentAboutErrors: () => void;
}) {
return (
<section
@@ -64,10 +69,28 @@ export function FormCard({
<span>帮我填</span>
</PaneButton>
</div>
{fieldErrors.length > 0 ? (
<div className="mt-3 flex flex-wrap items-center justify-between gap-2 rounded-xl border border-(--platform-warm-border) bg-(--platform-warm-bg) px-3 py-2">
<p className="text-xs leading-relaxed text-(--platform-text-base)">
有 {fieldErrors.length} 个字段不合平台限制。
</p>
<PaneButton
variant="primary"
ariaLabel="交给陶泥儿处理这些字段"
title="把这些字段的问题与当前值原样发给陶泥儿,让它直接改注册表"
disabled={!projectPath}
onClick={onAskAgentAboutErrors}
>
<Sparkles size={13} aria-hidden="true" />
<span>交给陶泥儿</span>
</PaneButton>
</div>
) : null}
<div className="mt-4 grid gap-4">
{XHS_MINITOOL_FORM_FIELDS.map((meta) => {
const error =
fieldError?.field === meta.field ? fieldError.reason : null;
fieldErrors.find((item) => item.field === meta.field)?.reason ??
null;
if (meta.field === 'iconPath') {
return (
<IconField
@@ -126,8 +149,8 @@ function FieldRow({
onChange: (value: string) => void;
}) {
const id = useId();
// 按码点计数,与宿主 `validate_text` 的 `chars().count()` 对齐:`String.length` 数的是 UTF-16
// 码元,一个 emoji 会被这里算成 2、宿主算成 1,计数徽标会比宿主更早报超限。
// 按码点计数:`String.length` 数的是 UTF-16 码元,一个 emoji 会被这里算成 2、平台算成 1,
// 计数徽标会比实际更早报超限。与 `validateXhsMinitoolForm` 用的是同一套计数。
const used = Array.from(value.trim()).length;
const overLimit = maxChars !== undefined && used > maxChars;
@@ -38,7 +38,9 @@ import {
XhsMinitoolFailureRepairability,
} from '../src/view/project-development/export/state/xhsMinitoolFailure';
import {
buildXhsMinitoolFormErrorInstruction,
XHS_MINITOOL_ARTIFACT_RELATIVE_PATH,
XHS_MINITOOL_REGISTRY_RELATIVE_PATH,
XHS_MINITOOL_SCRIPT_NAME,
} from '../src/view/project-development/export/state/xhsMinitoolInstruction';
import { ArtifactsPane } from '../src/view/project-development/export/tabs/xiaohongshu/ArtifactsPane';
@@ -209,6 +211,24 @@ describe('交给 code agent 的指令', () => {
// `saveConflict` 不在这张表里:它只出现在保存命令的失败表上,有专属的冲突面板;字段红字则
// 在界面这一层就被拦下,根本走不到保存命令。
});
it('字段红字交给 agent 的指令带上红字原话、当前值与落点', () => {
const text = buildXhsMinitoolFormErrorInstruction(
{ name: '太长的名字', introduction: '', iconPath: '' },
[
{ field: 'name', reason: '名称最多 14 个字,当前 16 个字' },
{ field: 'iconPath', reason: '图标不能为空' },
],
);
// 红字原话照抄:agent 不必回界面猜用户卡在哪。
expect(text).toContain('名称最多 14 个字,当前 16 个字');
expect(text).toContain('图标不能为空');
// 不合法的值没落盘,必须写进指令,agent 光读文件看不到。
expect(text).toContain('"太长的名字"');
// 落点是那份注册表文件,不是聊天里的一段值。
expect(text).toContain(XHS_MINITOOL_REGISTRY_RELATIVE_PATH);
});
});
describe('面板左半', () => {
@@ -248,6 +268,31 @@ describe('面板左半', () => {
screen.getByRole('button', { name: '下载代码包' });
});
it('字段被限制挡下时给出「交给陶泥儿」,没有问题时这颗按钮不出现', () => {
const handFieldErrorsToAgent = vi.fn();
renderPane(
fakeMinitool({
fieldErrors: [{ field: 'name', reason: '名称最多 14 个字' }],
handFieldErrorsToAgent,
}),
);
screen.getByText('有 1 个字段不合平台限制。');
// 红字挂在出错字段下面,用户与读屏都看得到。
screen.getByText('名称最多 14 个字');
fireEvent.click(
screen.getByRole('button', { name: '交给陶泥儿处理这些字段' }),
);
expect(handFieldErrorsToAgent).toHaveBeenCalledTimes(1);
cleanup();
// 没有红字时不给这个入口:没有东西可交。
renderPane();
expect(
screen.queryByRole('button', { name: '交给陶泥儿处理这些字段' }),
).toBeNull();
});
it('代码包只在打包成功后给出下载入口', () => {
// 还没打过:只有「打包」,「下载代码包」不出现(不给一个下不了的按钮)。
renderPane();
@@ -1117,7 +1162,9 @@ describe('状态 hook', () => {
await result.current.packageAll();
});
expect(result.current.fieldError?.field).toBe('name');
expect(result.current.fieldErrors.map((error) => error.field)).toEqual([
'name',
]);
expect(invoke).not.toHaveBeenCalledWith(
'save_xhs_minitool_export_form',
expect.anything(),
@@ -1264,10 +1311,53 @@ describe('状态 hook', () => {
});
// 保存被界面校验拦下,红字留在表单上,但这条消息已经发出去了——拦住它只剩用户自己改。
expect(result.current.fieldError?.field).toBe('name');
expect(result.current.fieldErrors.map((error) => error.field)).toEqual([
'name',
]);
expect(submitTurn).toHaveBeenCalledTimes(1);
});
it('把字段红字原话交给 agent,并清掉红字让自动重读能采纳它的修正', async () => {
const invoke = vi.fn(async (command: string) => {
if (command === 'read_xhs_minitool_export') {
return { form: FORM, hasScript: true };
}
if (command === 'read_xhs_minitool_export_hash') return CONTENT_HASH;
return { outputTail: '', omittedCharacters: 0 };
});
installTauriRuntime({ core: { invoke } });
const submitTurn = vi.fn(async () => true);
const { result } = renderHook(() =>
useXhsMinitoolExport({
enabled: true,
projectPath: PROJECT_PATH,
submitTurn,
}),
);
await waitFor(() => expect(result.current.form).toEqual(FORM));
await act(async () => {
result.current.updateField('name', '这个名字已经超过十四个字了平台会拒');
});
await act(async () => {
await result.current.packageAll();
});
await waitFor(() => expect(result.current.fieldErrors).toHaveLength(1));
await act(async () => {
result.current.handFieldErrorsToAgent();
});
const [content] = submitTurn.mock.calls[0] as unknown as [
Array<{ type: string; text: string }>,
];
expect(content[0]?.type).toBe('input_text');
// 指令必须带上界面给用户看的那句红字,agent 才知道用户卡在哪。
expect(content[0]?.text).toContain('名称最多 14 个字');
// 红字清掉、`dirty` 归零:否则自动重读会一直不肯采纳 agent 写回的新值。
expect(result.current.fieldErrors).toEqual([]);
});
it('适配把指令当成一条用户消息交给同一个会话', async () => {
const submitTurn = vi.fn(async () => true);
installTauriRuntime({
@@ -1492,7 +1582,7 @@ function fakeMinitool(
return {
form: FORM,
hasScript: true,
fieldError: null,
fieldErrors: [],
conflict: null,
failure: null,
loading: false,
@@ -1506,6 +1596,7 @@ function fakeMinitool(
adapt: vi.fn(),
repair: vi.fn(),
fillForm: vi.fn(async () => undefined),
handFieldErrorsToAgent: vi.fn(),
dismissAdaptPrompt: vi.fn(),
dismissFailure: vi.fn(),
...overrides,
@@ -1,5 +1,5 @@
/**
* 表单本地校验:必填、字数边界(按去空白后的码点数),以及「首个错误即返回」。
* 表单本地校验:必填、字数边界(按去空白后的码点数),以及「一次返回全部出错字段」。
*
* 宿主那侧不再校验表单,这份纯函数就是唯一的表单关;`xhsMinitoolExport.test.tsx` 只钉它接到
* `persist` 上的那一步,边界都在这里覆盖。
@@ -19,40 +19,55 @@ const VALID: XHSMiniToolExportForm = {
};
describe('validateXhsMinitoolForm', () => {
it('三个字段都合法时通过', () => {
expect(validateXhsMinitoolForm(VALID)).toBeNull();
it('三个字段都合法时返回空数组', () => {
expect(validateXhsMinitoolForm(VALID)).toEqual([]);
});
it('按去空白后的码点数判字数:正好到上限通过,多一个码点报错', () => {
const atLimit = '字'.repeat(XHS_MINITOOL_TEXT_MAX_CHARS);
expect(validateXhsMinitoolForm({ ...VALID, name: atLimit })).toBeNull();
expect(validateXhsMinitoolForm({ ...VALID, name: atLimit })).toEqual([]);
const over = `${atLimit}字`;
const error = validateXhsMinitoolForm({ ...VALID, name: over });
expect(error?.field).toBe('name');
expect(error?.reason).toContain(`${XHS_MINITOOL_TEXT_MAX_CHARS}`);
const errors = validateXhsMinitoolForm({ ...VALID, name: over });
expect(errors).toHaveLength(1);
expect(errors[0]?.field).toBe('name');
expect(errors[0]?.reason).toContain(`${XHS_MINITOOL_TEXT_MAX_CHARS}`);
});
it('一个 emoji 也算一个字:`String.length` 数的是码元,这里按码点算', () => {
const emojis = '🙂'.repeat(XHS_MINITOOL_TEXT_MAX_CHARS);
expect(validateXhsMinitoolForm({ ...VALID, name: emojis })).toBeNull();
expect(
validateXhsMinitoolForm({ ...VALID, name: `${emojis}🙂` })?.field,
).toBe('name');
expect(validateXhsMinitoolForm({ ...VALID, name: emojis })).toEqual([]);
expect(validateXhsMinitoolForm({ ...VALID, name: `${emojis}🙂` })).toEqual([
expect.objectContaining({ field: 'name' }),
]);
});
it('只挡首个错误:先报名称,名称修好后才轮到简介', () => {
const empty = { name: ' ', introduction: ' ', iconPath: '' };
expect(validateXhsMinitoolForm(empty)?.field).toBe('name');
expect(validateXhsMinitoolForm({ ...empty, name: '有名字了' })?.field).toBe(
it('一次返回全部出错字段,顺序与表单一致', () => {
const errors = validateXhsMinitoolForm({
name: ' ',
introduction: ' ',
iconPath: '',
});
expect(errors.map((error) => error.field)).toEqual([
'name',
'introduction',
);
'iconPath',
]);
});
it('只报没填的那几项:修好一个就少一条,不必等前一个通过才看得到后一个', () => {
const empty = { name: ' ', introduction: ' ', iconPath: '' };
expect(
validateXhsMinitoolForm({ ...empty, name: '有名字了' }).map(
(error) => error.field,
),
).toEqual(['introduction', 'iconPath']);
expect(
validateXhsMinitoolForm({
...empty,
name: '有名字了',
introduction: '有简介了',
})?.field,
).toBe('iconPath');
}).map((error) => error.field),
).toEqual(['iconPath']);
});
});