@@ -2,7 +2,7 @@ import { screen, waitFor } from '@testing-library/react';
22import userEvent from '@testing-library/user-event' ;
33import { AxiosError } from 'axios' ;
44import Attempts from './Attempts' ;
5- import { useAttempts , useResetAttempt } from '../data/apiHook' ;
5+ import { useAttempts , useResetAttempt , useResumeAttempt } from '../data/apiHook' ;
66import { Attempt } from '../types' ;
77import { renderWithAlertAndIntl } from '@src/testUtils' ;
88import messages from '../messages' ;
@@ -15,44 +15,86 @@ jest.mock('react-router-dom', () => ({
1515jest . mock ( '../data/apiHook' , ( ) => ( {
1616 useAttempts : jest . fn ( ) ,
1717 useResetAttempt : jest . fn ( ) ,
18+ useResumeAttempt : jest . fn ( ) ,
1819} ) ) ;
1920
20- const mockAttempt : Attempt = {
21+ const mockAttempts : Attempt [ ] = [ {
2122 id : 1 ,
22- user : { username : 'testuser' } ,
23+ user : { username : 'testuser' , id : 1 } ,
2324 examId : 42 ,
2425 examName : 'Midterm Exam' ,
2526 allowedTimeLimitMins : 60 ,
26- type : 'proctored' ,
27+ examType : 'proctored' ,
2728 startTime : '2024-01-01T00:00:00Z' ,
2829 endTime : '2024-01-01T01:00:00Z' ,
2930 status : 'started' ,
3031 readyToResume : false ,
31- } ;
32+ } , {
33+ id : 2 ,
34+ user : { username : 'testuser2' , id : 2 } ,
35+ examId : 43 ,
36+ examName : 'Final Exam' ,
37+ allowedTimeLimitMins : 120 ,
38+ examType : 'proctored' ,
39+ startTime : '2024-01-02T00:00:00Z' ,
40+ endTime : null ,
41+ status : 'error' ,
42+ readyToResume : false ,
43+ } ,
44+ {
45+ id : 3 ,
46+ user : { username : 'testuser3' , id : 3 } ,
47+ examId : 44 ,
48+ examName : 'Quiz' ,
49+ allowedTimeLimitMins : 30 ,
50+ examType : 'timed' ,
51+ startTime : '2024-01-03T00:00:00Z' ,
52+ endTime : null ,
53+ status : 'error' ,
54+ readyToResume : true ,
55+ }
56+ ] ;
3257
3358describe ( 'Attempts' , ( ) => {
3459 const mockResetAttempt = jest . fn ( ) ;
60+ const mockResumeAttempt = jest . fn ( ) ;
3561
3662 beforeEach ( ( ) => {
3763 jest . clearAllMocks ( ) ;
3864 ( useResetAttempt as jest . Mock ) . mockReturnValue ( { mutate : mockResetAttempt } ) ;
65+ ( useResumeAttempt as jest . Mock ) . mockReturnValue ( { mutate : mockResumeAttempt } ) ;
3966 ( useAttempts as jest . Mock ) . mockReturnValue ( {
40- data : { results : [ mockAttempt ] } ,
67+ data : { results : mockAttempts } ,
4168 isLoading : false ,
4269 isError : false ,
4370 } ) ;
4471 } ) ;
4572
46- const clickReset = async ( ) => {
47- const actionsButton = screen . getByRole ( 'button' , { name : messages . actions . defaultMessage } ) ;
48- await userEvent . click ( actionsButton ) ;
73+ const clickReset = async ( index = 0 ) => {
74+ const actionsButton = screen . getAllByRole ( 'button' , { name : messages . actions . defaultMessage } ) ;
75+ await userEvent . click ( actionsButton [ index ] ) ;
4976 const resetButton = screen . getByRole ( 'button' , { name : messages . reset . defaultMessage } ) ;
5077 await userEvent . click ( resetButton ) ;
78+ const confirmationModal = screen . getByRole ( 'dialog' , { name : messages . confirmationModal . defaultMessage } ) ;
79+ expect ( confirmationModal ) . toBeInTheDocument ( ) ;
80+ const confirmReset = screen . getByRole ( 'button' , { name : messages . reset . defaultMessage } ) ;
81+ await userEvent . click ( confirmReset ) ;
82+ } ;
83+
84+ const clickResume = async ( index = 0 ) => {
85+ const actionsButton = screen . getAllByRole ( 'button' , { name : messages . actions . defaultMessage } ) ;
86+ await userEvent . click ( actionsButton [ index ] ) ;
87+ const resumeButton = screen . getByRole ( 'button' , { name : messages . resume . defaultMessage } ) ;
88+ await userEvent . click ( resumeButton ) ;
89+ const confirmationModal = screen . getByRole ( 'dialog' , { name : messages . confirmationModal . defaultMessage } ) ;
90+ expect ( confirmationModal ) . toBeInTheDocument ( ) ;
91+ const confirmResume = screen . getByRole ( 'button' , { name : messages . resume . defaultMessage } ) ;
92+ await userEvent . click ( confirmResume ) ;
5193 } ;
5294
5395 it ( 'renders AttemptsList component' , ( ) => {
5496 renderWithAlertAndIntl ( < Attempts /> ) ;
55- const usernameCell = screen . getByRole ( 'cell' , { name : mockAttempt . user . username } ) ;
97+ const usernameCell = screen . getByRole ( 'cell' , { name : mockAttempts [ 0 ] . user . username } ) ;
5698 expect ( usernameCell ) . toBeInTheDocument ( ) ;
5799 } ) ;
58100
@@ -82,7 +124,7 @@ describe('Attempts', () => {
82124 renderWithAlertAndIntl ( < Attempts /> ) ;
83125 await clickReset ( ) ;
84126
85- const successMessage = screen . getByText ( messages . successOnReset . defaultMessage . replace ( '{student}' , mockAttempt . user . username ) . replace ( '{examName}' , mockAttempt . examName ) ) ;
127+ const successMessage = screen . getByText ( messages . successOnReset . defaultMessage . replace ( '{student}' , mockAttempts [ 0 ] . user . username ) . replace ( '{examName}' , mockAttempts [ 0 ] . examName ) ) ;
86128
87129 await waitFor ( ( ) => {
88130 expect ( successMessage ) . toBeInTheDocument ( ) ;
@@ -127,4 +169,82 @@ describe('Attempts', () => {
127169 } ) ;
128170 expect ( screen . getByText ( messages . close . defaultMessage ) ) . toBeInTheDocument ( ) ;
129171 } ) ;
172+
173+ it ( 'calls useResumeAttempt with courseId from route params' , ( ) => {
174+ renderWithAlertAndIntl ( < Attempts /> ) ;
175+ expect ( useResumeAttempt ) . toHaveBeenCalledWith ( 'course-v1:edX+Test+2024' ) ;
176+ } ) ;
177+
178+ it ( 'calls resumeAttempt with attemptId and userId when onResume is triggered' , async ( ) => {
179+ renderWithAlertAndIntl ( < Attempts /> ) ;
180+ const studentIndexToTest = 1 ;
181+ await clickResume ( studentIndexToTest ) ;
182+
183+ expect ( mockResumeAttempt ) . toHaveBeenCalledWith (
184+ { attemptId : mockAttempts [ studentIndexToTest ] . id , userId : mockAttempts [ studentIndexToTest ] . user . id } ,
185+ expect . objectContaining ( {
186+ onSuccess : expect . any ( Function ) ,
187+ onError : expect . any ( Function ) ,
188+ } ) ,
189+ ) ;
190+ } ) ;
191+
192+ it ( 'shows a success toast when resume succeeds' , async ( ) => {
193+ mockResumeAttempt . mockImplementation ( ( _params , options ) => {
194+ options . onSuccess ( ) ;
195+ } ) ;
196+
197+ renderWithAlertAndIntl ( < Attempts /> ) ;
198+ const studentIndexToTest = 1 ;
199+ await clickResume ( studentIndexToTest ) ;
200+
201+ const successMessage = screen . getByText ( messages . successOnResume . defaultMessage . replace ( '{student}' , mockAttempts [ studentIndexToTest ] . user . username ) ) ;
202+
203+ await waitFor ( ( ) => {
204+ expect ( successMessage ) . toBeInTheDocument ( ) ;
205+ } ) ;
206+ } ) ;
207+
208+ it ( 'shows an error modal with API detail when resume fails with AxiosError' , async ( ) => {
209+ const axiosError = new AxiosError ( 'Request failed' ) ;
210+ axiosError . response = {
211+ data : { detail : 'Cannot resume this attempt.' } ,
212+ status : 400 ,
213+ statusText : 'Bad Request' ,
214+ headers : { } ,
215+ config : { } as any ,
216+ } ;
217+
218+ mockResumeAttempt . mockImplementation ( ( _params , options ) => {
219+ options . onError ( axiosError ) ;
220+ } ) ;
221+
222+ renderWithAlertAndIntl ( < Attempts /> ) ;
223+
224+ const studentIndexToTest = 1 ;
225+ await clickResume ( studentIndexToTest ) ;
226+
227+ await waitFor ( ( ) => {
228+ expect ( screen . getByText ( 'Cannot resume this attempt.' ) ) . toBeInTheDocument ( ) ;
229+ } ) ;
230+ expect ( screen . getByText ( messages . close . defaultMessage ) ) . toBeInTheDocument ( ) ;
231+ } ) ;
232+
233+ it ( 'shows a generic error modal when resume fails with a non-Axios error' , async ( ) => {
234+ const genericError = new Error ( 'Something went wrong' ) ;
235+
236+ mockResumeAttempt . mockImplementation ( ( _params , options ) => {
237+ options . onError ( genericError ) ;
238+ } ) ;
239+
240+ renderWithAlertAndIntl ( < Attempts /> ) ;
241+
242+ const studentIndexToTest = 1 ;
243+ await clickResume ( studentIndexToTest ) ;
244+
245+ await waitFor ( ( ) => {
246+ expect ( screen . getByText ( messages . errorOnResume . defaultMessage ) ) . toBeInTheDocument ( ) ;
247+ } ) ;
248+ expect ( screen . getByText ( messages . close . defaultMessage ) ) . toBeInTheDocument ( ) ;
249+ } ) ;
130250} ) ;
0 commit comments