From a0fb639c34f7907ac8d0ef41ffff730065b27662 Mon Sep 17 00:00:00 2001 From: Saleem Latif Date: Tue, 21 May 2024 10:11:20 +0500 Subject: [PATCH] feat: Added scripts and marked strings for i18n. (#396) --- .gitignore | 4 + Makefile | 21 +++ i18n/.gitkeep | 1 + package.json | 3 +- .../src/ClearCurrentRefinements.jsx | 7 +- .../catalog-search/src/CurrentRefinements.jsx | 52 +++++- packages/catalog-search/src/FacetListBase.jsx | 13 +- .../src/LearningTypeRadioFacet.jsx | 32 +++- .../catalog-search/src/MobileFilterMenu.jsx | 41 ++++- packages/catalog-search/src/SearchBox.jsx | 14 +- packages/catalog-search/src/SearchContext.jsx | 13 +- .../catalog-search/src/SearchPagination.jsx | 28 ++- .../catalog-search/src/SearchSuggestions.jsx | 31 +++- packages/catalog-search/src/data/constants.js | 2 - packages/catalog-search/src/index.js | 4 + .../tests/ClearCurrentRefinements.test.jsx | 13 +- .../src/tests/CurrentRefinements.test.jsx | 13 +- .../src/tests/FacetListBase.test.jsx | 49 +++-- .../src/tests/FacetListRefinement.test.jsx | 30 ++-- .../src/tests/MobileFilterMenu.test.jsx | 13 +- .../src/tests/SearchBox.test.jsx | 10 +- .../src/tests/SearchPagination.test.jsx | 17 +- .../src/tests/SearchSuggestions.test.jsx | 18 +- packages/catalog-search/src/tests/utils.jsx | 21 ++- packages/catalog-search/src/utils.js | 169 ++++++++++++++++++ 25 files changed, 493 insertions(+), 126 deletions(-) create mode 100644 Makefile create mode 100644 i18n/.gitkeep create mode 100644 packages/catalog-search/src/utils.js diff --git a/.gitignore b/.gitignore index b7f8a607..ccae6240 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ lerna-debug.log coverage/ dist/ packages/*/package-lock.json + +# Transifex +i18n/transifex_input.json +temp diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9a214198 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +transifex_utils = ./node_modules/.bin/transifex-utils.js +i18n = ./i18n +transifex_input = $(i18n)/transifex_input.json + +# This directory must match .babelrc . +transifex_temp = ./temp/babel-plugin-formatjs + +i18n.extract: + # Pulling display strings from .jsx files into .json files... + rm -rf $(transifex_temp) + npm run-script i18n_extract + +i18n.concat: + # Gathering JSON messages into one file... + $(transifex_utils) $(transifex_temp) $(transifex_input) + +extract_translations: | requirements i18n.extract i18n.concat + +.PHONY: requirements +requirements: ## install ci requirements + npm ci diff --git a/i18n/.gitkeep b/i18n/.gitkeep new file mode 100644 index 00000000..ae683e3f --- /dev/null +++ b/i18n/.gitkeep @@ -0,0 +1 @@ +# Empty file to preserve directory structure diff --git a/package.json b/package.json index 181f7002..65e1943c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "lint:fix": "npm run lint:fix --workspaces", "bootstrap": "npm install conventional-changelog-conventionalcommits", "changed": "lerna changed", - "lerna:version": "npx lerna@6 version --conventional-commits --create-release github --no-push" + "lerna:version": "npx lerna@6 version --conventional-commits --create-release github --no-push", + "i18n_extract": "fedx-scripts formatjs extract packages/**/**/*.{js,jsx,ts,tsx}" }, "devDependencies": { "@commitlint/config-conventional": "17.6.0", diff --git a/packages/catalog-search/src/ClearCurrentRefinements.jsx b/packages/catalog-search/src/ClearCurrentRefinements.jsx index 84abd1af..97116134 100644 --- a/packages/catalog-search/src/ClearCurrentRefinements.jsx +++ b/packages/catalog-search/src/ClearCurrentRefinements.jsx @@ -1,6 +1,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { Button } from '@openedx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { SearchContext } from './SearchContext'; import { clearRefinementsAction } from './data/actions'; @@ -28,7 +29,11 @@ const ClearCurrentRefinements = ({ className, variant, ...props }) => { onClick={handleClearAllRefinementsClick} {...props} > - {CLEAR_ALL_TEXT} + ) } diff --git a/packages/catalog-search/src/CurrentRefinements.jsx b/packages/catalog-search/src/CurrentRefinements.jsx index 6529ac93..8664e302 100644 --- a/packages/catalog-search/src/CurrentRefinements.jsx +++ b/packages/catalog-search/src/CurrentRefinements.jsx @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { Badge, Button } from '@openedx/paragon'; import { CloseSmall } from '@openedx/paragon/icons'; import { connectCurrentRefinements } from 'react-instantsearch-dom'; +import { FormattedMessage, defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import ClearCurrentRefinements from './ClearCurrentRefinements'; @@ -14,6 +15,8 @@ import { NUM_CURRENT_REFINEMENTS_TO_DISPLAY, STYLE_VARIANTS, LEARNING_TYPE_PATHWAY, + LEARNING_TYPE_COURSE, + LEARNING_TYPE_PROGRAM, } from './data/constants'; import { useActiveRefinementsAsFlatArray, @@ -21,6 +24,24 @@ import { import { SearchContext } from './SearchContext'; import { removeFromRefinementArray, deleteRefinementAction } from './data/actions'; +const messages = defineMessages({ + [LEARNING_TYPE_COURSE]: { + id: 'search.facetFilters.filterTitle.course', + defaultMessage: 'Course', + description: 'Title for the course filter.', + }, + [LEARNING_TYPE_PROGRAM]: { + id: 'search.facetFilters.filterTitle.program', + defaultMessage: 'Program', + description: 'Title for the program filter.', + }, + [LEARNING_TYPE_PATHWAY]: { + id: 'search.facetFilters.filterTitle.pathway', + defaultMessage: 'Pathway', + description: 'Title for the pathway filter.', + }, +}); + export const CurrentRefinementsBase = ({ items, variant }) => { if (!items || !items.length) { return null; @@ -29,6 +50,7 @@ export const CurrentRefinementsBase = ({ items, variant }) => { const [showAllRefinements, setShowAllRefinements] = useState(false); const { refinements, dispatch } = useContext(SearchContext); const activeRefinementsAsFlatArray = useActiveRefinementsAsFlatArray(items); + const intl = useIntl(); /** * Determines the correct number of active refinements to show at any @@ -83,10 +105,19 @@ export const CurrentRefinementsBase = ({ items, variant }) => { variant="light" onClick={() => handleRefinementBadgeClick(item)} > - {/* Temporary fix : can be removed when learnerpathway content type is changed to pathways */} - {item.label === LEARNING_TYPE_PATHWAY ? 'Pathway' : item.label} + + {messages[item.label] ? intl.formatMessage(messages[item.label]) : item.label} + + - Remove the filter {item.label} + + + ))} @@ -98,7 +129,14 @@ export const CurrentRefinementsBase = ({ items, variant }) => { onClick={() => setShowAllRefinements(true)} > +{activeRefinementsAsFlatArray.length - NUM_CURRENT_REFINEMENTS_TO_DISPLAY} - Show all {activeRefinementsAsFlatArray.length} filters + + + )} @@ -113,7 +151,11 @@ export const CurrentRefinementsBase = ({ items, variant }) => { variant="link" size="inline" > - show less + )} diff --git a/packages/catalog-search/src/FacetListBase.jsx b/packages/catalog-search/src/FacetListBase.jsx index f510f757..f8bb5957 100644 --- a/packages/catalog-search/src/FacetListBase.jsx +++ b/packages/catalog-search/src/FacetListBase.jsx @@ -2,7 +2,8 @@ import React, { useCallback, useContext } from 'react'; import PropTypes from 'prop-types'; -import { NO_OPTIONS_FOUND, STYLE_VARIANTS } from './data/constants'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { STYLE_VARIANTS } from './data/constants'; import FacetDropdown from './FacetDropdown'; import TypeaheadFacetDropdown from './TypeaheadFacetDropdown'; import FacetItem from './FacetItem'; @@ -62,7 +63,15 @@ const FacetListBase = ({ const renderItems = useCallback( () => { if (!items?.length) { - return {NO_OPTIONS_FOUND}; + return ( + + + + ); } return items.map((item) => { diff --git a/packages/catalog-search/src/LearningTypeRadioFacet.jsx b/packages/catalog-search/src/LearningTypeRadioFacet.jsx index 449ac518..bb831531 100644 --- a/packages/catalog-search/src/LearningTypeRadioFacet.jsx +++ b/packages/catalog-search/src/LearningTypeRadioFacet.jsx @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Dropdown, Input } from '@openedx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { SearchContext } from './SearchContext'; import { setRefinementAction, @@ -11,6 +12,7 @@ import { LEARNING_TYPE_COURSE, LEARNING_TYPE_PROGRAM, LEARNING_TYPE_PATHWAY } fr const LearningTypeRadioFacet = ({ enablePathways }) => { const { refinements, dispatch } = useContext(SearchContext); + // only bold the dropdown title if the learning type is Course or Program const typeCourseSelected = refinements.content_type && refinements.content_type.includes(LEARNING_TYPE_COURSE); const typeProgramSelected = refinements.content_type && refinements.content_type.includes(LEARNING_TYPE_PROGRAM); @@ -33,7 +35,11 @@ const LearningTypeRadioFacet = ({ enablePathways }) => { variant="inverse-primary" className={classNames({ 'font-weight-bold': boldTitle })} > - Learning Type + @@ -45,7 +51,11 @@ const LearningTypeRadioFacet = ({ enablePathways }) => { data-testid="learning-type-any" /> - Any + @@ -57,7 +67,11 @@ const LearningTypeRadioFacet = ({ enablePathways }) => { data-testid="learning-type-courses" /> - Courses + @@ -69,7 +83,11 @@ const LearningTypeRadioFacet = ({ enablePathways }) => { data-testid="learning-type-programs" /> - Programs + { @@ -85,7 +103,11 @@ const LearningTypeRadioFacet = ({ enablePathways }) => { data-testid="learning-type-pathways" /> - Pathways + ) diff --git a/packages/catalog-search/src/MobileFilterMenu.jsx b/packages/catalog-search/src/MobileFilterMenu.jsx index 26a241cd..829b25ed 100644 --- a/packages/catalog-search/src/MobileFilterMenu.jsx +++ b/packages/catalog-search/src/MobileFilterMenu.jsx @@ -5,6 +5,7 @@ import { connectCurrentRefinements } from 'react-instantsearch-dom'; import { Button } from '@openedx/paragon'; import { ArrowDropDown, Close } from '@openedx/paragon/icons'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; import ClearCurrentRefinements from './ClearCurrentRefinements'; import { useActiveRefinementsAsFlatArray } from './data/hooks'; @@ -22,10 +23,19 @@ export const MobileFilterMenuBase = ({ children, className, items }) => { onClick={() => setIsOpen(true)} >
- Filters + {activeRefinementsAsFlatArray && activeRefinementsAsFlatArray.length > 0 && ( - ({activeRefinementsAsFlatArray.length} selected) + )}
@@ -45,10 +55,19 @@ export const MobileFilterMenuBase = ({ children, className, items }) => {
- All Filters + {activeRefinementsAsFlatArray && activeRefinementsAsFlatArray.length > 0 && ( - ({activeRefinementsAsFlatArray.length} selected) + )}
@@ -60,7 +79,13 @@ export const MobileFilterMenuBase = ({ children, className, items }) => { - close filter menu + + +
@@ -75,7 +100,11 @@ export const MobileFilterMenuBase = ({ children, className, items }) => { className="btn-brand-primary btn-block py-2 m-0" onClick={() => setIsOpen(false)} > - Done +
diff --git a/packages/catalog-search/src/SearchBox.jsx b/packages/catalog-search/src/SearchBox.jsx index 1334239d..54c5ec66 100644 --- a/packages/catalog-search/src/SearchBox.jsx +++ b/packages/catalog-search/src/SearchBox.jsx @@ -10,6 +10,7 @@ import classNames from 'classnames'; import { SearchField } from '@openedx/paragon'; import debounce from 'lodash.debounce'; import { connectSearchBox } from 'react-instantsearch-dom'; +import { useIntl, defineMessages } from '@edx/frontend-platform/i18n'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -26,7 +27,15 @@ import { } from './data/constants'; import SearchSuggestions from './SearchSuggestions'; -export const searchText = 'Search courses'; +const messages = defineMessages({ + searchCoursesText: { + id: 'header.search.input.box.placeholder', + description: 'Placeholder text for the search input box', + defaultMessage: 'Search courses', + }, +}); + +export const searchText = messages.searchCoursesText.defaultMessage; // this prefix will be combined with one of the SearchBox props to create a full tracking event name // only if event name prop is provided by user. In the absence of the tracking name prop, // no tracking event will be sent. @@ -53,6 +62,7 @@ export const SearchBoxBase = ({ const [showSuggestions, setShowSuggestions] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [preQueryHits, setPreQueryHits] = useState([]); + const intl = useIntl(); /** * Handles when a search is submitted by adding the user's search @@ -155,7 +165,7 @@ export const SearchBoxBase = ({ {!hideTitle && ( /* eslint-disable-next-line jsx-a11y/label-has-associated-control */ )} { refinementsReducer, {}, ); + const intl = useIntl(); + const searchFilters = searchFacetFilters || getSearchFacetFilters(intl); const { pathname, search } = useLocation(); const navigate = useNavigate(); @@ -57,7 +60,7 @@ const SearchData = ({ children, searchFacetFilters, trackingName }) => { */ useEffect(() => { const initialQueryParams = searchParamsToObject(new URLSearchParams(search)); - const activeFacetAttributes = searchFacetFilters.map(filter => filter.attribute); + const activeFacetAttributes = searchFilters.map(filter => filter.attribute); const refinementsToSet = getRefinementsToSet(initialQueryParams, activeFacetAttributes); dispatch(setMultipleRefinementsAction(refinementsToSet)); }, []); @@ -78,10 +81,10 @@ const SearchData = ({ children, searchFacetFilters, trackingName }) => { () => ({ refinements, dispatch, - searchFacetFilters, + searchFacetFilters: searchFilters, trackingName, }), - [JSON.stringify(refinements), dispatch, searchFacetFilters, trackingName], + [JSON.stringify(refinements), dispatch, searchFilters, trackingName], ); return ( @@ -90,7 +93,7 @@ const SearchData = ({ children, searchFacetFilters, trackingName }) => { }; SearchData.defaultProps = { - searchFacetFilters: SEARCH_FACET_FILTERS, + searchFacetFilters: null, trackingName: null, }; diff --git a/packages/catalog-search/src/SearchPagination.jsx b/packages/catalog-search/src/SearchPagination.jsx index 28532eb0..b3039a64 100644 --- a/packages/catalog-search/src/SearchPagination.jsx +++ b/packages/catalog-search/src/SearchPagination.jsx @@ -4,6 +4,7 @@ import { connectPagination } from 'react-instantsearch-dom'; import { Pagination, Icon } from '@openedx/paragon'; import { ArrowBackIos, ArrowForwardIos } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { SearchContext } from './SearchContext'; import { setRefinementAction, deleteRefinementAction } from './data/actions'; @@ -13,6 +14,7 @@ export const SearchPaginationBase = ({ maxPagesDisplayed, }) => { const { dispatch } = useContext(SearchContext); + const intl = useIntl(); const icons = useMemo( () => ({ @@ -35,9 +37,21 @@ export const SearchPaginationBase = ({ const buttonLabels = { previous: '', next: '', - page: 'Page', - currentPage: 'Current Page', - pageOfCount: 'of', + page: intl.formatMessage({ + id: 'catalog.search.pagination.page', + defaultMessage: 'Page', + description: 'Label for the page number in the pagination component', + }), + currentPage: intl.formatMessage({ + id: 'catalog.search.pagination.current.page', + defaultMessage: 'Current Page', + description: 'Label for the current page number in the pagination component', + }), + pageOfCount: intl.formatMessage({ + id: 'catalog.search.pagination.page.of.count', + defaultMessage: 'of', + description: 'Label for the page of count in the pagination component', + }), }; const handlePageSelect = (page) => { @@ -50,7 +64,13 @@ export const SearchPaginationBase = ({ return ( 0 && (
- Top-rated courses +
{ preQuerySuggestions.slice(0, MAX_NUM_PRE_QUERY_SUGGESTIONS) @@ -76,7 +81,11 @@ const SearchSuggestions = ({ {courses.length > 0 && (
- Courses +
{ courses.slice(0, MAX_NUM_SUGGESTIONS) @@ -96,7 +105,11 @@ const SearchSuggestions = ({ {programs.length > 0 && (
- Programs +
{ programs.slice(0, MAX_NUM_SUGGESTIONS) @@ -116,7 +129,11 @@ const SearchSuggestions = ({ {execEdCourses.length > 0 && (
- Executive Education +
{ execEdCourses.slice(0, MAX_NUM_SUGGESTIONS) @@ -135,7 +152,11 @@ const SearchSuggestions = ({ )} {!preQuerySuggestions.length && ( )}
diff --git a/packages/catalog-search/src/data/constants.js b/packages/catalog-search/src/data/constants.js index 22917b84..61fff6eb 100644 --- a/packages/catalog-search/src/data/constants.js +++ b/packages/catalog-search/src/data/constants.js @@ -97,8 +97,6 @@ export const NUM_RESULTS_PER_PAGE = 24; export const MAX_NUM_SUGGESTIONS = 3; export const MAX_NUM_PRE_QUERY_SUGGESTIONS = 5; -export const NO_OPTIONS_FOUND = 'No options found.'; - export const STYLE_VARIANTS = { default: 'default', inverse: 'inverse', diff --git a/packages/catalog-search/src/index.js b/packages/catalog-search/src/index.js index 1ecb42fc..3b729b43 100644 --- a/packages/catalog-search/src/index.js +++ b/packages/catalog-search/src/index.js @@ -21,3 +21,7 @@ export { setRefinementAction, clearRefinementsAction, } from './data/actions'; + +export { + getSearchFacetFilters, +} from './utils'; diff --git a/packages/catalog-search/src/tests/ClearCurrentRefinements.test.jsx b/packages/catalog-search/src/tests/ClearCurrentRefinements.test.jsx index 6256380f..8ff4bf1d 100644 --- a/packages/catalog-search/src/tests/ClearCurrentRefinements.test.jsx +++ b/packages/catalog-search/src/tests/ClearCurrentRefinements.test.jsx @@ -3,21 +3,30 @@ import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import ClearCurrentRefinements, { CLEAR_ALL_TEXT } from '../ClearCurrentRefinements'; import * as actions from '../data/actions'; import SearchData from '../SearchContext'; +const ClearCurrentRefinementsWrapper = () => ( + + + + + +); + describe('', () => { test('renders the clear all button', () => { - renderWithRouter(); + renderWithRouter(); expect(screen.queryByText(CLEAR_ALL_TEXT)).toBeInTheDocument(); }); test('dispatches the clear refinements action on click', async () => { const spy = jest.spyOn(actions, 'clearRefinementsAction'); - renderWithRouter(); + renderWithRouter(); // click a specific refinement to remove it fireEvent.click(screen.queryByText(CLEAR_ALL_TEXT)); diff --git a/packages/catalog-search/src/tests/CurrentRefinements.test.jsx b/packages/catalog-search/src/tests/CurrentRefinements.test.jsx index 9d7e7573..d9b528c9 100644 --- a/packages/catalog-search/src/tests/CurrentRefinements.test.jsx +++ b/packages/catalog-search/src/tests/CurrentRefinements.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; @@ -10,7 +9,7 @@ import { AVAILABLILITY, FACET_ATTRIBUTES, } from '../data/tests/constants'; -import SearchData from '../SearchContext'; +import { renderWithSearchContext } from './utils'; const mockedNavigator = jest.fn(); @@ -42,7 +41,7 @@ describe('', () => { ]; test('renders refinements and supports viewing all active refinements', () => { - renderWithRouter(); + renderWithSearchContext(); // assert first 3 active refinements are visible expect(screen.queryByText(SUBJECTS.COMPUTER_SCIENCE)).toBeInTheDocument(); @@ -55,7 +54,7 @@ describe('', () => { }); test('supports viewing all active refinements at once', () => { - renderWithRouter(); + renderWithSearchContext(); // click the "+1" button to show all refinements fireEvent.click(screen.queryByText('+1', { exact: false })); @@ -71,10 +70,8 @@ describe('', () => { }); test('supports removing an active refinement from the url by clicking on it', async () => { - renderWithRouter( - - - , + renderWithSearchContext( + , ); // click a specific refinement to remove it diff --git a/packages/catalog-search/src/tests/FacetListBase.test.jsx b/packages/catalog-search/src/tests/FacetListBase.test.jsx index a02ad95a..d3c33af0 100644 --- a/packages/catalog-search/src/tests/FacetListBase.test.jsx +++ b/packages/catalog-search/src/tests/FacetListBase.test.jsx @@ -1,14 +1,13 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { act, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { FREE_ALL_TITLE } from '../SearchFilters'; import FacetListBase from '../FacetListBase'; import { FACET_ATTRIBUTES, SUBJECTS } from '../data/tests/constants'; -import { NO_OPTIONS_FOUND, SHOW_ALL_NAME } from '../data/constants'; -import SearchData from '../SearchContext'; +import { SHOW_ALL_NAME } from '../data/constants'; +import { renderWithSearchContext } from './utils'; const mockedNavigator = jest.fn(); @@ -68,7 +67,7 @@ describe('', () => { }); test('renders with no options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert facet title exists expect(screen.queryByText(FREE_ALL_TITLE)).toBeInTheDocument(); @@ -77,14 +76,14 @@ describe('', () => { await act(async () => { fireEvent.click(screen.queryByText(FREE_ALL_TITLE)); }); - expect(screen.queryByText(NO_OPTIONS_FOUND)).toBeInTheDocument(); + expect(screen.queryByText('No options found.')).toBeInTheDocument(); }); test('renders with options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert the "no options" message does not show - expect(screen.queryByText(NO_OPTIONS_FOUND)).not.toBeInTheDocument(); + expect(screen.queryByText('No options found.')).not.toBeInTheDocument(); // assert the refinements appear with appropriate counts await act(async () => { @@ -94,17 +93,17 @@ describe('', () => { expect(screen.queryByText(NOT_FREE_LABEL)).toBeInTheDocument(); }); test('does not render if noDisplay is set to True', () => { - renderWithRouter(); + renderWithSearchContext(); expect(screen.queryByText(propsWithItems.title)).not.toBeInTheDocument(); }); test('renders with options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert the "no options" message does not show await act(async () => { fireEvent.click(screen.queryByText(FREE_ALL_TITLE)); }); - expect(screen.queryByText(NO_OPTIONS_FOUND)).not.toBeInTheDocument(); + expect(screen.queryByText('No options found.')).not.toBeInTheDocument(); // assert the refinements appear with appropriate styles expect(screen.queryByText(FREE_LABEL)).toBeInTheDocument(); @@ -115,12 +114,10 @@ describe('', () => { }); test('supports clicking on a refinement', async () => { - renderWithRouter( - - - , + renderWithSearchContext( + , ); // assert the refinements appear @@ -143,12 +140,10 @@ describe('', () => { }; useLocation.mockReturnValue(mockedLocation); - renderWithRouter( - - - , + renderWithSearchContext( + , ); // assert the refinements appear @@ -165,14 +160,12 @@ describe('', () => { }); test('renders a typeahead dropdown', async () => { - const { container } = renderWithRouter(( - - - + const { container } = renderWithSearchContext(( + )); // assert the "no options" message does not show - expect(screen.queryByText(NO_OPTIONS_FOUND)).not.toBeInTheDocument(); + expect(screen.queryByText('No options found.')).not.toBeInTheDocument(); // open the typeahead dropdown menu await act(async () => { @@ -186,7 +179,7 @@ describe('', () => { }); test('typeahead dropdown calls searchForItems with correct arguments', async () => { - renderWithRouter(); + renderWithSearchContext(); // open the typeahead dropdown menu await act(async () => { diff --git a/packages/catalog-search/src/tests/FacetListRefinement.test.jsx b/packages/catalog-search/src/tests/FacetListRefinement.test.jsx index a2b47f63..cb94d73d 100644 --- a/packages/catalog-search/src/tests/FacetListRefinement.test.jsx +++ b/packages/catalog-search/src/tests/FacetListRefinement.test.jsx @@ -1,13 +1,11 @@ import React from 'react'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { act, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { FacetListRefinementBase } from '../FacetListRefinement'; -import SearchData from '../SearchContext'; import { FACET_ATTRIBUTES, SUBJECTS } from '../data/tests/constants'; -import { NO_OPTIONS_FOUND } from '../data/constants'; +import { renderWithSearchContext } from './utils'; const mockedNavigator = jest.fn(); @@ -67,7 +65,7 @@ describe('', () => { }); test('renders with no options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert facet title exists expect(screen.queryByText(FACET_ATTRIBUTES.SUBJECTS)).toBeInTheDocument(); @@ -76,14 +74,14 @@ describe('', () => { await act(async () => { fireEvent.click(screen.queryByText(FACET_ATTRIBUTES.SUBJECTS)); }); - expect(screen.queryByText(NO_OPTIONS_FOUND)).toBeInTheDocument(); + expect(screen.queryByText('No options found.')).toBeInTheDocument(); }); test('renders with options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert the "no options" message does not show - expect(screen.queryByText(NO_OPTIONS_FOUND)).not.toBeInTheDocument(); + expect(screen.queryByText('No options found.')).not.toBeInTheDocument(); // assert the refinements appear with appropriate counts await act(async () => { @@ -97,13 +95,13 @@ describe('', () => { }); test('renders with options', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert the "no options" message does not show await act(async () => { fireEvent.click(screen.queryByText(FACET_ATTRIBUTES.SUBJECTS)); }); - expect(screen.queryByText(NO_OPTIONS_FOUND)).not.toBeInTheDocument(); + expect(screen.queryByText('No options found.')).not.toBeInTheDocument(); // assert the refinements appear with appropriate counts expect(screen.queryByText(SUBJECTS.COMPUTER_SCIENCE)).toBeInTheDocument(); @@ -116,7 +114,7 @@ describe('', () => { }); test('supports clicking on a refinement', async () => { - renderWithRouter(); + renderWithSearchContext(); // assert the refinements appear await act(async () => { @@ -134,13 +132,11 @@ describe('', () => { }); test('clears pagination when clicking on a refinement', async () => { - renderWithRouter( - - - , + renderWithSearchContext( + , ); // assert the refinements appear diff --git a/packages/catalog-search/src/tests/MobileFilterMenu.test.jsx b/packages/catalog-search/src/tests/MobileFilterMenu.test.jsx index 6f430f8f..98c87243 100644 --- a/packages/catalog-search/src/tests/MobileFilterMenu.test.jsx +++ b/packages/catalog-search/src/tests/MobileFilterMenu.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { MobileFilterMenuBase } from '../MobileFilterMenu'; import SearchData from '../SearchContext'; @@ -10,11 +11,13 @@ import { SUBJECTS, AVAILABLILITY, FACET_ATTRIBUTES } from '../data/tests/constan // eslint-disable-next-line react/prop-types const MobileFilterMenuWrapper = ({ items }) => ( - - - - - + + + + + + + ); describe('', () => { diff --git a/packages/catalog-search/src/tests/SearchBox.test.jsx b/packages/catalog-search/src/tests/SearchBox.test.jsx index 8121de4b..07380fa0 100644 --- a/packages/catalog-search/src/tests/SearchBox.test.jsx +++ b/packages/catalog-search/src/tests/SearchBox.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { screen, waitFor } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; @@ -155,10 +155,14 @@ describe('', () => { ); // fill in search input and submit the search - userEvent.type(screen.getByRole('searchbox'), TEST_QUERY); + await act(async () => { + userEvent.type(screen.getByRole('searchbox'), TEST_QUERY); + }); await waitFor(() => expect(screen.queryByTestId('suggestions')).not.toBeNull()); await waitFor(() => expect(screen.getByText('test-title')).toBeInTheDocument()); - userEvent.click(screen.getByText('test-title')); + await act(async () => { + userEvent.click(screen.getByText('test-title')); + }); expect(optimizelySuggestionClickHandler).toHaveBeenCalled(); expect(suggestionSubmitOverride).toHaveBeenCalledWith( { learning_type: 'course', _highlightResult: { title: { value: 'test-title' } } }, diff --git a/packages/catalog-search/src/tests/SearchPagination.test.jsx b/packages/catalog-search/src/tests/SearchPagination.test.jsx index 2e023855..f250558c 100644 --- a/packages/catalog-search/src/tests/SearchPagination.test.jsx +++ b/packages/catalog-search/src/tests/SearchPagination.test.jsx @@ -1,11 +1,10 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import { screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { SearchPaginationBase } from '../SearchPagination'; -import SearchData from '../SearchContext'; +import { renderWithSearchContext } from './utils'; const mockedNavigator = jest.fn(); @@ -22,7 +21,7 @@ describe('', () => { }); test('updates url when navigating right', () => { - renderWithRouter(); + renderWithSearchContext(); // assert no initial page query parameter expect(window.location.search).toEqual(''); @@ -38,10 +37,8 @@ describe('', () => { }; useLocation.mockReturnValue(mockedLocation); - renderWithRouter( - - - , + renderWithSearchContext( + , ); // assert SearchData does not modify the page expect(mockedNavigator.mock.calls[0][0]).toEqual({ pathname: '/', search: 'page=2' }); @@ -57,10 +54,8 @@ describe('', () => { }; useLocation.mockReturnValue(mockedLocation); - renderWithRouter( - - - , + renderWithSearchContext( + , ); // assert SearchData adds showAll diff --git a/packages/catalog-search/src/tests/SearchSuggestions.test.jsx b/packages/catalog-search/src/tests/SearchSuggestions.test.jsx index a422c036..804cec4a 100644 --- a/packages/catalog-search/src/tests/SearchSuggestions.test.jsx +++ b/packages/catalog-search/src/tests/SearchSuggestions.test.jsx @@ -1,9 +1,9 @@ -import { renderWithRouter } from '@edx/frontend-enterprise-utils'; import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import SearchSuggestions from '../SearchSuggestions'; import { COURSE_TYPE_EXECUTIVE_EDUCATION } from '../data/constants'; +import { renderWithIntlProvider } from './utils'; const fakeSuggestionsData = { nbHits: 2, @@ -66,7 +66,7 @@ const optimizelySuggestionClickHandler = jest.fn(); describe('', () => { test('renders all data', () => { - renderWithRouter(', () => { expect(screen.getByText('View all results')).not.toBeNull(); }); test('renders only prequery suggestions if isPreQueryEnabled is true', () => { - renderWithRouter(', () => { expect(screen.queryByText('Programs')).toBeNull(); }); test('renders no errors when no authoring orgs found for programs data', () => { - renderWithRouter(', () => { }); test('calls click handler on view all results', () => { - renderWithRouter(', () => { }); test('redirects to correct page on course click', () => { - const { container } = renderWithRouter(', () => { }); test('redirects to correct page on program click', () => { - const { container } = renderWithRouter(', () => { expect(optimizelySuggestionClickHandler).toHaveBeenCalled(); }); test('properly handles exec ed content', () => { - const { container } = renderWithRouter(', () => { expect(optimizelySuggestionClickHandler).toHaveBeenCalled(); }); test('does not display containers it does not have results for', () => { - renderWithRouter( renderWithRouter( - - {children} - , + + + {children} + + , ); export const renderWithSearchContextAndTracking = (children, trackingName) => renderWithRouter( - + + + {children} + + , +); + +export const renderWithIntlProvider = (children) => renderWithRouter( + {children} - , + , ); diff --git a/packages/catalog-search/src/utils.js b/packages/catalog-search/src/utils.js new file mode 100644 index 00000000..afcff3a1 --- /dev/null +++ b/packages/catalog-search/src/utils.js @@ -0,0 +1,169 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; +import { features } from './config'; + +const messages = defineMessages({ + skillsTitle: { + id: 'search.facetFilters.skills.title', + defaultMessage: 'Skills', + description: 'Title for the skills facet filter', + }, + skillsTypeaheadPlaceholder: { + id: 'search.facetFilters.skills.typeahead.placeholder', + defaultMessage: 'Find a skill...', + description: 'Placeholder for the skills typeahead input', + }, + skillsTypeaheadAriaLabel: { + id: 'search.facetFilters.skills.typeahead.aria.label', + defaultMessage: 'Type to find a skill', + description: 'Aria label for the skills typeahead input', + }, + subjectsTitle: { + id: 'search.facetFilters.subjects.title', + defaultMessage: 'Subject', + description: 'Title for the subjects facet filter', + }, + subjectsTypeaheadPlaceholder: { + id: 'search.facetFilters.subjects.typeahead.placeholder', + defaultMessage: 'Find a subject...', + description: 'Placeholder for the subjects typeahead input', + }, + subjectsTypeaheadAriaLabel: { + id: 'search.facetFilters.subjects.typeahead.aria.label', + defaultMessage: 'Type to find a subject', + description: 'Aria label for the subjects typeahead input', + }, + partnersTitle: { + id: 'search.facetFilters.partners.title', + defaultMessage: 'Partner', + description: 'Title for the partners facet filter', + }, + partnersTypeaheadPlaceholder: { + id: 'search.facetFilters.partners.typeahead.placeholder', + defaultMessage: 'Find a partner...', + description: 'Placeholder for the partners typeahead input', + }, + partnersTypeaheadAriaLabel: { + id: 'search.facetFilters.partners.typeahead.aria.label', + defaultMessage: 'Type to find a partner', + description: 'Aria label for the partners typeahead input', + }, + programsTitle: { + id: 'search.facetFilters.programs.title', + defaultMessage: 'Program', + description: 'Title for the programs facet filter', + }, + programsTypeaheadPlaceholder: { + id: 'search.facetFilters.programs.typeahead.placeholder', + defaultMessage: 'Find a program...', + description: 'Placeholder for the programs typeahead input', + }, + programsTypeaheadAriaLabel: { + id: 'search.facetFilters.programs.typeahead.aria.label', + defaultMessage: 'Type to find a program', + description: 'Aria label for the programs typeahead input', + }, + levelTitle: { + id: 'search.facetFilters.level.title', + defaultMessage: 'Level', + description: 'Title for the level facet filter', + }, + availabilityTitle: { + id: 'search.facetFilters.availability.title', + defaultMessage: 'Availability', + description: 'Title for the availability facet filter', + }, + languageTitle: { + id: 'search.facetFilters.language.title', + defaultMessage: 'Language', + description: 'Title for the language facet filter', + }, + learningTypeTitle: { + id: 'search.facetFilters.learningType.title', + defaultMessage: 'Learning Type', + description: 'Title for the learning type facet filter', + }, + subtitleTitle: { + id: 'search.facetFilters.subtitle.title', + defaultMessage: 'Subtitle', + description: 'Title for the subtitle facet filter', + }, +}); + +// eslint-disable-next-line import/prefer-default-export +export function getSearchFacetFilters(intl) { + const searchFacetFilters = [ + { + attribute: 'skill_names', + title: intl.formatMessage(messages.skillsTitle), + typeaheadOptions: { + placeholder: intl.formatMessage(messages.skillsTypeaheadPlaceholder), + ariaLabel: intl.formatMessage(messages.skillsTypeaheadAriaLabel), + minLength: 3, + }, + }, + { + attribute: 'subjects', + title: intl.formatMessage(messages.subjectsTitle), + typeaheadOptions: { + placeholder: intl.formatMessage(messages.subjectsTypeaheadPlaceholder), + ariaLabel: intl.formatMessage(messages.subjectsTypeaheadAriaLabel), + minLength: 3, + }, + }, + { + attribute: 'partners.name', + title: intl.formatMessage(messages.partnersTitle), + isSortedAlphabetical: true, + typeaheadOptions: { + placeholder: intl.formatMessage(messages.partnersTypeaheadPlaceholder), + ariaLabel: intl.formatMessage(messages.partnersTypeaheadAriaLabel), + minLength: 3, + }, + }, + { + attribute: (features.PROGRAM_TITLES_FACET ? 'program_titles' : 'programs'), + title: intl.formatMessage(messages.programsTitle), + isSortedAlphabetical: true, + typeaheadOptions: { + placeholder: intl.formatMessage(messages.programsTypeaheadPlaceholder), + ariaLabel: intl.formatMessage(messages.programsTypeaheadAriaLabel), + minLength: 3, + }, + }, + { + attribute: 'level_type', + title: intl.formatMessage(messages.levelTitle), + }, + { + attribute: 'availability', + title: intl.formatMessage(messages.availabilityTitle), + }, + ]; + + if (features.LANGUAGE_FACET) { + searchFacetFilters.push({ + attribute: 'language', + title: intl.formatMessage(messages.languageTitle), + isSortedAlphabetical: true, + }); + } + + if (features.LEARNING_TYPE_FACET) { + searchFacetFilters.push({ + attribute: 'content_type', + title: intl.formatMessage(messages.learningTypeTitle), + // algolia won't filter if not passed through connectRefinementsList, + // if we add without hiding, there will be a new facet created with courses and programs dropdown items only. + noDisplay: true, + }); + } + + if (features.SUBTITLE_FACET) { + searchFacetFilters.push({ + attribute: 'transcript_languages', + title: intl.formatMessage(messages.subtitleTitle), + isSortedAlphabetical: true, + }); + } + return searchFacetFilters; +}