@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("workspace-ui-kit smoke tests", () => {
|
||||
it("page module can be imported", async () => {
|
||||
const mod = await import("../app/page");
|
||||
expect(mod).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
||||
|
||||
import {
|
||||
calculateAge,
|
||||
formatISODate,
|
||||
parseISODate,
|
||||
} from "@/lib/computed/profile";
|
||||
|
||||
describe("parseISODate", () => {
|
||||
it("空文字は undefined", () => {
|
||||
expect(parseISODate("")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("不正フォーマットは undefined", () => {
|
||||
expect(parseISODate("not-a-date")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ローカル 0 時で返す(タイムゾーン跨ぎを避ける)", () => {
|
||||
const d = parseISODate("2000-01-15");
|
||||
expect(d).toBeDefined();
|
||||
expect(d?.getFullYear()).toBe(2000);
|
||||
expect(d?.getMonth()).toBe(0);
|
||||
expect(d?.getDate()).toBe(15);
|
||||
expect(d?.getHours()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatISODate", () => {
|
||||
it("undefined は空文字", () => {
|
||||
expect(formatISODate(undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("不正な Date は空文字", () => {
|
||||
expect(formatISODate(new Date("invalid"))).toBe("");
|
||||
});
|
||||
|
||||
it("YYYY-MM-DD にゼロ埋めで整形", () => {
|
||||
expect(formatISODate(new Date(2024, 0, 5))).toBe("2024-01-05");
|
||||
expect(formatISODate(new Date(2024, 11, 31))).toBe("2024-12-31");
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateAge", () => {
|
||||
beforeAll(() => {
|
||||
// テスト時点を 2026-05-09(日本時間)に固定する。
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 9));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("空文字は空文字", () => {
|
||||
expect(calculateAge("")).toBe("");
|
||||
});
|
||||
|
||||
it("不正フォーマットは空文字", () => {
|
||||
expect(calculateAge("hello")).toBe("");
|
||||
});
|
||||
|
||||
it("誕生日を過ぎていれば加齢済みの年齢", () => {
|
||||
expect(calculateAge("2000-01-15")).toBe("26 歳");
|
||||
});
|
||||
|
||||
it("誕生日前(同年内)は1引いた年齢", () => {
|
||||
expect(calculateAge("2000-12-31")).toBe("25 歳");
|
||||
});
|
||||
|
||||
it("当日(誕生日ぴったり)は加齢済み", () => {
|
||||
expect(calculateAge("2000-05-09")).toBe("26 歳");
|
||||
});
|
||||
|
||||
it("未来の生年月日は空文字(負の年齢を弾く)", () => {
|
||||
expect(calculateAge("2099-01-01")).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
candidatesSchema,
|
||||
departmentsSchema,
|
||||
workspaceSchema,
|
||||
} from "@/lib/schema";
|
||||
|
||||
import positionsData from "@/data/positions.json";
|
||||
import candidatesData from "@/data/candidates.json";
|
||||
import workspaceData from "@/data/workspace.json";
|
||||
|
||||
describe("data/*.json schema validation", () => {
|
||||
it("data/positions.json は departmentsSchema を満たす", () => {
|
||||
const result = departmentsSchema.safeParse(positionsData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("data/candidates.json は candidatesSchema を満たす", () => {
|
||||
const result = candidatesSchema.safeParse(candidatesData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("data/workspace.json は workspaceSchema を満たす", () => {
|
||||
const result = workspaceSchema.safeParse(workspaceData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema rejects invalid data", () => {
|
||||
it("departmentsSchema は配列を期待する", () => {
|
||||
expect(departmentsSchema.safeParse({}).success).toBe(false);
|
||||
expect(departmentsSchema.safeParse(null).success).toBe(false);
|
||||
});
|
||||
|
||||
it("candidate は stage が StageKey でないと不可", () => {
|
||||
expect(
|
||||
candidatesSchema.safeParse([
|
||||
{
|
||||
id: "x",
|
||||
profile: {
|
||||
name: "x",
|
||||
birthday: "",
|
||||
source: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
recruiter: "",
|
||||
desiredSalaryMin: "",
|
||||
desiredSalaryMax: "",
|
||||
availableStartDate: "",
|
||||
careerText: "",
|
||||
motivationFull: "",
|
||||
},
|
||||
scorecards: [],
|
||||
stage: "unknown-stage",
|
||||
},
|
||||
]).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("workspaceSchema は name と icon を要求する", () => {
|
||||
expect(workspaceSchema.safeParse({ name: "" }).success).toBe(false);
|
||||
expect(workspaceSchema.safeParse({ icon: "" }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { type Candidate, type Scorecard } from "@/lib/schema";
|
||||
import {
|
||||
calculateAverageScore,
|
||||
getCandidateAverageScore,
|
||||
getCommentedScorecards,
|
||||
getLatestDoneScorecard,
|
||||
getScorecardsAverageScore,
|
||||
} from "@/lib/computed/scorecards";
|
||||
|
||||
const baseScorecard = (over: Partial<Scorecard>): Scorecard => ({
|
||||
stage: "screening",
|
||||
label: "書類選考",
|
||||
date: "",
|
||||
format: "",
|
||||
interviewer: "",
|
||||
axisScores: {
|
||||
achievements: null,
|
||||
thinkingAbility: null,
|
||||
communication: null,
|
||||
cultureFit: null,
|
||||
},
|
||||
attachments: [],
|
||||
...over,
|
||||
});
|
||||
|
||||
describe("calculateAverageScore", () => {
|
||||
it("全フィールドが null なら null", () => {
|
||||
expect(
|
||||
calculateAverageScore({
|
||||
achievements: null,
|
||||
thinkingAbility: null,
|
||||
communication: null,
|
||||
cultureFit: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("一部 null は除外して平均", () => {
|
||||
expect(
|
||||
calculateAverageScore({
|
||||
achievements: 4,
|
||||
thinkingAbility: 3,
|
||||
communication: null,
|
||||
cultureFit: null,
|
||||
}),
|
||||
).toBe(3.5);
|
||||
});
|
||||
|
||||
it("4 軸全部入っていれば単純平均", () => {
|
||||
expect(
|
||||
calculateAverageScore({
|
||||
achievements: 5,
|
||||
thinkingAbility: 4,
|
||||
communication: 3,
|
||||
cultureFit: 4,
|
||||
}),
|
||||
).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLatestDoneScorecard", () => {
|
||||
it("done が無ければ undefined", () => {
|
||||
const cards = [
|
||||
baseScorecard({ stage: "screening", date: "2026-04-01" }),
|
||||
baseScorecard({ stage: "first" }),
|
||||
];
|
||||
expect(getLatestDoneScorecard(cards)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("配列末尾に近い done を返す(選考フローで最も進んだ評価)", () => {
|
||||
const cards = [
|
||||
baseScorecard({
|
||||
stage: "screening",
|
||||
date: "2026-04-01",
|
||||
decision: "通過",
|
||||
}),
|
||||
baseScorecard({ stage: "first", date: "2026-04-10", decision: "通過" }),
|
||||
baseScorecard({ stage: "second", date: "2026-04-20" }),
|
||||
];
|
||||
const latest = getLatestDoneScorecard(cards);
|
||||
expect(latest?.stage).toBe("first");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCandidateAverageScore / getScorecardsAverageScore", () => {
|
||||
it("done scorecard が無ければ null", () => {
|
||||
const candidate: Candidate = {
|
||||
id: "c1",
|
||||
profile: {
|
||||
name: "テスト 太郎",
|
||||
birthday: "",
|
||||
source: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
recruiter: "",
|
||||
desiredSalaryMin: "",
|
||||
desiredSalaryMax: "",
|
||||
availableStartDate: "",
|
||||
careerText: "",
|
||||
motivationFull: "",
|
||||
},
|
||||
scorecards: [],
|
||||
stage: "screening",
|
||||
};
|
||||
expect(getCandidateAverageScore(candidate)).toBeNull();
|
||||
expect(getScorecardsAverageScore([])).toBeNull();
|
||||
});
|
||||
|
||||
it("最新 done の axisScores を平均する", () => {
|
||||
const cards = [
|
||||
baseScorecard({
|
||||
stage: "screening",
|
||||
decision: "通過",
|
||||
axisScores: {
|
||||
achievements: 5,
|
||||
thinkingAbility: 5,
|
||||
communication: 5,
|
||||
cultureFit: 5,
|
||||
},
|
||||
}),
|
||||
baseScorecard({
|
||||
stage: "first",
|
||||
decision: "通過",
|
||||
axisScores: {
|
||||
achievements: 3,
|
||||
thinkingAbility: 3,
|
||||
communication: 3,
|
||||
cultureFit: 3,
|
||||
},
|
||||
}),
|
||||
];
|
||||
expect(getScorecardsAverageScore(cards)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCommentedScorecards", () => {
|
||||
it("done かつ comment 付きのみ STAGE_ORDER 順に返す", () => {
|
||||
const cards = [
|
||||
baseScorecard({
|
||||
stage: "first",
|
||||
decision: "通過",
|
||||
comment: "前向き",
|
||||
}),
|
||||
baseScorecard({
|
||||
stage: "screening",
|
||||
decision: "通過",
|
||||
comment: "書類良好",
|
||||
}),
|
||||
baseScorecard({
|
||||
stage: "second",
|
||||
date: "2026-05-01",
|
||||
comment: "未確定だが期待",
|
||||
}),
|
||||
];
|
||||
const result = getCommentedScorecards(cards);
|
||||
expect(result.map((c) => c.stage)).toEqual(["screening", "first"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { deriveStageStatus } from "@/lib/computed/scorecards";
|
||||
|
||||
describe("deriveStageStatus", () => {
|
||||
it("date も decision も空なら pending", () => {
|
||||
expect(deriveStageStatus("", undefined)).toBe("pending");
|
||||
expect(deriveStageStatus("")).toBe("pending");
|
||||
});
|
||||
|
||||
it("date があり decision が空なら planned", () => {
|
||||
expect(deriveStageStatus("2026-05-01", undefined)).toBe("planned");
|
||||
expect(deriveStageStatus("2026-05-01", "")).toBe("planned");
|
||||
});
|
||||
|
||||
it("decision があれば done(date の有無は問わない)", () => {
|
||||
expect(deriveStageStatus("2026-05-01", "通過")).toBe("done");
|
||||
expect(deriveStageStatus("", "不合格")).toBe("done");
|
||||
});
|
||||
|
||||
it("空白のみの decision は空扱いで done にならない", () => {
|
||||
expect(deriveStageStatus("2026-05-01", " ")).toBe("planned");
|
||||
expect(deriveStageStatus("", " ")).toBe("pending");
|
||||
});
|
||||
|
||||
it("空白のみの date は空扱いで planned にならない", () => {
|
||||
expect(deriveStageStatus(" ", undefined)).toBe("pending");
|
||||
expect(deriveStageStatus(" ", "通過")).toBe("done");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
Reference in New Issue
Block a user