feat: captcha only for learners (#792)

* feat: captcha only for learnerS

* test: fixed test cases

* test: fixed test cases for post editor
This commit is contained in:
sundasnoreen12
2025-07-28 21:53:22 +05:00
committed by GitHub
parent fa772053c4
commit d36c4af4e9
4 changed files with 37 additions and 12 deletions

View File

@@ -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 () => {

View File

@@ -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)),

View File

@@ -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)),
};

View File

@@ -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'));