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 { return { uid: "u3", name: "科室审核", type: "DEPT_KEY", deptKey: "41", userIds: [], countersign: "ANY", assignPolicy: null, ...over, }; } function stubApi(over: Partial = {}): 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("必选"))); });