From fa0877dd944df0d62c8715db2503c0f403380500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Tue, 29 Nov 2022 12:49:32 +0000 Subject: [PATCH 1/7] Add hook to allow the search trough the click of a chip --- .../SearchResultsArea/Offer/ChipList.js | 3 +- .../SearchResultsArea/Offer/OfferDetails.js | 5 ++ .../Offer/useChipsFieldSearch.js | 62 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js diff --git a/src/components/HomePage/SearchResultsArea/Offer/ChipList.js b/src/components/HomePage/SearchResultsArea/Offer/ChipList.js index 37b133cf..c83b3ab3 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/ChipList.js +++ b/src/components/HomePage/SearchResultsArea/Offer/ChipList.js @@ -26,7 +26,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ChipList = ({ type, values, loading }) => { +const ChipList = ({ type, values, loading, onChipClick }) => { const classes = useStyles(); if (loading) return ( @@ -44,6 +44,7 @@ const ChipList = ({ type, values, loading }) => { variant={type === "Technologies" ? "outlined" : "default"} size="small" className={classes.chip} + onClick={onChipClick ? () => onChipClick(value) : null} /> )} diff --git a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js index 63f97ebb..a493dbf1 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js +++ b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js @@ -19,6 +19,7 @@ import useSession from "../../../../hooks/useSession"; import useSearchResultsWidgetStyles from "../SearchResultsWidget/searchResultsWidgetStyles"; import { RouterLink } from "../../../../utils"; import { JOB_MAX_DURATION } from "../../../../reducers/searchOffersReducer"; +import { useChipsFieldSearch } from "./useChipsFieldSearch"; const defaultLogo = require("./default_icon.svg"); @@ -53,6 +54,8 @@ const OfferDetails = ({ + "Please contact support for more information." ), [offer, sessionData]); + const { setFields, setTechs } = useChipsFieldSearch(); + const getHiddenOfferMessage = useCallback(() => { if (visibilityState.isDisabled) return getDisabledOfferMessage(); @@ -248,11 +251,13 @@ const OfferDetails = ({ type="Technologies" values={offer?.technologies} loading={loading} + onChipClick={setTechs} /> diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js new file mode 100644 index 00000000..ae7147e6 --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { setFields, setTechs } from "../../../../actions/searchOffersActions"; + +import useSearchParams from "../../SearchArea/useUrlSearchParams"; +import { SearchResultsConstants } from "../SearchResultsWidget/SearchResultsUtils"; +import useOffersSearcher from "../SearchResultsWidget/useOffersSearcher"; + +export const useChipsFieldSearch = () => { + const dispatch = useDispatch(); + const fields = useSelector((state) => state.offerSearch.fields); + const techs = useSelector((state) => state.offerSearch.technologies); + const jobMaxDuration = useSelector((state) => state.offerSearch.jobMaxDuration); + const jobMinDuration = useSelector((state) => state.offerSearch.jobMinDuration); + const jobType = useSelector((state) => state.offerSearch.jobType); + const searchValue = useSelector((state) => state.offerSearch.searchValue); + + const [search, setSearch] = useState(false); + + const { search: searchOffers } = useOffersSearcher({ + value: searchValue, + jobMinDuration, + jobMaxDuration, + jobType, + fields, + technologies: techs, + }); + + const { + setFields: urlSetFields, + setTechs: urlSetTechs, + } = useSearchParams({ + setFields: (value) => dispatch(setFields(value)), + setTechs: (value) => dispatch(setTechs(value)), + }); + + const actualSetFields = useCallback((value) => { + if (!fields.includes(value)) { + urlSetFields([...fields, value]); + setSearch(true); + } + }, [fields, urlSetFields]); + + const actualSetTechs = useCallback((value) => { + if (!techs.includes(value)) { + urlSetTechs([...techs, value]); + setSearch(true); + } + }, [techs, urlSetTechs]); + + useEffect(() => { + if (search) { + searchOffers(SearchResultsConstants.INITIAL_LIMIT); + setSearch(false); + } + }, [searchOffers, fields, techs, search]); + + return { + setFields: actualSetFields, + setTechs: actualSetTechs, + }; +}; From 8084008ec09d4160d40fb6cdd30db05a1a12419c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Wed, 30 Nov 2022 11:45:09 +0000 Subject: [PATCH 2/7] fix redirect from offer page with filters --- src/actions/searchOffersActions.js | 6 +++ .../HomePage/SearchArea/SearchArea.js | 47 ++++++++++++++----- .../HomePage/SearchArea/useUrlSearchParams.js | 34 ++++++++++++++ .../SearchResultsArea/Offer/ChipList.js | 1 + .../SearchResultsArea/Offer/OfferDetails.js | 29 ++++++++++-- .../Offer/useChipsFieldSearch.js | 16 +++++-- src/reducers/searchOffersReducer.js | 5 ++ 7 files changed, 118 insertions(+), 20 deletions(-) diff --git a/src/actions/searchOffersActions.js b/src/actions/searchOffersActions.js index 751cdb0f..4b62d82d 100644 --- a/src/actions/searchOffersActions.js +++ b/src/actions/searchOffersActions.js @@ -6,6 +6,7 @@ export const OfferSearchTypes = Object.freeze({ SET_JOB_TYPE: "SET_JOB_TYPE", SET_JOB_FIELDS: "SET_JOB_FIELDS", SET_JOB_TECHS: "SET_JOB_TECHS", + SET_LOAD_URL_FROM_FILTERS: "SET_LOAD_URL_FROM_FILTERS", SET_OFFERS_SEARCH_RESULT: "SET_OFFERS_SEARCH_RESULT", SET_SEARCH_QUERY_TOKEN: "SET_SEARCH_QUERY_TOKEN", SET_OFFERS_LOADING: "SET_OFFERS_LOADING", @@ -67,6 +68,11 @@ export const setTechs = (technologies) => ({ technologies, }); +export const setLoadUrlFromFilters = (value) => ({ + type: OfferSearchTypes.SET_LOAD_URL_FROM_FILTERS, + value, +}); + export const setShowJobDurationSlider = (filterJobDuration) => ({ type: OfferSearchTypes.SET_JOB_DURATION_TOGGLE, filterJobDuration, diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index db00fee4..1df78824 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -11,6 +11,7 @@ import { setFields, setShowJobDurationSlider, setTechs, + setLoadUrlFromFilters, } from "../../../actions/searchOffersActions"; import { INITIAL_JOB_TYPE, INITIAL_JOB_DURATION } from "../../../reducers/searchOffersReducer"; @@ -37,6 +38,8 @@ export const AdvancedSearchController = ({ enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, onMobileClose, + // eslint-disable-next-line no-unused-vars + loadUrlFromFilters, setLoadUrlFromFilters, }) => { const { @@ -48,6 +51,7 @@ export const AdvancedSearchController = ({ setTechs: actualSetTechs, resetAdvancedSearchFields: actualResetAdvancedSearchFields, setSearchValue: setUrlSearchValue, + setUrlFilters, } = useSearchParams({ setJobDuration, setShowJobDurationSlider, @@ -92,17 +96,30 @@ export const AdvancedSearchController = ({ }, [onSubmit, searchOffers, searchValue, setUrlSearchValue]); useEffect(() => { - setShowJobDurationSlider(!!queryParams.jobMinDuration && !!queryParams.jobMaxDuration); - setJobDuration(null, [ - parseInt(queryParams.jobMinDuration, 10), - parseInt(queryParams.jobMaxDuration, 10), - ]); - - setJobType(queryParams.jobType); - setFields(ensureArray(queryParams.fields ?? [])); - setTechs(ensureArray(queryParams.technologies ?? [])); - - setSearchValue(queryParams.searchValue); + if (loadUrlFromFilters) { + setUrlFilters( + jobMinDuration, + jobMaxDuration, + fields, + technologies, + jobType, + searchValue, + ); + setLoadUrlFromFilters(false); + submitForm(); + } else { + setShowJobDurationSlider(queryParams.jobMinDuration && queryParams.jobMaxDuration); + setJobDuration(null, [ + parseInt(queryParams.jobMinDuration, 10), + parseInt(queryParams.jobMaxDuration, 10), + ]); + + setJobType(queryParams.jobType); + setFields(queryParams.fields || []); + setTechs(queryParams.technologies || []); + + setSearchValue(queryParams.searchValue); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -124,7 +141,8 @@ export const AdvancedSearchController = ({ export const SearchArea = ({ onSubmit, searchValue, jobMinDuration = INITIAL_JOB_DURATION, jobMaxDuration = INITIAL_JOB_DURATION + 1, jobType = INITIAL_JOB_TYPE, fields, technologies, showJobDurationSlider, setShowJobDurationSlider, advanced: enableAdvancedSearchDefault = false, - setSearchValue, setJobDuration, setJobType, setFields, setTechs, resetAdvancedSearchFields, onMobileClose }) => { + setSearchValue, setJobDuration, setJobType, setFields, setTechs, resetAdvancedSearchFields, onMobileClose, + loadUrlFromFilters, setLoadUrlFromFilters }) => { const classes = useSearchAreaStyles(); const { @@ -140,6 +158,7 @@ export const SearchArea = ({ onSubmit, searchValue, enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, onMobileClose, + loadUrlFromFilters, setLoadUrlFromFilters, }, AdvancedSearchControllerContext ); @@ -196,6 +215,8 @@ SearchArea.propTypes = { setSearchValue: PropTypes.func.isRequired, setJobDuration: PropTypes.func.isRequired, setJobType: PropTypes.func.isRequired, + setLoadUrlFromFilters: PropTypes.func.isRequired, + loadUrlFromFilters: PropTypes.bool, resetAdvancedSearchFields: PropTypes.func.isRequired, fields: PropTypes.array.isRequired, technologies: PropTypes.array.isRequired, @@ -215,6 +236,7 @@ export const mapStateToProps = ({ offerSearch }) => ({ fields: offerSearch.fields, technologies: offerSearch.technologies, showJobDurationSlider: offerSearch.filterJobDuration, + loadUrlFromFilters: offerSearch.loadUrlFromFilters, }); export const mapDispatchToProps = (dispatch) => ({ @@ -225,6 +247,7 @@ export const mapDispatchToProps = (dispatch) => ({ setTechs: (technologies) => dispatch(setTechs(technologies)), setShowJobDurationSlider: (val) => dispatch(setShowJobDurationSlider(val)), resetAdvancedSearchFields: () => dispatch(resetAdvancedSearchFields()), + setLoadUrlFromFilters: (value) => dispatch(setLoadUrlFromFilters(value)), }); export default connect(mapStateToProps, mapDispatchToProps)(SearchArea); diff --git a/src/components/HomePage/SearchArea/useUrlSearchParams.js b/src/components/HomePage/SearchArea/useUrlSearchParams.js index 157a630e..e499456e 100644 --- a/src/components/HomePage/SearchArea/useUrlSearchParams.js +++ b/src/components/HomePage/SearchArea/useUrlSearchParams.js @@ -128,6 +128,39 @@ export default ({ }, [clearURLFilters, location, resetAdvancedSearchFields]); + const setUrlFilters = useCallback(( + jobMinDuration, + jobMaxDuration, + fields, + technologies, + jobType, + searchValue, + ) => { + let currFilters = {}; + + if (jobMinDuration && jobMaxDuration) { + currFilters = { ...currFilters, jobMinDuration, jobMaxDuration }; + } + + if (fields) { + currFilters = { ...currFilters, fields }; + } + + if (technologies) { + currFilters = { ...currFilters, technologies }; + } + + if (jobType) { + currFilters = { ...currFilters, jobType }; + } + + if (searchValue) { + currFilters = { ...currFilters, searchValue }; + } + + changeURLFilters(location, {}, currFilters); + }, [changeURLFilters, location]); + return { queryParams, changeURLFilters, @@ -138,5 +171,6 @@ export default ({ setTechs: actualSetTechs, setSearchValue: actualSetSearchValue, resetAdvancedSearchFields: actualResetAdvancedSearchFields, + setUrlFilters, }; }; diff --git a/src/components/HomePage/SearchResultsArea/Offer/ChipList.js b/src/components/HomePage/SearchResultsArea/Offer/ChipList.js index c83b3ab3..8e39d122 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/ChipList.js +++ b/src/components/HomePage/SearchResultsArea/Offer/ChipList.js @@ -58,6 +58,7 @@ ChipList.propTypes = { type: PropTypes.string, values: PropTypes.arrayOf(PropTypes.string), loading: PropTypes.bool, + onChipClick: PropTypes.func, }; export default ChipList; diff --git a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js index a493dbf1..baf99a11 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js +++ b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js @@ -20,6 +20,7 @@ import useSearchResultsWidgetStyles from "../SearchResultsWidget/searchResultsWi import { RouterLink } from "../../../../utils"; import { JOB_MAX_DURATION } from "../../../../reducers/searchOffersReducer"; import { useChipsFieldSearch } from "./useChipsFieldSearch"; +import { useHistory } from "react-router-dom"; const defaultLogo = require("./default_icon.svg"); @@ -54,7 +55,29 @@ const OfferDetails = ({ + "Please contact support for more information." ), [offer, sessionData]); - const { setFields, setTechs } = useChipsFieldSearch(); + const { setFields, setTechs, setFieldsWithUrl, setTechsWithUrl, setLoadUrlFromFilters } = useChipsFieldSearch(); + + const handleChipSetFields = useCallback((values) => { + if (isPage) { + history.push("/"); + setFields(values); + setLoadUrlFromFilters(true); + } else { + setFieldsWithUrl(values); + } + }, [history, isPage, setFields, setFieldsWithUrl, setLoadUrlFromFilters]); + + const handleChipSetTechs = useCallback((values) => { + if (isPage) { + history.push("/"); + setTechs(values); + setLoadUrlFromFilters(true); + } else { + setTechsWithUrl(values); + } + }, [history, isPage, setLoadUrlFromFilters, setTechs, setTechsWithUrl]); + + const history = useHistory(); const getHiddenOfferMessage = useCallback(() => { if (visibilityState.isDisabled) @@ -251,13 +274,13 @@ const OfferDetails = ({ type="Technologies" values={offer?.technologies} loading={loading} - onChipClick={setTechs} + onChipClick={handleChipSetTechs} /> diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js index ae7147e6..149d45dc 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { setFields, setTechs } from "../../../../actions/searchOffersActions"; +import { setFields, setTechs, setLoadUrlFromFilters } from "../../../../actions/searchOffersActions"; import useSearchParams from "../../SearchArea/useUrlSearchParams"; import { SearchResultsConstants } from "../SearchResultsWidget/SearchResultsUtils"; @@ -26,6 +26,9 @@ export const useChipsFieldSearch = () => { technologies: techs, }); + const addField = useCallback((value) => dispatch(setFields([...fields, value])), [dispatch, fields]); + const addTech = useCallback((value) => dispatch(setTechs([...techs, value])), [dispatch, techs]); + const { setFields: urlSetFields, setTechs: urlSetTechs, @@ -34,14 +37,14 @@ export const useChipsFieldSearch = () => { setTechs: (value) => dispatch(setTechs(value)), }); - const actualSetFields = useCallback((value) => { + const setFieldsWithUrl = useCallback((value) => { if (!fields.includes(value)) { urlSetFields([...fields, value]); setSearch(true); } }, [fields, urlSetFields]); - const actualSetTechs = useCallback((value) => { + const setTechsWithUrl = useCallback((value) => { if (!techs.includes(value)) { urlSetTechs([...techs, value]); setSearch(true); @@ -56,7 +59,10 @@ export const useChipsFieldSearch = () => { }, [searchOffers, fields, techs, search]); return { - setFields: actualSetFields, - setTechs: actualSetTechs, + setFields: addField, + setTechs: addTech, + setFieldsWithUrl, + setTechsWithUrl, + setLoadUrlFromFilters: (value) => dispatch(setLoadUrlFromFilters(value)), }; }; diff --git a/src/reducers/searchOffersReducer.js b/src/reducers/searchOffersReducer.js index c07b18f1..30afa317 100644 --- a/src/reducers/searchOffersReducer.js +++ b/src/reducers/searchOffersReducer.js @@ -74,6 +74,11 @@ export default (state = initialState, action) => { ...state, technologies: action.technologies, }; + case OfferSearchTypes.SET_LOAD_URL_FROM_FILTERS: + return { + ...state, + loadUrlFromFilters: action.value, + }; case OfferSearchTypes.SET_JOB_DURATION_TOGGLE: return { ...state, From e5324422e77154f92a51d5f9be0ef046596b0b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Wed, 30 Nov 2022 11:47:41 +0000 Subject: [PATCH 3/7] Fix url being cleared after update --- src/components/HomePage/SearchArea/SearchArea.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 1df78824..0ba9015a 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -106,7 +106,7 @@ export const AdvancedSearchController = ({ searchValue, ); setLoadUrlFromFilters(false); - submitForm(); + submitForm(null, false); } else { setShowJobDurationSlider(queryParams.jobMinDuration && queryParams.jobMaxDuration); setJobDuration(null, [ @@ -115,8 +115,8 @@ export const AdvancedSearchController = ({ ]); setJobType(queryParams.jobType); - setFields(queryParams.fields || []); - setTechs(queryParams.technologies || []); + setFields(ensureArray(queryParams.fields ?? [])); + setTechs(ensureArray(queryParams.technologies ?? [])); setSearchValue(queryParams.searchValue); } From 6f0b1dd3ed03e7b96a6c7a8e78d22c58dd92de13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Mon, 19 Dec 2022 13:14:18 +0000 Subject: [PATCH 4/7] Refactor function names and add tests --- .../HomePage/SearchArea/SearchArea.js | 2 +- .../SearchResultsArea/Offer/OfferDetails.js | 14 +- .../Offer/useChipsFieldSearch.js | 12 +- .../Offer/useChipsFieldSearch.spec.js | 122 ++++++++++++++++++ 4 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 0ba9015a..9f9cbe70 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -108,7 +108,7 @@ export const AdvancedSearchController = ({ setLoadUrlFromFilters(false); submitForm(null, false); } else { - setShowJobDurationSlider(queryParams.jobMinDuration && queryParams.jobMaxDuration); + setShowJobDurationSlider(!!queryParams.jobMinDuration && !!queryParams.jobMaxDuration); setJobDuration(null, [ parseInt(queryParams.jobMinDuration, 10), parseInt(queryParams.jobMaxDuration, 10), diff --git a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js index baf99a11..da0f0077 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js +++ b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js @@ -55,27 +55,27 @@ const OfferDetails = ({ + "Please contact support for more information." ), [offer, sessionData]); - const { setFields, setTechs, setFieldsWithUrl, setTechsWithUrl, setLoadUrlFromFilters } = useChipsFieldSearch(); + const { addField, addTech, addFieldWithUrl, addTechWithUrl, setLoadUrlFromFilters } = useChipsFieldSearch(); const handleChipSetFields = useCallback((values) => { if (isPage) { history.push("/"); - setFields(values); + addField(values); setLoadUrlFromFilters(true); } else { - setFieldsWithUrl(values); + addFieldWithUrl(values); } - }, [history, isPage, setFields, setFieldsWithUrl, setLoadUrlFromFilters]); + }, [history, isPage, addField, addFieldWithUrl, setLoadUrlFromFilters]); const handleChipSetTechs = useCallback((values) => { if (isPage) { history.push("/"); - setTechs(values); + addTech(values); setLoadUrlFromFilters(true); } else { - setTechsWithUrl(values); + addTechWithUrl(values); } - }, [history, isPage, setLoadUrlFromFilters, setTechs, setTechsWithUrl]); + }, [history, isPage, setLoadUrlFromFilters, addTech, addTechWithUrl]); const history = useHistory(); diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js index 149d45dc..1529d974 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js @@ -37,14 +37,14 @@ export const useChipsFieldSearch = () => { setTechs: (value) => dispatch(setTechs(value)), }); - const setFieldsWithUrl = useCallback((value) => { + const addFieldWithUrl = useCallback((value) => { if (!fields.includes(value)) { urlSetFields([...fields, value]); setSearch(true); } }, [fields, urlSetFields]); - const setTechsWithUrl = useCallback((value) => { + const addTechWithUrl = useCallback((value) => { if (!techs.includes(value)) { urlSetTechs([...techs, value]); setSearch(true); @@ -59,10 +59,10 @@ export const useChipsFieldSearch = () => { }, [searchOffers, fields, techs, search]); return { - setFields: addField, - setTechs: addTech, - setFieldsWithUrl, - setTechsWithUrl, + addField, + addTech, + addFieldWithUrl, + addTechWithUrl, setLoadUrlFromFilters: (value) => dispatch(setLoadUrlFromFilters(value)), }; }; diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js new file mode 100644 index 00000000..2997364f --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js @@ -0,0 +1,122 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import React from "react"; +import { MemoryRouter, useLocation } from "react-router-dom"; +import qs from "qs"; + +import { renderWithStoreAndTheme, TestComponent } from "../../../../test-utils"; +import { act } from "react-dom/test-utils"; +import { useChipsFieldSearch } from "./useChipsFieldSearch"; +import { createTheme } from "@material-ui/core"; +import { applyMiddleware, createStore } from "redux"; +import searchOffersReducer from "../../../../reducers/searchOffersReducer"; +import thunk from "redux-thunk"; +import { useSelector } from "react-redux"; + +describe("useUrlSearchParams", () => { + const theme = createTheme({}); + + it("should change fields's search param when adding fields", async () => { + let addFieldWithUrl, location; + const initialState = { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }; + + const store = createStore(searchOffersReducer, initialState, applyMiddleware(...[thunk]),); + + const callback = () => { + addFieldWithUrl = useChipsFieldSearch().addFieldWithUrl; + + location = useLocation(); + }; + + + renderWithStoreAndTheme( + + + , + { store, theme } + ); + + + expect(location).toHaveProperty("search", ""); + + await act(() => { + addFieldWithUrl("TEST-FIELD"); + }); + + let expectedLocationSearch = `?${qs.stringify({ + fields: ["TEST-FIELD"], + }, { skipNulls: true, arrayFormat: "brackets" })}`; + + expect(location).toHaveProperty("search", expectedLocationSearch); + + // Wait for state update + await new Promise((r) => setTimeout(r, 500)); + + await act(() => { + addFieldWithUrl("TEST-FIELD-2"); + }); + + expectedLocationSearch = `?${qs.stringify({ + fields: ["TEST-FIELD", "TEST-FIELD-2"], + }, { skipNulls: true, arrayFormat: "brackets" })}`; + + expect(location).toHaveProperty("search", expectedLocationSearch); + }); + + it("should change technologies's search param when adding fields", async () => { + let addTechWithUrl, location; + const initialState = { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }; + + const store = createStore(searchOffersReducer, initialState, applyMiddleware(...[thunk]),); + + const callback = () => { + addTechWithUrl = useChipsFieldSearch().addTechWithUrl; + + location = useLocation(); + }; + + + renderWithStoreAndTheme( + + + , + { store, theme } + ); + + + expect(location).toHaveProperty("search", ""); + + await act(() => { + addTechWithUrl("TEST-TECH"); + }); + + let expectedLocationSearch = `?${qs.stringify({ + technologies: ["TEST-TECH"], + }, { skipNulls: true, arrayFormat: "brackets" })}`; + + expect(location).toHaveProperty("search", expectedLocationSearch); + + // Wait for state update + await new Promise((r) => setTimeout(r, 500)); + + await act(() => { + addTechWithUrl("TEST-TECH-2"); + }); + + expectedLocationSearch = `?${qs.stringify({ + technologies: ["TEST-TECH", "TEST-TECH-2"], + }, { skipNulls: true, arrayFormat: "brackets" })}`; + + expect(location).toHaveProperty("search", expectedLocationSearch); + }); + +}); From 514e770271b5599c89650c79857e20ea54543115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Mon, 19 Dec 2022 16:35:11 +0000 Subject: [PATCH 5/7] Add chip interaction tests --- .../SearchResultsArea/Offer/OfferDetails.js | 2 +- .../Offer/useChipFieldSearchOffer.spec.js | 160 ++++++++++++++++++ .../Offer/useChipsFieldSearch.js | 5 +- .../Offer/useChipsFieldSearch.spec.js | 36 ++-- 4 files changed, 181 insertions(+), 22 deletions(-) create mode 100644 src/components/HomePage/SearchResultsArea/Offer/useChipFieldSearchOffer.spec.js diff --git a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js index da0f0077..e4dd1c9c 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js +++ b/src/components/HomePage/SearchResultsArea/Offer/OfferDetails.js @@ -19,7 +19,7 @@ import useSession from "../../../../hooks/useSession"; import useSearchResultsWidgetStyles from "../SearchResultsWidget/searchResultsWidgetStyles"; import { RouterLink } from "../../../../utils"; import { JOB_MAX_DURATION } from "../../../../reducers/searchOffersReducer"; -import { useChipsFieldSearch } from "./useChipsFieldSearch"; +import useChipsFieldSearch from "./useChipsFieldSearch"; import { useHistory } from "react-router-dom"; const defaultLogo = require("./default_icon.svg"); diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipFieldSearchOffer.spec.js b/src/components/HomePage/SearchResultsArea/Offer/useChipFieldSearchOffer.spec.js new file mode 100644 index 00000000..2d023641 --- /dev/null +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipFieldSearchOffer.spec.js @@ -0,0 +1,160 @@ +import { createTheme } from "@material-ui/core"; +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import { renderWithStoreAndTheme, fireEvent } from "../../../../test-utils"; +import Offer from "./Offer"; +import OfferWidget from "./OfferWidget"; +import useChipsFieldSearch from "./useChipsFieldSearch"; + +jest.mock("./useChipsFieldSearch"); +const mockHistoryPush = jest.fn(); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +describe("useChipsFieldSearch", () => { + const theme = createTheme({}); + + + const offer = new Offer({ + _id: "id1", + title: "position1", + owner: "company_id", + ownerName: "company1", + ownerLogo: "", + location: "location1", + fields: ["BACKEND"], + technologies: ["Cassandra"], + jobMinDuration: 1, + jobMaxDuration: 12, + jobStartDate: (new Date()).toISOString(), + publishDate: "2021-04-22T22:35:57.177Z", + publishEndDate: "2021-09-19T23:00:00.000Z", + isPaid: false, + vacancies: 2, + description: "description1", + }); + + + it("should redirect and update field filter state if in page", async () => { + const addFieldMock = jest.fn(); + useChipsFieldSearch.mockImplementation(() => ({ + addField: addFieldMock, + setLoadUrlFromFilters: jest.fn(), + })); + + const initialState = { + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, + }; + + + const wrapper = renderWithStoreAndTheme( + + + , + { initialState, theme } + ); + + await fireEvent.click(wrapper.getByText("Back-End")); + + expect(mockHistoryPush).toHaveBeenCalledWith("/"); + expect(addFieldMock).toHaveBeenCalledWith("BACKEND"); + }); + + it("should update field filter and url if in search page", async () => { + const addFieldWithUrlMock = jest.fn(); + + useChipsFieldSearch.mockImplementation(() => ({ + addFieldWithUrl: addFieldWithUrlMock, + })); + + const initialState = { + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, + }; + + + const wrapper = renderWithStoreAndTheme( + + + , + { initialState, theme } + ); + + await fireEvent.click(wrapper.getByText("Back-End")); + + expect(addFieldWithUrlMock).toHaveBeenCalledWith("BACKEND"); + }); + + it("should redirect and update techs filter state if in page", async () => { + const addTechMock = jest.fn(); + useChipsFieldSearch.mockImplementation(() => ({ + addTech: addTechMock, + setLoadUrlFromFilters: jest.fn(), + })); + + const initialState = { + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, + }; + + + const wrapper = renderWithStoreAndTheme( + + + , + { initialState, theme } + ); + + await fireEvent.click(wrapper.getByText("Cassandra")); + + expect(mockHistoryPush).toHaveBeenCalledWith("/"); + expect(addTechMock).toHaveBeenCalledWith("Cassandra"); + }); + + it("should update techs filter and url if in search page", async () => { + const addTechWithUrlMock = jest.fn(); + + useChipsFieldSearch.mockImplementation(() => ({ + addTechWithUrl: addTechWithUrlMock, + })); + + const initialState = { + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, + }; + + + const wrapper = renderWithStoreAndTheme( + + + , + { initialState, theme } + ); + + await fireEvent.click(wrapper.getByText("Cassandra")); + + expect(addTechWithUrlMock).toHaveBeenCalledWith("Cassandra"); + }); +}); diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js index 1529d974..9c9f3106 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.js @@ -6,7 +6,7 @@ import useSearchParams from "../../SearchArea/useUrlSearchParams"; import { SearchResultsConstants } from "../SearchResultsWidget/SearchResultsUtils"; import useOffersSearcher from "../SearchResultsWidget/useOffersSearcher"; -export const useChipsFieldSearch = () => { +export default () => { const dispatch = useDispatch(); const fields = useSelector((state) => state.offerSearch.fields); const techs = useSelector((state) => state.offerSearch.technologies); @@ -38,6 +38,7 @@ export const useChipsFieldSearch = () => { }); const addFieldWithUrl = useCallback((value) => { + /* istanbul ignore else */ if (!fields.includes(value)) { urlSetFields([...fields, value]); setSearch(true); @@ -45,6 +46,7 @@ export const useChipsFieldSearch = () => { }, [fields, urlSetFields]); const addTechWithUrl = useCallback((value) => { + /* istanbul ignore else */ if (!techs.includes(value)) { urlSetTechs([...techs, value]); setSearch(true); @@ -52,6 +54,7 @@ export const useChipsFieldSearch = () => { }, [techs, urlSetTechs]); useEffect(() => { + /* istanbul ignore else */ if (search) { searchOffers(SearchResultsConstants.INITIAL_LIMIT); setSearch(false); diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js index 2997364f..bc914b7b 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js @@ -5,27 +5,23 @@ import qs from "qs"; import { renderWithStoreAndTheme, TestComponent } from "../../../../test-utils"; import { act } from "react-dom/test-utils"; -import { useChipsFieldSearch } from "./useChipsFieldSearch"; +import useChipsFieldSearch from "./useChipsFieldSearch"; import { createTheme } from "@material-ui/core"; -import { applyMiddleware, createStore } from "redux"; -import searchOffersReducer from "../../../../reducers/searchOffersReducer"; -import thunk from "redux-thunk"; -import { useSelector } from "react-redux"; -describe("useUrlSearchParams", () => { +describe("useChipsFieldSearch", () => { const theme = createTheme({}); it("should change fields's search param when adding fields", async () => { let addFieldWithUrl, location; const initialState = { - searchValue: "searchValue", - jobDuration: [1, 2], - fields: [], - technologies: [], + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, }; - const store = createStore(searchOffersReducer, initialState, applyMiddleware(...[thunk]),); - const callback = () => { addFieldWithUrl = useChipsFieldSearch().addFieldWithUrl; @@ -37,7 +33,7 @@ describe("useUrlSearchParams", () => { , - { store, theme } + { initialState, theme } ); @@ -70,14 +66,14 @@ describe("useUrlSearchParams", () => { it("should change technologies's search param when adding fields", async () => { let addTechWithUrl, location; const initialState = { - searchValue: "searchValue", - jobDuration: [1, 2], - fields: [], - technologies: [], + offerSearch: { + searchValue: "searchValue", + jobDuration: [1, 2], + fields: [], + technologies: [], + }, }; - const store = createStore(searchOffersReducer, initialState, applyMiddleware(...[thunk]),); - const callback = () => { addTechWithUrl = useChipsFieldSearch().addTechWithUrl; @@ -89,7 +85,7 @@ describe("useUrlSearchParams", () => { , - { store, theme } + { initialState, theme } ); From f782aa1055e6f0ab7d0b75dcf4767f4c70d73e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Tue, 24 Jan 2023 14:55:23 +0000 Subject: [PATCH 6/7] Improve search area coverage --- .../HomePage/SearchArea/SearchArea.js | 1 - .../HomePage/SearchArea/SearchArea.spec.js | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 9f9cbe70..66a16970 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -38,7 +38,6 @@ export const AdvancedSearchController = ({ enableAdvancedSearchDefault, showJobDurationSlider, setShowJobDurationSlider, jobMinDuration, jobMaxDuration, setJobDuration, jobType, setJobType, fields, setFields, technologies, setTechs, resetAdvancedSearchFields, onSubmit, searchValue, setSearchValue, onMobileClose, - // eslint-disable-next-line no-unused-vars loadUrlFromFilters, setLoadUrlFromFilters, }) => { diff --git a/src/components/HomePage/SearchArea/SearchArea.spec.js b/src/components/HomePage/SearchArea/SearchArea.spec.js index 3de94421..0304242d 100644 --- a/src/components/HomePage/SearchArea/SearchArea.spec.js +++ b/src/components/HomePage/SearchArea/SearchArea.spec.js @@ -214,6 +214,41 @@ describe("SearchArea", () => { expect(onSubmit).toHaveBeenCalledTimes(1); }); + it("should call onSubmit callback on search button click", () => { + const searchValue = "test"; + const setSearchValue = () => { }; + + const onSubmit = jest.fn(); + const addSnackbar = () => { }; + + // Simulate request success + fetch.mockResponse(JSON.stringify({ mockData: true })); + + renderWithStoreAndTheme( + + { }} + setShowJobDurationSlider={() => { }} + setTechs={() => { }} + setJobDuration={() => { }} + setFields={() => { }} + setJobType={() => { }} + onSubmit={onSubmit} + fields={[]} + technologies={[]} + setLoadUrlFromFilters={() => { }} + /> + , + { initialState, theme } + ); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); + it("should fill in search filters if they are present in the URL", () => { const urlParams = { From 83ec12018fcda361a8fff6c6bbcb2f1a1b02b86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20J=C3=BAlio=20Moreira?= Date: Tue, 11 Apr 2023 16:30:19 +0100 Subject: [PATCH 7/7] Refactor tests to use testHook and update to rtl --- .../Offer/useChipsFieldSearch.spec.js | 20 +++---------------- src/test-utils.js | 14 ++++++++++++- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js index bc914b7b..c9b66869 100644 --- a/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js +++ b/src/components/HomePage/SearchResultsArea/Offer/useChipsFieldSearch.spec.js @@ -1,9 +1,8 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import React from "react"; import { MemoryRouter, useLocation } from "react-router-dom"; import qs from "qs"; -import { renderWithStoreAndTheme, TestComponent } from "../../../../test-utils"; +import { testHookWithStoreAndTheme } from "../../../../test-utils"; import { act } from "react-dom/test-utils"; import useChipsFieldSearch from "./useChipsFieldSearch"; import { createTheme } from "@material-ui/core"; @@ -28,14 +27,7 @@ describe("useChipsFieldSearch", () => { location = useLocation(); }; - - renderWithStoreAndTheme( - - - , - { initialState, theme } - ); - + testHookWithStoreAndTheme(callback, initialState, theme, MemoryRouter); expect(location).toHaveProperty("search", ""); @@ -80,13 +72,7 @@ describe("useChipsFieldSearch", () => { location = useLocation(); }; - - renderWithStoreAndTheme( - - - , - { initialState, theme } - ); + testHookWithStoreAndTheme(callback, initialState, theme, MemoryRouter, { initialEntries: ["/"] }); expect(location).toHaveProperty("search", ""); diff --git a/src/test-utils.js b/src/test-utils.js index 2c8544c8..e6f63dbd 100644 --- a/src/test-utils.js +++ b/src/test-utils.js @@ -44,13 +44,25 @@ export const TestComponent = ({ callback }) => { }; export const testHook = (callback, Wrapper = React.Fragment, wrapperProps = {}) => { - mount( + defaultRender( ); }; +export const testHookWithStoreAndTheme = (callback, initialState, theme, Wrapper = React.Fragment, wrapperProps = {}) => { + renderWithStoreAndTheme( + + + , + { + initialState, + theme, + } + ); +}; + const [queryDescriptionOf, , getDescriptionOf, , findDescriptionOf] = buildQueries( function queryAllDescriptionsOf(container, element) { return container.querySelectorAll(`#${element.getAttribute("aria-describedby")}`);