-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: migrate enzyme to rtl #915
base: master
Are you sure you want to change the base?
Changes from all commits
a3ddfdd
300e707
3866582
f2b69fb
3e87d06
b146921
4ad3d21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,12 @@ | |
], | ||
"dependencies": { | ||
"@edx/brand": "npm:@openedx/[email protected]", | ||
"@edx/frontend-component-footer": "13.0.2", | ||
"@edx/frontend-component-footer": "13.0.4", | ||
"@edx/frontend-enterprise-catalog-search": "9.0.0", | ||
"@edx/frontend-enterprise-hotjar": "6.0.0", | ||
"@edx/frontend-enterprise-logistration": "8.0.0", | ||
"@edx/frontend-enterprise-utils": "8.0.0", | ||
"@edx/frontend-platform": "7.1.0", | ||
"@edx/frontend-platform": "7.1.1", | ||
"@edx/openedx-atlas": "^0.6.0", | ||
"@edx/reactifex": "^2.2.0", | ||
"@loadable/component": "5.16.3", | ||
|
@@ -61,16 +61,15 @@ | |
}, | ||
"devDependencies": { | ||
"@edx/browserslist-config": "1.1.1", | ||
"@edx/react-unit-test-utils": "^2.0.0", | ||
"@openedx/frontend-build": "13.0.28", | ||
"@tanstack/eslint-plugin-query": "4.29.9", | ||
"@testing-library/jest-dom": "5.11.9", | ||
"@testing-library/react": "12.1.5", | ||
"@testing-library/react-hooks": "3.7.0", | ||
"@testing-library/user-event": "13.5.0", | ||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0", | ||
"acorn": "8.5.0", | ||
"axios-mock-adapter": "1.19.0", | ||
"enzyme": "3.11.0", | ||
"jest-canvas-mock": "^2.4.0", | ||
"match-media-mock": "0.1.1", | ||
"mockdate": "3.0.5", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import React from 'react'; | ||
import { shallow } from 'enzyme'; | ||
import { shallow } from '@edx/react-unit-test-utils'; | ||
import '@testing-library/jest-dom/extend-expect'; | ||
|
||
import AuthenticatedUserSubsidyPage from './AuthenticatedUserSubsidyPage'; | ||
|
@@ -16,15 +16,15 @@ describe('<AuthenticatedUserSubsidyPage />', () => { | |
); | ||
}); | ||
it('renders <AuthenticatedPage>', () => { | ||
expect(wrapper.find(AuthenticatedPage)).toBeTruthy(); | ||
expect(wrapper.instance.findByType(AuthenticatedPage)).toBeTruthy(); | ||
}); | ||
it('renders <UserSubsidy>', () => { | ||
expect(wrapper.find(UserSubsidy)).toBeTruthy(); | ||
expect(wrapper.instance.findByType(UserSubsidy)).toBeTruthy(); | ||
}); | ||
it('renders <AutoActivateLicense>', () => { | ||
expect(wrapper.find(AutoActivateLicense)).toBeTruthy(); | ||
expect(wrapper.instance.findByType(AutoActivateLicense)).toBeTruthy(); | ||
}); | ||
it('renders children', () => { | ||
expect(wrapper.find('div.did-i-render')).toBeTruthy(); | ||
expect(wrapper.instance.findByType('div.did-i-render')).toBeTruthy(); | ||
Comment on lines
+19
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [inform] You don't need That said, I defer to you if you want to update given this component and its tests will be removed in the next week or two as part of our ongoing work to rearchitect this MFE to improve frontend performance with route loaders and more use of React Query. I'd be OK with leaving it as is for now. (We would like to avoid |
||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
import React from 'react'; | ||
import { mount } from 'enzyme'; | ||
import { StatefulButton } from '@openedx/paragon'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
|
||
import { EmailSettingsModal } from '../EmailSettingsModal'; | ||
import { updateEmailSettings } from '../data'; | ||
|
@@ -11,7 +10,7 @@ describe('<EmailSettingsModal />', () => { | |
let wrapper; | ||
|
||
beforeEach(() => { | ||
wrapper = mount(( | ||
wrapper = render(( | ||
<EmailSettingsModal | ||
onClose={() => {}} | ||
courseRunId="my+course+key" | ||
|
@@ -23,28 +22,38 @@ describe('<EmailSettingsModal />', () => { | |
// is initially `false` until the modal is opened, at which point it's | ||
// set to whatever the correct value is for that particular course run. | ||
// Setting the `hasEmailsEnabled` prop here simulates that behavior. | ||
wrapper.setProps({ | ||
hasEmailsEnabled: true, | ||
}); | ||
wrapper.rerender(( | ||
<EmailSettingsModal | ||
onClose={() => {}} | ||
courseRunId="my+course+key" | ||
hasEmailsEnabled | ||
/> | ||
)); | ||
Comment on lines
+25
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] It's unfortunate we will need to |
||
}); | ||
|
||
it('statefulbutton component state is initially set to default and disabled', () => { | ||
const defaultState = 'default'; | ||
expect(wrapper.find(StatefulButton).prop('state')).toEqual(defaultState); | ||
expect(wrapper.find(StatefulButton).prop('disabledStates')).toContain(defaultState); | ||
const buttonElement = screen.getAllByRole('button'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [curious/suggestion] Why use For example, something like: const buttonElement = screen.getByRole('button', { name: 'Save' }); Same feedback for similar usages in other test cases as well. |
||
|
||
expect(buttonElement[buttonElement.length - 1].getAttribute('aria-disabled')).toEqual('true'); | ||
expect(buttonElement[buttonElement.length - 1].disabled).toEqual(false); | ||
Comment on lines
+37
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] Would expect(buttonElement).toBeDisabled(); |
||
expect(screen.getByText('Save')).toBeTruthy(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When making assertions with |
||
}); | ||
|
||
it('statefulbutton component state is set to complete after click event', async () => { | ||
// Note: The following line is needed to properly resolve the | ||
// `updateEmailSettings` promise. | ||
const flushPromises = () => new Promise(setImmediate); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With tests using RTL, any use of this approach with |
||
const buttonElement = screen.getAllByRole('button'); | ||
|
||
expect(wrapper.find(StatefulButton).prop('state')).toEqual('default'); | ||
wrapper.find('input[type="checkbox"]').simulate('change', { target: { checked: false } }); | ||
wrapper.find(StatefulButton).simulate('click'); | ||
expect(screen.getByText('Save')).toBeTruthy(); | ||
expect(screen.queryByText('Saved')).toBeFalsy(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to using |
||
|
||
fireEvent.click(screen.getByRole('checkbox')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The only time
See these related resources for more details: |
||
fireEvent.click(buttonElement[buttonElement.length - 1]); | ||
await flushPromises(); | ||
wrapper.update(); | ||
|
||
expect(updateEmailSettings).toHaveBeenCalledTimes(1); | ||
expect(wrapper.find(StatefulButton).prop('state')).toEqual('complete'); | ||
expect(screen.getByText('Saved')).toBeTruthy(); | ||
expect(screen.queryByText('Save')).toBeFalsy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import React from 'react'; | ||
import { mount } from 'enzyme'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { act } from 'react-dom/test-utils'; | ||
import { AppContext } from '@edx/frontend-platform/react'; | ||
|
||
|
@@ -30,56 +30,59 @@ describe('<MarkCompleteModal />', () => { | |
course_run_status: 'completed', | ||
}, | ||
})); | ||
const wrapper = mount(( | ||
render(( | ||
<AppContext.Provider value={{ enterpriseConfig }}> | ||
<MarkCompleteModal | ||
{...initialProps} | ||
/> | ||
</AppContext.Provider> | ||
)); | ||
wrapper.find('.confirm-mark-complete-btn').hostNodes().simulate('click'); | ||
const buttonElement = screen.getAllByRole('button'); | ||
fireEvent.click(buttonElement[buttonElement.length - 1]); | ||
expect(service.updateCourseCompleteStatusRequest).toBeCalledWith({ | ||
course_id: initialProps.courseId, | ||
enterprise_id: enterpriseConfig.uuid, | ||
saved_for_later: true, | ||
}); | ||
expect(wrapper.find('.confirm-mark-complete-btn').hostNodes().text()).toEqual(MARK_SAVED_FOR_LATER_PENDING_LABEL); | ||
expect(buttonElement[buttonElement.length - 1].textContent).toEqual(MARK_SAVED_FOR_LATER_PENDING_LABEL); | ||
}); | ||
|
||
it('handles confirm click with error', async () => { | ||
// eslint-disable-next-line no-import-assign | ||
service.markCourseAsCompleteRequest = jest.fn() | ||
.mockImplementation(() => Promise.reject(new Error('test error'))); | ||
const wrapper = mount(( | ||
render(( | ||
<AppContext.Provider value={{ enterpriseConfig }}> | ||
<MarkCompleteModal | ||
{...initialProps} | ||
/> | ||
</AppContext.Provider> | ||
)); | ||
const buttonElement = screen.getAllByRole('button'); | ||
await act(async () => { | ||
wrapper.find('.confirm-mark-complete-btn').hostNodes().simulate('click'); | ||
fireEvent.click(buttonElement[buttonElement.length - 1]); | ||
}); | ||
expect(service.updateCourseCompleteStatusRequest).toBeCalledWith({ | ||
course_id: initialProps.courseId, | ||
enterprise_id: enterpriseConfig.uuid, | ||
saved_for_later: true, | ||
}); | ||
expect(wrapper.find('.confirm-mark-complete-btn').hostNodes().text()).toEqual(MARK_SAVED_FOR_LATER_DEFAULT_LABEL); | ||
expect(buttonElement[buttonElement.length - 1].textContent).toEqual(MARK_SAVED_FOR_LATER_DEFAULT_LABEL); | ||
}); | ||
|
||
it('handles close modal', () => { | ||
const mockOnClose = jest.fn(); | ||
const wrapper = mount(( | ||
render(( | ||
<AppContext.Provider value={{ enterpriseConfig }}> | ||
<MarkCompleteModal | ||
{...initialProps} | ||
onClose={mockOnClose} | ||
/> | ||
</AppContext.Provider> | ||
)); | ||
const buttonElement = screen.getAllByRole('button'); | ||
act(() => { | ||
wrapper.find('.modal-footer button.btn-link').hostNodes().simulate('click'); | ||
fireEvent.click(buttonElement[1]); | ||
}); | ||
Comment on lines
84
to
86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With RTL, side effects such as this |
||
expect(mockOnClose).toBeCalledTimes(1); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import React from 'react'; | ||
import { mount } from 'enzyme'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { AppContext } from '@edx/frontend-platform/react'; | ||
import { Skeleton } from '@openedx/paragon'; | ||
|
||
import dayjs from '../../../../../../utils/dayjs'; | ||
import BaseCourseCard from '../BaseCourseCard'; | ||
|
@@ -27,7 +26,7 @@ describe('<BaseCourseCard />', () => { | |
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
|
||
wrapper = mount(( | ||
wrapper = render(( | ||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<BaseCourseCard | ||
type="completed" | ||
|
@@ -39,14 +38,15 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
// open email settings modal | ||
wrapper.find('Dropdown').find('button.btn-icon').simulate('click'); | ||
wrapper.find('Dropdown').find('button.dropdown-item').simulate('click'); | ||
expect(wrapper.find('BaseCourseCard').state('modals').emailSettings.open).toBeTruthy(); | ||
fireEvent.click(wrapper.container.querySelector('button.btn-icon')); | ||
fireEvent.click(wrapper.container.querySelectorAll('button.dropdown-item')[0]); | ||
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of querying for implementation details (class names), can we query as if you're the user? I.e., query for the button / dropdown item using the rendered text, not the class names. This should help make testing less brittle (e.g., Paragon's ' |
||
|
||
expect(screen.getAllByText('Email settings')).toHaveLength(2); | ||
}); | ||
|
||
it('test modal close/cancel', () => { | ||
wrapper.find('EmailSettingsModal').find('.modal-footer .btn-link').first().simulate('click'); | ||
expect(wrapper.find('BaseCourseCard').state('modals').emailSettings.open).toBeFalsy(); | ||
fireEvent.click(screen.getAllByText('Close')[1]); | ||
expect(screen.getAllByText('Email settings')).toHaveLength(2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [clarification] With these changes, does this test have the same semantics as the existing test? Previously, it was asserting that the email settings modal is closed; however, in the new assertions, I'm not sure asserting that "Email settings" rendered twice is really testing that the modal is closed... |
||
}); | ||
}); | ||
|
||
|
@@ -56,7 +56,7 @@ describe('<BaseCourseCard />', () => { | |
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
|
||
wrapper = mount(( | ||
wrapper = render(( | ||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<ToastsContext.Provider value={{ addToast: mockAddToast }}> | ||
<CourseEnrollmentsContext.Provider value={{ removeCourseEnrollment: jest.fn() }}> | ||
|
@@ -73,19 +73,19 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
// open unenroll modal | ||
wrapper.find('Dropdown').find('button.btn-icon').simulate('click'); | ||
wrapper.find('Dropdown').find('button.dropdown-item').at(1).simulate('click'); | ||
expect(wrapper.find('BaseCourseCard').state('modals').unenroll.open).toBeTruthy(); | ||
fireEvent.click(wrapper.container.querySelector('button.btn-icon')); | ||
fireEvent.click(wrapper.container.querySelectorAll('button.dropdown-item')[1]); | ||
Comment on lines
+76
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same feedback about not using implementation details like class names when using RTL (and |
||
expect(screen.getByText('Unenroll from course?')).toBeTruthy(); | ||
}); | ||
|
||
it('test modal close/cancel', () => { | ||
wrapper.find('UnenrollModal').find('.btn-tertiary').simulate('click'); | ||
expect(wrapper.find('BaseCourseCard').state('modals').unenroll.open).toBeFalsy(); | ||
fireEvent.click(screen.getAllByText('Close')[1]); | ||
expect(screen.queryByText('Unenroll from course?')).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
it('should render Skeleton if isLoading = true', () => { | ||
wrapper = mount(( | ||
render(( | ||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<BaseCourseCard | ||
type="completed" | ||
|
@@ -98,7 +98,7 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
|
||
expect(wrapper.find(Skeleton)).toBeTruthy(); | ||
expect(screen.getByText('Loading...')).toBeTruthy(); | ||
}); | ||
|
||
it('renders with different startDate values', () => { | ||
|
@@ -112,7 +112,7 @@ describe('<BaseCourseCard />', () => { | |
const formattedStartDate = dayjs(startDate).format('MMMM Do, YYYY'); | ||
const isCourseStarted = dayjs(startDate) <= dayjs(); | ||
|
||
wrapper = mount(( | ||
wrapper = render(( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [clarification] If |
||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<BaseCourseCard | ||
type="in_progress" | ||
|
@@ -130,12 +130,11 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
|
||
const renderedStartDate = wrapper.instance().renderStartDate(); | ||
const expectedOutput = formattedStartDate && !isCourseStarted | ||
? <span className="font-weight-light">Starts {formattedStartDate}</span> | ||
: null; | ||
|
||
expect(renderedStartDate).toEqual(expectedOutput); | ||
if (formattedStartDate && !isCourseStarted) { | ||
expect(screen.getByText(`Starts ${formattedStartDate}`)).toBeTruthy(); | ||
} else { | ||
expect(screen.queryByText(`Starts ${formattedStartDate}`)).toBeFalsy(); | ||
} | ||
}); | ||
}); | ||
|
||
|
@@ -146,7 +145,7 @@ describe('<BaseCourseCard />', () => { | |
const formattedEndDate = dayjs(endDate).format('MMMM Do, YYYY'); | ||
const type = 'in_progress'; | ||
|
||
wrapper = mount(( | ||
wrapper = render(( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same feedback about not needing |
||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<BaseCourseCard | ||
type={type} | ||
|
@@ -163,12 +162,11 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
|
||
const renderedEndDate = wrapper.instance().renderEndDate(); | ||
const expectedOutput = formattedEndDate && dayjs(startDate) <= dayjs() && type !== 'completed' | ||
? <span className="font-weight-light">Ends {formattedEndDate}</span> | ||
: null; | ||
|
||
expect(renderedEndDate).toEqual(expectedOutput); | ||
if (formattedEndDate && dayjs(startDate) <= dayjs() && type !== 'completed') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Similar to the test above with |
||
expect(screen.getByText(`Ends ${formattedEndDate}`)).toBeTruthy(); | ||
} else { | ||
expect(screen.queryByText(`Ends ${formattedEndDate}`)).toBeFalsy(); | ||
} | ||
}); | ||
|
||
it('renders Enroll By Date if the user is not enrolled', () => { | ||
|
@@ -177,7 +175,7 @@ describe('<BaseCourseCard />', () => { | |
const formattedEnrollByDate = dayjs(enrollBy).format('MMMM Do, YYYY'); | ||
const courseRunStatus = 'assigned'; | ||
|
||
wrapper = mount(( | ||
wrapper = render(( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems |
||
<AppContext.Provider value={{ enterpriseConfig, authenticatedUser }}> | ||
<BaseCourseCard | ||
courseRunStatus={courseRunStatus} | ||
|
@@ -190,11 +188,10 @@ describe('<BaseCourseCard />', () => { | |
</AppContext.Provider> | ||
)); | ||
|
||
const isNotEnrolled = wrapper.instance().renderEnrollByDate(); | ||
const expectedOutput = formattedEnrollByDate && courseRunStatus === 'assigned' | ||
? <span className="font-weight-light">Enroll by {formattedEnrollByDate}</span> | ||
: null; | ||
|
||
expect(isNotEnrolled).toEqual(expectedOutput); | ||
if (formattedEnrollByDate && courseRunStatus === 'assigned') { | ||
expect(screen.getByText(`Enroll by ${formattedEnrollByDate}`)).toBeTruthy(); | ||
} else { | ||
expect(screen.queryByText(`Enroll by ${formattedEnrollByDate}`)).toBeFalsy(); | ||
} | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[inform/suggestion] Note, while
@edx/react-unit-test-utils
is acceptable as a short-term replacement for previous Enzyme tests to minimize initial refactoring, we generally would prefer not to use this NPM package in favor of using React Testing Library (RTL) directly.Rationale: RTL's testing philosophy is vastly different from Enzyme, and while
@edx/react-unit-test-utils
is using RTL under-the-hood, it generally continues to propagate Enzyme's testing philosophy of asserting based on implementation details (no longer desired).It does look like you're using RTL directly in all but 2 test cases, which is great. Perhaps considering refactoring those 2 test cases to also use RTL directly.