diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index 938415933..16a7e3be4 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -1,5 +1,5 @@ import { - Alert, AlertModal, Button, useToggle, ActionRow, + Alert, AlertModal, Button, useToggle, ActionRow, Form, } from '@edx/paragon'; import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useState } from 'react'; @@ -17,7 +17,20 @@ function CourseReset({ username, intl }) { const [courseResetData, setCourseResetData] = useState([]); const [error, setError] = useState(''); const [isOpen, open, close] = useToggle(false); + const [comment, setComment] = useState(''); + const [commentError, setCommentError] = useState(''); const POLLING_INTERVAL = 10000; + const MAX_COMMENT_LENGTH = 255; + + const handleCommentChange = (e) => { + const text = e.target.value; + setComment(text); + if (text.length > MAX_COMMENT_LENGTH) { + setCommentError('Maximum length allowed for comment is 255 characters'); + } else { + setCommentError(''); + } + }; useEffect(() => { let isMounted = true; @@ -64,7 +77,11 @@ function CourseReset({ username, intl }) { const handleSubmit = useCallback(async (courseID) => { setError(null); - const data = await postCourseReset(username, courseID); + if (commentError.length) { + return; + } + + const data = await postCourseReset(username, courseID, comment); if (data && !data.errors) { const updatedCourseResetData = courseResetData.map((course) => { if (course.course_id === data.course_id) { @@ -78,7 +95,7 @@ function CourseReset({ username, intl }) { setError(data.errors[0].text); } close(); - }, [username, courseResetData]); + }, [username, courseResetData, comment]); const renderResetData = courseResetData.map((data) => { const updatedData = { @@ -86,6 +103,7 @@ function CourseReset({ username, intl }) { courseId: data.course_id, status: data.status, action: 'Unavailable', + comment: data.comment, }; if (data.can_reset) { @@ -109,6 +127,7 @@ function CourseReset({ username, intl }) { @@ -124,6 +143,18 @@ function CourseReset({ username, intl }) { defaultMessage="Are you sure? This will erase all of this learner's data for this course. This can only happen once per learner per course." />

+ + + + {commentError && ( + {commentError} + )} ); @@ -164,6 +195,10 @@ function CourseReset({ username, intl }) { Header: intl.formatMessage(messages.recordTableHeaderStatus), accessor: 'status', }, + { + Header: 'Comment', + accessor: 'comment', + }, { Header: 'Action', accessor: 'action', diff --git a/src/users/CourseReset.test.jsx b/src/users/CourseReset.test.jsx index 55caceccf..6c2ddf3c5 100644 --- a/src/users/CourseReset.test.jsx +++ b/src/users/CourseReset.test.jsx @@ -1,5 +1,7 @@ import React from 'react'; -import { act, render, waitFor } from '@testing-library/react'; +import { + act, fireEvent, render, waitFor, +} from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -13,22 +15,31 @@ const CourseResetWrapper = (props) => ( ); +const apiDataMocks = () => { + jest + .spyOn(api, 'getLearnerCourseResetList') + .mockImplementationOnce(() => Promise.resolve(expectedGetData)); + const postRequest = jest + .spyOn(api, 'postCourseReset') + .mockImplementationOnce(() => Promise.resolve(expectedPostData)); + + return postRequest; +}; + describe('CourseReset', () => { it('renders the component with the provided user prop', () => { const user = 'John Doe'; - const screen = render(); - const container = screen.getByTestId('course-reset-container'); - expect(screen).toBeTruthy(); + const { getByText, getByTestId } = render(); + const container = getByTestId('course-reset-container'); expect(container).toBeInTheDocument(); + expect(getByText(/Course Name/)).toBeInTheDocument(); + expect(getByText(/Status/)).toBeInTheDocument(); + expect(getByText(/Comment/)).toBeInTheDocument(); + expect(getByText(/Action/)).toBeInTheDocument(); }); it('clicks on the reset button and make a post request successfully', async () => { - jest - .spyOn(api, 'getLearnerCourseResetList') - .mockImplementationOnce(() => Promise.resolve(expectedGetData)); - const postRequest = jest - .spyOn(api, 'postCourseReset') - .mockImplementationOnce(() => Promise.resolve(expectedPostData)); + const postRequest = apiDataMocks(); const user = 'John Doe'; let screen; @@ -159,4 +170,40 @@ describe('CourseReset', () => { expect(alertText).not.toBeInTheDocument(); }); }); + + it('asserts different comment state', async () => { + const postRequest = apiDataMocks(); + + const user = 'John Doe'; + let screen; + + await waitFor(() => { + screen = render(); + }); + const resetButton = screen.getByText('Reset', { selector: 'button' }); + fireEvent.click(resetButton); + + const submitButton = screen.getByText(/Yes/); + expect(submitButton).toBeInTheDocument(); + + // Get the comment textarea and make assertions + const commentInput = screen.getByRole('textbox'); + expect(commentInput).toBeInTheDocument(); + + // Assert that an error occurs when the characters length of comment text is more than 255 + fireEvent.change(commentInput, { target: { value: 'hello world'.repeat(200) } }); + expect(commentInput).toHaveValue('hello world'.repeat(200)); + const commentErrorText = screen.getByText('Maximum length allowed for comment is 255 characters'); + expect(commentErrorText).toBeInTheDocument(); + + // check that no error occurs with comment length less than 256 characters + fireEvent.change(commentInput, { target: { value: 'hello world' } }); + expect(commentInput).toHaveValue('hello world'); + const errorText = screen.queryByText('Maximum length allowed for comment is 255 characters'); + expect(errorText).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText(/Yes/)); + + await waitFor(() => expect(postRequest).toHaveBeenCalled()); + }); }); diff --git a/src/users/data/api.js b/src/users/data/api.js index 45c38c9dd..7a5eddd3c 100644 --- a/src/users/data/api.js +++ b/src/users/data/api.js @@ -801,10 +801,11 @@ export async function getLearnerCourseResetList(username) { } } -export async function postCourseReset(username, courseID) { +export async function postCourseReset(username, courseID, comment = '') { try { const { data } = await getAuthenticatedHttpClient().post(AppUrls.courseResetUrl(username), { course_id: courseID, + comment, }); return data; } catch (error) { diff --git a/src/users/data/test/courseReset.js b/src/users/data/test/courseReset.js index 12078ae8e..518919e79 100644 --- a/src/users/data/test/courseReset.js +++ b/src/users/data/test/courseReset.js @@ -4,18 +4,21 @@ export const expectedGetData = [ display_name: 'Demonstration Course', can_reset: false, status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', + comment: 'comment 1', }, { course_id: 'course-v1:EdxOrg+EDX101+2024_Q1', display_name: 'Intro to edx', can_reset: true, status: 'Available', + comment: 'comment 2', }, { course_id: 'course-v1:EdxOrg+EDX201+2024_Q2', display_name: 'Intro to new course', can_reset: false, status: 'in progress', + comment: 'comment 3', }, ]; @@ -24,4 +27,5 @@ export const expectedPostData = { display_name: 'Intro to edx', can_reset: false, status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', + comment: 'Post comment', };