/**
* 対応状況・備考の「編集モード」の下書き(ユーザー指示 2026-10-06。D-165)の期待値表。
*
* ★期待値表 → テスト → 実装の順(`CLAUDE.md` §6-2)★。表の番号(P1〜P8 / D1〜D5 / S1〜S3)は仕様の表と同じ。
*
* 行 R = { id: "1", resolution_status: "unresolved", note: null }
* 行 S = { id: "2", resolution_status: "in_progress", note: "abc" }
*/
import { describe, expect, it } from "vitest";
import {
draftNote,
draftStatus,
patchRequestFromDraft,
saveDrafts,
setDraftNote,
setDraftStatus,
type EditableRow,
type ResolutionDraft,
type ResolutionDrafts,
} from "@/lib/prechecks/edit-drafts";
import type { PrecheckStatusUpdateRequest } from "@/lib/types";
const R: EditableRow = { id: "1", resolution_status: "unresolved", note: null };
const S: EditableRow = { id: "2", resolution_status: "in_progress", note: "abc" };
const T: EditableRow = { id: "3", resolution_status: "resolved", note: "t" };
const EMPTY: ResolutionDrafts = new Map<string, ResolutionDraft>();
describe("patchRequestFromDraft(P1〜P8)", () => {
const cases: [string, EditableRow, ResolutionDraft | undefined, PrecheckStatusUpdateRequest | null][] = [
["P1 R・下書きなし → null", R, undefined, null],
["P2 R・{ status: unresolved } → null(同じ値)", R, { status: "unresolved" }, null],
["P3 R・{ status: resolved } → { status: resolved }(note キーなし)", R, { status: "resolved" }, { status: "resolved" }],
["P4 S・{ note: xyz } → { status: in_progress, note: xyz }", S, { note: "xyz" }, { status: "in_progress", note: "xyz" }],
["P5 S・{ note: abc } → null", S, { note: "abc" }, null],
[
"P6 R・{ status: in_progress, note: memo } → 両方",
R,
{ status: "in_progress", note: "memo" },
{ status: "in_progress", note: "memo" },
],
["P7 R・{ note: '' } → null(null と '' は同じ)", R, { note: "" }, null],
["P8 S・{ note: '' } → { status: in_progress, note: '' }(備考を消す)", S, { note: "" }, { status: "in_progress", note: "" }],
];
it.each(cases)("%s", (_label, row, draft, expected) => {
const actual = patchRequestFromDraft(row, draft);
expect(actual).toStrictEqual(expected);
if (actual && expected && !("note" in expected)) expect("note" in actual).toBe(false);
});
});
describe("setDraftStatus / setDraftNote / draftNote(D1〜D5)", () => {
it("D1 resolved にしてから unresolved に戻すと空の Map(エントリが消える)", () => {
const once = setDraftStatus(EMPTY, R, "resolved");
expect(once.get("1")).toStrictEqual({ status: "resolved" });
const back = setDraftStatus(once, R, "unresolved");
expect(back.size).toBe(0);
});
it("D2 備考 x → 対応状況 resolved で { note: x, status: resolved }", () => {
const d2 = setDraftStatus(setDraftNote(EMPTY, S, "x"), S, "resolved");
expect(d2.size).toBe(1);
expect(d2.get("2")).toStrictEqual({ note: "x", status: "resolved" });
});
it("D3 D2 の結果に備考を元の abc に戻すと { status: resolved } だけ残る", () => {
const d2 = setDraftStatus(setDraftNote(EMPTY, S, "x"), S, "resolved");
const d3 = setDraftNote(d2, S, "abc");
expect(d3.get("2")).toStrictEqual({ status: "resolved" });
});
it("D4 setDraft* は入力の Map を書き換えない", () => {
const input = new Map<string, ResolutionDraft>([["2", { note: "x" }]]);
const snapshot = structuredClone(input);
const a = setDraftStatus(input, S, "resolved");
const b = setDraftNote(input, S, "abc");
const c = setDraftNote(input, R, "memo");
expect(input).toStrictEqual(snapshot);
expect(input.get("2")).toStrictEqual({ note: "x" });
expect(a).not.toBe(input);
expect(b).not.toBe(input);
expect(c).not.toBe(input);
});
it("D5 draftNote(R, undefined) は ''", () => {
expect(draftNote(R, undefined)).toBe("");
});
it("(補足)draftStatus / draftNote は下書きがあればそれ、無ければ行の値", () => {
expect(draftStatus(S, undefined)).toBe("in_progress");
expect(draftStatus(S, { status: "resolved" })).toBe("resolved");
expect(draftNote(S, undefined)).toBe("abc");
expect(draftNote(S, { note: "" })).toBe("");
});
});
describe("saveDrafts(S1〜S3)", () => {
it("S1 R 変更あり・S 変更なし・T 変更あり、T の send が例外", async () => {
const drafts = new Map<string, ResolutionDraft>([
["1", { status: "resolved" }],
["3", { note: "changed" }],
]);
const calls: [string, PrecheckStatusUpdateRequest][] = [];
const tError = new Error("T failed");
const result = await saveDrafts([R, S, T], drafts, async (row, request) => {
calls.push([row.id, request]);
if (row.id === "3") throw tError;
return { ...row, resolution_status: request.status };
});
expect(calls).toStrictEqual([
["1", { status: "resolved" }],
["3", { status: "resolved", note: "changed" }],
]);
expect([...result.updated.keys()]).toStrictEqual(["1"]);
expect(result.updated.get("1")).toStrictEqual({ ...R, resolution_status: "resolved" });
expect([...result.failed.keys()]).toStrictEqual(["3"]);
expect(result.failed.get("3")).toBe(tError);
expect(result.remaining).toStrictEqual(new Map([["3", { note: "changed" }]]));
});
it("S1(補足)1行ずつ順に送る(前の send が終わるまで次を呼ばない)", async () => {
const drafts = new Map<string, ResolutionDraft>([
["1", { status: "resolved" }],
["3", { note: "changed" }],
]);
let inFlight = 0;
let maxInFlight = 0;
await saveDrafts([R, S, T], drafts, async (row) => {
inFlight += 1;
maxInFlight = Math.max(maxInFlight, inFlight);
await new Promise((resolve) => setTimeout(resolve, 1));
inFlight -= 1;
return row;
});
expect(maxInFlight).toBe(1);
});
it("S2 全部成功 → failed 空・remaining 空", async () => {
const drafts = new Map<string, ResolutionDraft>([
["1", { status: "resolved" }],
["2", { note: "x" }],
]);
const result = await saveDrafts([R, S, T], drafts, async (row) => row);
expect(result.updated.size).toBe(2);
expect(result.failed.size).toBe(0);
expect(result.remaining.size).toBe(0);
});
it("S3 onProgress は (1,2), (2,2) の順に呼ばれる(送る行の数で数える)", async () => {
const drafts = new Map<string, ResolutionDraft>([
["1", { status: "resolved" }],
["3", { note: "changed" }],
]);
const progress: [number, number][] = [];
await saveDrafts(
[R, S, T],
drafts,
async (row) => {
if (row.id === "3") throw new Error("x");
return row;
},
(done, total) => progress.push([done, total])
);
expect(progress).toStrictEqual([
[1, 2],
[2, 2],
]);
});
});