Files
flow-center-front/tests/convert-roundtrip.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

146 lines
7.3 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.
import { test } from "node:test";
import assert from "node:assert/strict";
import { toContentJson, fromContentJson } from "../src/views/flow/admin/def-editor/convert";
import { createApproval, createCondition, createNotify, createParallel } from "../src/views/flow/admin/def-editor/model";
import type { EditorItem, EditorModel } from "../src/views/flow/admin/def-editor/model";
/**
* 流程定义序列化回归闸门(任务书 §6.8「检查流程定义序列化」)。
* 本轮只动编辑器面板的**展示分层**,这里钉住:结构语义不因这些改动漂移。
* 对比口径按语义集合比(节点字段集 + 边三元组集),不比数组顺序——空泳道会产生纯顺序差。
*/
type Json = ReturnType<typeof toContentJson>;
function model(items: EditorItem[]): EditorModel {
return {
code: "rt-demo",
name: "往返回归",
list: items,
settings: { rejectPolicy: "END_REJECTED", urge: { enabled: true, intervalHours: 24, maxTimes: 3 } },
};
}
const clone = (o: unknown) => JSON.parse(JSON.stringify(o));
function fullItems(): EditorItem[] {
const approval = createApproval("科室审核", { type: "DEPT_KEY", deptKey: "41" });
approval.countersign = "ALL";
approval.deadlineHours = 8;
approval.assignPolicy = { mode: "SINGLE", required: true };
const outletNode = createApproval("多出口", { type: "USERS", userIds: [101, 102] });
outletNode.outlets = ["同意", "退回", "加签"];
outletNode.branches = [[createApproval("分支一")], [], [createNotify("分支三抄送")]];
const cond = createCondition();
cond.judgeMode = "EXPRESSION";
cond.outlets = ["大额", "小额"];
cond.conditions = ["amount > 1000", ""];
cond.branches = [[createApproval("大额复核")], [createApproval("小额直过")]];
const par = createParallel();
par.branches = [[createNotify("并行抄送A")], [createApproval("并行复核B", { type: "DEPT_KEY", deptKey: "42" })]];
return [approval, cond, par, outletNode];
}
/** 已知缺陷用:非首位并行容器重解析后 split/join 会换成新 id(见最后一条用例) */
function parallelDerivedIds(json: Json): string[] {
return json.nodes.filter((n) => n.type === "PARALLEL_SPLIT" || n.type === "PARALLEL_JOIN").map((n) => n.id);
}
function roundTrip(times: number, json: Json): Json {
let cur = json;
for (let i = 0; i < times; i++) {
cur = toContentJson({ ...model([]), ...fromContentJson(clone(cur)) } as EditorModel);
}
return cur;
}
test("结构往返两轮:每个节点的配置语义逐项一致(并行派生 id 另用例专门记录)", () => {
const first = toContentJson(model(fullItems()));
const second = roundTrip(1, first);
const third = roundTrip(2, first);
const byId = (json: Json) => {
const m = new Map<string, string>();
json.nodes.forEach((n) => m.set(n.id, JSON.stringify(n)));
return m;
};
const a = byId(first);
const b = byId(second);
const c = byId(third);
// 配置类节点(审批/抄送/条件)的 id 与内容必须逐字不变
["u1", "u2", "u3", "u4", "u5c", "u8", "u9", "start", "end"].forEach((id) => {
assert.equal(b.get(id), a.get(id), `节点 ${id} 内容在第二轮发生变化`);
assert.equal(c.get(id), a.get(id), `节点 ${id} 内容在第三轮发生变化`);
});
assert.deepEqual(second.settings, first.settings);
assert.deepEqual(third.settings, first.settings);
// 并行分支内的两名办理人同样不变
const branchIds = ["u13", "u14"];
branchIds.forEach((id) => assert.equal(b.get(id), a.get(id), `并行分支节点 ${id} 变化`));
// 出口边(含条件表达式)在两轮都存在
const edgeSet = (json: Json) => new Set(json.edges.map((e) => `${e.from}->${e.to}${e.name ? `(${e.name})` : ""}${e.condition ? `[${e.condition}]` : ""}`));
const ea = edgeSet(first);
const eb = edgeSet(second);
["u2->u3(同意)", "u2->u4(加签)", "u2->end(退回)", "u5c->u8(大额)[amount > 1000]", "u5c->u9(小额)"].forEach((e) => {
assert.ok(ea.has(e), `第一轮缺少边 ${e}`);
assert.ok(eb.has(e), `第二轮缺少边 ${e}`);
});
});
test("关键语义字段全部活过往返,不被展示层分层影响", () => {
const first = toContentJson(model(fullItems()));
const parsed = fromContentJson(clone(first));
const second = toContentJson({ ...model([]), ...parsed } as EditorModel);
// 会签 ALL 是后端缺省值,生成 JSON 时按设计省略(model.ts 注释);口径以解析回编辑态为准
const approvalItem = parsed.list.find((it) => it.kind === "approval" && it.assignee?.type === "DEPT_KEY");
assert.ok(approvalItem && approvalItem.kind === "approval");
assert.equal(approvalItem.countersign, "ALL", "会签语义在解析端丢失");
assert.equal(approvalItem.deadlineHours, 8, "办理时限丢失");
assert.deepEqual(approvalItem.assignPolicy, { mode: "SINGLE", required: true }, "发起时指派策略丢失");
assert.ok(second.nodes.some((n) => n.type === "NOTIFY" && n.receivers?.type === "USERS"), "抄送对象丢失");
assert.ok(second.nodes.some((n) => n.judgeMode === "EXPRESSION"), "条件判断方式丢失");
assert.ok(second.nodes.some((n) => n.type === "PARALLEL_SPLIT"), "并行拆分丢失");
assert.ok(second.nodes.some((n) => n.type === "PARALLEL_JOIN" && n.waitFor === "ALL"), "并行汇聚语义丢失");
assert.ok(second.nodes.some((n) => n.id === "start") && second.nodes.some((n) => n.id === "end"), "端点丢失");
assert.ok(second.edges.filter((e) => e.name).length >= 3, "多出口/条件分支的出口名丢失");
});
test("设置与端点名不被丢:rejectPolicy/urge/start-end name 原样回写", () => {
const m = model([createApproval("单节点")]);
m.startName = "提交申请";
m.endName = "办结";
const json = toContentJson(m);
assert.equal(json.settings.rejectPolicy, "END_REJECTED");
assert.equal(json.settings.urge.intervalHours, 24);
assert.equal((json.nodes.find((n) => n.type === "START") || {}).name, "提交申请");
assert.equal((json.nodes.find((n) => n.type === "END") || {}).name, "办结");
});
test("已知缺陷(本轮不修,修它等于改序列化契约):并行容器重解析后 split/join 换成新 id", () => {
const first = toContentJson(model([createApproval("a"), createParallel()]));
const before = parallelDerivedIds(first);
const rt = roundTrip(1, first);
const after = parallelDerivedIds(rt);
assert.equal(after.length, before.length, "并行拆/合节点数量发生变化");
assert.notDeepEqual(after, before, "并行 id 改名行为已变化——若已被修好,请把本用例改成断言 id 稳定并同步终审");
// 改名只涉及 id 字面,汇聚语义不变
const join = rt.nodes.find((n) => n.type === "PARALLEL_JOIN");
assert.equal(rt.nodes.filter((n) => n.type === "PARALLEL_SPLIT").length, 1);
assert.equal(join?.waitFor, "ALL", "并行汇聚 waitFor 语义丢失");
// 拆分/汇聚与分支节点的连接关系仍然闭合(每个分支节点都连到某条 split/join)
const anchors = new Set(after);
const linked = rt.edges.filter((e) => anchors.has(e.from) || anchors.has(e.to));
assert.ok(linked.length >= 4, `并行连线丢失,只剩 ${linked.length} 条`);
});