/**
* 保存できなかった行の文言(指示書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の値が不正です"
);
});
});