From 1c3adee6e1df3f11ff4793daace74e72214888d6 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Thu, 6 Feb 2025 18:52:53 -0500 Subject: [PATCH 1/6] fix: assign button grey out --- .../assignment-modal/AssignmentModalContent.jsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx index c64b885df..9440be580 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx @@ -59,28 +59,26 @@ const AssignmentModalContent = ({ setDropdownToggleLabel, setGroupMemberEmails, }); - const handleEmailAddressInputChange = (e) => { - const inputValue = e.target.value; - setEmailAddressesInputValue(inputValue); - }; + const handleEmailAddressesChanged = useCallback((value) => { if (!value) { setLearnerEmails([]); - onEmailAddressesChange([]); return; } const emails = value.split('\n').filter((email) => email.trim().length > 0); setLearnerEmails(emails); - }, [onEmailAddressesChange]); + }, []); const debouncedHandleEmailAddressesChanged = useMemo( () => debounce(handleEmailAddressesChanged, EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY), [handleEmailAddressesChanged], ); - useEffect(() => { - debouncedHandleEmailAddressesChanged(emailAddressesInputValue); - }, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]); + const handleEmailAddressInputChange = (e) => { + const inputValue = e.target.value; + debouncedHandleEmailAddressesChanged(inputValue); + setEmailAddressesInputValue(inputValue); + }; useEffect(() => { handleGroupsChanged(checkedGroups); From d4acede19d042de44f77e33c51cb0956fa58b955 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 7 Feb 2025 15:55:57 -0500 Subject: [PATCH 2/6] refactor: CourseCard tests --- .../cards/tests/CourseCard.test.jsx | 648 +++++++++--------- 1 file changed, 341 insertions(+), 307 deletions(-) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 389ff0af6..6c3e8f101 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -255,7 +255,7 @@ const CourseCardWrapper = ({ ); }; -describe('Course card works as expected', () => { +describe('CourseCard', () => { const mockAllocateContentAssignments = jest.spyOn(EnterpriseAccessApiService, 'allocateContentAssignments'); // Helper function to find the assignment error modal after failed allocation attempt @@ -397,337 +397,371 @@ describe('Course card works as expected', () => { expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); }); - test.each([ - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: 'content_not_in_catalog', - shouldRetryAllocationAfterException: false, // no ability to retry after this error - courseImportantDates: { - courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts:', - }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: 'not_enough_value_in_subsidy', - shouldRetryAllocationAfterException: false, - courseImportantDates: { - courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started:', - }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: 'not_enough_value_in_subsidy', - shouldRetryAllocationAfterException: true, - courseImportantDates: { - courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts:', - }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: 'policy_spend_limit_reached', - shouldRetryAllocationAfterException: false, - courseImportantDates: { - courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started:', - }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: 'policy_spend_limit_reached', - shouldRetryAllocationAfterException: true, - courseImportantDates: { - courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts:', - }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: null, - shouldRetryAllocationAfterException: false, - courseImportantDates: { - courseStartDate: pastStartDate, - expectedCourseStartText: 'Course started:', + describe('assignments submission', () => { + const setupAssignments = ({ + hasAllocationException, + allocationExceptionReason, + courseImportantDates, + }) => { + const mockUpdatedLearnerAssignments = [mockLearnerEmails[0]]; + const mockNoChangeLearnerAssignments = [mockLearnerEmails[1]]; + const mockCreatedLearnerAssignments = mockLearnerEmails.slice(2).map(learnerEmail => ({ + uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f', + assignment_configuration: 'fd456a98-653b-41e9-94d1-94d7b136832a', + learner_email: learnerEmail, + lms_user_id: 0, + content_key: 'string', + content_title: 'string', + content_quantity: 0, + state: 'allocated', + transaction_uuid: '3a6bcbed-b7dc-4791-84fe-b20f12be4001', + last_notification_at: '2019-08-24T14:15:22Z', + actions: [], + })); + + if (hasAllocationException) { + // mock Axios error + mockAllocateContentAssignments.mockRejectedValue({ + customAttributes: { + httpErrorStatus: allocationExceptionReason ? 422 : 500, + httpErrorResponseData: JSON.stringify([{ reason: allocationExceptionReason }]), + }, + }); + } else { + mockAllocateContentAssignments.mockResolvedValue({ + data: { + updated: mockUpdatedLearnerAssignments, + created: mockCreatedLearnerAssignments, + no_change: mockNoChangeLearnerAssignments, + }, + }); + } + const mockInvalidateQueries = jest.fn(); + useQueryClient.mockReturnValue({ + invalidateQueries: mockInvalidateQueries, + }); + const { + courseStartDate, expectedCourseStartText, + } = courseImportantDates; + const props = { + original: { + ...defaultProps.original, + normalized_metadata: { + ...defaultProps.original.normalized_metadata, + start_date: courseStartDate, + }, + courseRuns: [{ + ...defaultProps.original.courseRuns[0], + start: courseStartDate, + }, + ], + advertised_course_run: { + ...defaultProps.original.advertised_course_run, + start: courseStartDate, + }, + }, + }; + return { + props, + expectedCourseStartText, + courseStartDate, + mockInvalidateQueries, + mockCreatedLearnerAssignments, + mockUpdatedLearnerAssignments, + mockNoChangeLearnerAssignments, + }; + }; + + const renderAssignmentModal = ({ props }) => { + renderWithRouter(); + const assignCourseCTA = getButtonElement('Assign'); + expect(assignCourseCTA).toBeInTheDocument(); + + userEvent.click(assignCourseCTA); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + userEvent.click(screen.getByText(enrollByDropdownText)); + + const assignmentModal = within(screen.getByRole('dialog')); + return assignmentModal; + }; + + test.each([ + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: 'content_not_in_catalog', + shouldRetryAllocationAfterException: false, // no ability to retry after this error + courseImportantDates: { + courseStartDate: futureStartDate, + expectedCourseStartText: 'Course starts:', + }, }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: true, - allocationExceptionReason: null, - shouldRetryAllocationAfterException: true, - courseImportantDates: { - courseStartDate: futureStartDate, - expectedCourseStartText: 'Course starts:', + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: 'not_enough_value_in_subsidy', + shouldRetryAllocationAfterException: false, + courseImportantDates: { + courseStartDate: pastStartDate, + expectedCourseStartText: 'Course started:', + }, }, - }, - { - shouldSubmitAssignments: true, - hasAllocationException: false, - courseImportantDates: { - courseStartDate: null, - expectedCourseStartText: '', + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: 'not_enough_value_in_subsidy', + shouldRetryAllocationAfterException: true, + courseImportantDates: { + courseStartDate: futureStartDate, + expectedCourseStartText: 'Course starts:', + }, }, - }, - { - shouldSubmitAssignments: false, - hasAllocationException: false, - courseImportantDates: { - courseStartDate: null, - expectedCourseStartText: '', + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: 'policy_spend_limit_reached', + shouldRetryAllocationAfterException: false, + courseImportantDates: { + courseStartDate: pastStartDate, + expectedCourseStartText: 'Course started:', + }, }, - }, - ])('opens assignment modal, fills out information, and submits assignments accordingly - with success or with an exception (%s)', async ({ - shouldSubmitAssignments, - hasAllocationException, - allocationExceptionReason, - shouldRetryAllocationAfterException, - courseImportantDates, - }) => { - const mockUpdatedLearnerAssignments = [mockLearnerEmails[0]]; - const mockNoChangeLearnerAssignments = [mockLearnerEmails[1]]; - const mockCreatedLearnerAssignments = mockLearnerEmails.slice(2).map(learnerEmail => ({ - uuid: '095be615-a8ad-4c33-8e9c-c7612fbf6c9f', - assignment_configuration: 'fd456a98-653b-41e9-94d1-94d7b136832a', - learner_email: learnerEmail, - lms_user_id: 0, - content_key: 'string', - content_title: 'string', - content_quantity: 0, - state: 'allocated', - transaction_uuid: '3a6bcbed-b7dc-4791-84fe-b20f12be4001', - last_notification_at: '2019-08-24T14:15:22Z', - actions: [], - })); - - if (hasAllocationException) { - // mock Axios error - mockAllocateContentAssignments.mockRejectedValue({ - customAttributes: { - httpErrorStatus: allocationExceptionReason ? 422 : 500, - httpErrorResponseData: JSON.stringify([{ reason: allocationExceptionReason }]), + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: 'policy_spend_limit_reached', + shouldRetryAllocationAfterException: true, + courseImportantDates: { + courseStartDate: futureStartDate, + expectedCourseStartText: 'Course starts:', }, - }); - } else { - mockAllocateContentAssignments.mockResolvedValue({ - data: { - updated: mockUpdatedLearnerAssignments, - created: mockCreatedLearnerAssignments, - no_change: mockNoChangeLearnerAssignments, + }, + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: null, + shouldRetryAllocationAfterException: false, + courseImportantDates: { + courseStartDate: pastStartDate, + expectedCourseStartText: 'Course started:', }, - }); - } - const mockInvalidateQueries = jest.fn(); - useQueryClient.mockReturnValue({ - invalidateQueries: mockInvalidateQueries, - }); - const { - courseStartDate, expectedCourseStartText, - } = courseImportantDates; - const props = { - original: { - ...defaultProps.original, - normalized_metadata: { - ...defaultProps.original.normalized_metadata, - start_date: courseStartDate, + }, + { + shouldSubmitAssignments: true, + hasAllocationException: true, + allocationExceptionReason: null, + shouldRetryAllocationAfterException: true, + courseImportantDates: { + courseStartDate: futureStartDate, + expectedCourseStartText: 'Course starts:', }, - courseRuns: [{ - ...defaultProps.original.courseRuns[0], - start: courseStartDate, + }, + { + shouldSubmitAssignments: true, + hasAllocationException: false, + courseImportantDates: { + courseStartDate: null, + expectedCourseStartText: '', }, - ], - advertised_course_run: { - ...defaultProps.original.advertised_course_run, - start: courseStartDate, + }, + { + shouldSubmitAssignments: false, + hasAllocationException: false, + courseImportantDates: { + courseStartDate: null, + expectedCourseStartText: '', }, }, - }; - renderWithRouter(); - const assignCourseCTA = getButtonElement('Assign'); - expect(assignCourseCTA).toBeInTheDocument(); - - userEvent.click(assignCourseCTA); - expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); - userEvent.click(screen.getByText(enrollByDropdownText)); - - const assignmentModal = within(screen.getByRole('dialog')); - - expect(assignmentModal.getByText('Assign this course')).toBeInTheDocument(); - expect(assignmentModal.getByText('Use Learner Credit to assign this course')).toBeInTheDocument(); - - // Verify course card is displayed WITHOUT footer actions - const modalCourseCard = within(assignmentModal.getByText('Course Title').closest('.pgn__card')); - expect(modalCourseCard.getByText(defaultProps.original.title)).toBeInTheDocument(); - expect(modalCourseCard.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); - expect(modalCourseCard.getByText('$100')).toBeInTheDocument(); - expect(modalCourseCard.getByText('Per learner price')).toBeInTheDocument(); - expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); - const cardImage = modalCourseCard.getByAltText(imageAltText); - expect(cardImage).toBeInTheDocument(); - expect(cardImage.src).toBeDefined(); - expect(modalCourseCard.queryByText('View course', { selector: 'a' })).not.toBeInTheDocument(); - expect(getButtonElement('Assign', { screenOverride: modalCourseCard, isQueryByRole: true })).not.toBeInTheDocument(); + ])('opens assignment modal, fills out information, and submits assignments accordingly - with success or with an exception (%s)', async ({ + shouldSubmitAssignments, + hasAllocationException, + allocationExceptionReason, + shouldRetryAllocationAfterException, + courseImportantDates, + }) => { + const { + props, expectedCourseStartText, courseStartDate, mockInvalidateQueries, + mockCreatedLearnerAssignments, + mockUpdatedLearnerAssignments, + mockNoChangeLearnerAssignments, + } = setupAssignments({ + hasAllocationException, + allocationExceptionReason, + courseImportantDates, + }); - // Verify empty state - expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); - const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); - expect(textareaInputLabel).toBeInTheDocument(); - const textareaInput = textareaInputLabel.closest('textarea'); - expect(textareaInput).toBeInTheDocument(); - expect(assignmentModal.getByText('To add more than one learner, enter one email address per line.')).toBeInTheDocument(); - expect(assignmentModal.getByText('Pay by Learner Credit')).toBeInTheDocument(); - expect(assignmentModal.getByText('Summary')).toBeInTheDocument(); - expect(assignmentModal.getByText("You haven't entered any learners yet.")).toBeInTheDocument(); - expect(assignmentModal.getByText('Add learner emails to get started.')).toBeInTheDocument(); - expect(assignmentModal.getByText(`Learner Credit Budget: ${mockSubsidyAccessPolicy.displayName}`)).toBeInTheDocument(); - expect(assignmentModal.getByText('Available balance')).toBeInTheDocument(); - const expectedAvailableBalance = formatPrice(mockSubsidyAccessPolicy.aggregates.spendAvailableUsd); - expect(assignmentModal.getByText(expectedAvailableBalance)).toBeInTheDocument(); - - // Verify important dates - expect(assignmentModal.getByText('Enroll-by date:')).toBeInTheDocument(); - expect(assignmentModal.getByText( - dayjs.unix(enrollByTimestamp).format(DATETIME_FORMAT), - )).toBeInTheDocument(); - if (courseStartDate) { - expect(assignmentModal.getByText(expectedCourseStartText)).toBeInTheDocument(); + const assignmentModal = renderAssignmentModal({ props }); + + expect(assignmentModal.getByText('Assign this course')).toBeInTheDocument(); + expect(assignmentModal.getByText('Use Learner Credit to assign this course')).toBeInTheDocument(); + + // Verify course card is displayed WITHOUT footer actions + const modalCourseCard = within(assignmentModal.getByText('Course Title').closest('.pgn__card')); + expect(modalCourseCard.getByText(defaultProps.original.title)).toBeInTheDocument(); + expect(modalCourseCard.getByText(defaultProps.original.partners[0].name)).toBeInTheDocument(); + expect(modalCourseCard.getByText('$100')).toBeInTheDocument(); + expect(modalCourseCard.getByText('Per learner price')).toBeInTheDocument(); + expect(screen.getByText(enrollByDropdownText)).toBeInTheDocument(); + const cardImage = modalCourseCard.getByAltText(imageAltText); + expect(cardImage).toBeInTheDocument(); + expect(cardImage.src).toBeDefined(); + expect(modalCourseCard.queryByText('View course', { selector: 'a' })).not.toBeInTheDocument(); + expect(getButtonElement('Assign', { screenOverride: modalCourseCard, isQueryByRole: true })).not.toBeInTheDocument(); + + // Verify empty state + expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); + const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); + expect(textareaInputLabel).toBeInTheDocument(); + const textareaInput = textareaInputLabel.closest('textarea'); + expect(textareaInput).toBeInTheDocument(); + expect(assignmentModal.getByText('To add more than one learner, enter one email address per line.')).toBeInTheDocument(); + expect(assignmentModal.getByText('Pay by Learner Credit')).toBeInTheDocument(); + expect(assignmentModal.getByText('Summary')).toBeInTheDocument(); + expect(assignmentModal.getByText("You haven't entered any learners yet.")).toBeInTheDocument(); + expect(assignmentModal.getByText('Add learner emails to get started.')).toBeInTheDocument(); + expect(assignmentModal.getByText(`Learner Credit Budget: ${mockSubsidyAccessPolicy.displayName}`)).toBeInTheDocument(); + expect(assignmentModal.getByText('Available balance')).toBeInTheDocument(); + const expectedAvailableBalance = formatPrice(mockSubsidyAccessPolicy.aggregates.spendAvailableUsd); + expect(assignmentModal.getByText(expectedAvailableBalance)).toBeInTheDocument(); + + // Verify important dates + expect(assignmentModal.getByText('Enroll-by date:')).toBeInTheDocument(); expect(assignmentModal.getByText( - dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT), + dayjs.unix(enrollByTimestamp).format(DATETIME_FORMAT), )).toBeInTheDocument(); - } + if (courseStartDate) { + expect(assignmentModal.getByText(expectedCourseStartText)).toBeInTheDocument(); + expect(assignmentModal.getByText( + dayjs(courseStartDate).format(SHORT_MONTH_DATE_FORMAT), + )).toBeInTheDocument(); + } - // Verify collapsible - expect(assignmentModal.getByText('How assigning this course works')).toBeInTheDocument(); - expect(assignmentModal.getByText('Next steps for assigned learners')).toBeInTheDocument(); - expect(assignmentModal.getByText('Learners will be notified of this course assignment by email.')).toBeInTheDocument(); - const budgetImpact = assignmentModal.getByText('Impact on your Learner Credit budget'); - expect(budgetImpact).toBeInTheDocument(); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); - expect(assignmentModal.queryByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).not.toBeInTheDocument(); - userEvent.click(budgetImpact); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); - expect(assignmentModal.getByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).toBeInTheDocument(); - const managingAssignment = assignmentModal.getByText('Managing this assignment'); - expect(managingAssignment).toBeInTheDocument(); - expect(assignmentModal.queryByText('You will be able to monitor the status of this assignment', { exact: false })).not.toBeInTheDocument(); - userEvent.click(managingAssignment); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(3); - expect(assignmentModal.getByText('You will be able to monitor the status of this assignment', { exact: false })).toBeInTheDocument(); - const nextSteps = assignmentModal.getByText('Next steps for assigned learners'); - userEvent.click(nextSteps); - expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(4); - - // Verify modal footer - expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); - const cancelAssignmentCTA = getButtonElement('Cancel', { screenOverride: assignmentModal }); - expect(cancelAssignmentCTA).toBeInTheDocument(); - const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); - expect(submitAssignmentCTA).toBeInTheDocument(); - - if (shouldSubmitAssignments) { + // Verify collapsible + expect(assignmentModal.getByText('How assigning this course works')).toBeInTheDocument(); + expect(assignmentModal.getByText('Next steps for assigned learners')).toBeInTheDocument(); + expect(assignmentModal.getByText('Learners will be notified of this course assignment by email.')).toBeInTheDocument(); + const budgetImpact = assignmentModal.getByText('Impact on your Learner Credit budget'); + expect(budgetImpact).toBeInTheDocument(); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + expect(assignmentModal.queryByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).not.toBeInTheDocument(); + userEvent.click(budgetImpact); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(2); + expect(assignmentModal.getByText('The total assignment cost will be earmarked as "assigned" funds', { exact: false })).toBeInTheDocument(); + const managingAssignment = assignmentModal.getByText('Managing this assignment'); + expect(managingAssignment).toBeInTheDocument(); + expect(assignmentModal.queryByText('You will be able to monitor the status of this assignment', { exact: false })).not.toBeInTheDocument(); + userEvent.click(managingAssignment); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(3); + expect(assignmentModal.getByText('You will be able to monitor the status of this assignment', { exact: false })).toBeInTheDocument(); + const nextSteps = assignmentModal.getByText('Next steps for assigned learners'); + userEvent.click(nextSteps); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(4); + + // Verify modal footer + expect(assignmentModal.getByText('Help Center: Course Assignments')).toBeInTheDocument(); + const cancelAssignmentCTA = getButtonElement('Cancel', { screenOverride: assignmentModal }); + expect(cancelAssignmentCTA).toBeInTheDocument(); + const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA).toBeInTheDocument(); + + if (shouldSubmitAssignments) { // Verify textarea receives input - userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); - expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); + userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); + expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); - // Verify assignment summary UI updates - await waitFor(() => { - expect(assignmentModal.getByText(`Summary (${mockLearnerEmails.length})`)).toBeInTheDocument(); - }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); - expect(assignmentModal.queryByText('You haven\'t entered any learners yet.')).not.toBeInTheDocument(); - expect(assignmentModal.queryByText('Add learner emails to get started.')).not.toBeInTheDocument(); - mockLearnerEmails.forEach((learnerEmail) => { - expect(assignmentModal.getByText(learnerEmail)).toBeInTheDocument(); - }); - expect(assignmentModal.getByText('Total assignment cost')).toBeInTheDocument(); - const expectedAssignmentCost = mockLearnerEmails.length * defaultProps.original.normalized_metadata.content_price; - expect(assignmentModal.getByText(formatPrice(expectedAssignmentCost))).toBeInTheDocument(); - expect(assignmentModal.getByText('Remaining after assignment')).toBeInTheDocument(); - const expectedBalanceAfterAssignment = ( - mockSubsidyAccessPolicy.aggregates.spendAvailableUsd - expectedAssignmentCost - ); - expect(assignmentModal.getByText(formatPrice(expectedBalanceAfterAssignment))).toBeInTheDocument(); - - // Verify assignment is submitted successfully - userEvent.click(submitAssignmentCTA); - await waitFor(() => expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(1)); - expect(mockAllocateContentAssignments).toHaveBeenCalledWith( - mockSubsidyAccessPolicy.uuid, - expect.objectContaining({ - content_price_cents: 10000, - content_key: 'course-v1:edX+course-123x+3T2020', - learner_emails: mockLearnerEmails, - }), - ); - - // Verify error states - if (hasAllocationException) { - expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); - - // Assert the correct error modal is displayed - if (allocationExceptionReason === 'content_not_in_catalog') { - const assignmentErrorModal = getAssignmentErrorModal(); - expect(assignmentErrorModal.getByText(`This course is not in your ${mockSubsidyAccessPolicy.displayName} budget's catalog`)).toBeInTheDocument(); - const exitCTA = getButtonElement('Exit', { screenOverride: assignmentErrorModal }); - userEvent.click(exitCTA); - await waitFor(() => { + // Verify assignment summary UI updates + await waitFor(() => { + expect(assignmentModal.getByText(`Summary (${mockLearnerEmails.length})`)).toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); + expect(assignmentModal.queryByText('You haven\'t entered any learners yet.')).not.toBeInTheDocument(); + expect(assignmentModal.queryByText('Add learner emails to get started.')).not.toBeInTheDocument(); + mockLearnerEmails.forEach((learnerEmail) => { + expect(assignmentModal.getByText(learnerEmail)).toBeInTheDocument(); + }); + expect(assignmentModal.getByText('Total assignment cost')).toBeInTheDocument(); + const expectedAssignmentCost = mockLearnerEmails.length * defaultProps.original.normalized_metadata.content_price; + expect(assignmentModal.getByText(formatPrice(expectedAssignmentCost))).toBeInTheDocument(); + expect(assignmentModal.getByText('Remaining after assignment')).toBeInTheDocument(); + const expectedBalanceAfterAssignment = ( + mockSubsidyAccessPolicy.aggregates.spendAvailableUsd - expectedAssignmentCost + ); + expect(assignmentModal.getByText(formatPrice(expectedBalanceAfterAssignment))).toBeInTheDocument(); + + // Verify assignment is submitted successfully + userEvent.click(submitAssignmentCTA); + await waitFor(() => expect(mockAllocateContentAssignments).toHaveBeenCalledTimes(1)); + expect(mockAllocateContentAssignments).toHaveBeenCalledWith( + mockSubsidyAccessPolicy.uuid, + expect.objectContaining({ + content_price_cents: 10000, + content_key: 'course-v1:edX+course-123x+3T2020', + learner_emails: mockLearnerEmails, + }), + ); + + // Verify error states + if (hasAllocationException) { + expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); + + // Assert the correct error modal is displayed + if (allocationExceptionReason === 'content_not_in_catalog') { + const assignmentErrorModal = getAssignmentErrorModal(); + expect(assignmentErrorModal.getByText(`This course is not in your ${mockSubsidyAccessPolicy.displayName} budget's catalog`)).toBeInTheDocument(); + const exitCTA = getButtonElement('Exit', { screenOverride: assignmentErrorModal }); + userEvent.click(exitCTA); + await waitFor(() => { // Verify all modals close (error modal + assignment modal) - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(allocationExceptionReason)) { - const assignmentErrorModal = getAssignmentErrorModal(); - const errorModalTitle = 'Not enough balance'; - expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); - if (shouldRetryAllocationAfterException) { - await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + } else if (['not_enough_value_in_subsidy', 'policy_spend_limit_reached'].includes(allocationExceptionReason)) { + const assignmentErrorModal = getAssignmentErrorModal(); + const errorModalTitle = 'Not enough balance'; + expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); + if (shouldRetryAllocationAfterException) { + await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + } else { + await simulateClickErrorModalExit(assignmentErrorModal); + } } else { - await simulateClickErrorModalExit(assignmentErrorModal); + const assignmentErrorModal = getAssignmentErrorModal(); + const errorModalTitle = 'Something went wrong'; + expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); + if (shouldRetryAllocationAfterException) { + await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); + } else { + await simulateClickErrorModalExit(assignmentErrorModal); + expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); + } } } else { - const assignmentErrorModal = getAssignmentErrorModal(); - const errorModalTitle = 'Something went wrong'; - expect(assignmentErrorModal.getByText(errorModalTitle)).toBeInTheDocument(); - if (shouldRetryAllocationAfterException) { - await simulateClickErrorModalTryAgain(errorModalTitle, assignmentErrorModal); - expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); - } else { - await simulateClickErrorModalExit(assignmentErrorModal); - expect(sendEnterpriseTrackEvent).toHaveBeenCalled(); - } - } - } else { // Verify success state - expect(mockInvalidateQueries).toHaveBeenCalledTimes(2); - expect(mockInvalidateQueries).toHaveBeenCalledWith({ - queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), - }); - expect(mockInvalidateQueries).toHaveBeenCalledWith({ - queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseUUID), - }); - expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); - await waitFor(() => { + expect(mockInvalidateQueries).toHaveBeenCalledTimes(2); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), + }); + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: learnerCreditManagementQueryKeys.budgets(enterpriseUUID), + }); + expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); + await waitFor(() => { // Verify all modals close (error modal + assignment modal) - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - // Verify toast notification was displayed - expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledTimes(1); - expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledWith({ - totalLearnersAllocated: mockCreatedLearnerAssignments.length + mockUpdatedLearnerAssignments.length, - totalLearnersAlreadyAllocated: mockNoChangeLearnerAssignments.length, + // Verify toast notification was displayed + expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledTimes(1); + expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledWith({ + totalLearnersAllocated: mockCreatedLearnerAssignments.length + mockUpdatedLearnerAssignments.length, + totalLearnersAlreadyAllocated: mockNoChangeLearnerAssignments.length, + }); }); - }); - } - } else { + } + } else { // Otherwise, verify modal closes when cancel button is clicked - userEvent.click(cancelAssignmentCTA); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - } + userEvent.click(cancelAssignmentCTA); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + } + }); }); test.each([ From 7f93d4d78d5d44b9f20d21d884d930190f09b805 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 7 Feb 2025 16:35:47 -0500 Subject: [PATCH 3/6] test: assign button --- .../cards/tests/CourseCard.test.jsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 6c3e8f101..b65ff80d9 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -762,6 +762,47 @@ describe('CourseCard', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); } }); + + test('allows allocation if email address text area is empty', async () => { + const shouldSubmitAssignments = true; + const hasAllocationException = false; + const courseImportantDates = { + courseStartDate: null, + expectedCourseStartText: '', + }; + const { + props, expectedCourseStartText, courseStartDate, mockInvalidateQueries, + mockCreatedLearnerAssignments, + mockUpdatedLearnerAssignments, + mockNoChangeLearnerAssignments, + } = setupAssignments({ + hasAllocationException, + allocationExceptionReason: undefined, + courseImportantDates, + }); + + const assignmentModal = renderAssignmentModal({ props }); + + // Verify empty state + expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); + const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); + expect(textareaInputLabel).toBeInTheDocument(); + const textareaInput = textareaInputLabel.closest('textarea'); + expect(textareaInput).toBeInTheDocument(); + + const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA).toBeInTheDocument(); + + expect(submitAssignmentCTA).not.toBeDisabled(); + + userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); + expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); + + await waitFor(() => { + // Verify that assign button in footer is enabled + expect(submitAssignmentCTA).not.toBeDisabled(); + }); + }); }); test.each([ From 38b943d5a2a4e24ce39385fdab671c978f092979 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 7 Feb 2025 18:01:26 -0500 Subject: [PATCH 4/6] test: assignment modal --- .../cards/tests/CourseCard.test.jsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index b65ff80d9..ec3eaefef 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -793,15 +793,37 @@ describe('CourseCard', () => { const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); expect(submitAssignmentCTA).toBeInTheDocument(); - expect(submitAssignmentCTA).not.toBeDisabled(); + expect(submitAssignmentCTA).toBeDisabled(); + // Test user adding email addresses by typing into the input field userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); await waitFor(() => { - // Verify that assign button in footer is enabled + expect(assignmentModal.getByText(`Summary (${mockLearnerEmails.length})`)).toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 100 }); + + await waitFor(() => { + const submitAssignmentCTA2 = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA2).toBeInTheDocument(); + // Verify that assign button in footer is enabled expect(submitAssignmentCTA).not.toBeDisabled(); }); + + // Test user deleting the input field content by typing backspace + userEvent.type(textareaInput, '{backspace}'.repeat(mockLearnerEmails.join('\n').length)); + expect(textareaInput).toHaveValue(''); + + await waitFor(() => { + expect(assignmentModal.queryByText(`Summary (${mockLearnerEmails.length})`)).not.toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 100 }); + + await waitFor(() => { + const submitAssignmentCTA2 = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA2).toBeInTheDocument(); + // Verify that assign button in footer is enabled + expect(submitAssignmentCTA).toBeDisabled(); + }); }); }); From e9497776915e7084927f175b83062e97df0b3107 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Fri, 7 Feb 2025 18:28:34 -0500 Subject: [PATCH 5/6] test: email address assignment --- .../cards/tests/CourseCard.test.jsx | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index ec3eaefef..8bd284148 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -763,7 +763,7 @@ describe('CourseCard', () => { } }); - test('allows allocation if email address text area is empty', async () => { + test('prevents allocation if emails are empty', async () => { const shouldSubmitAssignments = true; const hasAllocationException = false; const courseImportantDates = { @@ -822,7 +822,103 @@ describe('CourseCard', () => { const submitAssignmentCTA2 = getButtonElement('Assign', { screenOverride: assignmentModal }); expect(submitAssignmentCTA2).toBeInTheDocument(); // Verify that assign button in footer is enabled - expect(submitAssignmentCTA).toBeDisabled(); + expect(submitAssignmentCTA2).toBeDisabled(); + }); + }); + + test('allows allocation if groups are assigned but emails are empty', async () => { + const shouldSubmitAssignments = true; + const hasAllocationException = false; + const courseImportantDates = { + courseStartDate: null, + expectedCourseStartText: '', + }; + const { + props, expectedCourseStartText, courseStartDate, mockInvalidateQueries, + mockCreatedLearnerAssignments, + mockUpdatedLearnerAssignments, + mockNoChangeLearnerAssignments, + } = setupAssignments({ + hasAllocationException, + allocationExceptionReason: undefined, + courseImportantDates, + }); + + useSubsidyAccessPolicy.mockReturnValue({ + data: { + ...mockSubsidyAccessPolicy, + aggregates: { + ...mockSubsidyAccessPolicy.aggregates, + spendAvailableUsd: 1000, + }, + }, + isLoading: false, + }); + + getGroupMemberEmails.mockReturnValue(['email@example.com', 'jhodge@example.com', '123@example.com']); + const assignmentModal = renderAssignmentModal({ props }); + + // Verify empty state + expect(assignmentModal.getByText('Assign to')).toBeInTheDocument(); + const textareaInputLabel = assignmentModal.getByLabelText('Learner email addresses'); + expect(textareaInputLabel).toBeInTheDocument(); + const textareaInput = textareaInputLabel.closest('textarea'); + expect(textareaInput).toBeInTheDocument(); + + const submitAssignmentCTA = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA).toBeInTheDocument(); + + expect(submitAssignmentCTA).toBeDisabled(); + + expect( + assignmentModal.getByText('Select one or more group to add its members to the assignment.'), + ).toBeInTheDocument(); + const dropdownMenu = assignmentModal.getByText('Select group'); + expect(dropdownMenu).toBeInTheDocument(); + userEvent.click(dropdownMenu); + const group1 = assignmentModal.getByText('Group 1 (2)'); + const group2 = assignmentModal.getByText('Group 2 (1)'); + expect(group1).toBeInTheDocument(); + expect(group2).toBeInTheDocument(); + + userEvent.click(group1); + userEvent.click(group2); + const applyButton = assignmentModal.getByText('Apply selections'); + + await waitFor(() => { + userEvent.click(applyButton); + expect(assignmentModal.getByText('2 groups selected')).toBeInTheDocument(); + expect(assignmentModal.getByText('email@example.com')).toBeInTheDocument(); + }); + + // Test user adding email addresses by typing into the input field + userEvent.type(textareaInput, mockLearnerEmails.join('{enter}')); + expect(textareaInput).toHaveValue(mockLearnerEmails.join('\n')); + + await waitFor(() => { + expect(assignmentModal.getByText(`Summary (${3 + mockLearnerEmails.length})`)).toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 100 }); + + await waitFor(() => { + const submitAssignmentCTA2 = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA2).toBeInTheDocument(); + // Verify that assign button in footer is enabled + expect(submitAssignmentCTA).not.toBeDisabled(); + }); + + // Test user deleting the input field content by typing backspace + userEvent.type(textareaInput, '{backspace}'.repeat(mockLearnerEmails.join('\n').length)); + expect(textareaInput).toHaveValue(''); + + await waitFor(() => { + expect(assignmentModal.queryByText('Summary (3)')).toBeInTheDocument(); + }, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 100 }); + + await waitFor(() => { + const submitAssignmentCTA2 = getButtonElement('Assign', { screenOverride: assignmentModal }); + expect(submitAssignmentCTA2).toBeInTheDocument(); + // Verify that assign button in footer is enabled + expect(submitAssignmentCTA2).not.toBeDisabled(); }); }); }); From 4ce070333b3826adf732008c8fc79bc1a883f970 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Mon, 10 Feb 2025 14:39:01 -0500 Subject: [PATCH 6/6] fix: lint --- .../cards/tests/CourseCard.test.jsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index 8bd284148..f5e9f14aa 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -679,7 +679,9 @@ describe('CourseCard', () => { expect(assignmentModal.getByText(learnerEmail)).toBeInTheDocument(); }); expect(assignmentModal.getByText('Total assignment cost')).toBeInTheDocument(); - const expectedAssignmentCost = mockLearnerEmails.length * defaultProps.original.normalized_metadata.content_price; + const expectedAssignmentCost = ( + mockLearnerEmails.length * defaultProps.original.normalized_metadata.content_price + ); expect(assignmentModal.getByText(formatPrice(expectedAssignmentCost))).toBeInTheDocument(); expect(assignmentModal.getByText('Remaining after assignment')).toBeInTheDocument(); const expectedBalanceAfterAssignment = ( @@ -764,17 +766,13 @@ describe('CourseCard', () => { }); test('prevents allocation if emails are empty', async () => { - const shouldSubmitAssignments = true; const hasAllocationException = false; const courseImportantDates = { courseStartDate: null, expectedCourseStartText: '', }; const { - props, expectedCourseStartText, courseStartDate, mockInvalidateQueries, - mockCreatedLearnerAssignments, - mockUpdatedLearnerAssignments, - mockNoChangeLearnerAssignments, + props, } = setupAssignments({ hasAllocationException, allocationExceptionReason: undefined, @@ -827,17 +825,13 @@ describe('CourseCard', () => { }); test('allows allocation if groups are assigned but emails are empty', async () => { - const shouldSubmitAssignments = true; const hasAllocationException = false; const courseImportantDates = { courseStartDate: null, expectedCourseStartText: '', }; const { - props, expectedCourseStartText, courseStartDate, mockInvalidateQueries, - mockCreatedLearnerAssignments, - mockUpdatedLearnerAssignments, - mockNoChangeLearnerAssignments, + props, } = setupAssignments({ hasAllocationException, allocationExceptionReason: undefined,