Skip to content
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

Add test coverage for Courseware Search components #1242

Merged
merged 3 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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