From b702324c82601d7344217513cd2cd2989f01e68b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 13:18:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=88=E3=83=A2?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AE=E6=8A=98=E3=82=8A=E8=BF=94=E3=81=97?= =?UTF-8?q?=E6=99=82=E3=81=ABTTS=E3=81=AE=E5=88=9D=E5=9B=9E=E6=94=BE?= =?UTF-8?q?=E9=80=81=E3=82=92=E5=86=8D=E7=99=BA=E7=81=AB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012dhifdRFT6ZRWghyvrDFzk --- src/hooks/useSimulationMode.test.tsx | 26 ++++++++++++++++++++++++-- src/hooks/useSimulationMode.ts | 5 +++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSimulationMode.test.tsx b/src/hooks/useSimulationMode.test.tsx index 1feefa82e..922269a31 100644 --- a/src/hooks/useSimulationMode.test.tsx +++ b/src/hooks/useSimulationMode.test.tsx @@ -38,6 +38,12 @@ jest.mock('~/store/atoms/navigation', () => ({ autoModeEnabledAtom: { toString: () => 'autoModeEnabledAtom' }, })); +jest.mock('~/store/atoms/speech', () => ({ + __esModule: true, + default: { toString: () => 'speechState' }, + resetFirstSpeechAtom: { toString: () => 'resetFirstSpeechAtom' }, +})); + jest.mock('~/store', () => ({ store: { get: jest.fn(() => null), @@ -654,8 +660,11 @@ describe('useSimulationMode', () => { .spyOn(trainSpeedModule, 'generateTrainSpeedProfile') .mockReturnValue([2000]); - (store.get as jest.Mock).mockReturnValue( - mockLocationObject(35.691, 139.777) + // resetFirstSpeechAtom は数値、それ以外(locationAtom)は位置オブジェクトを返す + (store.get as jest.Mock).mockImplementation((atom) => + atom?.toString?.() === 'resetFirstSpeechAtom' + ? 0 + : mockLocationObject(35.691, 139.777) ); renderHook(() => useSimulationMode(), { @@ -669,6 +678,12 @@ describe('useSimulationMode', () => { (call) => call[0]?.toString?.() === 'selectedDirectionAtom' ); expect(directionSetCalls).toHaveLength(0); + // 折り返す前は初回放送の再発火も起きない + expect( + (store.set as jest.Mock).mock.calls.filter( + (call) => call[0]?.toString?.() === 'resetFirstSpeechAtom' + ) + ).toHaveLength(0); // さらに進めて待機時間を超過させると方面(selectedDirection/selectedBound)が逆転する jest.advanceTimersByTime(40000); @@ -685,6 +700,13 @@ describe('useSimulationMode', () => { (call) => call[0]?.toString?.() === 'selectedBoundAtom' ); expect(boundSetCalls.length).toBeGreaterThanOrEqual(1); + + // 折り返し時に初回放送(firstSpeech)が再発火する(resetFirstSpeechをインクリメント) + const resetFirstSpeechCalls = (store.set as jest.Mock).mock.calls.filter( + (call) => call[0]?.toString?.() === 'resetFirstSpeechAtom' + ); + expect(resetFirstSpeechCalls.length).toBeGreaterThanOrEqual(1); + expect(resetFirstSpeechCalls[0][1]).toBe(1); }); it('ループ線では終点でも方面を逆転せず先頭に戻って周回を続ける', () => { diff --git a/src/hooks/useSimulationMode.ts b/src/hooks/useSimulationMode.ts index 57ea549a2..9ee5ca796 100644 --- a/src/hooks/useSimulationMode.ts +++ b/src/hooks/useSimulationMode.ts @@ -11,6 +11,7 @@ import { GET_TRAIN_ROUTE } from '~/lib/graphql/queries'; import { store } from '~/store'; import { locationAtom } from '~/store/atoms/location'; import { autoModeEnabledAtom } from '~/store/atoms/navigation'; +import { resetFirstSpeechAtom } from '~/store/atoms/speech'; import { generateTrainSpeedProfile } from '~/utils/trainSpeed'; import { selectedBoundAtom, @@ -520,6 +521,10 @@ export const useSimulationMode = (): void => { // 折り返し後の行き先(selectedBound)は現在の始発駅にあたる先頭要素。 store.set(selectedBoundAtom, maybeRevsersedStations[0] ?? null); store.set(selectedDirectionAtom, reversedDirection); + // 折り返し後は新しい行き先として初回放送(この電車は〜行きです)を再発火させる。 + // resetFirstSpeechAtom を進めると useTTS 側で firstSpeech が true に戻り、 + // 行き先変更 + 発車後(arrived=false)に初回TTSが改めて再生される。 + store.set(resetFirstSpeechAtom, store.get(resetFirstSpeechAtom) + 1); return; } From 076a2324dc5cb8dbfc079dcc1865aaeef673a1c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 15:15:44 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E6=8A=98=E3=82=8A=E8=BF=94=E3=81=97?= =?UTF-8?q?=E6=99=82=E3=81=AE=E5=88=9D=E5=9B=9E=E6=94=BE=E9=80=81=E5=86=8D?= =?UTF-8?q?=E7=99=BA=E7=81=AB=E3=83=86=E3=82=B9=E3=83=88=E3=81=A7=E7=8F=BE?= =?UTF-8?q?=E5=9C=A8=E5=80=A4+1=E3=81=A8=E5=8D=98=E4=B8=80=E7=99=BA?= =?UTF-8?q?=E7=81=AB=E3=82=92=E6=A4=9C=E8=A8=BC=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012dhifdRFT6ZRWghyvrDFzk --- src/hooks/useSimulationMode.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hooks/useSimulationMode.test.tsx b/src/hooks/useSimulationMode.test.tsx index 922269a31..41d7d3301 100644 --- a/src/hooks/useSimulationMode.test.tsx +++ b/src/hooks/useSimulationMode.test.tsx @@ -660,10 +660,11 @@ describe('useSimulationMode', () => { .spyOn(trainSpeedModule, 'generateTrainSpeedProfile') .mockReturnValue([2000]); - // resetFirstSpeechAtom は数値、それ以外(locationAtom)は位置オブジェクトを返す + // resetFirstSpeechAtom は非ゼロの数値、それ以外(locationAtom)は位置オブジェクトを返す。 + // 非ゼロ(3)にすることで「現在値 + 1」を読んでいることを検証できる(固定値1だと通ってしまう)。 (store.get as jest.Mock).mockImplementation((atom) => atom?.toString?.() === 'resetFirstSpeechAtom' - ? 0 + ? 3 : mockLocationObject(35.691, 139.777) ); @@ -701,12 +702,13 @@ describe('useSimulationMode', () => { ); expect(boundSetCalls.length).toBeGreaterThanOrEqual(1); - // 折り返し時に初回放送(firstSpeech)が再発火する(resetFirstSpeechをインクリメント) + // 折り返し時に初回放送(firstSpeech)が再発火する(resetFirstSpeechをインクリメント)。 + // 現在値3 + 1 = 4 が設定され、二重発火せず1回だけ呼ばれることを検証する。 const resetFirstSpeechCalls = (store.set as jest.Mock).mock.calls.filter( (call) => call[0]?.toString?.() === 'resetFirstSpeechAtom' ); - expect(resetFirstSpeechCalls.length).toBeGreaterThanOrEqual(1); - expect(resetFirstSpeechCalls[0][1]).toBe(1); + expect(resetFirstSpeechCalls).toHaveLength(1); + expect(resetFirstSpeechCalls[0][1]).toBe(4); }); it('ループ線では終点でも方面を逆転せず先頭に戻って周回を続ける', () => {