diff --git a/src/hooks/useSimulationMode.test.tsx b/src/hooks/useSimulationMode.test.tsx index 1feefa82e..41d7d3301 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,12 @@ describe('useSimulationMode', () => { .spyOn(trainSpeedModule, 'generateTrainSpeedProfile') .mockReturnValue([2000]); - (store.get as jest.Mock).mockReturnValue( - mockLocationObject(35.691, 139.777) + // resetFirstSpeechAtom は非ゼロの数値、それ以外(locationAtom)は位置オブジェクトを返す。 + // 非ゼロ(3)にすることで「現在値 + 1」を読んでいることを検証できる(固定値1だと通ってしまう)。 + (store.get as jest.Mock).mockImplementation((atom) => + atom?.toString?.() === 'resetFirstSpeechAtom' + ? 3 + : mockLocationObject(35.691, 139.777) ); renderHook(() => useSimulationMode(), { @@ -669,6 +679,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 +701,14 @@ describe('useSimulationMode', () => { (call) => call[0]?.toString?.() === 'selectedBoundAtom' ); expect(boundSetCalls.length).toBeGreaterThanOrEqual(1); + + // 折り返し時に初回放送(firstSpeech)が再発火する(resetFirstSpeechをインクリメント)。 + // 現在値3 + 1 = 4 が設定され、二重発火せず1回だけ呼ばれることを検証する。 + const resetFirstSpeechCalls = (store.set as jest.Mock).mock.calls.filter( + (call) => call[0]?.toString?.() === 'resetFirstSpeechAtom' + ); + expect(resetFirstSpeechCalls).toHaveLength(1); + expect(resetFirstSpeechCalls[0][1]).toBe(4); }); 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; }