From c0486cd729aa375973f2b8500b7c74d48417308e Mon Sep 17 00:00:00 2001 From: Nuno Pereira Date: Wed, 7 Dec 2022 20:53:04 +0000 Subject: [PATCH] Fixed passing array values in query --- .../HomePage/SearchArea/SearchArea.js | 6 ++- .../HomePage/SearchArea/SearchArea.spec.js | 4 ++ .../HomePage/SearchArea/useUrlSearchParams.js | 21 ++++++-- .../SearchArea/useUrlSearchParams.spec.js | 50 +++++++++++++++++-- .../HomePage/URLSearchParamsParser.js | 6 ++- src/utils/index.js | 6 +++ src/utils/index.spec.js | 13 ++++- 7 files changed, 94 insertions(+), 12 deletions(-) diff --git a/src/components/HomePage/SearchArea/SearchArea.js b/src/components/HomePage/SearchArea/SearchArea.js index 1e1a16e2..bed37551 100644 --- a/src/components/HomePage/SearchArea/SearchArea.js +++ b/src/components/HomePage/SearchArea/SearchArea.js @@ -29,6 +29,8 @@ import useOffersSearcher from "../SearchResultsArea/SearchResultsWidget/useOffer import useSearchParams from "./useUrlSearchParams"; +import { ensureArray } from "../../../utils"; + export const AdvancedSearchControllerContext = React.createContext({}); export const AdvancedSearchController = ({ @@ -97,8 +99,8 @@ export const AdvancedSearchController = ({ ]); setJobType(queryParams.jobType); - setFields(queryParams.fields || []); - setTechs(queryParams.technologies || []); + setFields(ensureArray(queryParams.fields ?? [])); + setTechs(ensureArray(queryParams.technologies ?? [])); setSearchValue(queryParams.searchValue); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/HomePage/SearchArea/SearchArea.spec.js b/src/components/HomePage/SearchArea/SearchArea.spec.js index 76c27909..109cb723 100644 --- a/src/components/HomePage/SearchArea/SearchArea.spec.js +++ b/src/components/HomePage/SearchArea/SearchArea.spec.js @@ -193,6 +193,10 @@ describe("SearchArea", () => { const filterJobDuration = false; props.setShowJobDurationSlider(filterJobDuration); expect(dispatch).toHaveBeenCalledWith(setShowJobDurationSlider(false)); + + dispatch.mockClear(); + props.resetAdvancedSearchFields(); + expect(dispatch).toHaveBeenCalled(); }); }); }); diff --git a/src/components/HomePage/SearchArea/useUrlSearchParams.js b/src/components/HomePage/SearchArea/useUrlSearchParams.js index 04651036..157a630e 100644 --- a/src/components/HomePage/SearchArea/useUrlSearchParams.js +++ b/src/components/HomePage/SearchArea/useUrlSearchParams.js @@ -5,6 +5,8 @@ import { useLocation, useHistory } from "react-router-dom"; import { throttle } from "lodash"; +import { ensureArray } from "../../../utils"; + const HISTORY_REPLACE_THROTTLE_DELAY_MS = 350; /** @@ -55,6 +57,7 @@ export default ({ const actualSetJobType = useCallback(({ target: { value: jobType } }) => { changeURLFilters(location, queryParams, { jobType }); + /* istanbul ignore else */ if (setJobType) setJobType(jobType); @@ -66,6 +69,7 @@ export default ({ changeURLFilters(location, queryParams, { jobMinDuration, jobMaxDuration }); + /* istanbul ignore else */ if (setJobDuration) setJobDuration(unused, duration); @@ -75,6 +79,7 @@ export default ({ if (!showJobDurationSlider) changeURLFilters(location, queryParams, { jobMinDuration: null, jobMaxDuration: null }); + /* istanbul ignore else */ if (setShowJobDurationSlider) setShowJobDurationSlider(showJobDurationSlider); @@ -82,19 +87,25 @@ export default ({ const actualSetFields = useCallback((fields) => { - changeURLFilters(location, queryParams, { fields }); + const sanitizedFields = ensureArray(fields); + + changeURLFilters(location, queryParams, { fields: sanitizedFields }); + /* istanbul ignore else */ if (setFields) - setFields(fields); + setFields(sanitizedFields); }, [changeURLFilters, location, queryParams, setFields]); const actualSetTechs = useCallback((technologies) => { - changeURLFilters(location, queryParams, { technologies }); + const sanitizedTechnologies = ensureArray(technologies); + + changeURLFilters(location, queryParams, { technologies: sanitizedTechnologies }); + /* istanbul ignore else */ if (setTechs) - setTechs(technologies); + setTechs(sanitizedTechnologies); }, [changeURLFilters, location, queryParams, setTechs]); @@ -102,6 +113,7 @@ export default ({ changeURLFilters(location, queryParams, { searchValue }); + /* istanbul ignore else */ if (setSearchValue) setSearchValue(searchValue); @@ -110,6 +122,7 @@ export default ({ const actualResetAdvancedSearchFields = useCallback(() => { clearURLFilters(location); + /* istanbul ignore else */ if (resetAdvancedSearchFields) resetAdvancedSearchFields(); diff --git a/src/components/HomePage/SearchArea/useUrlSearchParams.spec.js b/src/components/HomePage/SearchArea/useUrlSearchParams.spec.js index bb152cc8..bbf770ad 100644 --- a/src/components/HomePage/SearchArea/useUrlSearchParams.spec.js +++ b/src/components/HomePage/SearchArea/useUrlSearchParams.spec.js @@ -6,8 +6,7 @@ import useUrlSearchParams from "./useUrlSearchParams"; import { testHook } from "../../../test-utils"; import { act } from "react-dom/test-utils"; -const capitalizeFirstLetter = ([first, ...rest], locale = navigator.language) => - first === undefined ? "" : first.toLocaleUpperCase(locale) + rest.join(""); +import { capitalize } from "../../../utils"; describe("useUrlSearchParams", () => { @@ -46,7 +45,7 @@ describe("useUrlSearchParams", () => { let location, searchParamsResult; - const functionName = `set${capitalizeFirstLetter(fieldName)}`; + const functionName = `set${capitalize(fieldName)}`; const testFunction = jest.fn(); testHook(() => { @@ -94,6 +93,51 @@ describe("useUrlSearchParams", () => { expect(location).toHaveProperty("search", expectedLocationSearch); }); + it.each([ + ["fields", "TEST-FIELD", ["TEST-FIELD"]], + ["techs", "TEST-TECH", ["TEST-TECH"]], + ])("should parse array-like query value '%s' correctly", async (fieldName, fieldValue, expectedValue) => { + + let location, searchParamsResult; + + const functionName = `set${capitalize(fieldName)}`; + const testFunction = jest.fn(); + + testHook(() => { + searchParamsResult = useUrlSearchParams({ + [functionName]: testFunction, + }); + + location = useLocation(); + }, MemoryRouter); + + const hookedInTestFunction = searchParamsResult[functionName]; + + // using fail(...) is not recommended as Jasmine globals will be removed from Jest + if (!hookedInTestFunction) throw Error(`No hooked-in test function found for ${functionName}`); + + expect(location).toHaveProperty("search", ""); + + await act(() => { + hookedInTestFunction(fieldValue); + }); + + expect(testFunction).toHaveBeenCalled(); + + let params = { + [fieldName]: expectedValue, + }; + + if (fieldName === "techs") + params = { + "technologies": expectedValue, + }; + + const expectedLocationSearch = `?${qs.stringify(params, { skipNulls: true, arrayFormat: "brackets" })}`; + + expect(location).toHaveProperty("search", expectedLocationSearch); + }); + it("should clear the Location's search property", async () => { let location, searchParamsResult; diff --git a/src/components/HomePage/URLSearchParamsParser.js b/src/components/HomePage/URLSearchParamsParser.js index a4d861a4..a1635168 100644 --- a/src/components/HomePage/URLSearchParamsParser.js +++ b/src/components/HomePage/URLSearchParamsParser.js @@ -7,6 +7,8 @@ import { useSelector } from "react-redux"; import useOffersSearcher from "./SearchResultsArea/SearchResultsWidget/useOffersSearcher"; import { SearchResultsConstants } from "./SearchResultsArea/SearchResultsWidget/SearchResultsUtils"; +import { ensureArray } from "../../utils"; + // offerSearch params that should NOT trigger an auto-submit const invalidParams = ["error", "offers", "loading", "filterJobDuration"]; @@ -31,8 +33,8 @@ const URLSearchParamsParser = ({ showSearchResults }) => { jobMaxDuration: queryParams.jobMaxDuration, jobMinDuration: queryParams.jobMinDuration, jobType: queryParams.jobType, - fields: queryParams.fields, - technologies: queryParams.technologies, + fields: ensureArray(queryParams.fields), + technologies: ensureArray(queryParams.technologies), }); // we specifically want this to only run once to avoid infinite re-renders diff --git a/src/utils/index.js b/src/utils/index.js index cd7b0305..f405cb31 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -379,3 +379,9 @@ export const generalParseRequestErrors = (err, getHumanError) => { generalErrors, }; }; + +export const ensureArray = (val) => { + if (Array.isArray(val)) return val; + + else return [val]; +}; diff --git a/src/utils/index.spec.js b/src/utils/index.spec.js index d97ac72a..cd4dcfae 100644 --- a/src/utils/index.spec.js +++ b/src/utils/index.spec.js @@ -1,7 +1,7 @@ import React, { useContext } from "react"; import { createMemoryHistory } from "history"; import { Switch, Link, Router } from "react-router-dom"; -import { smoothScrollToRef, capitalize, Wrap, Route, ProtectedRoute } from "."; +import { smoothScrollToRef, capitalize, Wrap, Route, ProtectedRoute, ensureArray } from "."; import { render, renderWithStore, screen, act, fireEvent } from "../test-utils"; import useSession from "../hooks/useSession"; @@ -262,4 +262,15 @@ describe("utils", () => { expect(screen.getByText("No match")).toBeInTheDocument(); }); }); + + describe("ensureArray", () => { + it("should return array value when receiving array-like value", () => { + expect(ensureArray([1])).toEqual([1]); + expect(ensureArray(Array.of(1, 2))).toEqual([1, 2]); + }); + it("should return array value when receiving non-array-like value", () => { + expect(ensureArray(1)).toEqual([1]); + expect(ensureArray("test")).toEqual(["test"]); + }); + }); });