diff --git a/src/components/microlearning/BetaBadge.jsx b/src/components/microlearning/BetaBadge.jsx new file mode 100644 index 0000000000..a963be99cc --- /dev/null +++ b/src/components/microlearning/BetaBadge.jsx @@ -0,0 +1,34 @@ +import { + Badge, OverlayTrigger, Tooltip, +} from '@openedx/paragon'; +import './styles/VideoDetailPage.scss'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +const BetaBadge = () => ( + + {msg}, + }} + /> + + )} + > + + + + +); + +export default BetaBadge; diff --git a/src/components/microlearning/VideoBanner.jsx b/src/components/microlearning/VideoBanner.jsx new file mode 100644 index 0000000000..22f48f1412 --- /dev/null +++ b/src/components/microlearning/VideoBanner.jsx @@ -0,0 +1,73 @@ +import { + Card, Button, +} from '@openedx/paragon'; +import './styles/VideoDetailPage.scss'; +import { Link } from 'react-router-dom'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { AppContext } from '@edx/frontend-platform/react'; +import { useContext } from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import BetaBadge from './BetaBadge'; +import { useEnterpriseCustomer } from '../app/data'; + +const VideoBanner = () => { + const { data: enterpriseCustomer } = useEnterpriseCustomer(); + const { authenticatedUser: { userId } } = useContext(AppContext); + const sendPushEvent = () => { + sendEnterpriseTrackEvent( + enterpriseCustomer.uuid, + 'edx.ui.enterprise.learner_portal.video_banner.explore_videos_clicked', + { + userId, + }, + ); + }; + return ( +
+ + + +

+ +

+

+ +

+ +
+

+ +

+
+ + + +
+
+ ); +}; + +export default VideoBanner; diff --git a/src/components/microlearning/VideoDetailPage.jsx b/src/components/microlearning/VideoDetailPage.jsx index 31d1a08954..f0884c4838 100644 --- a/src/components/microlearning/VideoDetailPage.jsx +++ b/src/components/microlearning/VideoDetailPage.jsx @@ -52,7 +52,7 @@ const VideoDetailPage = () => { }; const enableVideos = ( features.FEATURE_ENABLE_VIDEO_CATALOG - && hasActivatedAndCurrentSubscription(subscriptionLicense) + && hasActivatedAndCurrentSubscription(subscriptionLicense, enterpriseCustomer.enableBrowseAndRequest) ); useEffect(() => { diff --git a/src/components/microlearning/styles/VideoDetailPage.scss b/src/components/microlearning/styles/VideoDetailPage.scss index 08c18d917f..dc47148a14 100644 --- a/src/components/microlearning/styles/VideoDetailPage.scss +++ b/src/components/microlearning/styles/VideoDetailPage.scss @@ -82,12 +82,14 @@ .vjs-transcribe-cueline { color: #00688d; } + .logo-cutom-style { max-width: 59px; max-height: 30px; box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.15), 0px 0px 4px 0px rgba(0, 0, 0, 0.15); } } + .preview-enroll-expand-body { max-height: 120px; -webkit-transition: max-height .5s ease; @@ -107,3 +109,12 @@ padding-left: 1.5rem; } } +.video-banner-class { + width: 906px; + height: 100px; +} +.video-beta-badge-tooltip{ + .tooltip-inner { + max-width: 310px; + } +} diff --git a/src/components/microlearning/tests/VideoBanner.test.jsx b/src/components/microlearning/tests/VideoBanner.test.jsx new file mode 100644 index 0000000000..cd57310c92 --- /dev/null +++ b/src/components/microlearning/tests/VideoBanner.test.jsx @@ -0,0 +1,73 @@ +import { screen, waitFor } from '@testing-library/react'; +import { AppContext } from '@edx/frontend-platform/react'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import userEvent from '@testing-library/user-event'; +import { useEnterpriseCustomer } from '../../app/data'; +import VideoBanner from '../VideoBanner'; +import { renderWithRouter } from '../../../utils/tests'; +import '@testing-library/jest-dom/extend-expect'; + +jest.mock('../../app/data', () => ({ + useEnterpriseCustomer: jest.fn(), +})); +jest.mock('@edx/frontend-enterprise-utils', () => ({ + sendEnterpriseTrackEvent: jest.fn(), +})); + +describe('VideoBanner', () => { + const mockEnterpriseCustomer = { + uuid: 'mock-uuid', + }; + + const mockAuthenticatedUser = { + userId: 'test-user-id', + }; + const VideoBannerWrapper = () => ( + + + + + + ); + + beforeEach(() => { + useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer }); + }); + + it('renders the video banner with correct title and description', () => { + renderWithRouter(); + + expect(screen.getByText('New!')).toBeInTheDocument(); + expect(screen.getByText('Videos Now Available with Your Subscription')).toBeInTheDocument(); + expect(screen.getByText('Transform your potential into success.')).toBeInTheDocument(); + }); + + it('renders the explore videos button', () => { + renderWithRouter(); + + expect(screen.getByText('Explore Videos')).toBeInTheDocument(); + }); + + it('calls sendEnterpriseTrackEvent when explore videos button is clicked', () => { + renderWithRouter(); + + const exploreVideosButton = screen.getByText('Explore Videos'); + exploreVideosButton.click(); + + expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith( + mockEnterpriseCustomer.uuid, + 'edx.ui.enterprise.learner_portal.video_banner.explore_videos_clicked', + { + userId: mockAuthenticatedUser.userId, + }, + ); + }); + it('hover on Beta badge', async () => { + renderWithRouter(); + userEvent.hover(screen.getByText('Beta')); + await waitFor(() => { + expect(screen.getByText('Beta version of the Videos.')).toBeVisible(); + }); + }); +}); diff --git a/src/components/microlearning/tests/VideoDetailPage.test.jsx b/src/components/microlearning/tests/VideoDetailPage.test.jsx index e9ff8f74dc..dec6fb469f 100644 --- a/src/components/microlearning/tests/VideoDetailPage.test.jsx +++ b/src/components/microlearning/tests/VideoDetailPage.test.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import axios from 'axios'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppContext } from '@edx/frontend-platform/react'; diff --git a/src/components/search/Search.jsx b/src/components/search/Search.jsx index 03616f9502..1c392493dd 100644 --- a/src/components/search/Search.jsx +++ b/src/components/search/Search.jsx @@ -33,6 +33,7 @@ import { useAlgoliaSearch } from '../../utils/hooks'; import ContentTypeSearchResultsContainer from './ContentTypeSearchResultsContainer'; import SearchVideo from './SearchVideo'; import { hasActivatedAndCurrentSubscription } from './utils'; +import VideoBanner from '../microlearning/VideoBanner'; export const sendPushEvent = (isPreQueryEnabled, courseKeyMetadata) => { if (isPreQueryEnabled) { @@ -97,7 +98,7 @@ const Search = () => { const enableVideos = ( canOnlyViewHighlightSets === false && features.FEATURE_ENABLE_VIDEO_CATALOG - && hasActivatedAndCurrentSubscription(subscriptionLicense) + && hasActivatedAndCurrentSubscription(subscriptionLicense, enterpriseCustomer.enableBrowseAndRequest) ); const PAGE_TITLE = intl.formatMessage({ @@ -179,12 +180,13 @@ const Search = () => { {/* No content type refinement */} {(contentType === undefined || contentType.length === 0) && ( + {enableVideos && } {!hasRefinements && } {canOnlyViewHighlightSets === false && enterpriseCustomer.enableAcademies && } {features.ENABLE_PATHWAYS && (canOnlyViewHighlightSets === false) && } {features.ENABLE_PROGRAMS && (canOnlyViewHighlightSets === false) && } - {canOnlyViewHighlightSets === false && } - {enableVideos && } + {canOnlyViewHighlightSets === false && } + {enableVideos && } )} {/* render a single contentType if the refinement exist and is either a course, program or learnerpathway */} diff --git a/src/components/search/SearchPage.jsx b/src/components/search/SearchPage.jsx index 1e7bb40e43..13c384bf7c 100644 --- a/src/components/search/SearchPage.jsx +++ b/src/components/search/SearchPage.jsx @@ -2,19 +2,20 @@ import React from 'react'; import { SearchData } from '@edx/frontend-enterprise-catalog-search'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useSubscriptions } from '../app/data'; +import { useEnterpriseCustomer, useSubscriptions } from '../app/data'; import Search from './Search'; import { SEARCH_TRACKING_NAME } from './constants'; import { getSearchFacetFilters, hasActivatedAndCurrentSubscription } from './utils'; import { features } from '../../config'; const SearchPage = () => { + const { data: enterpriseCustomer } = useEnterpriseCustomer(); const intl = useIntl(); const { data: { subscriptionLicense } } = useSubscriptions(); const enableVideos = ( features.FEATURE_ENABLE_VIDEO_CATALOG - && hasActivatedAndCurrentSubscription(subscriptionLicense) + && hasActivatedAndCurrentSubscription(subscriptionLicense, enterpriseCustomer.enableBrowseAndRequest) ); return ( diff --git a/src/components/search/SearchResults.jsx b/src/components/search/SearchResults.jsx index 9bbcf80768..1a6722313c 100644 --- a/src/components/search/SearchResults.jsx +++ b/src/components/search/SearchResults.jsx @@ -20,6 +20,7 @@ import { CARDGRID_COLUMN_SIZES, } from './constants'; import { getContentTypeFromTitle, getNoOfResultsFromTitle, getSkeletonCardFromTitle } from '../utils/search'; +import BetaBadge from '../microlearning/BetaBadge'; const SearchResults = ({ className, @@ -32,6 +33,7 @@ const SearchResults = ({ contentType, translatedTitle, isPathwaySearchResults, + showBetaBadge, }) => { const { refinements, dispatch } = useContext(SearchContext); const nbHits = useNbHitsFromSearchResults(searchResults); @@ -96,11 +98,14 @@ const SearchResults = ({ defaultMessage: 'result', description: 'Label for the search result count when we have only one result.', }); + // I have added a condition to check if showBetaBadge is true then show the BetaBadge component + // Theses changes are temporary and will be removed once the BetaBadge component is removed from + // the SearchResults component return ( - <> - {translatedTitle || title} ({nbHits} {resultsLabel}) +
+ {translatedTitle || title} ({nbHits} {resultsLabel}) {showBetaBadge && } {query && <>{' '}for "{query}"} - +
); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -196,6 +201,7 @@ SearchResults.propTypes = { title: PropTypes.string.isRequired, translatedTitle: PropTypes.string, isPathwaySearchResults: PropTypes.bool, + showBetaBadge: PropTypes.bool, }; SearchResults.defaultProps = { @@ -206,6 +212,7 @@ SearchResults.defaultProps = { contentType: undefined, translatedTitle: undefined, isPathwaySearchResults: false, + showBetaBadge: false, }; export default connectStateResults(SearchResults); diff --git a/src/components/search/SearchVideo.jsx b/src/components/search/SearchVideo.jsx index 1996278a49..32f51878fd 100644 --- a/src/components/search/SearchVideo.jsx +++ b/src/components/search/SearchVideo.jsx @@ -29,6 +29,7 @@ const SearchVideo = ({ filter }) => { description: 'Translated title for the enterprise search page videos section.', }) } + showBetaBadge /> ); diff --git a/src/components/search/tests/Search.test.jsx b/src/components/search/tests/Search.test.jsx new file mode 100644 index 0000000000..b5d0686807 --- /dev/null +++ b/src/components/search/tests/Search.test.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { SearchContext } from '@edx/frontend-enterprise-catalog-search'; +import { AppContext } from '@edx/frontend-platform/react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import '../../skills-quiz/__mocks__/react-instantsearch-dom'; +import { queryClient, renderWithRouter } from '../../../utils/tests'; +import '@testing-library/jest-dom'; +import Search from '../Search'; +import { useDefaultSearchFilters, useEnterpriseFeatures, useEnterpriseCustomer } from '../../app/data'; +import { useAlgoliaSearch } from '../../../utils/hooks'; +import { enterpriseCustomerFactory } from '../../app/data/services/data/__factories__'; +import { features } from '../../../config'; + +jest.mock('../../app/data', () => ({ + ...jest.requireActual('../../app/data'), + useEnterpriseCustomer: jest.fn(), + useSubscriptions: jest.fn(() => ({ + data: { + subscriptionLicense: { + status: 'activated', + subscriptionPlan: { isCurrent: true }, + }, + }, + })), + useRedeemablePolicies: jest.fn(() => ({ data: { redeemablePolicies: [] } })), + useCouponCodes: jest.fn(() => ({ data: { couponCodeAssignments: [] } })), + useEnterpriseOffers: jest.fn(() => ({ data: { currentEnterpriseOffers: [] } })), + useBrowseAndRequestConfiguration: jest.fn(() => ({ data: {} })), + useContentHighlightsConfiguration: jest.fn(() => ({ data: {} })), + useCanOnlyViewHighlights: jest.fn(() => ({ data: false })), + useIsAssignmentsOnlyLearner: jest.fn().mockReturnValue(false), + useEnterpriseFeatures: jest.fn(), + useDefaultSearchFilters: jest.fn(), +})); +jest.mock('../../../utils/hooks', () => ({ + ...jest.requireActual('../../../utils/hooks'), + useAlgoliaSearch: jest.fn(), +})); +jest.mock('../../../utils/optimizely', () => ({ + ...jest.requireActual('../../../utils/optimizely'), + pushEvent: jest.fn(), +})); +const searchContext4 = { + refinements: { content_type: undefined }, + dispatch: () => null, +}; +const initialAppState = { + authenticatedUser: { userId: 'test-user-id' }, +}; + +jest.mock('@edx/frontend-enterprise-catalog-search', () => ({ + ...jest.requireActual('@edx/frontend-enterprise-catalog-search'), + SearchHeader: jest.fn(() =>
), +})); + +const SearchWrapper = ({ + appState = initialAppState, + searchContext = searchContext4, + children, +}) => ( + + + + + {children} + + + + +); +const mockEnterpriseCustomer = enterpriseCustomerFactory(); +const mockFilter = `enterprise_customer_uuids: ${mockEnterpriseCustomer.uuid}`; +describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer }); + useEnterpriseFeatures.mockReturnValue({ data: { featurePrequerySearchSuggestions: false } }); + useDefaultSearchFilters.mockReturnValue(mockFilter); + useAlgoliaSearch.mockReturnValue([{ search: jest.fn(), appId: 'test-app-id' }, { indexName: 'mock-index-name' }]); + }); + test('renders the video beta banner component', () => { + features.FEATURE_ENABLE_VIDEO_CATALOG = true; + renderWithRouter( + + + , + ); + expect(screen.getByText('Videos Now Available with Your Subscription')).toBeInTheDocument(); + }); +}); diff --git a/src/components/search/tests/SearchPage.test.jsx b/src/components/search/tests/SearchPage.test.jsx index 8a75476abf..90247e8a2a 100644 --- a/src/components/search/tests/SearchPage.test.jsx +++ b/src/components/search/tests/SearchPage.test.jsx @@ -3,14 +3,16 @@ import { screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { AppContext } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { useSubscriptions } from '../../app/data'; +import { useEnterpriseCustomer, useSubscriptions } from '../../app/data'; import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants'; import SearchPage from '../SearchPage'; import { features } from '../../../config'; import { renderWithRouter } from '../../../utils/tests'; +import { enterpriseCustomerFactory } from '../../app/data/services/data/__factories__'; jest.mock('../../app/data', () => ({ useSubscriptions: jest.fn(), + useEnterpriseCustomer: jest.fn(), })); jest.mock('../utils', () => ({ @@ -33,11 +35,12 @@ const renderSearchPage = () => renderWithRouter( , ); - +const mockEnterpriseCustomer = enterpriseCustomerFactory(); describe('SearchPage', () => { beforeEach(() => { jest.clearAllMocks(); features.FEATURE_ENABLE_VIDEO_CATALOG = true; + useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer }); }); it('renders SearchPage component', () => { diff --git a/src/components/search/tests/utils.test.js b/src/components/search/tests/utils.test.js index 1a6247f0cf..6c9a3b4ab7 100644 --- a/src/components/search/tests/utils.test.js +++ b/src/components/search/tests/utils.test.js @@ -24,8 +24,9 @@ describe('hasActivatedAndCurrentSubscription', () => { isCurrent: true, }, }; + const enableBrowseAndRequest = false; - const result = hasActivatedAndCurrentSubscription(subscriptionLicense); + const result = hasActivatedAndCurrentSubscription(subscriptionLicense, enableBrowseAndRequest); expect(result).toBe(true); }); @@ -36,13 +37,20 @@ describe('hasActivatedAndCurrentSubscription', () => { isCurrent: false, }, }; + const enableBrowseAndRequest = false; - const result = hasActivatedAndCurrentSubscription(subscriptionLicense); + const result = hasActivatedAndCurrentSubscription(subscriptionLicense, enableBrowseAndRequest); expect(result).toBe(false); }); it('should return false when subscriptionLicense is undefined', () => { - const result = hasActivatedAndCurrentSubscription(undefined); + const enableBrowseAndRequest = false; + const result = hasActivatedAndCurrentSubscription(undefined, enableBrowseAndRequest); expect(result).toBe(false); }); + it('should return true when enableBrowseAndRequest is true', () => { + const enableBrowseAndRequest = true; + const result = hasActivatedAndCurrentSubscription(undefined, enableBrowseAndRequest); + expect(result).toBe(true); + }); }); diff --git a/src/components/search/utils.js b/src/components/search/utils.js index 6fa5bd2120..d41fde3ab2 100644 --- a/src/components/search/utils.js +++ b/src/components/search/utils.js @@ -60,7 +60,10 @@ export function getSearchFacetFilters(intl) { return searchFilters; } -export const hasActivatedAndCurrentSubscription = (subscriptionLicense) => ( - subscriptionLicense?.status === LICENSE_STATUS.ACTIVATED - && subscriptionLicense?.subscriptionPlan?.isCurrent +export const hasActivatedAndCurrentSubscription = ( + subscriptionLicense, + enableBrowseAndRequest, +) => ( + (subscriptionLicense?.status === LICENSE_STATUS.ACTIVATED + && subscriptionLicense?.subscriptionPlan?.isCurrent) || enableBrowseAndRequest );