From 5906576c6e1aaafa30970651d79f345ef678cdcb Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 4 Dec 2023 12:39:07 -0300 Subject: [PATCH] Add test coverage for Courseware Search components (WIP) (#1242) * chore: 100% coverage on CoursewareSearchResults.jsx * chore: Added test coverage for all CoursewareSearch components * chore: Minor fixes on Courseware Search components --- .../CoursewareResultsFilter.test.jsx | 9 +- .../courseware-search/CoursewareSearch.jsx | 12 +- .../CoursewareSearch.test.jsx | 179 ++- .../CoursewareSearchEmpty.test.jsx | 24 + .../CoursewareSearchForm.jsx | 2 +- .../CoursewareSearchResults.test.jsx | 11 +- .../CoursewareSearchEmpty.test.jsx.snap | 10 + .../CoursewareSearchResults.test.jsx.snap | 1238 +++++++++++++++++ .../map-search-response.test.js.snap | 14 + .../courseware-search/map-search-response.js | 6 +- .../map-search-response.test.js | 1 + .../test-data/mocked-response.json | 22 + .../test-data/search-results-factory.js | 17 + 13 files changed, 1516 insertions(+), 29 deletions(-) create mode 100644 src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx create mode 100644 src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap create mode 100644 src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap create mode 100644 src/course-home/courseware-search/test-data/search-results-factory.js diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx index 0ca7faea..034c80f2 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx @@ -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(), @@ -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); }); @@ -100,11 +101,7 @@ describe('CoursewareSearchResultsFilter', () => { }); it('should render', async () => { - useModel.mockReturnValue({ - total: 6, - results: mockResults, - filters: [], - }); + useModel.mockReturnValue(searchResultsFactory()); await renderComponent(); diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx index aca8480f..bb1abf92 100644 --- a/src/course-home/courseware-search/CoursewareSearch.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -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, }); @@ -109,18 +105,18 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { placeholder={intl.formatMessage(messages.searchBarPlaceholderText)} /> {status === 'loading' ? ( -
+
) : null} {status === 'error' && ( - + {intl.formatMessage(messages.searchResultsError)} )} {status === 'results' ? ( <> -
{total > 0 +
{total > 0 ? ( intl.formatMessage( total === 1 diff --git a/src/course-home/courseware-search/CoursewareSearch.test.jsx b/src/course-home/courseware-search/CoursewareSearch.test.jsx index df6944c8..cf852c7f 100644 --- a/src/course-home/courseware-search/CoursewareSearch.test.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.test.jsx @@ -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'; @@ -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', () => { @@ -65,7 +92,7 @@ describe('CoursewareSearch', () => { useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition })); }); - it('Should use useElementBoundingBox() and useLockScroll() hooks', () => { + it('should use useElementBoundingBox() and useLockScroll() hooks', () => { mockModels(); renderComponent(); @@ -73,7 +100,7 @@ describe('CoursewareSearch', () => { 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(); @@ -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(); @@ -95,7 +136,7 @@ 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' }); @@ -103,4 +144,128 @@ describe('CoursewareSearch', () => { 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, + }, + }); + }); + }); }); diff --git a/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx b/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx new file mode 100644 index 00000000..7ef5909a --- /dev/null +++ b/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { + initializeMockApp, + render, + screen, +} from '../../setupTest'; +import CoursewareSearchEmpty from './CoursewareSearchEmpty'; + +function renderComponent() { + const { container } = render(); + return container; +} + +describe('CoursewareSearchEmpty', () => { + beforeAll(async () => { + initializeMockApp(); + }); + + it('should match the snapshot', () => { + renderComponent(); + + expect(screen.getByTestId('no-results')).toMatchSnapshot(); + }); +}); diff --git a/src/course-home/courseware-search/CoursewareSearchForm.jsx b/src/course-home/courseware-search/CoursewareSearchForm.jsx index 3c5a88be..023a85c3 100644 --- a/src/course-home/courseware-search/CoursewareSearchForm.jsx +++ b/src/course-home/courseware-search/CoursewareSearchForm.jsx @@ -20,7 +20,7 @@ const CoursewareSearchForm = ({
- + ); diff --git a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx index a49ef62a..eae95083 100644 --- a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx +++ b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx @@ -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'); @@ -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(); }); - }); */ + }); }); diff --git a/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap b/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap new file mode 100644 index 00000000..e87c856e --- /dev/null +++ b/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CoursewareSearchEmpty should match the snapshot 1`] = ` +

+ No results found. +

+`; diff --git a/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap b/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap new file mode 100644 index 00000000..c38958db --- /dev/null +++ b/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap @@ -0,0 +1,1238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = ` +
+ +
+ + + +
+
+
+ + Demo Course Overview + +
+
    +
  • +
    + Introduction +
    +
  • +
  • +
    + Demo Course Overview +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Passing a Course + + + 1 + +
+
    +
  • +
    + About Exams and Certificates +
    +
  • +
  • +
    + edX Exams +
    +
  • +
  • +
    + Passing a Course +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Passing a Course + +
+
    +
  • +
    + About Exams and Certificates +
    +
  • +
  • +
    + edX Exams +
    +
  • +
  • +
    + Passing a Course +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Text Input + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Homework - Question Styles +
    +
  • +
  • +
    + Text input +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Pointing on a Picture + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Homework - Question Styles +
    +
  • +
  • +
    + Pointing on a Picture +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Getting Answers + +
+
    +
  • +
    + About Exams and Certificates +
    +
  • +
  • +
    + edX Exams +
    +
  • +
  • +
    + Getting Answers +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Welcome! + + + 30 + +
+
    +
  • +
    + Introduction +
    +
  • +
  • +
    + Demo Course Overview +
    +
  • +
  • +
    + Introduction: Video and Sequences +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Multiple Choice Questions + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Homework - Question Styles +
    +
  • +
  • +
    + Multiple Choice Questions +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Numerical Input + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Homework - Question Styles +
    +
  • +
  • +
    + Numerical Input +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Connecting a Circuit and a Circuit Diagram + + + 3 + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Lesson 1 - Getting Started +
    +
  • +
  • +
    + Video Presentation Styles +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + CAPA + +
+
    +
  • +
    + Example Week 2: Get Interactive +
    +
  • +
  • +
    + Homework - Labs and Demos +
    +
  • +
  • +
    + Code Grader +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Interactive Questions + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Lesson 1 - Getting Started +
    +
  • +
  • +
    + Interactive Questions +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Blank HTML Page + + + 6 + +
+
    +
  • +
    + Introduction +
    +
  • +
  • +
    + Demo Course Overview +
    +
  • +
  • +
    + Introduction: Video and Sequences +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Discussion Forums + + + 5 + +
+
    +
  • +
    + Example Week 3: Be Social +
    +
  • +
  • +
    + Lesson 3 - Be Social +
    +
  • +
  • +
    + Discussion Forums +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Overall Grade + + + 7 + +
+
    +
  • +
    + About Exams and Certificates +
    +
  • +
  • +
    + edX Exams +
    +
  • +
  • +
    + Overall Grade Performance +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Blank HTML Page + + + 3 + +
+
    +
  • +
    + Example Week 3: Be Social +
    +
  • +
  • +
    + Lesson 3 - Be Social +
    +
  • +
  • +
    + Homework - Find Your Study Buddy +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Find Your Study Buddy + + + 3 + +
+
    +
  • +
    + Example Week 3: Be Social +
    +
  • +
  • +
    + Homework - Find Your Study Buddy +
    +
  • +
  • +
    + Homework - Find Your Study Buddy +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + Be Social + + + 4 + +
+
    +
  • +
    + Example Week 3: Be Social +
    +
  • +
  • +
    + Lesson 3 - Be Social +
    +
  • +
  • +
    + Be Social +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + EdX Exams + + + 4 + +
+
    +
  • +
    + About Exams and Certificates +
    +
  • +
  • +
    + edX Exams +
    +
  • +
  • +
    + EdX Exams +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + When Are Your Exams? + + + 2 + +
+
    +
  • +
    + Example Week 1: Getting Started +
    +
  • +
  • +
    + Lesson 1 - Getting Started +
    +
  • +
  • +
    + When Are Your Exams? +
    +
  • +
+
+
+ +
+ + + +
+
+
+ + External Course Link Test + +
+
+
+
+`; diff --git a/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap b/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap index 9fb892ca..b3c19353 100644 --- a/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap +++ b/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap @@ -18,6 +18,11 @@ Object { "key": "text", "label": "Text", }, + Object { + "count": 1, + "key": "unknown", + "label": "Unknown", + }, Object { "count": 2, "key": "video", @@ -286,6 +291,15 @@ Object { "type": "text", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", }, + Object { + "contentHits": 0, + "id": "random-element-id", + "location": null, + "score": 0.82610154, + "title": "External Course Link Test", + "type": "unknown", + "url": "https://www.edx.org", + }, ], "total": 29, } diff --git a/src/course-home/courseware-search/map-search-response.js b/src/course-home/courseware-search/map-search-response.js index 2c005bd1..a03e99b1 100644 --- a/src/course-home/courseware-search/map-search-response.js +++ b/src/course-home/courseware-search/map-search-response.js @@ -1,8 +1,8 @@ const Joi = require('joi'); const endpointSchema = Joi.object({ - took: Joi.number(), - total: Joi.number(), + took: Joi.number().required(), + total: Joi.number().required(), maxScore: Joi.number().allow(null), results: Joi.array().items(Joi.object({ id: Joi.string(), @@ -14,7 +14,7 @@ const endpointSchema = Joi.object({ htmlContent: Joi.string(), transcriptEn: Joi.string(), }), - }).unknown(true)).strict(), + }).required().unknown(true)).strict(), }).unknown(true).strict(); const defaultType = 'text'; diff --git a/src/course-home/courseware-search/map-search-response.test.js b/src/course-home/courseware-search/map-search-response.test.js index ee55a45b..2ad4318a 100644 --- a/src/course-home/courseware-search/map-search-response.test.js +++ b/src/course-home/courseware-search/map-search-response.test.js @@ -19,6 +19,7 @@ describe('mapSearchResponse', () => { { key: 'capa', label: 'CAPA', count: 7 }, { key: 'sequence', label: 'Sequence', count: 2 }, { key: 'text', label: 'Text', count: 9 }, + { key: 'unknown', label: 'Unknown', count: 1 }, { key: 'video', label: 'Video', count: 2 }, ]; expect(response.filters).toEqual(expectedFilters); diff --git a/src/course-home/courseware-search/test-data/mocked-response.json b/src/course-home/courseware-search/test-data/mocked-response.json index 209ce043..06ff12cb 100644 --- a/src/course-home/courseware-search/test-data/mocked-response.json +++ b/src/course-home/courseware-search/test-data/mocked-response.json @@ -542,6 +542,28 @@ "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf" }, "score": 0.82610154 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "some-external-reference", + "data": { + "course": "External Course", + "org": "edX", + "content": { + "display_name": "External Course Link Test", + "html_content": "This should open a new tab when following the link." + }, + "content_type": "Unknown", + "id": "random-element-id", + "start_date": "2013-02-05T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": null, + "excerpt": "Just testing external links", + "url": "https://www.edx.org" + }, + "score": 0.82610154 } ], "access_denied_count": 0 diff --git a/src/course-home/courseware-search/test-data/search-results-factory.js b/src/course-home/courseware-search/test-data/search-results-factory.js new file mode 100644 index 00000000..7d0ca8c2 --- /dev/null +++ b/src/course-home/courseware-search/test-data/search-results-factory.js @@ -0,0 +1,17 @@ +import { camelCaseObject } from '@edx/frontend-platform'; +import mockedData from './mocked-response.json'; +import mapSearchResponse from '../map-search-response'; + +function searchResultsFactory(searchKeywords = '', moreInfo = {}) { + const data = camelCaseObject(mockedData); + const info = mapSearchResponse(data, searchKeywords); + + const result = { + ...info, + ...moreInfo, + }; + + return result; +} + +export default searchResultsFactory;