From 5e7869d75fca147ae3ae24074f6a9e889d247e79 Mon Sep 17 00:00:00 2001
From: hajorg
Date: Wed, 27 Mar 2024 14:20:08 +0100
Subject: [PATCH 1/2] feat: add comment to course reset
---
src/users/CourseReset.jsx | 40 ++++++++++++++++--
src/users/CourseReset.test.jsx | 67 +++++++++++++++++++++++++-----
src/users/data/api.js | 3 +-
src/users/data/test/courseReset.js | 4 ++
4 files changed, 100 insertions(+), 14 deletions(-)
diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx
index 938415933..14ce3c30a 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,8 +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 handleCommentChange = (e) => {
+ const text = e.target.value;
+ setComment(text);
+ if (text.length > 255) {
+ setCommentError('Maximum length allowed for comment is 255 characters');
+ } else {
+ setCommentError('');
+ }
+ };
+
useEffect(() => {
let isMounted = true;
@@ -64,7 +76,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 +94,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 +102,7 @@ function CourseReset({ username, intl }) {
courseId: data.course_id,
status: data.status,
action: 'Unavailable',
+ comment: data.comment,
};
if (data.can_reset) {
@@ -109,6 +126,7 @@ function CourseReset({ username, intl }) {
@@ -124,6 +142,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 +194,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',
};
From 75ac4c213cf46cbe3f8e58df4407bbf742a76ae0 Mon Sep 17 00:00:00 2001
From: hajorg
Date: Fri, 29 Mar 2024 15:43:53 +0100
Subject: [PATCH 2/2] fix: make default comment an empty string
---
src/users/CourseReset.jsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx
index 14ce3c30a..16a7e3be4 100644
--- a/src/users/CourseReset.jsx
+++ b/src/users/CourseReset.jsx
@@ -17,14 +17,15 @@ function CourseReset({ username, intl }) {
const [courseResetData, setCourseResetData] = useState([]);
const [error, setError] = useState('');
const [isOpen, open, close] = useToggle(false);
- const [comment, setComment] = useState();
+ 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 > 255) {
+ if (text.length > MAX_COMMENT_LENGTH) {
setCommentError('Maximum length allowed for comment is 255 characters');
} else {
setCommentError('');