|
1 | 1 | import { renderHook } from '@testing-library/react-native'; |
2 | 2 | import * as Location from 'expo-location'; |
3 | | -import { LOCATION_TASK_NAME, LOCATION_TASK_OPTIONS } from '../constants'; |
| 3 | +import { |
| 4 | + LOCATION_START_MAX_RETRIES, |
| 5 | + LOCATION_TASK_NAME, |
| 6 | + LOCATION_TASK_OPTIONS, |
| 7 | +} from '../constants'; |
4 | 8 | import { useLocationPermissionsGranted } from './useLocationPermissionsGranted'; |
5 | 9 | import { useStartBackgroundLocationUpdates } from './useStartBackgroundLocationUpdates'; |
6 | 10 |
|
@@ -98,27 +102,157 @@ describe('useStartBackgroundLocationUpdates', () => { |
98 | 102 | ); |
99 | 103 | }); |
100 | 104 |
|
101 | | - test('should handle startLocationUpdatesAsync error gracefully', async () => { |
| 105 | + test('should retry startLocationUpdatesAsync on failure and succeed', async () => { |
| 106 | + jest.useFakeTimers(); |
| 107 | + const consoleWarnSpy = jest |
| 108 | + .spyOn(console, 'warn') |
| 109 | + .mockImplementation(() => {}); |
| 110 | + mockStartLocationUpdatesAsync |
| 111 | + .mockRejectedValueOnce(new Error('Temporary failure')) |
| 112 | + .mockResolvedValueOnce(undefined); |
| 113 | + mockAutoModeEnabled = false; |
| 114 | + mockUseLocationPermissionsGranted.mockReturnValue(true); |
| 115 | + |
| 116 | + renderHook(() => useStartBackgroundLocationUpdates()); |
| 117 | + |
| 118 | + // 初回失敗 |
| 119 | + await jest.advanceTimersByTimeAsync(0); |
| 120 | + |
| 121 | + expect(consoleWarnSpy).toHaveBeenCalledWith( |
| 122 | + `バックグラウンド位置情報の更新開始に失敗しました(リトライ 1/${LOCATION_START_MAX_RETRIES}):`, |
| 123 | + expect.any(Error) |
| 124 | + ); |
| 125 | + |
| 126 | + // リトライ待機(1000ms)後に成功 |
| 127 | + await jest.advanceTimersByTimeAsync(1000); |
| 128 | + |
| 129 | + expect(mockStartLocationUpdatesAsync).toHaveBeenCalledTimes(2); |
| 130 | + |
| 131 | + consoleWarnSpy.mockRestore(); |
| 132 | + jest.useRealTimers(); |
| 133 | + }); |
| 134 | + |
| 135 | + test('should log final error after all retries exhausted', async () => { |
| 136 | + jest.useFakeTimers(); |
102 | 137 | const consoleWarnSpy = jest |
103 | 138 | .spyOn(console, 'warn') |
104 | 139 | .mockImplementation(() => {}); |
105 | 140 | mockStartLocationUpdatesAsync.mockRejectedValue( |
106 | | - new Error('Permission denied') |
| 141 | + new Error('Persistent failure') |
107 | 142 | ); |
108 | 143 | mockAutoModeEnabled = false; |
109 | 144 | mockUseLocationPermissionsGranted.mockReturnValue(true); |
110 | 145 |
|
111 | 146 | renderHook(() => useStartBackgroundLocationUpdates()); |
112 | 147 |
|
| 148 | + // 初回 + リトライ3回分すべて実行 |
| 149 | + for (let i = 0; i <= LOCATION_START_MAX_RETRIES; i++) { |
| 150 | + await jest.advanceTimersByTimeAsync(1000 * 2 ** i); |
| 151 | + } |
| 152 | + |
| 153 | + expect(mockStartLocationUpdatesAsync).toHaveBeenCalledTimes( |
| 154 | + LOCATION_START_MAX_RETRIES + 1 |
| 155 | + ); |
| 156 | + expect(consoleWarnSpy).toHaveBeenLastCalledWith( |
| 157 | + 'バックグラウンド位置情報の更新開始に失敗しました(リトライ上限到達):', |
| 158 | + expect.any(Error) |
| 159 | + ); |
| 160 | + |
| 161 | + consoleWarnSpy.mockRestore(); |
| 162 | + jest.useRealTimers(); |
| 163 | + }); |
| 164 | + |
| 165 | + test('should stop location updates if cancelled during startLocationUpdatesAsync', async () => { |
| 166 | + mockAutoModeEnabled = false; |
| 167 | + mockUseLocationPermissionsGranted.mockReturnValue(true); |
| 168 | + |
| 169 | + // startLocationUpdatesAsyncが解決する前にunmountされるケースをシミュレート |
| 170 | + let resolveStart: () => void = () => {}; |
| 171 | + mockStartLocationUpdatesAsync.mockImplementation( |
| 172 | + () => |
| 173 | + new Promise<void>((resolve) => { |
| 174 | + resolveStart = resolve; |
| 175 | + }) |
| 176 | + ); |
| 177 | + |
| 178 | + const { unmount } = renderHook(() => useStartBackgroundLocationUpdates()); |
| 179 | + |
| 180 | + // startが進行中の間にクリーンアップ |
| 181 | + unmount(); |
| 182 | + |
| 183 | + // startが完了 |
| 184 | + resolveStart(); |
113 | 185 | await new Promise(process.nextTick); |
114 | 186 |
|
| 187 | + // クリーンアップのstop + cancelled検知後のstopで2回呼ばれる |
| 188 | + expect(mockStopLocationUpdatesAsync).toHaveBeenCalledTimes(2); |
| 189 | + expect(mockStopLocationUpdatesAsync).toHaveBeenCalledWith( |
| 190 | + LOCATION_TASK_NAME |
| 191 | + ); |
| 192 | + }); |
| 193 | + |
| 194 | + test('should not retry when cancelled stop fails after successful start', async () => { |
| 195 | + const consoleWarnSpy = jest |
| 196 | + .spyOn(console, 'warn') |
| 197 | + .mockImplementation(() => {}); |
| 198 | + mockAutoModeEnabled = false; |
| 199 | + mockUseLocationPermissionsGranted.mockReturnValue(true); |
| 200 | + |
| 201 | + let resolveStart: () => void = () => {}; |
| 202 | + mockStartLocationUpdatesAsync.mockImplementation( |
| 203 | + () => |
| 204 | + new Promise<void>((resolve) => { |
| 205 | + resolveStart = resolve; |
| 206 | + }) |
| 207 | + ); |
| 208 | + mockStopLocationUpdatesAsync.mockRejectedValue(new Error('Stop failed')); |
| 209 | + |
| 210 | + const { unmount } = renderHook(() => useStartBackgroundLocationUpdates()); |
| 211 | + |
| 212 | + // startが進行中の間にクリーンアップ |
| 213 | + unmount(); |
| 214 | + |
| 215 | + // startが完了 → cancelled=trueなのでstopを試みるが失敗する |
| 216 | + resolveStart(); |
| 217 | + await new Promise(process.nextTick); |
| 218 | + |
| 219 | + // startは1回だけ(stop失敗でリトライされない) |
| 220 | + expect(mockStartLocationUpdatesAsync).toHaveBeenCalledTimes(1); |
115 | 221 | expect(consoleWarnSpy).toHaveBeenCalledWith( |
116 | | - 'バックグラウンド位置情報の更新開始に失敗しました:', |
| 222 | + 'バックグラウンド位置情報の更新停止に失敗しました:', |
117 | 223 | expect.any(Error) |
118 | 224 | ); |
119 | 225 |
|
120 | 226 | consoleWarnSpy.mockRestore(); |
121 | 227 | }); |
| 228 | + |
| 229 | + test('should stop retrying on cleanup', async () => { |
| 230 | + jest.useFakeTimers(); |
| 231 | + const consoleWarnSpy = jest |
| 232 | + .spyOn(console, 'warn') |
| 233 | + .mockImplementation(() => {}); |
| 234 | + mockStartLocationUpdatesAsync.mockRejectedValue(new Error('Failure')); |
| 235 | + mockAutoModeEnabled = false; |
| 236 | + mockUseLocationPermissionsGranted.mockReturnValue(true); |
| 237 | + |
| 238 | + const { unmount } = renderHook(() => useStartBackgroundLocationUpdates()); |
| 239 | + |
| 240 | + // 初回失敗 |
| 241 | + await jest.advanceTimersByTimeAsync(0); |
| 242 | + |
| 243 | + expect(mockStartLocationUpdatesAsync).toHaveBeenCalledTimes(1); |
| 244 | + |
| 245 | + // クリーンアップでリトライを中止 |
| 246 | + unmount(); |
| 247 | + |
| 248 | + // リトライ待機時間を経過させてもこれ以上呼ばれない |
| 249 | + await jest.advanceTimersByTimeAsync(10000); |
| 250 | + |
| 251 | + expect(mockStartLocationUpdatesAsync).toHaveBeenCalledTimes(1); |
| 252 | + |
| 253 | + consoleWarnSpy.mockRestore(); |
| 254 | + jest.useRealTimers(); |
| 255 | + }); |
122 | 256 | }); |
123 | 257 |
|
124 | 258 | describe('foreground location watch (fallback)', () => { |
|
0 commit comments