Newer
Older
client_dwatch / lib / format / __tests__ / clean-result.test.ts
/**
 * `formatCleanTaskStatus()` の期待値表(指示書AS §4 表3。★期待値表を先に確定させてから実装する★)。
 *
 * ★★2026-10-02: `status` の値の意味を受領した★★(TeamWorks 第10回 N-2。D-155 項2)—
 *   それまでは定義書に値の説明が無く、★生値をそのまま出していた★(指示書AQ §1-2。D-077)。
 *   ★意味を受領したので表示名にする★。★日本語は N-2 の原文から1文字も変えない★。
 *   ★表に無い値(CSV実測の 6・255 など)は生値の数字のまま★(「その他」「不明」に潰さない。
 *   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(CSV実測にある・意味は未受領) | `6` | `6` |
 * | 255 | `255` | `255` |
 *
 * ★★`CLEAN_ERROR` / `CLEAN_ERROR_LABELS` をここから引かないこと★★ —
 *   `clean_error`(`clean_status_histories` 側)と `status`(`clean_task_reports` 側)は
 *   ★別のカラムの別のコード表★。互いに読み替えない。
 */
import { describe, expect, it } from "vitest";
import {
  CLEAN_RESULT_NOT_DEPARTED_LABEL,
  CLEAN_RESULT_PENDING_LABEL,
  cleanLogRowView,
  formatCleanTaskStatus,
} from "@/lib/format/clean-result";
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, "6", "6"],
    [255, "255", "255"],
  ])("task_status=%s → 日本語「%s」/ 英語「%s」", (status, ja, en) => {
    expect(formatCleanTaskStatus(status)).toBe(ja);
    expect(translate("en", formatCleanTaskStatus(status))).toBe(en);
  });

  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.each(["その他", "不明"])("表に無い値(6 / 255 / -1)の表示に「%s」が出てこない", (word) => {
    for (const status of [6, 255, -1]) {
      expect(formatCleanTaskStatus(status)).not.toContain(word);
    }
  });

  it("全値が互いに異なる表示になる(1つに潰していないこと)", () => {
    const statuses = [null, 0, 1, 2, 3, 4, 5, 6, 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,
  } 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,
  } 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: "結果待ち",
      completionRate: null,
      gradeShown: false,
      expandable: false,
    });
    expect(CLEAN_RESULT_PENDING_LABEL).toBe("結果待ち");
  });

  it("C2-2: 確定した出発失敗は今までどおり(✗・出発失敗・× を出す・開ける)", () => {
    expect(cleanLogRowView(F)).toEqual({
      departed: "ng",
      resultLabel: "出発失敗",
      completionRate: null,
      gradeShown: true,
      expandable: true,
    });
  });

  it("C2-3: ◎ は ✓・表示名・完了率そのまま・バッジあり・開けない", () => {
    expect(cleanLogRowView(S)).toEqual({
      departed: "ok",
      resultLabel: "清掃正常終了",
      completionRate: 95.2,
      gradeShown: true,
      expandable: false,
    });
  });

  it("C2-4: △ は開ける(アドバイスあり)", () => {
    expect(cleanLogRowView(W)).toEqual({
      departed: "ok",
      resultLabel: "清掃中断",
      completionRate: 45,
      gradeShown: true,
      expandable: true,
    });
  });

  it("「結果待ち」「レポートの到着待ち」は英語に訳される(ダッシュボードのカードも同じキー)", () => {
    expect(translate("en", CLEAN_RESULT_PENDING_LABEL)).toBe("Pending");
    expect(translate("en", "レポートの到着待ち")).toBe("Waiting for report");
  });
});