From d9c7096fd792d4eaae46f362adbd67e27d326564 Mon Sep 17 00:00:00 2001 From: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:05:18 -0400 Subject: [PATCH] Revert "feat: course recommendations for Skills Builder" --- .../CareerInterestSelect.jsx | 29 ++- .../select-preferences/GoalSelect.jsx | 42 +++-- .../JobTitleInstantSearch.jsx | 10 +- .../select-preferences/JobTitleSelect.jsx | 25 ++- .../select-preferences/messages.js | 9 +- .../test/SelectPreferences.test.jsx | 109 ----------- .../view-results/CarouselStack.jsx | 52 ------ .../view-results/RecommendationCard.jsx | 54 ------ .../RelatedSkillsSelectableBoxSet.jsx | 2 +- .../view-results/ViewResults.jsx | 56 +----- .../view-results/data/constants.js | 11 -- .../view-results/messages.js | 10 - .../view-results/test/ViewResults.test.jsx | 79 -------- .../test/SkillsBuilder.test.jsx | 174 +++++++++++++++++- .../test/__mocks__/jobSkills.mockData.js | 43 +---- .../test/setupSkillsBuilder.jsx | 49 ----- src/skills-builder/utils/search.jsx | 2 +- .../utils/tests/search.test.jsx | 2 +- 18 files changed, 251 insertions(+), 507 deletions(-) delete mode 100644 src/skills-builder/skills-builder-modal/select-preferences/test/SelectPreferences.test.jsx delete mode 100644 src/skills-builder/skills-builder-modal/view-results/CarouselStack.jsx delete mode 100644 src/skills-builder/skills-builder-modal/view-results/RecommendationCard.jsx delete mode 100644 src/skills-builder/skills-builder-modal/view-results/data/constants.js delete mode 100644 src/skills-builder/skills-builder-modal/view-results/test/ViewResults.test.jsx delete mode 100644 src/skills-builder/test/setupSkillsBuilder.jsx diff --git a/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx index aedbad5..74fbd1f 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx @@ -1,9 +1,7 @@ import React, { useContext } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Stack, Row, Col, Form, -} from '@edx/paragon'; +import { Stack, Row, Col } from '@edx/paragon'; import { InstantSearch } from 'react-instantsearch-hooks-web'; import JobTitleInstantSearch from './JobTitleInstantSearch'; import CareerInterestCard from './CareerInterestCard'; @@ -18,24 +16,25 @@ const CareerInterestSelect = () => { const { searchClient } = algolia; const handleCareerInterestSelect = (value) => { - if (!careerInterests.includes(value) && careerInterests.length < 3) { + // By checking for a value to exist, we avoid adding a null value to the careerInterests array + // The 'onSelected' function is fired during every 'onChange' event + // A null value was being passed to this function whenever the search box received input, resulting in empty cards + if (value && careerInterests.length < 3) { dispatch(addCareerInterest(value)); } }; return ( - -

- {formatMessage(messages.careerInterestPrompt)} -

- - - -
+

+ {formatMessage(messages.careerInterestPrompt)} +

+ + + {careerInterests.map((interest, index) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx b/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx index b5e8cfb..77f5fb0 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx @@ -1,6 +1,7 @@ import React, { useContext } from 'react'; import { Form, + Stack, } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { setGoal } from '../../data/actions'; @@ -13,26 +14,27 @@ const GoalDropdown = () => { const { currentGoal } = state; return ( - - -

- {formatMessage(messages.learningGoalPrompt)} -

-
- dispatch(setGoal(e.target.value))} - data-testid="goal-select-dropdown" - > - - - - - - - -
+ +

+ {formatMessage(messages.learningGoalPrompt)} +

+ + dispatch(setGoal(e.target.value))} + data-testid="goal-select-dropdown" + > + + + + + + + + +
+ ); }; diff --git a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx index 138c61e..5c29d7b 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx @@ -24,11 +24,12 @@ const JobTitleInstantSearch = (props) => { value={jobInput} onChange={handleAutosuggestChange} name="job-title-suggest" + onSelected={props.onSelected} autoComplete="off" - {...props} + placeholder={props.placeholder} > {hits.map(job => ( - + {job.name} ))} @@ -36,8 +37,13 @@ const JobTitleInstantSearch = (props) => { ); }; +JobTitleInstantSearch.defaultProps = { + placeholder: '', +}; + JobTitleInstantSearch.propTypes = { onSelected: PropTypes.func.isRequired, + placeholder: PropTypes.string, }; export default JobTitleInstantSearch; diff --git a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleSelect.jsx b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleSelect.jsx index ea20aa3..35552ff 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleSelect.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleSelect.jsx @@ -12,8 +12,9 @@ import messages from './messages'; const JobTitleSelect = () => { const { formatMessage } = useIntl(); - const { dispatch, algolia } = useContext(SkillsBuilderContext); + const { state, dispatch, algolia } = useContext(SkillsBuilderContext); const { searchClient } = algolia; + const { currentJobTitle } = state; const handleCurrentJobTitleSelect = (value) => { dispatch(setCurrentJobTitle(value)); @@ -24,18 +25,16 @@ const JobTitleSelect = () => { const handleCheckboxChange = (e) => dispatch(setCurrentJobTitle(e.target.value)); return ( - - -

- {formatMessage(messages.jobTitlePrompt)} -

- - - -
+ +

+ {formatMessage(messages.jobTitlePrompt)} +

+ + + { - beforeAll(() => { - mergeConfig({ - ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name', - }); - }); - beforeEach(() => cleanup()); - - describe('render behavior', () => { - it('should render the second prompt if a goal is selected', () => { - render( - SkillsBuilderWrapperWithContext( - { - ...contextValue, - state: { - ...contextValue.state, - currentGoal: 'I want to start my career', - }, - }, - ), - ); - const expectedGoal = { - payload: 'I want to advance my career', - type: 'SET_GOAL', - }; - const expectedJobTitle = { - payload: 'Student', - type: 'SET_CURRENT_JOB_TITLE', - }; - - const goalSelect = screen.getByTestId('goal-select-dropdown'); - fireEvent.change(goalSelect, { target: { value: 'I want to advance my career' } }); - - const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' }); - fireEvent.click(checkbox); - - expect(screen.getByText('Next, search and select your current job title')).toBeTruthy(); - expect(dispatchMock).toHaveBeenCalledWith(expectedGoal); - expect(dispatchMock).toHaveBeenCalledWith(expectedJobTitle); - }); - - it('should render the third prompt if a current job title is selected', () => { - render( - SkillsBuilderWrapperWithContext( - { - ...contextValue, - state: { - ...contextValue.state, - currentGoal: 'I want to start my career', - currentJobTitle: 'Goblin Guide', - }, - }, - ), - ); - expect(screen.getByText('What careers are you interested in?')).toBeTruthy(); - }); - - it('should render a for each career interest', () => { - render( - SkillsBuilderWrapperWithContext( - { - ...contextValue, - state: { - ...contextValue.state, - currentGoal: 'I want to start my career', - currentJobTitle: 'Goblin Lackey', - careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'], - }, - }, - ), - ); - expect(screen.getByText('Prospector')).toBeTruthy(); - expect(screen.getByText('Mirror Breaker')).toBeTruthy(); - expect(screen.getByText('Bombardment')).toBeTruthy(); - }); - }); - - describe('controlled behavior', () => { - it('should remove a when the corresponding close button is selected', () => { - render( - SkillsBuilderWrapperWithContext( - { - ...contextValue, - state: { - ...contextValue.state, - currentGoal: 'I want to start my career', - currentJobTitle: 'Goblin Lackey', - careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'], - }, - }, - ), - ); - - const expected = { - payload: 'Prospector', - type: 'REMOVE_CAREER_INTEREST', - }; - - fireEvent.click(screen.getByLabelText('Remove career interest: Prospector')); - expect(dispatchMock).toHaveBeenCalledWith(expected); - }); - }); -}); diff --git a/src/skills-builder/skills-builder-modal/view-results/CarouselStack.jsx b/src/skills-builder/skills-builder-modal/view-results/CarouselStack.jsx deleted file mode 100644 index 78a6ced..0000000 --- a/src/skills-builder/skills-builder-modal/view-results/CarouselStack.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { CardCarousel } from '@edx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import RecommendationCard from './RecommendationCard'; -import messages from './messages'; - -const CarouselStack = ({ selectedRecommendations }) => { - const { formatMessage } = useIntl(); - const { name: jobName, recommendations } = selectedRecommendations; - const productTypeNames = Object.keys(recommendations); - - const normalizeProductTypeName = (productType) => { - // If the productType is more than one word (i.e. boot_camp) - if (productType.includes('_')) { - // split to remove underscore and return an array of strings (i.e. ['boot', 'camp']) - const splitStrings = productType.split('_'); - - // map through the array and normalize each string (i.e. ['Boot', 'Camp']) - const normalizeStrings = splitStrings.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()); - - // return the array as a string joined by white spaces (i.e. Boot Camp) - return normalizeStrings.join(' '); - } - // Otherwise, return a normalized string - const normalizeString = productType[0].toUpperCase() + productType.slice(1).toLowerCase(); - return normalizeString; - }; - - const renderCarouselTitle = (productType) => ( -

- {formatMessage(messages.productRecommendationsHeaderText, { - productType: normalizeProductTypeName(productType), - jobName, - })} -

- ); - - return ( - productTypeNames.map(productType => ( - - {recommendations[productType].map(rec => ( - - ))} - - ))); -}; - -export default CarouselStack; diff --git a/src/skills-builder/skills-builder-modal/view-results/RecommendationCard.jsx b/src/skills-builder/skills-builder-modal/view-results/RecommendationCard.jsx deleted file mode 100644 index b67117c..0000000 --- a/src/skills-builder/skills-builder-modal/view-results/RecommendationCard.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { Card, Chip, Hyperlink } from '@edx/paragon'; -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import cardImageCapFallbackSrc from '@edx/brand/paragon/images/card-imagecap-fallback.png'; - -const RecommendationCard = ({ rec }) => { - const { - card_image_url: cardImageUrl, - marketing_url: marketingUrl, - owners, - partner, - title, - } = rec; - - const { logoImageUrl } = owners[0]; - - return ( - - - - - - {partner.map((orgName, index) => ( - // eslint-disable-next-line react/no-array-index-key - - {orgName} - - ))} - - - - ); -}; - -RecommendationCard.propTypes = { - rec: PropTypes.shape({ - title: PropTypes.string, - card_image_url: PropTypes.string, - marketing_url: PropTypes.string, - partner: PropTypes.arrayOf(PropTypes.string), - owners: PropTypes.arrayOf(PropTypes.shape({ - key: PropTypes.string, - logoImageUrl: PropTypes.string, - })), - }).isRequired, -}; - -export default RecommendationCard; diff --git a/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx b/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx index d10ac5e..1da521c 100644 --- a/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx +++ b/src/skills-builder/skills-builder-modal/view-results/RelatedSkillsSelectableBoxSet.jsx @@ -14,7 +14,7 @@ const RelatedSkillsSelectableBoxSet = ({ jobSkillsList, selectedJobTitle, onChan const topFiveSkills = skills.sort((a, b) => b.significance - a.significance).slice(0, 5); return ( topFiveSkills.map(skill => ( - + {skill.name} )) diff --git a/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx b/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx index ff0920b..3879fda 100644 --- a/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx +++ b/src/skills-builder/skills-builder-modal/view-results/ViewResults.jsx @@ -3,13 +3,11 @@ import { Stack, Row, Alert, Spinner, } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { CheckCircle, ErrorOutline } from '@edx/paragon/icons'; +import { CheckCircle } from '@edx/paragon/icons'; import { SkillsBuilderContext } from '../../skills-builder-context'; import RelatedSkillsSelectableBoxSet from './RelatedSkillsSelectableBoxSet'; import { searchJobs, getProductRecommendations } from '../../utils/search'; import messages from './messages'; -import { productTypes } from './data/constants'; -import CarouselStack from './CarouselStack'; const ViewResults = () => { const { formatMessage } = useIntl(); @@ -19,13 +17,12 @@ const ViewResults = () => { const [selectedJobTitle, setSelectedJobTitle] = useState(''); const [jobSkillsList, setJobSkillsList] = useState([]); - const [productRecommendations, setProductRecommendations] = useState([]); - const [selectedRecommendations, setSelectedRecommendations] = useState({}); + // eslint-disable-next-line no-unused-vars + const [courseRecommendations, setCourseRecommendations] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [fetchError, setFetchError] = useState(false); useEffect(() => { - const getRecommendations = async () => { + const getJobs = async () => { // fetch list of jobs with related skills const jobInfo = await searchJobs(jobSearchIndex, careerInterests); @@ -33,56 +30,25 @@ const ViewResults = () => { const results = await Promise.all(jobInfo.map(async (job) => { const formattedSkills = job.skills.map(skill => skill.name); - // create a data object for each job + const recommendations = await getProductRecommendations(productSearchIndex, 'course', formattedSkills); + const data = { id: job.id, name: job.name, - recommendations: {}, + recommendations, }; - // get recommendations for each product type based on the skills for the current job - await Promise.all(productTypes.map(async (productType) => { - const response = await getProductRecommendations(productSearchIndex, productType, formattedSkills); - - // replace all white spaces with an underscore - const formattedProductType = productType.replace(' ', '_'); - - // add a new key to the recommendations object and set the value to the response - data.recommendations[formattedProductType] = response; - })); - return data; })); setJobSkillsList(jobInfo); setSelectedJobTitle(jobInfo[0].name); - setProductRecommendations(results); + setCourseRecommendations(results); setIsLoading(false); }; - getRecommendations() - .catch(() => { - setFetchError(true); - setIsLoading(false); - }); + getJobs(); }, [careerInterests, jobSearchIndex, productSearchIndex]); - useEffect(() => { - setSelectedRecommendations(productRecommendations.find(rec => rec.name === selectedJobTitle)); - }, [productRecommendations, selectedJobTitle]); - - if (fetchError) { - return ( - - - {formatMessage(messages.matchesNotFoundDangerAlert)} - - - ); - } - return ( isLoading ? ( @@ -93,7 +59,7 @@ const ViewResults = () => { /> ) : ( - + { selectedJobTitle={selectedJobTitle} onChange={(e) => setSelectedJobTitle(e.target.value)} /> - - ) ); diff --git a/src/skills-builder/skills-builder-modal/view-results/data/constants.js b/src/skills-builder/skills-builder-modal/view-results/data/constants.js deleted file mode 100644 index c09761c..0000000 --- a/src/skills-builder/skills-builder-modal/view-results/data/constants.js +++ /dev/null @@ -1,11 +0,0 @@ -const COURSE = 'course'; - -/* The below strings can be used to demonstrate how we are able to retrieve recommendations for other product types -const BOOT_CAMP = 'boot camp'; -const EXECUTIVE_EDUCATION = 'executive education'; -*/ - -// eslint-disable-next-line import/prefer-default-export -export const productTypes = [ - COURSE, -]; diff --git a/src/skills-builder/skills-builder-modal/view-results/messages.js b/src/skills-builder/skills-builder-modal/view-results/messages.js index 91660d6..0fc53ce 100644 --- a/src/skills-builder/skills-builder-modal/view-results/messages.js +++ b/src/skills-builder/skills-builder-modal/view-results/messages.js @@ -6,11 +6,6 @@ const messages = defineMessages({ defaultMessage: 'We found skills and courses that match your preferences!', description: 'Success alert message to display when recommendations are presented to the learner.', }, - matchesNotFoundDangerAlert: { - id: 'matches.not.found.danger.alert', - defaultMessage: 'We were not able to retrieve recommendations at this time. Please try again later.', - description: 'Danger alert message to display when the component fails to get recommendations.', - }, relatedSkillsHeading: { id: 'related.skills.heading', defaultMessage: 'Related Skills', @@ -21,11 +16,6 @@ const messages = defineMessages({ defaultMessage: 'Related skills:', description: 'Label text for a selectable box that displays related skills for a corresponding selected job title.', }, - productRecommendationsHeaderText: { - id: 'product.recommendations.header.text', - defaultMessage: '{productType} recommendations for {jobName}', - description: 'Header text for a carousel of product recommendations.', - }, }); export default messages; diff --git a/src/skills-builder/skills-builder-modal/view-results/test/ViewResults.test.jsx b/src/skills-builder/skills-builder-modal/view-results/test/ViewResults.test.jsx deleted file mode 100644 index 8907c08..0000000 --- a/src/skills-builder/skills-builder-modal/view-results/test/ViewResults.test.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - screen, render, cleanup, fireEvent, act, -} from '@testing-library/react'; -import { mergeConfig } from '@edx/frontend-platform'; -import { SkillsBuilderWrapperWithContext, contextValue } from '../../../test/setupSkillsBuilder'; -import { getProductRecommendations } from '../../../utils/search'; - -const renderSkillsBuilderWrapper = ( - value = { - ...contextValue, - state: { - ...contextValue.state, - currentGoal: 'I want to start my career', - currentJobTitle: 'Goblin Lackey', - careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'], - }, - }, -) => { - render(SkillsBuilderWrapperWithContext(value)); -}; - -describe('view-results', () => { - beforeAll(() => { - mergeConfig({ - ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name', - }); - }); - - describe('user interface', () => { - beforeEach(async () => { - cleanup(); - // Render the form filled out - renderSkillsBuilderWrapper(); - // Click the next button to trigger "fetching" the data - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Next Step' })); - }); - }); - - it('should render a for each career interest the learner has submitted', async () => { - expect(screen.getByText('Prospector')).toBeTruthy(); - expect(screen.getByText('Mirror Breaker')).toBeTruthy(); - - const chipComponents = document.querySelectorAll('.pgn__chip'); - expect(chipComponents[0].textContent).toEqual('finding shiny things'); - expect(chipComponents[1].textContent).toEqual('mining'); - }); - - it('renders a carousel of components', async () => { - expect(screen.getByText('Course recommendations for Prospector')).toBeTruthy(); - }); - - it('changes the recommendations based on the selected job title', () => { - fireEvent.click(screen.getByRole('radio', { name: 'Mirror Breaker' })); - expect(screen.getByText('Course recommendations for Mirror Breaker')).toBeTruthy(); - }); - }); - - describe('fetch recommendations', () => { - beforeEach(() => { - cleanup(); - // Render the form filled out - renderSkillsBuilderWrapper(); - }); - - it('renders an alert if an error is thrown while fetching', async () => { - getProductRecommendations.mockImplementationOnce(() => { - throw new Error(); - }); - - // Click the next button to trigger "fetching" the data - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Next Step' })); - }); - - expect(screen.getByText('We were not able to retrieve recommendations at this time. Please try again later.')).toBeTruthy(); - }); - }); -}); diff --git a/src/skills-builder/test/SkillsBuilder.test.jsx b/src/skills-builder/test/SkillsBuilder.test.jsx index 6803376..30ddd4f 100644 --- a/src/skills-builder/test/SkillsBuilder.test.jsx +++ b/src/skills-builder/test/SkillsBuilder.test.jsx @@ -1,12 +1,66 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import React from 'react'; import { - screen, render, act, + screen, render, cleanup, fireEvent, act, } from '@testing-library/react'; +import { mergeConfig } from '@edx/frontend-platform'; import { SkillsBuilder } from '..'; -import { SkillsBuilderProvider } from '../skills-builder-context'; +import { SkillsBuilderModal } from '../skills-builder-modal'; +import { SkillsBuilderProvider, SkillsBuilderContext } from '../skills-builder-context'; +import { skillsInitialState } from '../data/reducer'; +import { mockData } from './__mocks__/jobSkills.mockData'; +import { getProductRecommendations, searchJobs, useAlgoliaSearch } from '../utils/search'; + +const dispatchMock = jest.fn(); + +jest.mock('@edx/frontend-platform/logging'); + +jest.mock('react-instantsearch-hooks-web', () => ({ + // eslint-disable-next-line react/prop-types + InstantSearch: ({ children }) => (
{children}
), + useSearchBox: jest.fn(() => ({ refine: jest.fn() })), + useHits: jest.fn(() => ({ hits: mockData.hits })), +})); + +jest.mock('../utils/search', () => ({ + searchJobs: jest.fn(), + getProductRecommendations: jest.fn(), + useAlgoliaSearch: jest.fn(), +})); + +searchJobs.mockReturnValue(mockData.searchJobs); +getProductRecommendations.mockReturnValue(mockData.productRecommendations); +useAlgoliaSearch.mockReturnValue(mockData.useAlgoliaSearch); + +const contextValue = { + state: { + ...skillsInitialState, + }, + dispatch: dispatchMock, + algolia: { + // Without this, tests would fail to destructure `searchClient` in the component + searchClient: {}, + productSearchIndex: {}, + jobSearchIndex: {}, + }, +}; + +const SkillsBuilderWrapperWithContext = (value) => ( + + + + + +); describe('skills-builder', () => { + beforeAll(async () => { + await mergeConfig({ + ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name', + }); + }); + beforeEach(() => cleanup()); + it('should render a Skills Builder modal with a prompt for the user', () => { act(() => { render( @@ -20,4 +74,120 @@ describe('skills-builder', () => { expect(screen.getByText('Skills Builder')).toBeTruthy(); expect(screen.getByText('First, tell us what you want to achieve')).toBeTruthy(); }); + + it('should render the second prompt if a goal is selected', () => { + render( + SkillsBuilderWrapperWithContext( + { + ...contextValue, + state: { + ...contextValue.state, + currentGoal: 'I want to start my career', + }, + }, + ), + ); + const expectedGoal = { + payload: 'I want to advance my career', + type: 'SET_GOAL', + }; + const expectedJobTitle = { + payload: 'Student', + type: 'SET_CURRENT_JOB_TITLE', + }; + + const goalSelect = screen.getByTestId('goal-select-dropdown'); + fireEvent.change(goalSelect, { target: { value: 'I want to advance my career' } }); + + const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' }); + fireEvent.click(checkbox); + + expect(screen.getByText('Next, search and select your current job title')).toBeTruthy(); + expect(dispatchMock).toHaveBeenCalledWith(expectedGoal); + expect(dispatchMock).toHaveBeenCalledWith(expectedJobTitle); + }); + + it('should render the third prompt if a current job title is selected', () => { + render( + SkillsBuilderWrapperWithContext( + { + ...contextValue, + state: { + ...contextValue.state, + currentGoal: 'I want to start my career', + currentJobTitle: 'Goblin Guide', + }, + }, + ), + ); + expect(screen.getByText('What careers are you interested in?')).toBeTruthy(); + }); + + it('should render a for each career interest', () => { + render( + SkillsBuilderWrapperWithContext( + { + ...contextValue, + state: { + ...contextValue.state, + currentGoal: 'I want to start my career', + currentJobTitle: 'Goblin Lackey', + careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'], + }, + }, + ), + ); + expect(screen.getByText('Prospector')).toBeTruthy(); + expect(screen.getByText('Mirror Breaker')).toBeTruthy(); + expect(screen.getByText('Bombardment')).toBeTruthy(); + }); + + it('should remove a when the corresponding close button is selected', () => { + render( + SkillsBuilderWrapperWithContext( + { + ...contextValue, + state: { + ...contextValue.state, + currentGoal: 'I want to start my career', + currentJobTitle: 'Goblin Lackey', + careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'], + }, + }, + ), + ); + + const expected = { + payload: 'Prospector', + type: 'REMOVE_CAREER_INTEREST', + }; + + fireEvent.click(screen.getByLabelText('Remove career interest: Prospector')); + expect(dispatchMock).toHaveBeenCalledWith(expected); + }); + + it('should render a for each career interest the learner has submitted', async () => { + render( + SkillsBuilderWrapperWithContext( + { + ...contextValue, + state: { + ...contextValue.state, + currentGoal: 'I want to start my career', + currentJobTitle: 'Goblin Lackey', + careerInterests: ['Prospector'], + }, + }, + ), + ); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Next Step' })); + }); + + const chipComponents = document.querySelectorAll('.pgn__chip'); + expect(chipComponents[0].textContent).toEqual('finding shiny things'); + expect(chipComponents[1].textContent).toEqual('mining'); + + expect(screen.getByText('Prospector')).toBeTruthy(); + }); }); diff --git a/src/skills-builder/test/__mocks__/jobSkills.mockData.js b/src/skills-builder/test/__mocks__/jobSkills.mockData.js index e11bb5e..5150dec 100644 --- a/src/skills-builder/test/__mocks__/jobSkills.mockData.js +++ b/src/skills-builder/test/__mocks__/jobSkills.mockData.js @@ -14,24 +14,11 @@ export const mockData = { id: 0, name: 'Prospector', skills: [ - { external_id: 0, - name: 'mining', - significance: 50, - }, - { external_id: 1, - name: 'finding shiny things', - significance: 100, - }], - }, - { - id: 1, - name: 'Mirror Breaker', - skills: [ - { external_id: 0, + { id: 0, name: 'mining', significance: 50, }, - { external_id: 1, + { id: 1, name: 'finding shiny things', significance: 100, }], @@ -39,28 +26,14 @@ export const mockData = { ], productRecommendations: [ { - title: 'Mining with the Mons', - uuid: 'thisIsARandomString01', - partner: ['edx'], - card_image_url: 'https://thisIsAUrl.ForAnImage.01.jpeg', - marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.01.com', - owners: [ - { - logoImageUrl: 'https://thisIsAUrl.ForALogoImage.01.jpeg', - } - ] + id: 0, + name: 'Prospector', + recommendations: [{ name: 'Mining with the Mons' }, { name: 'The Art of Warren Upkeep' }], }, { - title: 'The Art of Warren Upkeep', - uuid: 'thisIsARandomString02', - partner: ['edx'], - card_image_url: 'https://thisIsAUrl.ForAnImage.02.jpeg', - marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.02.com', - owners: [ - { - logoImageUrl: 'https://thisIsAUrl.ForALogoImage.02.jpeg', - } - ] + id: 1, + name: 'Mirror Breaker', + recommendations: [{ name: 'Mirror Breaking 101' }], }, ], useAlgoliaSearch: [{}, {}, {}], diff --git a/src/skills-builder/test/setupSkillsBuilder.jsx b/src/skills-builder/test/setupSkillsBuilder.jsx deleted file mode 100644 index fb130af..0000000 --- a/src/skills-builder/test/setupSkillsBuilder.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import React from 'react'; -import { SkillsBuilderModal } from '../skills-builder-modal'; -import { SkillsBuilderContext } from '../skills-builder-context'; -import { skillsInitialState } from '../data/reducer'; -import { mockData } from './__mocks__/jobSkills.mockData'; -import { getProductRecommendations, searchJobs, useAlgoliaSearch } from '../utils/search'; - -jest.mock('@edx/frontend-platform/logging'); - -jest.mock('react-instantsearch-hooks-web', () => ({ - // eslint-disable-next-line react/prop-types - InstantSearch: ({ children }) => (
{children}
), - useSearchBox: jest.fn(() => ({ refine: jest.fn() })), - useHits: jest.fn(() => ({ hits: mockData.hits })), -})); - -jest.mock('../utils/search', () => ({ - searchJobs: jest.fn(), - getProductRecommendations: jest.fn(), - useAlgoliaSearch: jest.fn(), -})); - -searchJobs.mockReturnValue(mockData.searchJobs); -getProductRecommendations.mockReturnValue(mockData.productRecommendations); -useAlgoliaSearch.mockReturnValue(mockData.useAlgoliaSearch); - -export const dispatchMock = jest.fn(); - -export const contextValue = { - state: { - ...skillsInitialState, - }, - dispatch: dispatchMock, - algolia: { - // Without this, tests would fail to destructure `searchClient` in the component - searchClient: {}, - productSearchIndex: {}, - jobSearchIndex: {}, - }, -}; - -export const SkillsBuilderWrapperWithContext = (value = contextValue) => ( - - - - - -); diff --git a/src/skills-builder/utils/search.jsx b/src/skills-builder/utils/search.jsx index ffc9234..0e4d0ac 100644 --- a/src/skills-builder/utils/search.jsx +++ b/src/skills-builder/utils/search.jsx @@ -96,7 +96,7 @@ export const getProductRecommendations = async (productIndex, productType, skill const formattedSkillNames = formatFacetFilterData('skills.skill', skills); try { const { hits } = await productIndex.search('', { - filters: `product: "${productType}"`, + filters: `product:${productType}`, facetFilters: [ formattedSkillNames, ], diff --git a/src/skills-builder/utils/tests/search.test.jsx b/src/skills-builder/utils/tests/search.test.jsx index f879d73..6fa20ca 100644 --- a/src/skills-builder/utils/tests/search.test.jsx +++ b/src/skills-builder/utils/tests/search.test.jsx @@ -55,7 +55,7 @@ describe('Algolias utility function', () => { it('getProductRecommendations() queries Algolia with the expected search parameters', async () => { const expectedSearchParameters = { - filters: 'product: "Course"', + filters: 'product:Course', facetFilters: [ ['skills.skill:Sword Lobbing'], ],