11import { ReactNode } from 'react' ;
22
33import { NavigationContainer } from '@react-navigation/native' ;
4- import { fireEvent , render , waitFor } from '@testing-library/react-native' ;
4+ import { act , fireEvent , render , waitFor } from '@testing-library/react-native' ;
55
66import { ConfirmCode } from '@/screens/auth/password/ConfirmCode' ;
77import { resendResetOtp , verifyResetCode } from '@/services/auth' ;
@@ -39,9 +39,14 @@ const mockResendResetOtp = resendResetOtp as jest.MockedFunction<typeof resendRe
3939describe ( 'ConfirmCode Component' , ( ) => {
4040 beforeEach ( ( ) => {
4141 jest . clearAllMocks ( ) ;
42+ jest . useFakeTimers ( ) ;
4243 } ) ;
4344
44- it ( 'renders correctly with initial state' , ( ) => {
45+ afterEach ( ( ) => {
46+ jest . useRealTimers ( ) ;
47+ } ) ;
48+
49+ it ( 'renders correctly with initial state and timer' , ( ) => {
4550 const { getByText, getByTestId } = render (
4651 < NavigationContainer >
4752 < ConfirmCode />
@@ -51,7 +56,8 @@ describe('ConfirmCode Component', () => {
5156 expect ( getByText ( 'We sent you a code' ) ) . toBeTruthy ( ) ;
5257 expect ( getByTestId ( 'text-input' ) ) . toBeTruthy ( ) ;
5358 expect ( getByText ( 'Confirm' ) ) . toBeTruthy ( ) ;
54- expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
59+ // Timer should be shown on initial load
60+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
5561 } ) ;
5662
5763 it ( 'validates code input' , ( ) => {
@@ -126,18 +132,27 @@ describe('ConfirmCode Component', () => {
126132 } ) ;
127133 } ) ;
128134
129- it ( 'handles resend OTP successfully' , async ( ) => {
135+ it ( 'handles resend OTP successfully and resets timer ' , async ( ) => {
130136 mockResendResetOtp . mockResolvedValue ( {
131137 success : true ,
132138 message : 'A new code has been sent' ,
133139 } ) ;
134140
135- const { getByText } = render (
141+ const { getByText, queryByText } = render (
136142 < NavigationContainer >
137143 < ConfirmCode />
138144 </ NavigationContainer >
139145 ) ;
140146
147+ // Wait for timer to expire so resend button appears
148+ act ( ( ) => {
149+ jest . advanceTimersByTime ( 60000 ) ;
150+ } ) ;
151+
152+ await waitFor ( ( ) => {
153+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
154+ } ) ;
155+
141156 const resendButton = getByText ( 'Resend' ) ;
142157 fireEvent . press ( resendButton ) ;
143158
@@ -147,6 +162,10 @@ describe('ConfirmCode Component', () => {
147162 } ) ;
148163 expect ( getByText ( 'A new code has been sent to your email' ) ) . toBeTruthy ( ) ;
149164 } ) ;
165+
166+ // Timer should be reset to 60s after successful resend
167+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
168+ expect ( queryByText ( / ^ R e s e n d $ / ) ) . toBeNull ( ) ;
150169 } ) ;
151170
152171 it ( 'handles resend OTP error' , async ( ) => {
@@ -161,6 +180,15 @@ describe('ConfirmCode Component', () => {
161180 </ NavigationContainer >
162181 ) ;
163182
183+ // Wait for timer to expire so resend button appears
184+ act ( ( ) => {
185+ jest . advanceTimersByTime ( 60000 ) ;
186+ } ) ;
187+
188+ await waitFor ( ( ) => {
189+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
190+ } ) ;
191+
164192 const resendButton = getByText ( 'Resend' ) ;
165193 fireEvent . press ( resendButton ) ;
166194
@@ -205,13 +233,28 @@ describe('ConfirmCode Component', () => {
205233 </ NavigationContainer >
206234 ) ;
207235
236+ // Wait for timer to expire so resend button appears
237+ act ( ( ) => {
238+ jest . advanceTimersByTime ( 60000 ) ;
239+ } ) ;
240+
241+ await waitFor ( ( ) => {
242+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
243+ } ) ;
244+
208245 const resendButton = getByText ( 'Resend' ) ;
209246 fireEvent . press ( resendButton ) ;
210247
211248 expect ( getByText ( 'Sending...' ) ) . toBeTruthy ( ) ;
212249
250+ // Advance timers to resolve the promise
251+ act ( ( ) => {
252+ jest . advanceTimersByTime ( 100 ) ;
253+ } ) ;
254+
213255 await waitFor ( ( ) => {
214- expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ; // Button text should return to normal
256+ // After successful resend, timer should be shown instead of Resend button
257+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
215258 } ) ;
216259 } ) ;
217260
@@ -259,4 +302,101 @@ describe('ConfirmCode Component', () => {
259302
260303 expect ( mockGoBack ) . toHaveBeenCalled ( ) ;
261304 } ) ;
305+
306+ it ( 'timer starts on screen load and prevents immediate resend' , async ( ) => {
307+ const { getByText, queryByText } = render (
308+ < NavigationContainer >
309+ < ConfirmCode />
310+ </ NavigationContainer >
311+ ) ;
312+
313+ // Timer should be shown immediately on load
314+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
315+ expect ( queryByText ( / ^ R e s e n d $ / ) ) . toBeNull ( ) ;
316+
317+ // resendResetOtp should not have been called
318+ expect ( mockResendResetOtp ) . not . toHaveBeenCalled ( ) ;
319+ } ) ;
320+
321+ it ( 'counts down the timer correctly from initial load' , async ( ) => {
322+ const { getByText } = render (
323+ < NavigationContainer >
324+ < ConfirmCode />
325+ </ NavigationContainer >
326+ ) ;
327+
328+ // Timer should start at 60s on load
329+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
330+
331+ // Advance timer by 1 second
332+ act ( ( ) => {
333+ jest . advanceTimersByTime ( 1000 ) ;
334+ } ) ;
335+
336+ await waitFor ( ( ) => {
337+ expect ( getByText ( 'Resend code in 59s' ) ) . toBeTruthy ( ) ;
338+ } ) ;
339+
340+ // Advance timer by 10 more seconds
341+ act ( ( ) => {
342+ jest . advanceTimersByTime ( 10000 ) ;
343+ } ) ;
344+
345+ await waitFor ( ( ) => {
346+ expect ( getByText ( 'Resend code in 49s' ) ) . toBeTruthy ( ) ;
347+ } ) ;
348+ } ) ;
349+
350+ it ( 'shows resend button after timer expires on initial load' , async ( ) => {
351+ const { getByText } = render (
352+ < NavigationContainer >
353+ < ConfirmCode />
354+ </ NavigationContainer >
355+ ) ;
356+
357+ // Timer should start at 60s
358+ expect ( getByText ( 'Resend code in 60s' ) ) . toBeTruthy ( ) ;
359+
360+ // Advance timer by 60 seconds to expire the timer
361+ act ( ( ) => {
362+ jest . advanceTimersByTime ( 60000 ) ;
363+ } ) ;
364+
365+ await waitFor ( ( ) => {
366+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
367+ } ) ;
368+ } ) ;
369+
370+ it ( 'does not start timer on failed resend' , async ( ) => {
371+ mockResendResetOtp . mockResolvedValue ( {
372+ success : false ,
373+ message : 'Failed to resend code' ,
374+ } ) ;
375+
376+ const { getByText, queryByText } = render (
377+ < NavigationContainer >
378+ < ConfirmCode />
379+ </ NavigationContainer >
380+ ) ;
381+
382+ // Wait for initial timer to expire
383+ act ( ( ) => {
384+ jest . advanceTimersByTime ( 60000 ) ;
385+ } ) ;
386+
387+ await waitFor ( ( ) => {
388+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
389+ } ) ;
390+
391+ const resendButton = getByText ( 'Resend' ) ;
392+ fireEvent . press ( resendButton ) ;
393+
394+ await waitFor ( ( ) => {
395+ expect ( getByText ( 'Failed to resend code' ) ) . toBeTruthy ( ) ;
396+ } ) ;
397+
398+ // Timer should not restart on failure - resend button should still be available
399+ expect ( queryByText ( / R e s e n d c o d e i n / ) ) . toBeNull ( ) ;
400+ expect ( getByText ( 'Resend' ) ) . toBeTruthy ( ) ;
401+ } ) ;
262402} ) ;
0 commit comments