1- import React , { useRef } from 'react' ;
2-
31import {
42 act , fireEvent , render , screen , waitFor , within ,
53} from '@testing-library/react' ;
64import MockAdapter from 'axios-mock-adapter' ;
5+ import { useGoogleReCaptcha } from 'react-google-recaptcha-v3' ;
76import { IntlProvider } from 'react-intl' ;
87import {
98 MemoryRouter , Route , Routes , useLocation ,
@@ -27,12 +26,6 @@ import fetchCourseConfig from '../data/thunks';
2726import DiscussionContent from '../discussions-home/DiscussionContent' ;
2827import { getThreadsApiUrl } from '../posts/data/api' ;
2928import { fetchThread , fetchThreads } from '../posts/data/thunks' ;
30- import MockReCAPTCHA , {
31- mockOnChange ,
32- mockOnError ,
33- mockOnExpired ,
34- mockReset ,
35- } from '../posts/post-editor/mocksData/react-google-recaptcha' ;
3629import fetchCourseTopics from '../topics/data/thunks' ;
3730import { getDiscussionTourUrl } from '../tours/data/api' ;
3831import selectTours from '../tours/data/selectors' ;
@@ -63,7 +56,11 @@ let testLocation;
6356let container ;
6457let unmount ;
6558
66- jest . mock ( 'react-google-recaptcha' , ( ) => MockReCAPTCHA ) ;
59+ jest . mock ( 'react-google-recaptcha-v3' , ( ) => ( {
60+ useGoogleReCaptcha : jest . fn ( ) ,
61+ // eslint-disable-next-line react/prop-types
62+ GoogleReCaptchaProvider : ( { children } ) => < div > { children } </ div > ,
63+ } ) ) ;
6764
6865async function mockAxiosReturnPagedComments ( threadId , threadType = ThreadType . DISCUSSION , page = 1 , count = 2 ) {
6966 axiosMock . onGet ( commentsApiUrl ) . reply ( 200 , Factory . build ( 'commentsResult' , { can_delete : true } , {
@@ -310,6 +307,8 @@ describe('ThreadView', () => {
310307
311308 it ( 'should show and hide the editor' , async ( ) => {
312309 await setupCourseConfig ( ) ;
310+ const mockExecuteRecaptchaNew = jest . fn ( ( ) => Promise . resolve ( 'mock-token' ) ) ;
311+ useGoogleReCaptcha . mockReturnValue ( { executeRecaptcha : mockExecuteRecaptchaNew } ) ;
313312 await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
314313
315314 const post = screen . getByTestId ( 'post-thread-1' ) ;
@@ -348,13 +347,18 @@ describe('ThreadView', () => {
348347
349348 it ( 'should allow posting a comment with CAPTCHA' , async ( ) => {
350349 await setupCourseConfig ( true , false , false ) ;
350+ const mockExecuteRecaptcha = jest . fn ( ( ) => Promise . resolve ( 'mock-token' ) ) ;
351+ useGoogleReCaptcha . mockReturnValue ( { executeRecaptcha : mockExecuteRecaptcha } ) ;
352+
351353 await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
352354
353355 const comment = await waitFor ( ( ) => screen . findByTestId ( 'comment-comment-1' ) ) ;
354356 const hoverCard = within ( comment ) . getByTestId ( 'hover-card-comment-1' ) ;
357+
355358 await act ( async ( ) => { fireEvent . click ( within ( hoverCard ) . getByRole ( 'button' , { name : / A d d c o m m e n t / i } ) ) ; } ) ;
359+
360+ await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
356361 await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'New comment with CAPTCHA' } } ) ; } ) ;
357- await act ( async ( ) => { fireEvent . click ( screen . getByText ( 'Solve CAPTCHA' ) ) ; } ) ;
358362 await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
359363
360364 await waitFor ( ( ) => {
@@ -366,10 +370,43 @@ describe('ThreadView', () => {
366370 raw_body : 'New comment with CAPTCHA' ,
367371 thread_id : 'thread-1' ,
368372 } ) ;
369- expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
370373 } ) ;
371374 } ) ;
372375
376+ it ( 'should show captcha error if executeRecaptcha returns null token' , async ( ) => {
377+ await setupCourseConfig ( true , false , false ) ;
378+
379+ const mockExecuteRecaptcha = jest . fn ( ) . mockResolvedValue ( null ) ;
380+ useGoogleReCaptcha . mockReturnValue ( { executeRecaptcha : mockExecuteRecaptcha } ) ;
381+
382+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
383+
384+ const comment = await waitFor ( ( ) => screen . findByTestId ( 'comment-comment-1' ) ) ;
385+ const hoverCard = within ( comment ) . getByTestId ( 'hover-card-comment-1' ) ;
386+ await act ( async ( ) => { fireEvent . click ( within ( hoverCard ) . getByRole ( 'button' , { name : / A d d c o m m e n t / i } ) ) ; } ) ;
387+ await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'New comment with CAPTCHA' } } ) ; } ) ;
388+ await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
389+
390+ expect ( screen . getByText ( 'CAPTCHA verification failed.' ) ) . toBeInTheDocument ( ) ;
391+ } ) ;
392+
393+ it ( 'should show captcha error if executeRecaptcha throws' , async ( ) => {
394+ await setupCourseConfig ( true , false , false ) ;
395+
396+ const mockExecuteRecaptcha = jest . fn ( ) . mockRejectedValue ( new Error ( 'recaptcha failed' ) ) ;
397+ useGoogleReCaptcha . mockReturnValue ( { executeRecaptcha : mockExecuteRecaptcha } ) ;
398+
399+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
400+
401+ const comment = await waitFor ( ( ) => screen . findByTestId ( 'comment-comment-1' ) ) ;
402+ const hoverCard = within ( comment ) . getByTestId ( 'hover-card-comment-1' ) ;
403+ await act ( async ( ) => { fireEvent . click ( within ( hoverCard ) . getByRole ( 'button' , { name : / A d d c o m m e n t / i } ) ) ; } ) ;
404+ await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'New comment with CAPTCHA' } } ) ; } ) ;
405+ await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
406+
407+ expect ( screen . getByText ( 'CAPTCHA verification failed.' ) ) . toBeInTheDocument ( ) ;
408+ } ) ;
409+
373410 it ( 'should allow posting a comment' , async ( ) => {
374411 await setupCourseConfig ( ) ;
375412 await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
@@ -659,46 +696,6 @@ describe('ThreadView', () => {
659696 describe ( 'for discussion thread' , ( ) => {
660697 const findLoadMoreCommentsButton = ( ) => screen . findByTestId ( 'load-more-comments' ) ;
661698
662- it ( 'renders the mocked ReCAPTCHA.' , async ( ) => {
663- await setupCourseConfig ( true , false , false ) ;
664- await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
665- await act ( async ( ) => {
666- fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
667- } ) ;
668- expect ( screen . getByTestId ( 'mocked-recaptcha' ) ) . toBeInTheDocument ( ) ;
669- } ) ;
670-
671- it ( 'successfully calls onTokenChange when Solve CAPTCHA button is clicked' , async ( ) => {
672- await setupCourseConfig ( true , false , false ) ;
673- await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
674- await act ( async ( ) => {
675- fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
676- } ) ;
677- const solveButton = screen . getByText ( 'Solve CAPTCHA' ) ;
678- fireEvent . click ( solveButton ) ;
679- expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
680- } ) ;
681-
682- it ( 'successfully calls onExpired handler when CAPTCHA expires' , async ( ) => {
683- await setupCourseConfig ( true , false , false ) ;
684- await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
685- await act ( async ( ) => {
686- fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
687- } ) ;
688- fireEvent . click ( screen . getByText ( 'Expire CAPTCHA' ) ) ;
689- expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
690- } ) ;
691-
692- it ( 'successfully calls onError handler when CAPTCHA errors' , async ( ) => {
693- await setupCourseConfig ( true , false , false ) ;
694- await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
695- await act ( async ( ) => {
696- fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
697- } ) ;
698- fireEvent . click ( screen . getByText ( 'Error CAPTCHA' ) ) ;
699- expect ( mockOnError ) . toHaveBeenCalled ( ) ;
700- } ) ;
701-
702699 it ( 'shown post not found when post id does not belong to course' , async ( ) => {
703700 await waitFor ( ( ) => renderComponent ( 'unloaded-id' ) ) ;
704701 expect ( await screen . findByText ( 'Thread not found' , { exact : true } ) )
@@ -1161,61 +1158,3 @@ describe('ThreadView', () => {
11611158 expect ( screen . queryByTestId ( 'tinymce-editor' ) ) . toBeInTheDocument ( ) ;
11621159 } ) ;
11631160} ) ;
1164-
1165- describe ( 'MockReCAPTCHA' , ( ) => {
1166- beforeEach ( ( ) => {
1167- jest . clearAllMocks ( ) ;
1168- } ) ;
1169-
1170- test ( 'uses defaultProps when props are not provided' , ( ) => {
1171- render ( < MockReCAPTCHA /> ) ;
1172-
1173- expect ( screen . getByTestId ( 'mocked-recaptcha' ) ) . toBeInTheDocument ( ) ;
1174-
1175- fireEvent . click ( screen . getByText ( 'Solve CAPTCHA' ) ) ;
1176- fireEvent . click ( screen . getByText ( 'Expire CAPTCHA' ) ) ;
1177- fireEvent . click ( screen . getByText ( 'Error CAPTCHA' ) ) ;
1178-
1179- expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
1180- expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
1181- expect ( mockOnError ) . toHaveBeenCalled ( ) ;
1182- } ) ;
1183-
1184- it ( 'triggers all callbacks and exposes reset via ref' , ( ) => {
1185- const onChange = jest . fn ( ) ;
1186- const onExpired = jest . fn ( ) ;
1187- const onError = jest . fn ( ) ;
1188-
1189- const Wrapper = ( ) => {
1190- const recaptchaRef = useRef ( null ) ;
1191- return (
1192- < div >
1193- < MockReCAPTCHA
1194- ref = { recaptchaRef }
1195- onChange = { onChange }
1196- onExpired = { onExpired }
1197- onError = { onError }
1198- />
1199- < button onClick = { ( ) => recaptchaRef . current . reset ( ) } data-testid = "reset-btn" type = "button" > Reset</ button >
1200- </ div >
1201- ) ;
1202- } ;
1203-
1204- const { getByText, getByTestId } = render ( < Wrapper /> ) ;
1205-
1206- fireEvent . click ( getByText ( 'Solve CAPTCHA' ) ) ;
1207- fireEvent . click ( getByText ( 'Expire CAPTCHA' ) ) ;
1208- fireEvent . click ( getByText ( 'Error CAPTCHA' ) ) ;
1209-
1210- fireEvent . click ( getByTestId ( 'reset-btn' ) ) ;
1211-
1212- expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
1213- expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
1214- expect ( mockOnError ) . toHaveBeenCalled ( ) ;
1215-
1216- expect ( onChange ) . toHaveBeenCalledWith ( 'mock-token' ) ;
1217- expect ( onExpired ) . toHaveBeenCalled ( ) ;
1218- expect ( onError ) . toHaveBeenCalled ( ) ;
1219- expect ( mockReset ) . toHaveBeenCalled ( ) ;
1220- } ) ;
1221- } ) ;
0 commit comments