/**
* `formatCleanTaskStatus()` の期待値表(指示書AS §4 表3。★期待値表を先に確定させてから実装する★)。
*
* ★★2026-10-02: `status` の値の意味を受領した★★(TeamWorks 第10回 N-2。D-155 項2)—
* それまでは定義書に値の説明が無く、★生値をそのまま出していた★(指示書AQ §1-2。D-077)。
* ★意味を受領したので表示名にする★。★日本語は N-2 の原文から1文字も変えない★。
* ★表に無い値(7・255 など)は「不明(7)」★(★6 は 3 と同じ「清掃キャンセル」。どちらもユーザー指示 2026-10-30★)(「その他」「不明」に潰さない。
* D-089 項3 / D-104 項2)。★英語は仮★(ユーザーが後でまとめて見直す)。
*
* ### 表3 — 清掃結果の表示名(指示書AS §4。出所: 第10回 N-2 の原文)
*
* | 入力(`task_status`) | 日本語(画面・CSV) | 英語(画面。★仮★) |
* |---|---|---|
* | `null`(レポートの行が無い) | 出発失敗 | Launch failed |
* | 0 | 清掃開始前 | Not started |
* | 1 | 清掃中 | Cleaning |
* | 2 | 清掃中断 | Interrupted |
* | 3 | 清掃キャンセル | Cancelled |
* | 4 | 清掃正常終了 | Completed |
* | 5 | 清掃異常終了 | Ended abnormally |
* | 6(★N-2 に無い。ユーザー指示 2026-10-30★) | 清掃キャンセル(3 と同じ) | Cancelled |
* | 7(表に無い値) | 不明(7) | Unknown (7) |
* | 255 | 不明(255) | Unknown (255) |
*
* ★★`CLEAN_ERROR`(と旧 `CLEAN_ERROR_LABELS`。2026-10-19 削除)をここから引かないこと★★ —
* `clean_error`(`clean_status_histories` 側)と `status`(`clean_task_reports` 側)は
* ★別のカラムの別のコード表★。互いに読み替えない。
*/
import { describe, expect, it } from "vitest";
import {
CLEAN_LOG_UNSCHEDULED_LABEL,
CLEAN_RESULT_NOT_DEPARTED_LABEL,
CLEAN_RESULT_PENDING_LABEL,
cleanLogRowView,
formatCleanTaskStatus,
} from "@/lib/format/clean-result";
import { CLEAN_TASK_STATUS_UNKNOWN_TEMPLATE } from "@/lib/config";
import { translate } from "@/lib/i18n";
describe("formatCleanTaskStatus — 表3(指示書AS §4)", () => {
it.each<[number | null, string, string]>([
[null, "出発失敗", "Launch failed"],
[0, "清掃開始前", "Not started"],
[1, "清掃中", "Cleaning"],
[2, "清掃中断", "Interrupted"],
[3, "清掃キャンセル", "Cancelled"],
[4, "清掃正常終了", "Completed"],
[5, "清掃異常終了", "Ended abnormally"],
// ★6 は 3 と同じ「清掃キャンセル」★(ユーザー指示 2026-10-30)
[6, "清掃キャンセル", "Cancelled"],
])("task_status=%s → 日本語「%s」/ 英語「%s」", (status, ja, en) => {
expect(formatCleanTaskStatus(status)).toBe(ja);
expect(translate("en", formatCleanTaskStatus(status))).toBe(en);
});
// ★表に無い値は「不明(7)」★(ユーザー指示 2026-10-30「7以上は不明(7)となるようにしてください」)。
// 英語は画面が `t("不明({id})")` の `{id}` を置き換えて "Unknown (7)"(`cleanLogRowView().unknownStatus`)
it.each<[number, string, string]>([
[7, "不明(7)", "Unknown (7)"],
[255, "不明(255)", "Unknown (255)"],
[-1, "不明(-1)", "Unknown (-1)"],
])("task_status=%s(表に無い値)→ 日本語「%s」/ 英語「%s」", (status, ja, en) => {
expect(formatCleanTaskStatus(status)).toBe(ja);
const view = cleanLogRowView({
departed: true,
task_status: status,
completion_rate: 50,
grade: "△",
result_pending: false,
unscheduled: false,
});
expect(view.resultLabel).toBe(ja);
expect(view.unknownStatus).toBe(status);
expect(translate("en", CLEAN_TASK_STATUS_UNKNOWN_TEMPLATE).replace("{id}", String(view.unknownStatus))).toBe(en);
});
it("表に載っている値(0〜6)は unknownStatus が null(訳は表示名から引く)", () => {
for (const status of [0, 1, 2, 3, 4, 5, 6]) {
const view = cleanLogRowView({
departed: true,
task_status: status,
completion_rate: 50,
grade: "△",
result_pending: false,
unscheduled: false,
});
expect(view.unknownStatus).toBeNull();
}
});
it("null(レポート行が無い)は CLEAN_RESULT_NOT_DEPARTED_LABEL(従来どおり)", () => {
expect(formatCleanTaskStatus(null)).toBe(CLEAN_RESULT_NOT_DEPARTED_LABEL);
});
it("status=0 は「出発失敗」にならない(★`??` と `||` の取り違え防止★)", () => {
// `status || ...` と書くと 0 が「出発失敗」に化ける。例外も tsc エラーも出ない
expect(formatCleanTaskStatus(0)).not.toBe(CLEAN_RESULT_NOT_DEPARTED_LABEL);
});
// ★D-089 項3 / D-104 項2: 表に無い値を「その他」や数字なしの「不明」に潰さない(生値を必ず残す)★
it("表に無い値(7 / 255 / -1)の表示に「その他」が出ず、生値の数字が残る", () => {
for (const status of [7, 255, -1]) {
expect(formatCleanTaskStatus(status)).not.toContain("その他");
expect(formatCleanTaskStatus(status)).toContain(String(status));
}
});
it("6 と 3 は同じ「清掃キャンセル」(ユーザー指示 2026-10-30)", () => {
expect(formatCleanTaskStatus(6)).toBe(formatCleanTaskStatus(3));
});
it("6 以外は全値が互いに異なる表示になる(1つに潰していないこと)", () => {
const statuses = [null, 0, 1, 2, 3, 4, 5, 7, 255];
const labels = statuses.map((s) => formatCleanTaskStatus(s));
expect(new Set(labels).size).toBe(statuses.length);
});
it("代替文言そのものが空でないこと(実装が壊れて空文字を返す事故を検知する)", () => {
expect(CLEAN_RESULT_NOT_DEPARTED_LABEL.length).toBeGreaterThan(0);
});
});
/**
* ★結果待ち(D-173)の行の見え方★(ユーザー判断 2026-10-07。表2 C2)
*
* 清掃詳細ログの表(`CleanLogTable.tsx`。ダッシュボードの縮小表示も同じ)が使う `cleanLogRowView()`。
* ★結果待ち(R)は `departed: false` / `task_status: null` / `completion_rate: null` / `grade: "×"` のまま★
* (不変条件を壊さない)なので、★旗(`result_pending`)を見ないと出発失敗(F)と区別できない★。
*
* | # | 行 | 出発 | 清掃結果 | 完了率 | 判定バッジ | 行を開ける |
* |------|----|-----------|----------------|--------|------------|------------|
* | C2-1 | R | pending(…) | 結果待ち | null(-) | 出さない(-) | ★開けない★ |
* | C2-2 | F | ng(✗) | 出発失敗 | null(-) | 出す(×) | 開ける |
* | C2-3 | S | ok(✓) | 清掃正常終了 | 95.2 | 出す(◎) | 開けない |
* | C2-4 | △ | ok(✓) | 清掃中断 | 45 | 出す(△) | 開ける |
*/
describe("cleanLogRowView — 結果待ち(表2 C2。D-173)", () => {
const F = {
departed: false,
task_status: null,
completion_rate: null,
grade: "×",
result_pending: false,
unscheduled: false,
} as const;
const R = { ...F, result_pending: true } as const;
const S = {
departed: true,
task_status: 4,
completion_rate: 95.2,
grade: "◎",
result_pending: false,
unscheduled: false,
} as const;
const W = { ...S, task_status: 2, completion_rate: 45, grade: "△" } as const;
it("C2-1: 結果待ちは 出発 pending・清掃結果「結果待ち」・完了率 null・バッジなし・開けない", () => {
expect(cleanLogRowView(R)).toEqual({
departed: "pending",
resultLabel: "結果待ち",
unknownStatus: null,
completionRate: null,
gradeShown: false,
expandable: false,
areaDetail: false,
});
expect(CLEAN_RESULT_PENDING_LABEL).toBe("結果待ち");
});
it("C2-2: 確定した出発失敗は今までどおり(✗・出発失敗・× を出す・開ける)", () => {
expect(cleanLogRowView(F)).toEqual({
departed: "ng",
resultLabel: "出発失敗",
unknownStatus: null,
completionRate: null,
gradeShown: true,
expandable: true,
areaDetail: true,
});
});
it("C2-3: ◎ は ✓・表示名・完了率そのまま・バッジあり・開けない", () => {
expect(cleanLogRowView(S)).toEqual({
departed: "ok",
resultLabel: "清掃正常終了",
unknownStatus: null,
completionRate: 95.2,
gradeShown: true,
expandable: false,
areaDetail: false,
});
});
it("予定なしの行は △× なら開ける(アドバイスあり)。エリア別詳細は出さない", () => {
expect(cleanLogRowView({ ...W, unscheduled: true })).toEqual({
departed: "ok",
resultLabel: "清掃中断",
unknownStatus: null,
completionRate: 45,
gradeShown: true,
expandable: true,
areaDetail: false,
});
expect(CLEAN_LOG_UNSCHEDULED_LABEL).toBe("予定なし"); // ユーザー指示 2026-11-04(旧: 「予定外」)
expect(translate("en", CLEAN_LOG_UNSCHEDULED_LABEL)).toBe("No schedule");
});
it("V-3: 予定なし・×(清掃キャンセル)は開ける。エリア別詳細は出さない", () => {
expect(
cleanLogRowView({
...S,
task_status: 6,
completion_rate: 0,
grade: "×",
unscheduled: true,
})
).toEqual({
departed: "ok",
resultLabel: "清掃キャンセル",
unknownStatus: null,
completionRate: 0,
gradeShown: true,
expandable: true,
areaDetail: false,
});
});
it("V-5: 予定なし・◎ は開けない", () => {
expect(cleanLogRowView({ ...S, unscheduled: true })).toEqual({
departed: "ok",
resultLabel: "清掃正常終了",
unknownStatus: null,
completionRate: 95.2,
gradeShown: true,
expandable: false,
areaDetail: false,
});
});
it("C2-4: △ は開ける(アドバイスあり)", () => {
expect(cleanLogRowView(W)).toEqual({
departed: "ok",
resultLabel: "清掃中断",
unknownStatus: null,
completionRate: 45,
gradeShown: true,
expandable: true,
areaDetail: true,
});
});
it("「結果待ち」「レポートの到着待ち」は英語に訳される(ダッシュボードのカードも同じキー)", () => {
expect(translate("en", CLEAN_RESULT_PENDING_LABEL)).toBe("Pending");
expect(translate("en", "レポートの到着待ち")).toBe("Waiting for report");
});
});