1- import { render , screen , fireEvent , waitFor } from "@testing-library/react" ;
1+ import { fireEvent , render , screen , waitFor } from "@testing-library/react" ;
22
33// Mock next/router
44vi . mock ( "next/router" , ( ) => ( {
@@ -23,6 +23,7 @@ vi.mock("@/lib/challenge-runner", () => ({
2323} ) ) ;
2424
2525import { runChallenge } from "@/lib/challenge-runner" ;
26+
2627const mockRunChallenge = vi . mocked ( runChallenge ) ;
2728
2829const mockFetch = vi . fn ( ) ;
@@ -103,15 +104,16 @@ describe("ChallengesPage", () => {
103104 if ( url . includes ( "start" ) && opts ?. method === "POST" ) {
104105 return Promise . resolve ( {
105106 ok : true ,
106- json : ( ) => Promise . resolve ( {
107- id : "ch-1" ,
108- title : "FizzBuzz" ,
109- description : "Write a FizzBuzz function" ,
110- topic : "javascript" ,
111- difficulty : "easy" ,
112- language : "javascript" ,
113- starter_code : "function fizzBuzz(n) {\n // your code\n}" ,
114- } ) ,
107+ json : ( ) =>
108+ Promise . resolve ( {
109+ id : "ch-1" ,
110+ title : "FizzBuzz" ,
111+ description : "Write a FizzBuzz function" ,
112+ topic : "javascript" ,
113+ difficulty : "easy" ,
114+ language : "javascript" ,
115+ starter_code : "function fizzBuzz(n) {\n // your code\n}" ,
116+ } ) ,
115117 } ) ;
116118 }
117119 return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
@@ -155,27 +157,29 @@ describe("ChallengesPage", () => {
155157 if ( url . includes ( "start" ) && opts ?. method === "POST" ) {
156158 return Promise . resolve ( {
157159 ok : true ,
158- json : ( ) => Promise . resolve ( {
159- id : "ch-2" ,
160- title : "Sum Array" ,
161- description : "Sum all elements" ,
162- topic : "javascript" ,
163- difficulty : "easy" ,
164- language : "javascript" ,
165- test_cases : [
166- { input : "[1, 2, 3]" , expected_output : "6" , hidden : false } ,
167- ] ,
168- } ) ,
160+ json : ( ) =>
161+ Promise . resolve ( {
162+ id : "ch-2" ,
163+ title : "Sum Array" ,
164+ description : "Sum all elements" ,
165+ topic : "javascript" ,
166+ difficulty : "easy" ,
167+ language : "javascript" ,
168+ test_cases : [
169+ { input : "[1, 2, 3]" , expected_output : "6" , hidden : false } ,
170+ ] ,
171+ } ) ,
169172 } ) ;
170173 }
171174 if ( url . includes ( "submit" ) && opts ?. method === "POST" ) {
172175 return Promise . resolve ( {
173176 ok : true ,
174- json : ( ) => Promise . resolve ( {
175- passed : true ,
176- score : 100 ,
177- feedback : "Perfect solution!" ,
178- } ) ,
177+ json : ( ) =>
178+ Promise . resolve ( {
179+ passed : true ,
180+ score : 100 ,
181+ feedback : "Perfect solution!" ,
182+ } ) ,
179183 } ) ;
180184 }
181185 return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
@@ -200,8 +204,8 @@ describe("ChallengesPage", () => {
200204 } ) ;
201205
202206 // The submit POST must include the runner's structured client_results.
203- const submitCall = mockFetch . mock . calls . find ( ( [ url , opts ] ) =>
204- String ( url ) . includes ( "submit" ) && opts ?. method === "POST"
207+ const submitCall = mockFetch . mock . calls . find (
208+ ( [ url , opts ] ) => String ( url ) . includes ( "submit" ) && opts ?. method === "POST"
205209 ) ;
206210 expect ( submitCall ) . toBeDefined ( ) ;
207211 const body = JSON . parse ( submitCall ! [ 1 ] . body ) ;
@@ -215,16 +219,17 @@ describe("ChallengesPage", () => {
215219 if ( url . includes ( "recommended" ) ) {
216220 return Promise . resolve ( {
217221 ok : true ,
218- json : ( ) => Promise . resolve ( [
219- {
220- id : "rec-1" ,
221- title : "Binary Search" ,
222- description : "Implement binary search" ,
223- topic : "algorithms" ,
224- difficulty : "medium" ,
225- language : "javascript" ,
226- } ,
227- ] ) ,
222+ json : ( ) =>
223+ Promise . resolve ( [
224+ {
225+ id : "rec-1" ,
226+ title : "Binary Search" ,
227+ description : "Implement binary search" ,
228+ topic : "algorithms" ,
229+ difficulty : "medium" ,
230+ language : "javascript" ,
231+ } ,
232+ ] ) ,
228233 } ) ;
229234 }
230235 return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
@@ -244,26 +249,27 @@ describe("ChallengesPage", () => {
244249 if ( url . includes ( "history" ) ) {
245250 return Promise . resolve ( {
246251 ok : true ,
247- json : ( ) => Promise . resolve ( [
248- {
249- id : "att-1" ,
250- challenge_id : "ch-1" ,
251- title : "Two Sum" ,
252- topic : "arrays" ,
253- difficulty : "easy" ,
254- passed : true ,
255- submitted_at : "2026-04-01T10:00:00Z" ,
256- } ,
257- {
258- id : "att-2" ,
259- challenge_id : "ch-2" ,
260- title : "Merge Sort" ,
261- topic : "algorithms" ,
262- difficulty : "hard" ,
263- passed : false ,
264- submitted_at : "2026-04-02T10:00:00Z" ,
265- } ,
266- ] ) ,
252+ json : ( ) =>
253+ Promise . resolve ( [
254+ {
255+ id : "att-1" ,
256+ challenge_id : "ch-1" ,
257+ title : "Two Sum" ,
258+ topic : "arrays" ,
259+ difficulty : "easy" ,
260+ passed : true ,
261+ submitted_at : "2026-04-01T10:00:00Z" ,
262+ } ,
263+ {
264+ id : "att-2" ,
265+ challenge_id : "ch-2" ,
266+ title : "Merge Sort" ,
267+ topic : "algorithms" ,
268+ difficulty : "hard" ,
269+ passed : false ,
270+ submitted_at : "2026-04-02T10:00:00Z" ,
271+ } ,
272+ ] ) ,
267273 } ) ;
268274 }
269275 return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
@@ -297,19 +303,117 @@ describe("ChallengesPage", () => {
297303 } ) ;
298304 } ) ;
299305
306+ it ( "shows an error with retry when the recommended fetch rejects" , async ( ) => {
307+ const consoleSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
308+ mockFetch . mockImplementation ( ( url : string ) => {
309+ if ( url . includes ( "recommended" ) ) {
310+ return Promise . reject ( new TypeError ( "Failed to fetch" ) ) ;
311+ }
312+ return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
313+ } ) ;
314+
315+ render ( < ChallengesPage /> ) ;
316+
317+ await waitFor ( ( ) => {
318+ expect ( screen . getByText ( "Failed to fetch" ) ) . toBeInTheDocument ( ) ;
319+ } ) ;
320+ expect ( screen . getByText ( "Retry" ) ) . toBeInTheDocument ( ) ;
321+
322+ // Retry refetches and renders the recovered list
323+ mockFetch . mockImplementation ( ( url : string ) => {
324+ if ( url . includes ( "recommended" ) ) {
325+ return Promise . resolve ( {
326+ ok : true ,
327+ json : ( ) =>
328+ Promise . resolve ( [
329+ {
330+ id : "rec-1" ,
331+ title : "Binary Search" ,
332+ topic : "algorithms" ,
333+ difficulty : "medium" ,
334+ } ,
335+ ] ) ,
336+ } ) ;
337+ }
338+ return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
339+ } ) ;
340+
341+ fireEvent . click ( screen . getByText ( "Retry" ) ) ;
342+
343+ await waitFor ( ( ) => {
344+ expect ( screen . getByText ( "Binary Search" ) ) . toBeInTheDocument ( ) ;
345+ } ) ;
346+ consoleSpy . mockRestore ( ) ;
347+ } ) ;
348+
349+ it ( "shows an error when the history request returns non-ok" , async ( ) => {
350+ const consoleSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
351+ mockFetch . mockImplementation ( ( url : string ) => {
352+ if ( url . includes ( "history" ) ) {
353+ return Promise . resolve ( { ok : false , json : ( ) => Promise . resolve ( { } ) } ) ;
354+ }
355+ return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
356+ } ) ;
357+
358+ render ( < ChallengesPage /> ) ;
359+
360+ await waitFor ( ( ) => {
361+ expect ( screen . getByText ( "Failed to load your history" ) ) . toBeInTheDocument ( ) ;
362+ } ) ;
363+ consoleSpy . mockRestore ( ) ;
364+ } ) ;
365+
366+ it ( "surfaces an error when fetching a hint fails" , async ( ) => {
367+ const consoleSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
368+ mockFetch . mockImplementation ( ( url : string , opts ?: any ) => {
369+ if ( url . includes ( "start" ) && opts ?. method === "POST" ) {
370+ return Promise . resolve ( {
371+ ok : true ,
372+ json : ( ) =>
373+ Promise . resolve ( {
374+ id : "ch-1" ,
375+ title : "Test" ,
376+ description : "Test desc" ,
377+ topic : "js" ,
378+ difficulty : "easy" ,
379+ language : "javascript" ,
380+ } ) ,
381+ } ) ;
382+ }
383+ if ( url . includes ( "hint" ) ) {
384+ return Promise . reject ( new TypeError ( "Failed to fetch" ) ) ;
385+ }
386+ return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
387+ } ) ;
388+
389+ render ( < ChallengesPage /> ) ;
390+
391+ fireEvent . click ( screen . getByText ( "Start Challenge" ) ) ;
392+
393+ await waitFor ( ( ) => screen . getByText ( "Test" ) ) ;
394+
395+ fireEvent . click ( screen . getByText ( "Get Hint (0 used)" ) ) ;
396+
397+ await waitFor ( ( ) => {
398+ expect ( screen . getByText ( "Failed to fetch" ) ) . toBeInTheDocument ( ) ;
399+ } ) ;
400+ consoleSpy . mockRestore ( ) ;
401+ } ) ;
402+
300403 it ( "navigates back from active challenge" , async ( ) => {
301404 mockFetch . mockImplementation ( ( url : string , opts ?: any ) => {
302405 if ( url . includes ( "start" ) && opts ?. method === "POST" ) {
303406 return Promise . resolve ( {
304407 ok : true ,
305- json : ( ) => Promise . resolve ( {
306- id : "ch-1" ,
307- title : "Test" ,
308- description : "Test desc" ,
309- topic : "js" ,
310- difficulty : "easy" ,
311- language : "javascript" ,
312- } ) ,
408+ json : ( ) =>
409+ Promise . resolve ( {
410+ id : "ch-1" ,
411+ title : "Test" ,
412+ description : "Test desc" ,
413+ topic : "js" ,
414+ difficulty : "easy" ,
415+ language : "javascript" ,
416+ } ) ,
313417 } ) ;
314418 }
315419 return Promise . resolve ( { ok : true , json : ( ) => Promise . resolve ( [ ] ) } ) ;
0 commit comments