const test = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const os = require("os");
const fs = require("fs");
const {
parseChoicesText,
resolveSiteDir,
formatEdgeLabel,
getFilterStatusList,
buildEdges,
loadStatusLabels,
buildMermaid,
} = require("./generate-process-flowchart");
test("parseChoicesText: 値,ラベル,短縮ラベル,色 形式を 値→ラベル のMapへ変換する", () => {
const input = "100,新規,新規,new-status-white\n450,依頼者質疑中,質疑中,new-status-yellow";
const result = parseChoicesText(input);
assert.equal(result.get("100"), "新規");
assert.equal(result.get("450"), "依頼者質疑中");
assert.equal(result.size, 2);
});
test("parseChoicesText: undefinedやから文字列は空Mapを返す", () => {
assert.equal(parseChoicesText(undefined).size, 0);
assert.equal(parseChoicesText("").size, 0);
});
test("parseChoicesText: ラベル省略時は値をラベルとして使う", () => {
const result = parseChoicesText("999");
assert.equal(result.get("999"), "999");
});
test("resolveSiteDir: site-{id}_{name}形式のフォルダを前方一致で解決する", () => {
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-"));
fs.mkdirSync(path.join(tmpBase, "configs", "site-999_テストサイト"), { recursive: true });
const result = resolveSiteDir(tmpBase, "site-999");
assert.equal(result, path.join(tmpBase, "configs", "site-999_テストサイト"));
});
test("formatEdgeLabel: Depts・Notifications無しはプロセス名のみ", () => {
assert.equal(formatEdgeLabel({ Id: 2, DisplayName: "回答送信" }), "回答送信");
});
test("formatEdgeLabel: Depts指定時は (Dept番号) を付記する", () => {
assert.equal(formatEdgeLabel({ Id: 3, DisplayName: "営業積算確認", Depts: [25] }), "営業積算確認 (Dept25)");
});
test("formatEdgeLabel: Notifications指定時は先頭に📧を付ける", () => {
const process = { Id: 6, DisplayName: "遅延連絡", Depts: [25], Notifications: [{ Type: 1 }] };
assert.equal(formatEdgeLabel(process), "📧遅延連絡 (Dept25)");
});
test("formatEdgeLabel: DisplayName省略時はNameを使う", () => {
assert.equal(formatEdgeLabel({ Id: 1, Name: "質疑送信" }), "質疑送信");
});
test("getFilterStatusList: View.ColumnFilterHash.Statusを文字列配列で返す", () => {
const process = { View: { ColumnFilterHash: { Status: '["100","400"]' } } };
assert.deepEqual(getFilterStatusList(process), ["100", "400"]);
});
test("getFilterStatusList: Status指定が無ければnullを返す", () => {
assert.equal(getFilterStatusList({ View: { ColumnFilterHash: { ClassC: '["Own"]' } } }), null);
assert.equal(getFilterStatusList({}), null);
});
test("buildEdges: 通常遷移(CurrentStatus≠-1, ChangedStatus≠-1)", () => {
const processes = [{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500, Depts: [25] }];
const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes);
assert.deepEqual(edgeLines, [' 450 -->|"営業積算確認 (Dept25)"| 500']);
assert.deepEqual([...statusValues].sort(), ["450", "500"]);
assert.equal(needsAnyNode, false);
});
test("buildEdges: 任意状態からの遷移・ColumnFilterHash.Statusあり", () => {
const processes = [
{
Id: 1,
DisplayName: "質疑送信",
CurrentStatus: -1,
ChangedStatus: 450,
View: { ColumnFilterHash: { Status: '["100","400"]' } },
},
];
const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes);
assert.deepEqual(edgeLines, [' 100 -->|"質疑送信"| 450', ' 400 -->|"質疑送信"| 450']);
assert.deepEqual([...statusValues].sort(), ["100", "400", "450"]);
assert.equal(needsAnyNode, false);
});
test("buildEdges: 任意状態からの遷移・ColumnFilterHash無しはanyノード起点", () => {
const processes = [{ Id: 9, DisplayName: "サンプル", CurrentStatus: -1, ChangedStatus: 500 }];
const { edgeLines, needsAnyNode } = buildEdges(processes);
assert.deepEqual(edgeLines, [' any -->|"サンプル"| 500']);
assert.equal(needsAnyNode, true);
});
test("buildEdges: 状態維持アクション(ChangedStatus=-1)は自己ループ", () => {
const processes = [{ Id: 4, DisplayName: "質疑回答削除", CurrentStatus: 450, ChangedStatus: -1, Depts: [25] }];
const { edgeLines, statusValues } = buildEdges(processes);
assert.deepEqual(edgeLines, [' 450 -->|"質疑回答削除 (Dept25)"| 450']);
assert.deepEqual([...statusValues], ["450"]);
});
test("buildEdges: 状態変化なし・ColumnFilterHash.Statusありは各値へ自己ループ", () => {
const processes = [
{
Id: 7,
DisplayName: "見積回答",
CurrentStatus: -1,
ChangedStatus: -1,
Depts: [25],
Notifications: [{ Type: 1 }],
View: { ColumnFilterHash: { Status: '["800","900","901"]' } },
},
];
const { edgeLines, needsAnyNode } = buildEdges(processes);
assert.deepEqual(edgeLines, [
' 800 -->|"📧見積回答 (Dept25)"| 800',
' 900 -->|"📧見積回答 (Dept25)"| 900',
' 901 -->|"📧見積回答 (Dept25)"| 901',
]);
assert.equal(needsAnyNode, false);
});
test("buildEdges: 状態変化なし・ColumnFilterHash無しはanyノードへ自己ループ", () => {
const processes = [
{ Id: 6, DisplayName: "遅延連絡", CurrentStatus: -1, ChangedStatus: -1, Depts: [25], Notifications: [{ Type: 1 }] },
];
const { edgeLines, needsAnyNode } = buildEdges(processes);
assert.deepEqual(edgeLines, [' any -->|"📧遅延連絡 (Dept25)"| any']);
assert.equal(needsAnyNode, true);
});
test("loadStatusLabels: latest.jsonのStatus列ChoicesTextを解決する", () => {
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-"));
fs.mkdirSync(path.join(tmpBase, "siteSettingJsons"), { recursive: true });
const fakeLatest = {
Response: {
Data: {
SiteSettings: {
Columns: [
{ ColumnName: "ClassA", LabelText: "親番" },
{ ColumnName: "Status", ChoicesText: "100,新規,新規,white\n450,依頼者質疑中,質疑中,yellow" },
],
},
},
},
};
fs.writeFileSync(
path.join(tmpBase, "siteSettingJsons", "site-999_latest.json"),
JSON.stringify(fakeLatest),
"utf-8"
);
const result = loadStatusLabels(tmpBase, "999");
assert.equal(result.get("100"), "新規");
assert.equal(result.get("450"), "依頼者質疑中");
});
test("loadStatusLabels: latest.jsonが無ければ空Mapを返す", () => {
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-"));
const result = loadStatusLabels(tmpBase, "000");
assert.equal(result.size, 0);
});
test("buildMermaid: ノード定義とエッジ定義を含む完成文字列を返す", () => {
const processes = [{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500, Depts: [25] }];
const statusLabels = new Map([
["450", "依頼者質疑中"],
["500", "積算対応中"],
]);
const result = buildMermaid(processes, statusLabels);
assert.match(result, /^```mermaid\nflowchart LR\n/);
assert.match(result, /450\(\(依頼者質疑中
450\)\)/);
assert.match(result, /500\(\(積算対応中
500\)\)/);
assert.match(result, /450 -->\|"営業積算確認 \(Dept25\)"\| 500/);
assert.match(result, /```$/);
});
test("buildMermaid: statusLabelsに無い値は値のみのラベルにフォールバックする", () => {
const processes = [{ Id: 3, DisplayName: "テスト", CurrentStatus: 100, ChangedStatus: 200 }];
const result = buildMermaid(processes, new Map());
assert.match(result, /100\(\(100
100\)\)/);
assert.match(result, /200\(\(200
200\)\)/);
});
test("buildMermaid: anyノードが必要な場合のみ ((任意)) を含める", () => {
const withAny = buildMermaid([{ Id: 6, DisplayName: "遅延連絡", CurrentStatus: -1, ChangedStatus: -1 }], new Map());
assert.match(withAny, /any\(\(任意\)\)/);
const withoutAny = buildMermaid(
[{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500 }],
new Map()
);
assert.doesNotMatch(withoutAny, /any\(\(任意\)\)/);
});