From 0d7aeb282e166163bc152cd22625980c6e241505 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Tue, 9 Jun 2026 23:33:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(frontend):=20=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=20SSoT=20=E7=B5=B1=E4=B8=80?= =?UTF-8?q?=E3=81=A8=E8=81=B7=E5=8B=99=E7=B5=8C=E6=AD=B4=20mutator=20?= =?UTF-8?q?=E3=81=AE=E9=87=8D=E8=A4=87=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 削除確認ダイアログ/保存ボタンの日本語リテラルを UI_MESSAGES に集約 (ConfirmDialog・CareerResumeForm・useDocumentForm の隠れた SSoT 違反を解消) - useCareerExperienceMutators に updateExperienceAt/updateClientAt を導入し 三階層 immutable 更新の重複を集約(戻り値契約は不変) - 取引先配下のハンドラ Props を ClientMutationHandlers 型に切り出し CareerExperienceEditor/ClientEditor で共有 - 未ログイン保存導線(onSubmit 匿名分岐)の component テストを追加 Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/ConfirmDialog.tsx | 5 +- .../CareerExperienceEditor.tsx | 29 +-- .../forms/CareerFormEditors/ClientEditor.tsx | 25 +-- .../forms/CareerResumeForm.test.tsx | 77 +++++++ .../src/components/forms/CareerResumeForm.tsx | 4 +- frontend/src/constants/messages.ts | 11 + frontend/src/formTypes.ts | 27 +++ .../career/useCareerExperienceMutators.ts | 190 +++++++----------- frontend/src/hooks/useDocumentForm.ts | 6 +- 9 files changed, 202 insertions(+), 172 deletions(-) create mode 100644 frontend/src/components/forms/CareerResumeForm.test.tsx diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index 32fcc9ab..5b90c1ca 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx @@ -1,3 +1,4 @@ +import { UI_MESSAGES } from "../constants/messages"; import styles from "./ConfirmDialog.module.css"; export function ConfirmDialog({ @@ -21,10 +22,10 @@ export function ConfirmDialog({

{message}

diff --git a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx index 7171c8db..ac198a74 100644 --- a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx @@ -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 { @@ -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; /** この職務経歴のインデックス */ @@ -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; /** プロジェクトサマリーテキストを生成する関数 */ diff --git a/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx b/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx index 4e54f0c4..f0a147e9 100644 --- a/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx @@ -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 { @@ -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; /** 親となる職務経歴のインデックス */ @@ -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; /** バリデーション失敗フィールドの位置情報(休暇期間のフォーカス・赤枠用) */ diff --git a/frontend/src/components/forms/CareerResumeForm.test.tsx b/frontend/src/components/forms/CareerResumeForm.test.tsx new file mode 100644 index 00000000..7a83a315 --- /dev/null +++ b/frontend/src/components/forms/CareerResumeForm.test.tsx @@ -0,0 +1,77 @@ +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: () =>
, +})); + +// マスタデータ取得は実 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( + + + + + + + , + ); + return { requestLogin }; +} + +describe("CareerResumeForm(未ログインの保存導線)", () => { + beforeEach(() => { + // ドラフト退避(sessionStorage)がテスト間で漏れないようにする。 + window.sessionStorage.clear(); + }); + + /** 保存ボタンはマスタデータ取得が終わるまで非活性なので、活性化を待ってから返す。 */ + async function waitForEnabledSaveButton(): Promise { + 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.getByPlaceholderText("例: 山田 太郎"), { + target: { value: "山田 太郎" }, + }); + fireEvent.click(saveButton); + + expect(requestLogin).toHaveBeenCalledTimes(1); + expect(screen.queryByText(VALIDATION_MESSAGES.FULL_NAME_REQUIRED)).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/forms/CareerResumeForm.tsx b/frontend/src/components/forms/CareerResumeForm.tsx index 8d109f31..d930b1ac 100644 --- a/frontend/src/components/forms/CareerResumeForm.tsx +++ b/frontend/src/components/forms/CareerResumeForm.tsx @@ -298,8 +298,8 @@ export function CareerResumeForm({ isAuthenticated }: { isAuthenticated: boolean <> {showDeleteConfirm && ( setShowDeleteConfirm(false)} confirming={deleting} diff --git a/frontend/src/constants/messages.ts b/frontend/src/constants/messages.ts index 14ff1b87..a5d277cc 100644 --- a/frontend/src/constants/messages.ts +++ b/frontend/src/constants/messages.ts @@ -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 */ diff --git a/frontend/src/formTypes.ts b/frontend/src/formTypes.ts index 945531dd..611a78ed 100644 --- a/frontend/src/formTypes.ts +++ b/frontend/src/formTypes.ts @@ -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; +}; diff --git a/frontend/src/hooks/career/useCareerExperienceMutators.ts b/frontend/src/hooks/career/useCareerExperienceMutators.ts index b88e47d0..d6b668d3 100644 --- a/frontend/src/hooks/career/useCareerExperienceMutators.ts +++ b/frontend/src/hooks/career/useCareerExperienceMutators.ts @@ -10,6 +10,7 @@ import type { CareerExperienceFieldKey, } from "../../formTypes"; import type { + CareerClientForm, CareerExperienceForm, CareerFormState, CareerProjectForm, @@ -24,23 +25,48 @@ export function useCareerExperienceMutators( experiences: CareerExperienceForm[], setForm: Dispatch>, ) { + /** + * 指定 index の experience を updater で書き換える共通ヘルパ。 + * 三階層 immutable 更新の「該当 1 件だけ map で差し替える」定型をここに集約する。 + */ + const updateExperienceAt = ( + expIndex: number, + updater: (exp: CareerExperienceForm) => CareerExperienceForm, + ) => { + setForm((prev) => ({ + ...prev, + experiences: prev.experiences.map((exp, ei) => (ei === expIndex ? updater(exp) : exp)), + })); + }; + + /** + * 指定座標(experience → client)の client を updater で書き換える共通ヘルパ。 + * updateExperienceAt の上に client 1 件差し替えを重ねる。 + */ + const updateClientAt = ( + expIndex: number, + clientIndex: number, + updater: (client: CareerClientForm) => CareerClientForm, + ) => { + updateExperienceAt(expIndex, (exp) => ({ + ...exp, + clients: exp.clients.map((c, ci) => (ci === clientIndex ? updater(c) : c)), + })); + }; + /** experience フィールド変更ハンドラ */ const updateExperienceField = ( index: number, key: CareerExperienceFieldKey, value: string | boolean, ) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, i) => { - if (i !== index) return exp; - if (key === "is_current") { - const isCurrent = Boolean(value); - return { ...exp, is_current: isCurrent, end_date: isCurrent ? "" : exp.end_date }; - } - return { ...exp, [key]: value }; - }), - })); + updateExperienceAt(index, (exp) => { + if (key === "is_current") { + const isCurrent = Boolean(value); + return { ...exp, is_current: isCurrent, end_date: isCurrent ? "" : exp.end_date }; + } + return { ...exp, [key]: value }; + }); }; /** client フィールド変更ハンドラ */ @@ -50,50 +76,21 @@ export function useCareerExperienceMutators( key: CareerClientFieldKey, value: string, ) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => - ci === clientIndex ? { ...c, [key]: value } : c, - ), - }; - }), - })); + updateClientAt(expIndex, clientIndex, (c) => ({ ...c, [key]: value })); }; /** 「取引先なし」フラグ切り替えハンドラ */ const updateClientHasClient = (expIndex: number, clientIndex: number, value: boolean) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => - ci === clientIndex ? { ...c, has_client: value, name: value ? c.name : "" } : c, - ), - }; - }), + updateClientAt(expIndex, clientIndex, (c) => ({ + ...c, + has_client: value, + name: value ? c.name : "", })); }; /** 「休暇」フラグ切り替えハンドラ */ const updateClientIsVacation = (expIndex: number, clientIndex: number, value: boolean) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => - ci === clientIndex ? { ...c, is_vacation: value } : c, - ), - }; - }), - })); + updateClientAt(expIndex, clientIndex, (c) => ({ ...c, is_vacation: value })); }; /** 休暇の「継続中」フラグ切り替えハンドラ(継続中なら終了年月をクリア) */ @@ -102,75 +99,40 @@ export function useCareerExperienceMutators( clientIndex: number, value: boolean, ) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => - ci === clientIndex - ? { - ...c, - vacation_is_current: value, - vacation_end_date: value ? "" : c.vacation_end_date, - } - : c, - ), - }; - }), + updateClientAt(expIndex, clientIndex, (c) => ({ + ...c, + vacation_is_current: value, + vacation_end_date: value ? "" : c.vacation_end_date, })); }; /** 取引先追加ハンドラ */ const addClient = (expIndex: number) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => - ei === expIndex - ? { ...exp, clients: [...exp.clients, { ...blankCareerClient }] } - : exp, - ), + updateExperienceAt(expIndex, (exp) => ({ + ...exp, + clients: [...exp.clients, { ...blankCareerClient }], })); }; - /** 取引先削除ハンドラ */ + /** 取引先削除ハンドラ(最後の 1 件は空レコードへリセット) */ const removeClient = (expIndex: number, clientIndex: number) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: - exp.clients.length === 1 - ? [{ ...blankCareerClient }] - : exp.clients.filter((_, ci) => ci !== clientIndex), - }; - }), + updateExperienceAt(expIndex, (exp) => ({ + ...exp, + clients: + exp.clients.length === 1 + ? [{ ...blankCareerClient }] + : exp.clients.filter((_, ci) => ci !== clientIndex), })); }; - /** プロジェクト削除ハンドラ */ + /** プロジェクト削除ハンドラ(最後の 1 件は空レコードへリセット) */ const removeProject = (expIndex: number, clientIndex: number, projIndex: number) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => { - if (ci !== clientIndex) return c; - return { - ...c, - projects: - c.projects.length === 1 - ? [{ ...blankCareerProject }] - : c.projects.filter((_, pi) => pi !== projIndex), - }; - }), - }; - }), + updateClientAt(expIndex, clientIndex, (c) => ({ + ...c, + projects: + c.projects.length === 1 + ? [{ ...blankCareerProject }] + : c.projects.filter((_, pi) => pi !== projIndex), })); }; @@ -223,25 +185,11 @@ export function useCareerExperienceMutators( projIndex: number | null, project: CareerProjectForm, ) => { - setForm((prev) => ({ - ...prev, - experiences: prev.experiences.map((exp, ei) => { - if (ei !== expIndex) return exp; - return { - ...exp, - clients: exp.clients.map((c, ci) => { - if (ci !== clientIndex) return c; - if (projIndex === null) { - return { ...c, projects: [...c.projects, project] }; - } - return { - ...c, - projects: c.projects.map((p, pi) => (pi === projIndex ? project : p)), - }; - }), - }; - }), - })); + updateClientAt(expIndex, clientIndex, (c) => + projIndex === null + ? { ...c, projects: [...c.projects, project] } + : { ...c, projects: c.projects.map((p, pi) => (pi === projIndex ? project : p)) }, + ); }; return { diff --git a/frontend/src/hooks/useDocumentForm.ts b/frontend/src/hooks/useDocumentForm.ts index 5e68f9e6..03c52a44 100644 --- a/frontend/src/hooks/useDocumentForm.ts +++ b/frontend/src/hooks/useDocumentForm.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { Dispatch, SetStateAction } from "react"; -import { FALLBACK_MESSAGES } from "../constants/messages"; +import { FALLBACK_MESSAGES, UI_MESSAGES } from "../constants/messages"; import { useAppDispatch, useAppSelector } from "../store"; import { clearCache, @@ -172,8 +172,8 @@ export function useDocumentForm { - if (saving) return "保存中..."; - return documentId ? "更新する" : "保存する"; + if (saving) return UI_MESSAGES.FORM_SAVING; + return documentId ? UI_MESSAGES.FORM_UPDATE : UI_MESSAGES.FORM_SAVE; }, [documentId, saving]); /** From cceb239c5865868967099cb67746b1bb1df118fd Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Wed, 10 Jun 2026 12:06:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test(frontend):=20=E6=B0=8F=E5=90=8D?= =?UTF-8?q?=E5=85=A5=E5=8A=9B=E3=81=AE=E7=89=B9=E5=AE=9A=E3=82=92=E3=83=97?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=B9=E3=83=9B=E3=83=AB=E3=83=80=E3=81=8B?= =?UTF-8?q?=E3=82=89=E3=83=A9=E3=83=99=E3=83=AB=E3=83=AD=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=81=B8=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビュー指摘対応。getByPlaceholderText のリテラル結合を避け、 getByRole("textbox", { name: /氏名/ }) でラベル経由に特定して プレースホルダ文言変更に強くする。 Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/forms/CareerResumeForm.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/forms/CareerResumeForm.test.tsx b/frontend/src/components/forms/CareerResumeForm.test.tsx index 7a83a315..8d5f0f18 100644 --- a/frontend/src/components/forms/CareerResumeForm.test.tsx +++ b/frontend/src/components/forms/CareerResumeForm.test.tsx @@ -66,7 +66,8 @@ describe("CareerResumeForm(未ログインの保存導線)", () => { const { requestLogin } = renderAnonymousForm(); const saveButton = await waitForEnabledSaveButton(); - fireEvent.change(screen.getByPlaceholderText("例: 山田 太郎"), { + // 氏名入力はプレースホルダ文言ではなくラベル(ロール + アクセシブル名)で特定する。 + fireEvent.change(screen.getByRole("textbox", { name: /氏名/ }), { target: { value: "山田 太郎" }, }); fireEvent.click(saveButton); From 5371e25eac74238ac56aa71451353e46f7cdb391 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Wed, 10 Jun 2026 12:26:14 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(frontend):=20shell-quote=20=E3=82=92=20?= =?UTF-8?q?1.8.4=20=E3=81=AB=E4=B8=8A=E6=9B=B8=E3=81=8D=E3=81=97=E3=81=A6?= =?UTF-8?q?=20critical=20=E7=9B=A3=E6=9F=BB=E3=82=92=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit concurrently 経由の推移依存 shell-quote@1.8.3 が GHSA-w7jw-789q-3m8p (critical) に該当し npm audit --audit-level=high が CI で fail していた。 package.json の overrides で patch 版 1.8.4 を強制し、concurrently の 破壊的ダウングレード(--force)を避けて解消する。 Co-Authored-By: Claude Opus 4.8 --- frontend/package-lock.json | 6 +++--- frontend/package.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1995faa9..7acfd81f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7262,9 +7262,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "dev": true, "license": "MIT", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 1eb1842e..c03a4f88 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,5 +59,8 @@ "vite": "^6.0.5", "vitest": "^4.1.2", "wrangler": "^4.90.0" + }, + "overrides": { + "shell-quote": "^1.8.4" } }