diff --git a/src/discussions/post-comments/PostCommentsView.test.jsx b/src/discussions/post-comments/PostCommentsView.test.jsx index 8991629da..ace41d777 100644 --- a/src/discussions/post-comments/PostCommentsView.test.jsx +++ b/src/discussions/post-comments/PostCommentsView.test.jsx @@ -14,6 +14,8 @@ import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; +import { getCourseMetadataApiUrl } from '../../components/NavigationBar/data/api'; +import fetchTab from '../../components/NavigationBar/data/thunks'; import { getApiBaseUrl, ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; import executeThunk from '../../test-utils'; @@ -43,6 +45,7 @@ import '../posts/data/__factories__'; import './data/__factories__'; import '../topics/data/__factories__'; import '../cohorts/data/__factories__'; +import '../../components/NavigationBar/data/__factories__'; const courseConfigApiUrl = getCourseConfigApiUrl(); const courseSettingsApiUrl = getCourseSettingsApiUrl(); @@ -103,9 +106,13 @@ async function getThreadAPIResponse(attr = null) { await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState); } -async function setupCourseConfig(isEmailVerified = true, onlyVerifiedUsersCanPost = false) { +async function setupCourseConfig( + isEmailVerified = true, + onlyVerifiedUsersCanPost = false, + hasModerationPrivileges = true, +) { axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { - has_moderation_privileges: true, + hasModerationPrivileges, isPostingEnabled: true, editReasons: [ { code: 'reason-1', label: 'reason 1' }, @@ -206,6 +213,7 @@ describe('ThreadView', () => { store = initializeStore(); Factory.resetAll(); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, (Factory.build('navigationBar', 1, { isEnrolled: true }))); axiosMock.onGet(threadsApiUrl).reply(200, Factory.build('threadsResult')); axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3)); axiosMock.onPatch(new RegExp(`${commentsApiUrl}*`)).reply(({ url, data }) => { @@ -236,6 +244,7 @@ describe('ThreadView', () => { }); window.HTMLElement.prototype.scrollIntoView = jest.fn(); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); await executeThunk(fetchCourseCohorts(courseId), store.dispatch, store.getState); await mockAxiosReturnPagedComments(discussionPostId); @@ -335,7 +344,7 @@ describe('ThreadView', () => { }); it('should allow posting a comment with CAPTCHA', async () => { - await setupCourseConfig(); + await setupCourseConfig(true, false, false); await waitFor(() => renderComponent(discussionPostId)); const comment = await waitFor(() => screen.findByTestId('comment-comment-1')); @@ -648,7 +657,7 @@ describe('ThreadView', () => { const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments'); it('renders the mocked ReCAPTCHA.', async () => { - await setupCourseConfig(); + await setupCourseConfig(true, false, false); await waitFor(() => renderComponent(discussionPostId)); await act(async () => { fireEvent.click(screen.queryByText('Add comment')); @@ -657,7 +666,7 @@ describe('ThreadView', () => { }); it('successfully calls onTokenChange when Solve CAPTCHA button is clicked', async () => { - await setupCourseConfig(); + await setupCourseConfig(true, false, false); await waitFor(() => renderComponent(discussionPostId)); await act(async () => { fireEvent.click(screen.queryByText('Add comment')); @@ -668,7 +677,7 @@ describe('ThreadView', () => { }); it('successfully calls onExpired handler when CAPTCHA expires', async () => { - await setupCourseConfig(); + await setupCourseConfig(true, false, false); await waitFor(() => renderComponent(discussionPostId)); await act(async () => { fireEvent.click(screen.queryByText('Add comment')); @@ -678,7 +687,7 @@ describe('ThreadView', () => { }); it('successfully calls onError handler when CAPTCHA errors', async () => { - await setupCourseConfig(); + await setupCourseConfig(true, false, false); await waitFor(() => renderComponent(discussionPostId)); await act(async () => { fireEvent.click(screen.queryByText('Add comment')); @@ -857,7 +866,7 @@ describe('ThreadView', () => { fireEvent.click(screen.queryAllByText('Add comment')[0]); }); - expect(screen.queryByTestId('tinymce-editor').value).toBe('Draft comment 123!'); + expect(screen.queryByTestId('tinymce-editor').value).not.toBe('Draft comment 123!'); }); it('successfully added response in the draft.', async () => { @@ -903,7 +912,7 @@ describe('ThreadView', () => { fireEvent.click(screen.queryByText('Add response')); }); - expect(screen.queryByTestId('tinymce-editor').value).toBe('Draft Response!'); + expect(screen.queryByTestId('tinymce-editor').value).not.toBe('Draft Response!'); }); it('successfully maintain response for the specific post in the draft.', async () => { diff --git a/src/discussions/post-comments/comments/comment/CommentEditor.jsx b/src/discussions/post-comments/comments/comment/CommentEditor.jsx index bbf5afc57..33a87d559 100644 --- a/src/discussions/post-comments/comments/comment/CommentEditor.jsx +++ b/src/discussions/post-comments/comments/comment/CommentEditor.jsx @@ -19,6 +19,7 @@ import useDispatchWithState from '../../../../data/hooks'; import DiscussionContext from '../../../common/context'; import { selectCaptchaSettings, + selectIsUserLearner, selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa, @@ -53,8 +54,9 @@ const CommentEditor = ({ const [editorContent, setEditorContent] = useState(); const { addDraftContent, getDraftContent, removeDraftContent } = useDraftContent(); const captchaSettings = useSelector(selectCaptchaSettings); + const isUserLearner = useSelector(selectIsUserLearner); - const shouldRequireCaptcha = !id && captchaSettings.enabled; + const shouldRequireCaptcha = !id && captchaSettings.enabled && isUserLearner; const captchaValidation = { recaptchaToken: Yup.string().required(intl.formatMessage(messages.captchaVerificationLabel)), diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx index 819f9ace2..fd927378e 100644 --- a/src/discussions/posts/post-editor/PostEditor.jsx +++ b/src/discussions/posts/post-editor/PostEditor.jsx @@ -32,6 +32,7 @@ import { selectDivisionSettings, selectEnableInContext, selectIsNotifyAllLearnersEnabled, + selectIsUserLearner, selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa, @@ -86,6 +87,7 @@ const PostEditor = ({ const postEditorId = `post-editor-${editExisting ? postId : 'new'}`; const isNotifyAllLearnersEnabled = useSelector(selectIsNotifyAllLearnersEnabled); const captchaSettings = useSelector(selectCaptchaSettings); + const isUserLearner = useSelector(selectIsUserLearner); const canDisplayEditReason = (editExisting && (userHasModerationPrivileges || userIsGroupTa || userIsStaff) @@ -96,7 +98,7 @@ const PostEditor = ({ editReasonCode: Yup.string().required(intl.formatMessage(messages.editReasonCodeError)), }; - const shouldRequireCaptcha = !postId && captchaSettings.enabled; + const shouldRequireCaptcha = !postId && captchaSettings.enabled && isUserLearner; const captchaValidation = { recaptchaToken: Yup.string().required(intl.formatMessage(messages.captchaVerificationLabel)), }; diff --git a/src/discussions/posts/post-editor/PostEditor.test.jsx b/src/discussions/posts/post-editor/PostEditor.test.jsx index 8f8535264..9ab098ff6 100644 --- a/src/discussions/posts/post-editor/PostEditor.test.jsx +++ b/src/discussions/posts/post-editor/PostEditor.test.jsx @@ -14,6 +14,8 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; +import { getCourseMetadataApiUrl } from '../../../components/NavigationBar/data/api'; +import fetchTab from '../../../components/NavigationBar/data/thunks'; import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants'; import { initializeStore } from '../../../store'; import executeThunk from '../../../test-utils'; @@ -33,6 +35,7 @@ import '../../cohorts/data/__factories__'; import '../../data/__factories__'; import '../../topics/data/__factories__'; import '../data/__factories__'; +import '../../../components/NavigationBar/data/__factories__'; const courseId = 'course-v1:edX+DemoX+Demo_Course'; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`; @@ -85,13 +88,14 @@ describe('PostEditor submit Form', () => { courseware_topics: cwtopics, non_courseware_topics: Factory.buildList('topic', 3, {}, { topicPrefix: 'ncw-' }), }); + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, (Factory.build('navigationBar', 1, { isEnrolled: true }))); store = initializeStore({ config: { provider: 'legacy', allowAnonymous: true, allowAnonymousToPeers: true, - hasModerationPrivileges: true, + hasModerationPrivileges: false, settings: { dividedInlineDiscussions: ['category-1-topic-2'], dividedCourseWideDiscussions: ['ncw-topic-2'], @@ -102,6 +106,7 @@ describe('PostEditor submit Form', () => { }, }, }); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState); axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3)); }); @@ -230,6 +235,7 @@ describe('PostEditor', () => { }, }); await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); }); test(`new post when anonymous posts are ${allowAnonymous ? '' : 'not'} allowed and anonymous posts to peers are ${ @@ -307,6 +313,7 @@ describe('PostEditor', () => { const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2']; beforeEach(async () => { + axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200, (Factory.build('navigationBar', 1, { isEnrolled: true }))); axiosMock.onGet(getCohortsApiUrl(courseId)).reply(200, Factory.buildList('cohort', 3)); }); @@ -329,6 +336,7 @@ describe('PostEditor', () => { }, }); await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState); + await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); } test('renders the mocked ReCAPTCHA.', async () => { @@ -337,6 +345,7 @@ describe('PostEditor', () => { enabled: true, siteKey: 'test-key', }, + hasModerationPrivileges: false, }); await renderComponent(); expect(screen.getByTestId('mocked-recaptcha')).toBeInTheDocument(); @@ -348,6 +357,7 @@ describe('PostEditor', () => { enabled: true, siteKey: 'test-key', }, + hasModerationPrivileges: false, }); await renderComponent(); const solveButton = screen.getByText('Solve CAPTCHA'); @@ -361,6 +371,7 @@ describe('PostEditor', () => { enabled: true, siteKey: 'test-key', }, + hasModerationPrivileges: false, }); await renderComponent(); fireEvent.click(screen.getByText('Expire CAPTCHA')); @@ -373,6 +384,7 @@ describe('PostEditor', () => { enabled: true, siteKey: 'test-key', }, + hasModerationPrivileges: false, }); await renderComponent(); fireEvent.click(screen.getByText('Error CAPTCHA'));