1
+ import React , { useRef } from 'react' ;
2
+
1
3
import {
2
4
act , fireEvent , render , screen , waitFor , within ,
3
5
} from '@testing-library/react' ;
@@ -23,6 +25,12 @@ import fetchCourseConfig from '../data/thunks';
23
25
import DiscussionContent from '../discussions-home/DiscussionContent' ;
24
26
import { getThreadsApiUrl } from '../posts/data/api' ;
25
27
import { fetchThread , fetchThreads } from '../posts/data/thunks' ;
28
+ import MockReCAPTCHA , {
29
+ mockOnChange ,
30
+ mockOnError ,
31
+ mockOnExpired ,
32
+ mockReset ,
33
+ } from '../posts/post-editor/mocksData/react-google-recaptcha' ;
26
34
import fetchCourseTopics from '../topics/data/thunks' ;
27
35
import { getDiscussionTourUrl } from '../tours/data/api' ;
28
36
import selectTours from '../tours/data/selectors' ;
@@ -51,6 +59,8 @@ let testLocation;
51
59
let container ;
52
60
let unmount ;
53
61
62
+ jest . mock ( 'react-google-recaptcha' , ( ) => MockReCAPTCHA ) ;
63
+
54
64
async function mockAxiosReturnPagedComments ( threadId , threadType = ThreadType . DISCUSSION , page = 1 , count = 2 ) {
55
65
axiosMock . onGet ( commentsApiUrl ) . reply ( 200 , Factory . build ( 'commentsResult' , { can_delete : true } , {
56
66
threadId,
@@ -215,7 +225,13 @@ describe('ThreadView', () => {
215
225
endorsed : false ,
216
226
} ) ] ;
217
227
} ) ;
218
- axiosMock . onGet ( `${ courseConfigApiUrl } ${ courseId } /` ) . reply ( 200 , { isPostingEnabled : true } ) ;
228
+ axiosMock . onGet ( `${ courseConfigApiUrl } ${ courseId } /` ) . reply ( 200 , {
229
+ isPostingEnabled : true ,
230
+ captchaSettings : {
231
+ enabled : true ,
232
+ siteKey : 'test-key' ,
233
+ } ,
234
+ } ) ;
219
235
window . HTMLElement . prototype . scrollIntoView = jest . fn ( ) ;
220
236
221
237
await executeThunk ( fetchCourseConfig ( courseId ) , store . dispatch , store . getState ) ;
@@ -292,6 +308,29 @@ describe('ThreadView', () => {
292
308
expect ( screen . queryByTestId ( 'tinymce-editor' ) ) . not . toBeInTheDocument ( ) ;
293
309
} ) ;
294
310
311
+ it ( 'should allow posting a comment with CAPTCHA' , async ( ) => {
312
+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
313
+
314
+ const comment = await waitFor ( ( ) => screen . findByTestId ( 'comment-comment-1' ) ) ;
315
+ const hoverCard = within ( comment ) . getByTestId ( 'hover-card-comment-1' ) ;
316
+ await act ( async ( ) => { fireEvent . click ( within ( hoverCard ) . getByRole ( 'button' , { name : / A d d c o m m e n t / i } ) ) ; } ) ;
317
+ await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'New comment with CAPTCHA' } } ) ; } ) ;
318
+ await act ( async ( ) => { fireEvent . click ( screen . getByText ( 'Solve CAPTCHA' ) ) ; } ) ;
319
+ await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
320
+
321
+ await waitFor ( ( ) => {
322
+ expect ( axiosMock . history . post ) . toHaveLength ( 1 ) ;
323
+ expect ( JSON . parse ( axiosMock . history . post [ 0 ] . data ) ) . toMatchObject ( {
324
+ captcha_token : 'm' ,
325
+ enable_in_context_sidebar : false ,
326
+ parent_id : 'comment-1' ,
327
+ raw_body : 'New comment with CAPTCHA' ,
328
+ thread_id : 'thread-1' ,
329
+ } ) ;
330
+ expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
331
+ } ) ;
332
+ } ) ;
333
+
295
334
it ( 'should allow posting a comment' , async ( ) => {
296
335
await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
297
336
@@ -302,7 +341,6 @@ describe('ThreadView', () => {
302
341
await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'testing123' } } ) ; } ) ;
303
342
await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
304
343
305
- expect ( screen . queryByTestId ( 'tinymce-editor' ) ) . not . toBeInTheDocument ( ) ;
306
344
await waitFor ( async ( ) => expect ( await screen . findByTestId ( 'comment-1' ) ) . toBeInTheDocument ( ) ) ;
307
345
} ) ;
308
346
@@ -323,7 +361,6 @@ describe('ThreadView', () => {
323
361
await act ( async ( ) => { fireEvent . change ( screen . getByTestId ( 'tinymce-editor' ) , { target : { value : 'testing123' } } ) ; } ) ;
324
362
await act ( async ( ) => { fireEvent . click ( screen . getByText ( / s u b m i t / i) ) ; } ) ;
325
363
326
- expect ( screen . queryByTestId ( 'tinymce-editor' ) ) . not . toBeInTheDocument ( ) ;
327
364
await waitFor ( async ( ) => expect ( await screen . findByTestId ( 'reply-comment-2' ) ) . toBeInTheDocument ( ) ) ;
328
365
} ) ;
329
366
@@ -581,6 +618,42 @@ describe('ThreadView', () => {
581
618
describe ( 'for discussion thread' , ( ) => {
582
619
const findLoadMoreCommentsButton = ( ) => screen . findByTestId ( 'load-more-comments' ) ;
583
620
621
+ it ( 'renders the mocked ReCAPTCHA.' , async ( ) => {
622
+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
623
+ await act ( async ( ) => {
624
+ fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
625
+ } ) ;
626
+ expect ( screen . getByTestId ( 'mocked-recaptcha' ) ) . toBeInTheDocument ( ) ;
627
+ } ) ;
628
+
629
+ it ( 'successfully calls onTokenChange when Solve CAPTCHA button is clicked' , async ( ) => {
630
+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
631
+ await act ( async ( ) => {
632
+ fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
633
+ } ) ;
634
+ const solveButton = screen . getByText ( 'Solve CAPTCHA' ) ;
635
+ fireEvent . click ( solveButton ) ;
636
+ expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
637
+ } ) ;
638
+
639
+ it ( 'successfully calls onExpired handler when CAPTCHA expires' , async ( ) => {
640
+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
641
+ await act ( async ( ) => {
642
+ fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
643
+ } ) ;
644
+ fireEvent . click ( screen . getByText ( 'Expire CAPTCHA' ) ) ;
645
+ expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
646
+ } ) ;
647
+
648
+ it ( 'successfully calls onError handler when CAPTCHA errors' , async ( ) => {
649
+ await waitFor ( ( ) => renderComponent ( discussionPostId ) ) ;
650
+ await act ( async ( ) => {
651
+ fireEvent . click ( screen . queryByText ( 'Add comment' ) ) ;
652
+ } ) ;
653
+ fireEvent . click ( screen . getByText ( 'Error CAPTCHA' ) ) ;
654
+ expect ( mockOnError ) . toHaveBeenCalled ( ) ;
655
+ } ) ;
656
+
584
657
it ( 'shown post not found when post id does not belong to course' , async ( ) => {
585
658
await waitFor ( ( ) => renderComponent ( 'unloaded-id' ) ) ;
586
659
expect ( await screen . findByText ( 'Thread not found' , { exact : true } ) )
@@ -749,7 +822,7 @@ describe('ThreadView', () => {
749
822
fireEvent . click ( screen . queryAllByText ( 'Add comment' ) [ 0 ] ) ;
750
823
} ) ;
751
824
752
- expect ( screen . queryByTestId ( 'tinymce-editor' ) . value ) . toBe ( '' ) ;
825
+ expect ( screen . queryByTestId ( 'tinymce-editor' ) . value ) . toBe ( 'Draft comment 123! ' ) ;
753
826
} ) ;
754
827
755
828
it ( 'successfully added response in the draft.' , async ( ) => {
@@ -793,7 +866,7 @@ describe('ThreadView', () => {
793
866
fireEvent . click ( screen . queryByText ( 'Add response' ) ) ;
794
867
} ) ;
795
868
796
- expect ( screen . queryByTestId ( 'tinymce-editor' ) . value ) . toBe ( '' ) ;
869
+ expect ( screen . queryByTestId ( 'tinymce-editor' ) . value ) . toBe ( 'Draft Response! ' ) ;
797
870
} ) ;
798
871
799
872
it ( 'successfully maintain response for the specific post in the draft.' , async ( ) => {
@@ -975,3 +1048,61 @@ describe('ThreadView', () => {
975
1048
} ) ;
976
1049
} ) ;
977
1050
} ) ;
1051
+
1052
+ describe ( 'MockReCAPTCHA' , ( ) => {
1053
+ beforeEach ( ( ) => {
1054
+ jest . clearAllMocks ( ) ;
1055
+ } ) ;
1056
+
1057
+ test ( 'uses defaultProps when props are not provided' , ( ) => {
1058
+ render ( < MockReCAPTCHA /> ) ;
1059
+
1060
+ expect ( screen . getByTestId ( 'mocked-recaptcha' ) ) . toBeInTheDocument ( ) ;
1061
+
1062
+ fireEvent . click ( screen . getByText ( 'Solve CAPTCHA' ) ) ;
1063
+ fireEvent . click ( screen . getByText ( 'Expire CAPTCHA' ) ) ;
1064
+ fireEvent . click ( screen . getByText ( 'Error CAPTCHA' ) ) ;
1065
+
1066
+ expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
1067
+ expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
1068
+ expect ( mockOnError ) . toHaveBeenCalled ( ) ;
1069
+ } ) ;
1070
+
1071
+ it ( 'triggers all callbacks and exposes reset via ref' , ( ) => {
1072
+ const onChange = jest . fn ( ) ;
1073
+ const onExpired = jest . fn ( ) ;
1074
+ const onError = jest . fn ( ) ;
1075
+
1076
+ const Wrapper = ( ) => {
1077
+ const recaptchaRef = useRef ( null ) ;
1078
+ return (
1079
+ < div >
1080
+ < MockReCAPTCHA
1081
+ ref = { recaptchaRef }
1082
+ onChange = { onChange }
1083
+ onExpired = { onExpired }
1084
+ onError = { onError }
1085
+ />
1086
+ < button onClick = { ( ) => recaptchaRef . current . reset ( ) } data-testid = "reset-btn" type = "button" > Reset</ button >
1087
+ </ div >
1088
+ ) ;
1089
+ } ;
1090
+
1091
+ const { getByText, getByTestId } = render ( < Wrapper /> ) ;
1092
+
1093
+ fireEvent . click ( getByText ( 'Solve CAPTCHA' ) ) ;
1094
+ fireEvent . click ( getByText ( 'Expire CAPTCHA' ) ) ;
1095
+ fireEvent . click ( getByText ( 'Error CAPTCHA' ) ) ;
1096
+
1097
+ fireEvent . click ( getByTestId ( 'reset-btn' ) ) ;
1098
+
1099
+ expect ( mockOnChange ) . toHaveBeenCalled ( ) ;
1100
+ expect ( mockOnExpired ) . toHaveBeenCalled ( ) ;
1101
+ expect ( mockOnError ) . toHaveBeenCalled ( ) ;
1102
+
1103
+ expect ( onChange ) . toHaveBeenCalledWith ( 'mock-token' ) ;
1104
+ expect ( onExpired ) . toHaveBeenCalled ( ) ;
1105
+ expect ( onError ) . toHaveBeenCalled ( ) ;
1106
+ expect ( mockReset ) . toHaveBeenCalled ( ) ;
1107
+ } ) ;
1108
+ } ) ;
0 commit comments