Files
flow-center-front/tests/assign-preview.test.ts
T
chenxing c93313cdb7
build / build (push) Successful in 1m28s
feat(flow): 流程图入口/失败提示/指派预览/线级停发闭环/配置分层/选人宿主
- 修复办理页「查看流程图」双 hash URL:改 router.resolve + toAppHashUrl 单 hash
- 修复管理端办件抽屉「查看大图」域名尾点(origin + BASE_URL("./") 拼接)
- 新增 flowError 口径层与办理页内联失败条,审批失败提示可读、不泄漏堆栈
- 审批节点抽屉新增「预览候选人」真入口 + AssigneePreviewModal(只读,不写回定义)
- 线级停发/恢复失败改为单条错误面,避免同错双 toast
- 审批抽屉核心区/高级设置分层;插入菜单常用/高级分组
- 新增 FlowAssignEmbed 选人 iframe 父侧宿主(origin/source/信封/context 四校验)
- 修复 /diagram、/assign-preview 面包屑误显示「流程编辑器」
2026-10-28 03:18:54 +08:00

163 lines
6.9 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveNodePreview } from "../src/views/flow/admin/def-editor/assignPreview";
import type { PreviewApi, PreviewDept, PreviewNodeConfig } from "../src/views/flow/admin/def-editor/assignPreview";
/**
* 编辑器指派预览解析单测:断言的是「用什么接口、给什么结论、失败怎么讲」。
* 数据用桩,不代表真实科室数据与线上服务已验证。
*/
const depts: PreviewDept[] = [
{ deptKey: "41", deptName: "校长室", count: 2 },
{ deptKey: "42", deptName: "空科室", count: 0 },
];
function cfg(over: Partial<PreviewNodeConfig> = {}): PreviewNodeConfig {
return {
uid: "u3",
name: "科室审核",
type: "DEPT_KEY",
deptKey: "41",
userIds: [],
countersign: "ANY",
assignPolicy: null,
...over,
};
}
function stubApi(over: Partial<PreviewApi> = {}): PreviewApi & { calls: string[] } {
const calls: string[] = [];
return {
calls,
deptMembers: async (deptId) => {
calls.push(`deptMembers:${deptId}`);
if (deptId === "41") {
return [
{ userId: 101, userName: "陈星", status: 0 },
{ userId: 102, userName: "刘文娟", status: 0 },
{ userId: 103, userName: "测试教师10", status: 1 },
];
}
return [];
},
lookupUser: async (userId) => {
calls.push(`lookupUser:${userId}`);
if (userId === 101) return { id: 101, name: "陈星", status: 1 };
if (userId === 999) return null;
return { id: userId, name: `用户${userId}`, status: 0 };
},
applyPreview: async (flowGuid) => {
calls.push(`applyPreview:${flowGuid}`);
return { flowGuid, defVersion: 3, nodes: [] };
},
...over,
};
}
test("科室映射节点:读既有科室成员接口,启用成员进候选、停用成员标注不计入", async () => {
const api = stubApi();
const out = await resolveNodePreview(cfg(), depts, api);
assert.equal(out.ok, true);
assert.ok(out.headline.includes("校长室"));
assert.ok(out.headline.includes("2 名启用办理人"));
assert.deepEqual(api.calls.filter((c) => c.startsWith("deptMembers")), ["deptMembers:41"]);
assert.equal(out.rows.length, 3);
assert.deepEqual(out.rows.map((r) => r.userName), ["陈星", "刘文娟", "测试教师10"]);
assert.equal(out.rows[2].warn, "该成员已停用,不计入候选");
assert.ok(out.notes.some((n) => n.includes("预览只读")));
});
test("未选科室=明确的不可预览原因,而不是空列表里什么都不说", async () => {
const out = await resolveNodePreview(cfg({ deptKey: "" }), depts, stubApi());
assert.equal(out.ok, false);
assert.equal(out.blocked, "尚未选择科室映射,请先在「科室映射」里选一个科室。");
});
test("科室已被删除:点名到映射键并说明发布会被拦截", async () => {
const out = await resolveNodePreview(cfg({ deptKey: "99" }), depts, stubApi());
assert.equal(out.ok, false);
assert.ok(out.blocked?.includes("「99」"));
assert.ok(out.blocked?.includes("发布会被拦截"));
});
test("科室 0 名启用成员:给出补人路径,不误报成功", async () => {
const out = await resolveNodePreview(cfg({ deptKey: "42" }), depts, stubApi());
assert.equal(out.ok, false);
assert.ok(out.blocked?.includes("0 名启用办理人"));
assert.ok(out.blocked?.includes("办理人映射"));
});
test("指定用户节点:逐个走 UserBase 检索,禁用账号标注服务端会拒绝", async () => {
const api = stubApi();
const out = await resolveNodePreview(
cfg({ type: "USERS", deptKey: "", userIds: [101, 102, 999] }),
depts,
api,
);
assert.equal(out.ok, true);
assert.deepEqual(out.rows.map((r) => r.userName), ["陈星", "用户102", "#999"]);
assert.ok(out.rows[1].warn?.includes("非启用"));
assert.ok(out.rows[2].warn?.includes("UserBase 查不到"));
});
test("指定用户检索失败(服务不可达):原因可读,堆栈类原文不外泄", async () => {
const api = stubApi({
lookupUser: async () => {
throw new Error("java.lang.RuntimeException: connect timed out at com.yunhai.UserClient");
},
});
const out = await resolveNodePreview(cfg({ type: "USERS", deptKey: "", userIds: [101] }), depts, api);
assert.equal(out.ok, false);
assert.ok(out.blocked?.includes("发布会被拦截"));
assert.ok(!out.rows[0].warn?.includes("java."));
assert.ok(out.rows[0].warn?.includes("服务未能处理本次请求"));
});
test("新建未保存线:已发布版本对账说明「尚未保存」,不假装有一版可比", async () => {
const out = await resolveNodePreview(cfg(), depts, stubApi(), "");
assert.equal(out.published?.state, "unavailable");
assert.ok(out.published?.text.includes("尚未保存"));
});
test("草稿节点不在已发布预览中:区分「未开启指派」与「尚未发布」", async () => {
const out = await resolveNodePreview(cfg(), depts, stubApi(), "g1");
assert.equal(out.published?.state, "missing");
assert.ok(out.published?.text.includes("v3"));
assert.ok(out.published?.text.includes("未开启发起时指派"));
});
test("已发布版本有本节点:候选人按服务端解析结果展示(含提单人剔除后的集)", async () => {
const api = stubApi({
applyPreview: async () => ({
defVersion: 4,
nodes: [{ nodeId: "u3", nodeName: "科室审核", candidates: [{ userId: 102, userName: "刘文娟" }] }],
}),
});
const out = await resolveNodePreview(cfg(), depts, api, "g1");
assert.equal(out.published?.state, "ok");
assert.equal(out.published?.defVersion, 4);
assert.deepEqual(out.published?.rows.map((r) => r.userName), ["刘文娟"]);
});
test("已发布预览被服务端拒绝(如流程线已停发):把拒绝原因原样讲清,不静默", async () => {
const api = stubApi({
applyPreview: async () => {
throw new Error("流程线已停发,不可发起新办件");
},
});
const out = await resolveNodePreview(cfg(), depts, api, "g1");
assert.equal(out.published?.state, "unavailable");
assert.ok(out.published?.text.includes("流程线已停发,不可发起新办件"));
});
test("会签/发起时指派口径进说明文案(分层后信息不丢)", async () => {
const out = await resolveNodePreview(
cfg({ countersign: "ALL", assignPolicy: { mode: "SINGLE", required: true } }),
depts,
stubApi(),
);
assert.ok(out.notes.some((n) => n.includes("会签(全部同意才通过)")));
assert.ok(out.notes.some((n) => n.includes("单选 1 人") && n.includes("必选")));
});