import { describe, expect, it } from "vitest";
import { displayRate, isProblem, isProblemGrade, judgeGrade } from "@/lib/judge";
import type { Grade } from "@/lib/types";
/**
* ◎○△× 判定の期待値表テスト
*
* ★この表は docs/fixtures.md 「2.1 判定の境界値」と同じ値です★
* lib/fixtures/clean-logs.ts の boundaryCases も同じ10ケースを持っています。
* 画面表示とテストで挙動が一致していることを目視でも確認できます。
*
* ★★境界はすべて「上のランクに入る」★★(★D-139★。ユーザー指示 2026-09-16
* 「◯、×は90%以上のように境はふくめてください」)。★90.0 は ◎、70.0 は ○★。
* ★× の境界だけは変わらない★(同日「10%未満を×としてください」)ので 10.0 は △。
* ★旧 D-020b(境界はすべて下のランクに落ちる)は破棄★。
*
* 表を変えたくなったら、先に CLAUDE.md 4.1 の仕様を確認してください。
*/
const table: Array<{
name: string;
rate: number | null;
grade: Grade;
display: string;
}> = [
{ name: "◎の下限すぐ上", rate: 90.1, grade: "◎", display: "90%" },
{ name: "◎/○の境界(90%以上が◎なので90.0は◎。D-139)", rate: 90.0, grade: "◎", display: "90%" },
{ name: "◎の境界のすぐ下", rate: 89.9, grade: "○", display: "90%" },
{ name: "丸めると90だが○", rate: 89.5, grade: "○", display: "90%" },
{ name: "○の下限すぐ上", rate: 70.1, grade: "○", display: "70%" },
{ name: "○/△の境界(70%以上が○なので70.0は○。D-139)", rate: 70.0, grade: "○", display: "70%" },
{ name: "○の境界のすぐ下", rate: 69.9, grade: "△", display: "70%" },
{ name: "△の下限すぐ上", rate: 10.1, grade: "△", display: "10%" },
{ name: "△/×の境界(10%未満が×なので10.0は△。D-139でも変わらない)", rate: 10.0, grade: "△", display: "10%" },
{ name: "×の代表値", rate: 9.9, grade: "×", display: "10%" },
{ name: "ゼロ", rate: 0, grade: "×", display: "0%" },
{ name: "未実施", rate: null, grade: "×", display: "-" },
];
describe("judgeGrade", () => {
it.each(table)("$name: $rate → $grade", ({ rate, grade }) => {
expect(judgeGrade(rate)).toBe(grade);
});
it("丸めてから判定すると判定が変わることを確認(アンチパターンの記録)", () => {
// ★D-139 で境界が「以上」になったため、事故の向きが逆になった★ —
// 旧規則では 90.4 を丸めると ◎ が「消えた」が、新規則では 90 も ◎ なので消えない。
// いまは ★無い ◎(○)が丸めで生まれる★ 側が事故になる。
// ★どちらにせよ「表示用に丸めた値で判定しない」(CLAUDE.md §4.3)は変わらない★。
expect(judgeGrade(90.4)).toBe("◎"); // ✅ 生値で判定
expect(judgeGrade(Math.round(90.4))).toBe("◎"); // 90 も ◎(D-139 以降)
expect(judgeGrade(89.5)).toBe("○"); // ✅ 生値で判定
expect(judgeGrade(Math.round(89.5))).toBe("◎"); // ❌ 丸めると90になり ◎ に化ける
expect(judgeGrade(69.5)).toBe("△"); // ✅ 生値で判定
expect(judgeGrade(Math.round(69.5))).toBe("○"); // ❌ 丸めると70になり ○ に化ける
});
});
describe("displayRate", () => {
it.each(table)("$name: $rate → $display", ({ rate, display }) => {
expect(displayRate(rate)).toBe(display);
});
it("小数第1位を四捨五入する", () => {
expect(displayRate(91.4)).toBe("91%");
expect(displayRate(91.5)).toBe("92%");
});
});
describe("isProblem", () => {
it("出発失敗は完了率に関わらず問題あり", () => {
expect(isProblem({ departed: false, completionRate: null })).toBe(true);
expect(isProblem({ departed: false, completionRate: 95 })).toBe(true);
});
it("△と×は問題あり", () => {
expect(isProblem({ departed: true, completionRate: 69.9 })).toBe(true); // △
expect(isProblem({ departed: true, completionRate: 9.9 })).toBe(true); // ×
});
it("◎と○は問題なし", () => {
expect(isProblem({ departed: true, completionRate: 90.1 })).toBe(false); // ◎
expect(isProblem({ departed: true, completionRate: 70.1 })).toBe(false); // ○
// ★D-139: ちょうど 70.0 は ○ なので問題なし(旧規則では △ で問題ありだった)★
expect(isProblem({ departed: true, completionRate: 70.0 })).toBe(false);
});
});
// ---------------------------------------------------------------------------
// G. isProblemGrade(出発の有無を見ない版。エリア別完了率が使う)
//
// grade 期待
// ◎ / ○ false
// △ / × true
// ---------------------------------------------------------------------------
describe("isProblemGrade", () => {
it.each([
{ grade: "◎" as Grade, expected: false },
{ grade: "○" as Grade, expected: false },
{ grade: "△" as Grade, expected: true },
{ grade: "×" as Grade, expected: true },
])("$grade → $expected", ({ grade, expected }) => {
expect(isProblemGrade(grade)).toBe(expected);
});
it("★isProblem() は出発していれば isProblemGrade(judgeGrade()) と一致する★", () => {
for (const rate of [90.1, 90.0, 89.5, 70.1, 70.0, 10.0, 9.9, 0, null]) {
expect(isProblem({ departed: true, completionRate: rate })).toBe(
isProblemGrade(judgeGrade(rate))
);
}
});
});