Skip to content

Commit

Permalink
Endpoint usage implementation for Courseware Search (WIP) (#1232)
Browse files Browse the repository at this point in the history
* feat: Endpoint usage implementation for Courseware Search

Co-authored-by: Simon Chen <[email protected]>

* chore: Added tests and error case

* fix: remove console log

* fix: update tests

* fix: update tests

* fix: update variables

---------

Co-authored-by: Simon Chen <[email protected]>
Co-authored-by: German <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent 308f03c commit a70a26f
Show file tree
Hide file tree
Showing 23 changed files with 1,473 additions and 713 deletions.
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"classnames": "2.3.2",
"core-js": "3.22.2",
"history": "5.3.0",
"joi": "^17.11.0",
"js-cookie": "3.0.5",
"lodash.camelcase": "4.3.0",
"prop-types": "15.8.1",
Expand Down
64 changes: 38 additions & 26 deletions src/course-home/courseware-search/CoursewareResultsFilter.jsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@edx/paragon';

import CoursewareSearchResultPropType from './CoursewareSearchResult.PropTypeDefinition';
import { useParams } from 'react-router';
import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages';
import { useModel } from '../../generic/model-store';

export const filteredResultsBySelection = ({ filterKey = 'all', results = [] }) => {
if (['document', 'video', 'text'].includes(filterKey)) {
return results.filter(result => result?.type?.toLowerCase() === filterKey);
}
const noFilterKey = 'none';
const noFilterLabel = 'All content';

return results || [];
};
export const filteredResultsBySelection = ({ key = noFilterKey, results = [] }) => (
key === noFilterKey ? results : results.filter(({ type }) => type === key)
);

export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId);

if (!lastSearch || !lastSearch?.results?.length) { return null; }

const tabConfiguration = [
{ eventKey: 'all', title: 'All content' },
{ eventKey: 'document', title: 'Course outline' },
{ eventKey: 'text', title: 'Text' },
{ eventKey: 'video', title: 'Video' },
];
const { total, results } = lastSearch;

export const CoursewareSearchResultsFilter = ({ intl, results }) => {
if (!results || !results.length) { return null; }
const filters = [
{
key: noFilterKey,
label: noFilterLabel,
count: total,
},
...lastSearch.filters,
];

const getFilterTitle = (key, fallback) => {
const msg = messages[`filter:${key}`];
if (!msg) { return fallback; }
return intl.formatMessage(msg);
};

return (
<Tabs id="courseware-search-results-tabs" data-testid="courseware-search-results-tabs" variant="tabs" defaultActiveKey="all">
{tabConfiguration.map((tab) => (
<Tab {...tab} key={tab.eventKey}>
<Tabs
id="courseware-search-results-tabs"
data-testid="courseware-search-results-tabs"
variant="tabs"
defaultActiveKey={noFilterKey}
>
{filters.map(({ key, label }) => (
<Tab key={key} eventKey={key} title={getFilterTitle(key, label)}>
<CoursewareSearchResults
intl={intl}
results={filteredResultsBySelection({ filterKey: tab.eventKey, results })}
results={filteredResultsBySelection({ key, results })}
/>
</Tab>
))}
Expand All @@ -40,11 +57,6 @@ export const CoursewareSearchResultsFilter = ({ intl, results }) => {

CoursewareSearchResultsFilter.propTypes = {
intl: intlShape.isRequired,
results: PropTypes.arrayOf(CoursewareSearchResultPropType),
};

CoursewareSearchResultsFilter.defaultProps = {
results: [],
};

export default injectIntl(CoursewareSearchResultsFilter);
94 changes: 63 additions & 31 deletions src/course-home/courseware-search/CoursewareResultsFilter.test.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,64 @@
import React from 'react';
import { AppProvider } from '@edx/frontend-platform/react';
import { Route, Routes } from 'react-router-dom';
import { history } from '@edx/frontend-platform';
import {
initializeMockApp,
render,
screen,
waitFor,
} from '../../setupTest';
import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter';
import initializeStore from '../../store';
import { useModel } from '../../generic/model-store';

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

const mockResults = [
{ type: 'video', title: 'video_title' },
{ type: 'video', title: 'video_title2' },
{ type: 'document', title: 'document_title' },
{ type: 'text', title: 'text_title1' },
{ type: 'text', title: 'text_title2' },
{ type: 'text', title: 'text_title3' },
{
id: 'video-1', type: 'video', title: 'video_title', score: 3, contentHits: 1, url: '/video-1', location: ['path1', 'path2'],
},
{
id: 'video-2', type: 'video', title: 'video_title2', score: 2, contentHits: 1, url: '/video-2', location: ['path1', 'path2'],
},
{
id: 'document-1', type: 'document', title: 'document_title', score: 3, contentHits: 1, url: '/document-1', location: ['path1', 'path2'],
},
{
id: 'text-1', type: 'text', title: 'text_title1', score: 3, contentHits: 1, url: '/text-1', location: ['path1', 'path2'],
},
{
id: 'text-2', type: 'text', title: 'text_title2', score: 2, contentHits: 1, url: '/text-2', location: ['path1', 'path2'],
},
{
id: 'text-3', type: 'text', title: 'text_title3', score: 1, contentHits: 1, url: '/text-3', location: ['path1', 'path2'],
},
];

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';
const pathname = `/course/${decodedCourseId}/${decodedSequenceId}/${decodedUnitId}`;

const intl = {
formatMessage: (message) => message?.defaultMessage || '',
};

function renderComponent(props = {}) {
const store = initializeStore();
history.push(pathname);
const { container } = render(
<AppProvider store={store}>
<Routes>
<Route path="/course/:courseId/:sequenceId/:unitId" element={<CoursewareSearchResultsFilter intl={intl} {...props} />} />
</Routes>
</AppProvider>,
);
return container;
}

describe('CoursewareSearchResultsFilter', () => {
beforeAll(initializeMockApp);

Expand All @@ -32,53 +75,42 @@ describe('CoursewareSearchResultsFilter', () => {
expect(results.length).toEqual(mockResults.length);
});

it('returns all values when the key value "all" is provided', () => {
const results = filteredResultsBySelection({ filterKey: 'all', results: mockResults });

expect(results.length).toEqual(mockResults.length);
});

it('returns only "video"-typed elements when the key value "video" is given', () => {
const results = filteredResultsBySelection({ filterKey: 'video', results: mockResults });
const results = filteredResultsBySelection({ key: 'video', results: mockResults });

expect(results.length).toEqual(2);
});

it('returns only "course_outline"-typed elements when the key value "document" is given', () => {
const results = filteredResultsBySelection({ filterKey: 'document', results: mockResults });
const results = filteredResultsBySelection({ key: 'document', results: mockResults });

expect(results.length).toEqual(1);
});

it('returns only "text"-typed elements when the key value "text" is given', () => {
const results = filteredResultsBySelection({ filterKey: 'text', results: mockResults });
const results = filteredResultsBySelection({ key: 'text', results: mockResults });

expect(results.length).toEqual(3);
});
});

describe('</CoursewareSearchResultsFilter />', () => {
it('should render', async () => {
await render(<CoursewareSearchResultsFilter results={mockResults} />);
beforeEach(() => {
jest.clearAllMocks();
});

await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByText(/All content/)).toBeInTheDocument();
expect(screen.queryByText(/Course outline/)).toBeInTheDocument();
expect(screen.queryByText(/Text/)).toBeInTheDocument();
expect(screen.queryByText(/Video/)).toBeInTheDocument();
it('should render', async () => {
useModel.mockReturnValue({
total: 6,
results: mockResults,
filters: [],
});
});

it('should not render if no results are provided', async () => {
await render(<CoursewareSearchResultsFilter results={[]} />);
await renderComponent();

await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
expect(screen.queryByText(/All content/)).not.toBeInTheDocument();
expect(screen.queryByText(/Course outline/)).not.toBeInTheDocument();
expect(screen.queryByText(/Text/)).not.toBeInTheDocument();
expect(screen.queryByText(/Video/)).not.toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByText(/All content/)).toBeInTheDocument();
});
});
});
Expand Down
Loading

0 comments on commit a70a26f

Please sign in to comment.