Skip to content

Commit

Permalink
chore: Added test coverage for all CoursewareSearch components
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Dec 1, 2023
1 parent 824032d commit f5350ee
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 16 deletions.
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('search-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

0 comments on commit f5350ee

Please sign in to comment.