Newer
Older
client_dwatch / lib / errors / __tests__ / save-error-message.test.ts
/**
 * 保存できなかった行の文言(指示書BF §1 の表)。
 * ★サーバーの 422 の形は `lib/errors/status-update.ts` / `lib/prechecks/status-update.ts` が返すものに合わせる★。
 */
import { describe, expect, it } from "vitest";
import { ApiFetchError } from "@/lib/api/client";
import { PRECHECK_NOTE_MAX_LENGTH } from "@/lib/config";
import { NOTE_REQUIRED_ROW_MESSAGE } from "@/lib/errors/edit-drafts";
import {
  SAVE_ERROR_DEFAULT,
  SAVE_ERROR_LIST_NOT_SELECTABLE,
  SAVE_ERROR_LIST_UNAVAILABLE,
  SAVE_ERROR_NETWORK,
  SAVE_ERROR_NOT_FOUND,
  SAVE_ERROR_NOTE_TOO_LONG,
  SAVE_ERROR_SERVER,
  SAVE_ERROR_UNAUTHORIZED,
  SAVE_ERROR_VALIDATION_FALLBACK,
  renderSaveErrorMessage,
  saveErrorMessage,
} from "@/lib/errors/save-error-message";
import { translate } from "@/lib/i18n";
import type { ApiErrorDetails } from "@/lib/types";

function apiError(
  status: number,
  serverMessage: string | null = null,
  details: ApiErrorDetails = null
): ApiFetchError {
  return new ApiFetchError({
    status,
    code: status === 422 ? "validation_failed" : null,
    serverMessage,
    details,
  });
}

describe("saveErrorMessage(指示書BF §1 の表)", () => {
  it("保存前の検証の文言は今のまま(その他・備考なし)", () => {
    expect(NOTE_REQUIRED_ROW_MESSAGE).toBe("その他を選んだときは備考を入力してください");
  });

  it("422・details.error_list_id「この行ではエラー内容を選べません」→ 選べない行", () => {
    const e = apiError(422, "この行ではエラー内容を選べません", {
      error_list_id: ["この行ではエラー内容を選べません"],
    });
    expect(saveErrorMessage(e)).toBe(SAVE_ERROR_LIST_NOT_SELECTABLE);
    expect(SAVE_ERROR_LIST_NOT_SELECTABLE).toBe("この行ではエラー内容を選べません");
  });

  it("422・details.error_list_id がそれ以外(選択肢に無い・無効)→ 選び直してください", () => {
    const e = apiError(422, "error_list_idの値が不正です", {
      error_list_id: ["有効なエラー内容を指定してください"],
    });
    expect(saveErrorMessage(e)).toBe(SAVE_ERROR_LIST_UNAVAILABLE);
    expect(SAVE_ERROR_LIST_UNAVAILABLE).toBe("選んだエラー内容は今は使えません。選び直してください");
  });

  it("422・details.note(その他・備考なし。サーバー側で止まった)→ 備考を入力してください", () => {
    const e = apiError(422, "備考を入力してください", {
      note: ["このエラー内容を選んだときは備考が必須です"],
    });
    expect(saveErrorMessage(e)).toBe(NOTE_REQUIRED_ROW_MESSAGE);
  });

  it("422・備考の長さ(400文字超)→ 備考は400文字以内で入力してください", () => {
    const e = apiError(422, "noteが長すぎます", {
      note: [`${PRECHECK_NOTE_MAX_LENGTH}文字以内で入力してください`],
    });
    expect(saveErrorMessage(e)).toBe(SAVE_ERROR_NOTE_TOO_LONG);
    expect(renderSaveErrorMessage(SAVE_ERROR_NOTE_TOO_LONG, (s) => s)).toBe(
      "備考は400文字以内で入力してください"
    );
  });

  it("422 のその他 → サーバーの文言", () => {
    expect(saveErrorMessage(apiError(422, "statusの値が不正です", { status: ["x"] }))).toBe(
      "statusの値が不正です"
    );
    // error_list_id の型違い(details は null)
    expect(saveErrorMessage(apiError(422, "error_list_idは1以上の整数で指定してください"))).toBe(
      "error_list_idは1以上の整数で指定してください"
    );
  });

  it("422 のその他でサーバーの文言が無い → 入力内容を確かめてください", () => {
    expect(saveErrorMessage(apiError(422))).toBe(SAVE_ERROR_VALIDATION_FALLBACK);
    expect(SAVE_ERROR_VALIDATION_FALLBACK).toBe("入力内容を確かめてください");
  });

  it("422 で details が文字列の配列でない → 422 のその他として扱う", () => {
    expect(saveErrorMessage(apiError(422, "x", { error_list_id: 3 }))).toBe("x");
    expect(saveErrorMessage(apiError(422, null, { note: [1, 2] }))).toBe(SAVE_ERROR_VALIDATION_FALLBACK);
  });

  it("404 → 削除されたため保存できません", () => {
    expect(saveErrorMessage(apiError(404, "エラーが見つかりません"))).toBe(SAVE_ERROR_NOT_FOUND);
    expect(SAVE_ERROR_NOT_FOUND).toBe(
      "このエラーは削除されたため保存できません。画面を読み込み直してください"
    );
  });

  it("401 → ログインし直してください", () => {
    expect(saveErrorMessage(apiError(401, "認証が必要です"))).toBe(SAVE_ERROR_UNAUTHORIZED);
    expect(SAVE_ERROR_UNAUTHORIZED).toBe("ログインし直してください");
  });

  it.each([500, 502, 503, 504])("%i → サーバーで保存に失敗しました", (status) => {
    expect(saveErrorMessage(apiError(status, "対応状況の更新に失敗しました"))).toBe(SAVE_ERROR_SERVER);
  });

  it("5xx の文言", () => {
    expect(SAVE_ERROR_SERVER).toBe(
      "サーバーで保存に失敗しました(DB への書き込みエラー)。時間をおいて保存し直し、続くときは管理者に連絡してください"
    );
  });

  it("通信の失敗(status 0)→ 通信に失敗しました", () => {
    expect(saveErrorMessage(apiError(0))).toBe(SAVE_ERROR_NETWORK);
    expect(SAVE_ERROR_NETWORK).toBe("通信に失敗しました。接続を確かめて保存し直してください");
  });

  it.each([400, 403, 409, 429])("どれにも当たらない(%i)→ 保存に失敗しました", (status) => {
    expect(saveErrorMessage(apiError(status, "サーバーの文言"))).toBe(SAVE_ERROR_DEFAULT);
  });

  it.each([new Error("boom"), new TypeError("x"), "string", null, undefined])(
    "ApiFetchError でない(%s)→ 保存に失敗しました",
    (e) => {
      expect(saveErrorMessage(e)).toBe(SAVE_ERROR_DEFAULT);
    }
  );
});

describe("renderSaveErrorMessage と辞書(JP / EN)", () => {
  const MESSAGES = [
    NOTE_REQUIRED_ROW_MESSAGE,
    SAVE_ERROR_LIST_NOT_SELECTABLE,
    SAVE_ERROR_LIST_UNAVAILABLE,
    SAVE_ERROR_NOTE_TOO_LONG,
    SAVE_ERROR_VALIDATION_FALLBACK,
    SAVE_ERROR_NOT_FOUND,
    SAVE_ERROR_UNAUTHORIZED,
    SAVE_ERROR_SERVER,
    SAVE_ERROR_NETWORK,
    SAVE_ERROR_DEFAULT,
  ];

  it.each(MESSAGES)("EN の訳がある: %s", (message) => {
    expect(translate("en", message)).not.toBe(message);
  });

  it("備考の長さは EN でも上限の数字が入る", () => {
    const en = renderSaveErrorMessage(SAVE_ERROR_NOTE_TOO_LONG, (s) => translate("en", s));
    expect(en).toContain(String(PRECHECK_NOTE_MAX_LENGTH));
    expect(en).not.toContain("{n}");
  });

  it("サーバーの文言(辞書に無い)はそのまま出す", () => {
    expect(renderSaveErrorMessage("statusの値が不正です", (s) => translate("en", s))).toBe(
      "statusの値が不正です"
    );
  });
});