Skip to content

Commit

Permalink
Merge pull request openedx#379 from openedx/hajorg/au-1933-course-res…
Browse files Browse the repository at this point in the history
…et-comment

Add comment to course reset
  • Loading branch information
hajorg authored Mar 29, 2024
2 parents 124cebd + 75ac4c2 commit da37a62
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 14 deletions.
41 changes: 38 additions & 3 deletions src/users/CourseReset.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -78,14 +95,15 @@ function CourseReset({ username, intl }) {
setError(data.errors[0].text);
}
close();
}, [username, courseResetData]);
}, [username, courseResetData, comment]);

const renderResetData = courseResetData.map((data) => {
const updatedData = {
displayName: data.display_name,
courseId: data.course_id,
status: data.status,
action: 'Unavailable',
comment: data.comment,
};

if (data.can_reset) {
Expand All @@ -109,6 +127,7 @@ function CourseReset({ username, intl }) {
<Button
variant="primary"
onClick={() => handleSubmit(data.course_id)}
disabled={!!commentError.length}
>
Yes
</Button>
Expand All @@ -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."
/>
</p>
<Form.Group>
<Form.Control
floatingLabel="Comment"
value={comment}
onChange={handleCommentChange}
as="textarea"
autoResize
/>
</Form.Group>
{commentError && (
<Alert variant="danger">{commentError}</Alert>
)}
</AlertModal>
</>
);
Expand Down Expand Up @@ -164,6 +195,10 @@ function CourseReset({ username, intl }) {
Header: intl.formatMessage(messages.recordTableHeaderStatus),
accessor: 'status',
},
{
Header: 'Comment',
accessor: 'comment',
},
{
Header: 'Action',
accessor: 'action',
Expand Down
67 changes: 57 additions & 10 deletions src/users/CourseReset.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,22 +15,31 @@ const CourseResetWrapper = (props) => (
</IntlProvider>
);

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(<CourseResetWrapper username={user} />);
const container = screen.getByTestId('course-reset-container');
expect(screen).toBeTruthy();
const { getByText, getByTestId } = render(<CourseResetWrapper username={user} />);
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;
Expand Down Expand Up @@ -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(<CourseResetWrapper username={user} />);
});
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());
});
});
3 changes: 2 additions & 1 deletion src/users/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/users/data/test/courseReset.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
];

Expand All @@ -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',
};

0 comments on commit da37a62

Please sign in to comment.