From 27b858f4a0899f992f2f3083892aa2a317b30cfb Mon Sep 17 00:00:00 2001 From: hajorg Date: Fri, 23 Feb 2024 15:41:28 +0100 Subject: [PATCH 1/6] feat: add course reset tab to learner's information --- src/users/CourseReset.jsx | 102 ++++++++++++++++++++++++++ src/users/CourseReset.test.jsx | 45 ++++++++++++ src/users/LearnerInformation.jsx | 5 ++ src/users/LearnerInformation.test.jsx | 23 ++++++ 4 files changed, 175 insertions(+) create mode 100644 src/users/CourseReset.jsx create mode 100644 src/users/CourseReset.test.jsx diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx new file mode 100644 index 000000000..ba5deedc0 --- /dev/null +++ b/src/users/CourseReset.jsx @@ -0,0 +1,102 @@ +import { + AlertModal, Button, useToggle, ActionRow, +} from '@edx/paragon'; +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; +import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; +import Table from '../components/Table'; + +import messages from './messages'; + +function CourseReset({ username, intl }) { + const [courseResetData, setCourseResetData] = useState([]); + const [isOpen, open, close] = useToggle(false); + const handleSubmit = () => { + console.log(`Request sent! for ${username}`); + close(); + }; + + useEffect(() => { + const currentData = [ + { course_id: 'Into to re-dos', status: 'Completed on 1/3/24 12:13:34 by rdoris@edx.org', can_reset: false }, + { course_id: 'Intermediate try again', status: 'Unavailable', can_reset: false }, + { course_id: 'Advanced Erasing', status: 'Course no yet begun', can_reset: false }, + { course_id: 'Re-Dos 301', status: 'In Progress - Started 2/15/24 1:08:00', can_reset: false }, + { course_id: 'Re-Dos 201', status: 'Available', can_reset: true }, + ]; + setCourseResetData(currentData); + }, []); + + const renderResetData = courseResetData.map((data) => { + const updatedData = { courseId: data.course_id, status: data.status, action: 'Unavailable' }; + + if (data.can_reset) { + updatedData.action = ( + <> + + + + + + + )} + > +

+ +

+
+ + ); + } + + if (data.status.toLowerCase().includes('in progress')) { + updatedData.action = ; + } + + return updatedData; + }); + + return ( +
+

Course Reset

+ + + ); +} + +CourseReset.propTypes = { + username: PropTypes.string.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(CourseReset); diff --git a/src/users/CourseReset.test.jsx b/src/users/CourseReset.test.jsx new file mode 100644 index 000000000..9602623ac --- /dev/null +++ b/src/users/CourseReset.test.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { 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'; +import CourseReset from './CourseReset'; +import UserMessagesProvider from '../userMessages/UserMessagesProvider'; + +const CourseResetWrapper = (props) => ( + + + + + +); + +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(); + expect(container).toBeInTheDocument(); + }); + + it('clicks on the reset button', async () => { + const user = 'John Doe'; + const screen = render(); + const btn = screen.queryAllByText(/Reset/); + userEvent.click(btn[1]); + await waitFor(() => { + screen.debug(undefined, 300000000000); + const submitButton = screen.getByText(/Yes/); + userEvent.click(submitButton); + expect(screen.getByText(/Yes/)).toBeInTheDocument(); + }); + + userEvent.click(screen.queryByText(/Yes/)); + + await waitFor(() => { + expect(screen.queryByText(/Warning/)).not.toBeInTheDocument(); + }); + expect(screen).toBeTruthy(); + }); +}); diff --git a/src/users/LearnerInformation.jsx b/src/users/LearnerInformation.jsx index fda37b1c9..d7ccdc594 100644 --- a/src/users/LearnerInformation.jsx +++ b/src/users/LearnerInformation.jsx @@ -8,6 +8,7 @@ import EntitlementsAndEnrollmentsContainer from './EntitlementsAndEnrollmentsCon import LearnerCredentials from './LearnerCredentials'; import LearnerRecords from './LearnerRecords'; import LearnerPurchases from './LearnerPurchases'; +import CourseReset from './CourseReset'; export default function LearnerInformation({ user, changeHandler, @@ -57,6 +58,10 @@ export default function LearnerInformation({
+ +
+ +
); diff --git a/src/users/LearnerInformation.test.jsx b/src/users/LearnerInformation.test.jsx index c06347db5..703bdb2d4 100644 --- a/src/users/LearnerInformation.test.jsx +++ b/src/users/LearnerInformation.test.jsx @@ -61,6 +61,7 @@ describe('Learners and Enrollments component', () => { expect(tabs.at(3).text()).toEqual('SSO/License Info'); expect(tabs.at(4).text()).toEqual('Learner Credentials'); expect(tabs.at(5).text()).toEqual('Learner Records'); + expect(tabs.at(6).text()).toEqual('Course Reset'); }); it('Account Information Tab', () => { @@ -180,4 +181,26 @@ describe('Learners and Enrollments component', () => { expect.stringContaining('Learner Records'), ); }); + + it('Course Reset Tab', () => { + let tabs = wrapper.find('nav.nav-tabs a'); + + tabs.at(6).simulate('click'); + tabs = wrapper.find('nav.nav-tabs a'); + expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(3).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(4).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(5).html()).not.toEqual(expect.stringContaining('active')); + expect(tabs.at(6).html()).toEqual(expect.stringContaining('active')); + + const records = wrapper.find( + '.tab-content div#learner-information-tabpane-course-reset', + ); + expect(records.html()).toEqual(expect.stringContaining('active')); + expect(records.html()).toEqual( + expect.stringContaining('Course Reset'), + ); + }); }); From 0738d4db20e9ffd6b3cd299c81d18a95200b759b Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 4 Mar 2024 14:13:18 +0100 Subject: [PATCH 2/6] feat: connect course reset UI to the backend --- src/users/CourseReset.jsx | 90 +++++++++++++++++++------ src/users/CourseReset.test.jsx | 104 ++++++++++++++++++++++++++--- src/users/data/api.js | 40 +++++++++++ src/users/data/api.test.js | 79 ++++++++++++++++++++++ src/users/data/test/courseReset.js | 27 ++++++++ src/users/data/urls.js | 2 + 6 files changed, 312 insertions(+), 30 deletions(-) create mode 100644 src/users/data/test/courseReset.js diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index ba5deedc0..46e95710c 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -1,39 +1,71 @@ import { - AlertModal, Button, useToggle, ActionRow, + Alert, AlertModal, Button, useToggle, ActionRow, } from '@edx/paragon'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; -import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + injectIntl, + intlShape, + FormattedMessage, +} from '@edx/frontend-platform/i18n'; import Table from '../components/Table'; +import { getLearnerCourseResetList, postCourseReset } from './data/api'; import messages from './messages'; function CourseReset({ username, intl }) { const [courseResetData, setCourseResetData] = useState([]); + const [error, setError] = useState(''); const [isOpen, open, close] = useToggle(false); - const handleSubmit = () => { - console.log(`Request sent! for ${username}`); + + const handleSubmit = async (courseID) => { + setError(null); + const data = await postCourseReset(username, courseID); + if (data && !data.errors) { + const updatedCourseResetData = courseResetData.map((course) => { + if (course.course_id === data.course_id) { + return data; + } + return course; + }); + setCourseResetData(updatedCourseResetData); + } + if (data && data.errors) { + setError(data.errors[0].text); + } close(); }; - useEffect(() => { - const currentData = [ - { course_id: 'Into to re-dos', status: 'Completed on 1/3/24 12:13:34 by rdoris@edx.org', can_reset: false }, - { course_id: 'Intermediate try again', status: 'Unavailable', can_reset: false }, - { course_id: 'Advanced Erasing', status: 'Course no yet begun', can_reset: false }, - { course_id: 'Re-Dos 301', status: 'In Progress - Started 2/15/24 1:08:00', can_reset: false }, - { course_id: 'Re-Dos 201', status: 'Available', can_reset: true }, - ]; - setCourseResetData(currentData); + useEffect(async () => { + const handleRequest = async () => { + const data = await getLearnerCourseResetList(username); + if (data.length) { + setCourseResetData(data); + } else { + setCourseResetData([]); + setError(data.errors[0].text); + } + }; + + handleRequest(); }, []); const renderResetData = courseResetData.map((data) => { - const updatedData = { courseId: data.course_id, status: data.status, action: 'Unavailable' }; + const updatedData = { + displayName: data.display_name, + courseId: data.course_id, + status: data.status, + action: 'Unavailable', + }; if (data.can_reset) { updatedData.action = ( <> - @@ -44,12 +76,17 @@ function CourseReset({ username, intl }) { variant="warning" footerNode={( - + - )} + )} >

In Progress; + updatedData.action = ( + + ); } return updatedData; @@ -72,11 +113,22 @@ function CourseReset({ username, intl }) { return (

Course Reset

+ {error && ( + { + setError(null); + }} + > + {error} + + )}
( - - - + ); @@ -23,13 +22,24 @@ describe('CourseReset', () => { expect(container).toBeInTheDocument(); }); - it('clicks on the reset button', async () => { + 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 user = 'John Doe'; - const screen = render(); - const btn = screen.queryAllByText(/Reset/); - userEvent.click(btn[1]); + let screen; + + await waitFor(() => { + screen = render(); + }); + screen.debug(undefined, 300000000000); + const btn = screen.getByText('Reset', { selector: 'button' }); + userEvent.click(btn); await waitFor(() => { - screen.debug(undefined, 300000000000); const submitButton = screen.getByText(/Yes/); userEvent.click(submitButton); expect(screen.getByText(/Yes/)).toBeInTheDocument(); @@ -40,6 +50,78 @@ describe('CourseReset', () => { await waitFor(() => { expect(screen.queryByText(/Warning/)).not.toBeInTheDocument(); }); - expect(screen).toBeTruthy(); + expect(postRequest).toHaveBeenCalled(); + }); + + it('returns an empty table if it cannot fetch course reset list', async () => { + jest + .spyOn(api, 'getLearnerCourseResetList') + .mockResolvedValueOnce({ + errors: [ + { + code: null, + dismissible: true, + text: 'An error occurred fetching course reset list for user', + type: 'danger', + }, + ], + }); + + let screen; + const user = 'john'; + await act(async () => { + screen = render(); + }); + const alertText = screen.getByText(/An error occurred fetching course reset list for user/); + expect(alertText).toBeInTheDocument(); + }); + + it('returns an error when resetting a course', async () => { + const user = 'John Doe'; + let screen; + + jest.spyOn(api, 'getLearnerCourseResetList').mockResolvedValueOnce(expectedGetData); + jest + .spyOn(api, 'postCourseReset') + .mockResolvedValueOnce({ + errors: [ + { + code: null, + dismissible: true, + text: 'An error occurred resetting course for user', + type: 'danger', + topic: 'credentials', + }, + ], + }); + + await act(async () => { + screen = render(); + await waitFor(() => { + const btn = screen.getByText('Reset', { selector: 'button' }); + userEvent.click(btn); + }); + + await waitFor(() => { + const submitButton = screen.getByText(/Yes/); + userEvent.click(submitButton); + expect(screen.getByText(/Yes/)).toBeInTheDocument(); + }); + + userEvent.click(screen.queryByText(/Yes/)); + + await waitFor(() => { + expect(screen.queryByText(/Warning/)).not.toBeInTheDocument(); + }); + }); + + expect(api.postCourseReset).toHaveBeenCalled(); + const alertText = screen.getByText(/An error occurred resetting course for user/); + expect(alertText).toBeInTheDocument(); + const dismiss = screen.getByText(/dismiss/i); + userEvent.click(dismiss); + await waitFor(() => { + expect(alertText).not.toBeInTheDocument(); + }); }); }); diff --git a/src/users/data/api.js b/src/users/data/api.js index 4e20348a8..45c38c9dd 100644 --- a/src/users/data/api.js +++ b/src/users/data/api.js @@ -781,3 +781,43 @@ export async function getOrderHistory(username) { }; } } + +export async function getLearnerCourseResetList(username) { + try { + const { data } = await getAuthenticatedHttpClient().get(AppUrls.courseResetUrl(username)); + return data; + } catch (error) { + return { + errors: [ + { + code: null, + dismissible: true, + text: 'There was an error retrieving list of course reset for the user', + type: 'danger', + topic: 'courseReset', + }, + ], + }; + } +} + +export async function postCourseReset(username, courseID) { + try { + const { data } = await getAuthenticatedHttpClient().post(AppUrls.courseResetUrl(username), { + course_id: courseID, + }); + return data; + } catch (error) { + return { + errors: [ + { + code: null, + dismissible: true, + text: 'An error occurred when resetting user\'s course', + type: 'danger', + topic: 'courseReset', + }, + ], + }; + } +} diff --git a/src/users/data/api.test.js b/src/users/data/api.test.js index 890da6b1d..ec5c5ba31 100644 --- a/src/users/data/api.test.js +++ b/src/users/data/api.test.js @@ -36,6 +36,7 @@ describe('API', () => { const programRecordsUrl = urls.getLearnerRecordsUrl(); const retirementApiUrl = urls.userRetirementUrl(); const orderHistoryApiUrl = urls.getOrderHistoryUrl(); + const courseResetUrl = urls.courseResetUrl(testUsername); let mockAdapter; @@ -1267,4 +1268,82 @@ describe('API', () => { expect(result).toEqual(expectedError); }); }); + + describe('Course Reset', () => { + it('should return course reset list for a user', async () => { + const expectedData = [ + { + course_id: 'course-v1:edX+DemoX+Demo_Course', + display_name: 'Demonstration Course', + can_reset: false, + status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', + }, + { + course_id: 'course-v1:EdxOrg+EDX101+2024_Q1', + display_name: 'Intro to edx', + can_reset: true, + status: 'Available', + }, + ]; + + mockAdapter.onGet(courseResetUrl).reply(200, expectedData); + + const result = await api.getLearnerCourseResetList(testUsername); + + expect(result).toEqual(expectedData); + }); + + it('should return an empty array when an error occurs', async () => { + const expectedError = { + errors: [ + { + code: null, + dismissible: true, + text: 'There was an error retrieving list of course reset for the user', + type: 'danger', + topic: 'courseReset', + }, + ], + }; + mockAdapter.onGet().reply(() => throwError(404, '')); + + const result = await api.getLearnerCourseResetList(testUsername); + + expect(result).toEqual(expectedError); + }); + + it('should post a course reset', async () => { + const expectedData = { + course_id: 'course-v1:EdxOrg+EDX101+2024_Q1', + display_name: 'Intro to edx', + can_reset: false, + status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', + }; + + mockAdapter.onPost(courseResetUrl).reply(201, expectedData); + + const result = await api.postCourseReset(testUsername, 'course-v1:EdxOrg+EDX101+2024_Q1'); + + expect(result).toEqual(expectedData); + }); + }); + + it('returns a 400 error', async () => { + const expectedError = { + errors: [ + { + code: null, + dismissible: true, + text: 'An error occurred when resetting user\'s course', + type: 'danger', + topic: 'courseReset', + }, + ], + }; + mockAdapter.onPost().reply(() => throwError(400, '')); + + const result = await api.postCourseReset(testUsername); + + expect(result).toEqual(expectedError); + }); }); diff --git a/src/users/data/test/courseReset.js b/src/users/data/test/courseReset.js new file mode 100644 index 000000000..12078ae8e --- /dev/null +++ b/src/users/data/test/courseReset.js @@ -0,0 +1,27 @@ +export const expectedGetData = [ + { + course_id: 'course-v1:edX+DemoX+Demo_Course', + display_name: 'Demonstration Course', + can_reset: false, + status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', + }, + { + course_id: 'course-v1:EdxOrg+EDX101+2024_Q1', + display_name: 'Intro to edx', + can_reset: true, + status: 'Available', + }, + { + course_id: 'course-v1:EdxOrg+EDX201+2024_Q2', + display_name: 'Intro to new course', + can_reset: false, + status: 'in progress', + }, +]; + +export const expectedPostData = { + course_id: 'course-v1:EdxOrg+EDX101+2024_Q1', + display_name: 'Intro to edx', + can_reset: false, + status: 'Enqueued - Created 2024-02-28 11:29:06.318091+00:00 by edx', +}; diff --git a/src/users/data/urls.js b/src/users/data/urls.js index 1a45ffad8..ea63a5d29 100644 --- a/src/users/data/urls.js +++ b/src/users/data/urls.js @@ -111,3 +111,5 @@ export const getUserCredentialsUrl = () => `${CREDENTIALS_BASE_URL}/api/v2/crede export const getLearnerRecordsUrl = () => `${CREDENTIALS_BASE_URL}/records/api/v1/program_records`; export const getOrderHistoryUrl = () => `${ECOMMERCE_BASE_URL}/api/v2/orders`; + +export const courseResetUrl = (username) => `${LMS_BASE_URL}/support/course_reset/${username}`; From 4dc16cd227f75eca0455193b60b8c0f264cc2259 Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 4 Mar 2024 15:14:36 +0100 Subject: [PATCH 3/6] feat: poll course reset list --- src/users/CourseReset.jsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index 46e95710c..bd14ef3ab 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -17,6 +17,24 @@ function CourseReset({ username, intl }) { const [courseResetData, setCourseResetData] = useState([]); const [error, setError] = useState(''); const [isOpen, open, close] = useToggle(false); + const POLLING_INTERVAL = 10000; + + useEffect(() => { + // check if there is an enqueued or in progress course reset + const shouldPoll = courseResetData.some((course) => { + const status = course.status.toLowerCase(); + return status.includes('in progress') || status.includes('enqueued'); + }); + + let intervalId; + if (shouldPoll) { + intervalId = setInterval(async () => { + const data = await getLearnerCourseResetList(username); + setCourseResetData(data); + }, POLLING_INTERVAL); + } + return () => clearInterval(intervalId); + }, [courseResetData]); const handleSubmit = async (courseID) => { setError(null); From d20228398149d549bb856b13b12e589d9f3c34ef Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 4 Mar 2024 22:09:06 +0100 Subject: [PATCH 4/6] feat: add polling test --- src/users/CourseReset.jsx | 22 +++++++++++++-------- src/users/CourseReset.test.jsx | 36 +++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index bd14ef3ab..363831928 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -54,18 +54,24 @@ function CourseReset({ username, intl }) { close(); }; - useEffect(async () => { - const handleRequest = async () => { + useEffect(() => { + let isMounted = true; + const fetchData = async () => { const data = await getLearnerCourseResetList(username); - if (data.length) { - setCourseResetData(data); - } else { - setCourseResetData([]); - setError(data.errors[0].text); + if (isMounted) { + if (data.length) { + setCourseResetData(data); + } else if (data && data.errors) { + setCourseResetData([]); + setError(data.errors[0]?.text); + } } }; - handleRequest(); + fetchData(); + return () => { + isMounted = false; + }; }, []); const renderResetData = courseResetData.map((data) => { diff --git a/src/users/CourseReset.test.jsx b/src/users/CourseReset.test.jsx index a2ce31c4d..774edfe44 100644 --- a/src/users/CourseReset.test.jsx +++ b/src/users/CourseReset.test.jsx @@ -36,7 +36,6 @@ describe('CourseReset', () => { await waitFor(() => { screen = render(); }); - screen.debug(undefined, 300000000000); const btn = screen.getByText('Reset', { selector: 'button' }); userEvent.click(btn); await waitFor(() => { @@ -53,6 +52,41 @@ describe('CourseReset', () => { expect(postRequest).toHaveBeenCalled(); }); + it('polls new data', async () => { + jest.useFakeTimers(); + const data = [{ + course_id: 'course-v1:edX+DemoX+Demo_Course', + display_name: 'Demonstration Course', + can_reset: false, + status: 'In progress - Created 2024-02-28 11:29:06.318091+00:00 by edx', + }]; + + const updatedData = [{ + course_id: 'course-v1:edX+DemoX+Demo_Course', + display_name: 'Demonstration Course', + can_reset: false, + status: 'Completed by Support 2024-02-28 11:29:06.318091+00:00 by edx', + }]; + + jest + .spyOn(api, 'getLearnerCourseResetList') + .mockImplementationOnce(() => Promise.resolve(data)) + .mockImplementationOnce(() => Promise.resolve(updatedData)); + const user = 'John Doe'; + let screen; + await act(async () => { + screen = render(); + }); + + const inProgressText = screen.getByText(/in progress/i); + expect(inProgressText).toBeInTheDocument(); + + jest.advanceTimersByTime(10000); + + const completedText = await screen.findByText(/Completed by/i); + expect(completedText).toBeInTheDocument(); + }); + it('returns an empty table if it cannot fetch course reset list', async () => { jest .spyOn(api, 'getLearnerCourseResetList') From a1d41ff1ab3c569d0cce0d556d80b4c10ec80315 Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 11 Mar 2024 15:38:24 +0100 Subject: [PATCH 5/6] chore: address CR --- src/users/CourseReset.jsx | 73 ++++++++++++++++++---------------- src/users/CourseReset.test.jsx | 27 +++++++------ 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index 363831928..73dc8c312 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -2,7 +2,7 @@ import { Alert, AlertModal, Button, useToggle, ActionRow, } from '@edx/paragon'; import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { injectIntl, intlShape, @@ -20,23 +20,49 @@ function CourseReset({ username, intl }) { const POLLING_INTERVAL = 10000; useEffect(() => { - // check if there is an enqueued or in progress course reset - const shouldPoll = courseResetData.some((course) => { - const status = course.status.toLowerCase(); + let isMounted = true; + + const fetchData = async () => { + const data = await getLearnerCourseResetList(username); + if (isMounted) { + if (data.length) { + setCourseResetData(data); + } else if (data && data.errors) { + setCourseResetData([]); + setError(data.errors[0]?.text); + } + } + }; + + const shouldPoll = courseResetData.some((data) => { + const status = data.status.toLowerCase(); return status.includes('in progress') || status.includes('enqueued'); }); let intervalId; - if (shouldPoll) { - intervalId = setInterval(async () => { - const data = await getLearnerCourseResetList(username); - setCourseResetData(data); - }, POLLING_INTERVAL); + const initializeAndPoll = async () => { + if (!courseResetData.length) { + await fetchData(); // Initial data fetch + } + + if (shouldPoll) { + intervalId = setInterval(() => { + fetchData(); + }, POLLING_INTERVAL); + } + }; + + if (isMounted) { + initializeAndPoll(); // Execute initial fetch and start polling if necessary } - return () => clearInterval(intervalId); - }, [courseResetData]); - const handleSubmit = async (courseID) => { + return () => { + isMounted = false; + clearInterval(intervalId); + }; + }, [courseResetData, username]); + + const handleSubmit = useCallback(async (courseID) => { setError(null); const data = await postCourseReset(username, courseID); if (data && !data.errors) { @@ -52,27 +78,7 @@ function CourseReset({ username, intl }) { setError(data.errors[0].text); } close(); - }; - - useEffect(() => { - let isMounted = true; - const fetchData = async () => { - const data = await getLearnerCourseResetList(username); - if (isMounted) { - if (data.length) { - setCourseResetData(data); - } else if (data && data.errors) { - setCourseResetData([]); - setError(data.errors[0]?.text); - } - } - }; - - fetchData(); - return () => { - isMounted = false; - }; - }, []); + }, [username, courseResetData]); const renderResetData = courseResetData.map((data) => { const updatedData = { @@ -164,7 +170,6 @@ function CourseReset({ username, intl }) { }, ]} data={renderResetData} - styleName="custom-table" /> ); diff --git a/src/users/CourseReset.test.jsx b/src/users/CourseReset.test.jsx index 774edfe44..55caceccf 100644 --- a/src/users/CourseReset.test.jsx +++ b/src/users/CourseReset.test.jsx @@ -131,22 +131,23 @@ describe('CourseReset', () => { await act(async () => { screen = render(); - await waitFor(() => { - const btn = screen.getByText('Reset', { selector: 'button' }); - userEvent.click(btn); - }); + }); - await waitFor(() => { - const submitButton = screen.getByText(/Yes/); - userEvent.click(submitButton); - expect(screen.getByText(/Yes/)).toBeInTheDocument(); - }); + await waitFor(() => { + const btn = screen.getByText('Reset', { selector: 'button' }); + userEvent.click(btn); + }); - userEvent.click(screen.queryByText(/Yes/)); + await waitFor(() => { + const submitButton = screen.getByText(/Yes/); + userEvent.click(submitButton); + expect(screen.getByText(/Yes/)).toBeInTheDocument(); + }); - await waitFor(() => { - expect(screen.queryByText(/Warning/)).not.toBeInTheDocument(); - }); + userEvent.click(screen.queryByText(/Yes/)); + + await waitFor(() => { + expect(screen.queryByText(/Warning/)).not.toBeInTheDocument(); }); expect(api.postCourseReset).toHaveBeenCalled(); From 1cd0f79bfa8e338df6c24a669bdb85432adfc77b Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 11 Mar 2024 16:09:25 +0100 Subject: [PATCH 6/6] feat: add styleName for table component --- src/users/CourseReset.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/users/CourseReset.jsx b/src/users/CourseReset.jsx index 73dc8c312..938415933 100644 --- a/src/users/CourseReset.jsx +++ b/src/users/CourseReset.jsx @@ -170,6 +170,7 @@ function CourseReset({ username, intl }) { }, ]} data={renderResetData} + styleName="custom-table" /> );