initial commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
snc777
2026-05-09 14:24:17 +09:00
co-authored by Cursor
commit f6a781c47e
196 changed files with 31290 additions and 0 deletions
+8
View File
@@ -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();
});
});
+77
View File
@@ -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("");
});
});
+66
View File
@@ -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);
});
});
+161
View File
@@ -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"]);
});
});
+29
View File
@@ -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");
});
});
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";