build / build (push) Successful in 1m28s
- 修复办理页「查看流程图」双 hash URL:改 router.resolve + toAppHashUrl 单 hash
- 修复管理端办件抽屉「查看大图」域名尾点(origin + BASE_URL("./") 拼接)
- 新增 flowError 口径层与办理页内联失败条,审批失败提示可读、不泄漏堆栈
- 审批节点抽屉新增「预览候选人」真入口 + AssigneePreviewModal(只读,不写回定义)
- 线级停发/恢复失败改为单条错误面,避免同错双 toast
- 审批抽屉核心区/高级设置分层;插入菜单常用/高级分组
- 新增 FlowAssignEmbed 选人 iframe 父侧宿主(origin/source/信封/context 四校验)
- 修复 /diagram、/assign-preview 面包屑误显示「流程编辑器」
146 lines
7.3 KiB
TypeScript
146 lines
7.3 KiB
TypeScript
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} 条`);
|
||
});
|