Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@
"vite": "^6.0.5",
"vitest": "^4.1.2",
"wrangler": "^4.90.0"
},
"overrides": {
"shell-quote": "^1.8.4"
}
}
5 changes: 3 additions & 2 deletions frontend/src/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UI_MESSAGES } from "../constants/messages";
import styles from "./ConfirmDialog.module.css";

export function ConfirmDialog({
Expand All @@ -21,10 +22,10 @@ export function ConfirmDialog({
<p className={styles.message}>{message}</p>
<div className={styles.actions}>
<button type="button" className="danger" onClick={onConfirm} disabled={confirming}>
{confirming ? (confirmingLabel ?? "削除中...") : confirmLabel}
{confirming ? (confirmingLabel ?? UI_MESSAGES.CONFIRM_DELETING) : confirmLabel}
</button>
<button type="button" onClick={onCancel} disabled={confirming}>
キャンセル
{UI_MESSAGES.CONFIRM_CANCEL}
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CAPITAL_UNITS } from "../../../constants";
import type { CareerClientFieldKey, CareerExperienceFieldKey } from "../../../formTypes";
import type { CareerExperienceFieldKey, ClientMutationHandlers } from "../../../formTypes";
import type { ExperienceDirty } from "../../../hooks/career/useCareerDirty";
import { useFocusOnMatch } from "../../../hooks/useFocusOnMatch";
import {
Expand All @@ -17,8 +17,12 @@ import { DirtyDot } from "../../ui/DirtyDot";
import { PlusIcon } from "../../icons/PlusIcon";
import { ClientEditor } from "./ClientEditor";

/** CareerExperienceEditor のプロパティ型 */
type CareerExperienceEditorProps = {
/**
* CareerExperienceEditor のプロパティ型。
* 取引先(client)配下のハンドラ群は ClientEditor へ素通しするため
* ClientMutationHandlers で ClientEditor と共有する。
*/
type CareerExperienceEditorProps = ClientMutationHandlers & {
/** 編集対象の職務経歴データ */
exp: CareerExperienceForm;
/** この職務経歴のインデックス */
Expand All @@ -29,27 +33,8 @@ type CareerExperienceEditorProps = {
key: CareerExperienceFieldKey,
value: string | boolean,
) => void;
/** 取引先フィールド変更ハンドラ */
onUpdateClientField: (
expIndex: number,
clientIndex: number,
key: CareerClientFieldKey,
value: string,
) => void;
/** 取引先「取引先なし」切替ハンドラ */
onUpdateClientHasClient: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 取引先「休暇」切替ハンドラ */
onUpdateClientIsVacation: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 休暇「継続中」切替ハンドラ */
onUpdateClientVacationIsCurrent: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 取引先追加ハンドラ */
onAddClient: (expIndex: number) => void;
/** 取引先削除ハンドラ */
onRemoveClient: (expIndex: number, clientIndex: number) => void;
/** プロジェクト削除ハンドラ */
onRemoveProject: (expIndex: number, clientIndex: number, projIndex: number) => void;
/** プロジェクト編集モーダルを開くハンドラ */
onOpenProjectModal: (expIndex: number, clientIndex: number, projIndex: number | null) => void;
/** 職務経歴削除ハンドラ */
onRemoveExperience: (index: number) => void;
/** プロジェクトサマリーテキストを生成する関数 */
Expand Down
25 changes: 3 additions & 22 deletions frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CareerClientFieldKey } from "../../../formTypes";
import type { ClientMutationHandlers } from "../../../formTypes";
import type { ClientDirty } from "../../../hooks/career/useCareerDirty";
import { useFocusOnMatch } from "../../../hooks/useFocusOnMatch";
import {
Expand All @@ -14,8 +14,8 @@ import { DeleteIconButton } from "../../ui/DeleteIconButton";
import { DirtyDot } from "../../ui/DirtyDot";
import { PlusIcon } from "../../icons/PlusIcon";

/** ClientEditor のプロパティ型 */
type ClientEditorProps = {
/** ClientEditor のプロパティ型(取引先配下のハンドラ群は ClientMutationHandlers で共有) */
type ClientEditorProps = ClientMutationHandlers & {
/** 編集対象の取引先データ */
client: CareerClientForm;
/** 親となる職務経歴のインデックス */
Expand All @@ -24,25 +24,6 @@ type ClientEditorProps = {
clientIndex: number;
/** この取引先の dirty 情報。未指定なら 🔴 表示なし。 */
dirty?: ClientDirty;
/** 取引先フィールド変更ハンドラ */
onUpdateClientField: (
expIndex: number,
clientIndex: number,
key: CareerClientFieldKey,
value: string,
) => void;
/** 取引先「取引先なし」切替ハンドラ */
onUpdateClientHasClient: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 取引先「休暇」切替ハンドラ */
onUpdateClientIsVacation: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 休暇「継続中」切替ハンドラ */
onUpdateClientVacationIsCurrent: (expIndex: number, clientIndex: number, value: boolean) => void;
/** プロジェクト削除ハンドラ */
onRemoveProject: (expIndex: number, clientIndex: number, projIndex: number) => void;
/** プロジェクト編集モーダルを開くハンドラ */
onOpenProjectModal: (expIndex: number, clientIndex: number, projIndex: number | null) => void;
/** 取引先削除ハンドラ */
onRemoveClient: (expIndex: number, clientIndex: number) => void;
/** プロジェクトサマリーテキストを生成する関数 */
projectSummary: (proj: CareerProjectForm) => string;
/** バリデーション失敗フィールドの位置情報(休暇期間のフォーカス・赤枠用) */
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/components/forms/CareerResumeForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { describe, it, expect, vi, beforeEach } from "vitest";

import { CareerResumeForm } from "./CareerResumeForm";
import { LoginPromptContext } from "../auth/loginPromptContext";
import { ToastProvider } from "../ui/toast";
import formCacheReducer from "../../store/formCacheSlice";
import { UI_MESSAGES, VALIDATION_MESSAGES } from "../../constants/messages";

// 原本ビューは react-pdf / pdf.js を遅延ロードするため、フォームの単体テストではスタブに差し替える。
vi.mock("./ResumeSourceTracePanel", () => ({
ResumeSourceTracePanel: () => <div data-testid="source-panel-stub" />,
}));

// マスタデータ取得は実 fetch を避けて空配列を返す(匿名分岐の検証には不要なため)。
vi.mock("../../api/master-data", () => ({
getQualifications: vi.fn().mockResolvedValue([]),
getTechnologyStacks: vi.fn().mockResolvedValue([]),
}));

/** formCache だけを持つ最小ストアを作る。 */
function makeStore() {
return configureStore({ reducer: { formCache: formCacheReducer } });
}

/** 未ログイン状態の CareerResumeForm を描画し、requestLogin スパイを返す。 */
function renderAnonymousForm() {
const requestLogin = vi.fn();
render(
<Provider store={makeStore()}>
<LoginPromptContext.Provider value={requestLogin}>
<ToastProvider>
<CareerResumeForm isAuthenticated={false} />
</ToastProvider>
</LoginPromptContext.Provider>
</Provider>,
);
return { requestLogin };
}

describe("CareerResumeForm(未ログインの保存導線)", () => {
beforeEach(() => {
// ドラフト退避(sessionStorage)がテスト間で漏れないようにする。
window.sessionStorage.clear();
});

/** 保存ボタンはマスタデータ取得が終わるまで非活性なので、活性化を待ってから返す。 */
async function waitForEnabledSaveButton(): Promise<HTMLElement> {
const button = screen.getByRole("button", { name: UI_MESSAGES.FORM_SAVE });
await waitFor(() => expect(button).toBeEnabled());
return button;
}

it("氏名が空のまま保存すると、ログインを促さず氏名必須エラーを表示する", async () => {
const { requestLogin } = renderAnonymousForm();

fireEvent.click(await waitForEnabledSaveButton());

expect(screen.getByText(VALIDATION_MESSAGES.FULL_NAME_REQUIRED)).toBeInTheDocument();
expect(requestLogin).not.toHaveBeenCalled();
});

it("氏名を入力して保存すると、バリデーションを通してログイン促進を呼ぶ", async () => {
const { requestLogin } = renderAnonymousForm();

const saveButton = await waitForEnabledSaveButton();
// 氏名入力はプレースホルダ文言ではなくラベル(ロール + アクセシブル名)で特定する。
fireEvent.change(screen.getByRole("textbox", { name: /氏名/ }), {
target: { value: "山田 太郎" },
});
fireEvent.click(saveButton);

expect(requestLogin).toHaveBeenCalledTimes(1);
expect(screen.queryByText(VALIDATION_MESSAGES.FULL_NAME_REQUIRED)).not.toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions frontend/src/components/forms/CareerResumeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ export function CareerResumeForm({ isAuthenticated }: { isAuthenticated: boolean
<>
{showDeleteConfirm && (
<ConfirmDialog
message="職務経歴書のデータを全て削除します。この操作は取り消せません。本当に削除しますか?"
confirmLabel="削除する"
message={UI_MESSAGES.RESUME_DELETE_CONFIRM}
confirmLabel={UI_MESSAGES.RESUME_DELETE_CONFIRM_LABEL}
onConfirm={handleDelete}
onCancel={() => setShowDeleteConfirm(false)}
confirming={deleting}
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ export const UI_MESSAGES = {
RESUME_DELETE_VACATION: "休業を削除",
RESUME_DELETE_PROJECT: "プロジェクトを削除",
RESUME_DELETE_QUALIFICATION: "資格を削除",
// 職務経歴書「データを削除」確認ダイアログ(ConfirmDialog の message / confirmLabel)
RESUME_DELETE_CONFIRM:
"職務経歴書のデータを全て削除します。この操作は取り消せません。本当に削除しますか?",
RESUME_DELETE_CONFIRM_LABEL: "削除する",
// 確認ダイアログ共通(ConfirmDialog のボタン文言)
CONFIRM_DELETING: "削除中...",
CONFIRM_CANCEL: "キャンセル",
// ドキュメントフォームの保存ボタン文言(useDocumentForm の saveButtonText)
FORM_SAVING: "保存中...",
FORM_UPDATE: "更新する",
FORM_SAVE: "保存する",
} as const;

/** 外部リンク URL(GitHub リポジトリ / Issue 報告先など)の SSoT */
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/formTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,30 @@ export type CareerClientFieldKey =
| "vacation_description";
export type CareerProjectFieldKey = "name" | "role" | "description";
export type CareerProjectPeriodFieldKey = "start_date" | "end_date" | "is_current";

/**
* 取引先(client)配下のミューテーションハンドラ群。
* CareerExperienceEditor が ClientEditor へ素通しするため、両 Props 型で共有する
* (片方だけ引数を足したときの取り違えを型で防ぐ)。
*/
export type ClientMutationHandlers = {
/** 取引先フィールド変更ハンドラ */
onUpdateClientField: (
expIndex: number,
clientIndex: number,
key: CareerClientFieldKey,
value: string,
) => void;
/** 取引先「取引先なし」切替ハンドラ */
onUpdateClientHasClient: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 取引先「休暇」切替ハンドラ */
onUpdateClientIsVacation: (expIndex: number, clientIndex: number, value: boolean) => void;
/** 休暇「継続中」切替ハンドラ */
onUpdateClientVacationIsCurrent: (expIndex: number, clientIndex: number, value: boolean) => void;
/** プロジェクト削除ハンドラ */
onRemoveProject: (expIndex: number, clientIndex: number, projIndex: number) => void;
/** プロジェクト編集モーダルを開くハンドラ */
onOpenProjectModal: (expIndex: number, clientIndex: number, projIndex: number | null) => void;
/** 取引先削除ハンドラ */
onRemoveClient: (expIndex: number, clientIndex: number) => void;
};
Loading
Loading