Skip to content

Commit

Permalink
Add test coverage for Courseware Search components (WIP) (#1242)
Browse files Browse the repository at this point in the history
* chore: 100% coverage on CoursewareSearchResults.jsx

* chore: Added test coverage for all CoursewareSearch components

* chore: Minor fixes on Courseware Search components
  • Loading branch information
rijuma authored Dec 4, 2023
1 parent d7cffcd commit 5906576
Show file tree
Hide file tree
Showing 13 changed files with 1,516 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter';
import initializeStore from '../../store';
import { useModel } from '../../generic/model-store';
import searchResultsFactory from './test-data/search-results-factory';

jest.mock('../../generic/model-store', () => ({
useModel: jest.fn(),
Expand Down Expand Up @@ -64,7 +65,7 @@ describe('CoursewareSearchResultsFilter', () => {

describe('filteredResultsBySelection', () => {
it('returns a no values array when no results are provided', () => {
const results = filteredResultsBySelection({ results: [] });
const results = filteredResultsBySelection({});

expect(results.length).toEqual(0);
});
Expand Down Expand Up @@ -100,11 +101,7 @@ describe('CoursewareSearchResultsFilter', () => {
});

it('should render', async () => {
useModel.mockReturnValue({
total: 6,
results: mockResults,
filters: [],
});
useModel.mockReturnValue(searchResultsFactory());

await renderComponent();

Expand Down
12 changes: 4 additions & 8 deletions src/course-home/courseware-search/CoursewareSearch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,9 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
return;
}

const eventProperties = {
sendTrackingLogEvent('edx.course.home.courseware_search.submit', {
org_key: org,
courserun_key: courseId,
};

sendTrackingLogEvent('edx.course.home.courseware_search.submit', {
...eventProperties,
event_type: 'searchKeyword',
keyword: searchKeyword,
});
Expand Down Expand Up @@ -109,18 +105,18 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
/>
{status === 'loading' ? (
<div className="courseware-search__spinner">
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
</div>
) : null}
{status === 'error' && (
<Alert className="mt-4" variant="danger">
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
{intl.formatMessage(messages.searchResultsError)}
</Alert>
)}
{status === 'results' ? (
<>
<div className="courseware-search__results-summary">{total > 0
<div className="courseware-search__results-summary" data-testid="courseware-search-summary">{total > 0
? (
intl.formatMessage(
total === 1
Expand Down
179 changes: 172 additions & 7 deletions src/course-home/courseware-search/CoursewareSearch.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@ import React from 'react';
import { history } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { Route, Routes } from 'react-router-dom';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import {
initializeMockApp,
render,
screen,
waitFor,
fireEvent,
} from '../../setupTest';
import { CoursewareSearch } from './index';
import { useElementBoundingBox, useLockScroll } from './hooks';
import initializeStore from '../../store';
import { useModel } from '../../generic/model-store';
import { useModel, updateModel } from '../../generic/model-store';
import { searchCourseContent } from '../data/thunks';
import { setShowSearch } from '../data/slice';

jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({
updateModel: jest.fn(),
useModel: jest.fn(),
}));

jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackingLogEvent: jest.fn(),
}));

jest.mock('../data/thunks', () => ({
searchCourseContent: jest.fn(),
}));

jest.mock('../data/slice', () => ({
setShowSearch: jest.fn(),
}));

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));

const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
Expand Down Expand Up @@ -49,8 +73,11 @@ function renderComponent(props = {}) {
return container;
}

const mockModels = ((props = defaultProps) => {
useModel.mockReturnValue(props);
const mockModels = ((props) => {
useModel.mockReturnValue({
...defaultProps,
...props,
});
});

describe('CoursewareSearch', () => {
Expand All @@ -65,15 +92,15 @@ describe('CoursewareSearch', () => {
useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition }));
});

it('Should use useElementBoundingBox() and useLockScroll() hooks', () => {
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
mockModels();
renderComponent();

expect(useElementBoundingBox).toBeCalledTimes(1);
expect(useLockScroll).toBeCalledTimes(1);
});

it('Should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
mockModels();
renderComponent();

Expand All @@ -82,8 +109,22 @@ describe('CoursewareSearch', () => {
});
});

describe('when clicking on the "Close" button', () => {
it('should dispatch setShowSearch(false)', async () => {
mockModels();
renderComponent();

await waitFor(() => {
const close = screen.queryByTestId('courseware-search-close-button');
fireEvent.click(close);
});

expect(setShowSearch).toBeCalledWith(false);
});
});

describe('when CourseTabsNavigation is not present', () => {
it('Should use "--modal-top-position: 0" if nce element is not present', () => {
it('should use "--modal-top-position: 0" if nce element is not present', () => {
useElementBoundingBox.mockImplementation(() => undefined);

mockModels();
Expand All @@ -95,12 +136,136 @@ describe('CoursewareSearch', () => {
});

describe('when passing extra props', () => {
it('Should pass on extra props to section element', () => {
it('should pass on extra props to section element', () => {
mockModels();
renderComponent({ foo: 'bar' });

const section = screen.getByTestId('courseware-search-section');
expect(section).toHaveAttribute('foo', 'bar');
});
});

describe('when submitting an empty search', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels();
renderComponent();

await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});

expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});

describe('when submitting a search', () => {
it('should show a loading state', () => {
mockModels({
loading: true,
});
renderComponent();

expect(screen.queryByTestId('courseware-search-spinner')).toBeInTheDocument();
});

it('should call searchCourseContent', async () => {
mockModels();
renderComponent();

const searchKeyword = 'course';

await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: searchKeyword } });
});

await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});

expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.course.home.courseware_search.submit', {
org_key: defaultProps.org,
courserun_key: decodedCourseId,
event_type: 'searchKeyword',
keyword: searchKeyword,
});
expect(searchCourseContent).toHaveBeenCalledWith(decodedCourseId, searchKeyword);
});

it('should show an error state if any', () => {
mockModels({
errors: ['foo'],
});
renderComponent();

expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
});

it('should show "No results found." if results is empty', () => {
mockModels({
searchKeyword: 'test',
total: 0,
});
renderComponent();

expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('No results found.');
});

it('should show a summary for a single result', () => {
mockModels({
searchKeyword: 'fubar',
total: 1,
});
renderComponent();

expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('1 match found for "fubar":');
});

it('should show a summary for multiple results', () => {
mockModels({
searchKeyword: 'fubar',
total: 2,
});
renderComponent();

expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('2 matches found for "fubar":');
});
});

describe('when clearing the search input', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels({
searchKeyword: 'fubar',
total: 2,
});
renderComponent();

await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: '' } });
});

expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});
});
24 changes: 24 additions & 0 deletions src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import {
initializeMockApp,
render,
screen,
} from '../../setupTest';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';

function renderComponent() {
const { container } = render(<CoursewareSearchEmpty />);
return container;
}

describe('CoursewareSearchEmpty', () => {
beforeAll(async () => {
initializeMockApp();
});

it('should match the snapshot', () => {
renderComponent();

expect(screen.getByTestId('no-results')).toMatchSnapshot();
});
});
2 changes: 1 addition & 1 deletion src/course-home/courseware-search/CoursewareSearchForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const CoursewareSearchForm = ({
<SearchField.Input placeholder={placeholder} autoFocus />
<SearchField.ClearButton />
</div>
<SearchField.SubmitButton submitButtonLocation="external" />
<SearchField.SubmitButton submitButtonLocation="external" data-testid="courseware-search-form-submit" />
</SearchField.Advanced>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '../../setupTest';
import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages';
// import mockedData from './test-data/mockedResults'; // TODO: Update this test.
import searchResultsFactory from './test-data/search-results-factory';

jest.mock('react-redux');

Expand All @@ -28,11 +28,14 @@ describe('CoursewareSearchResults', () => {
});
});

/* describe('when list of results is provided', () => {
beforeEach(() => { renderComponent({ results: mockedData }); });
describe('when list of results is provided', () => {
beforeEach(() => {
const { results } = searchResultsFactory('course');
renderComponent({ results });
});

it('should match the snapshot', () => {
expect(screen.getByTestId('search-results')).toMatchSnapshot();
});
}); */
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
<p
class="courseware-search-results__empty"
data-testid="no-results"
>
No results found.
</p>
`;
Loading

0 comments on commit 5906576

Please sign in to comment.