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

feat: add course/program detail to 'View Course' button #1065

Merged
merged 29 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cfc8972
feat: display search result cards in catalog tab
katrinan029 Oct 12, 2023
853811c
fix: failing test in BudgetDetailPage
katrinan029 Oct 13, 2023
2a94621
fix: replace word register with enroll
katrinan029 Oct 13, 2023
3689f23
fix: implemented reviewer comments
katrinan029 Oct 16, 2023
c5f3d6e
fix: lint error
katrinan029 Oct 16, 2023
4695894
chore: rebase
katrinan029 Oct 19, 2023
af78b0d
fix: lint error
katrinan029 Oct 19, 2023
db847ca
feat: added policy's catalog uuid to search filter
katrinan029 Oct 19, 2023
c11a6d3
feat: implement view course button to learn more
katrinan029 Oct 19, 2023
2c7bd80
fix: failing test
katrinan029 Oct 19, 2023
4328754
chore: rebase
katrinan029 Oct 19, 2023
a7158f6
fix: added test
katrinan029 Oct 19, 2023
602cdfa
fix: added test coverage
katrinan029 Oct 19, 2023
08e54d4
fix: refactored based on reviewer feedback
katrinan029 Oct 25, 2023
f95d489
fix: lint error
katrinan029 Oct 25, 2023
94bf5ea
fix: refactored code to include new api field and updated test
katrinan029 Oct 26, 2023
284eade
fix: removing unused prop in test
katrinan029 Oct 26, 2023
491fc21
Merge remote-tracking branch 'origin' into knguyen2/ENT-7591
katrinan029 Oct 26, 2023
f621880
fix: refactored
katrinan029 Oct 26, 2023
5a56945
fix: search filters
katrinan029 Oct 26, 2023
dc7109d
chore: rebase
katrinan029 Oct 27, 2023
b2543ac
chore: rebase
katrinan029 Oct 27, 2023
4736de4
chore: rebase
katrinan029 Oct 27, 2023
74e91df
chore: refactored
katrinan029 Oct 27, 2023
2676168
chore: fix lint error
katrinan029 Oct 27, 2023
2555222
chore: refactored
katrinan029 Oct 30, 2023
9373a68
Merge remote-tracking branch 'origin' into knguyen/ENT-7594
katrinan029 Oct 30, 2023
7fd32e3
fix: updated failing test
katrinan029 Oct 30, 2023
1ac7055
Merge remote-tracking branch 'origin' into knguyen/ENT-7594
katrinan029 Oct 30, 2023
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 @@ -8,17 +8,10 @@ import CatalogSearch from './search/CatalogSearch';
import {
LANGUAGE_REFINEMENT,
LEARNING_TYPE_REFINEMENT,
useBudgetId,
useSubsidyAccessPolicy,
} from './data';
import { configuration } from '../../config';

const BudgetDetailCatalogTabContents = () => {
const { subsidyAccessPolicyId } = useBudgetId();
const {
data: subsidyAccessPolicy,
} = useSubsidyAccessPolicy(subsidyAccessPolicyId);

const language = {
attribute: LANGUAGE_REFINEMENT,
title: 'Language',
Expand Down Expand Up @@ -48,7 +41,7 @@ const BudgetDetailCatalogTabContents = () => {
indexName={configuration.ALGOLIA.INDEX_NAME}
searchClient={searchClient}
>
<CatalogSearch catalogUuid={subsidyAccessPolicy?.catalogUuid} />
<CatalogSearch />
</InstantSearch>
</SearchData>
</Col>
Expand Down
26 changes: 21 additions & 5 deletions src/components/learner-credit-management/cards/CourseCard.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
// variables taken from algolia not in camelcase
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { connect } from 'react-redux';

import {
Badge,
Expand All @@ -21,17 +23,19 @@ import { formatPrice, formatDate, getEnrollmentDeadline } from '../data/utils';
import CARD_TEXT from '../constants';

const CourseCard = ({
original,
original, enterpriseSlug,
}) => {
const {
availability,
cardImageUrl,
courseType,
key,
normalizedMetadata,
partners,
title,
} = camelCaseObject(original);

const { config: { ENTERPRISE_LEARNER_PORTAL_URL } } = useContext(AppContext);
const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth });
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });

Expand Down Expand Up @@ -72,6 +76,13 @@ const CourseCard = ({

const isExecEd = courseType === EXEC_ED_COURSE_TYPE;

let linkToCourse;
if (isExecEd) {
linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/executive-education-2u/course/${key}`;
} else {
linkToCourse = `${ENTERPRISE_LEARNER_PORTAL_URL}/${enterpriseSlug}/course/${key}`;
}

return (
<Card
orientation={isSmall ? 'vertical' : 'horizontal'}
Expand Down Expand Up @@ -104,9 +115,9 @@ const CourseCard = ({
textElement={isExecEd ? execEdEnrollmentInfo : courseEnrollmentInfo}
>
<Button
// TODO: Implementation to follow in ENT-7594
as={Hyperlink}
destination="https://enterprise.stage.edx.org"
data-testid="hyperlink-view-course"
destination={linkToCourse}
target="_blank"
variant="outline-primary"
>
Expand All @@ -120,6 +131,7 @@ const CourseCard = ({
};

CourseCard.propTypes = {
enterpriseSlug: PropTypes.string.isRequired,
original: PropTypes.shape({
availability: PropTypes.arrayOf(PropTypes.string),
cardImageUrl: PropTypes.string,
Expand All @@ -136,4 +148,8 @@ CourseCard.propTypes = {
}).isRequired,
};

export default injectIntl(CourseCard);
const mapStateToProps = state => ({
enterpriseSlug: state.portalConfiguration.enterpriseSlug,
});

export default connect(mapStateToProps)(injectIntl(CourseCard));
67 changes: 51 additions & 16 deletions src/components/learner-credit-management/cards/CourseCard.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { AppContext } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import CourseCard from './CourseCard';

const originalData = {
availability: ['Upcoming'],
card_image_url: undefined,
course_type: 'course',
key: 'course-123x',
normalized_metadata: {
enroll_by_date: '2016-02-18T04:00:00Z',
start_date: '2016-04-18T04:00:00Z',
Expand All @@ -23,10 +27,13 @@ const defaultProps = {
original: originalData,
};

const mockLearnerPortal = 'https://enterprise.stage.edx.org';

const execEdData = {
availability: ['Upcoming'],
card_image_url: undefined,
course_type: 'executive-education-2u',
key: 'exec-ed-course-123x',
entitlements: [{ price: '999.00' }],
normalized_metadata: {
enroll_by_date: '2016-02-18T04:00:00Z',
Expand All @@ -42,13 +49,41 @@ const execEdProps = {
original: execEdData,
};

const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
const enterpriseSlug = 'test-enterprise-slug';
const enterpriseUUID = '1234';
const initialStoreState = {
portalConfiguration: {
enterpriseId: enterpriseUUID,
enterpriseSlug,
},
};

const CourseCardWrapper = ({
initialState = initialStoreState,
...rest
}) => {
const store = getMockStore({ ...initialState });

return (
<IntlProvider locale="en">
<Provider store={store}>
<AppContext.Provider
value={{
config: { ENTERPRISE_LEARNER_PORTAL_URL: mockLearnerPortal },
}}
>
<CourseCard {...rest} />
</AppContext.Provider>
</Provider>
</IntlProvider>
);
};

describe('Course card works as expected', () => {
test('course card renders', () => {
render(
<IntlProvider locale="en">
<CourseCard {...defaultProps} />
katrinan029 marked this conversation as resolved.
Show resolved Hide resolved
</IntlProvider>,
);
render(<CourseCardWrapper {...defaultProps} />);
expect(screen.queryByText(defaultProps.original.title)).toBeInTheDocument();
expect(
screen.queryByText(defaultProps.original.partners[0].name),
Expand All @@ -59,27 +94,27 @@ describe('Course card works as expected', () => {
expect(screen.queryByText('Course')).toBeInTheDocument();
expect(screen.queryByText('View course')).toBeInTheDocument();
expect(screen.queryByText('Assign')).toBeInTheDocument();
const hyperlink = screen.getByRole('link', {
name: 'View course Opens in a new tab',
});
expect(hyperlink.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/course/course-123x');
});

test('test card renders default image', async () => {
render(
<IntlProvider locale="en">
<CourseCard {...defaultProps} />
</IntlProvider>,
);
render(<CourseCardWrapper {...defaultProps} />);
const imageAltText = `${originalData.title} course image`;
fireEvent.error(screen.getByAltText(imageAltText));
await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined;
});

test('exec ed card renders', async () => {
render(
<IntlProvider locale="en">
<CourseCard {...execEdProps} />
</IntlProvider>,
);
render(<CourseCardWrapper {...execEdProps} />);
expect(screen.queryByText('$999')).toBeInTheDocument();
expect(screen.queryByText('Starts Apr 18, 2016 • Learner must enroll by Feb 18, 2016')).toBeInTheDocument();
expect(screen.queryByText('Executive Education')).toBeInTheDocument();
const hyperlink = screen.getByRole('link', {
name: 'View course Opens in a new tab',
});
expect(hyperlink.href).toContain('https://enterprise.stage.edx.org/test-enterprise-slug/executive-education-2u/course/exec-ed-course-123x');
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { Configure, InstantSearch } from 'react-instantsearch-dom';
import PropTypes from 'prop-types';

import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { SearchHeader } from '@edx/frontend-enterprise-catalog-search';

import { configuration } from '../../../config';
import CatalogSearchResults from './CatalogSearchResults';
import { SEARCH_RESULT_PAGE_SIZE } from '../data';
import { SEARCH_RESULT_PAGE_SIZE, useBudgetId, useSubsidyAccessPolicy } from '../data';

const CatalogSearch = ({ catalogUuid }) => {
const CatalogSearch = () => {
const searchClient = algoliasearch(configuration.ALGOLIA.APP_ID, configuration.ALGOLIA.SEARCH_API_KEY);
const searchFilters = `enterprise_catalog_uuids:${catalogUuid} AND content_type:course`;
const { subsidyAccessPolicyId } = useBudgetId();
const {
data: subsidyAccessPolicy,
} = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const searchFilters = `enterprise_catalog_uuids:${subsidyAccessPolicy.catalogUuid} AND content_type:course`;

return (
<section>
Expand Down Expand Up @@ -41,8 +44,4 @@ const CatalogSearch = ({ catalogUuid }) => {
);
};

CatalogSearch.propTypes = {
catalogUuid: PropTypes.string.isRequired,
};

export default CatalogSearch;
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,48 @@ import {
} from '@edx/frontend-enterprise-catalog-search';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderWithRouter } from '../../test/testUtils';
import CatalogSearch from '../search/CatalogSearch';

import { useBudgetId, useSubsidyAccessPolicy } from '../data';

jest.mock('react-instantsearch-dom', () => ({
...jest.requireActual('react-instantsearch-dom'),
InstantSearch: () => <div>SEARCH</div>,
Index: () => <div>SEARCH</div>,
}));

jest.mock('../data');

const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} };
const queryClient = new QueryClient();

const SearchDataWrapper = ({ children, searchContextValue }) => (
<IntlProvider locale="en">
<SearchContext.Provider
value={searchContextValue}
searchFacetFilters={[
...SEARCH_FACET_FILTERS,
]}
>
{children}
</SearchContext.Provider>
</IntlProvider>
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<SearchContext.Provider
value={searchContextValue}
searchFacetFilters={[
...SEARCH_FACET_FILTERS,
]}
>
{children}
</SearchContext.Provider>
</IntlProvider>
</QueryClientProvider>
);

describe('Catalog Search component', () => {
it('properly renders component', () => {
useBudgetId.mockReturnValue({
subsidyAccessPolicyId: 'test-id',
});
useSubsidyAccessPolicy.mockReturnValue({
data: {
catalogUuid: '123',
},
});
renderWithRouter(
<SearchDataWrapper searchContextValue={DEFAULT_SEARCH_CONTEXT_VALUE}>
<CatalogSearch />
Expand Down
Loading