-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: display success toast on assignment allocation (#1083)
- Loading branch information
1 parent
16373b9
commit d518a4d
Showing
8 changed files
with
208 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ import { | |
import { getButtonElement, queryClient } from '../../test/testUtils'; | ||
|
||
import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; | ||
import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; | ||
|
||
jest.mock('@tanstack/react-query', () => ({ | ||
...jest.requireActual('@tanstack/react-query'), | ||
|
@@ -94,8 +95,17 @@ const mockSubsidyAccessPolicy = { | |
}; | ||
const mockLearnerEmails = ['[email protected]', '[email protected]']; | ||
|
||
const mockDisplaySuccessfulAssignmentToast = jest.fn(); | ||
const defaultBudgetDetailPageContextValue = { | ||
isSuccessfulAssignmentAllocationToastOpen: false, | ||
totalLearnersAssigned: undefined, | ||
displayToastForAssignmentAllocation: mockDisplaySuccessfulAssignmentToast, | ||
closeToastForAssignmentAllocation: jest.fn(), | ||
}; | ||
|
||
const CourseCardWrapper = ({ | ||
initialState = initialStoreState, | ||
budgetDetailPageContextValue = defaultBudgetDetailPageContextValue, | ||
...rest | ||
}) => { | ||
const store = getMockStore({ ...initialState }); | ||
|
@@ -109,7 +119,9 @@ const CourseCardWrapper = ({ | |
config: { ENTERPRISE_LEARNER_PORTAL_URL: mockLearnerPortal }, | ||
}} | ||
> | ||
<CourseCard {...rest} /> | ||
<BudgetDetailPageContext.Provider value={budgetDetailPageContextValue}> | ||
<CourseCard {...rest} /> | ||
</BudgetDetailPageContext.Provider> | ||
</AppContext.Provider> | ||
</Provider> | ||
</IntlProvider> | ||
|
@@ -221,22 +233,26 @@ describe('Course card works as expected', () => { | |
errorReason: 'not_enough_value_in_subsidy', | ||
shouldRetryAfterError: true, | ||
}, | ||
{ shouldSubmitAssignments: true, | ||
{ | ||
shouldSubmitAssignments: true, | ||
hasAllocationException: true, | ||
errorReason: 'policy_spend_limit_reached', | ||
shouldRetryAfterError: false, | ||
}, | ||
{ shouldSubmitAssignments: true, | ||
{ | ||
shouldSubmitAssignments: true, | ||
hasAllocationException: true, | ||
errorReason: 'policy_spend_limit_reached', | ||
shouldRetryAfterError: true, | ||
}, | ||
{ shouldSubmitAssignments: true, | ||
{ | ||
shouldSubmitAssignments: true, | ||
hasAllocationException: true, | ||
errorReason: null, | ||
shouldRetryAfterError: false, | ||
}, | ||
{ shouldSubmitAssignments: true, | ||
{ | ||
shouldSubmitAssignments: true, | ||
hasAllocationException: true, | ||
errorReason: null, | ||
shouldRetryAfterError: true, | ||
|
@@ -363,7 +379,7 @@ describe('Course card works as expected', () => { | |
// Verify error states | ||
if (hasAllocationException) { | ||
expect(getButtonElement('Try again', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'false'); | ||
|
||
// Assert the correct error modal is displayed | ||
if (errorReason === 'content_not_in_catalog') { | ||
const assignmentErrorModal = getAssignmentErrorModal(); | ||
|
@@ -393,17 +409,22 @@ describe('Course card works as expected', () => { | |
await simulateClickErrorModalExit(assignmentErrorModal); | ||
} | ||
} | ||
|
||
} else { | ||
// Verify success state | ||
expect(mockInvalidateQueries).toHaveBeenCalledTimes(1); | ||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ | ||
queryKey: learnerCreditManagementQueryKeys.budget(mockSubsidyAccessPolicy.uuid), | ||
}); | ||
expect(getButtonElement('Assigned', { screenOverride: assignmentModal })).toHaveAttribute('aria-disabled', 'true'); | ||
// Verify modal closes | ||
await waitFor(() => { | ||
// Verify all modals close (error modal + assignment modal) | ||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); | ||
|
||
// Verify toast notification was displayed | ||
expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledTimes(1); | ||
expect(mockDisplaySuccessfulAssignmentToast).toHaveBeenCalledWith({ | ||
totalLearnersAssigned: mockLearnerEmails.length, | ||
}); | ||
}); | ||
} | ||
} else { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...mponents/learner-credit-management/data/hooks/useSuccessfulAssignmentToastContextValue.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useCallback, useMemo, useState } from 'react'; | ||
|
||
const useSuccessfulAssignmentToastContextValue = () => { | ||
const [isToastOpen, setIsToastOpen] = useState(false); | ||
const [learnersAssignedCount, setLearnersAssignedCount] = useState(); | ||
|
||
const handleDisplayToast = useCallback(({ totalLearnersAssigned }) => { | ||
setIsToastOpen(true); | ||
setLearnersAssignedCount(totalLearnersAssigned); | ||
}, []); | ||
|
||
const handleCloseToast = useCallback(() => { | ||
setIsToastOpen(false); | ||
}, []); | ||
|
||
const successfulAssignmentAllocationToastMessage = `Course successfully assigned to ${learnersAssignedCount} ${learnersAssignedCount === 1 ? 'learner' : 'learners'}.`; | ||
|
||
const successfulAssignmentToastContextValue = useMemo(() => ({ | ||
isSuccessfulAssignmentAllocationToastOpen: isToastOpen, | ||
displayToastForAssignmentAllocation: handleDisplayToast, | ||
closeToastForAssignmentAllocation: handleCloseToast, | ||
totalLearnersAssigned: learnersAssignedCount, | ||
successfulAssignmentAllocationToastMessage, | ||
}), [ | ||
isToastOpen, | ||
handleDisplayToast, | ||
handleCloseToast, | ||
learnersAssignedCount, | ||
successfulAssignmentAllocationToastMessage, | ||
]); | ||
|
||
return successfulAssignmentToastContextValue; | ||
}; | ||
|
||
export default useSuccessfulAssignmentToastContextValue; |
100 changes: 100 additions & 0 deletions
100
src/components/learner-credit-management/tests/BudgetDetailPageWrapper.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { useContext } from 'react'; | ||
import { Button } from '@edx/paragon'; | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import configureMockStore from 'redux-mock-store'; | ||
import { Provider } from 'react-redux'; | ||
import thunk from 'redux-thunk'; | ||
import { IntlProvider } from '@edx/frontend-platform/i18n'; | ||
import '@testing-library/jest-dom/extend-expect'; | ||
|
||
import BudgetDetailPageWrapper, { BudgetDetailPageContext } from '../BudgetDetailPageWrapper'; | ||
import { getButtonElement } from '../../test/testUtils'; | ||
|
||
const mockStore = configureMockStore([thunk]); | ||
const getMockStore = store => mockStore(store); | ||
const enterpriseSlug = 'test-enterprise'; | ||
const enterpriseUUID = '1234'; | ||
const defaultStoreState = { | ||
portalConfiguration: { | ||
enterpriseId: enterpriseUUID, | ||
enterpriseSlug, | ||
enableLearnerPortal: true, | ||
enterpriseFeatures: { | ||
topDownAssignmentRealTimeLcm: true, | ||
}, | ||
}, | ||
}; | ||
|
||
const MockBudgetDetailPageWrapper = ({ | ||
initialStoreState = defaultStoreState, | ||
children, | ||
}) => { | ||
const store = getMockStore(initialStoreState); | ||
return ( | ||
<IntlProvider locale="en"> | ||
<Provider store={store}> | ||
<BudgetDetailPageWrapper> | ||
{children} | ||
</BudgetDetailPageWrapper> | ||
</Provider> | ||
</IntlProvider> | ||
); | ||
}; | ||
|
||
describe('<BudgetDetailPageWrapper />', () => { | ||
it('should render its children and display hero by default', () => { | ||
render(<MockBudgetDetailPageWrapper><div>hello world</div></MockBudgetDetailPageWrapper>); | ||
// Verify children are rendered | ||
expect(screen.getByText('hello world')).toBeInTheDocument(); | ||
// Verify Hero is rendered with the expected page title | ||
expect(screen.getByText('Learner Credit Management')).toBeInTheDocument(); | ||
}); | ||
|
||
it.each([ | ||
{ totalLearnersAssigned: 1, expectedLearnerString: 'learner' }, | ||
{ totalLearnersAssigned: 2, expectedLearnerString: 'learners' }, | ||
])('should render Toast notification for successful assignment allocation (%s)', async ({ | ||
totalLearnersAssigned, | ||
expectedLearnerString, | ||
}) => { | ||
const ToastContextController = () => { | ||
const { | ||
displayToastForAssignmentAllocation, | ||
closeToastForAssignmentAllocation, | ||
} = useContext(BudgetDetailPageContext); | ||
|
||
const handleDisplayToast = () => { | ||
displayToastForAssignmentAllocation({ totalLearnersAssigned }); | ||
}; | ||
|
||
const handleCloseToast = () => { | ||
closeToastForAssignmentAllocation(); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<Button onClick={handleDisplayToast}>Open Toast</Button> | ||
<Button onClick={handleCloseToast}>Close Toast</Button> | ||
</div> | ||
); | ||
}; | ||
render(<MockBudgetDetailPageWrapper><ToastContextController /></MockBudgetDetailPageWrapper>); | ||
|
||
const expectedToastMessage = `Course successfully assigned to ${totalLearnersAssigned} ${expectedLearnerString}.`; | ||
|
||
// Open Toast notification | ||
userEvent.click(getButtonElement('Open Toast')); | ||
|
||
// Verify Toast notification is rendered | ||
expect(screen.getByText(expectedToastMessage)).toBeInTheDocument(); | ||
|
||
// Close Toast notification | ||
userEvent.click(getButtonElement('Close Toast')); | ||
|
||
// Verify Toast notification is no longer rendered | ||
await waitFor(() => { | ||
expect(screen.queryByText(expectedToastMessage)).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters