diff --git a/.eslintrc.js b/.eslintrc.js index 045f5f64..3d2fbbbd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,5 +19,15 @@ module.exports = { }], '@typescript-eslint/semi': ['error'], '@typescript-eslint/type-annotation-spacing': ['error'], + '@typescript-eslint/member-delimiter-style': ['error', { + multiline: { + delimiter: 'comma', + requireLast: false + }, + singleline: { + delimiter: 'comma', + requireLast: false + }, + }] } }; diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 2c3e441c..f2efbb90 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -278,6 +278,66 @@ SOFTWARE. ----------- +The following NPM package may be included in this product: + + - js-tokens@4.0.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - loose-envify@1.4.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2015 Andres Suarez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------- + The following NPM package may be included in this product: - node-fetch@2.6.1 @@ -308,6 +368,36 @@ SOFTWARE. ----------- +The following NPM package may be included in this product: + + - object-assign@4.1.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------- + The following NPM package may be included in this product: - react@17.0.2 diff --git a/__mocks__/styleMock.ts b/__mocks__/styleMock.ts new file mode 100644 index 00000000..a0995453 --- /dev/null +++ b/__mocks__/styleMock.ts @@ -0,0 +1 @@ +module.exports = {}; \ No newline at end of file diff --git a/__mocks__/svgMock.tsx b/__mocks__/svgMock.tsx new file mode 100644 index 00000000..a4175d2f --- /dev/null +++ b/__mocks__/svgMock.tsx @@ -0,0 +1,2 @@ +import * as React from 'react'; +module.exports = { ReactComponent: () => }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9338d010..c7bf5421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.2.1", - "@types/jest": "^27.0.1", + "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", "eslint": "^7.32.0", @@ -2982,9 +2982,9 @@ } }, "node_modules/@types/jest": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz", - "integrity": "sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==", + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", + "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", "dev": true, "dependencies": { "jest-diff": "^27.0.0", @@ -13451,9 +13451,9 @@ } }, "@types/jest": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz", - "integrity": "sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==", + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", + "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", "dev": true, "requires": { "jest-diff": "^27.0.0", diff --git a/package.json b/package.json index d28991a6..1e41e9cc 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.2.1", - "@types/jest": "^27.0.1", + "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", "eslint": "^7.32.0", @@ -73,6 +73,11 @@ "testEnvironment": "jsdom", "testMatch": [ "/tests/**/*.(test).ts(x)?" - ] + ], + "moduleNameMapper": { + "@yext/answers-headless-react": "/src", + "\\.svg$": "/__mocks__/svgMock.tsx", + "\\.(css|scss)$": "/__mocks__/styleMock.ts" + } } } diff --git a/react-app.d.ts b/react-app.d.ts new file mode 100644 index 00000000..51275950 --- /dev/null +++ b/react-app.d.ts @@ -0,0 +1,14 @@ +/** + * Allows TypeScript to correctly recognize the .svg module declarations, + * where svg can be used as a React component. + */ +declare module '*.svg' { + import * as React from 'react'; + + export const ReactComponent: React.FunctionComponent & { title?: string }>; + + const src: string; + export default src; +} \ No newline at end of file diff --git a/sample-app/package-lock.json b/sample-app/package-lock.json index d9275498..b9f0ee6a 100644 --- a/sample-app/package-lock.json +++ b/sample-app/package-lock.json @@ -89,7 +89,7 @@ }, "..": { "name": "@yext/answers-headless-react", - "version": "0.3.0-beta.0", + "version": "0.4.0-beta.0", "dependencies": { "@reduxjs/toolkit": "^1.6.2", "@types/react": "^17.0.15", @@ -103,7 +103,7 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.2.1", - "@types/jest": "^27.0.1", + "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", "eslint": "^7.32.0", @@ -25124,7 +25124,7 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.2.1", - "@types/jest": "^27.0.1", + "@types/jest": "^27.0.2", "@types/react": "^17.0.15", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", diff --git a/sample-app/package.json b/sample-app/package.json index 7b83b345..67b352de 100644 --- a/sample-app/package.json +++ b/sample-app/package.json @@ -84,13 +84,24 @@ "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js", - "postinstall": "./scripts/link-modules.sh" + "postinstall": "./scripts/link-modules.sh", + "eslint": "eslint" }, "eslintConfig": { - "extends": "react-app", + "extends": ["react-app"], "rules": { "object-curly-spacing": ["error", "always"], - "indent": ["error", 2, { "SwitchCase": 1, "ignoreComments": true }] + "indent": ["error", 2, { "SwitchCase": 1, "ignoreComments": true }], + "@typescript-eslint/member-delimiter-style": ["error", { + "multiline": { + "delimiter": "comma", + "requireLast": false + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + } + }] } }, "browserslist": { diff --git a/sample-app/src/PageRouter.tsx b/sample-app/src/PageRouter.tsx index 2b16076a..3a746e63 100644 --- a/sample-app/src/PageRouter.tsx +++ b/sample-app/src/PageRouter.tsx @@ -2,15 +2,15 @@ import { ComponentType } from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; interface RouteData { - path: string - page: JSX.Element + path: string, + page: JSX.Element, exact?: boolean } export type LayoutComponent = ComponentType<{ page: JSX.Element }> interface PageProps { - Layout?: LayoutComponent + Layout?: LayoutComponent, routes: RouteData[] } diff --git a/sample-app/src/components/AlternativeVerticals.tsx b/sample-app/src/components/AlternativeVerticals.tsx index 75f7edc4..cc13f84d 100644 --- a/sample-app/src/components/AlternativeVerticals.tsx +++ b/sample-app/src/components/AlternativeVerticals.tsx @@ -11,7 +11,7 @@ interface VerticalConfig { } interface VerticalSuggestion extends VerticalConfig { - resultsCount: number; + resultsCount: number } function isVerticalSuggestion (suggestion: VerticalSuggestion | null): suggestion is VerticalSuggestion { @@ -21,7 +21,7 @@ function isVerticalSuggestion (suggestion: VerticalSuggestion | null): suggestio interface Props { currentVerticalLabel: string, verticalsConfig: VerticalConfig[], - displayAllResults?: boolean; + displayAllResults?: boolean } export default function AlternativeVerticals (props: Props): JSX.Element | null { diff --git a/sample-app/src/components/AppliedFilters.tsx b/sample-app/src/components/AppliedFilters.tsx index d182d227..4298e306 100644 --- a/sample-app/src/components/AppliedFilters.tsx +++ b/sample-app/src/components/AppliedFilters.tsx @@ -1,6 +1,9 @@ import { DisplayableFilter } from '../models/displayableFilter'; import { GroupedFilters } from '../models/groupedFilters'; import '../sass/AppliedFilters.scss'; +import { ReactComponent as CloseX } from '../icons/x.svg'; +import { useAnswersActions } from '@yext/answers-headless-react' +import { isNearFilterValue } from '../utils/filterutils'; interface Props { showFieldNames?: boolean, @@ -12,7 +15,12 @@ interface Props { /** * Renders AppliedFilters component */ -export default function AppliedFilters({ showFieldNames, labelText, delimiter, appliedFilters }: Props): JSX.Element { +export default function AppliedFilters({ + showFieldNames, + labelText, + delimiter, + appliedFilters +}: Props): JSX.Element { return (
{appliedFilters.map((filterGroup: GroupedFilters, index: number) => { @@ -39,12 +47,43 @@ function renderFilterLabel(label: string): JSX.Element { function renderAppliedFilters(filters: Array): JSX.Element { const filterElems = filters.map((filter: DisplayableFilter, index: number) => { - return ( -
- {filter.label} - {index < filters.length - 1 && ,} -
- ); + if (filter.filterType === 'NLP_FILTER') { + return ( +
+ {filter.label} + {index < filters.length - 1 && ,} +
+ ); + } + return }); + return <>{filterElems}; -} \ No newline at end of file +} + +function RemovableFilter({ filter }: {filter: DisplayableFilter }): JSX.Element { + const answersAction = useAnswersActions(); + + const onRemoveFacetOption = () => { + const { fieldId, matcher, value } = filter.filter; + if (isNearFilterValue(value)) { + console.error('A Filter with a NearFilterValue is not a supported RemovableFilter.'); + return; + } + answersAction.unselectFacetOption(fieldId, { matcher, value }); + answersAction.executeVerticalQuery(); + } + + const onRemoveStaticFilterOption = () => { + document.getElementById(`${filter.filter.fieldId + "_" + filter.filter.value}`)?.click(); + } + + const onRemoveFilter = filter.filterType === 'FACET' ? onRemoveFacetOption : onRemoveStaticFilterOption; + + return ( +
+ {filter.label} +
+
+ ); +} diff --git a/sample-app/src/components/DecoratedAppliedFilters.tsx b/sample-app/src/components/DecoratedAppliedFilters.tsx index 7e32195f..fc99487d 100644 --- a/sample-app/src/components/DecoratedAppliedFilters.tsx +++ b/sample-app/src/components/DecoratedAppliedFilters.tsx @@ -17,7 +17,8 @@ export interface DecoratedAppliedFiltersConfig { */ export function DecoratedAppliedFiltersDisplay(props : DecoratedAppliedFiltersConfig): JSX.Element { const { hiddenFields = [], appliedQueryFilters = [], ...otherProps } = props; - const filterState = useAnswersState(state => state.filters); + const state = useAnswersState(state => state); + const filterState = state.vertical.results ? state.filters : {}; const groupedFilters: Array = getGroupedAppliedFilters(filterState, appliedQueryFilters, hiddenFields); return } diff --git a/sample-app/src/components/Dropdown.tsx b/sample-app/src/components/Dropdown.tsx index 5f00dba2..565ca57d 100644 --- a/sample-app/src/components/Dropdown.tsx +++ b/sample-app/src/components/Dropdown.tsx @@ -1,17 +1,17 @@ import classNames from 'classnames'; export interface Option { - value: string - render: () => JSX.Element | null; + value: string, + render: () => JSX.Element | null } interface Props { - options: Option[] + options: Option[], onClickOption?: (option: Option) => void, - focusedOptionIndex: number | undefined; + focusedOptionIndex: number | undefined, cssClasses: { optionContainer: string, - option: string + option: string, focusedOption: string } } diff --git a/sample-app/src/components/InputDropdown.tsx b/sample-app/src/components/InputDropdown.tsx index 8fc020ad..2857e698 100644 --- a/sample-app/src/components/InputDropdown.tsx +++ b/sample-app/src/components/InputDropdown.tsx @@ -2,24 +2,24 @@ import { useReducer, KeyboardEvent, useRef, useEffect, useState } from "react" import Dropdown, { Option } from './Dropdown'; interface Props { - inputValue?: string - placeholder?: string - options: Option[] - onSubmit?: (value: string) => void - updateInputValue: (value: string) => void - updateDropdown: () => void - renderButtons?: () => JSX.Element | null + inputValue?: string, + placeholder?: string, + options: Option[], + onSubmit?: (value: string) => void, + updateInputValue: (value: string) => void, + updateDropdown: () => void, + renderButtons?: () => JSX.Element | null, cssClasses: { optionContainer: string, - option: string - focusedOption: string + option: string, + focusedOption: string, inputElement: string, inputContainer: string } } interface State { - focusedOptionIndex?: number + focusedOptionIndex?: number, shouldDisplayDropdown: boolean } diff --git a/sample-app/src/components/LocationBias.tsx b/sample-app/src/components/LocationBias.tsx index ced90a4f..a115daeb 100644 --- a/sample-app/src/components/LocationBias.tsx +++ b/sample-app/src/components/LocationBias.tsx @@ -5,14 +5,14 @@ import { useAnswersActions, useAnswersState } from '@yext/answers-headless-react interface Props { isVertical: boolean, geolocationOptions?: { - enableHighAccuracy?: boolean - timeout?: number + enableHighAccuracy?: boolean, + timeout?: number, maximumAge?: number - } + }, cssClasses?: { - container: string - location: string - source: string + container: string, + location: string, + source: string, button: string } } diff --git a/sample-app/src/components/Navigation.tsx b/sample-app/src/components/Navigation.tsx index ed2817d3..71e2a29b 100644 --- a/sample-app/src/components/Navigation.tsx +++ b/sample-app/src/components/Navigation.tsx @@ -5,7 +5,7 @@ import { ReactComponent as KebabIcon } from '../icons/kebab.svg'; import '../sass/Navigation.scss'; interface LinkData { - to: string + to: string, label: string } diff --git a/sample-app/src/components/SearchBar.tsx b/sample-app/src/components/SearchBar.tsx index c1c0a8ee..1f6dabf0 100644 --- a/sample-app/src/components/SearchBar.tsx +++ b/sample-app/src/components/SearchBar.tsx @@ -8,7 +8,7 @@ import '../sass/Autocomplete.scss'; import LoadingIndicator from './LoadingIndicator'; interface Props { - placeholder?: string + placeholder?: string, isVertical: boolean } diff --git a/sample-app/src/components/StaticFilters.tsx b/sample-app/src/components/StaticFilters.tsx index 532fe838..06c0b846 100644 --- a/sample-app/src/components/StaticFilters.tsx +++ b/sample-app/src/components/StaticFilters.tsx @@ -14,7 +14,7 @@ interface FilterBoxProps { value: string, label: string }[], - title: string, + title: string } interface FiltersState { diff --git a/sample-app/src/components/utils/processTranslation.ts b/sample-app/src/components/utils/processTranslation.ts index d1e0f0fc..63f9fa1e 100644 --- a/sample-app/src/components/utils/processTranslation.ts +++ b/sample-app/src/components/utils/processTranslation.ts @@ -1,7 +1,7 @@ export function processTranslation(args: { phrase: string, pluralForm?: string, - count?: number, + count?: number }) { if (args.count && args.pluralForm && args.count >= 2) { return args.pluralForm diff --git a/sample-app/src/components/utils/renderWithHighlighting.tsx b/sample-app/src/components/utils/renderWithHighlighting.tsx index eff6384b..ba35f5a5 100644 --- a/sample-app/src/components/utils/renderWithHighlighting.tsx +++ b/sample-app/src/components/utils/renderWithHighlighting.tsx @@ -1,10 +1,10 @@ interface HighlightedValue { - value: string; + value: string, matchedSubstrings?: { - length: number; - offset: number; - }[]; + length: number, + offset: number + }[] } /** diff --git a/sample-app/src/icons/x.svg b/sample-app/src/icons/x.svg new file mode 100644 index 00000000..ee6f5a2e --- /dev/null +++ b/sample-app/src/icons/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/sample-app/src/react-app-env.d.ts b/sample-app/src/react-app-env.d.ts index 624c875e..a278f0d7 100644 --- a/sample-app/src/react-app-env.d.ts +++ b/sample-app/src/react-app-env.d.ts @@ -4,8 +4,8 @@ declare namespace NodeJS { interface ProcessEnv { - readonly NODE_ENV: 'development' | 'production' | 'test'; - readonly PUBLIC_URL: string; + readonly NODE_ENV: 'development' | 'production' | 'test', + readonly PUBLIC_URL: string } } diff --git a/sample-app/src/sass/AppliedFilters.scss b/sample-app/src/sass/AppliedFilters.scss index 008359ca..e35ecb37 100644 --- a/sample-app/src/sass/AppliedFilters.scss +++ b/sample-app/src/sass/AppliedFilters.scss @@ -8,6 +8,15 @@ display: flex; } + &__filterLabel, + &__filterValueComma { + margin-right: 5px; + } + + &__filterValue { + display: flex; + } + &__filterSeparator { margin: 0 10px; } @@ -16,5 +25,18 @@ &__filterValueComma { font-style: italic; } + + &__removeFilterButton { + margin: 0 5px; + width: .5625rem; + cursor: pointer; + } + + &__removableFilter { + margin: 0 5px; + display: flex; + justify-content: center; + background-color: #EFEFEF; + } } diff --git a/sample-app/src/utils/filterutils.tsx b/sample-app/src/utils/filterutils.tsx index ba59c9ce..1ba1678f 100644 --- a/sample-app/src/utils/filterutils.tsx +++ b/sample-app/src/utils/filterutils.tsx @@ -3,8 +3,8 @@ import { NearFilterValue, CombinedFilter, Filter } from '@yext/answers-core'; /** * Check if the object follows NearFilterValue interface */ -export function isNearFilterValue(obj: Object): obj is NearFilterValue { - return 'radius' in obj && 'lat' in obj && 'long' in obj; +export function isNearFilterValue(obj: any): obj is NearFilterValue { + return typeof obj === 'object' && 'radius' in obj && 'lat' in obj && 'long' in obj; } /** diff --git a/tests/components/appliedFilters.test.tsx b/tests/components/appliedFilters.test.tsx new file mode 100644 index 00000000..b1b37a76 --- /dev/null +++ b/tests/components/appliedFilters.test.tsx @@ -0,0 +1,126 @@ +import { act, render } from '@testing-library/react'; +import { provideAnswersHeadless } from '@yext/answers-headless'; +import { AnswersHeadlessContext } from '../../src'; +import DecoratedAppliedFilters from '../../sample-app/src/components/DecoratedAppliedFilters'; +import { Matcher } from '@yext/answers-core'; +import { useCallback } from 'react'; +import { verticalQueryResponseWithNlpFilters } from '../setup/responses/vertical-query'; + +describe('AppliedFilters component work as expected', () => { + + it('see that selected static filters appears and is removable', async () => { + const answers = createAnswersHeadless(); + const mockedFilter = { + fieldId: 'c_employeeCountry', + matcher: Matcher.Equals, + value: 'United States' + }; + + const MockedStaticFilter = () => { + const onChange = useCallback(() => { + answers.setFilter(null); + }, []); + return ; + }; + + const { container } = render( + + + + + ); + + act(() => answers.setQuery('someQuery')); + await act( () => answers.executeVerticalQuery()); + act(() => answers.setFilter(mockedFilter)); + + let filterLabels = container.getElementsByClassName('AppliedFilters__filterValueText'); + expect(filterLabels.length).toBe(1); + expect(filterLabels[0].innerHTML).toBe(mockedFilter.value); + const filerRemoveButton = container + .getElementsByClassName('AppliedFilters__removeFilterButton')[0] as HTMLElement; + expect(filerRemoveButton).toBeTruthy(); + + filerRemoveButton.click(); + filterLabels = container.getElementsByClassName('AppliedFilters__filterValueText'); + expect(filterLabels.length).toBe(0); + }); + + it('see that selected facet appears and is removable', async () => { + const answers = createAnswersHeadless(); + const mockedFacets = [ + { + fieldId: 'c_employeeDepartment', + displayName: 'Employee Department', + options: [{ + matcher: Matcher.Equals, + value: 'technology', + displayName: 'Technology', + count: 1, + selected: true + }] + } + ]; + + const { container } = render( + \ + + + ); + + act(() => answers.setQuery('someQuery')); + await act( () => answers.executeVerticalQuery()); + act(() => answers.setFacets(mockedFacets)); + + let facetLabels = container.getElementsByClassName('AppliedFilters__filterValueText'); + expect(facetLabels.length).toBe(1); + expect(facetLabels[0].innerHTML).toBe(mockedFacets[0].options[0].displayName); + const filerRemoveButton = container + .getElementsByClassName('AppliedFilters__removeFilterButton')[0] as HTMLElement; + expect(filerRemoveButton).toBeTruthy(); + + filerRemoveButton.click(); + facetLabels = container.getElementsByClassName('AppliedFilters__filterValueText'); + expect(facetLabels.length).toBe(0); + }); + + it('see that nlp filters appears and is not removable', async () => { + const answers = createAnswersHeadless(); + const { container } = render( + \ + + + ); + + act(() => answers.setQuery('resultsWithNlpFilter')); + await act( () => answers.executeVerticalQuery()); + + const nlpFilterLabels = container.getElementsByClassName('AppliedFilters__filterValueText'); + expect(nlpFilterLabels.length).toBe(1); + expect(nlpFilterLabels[0].innerHTML) + .toBe(verticalQueryResponseWithNlpFilters.response.appliedQueryFilters[0].displayValue); + const filerRemoveButton = container + .getElementsByClassName('AppliedFilters__removeFilterButton')[0] as HTMLElement; + expect(filerRemoveButton).toBeFalsy(); + }); + +}); + +function createAnswersHeadless() { + const answers = provideAnswersHeadless({ + apiKey: 'fake api key', + experienceKey: 'fake exp key', + locale: 'en', + }); + answers.setVerticalKey('fakeVerticalKey'); + return answers; +} \ No newline at end of file diff --git a/tests/setup/responses/vertical-query.ts b/tests/setup/responses/vertical-query.ts index e6530f3c..d6a55caa 100644 --- a/tests/setup/responses/vertical-query.ts +++ b/tests/setup/responses/vertical-query.ts @@ -1,6 +1,580 @@ import { v4 as uuid } from 'uuid'; -export const createVerticalQueryResponse = () => ({ +export const verticalQueryResponseWithNlpFilters = { + meta: { + uuid: "017cb8d9-2f76-1f19-5bb9-a2f931650de2", + errors: [] + }, + response: { + businessId: 3350634, + queryId: "9da068df-fd31-4f75-90b3-e0be900fe9f2", + resultsCount: 4, + results: [ + { + data: { + id: "Employee-2116", + type: "ce_person", + website: "http://www.test.com", + address: { + line1: "7900 Westpark Drive", + city: "Mclean", + region: "VA", + postalCode: "22102", + countryCode: "US" + }, + associations: [ + "Runners Association" + ], + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Mollis nunc sed id semper risus. Commodo elit at imperdiet dui accumsan sit amet nulla facilisi. Scelerisque viverra mauris in aliquam. A erat nam at lectus. Egestas purus viverra accumsan in nisl nisi scelerisque. Habitant morbi tristique senectus et netus. Congue nisi vitae suscipit tellus mauris a diam maecenas sed.", + name: "Amani Farooque", + cityCoordinate: { + latitude: 38.936519622802734, + longitude: -77.18428039550781 + }, + c_allstateLadyCTA: { + label: "Learn More", + linkType: "URL", + link: "http://yext.com" + }, + c_employeeCity: "Tysons Corner", + c_employeeCountry: "United States", + c_employeeDepartment: "Consulting", + c_employeeRegion: "Virginia", + c_employeeTitle: "Associate Technical Project Manager", + c_oliverCta: { + url: "https://www.yext.com/s/2287528/entity/edit2?entityIds=13411251", + icon: "yext", + label: "test cta" + }, + c_startDate: "2017-09-18", + displayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + emails: [ + "afarooque@yext.com", + "af@yext.com", + "aplomb@yext.com" + ], + firstName: "Amani", + geocodedCoordinate: { + latitude: 38.924654, + longitude: -77.216891 + }, + headshot: { + url: "http://a.mktgcdn.com/p/bmc3W88h2mMRXk-wHQ7dB0Em4rR_dfia6OVrAK3LjYU/192x191.png", + width: 192, + height: 191, + sourceUrl: "http://a.mktgcdn.com/p/bmc3W88h2mMRXk-wHQ7dB0Em4rR_dfia6OVrAK3LjYU/192x191.png" + }, + languages: [ + "German" + ], + lastName: "Farooque", + mainPhone: "+18003332222", + routableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + specialities: [ + "Documentation", + "Coding" + ], + websiteUrl: { + url: "http://www.test.com", + displayUrl: "http://www.testdisplay.com", + preferDisplayUrl: true + }, + yextDisplayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + yextRoutableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + uid: "18716057" + }, + highlightedFields: {}, + distance: 29722, + distanceFromFilter: 184938 + }, + { + data: { + id: "Employee-2143", + type: "ce_person", + address: { + line1: "7900 Westpark Drive", + city: "Mclean", + region: "VA", + postalCode: "22102", + countryCode: "US" + }, + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tristique senectus et netus et malesuada fames ac turpis. Porttitor eget dolor morbi non arcu risus quis. Tempor orci dapibus ultrices in iaculis. Viverra tellus in hac habitasse platea dictumst vestibulum rhoncus est. Eleifend donec pretium vulputate sapien nec. Ornare suspendisse sed nisi lacus sed viverra tellus in hac. Morbi tristique senectus et netus et malesuada. Vel pharetra vel turpis nunc eget lorem dolor sed viverra. Sapien nec sagittis aliquam malesuada.\n\nAt lectus urna duis convallis convallis tellus. Luctus venenatis lectus magna fringilla urna porttitor rhoncus. Blandit volutpat maecenas volutpat blandit aliquam etiam erat velit. Amet dictum sit amet justo donec enim diam vulputate ut. Ultrices in iaculis nunc sed augue lacus viverra. Feugiat sed lectus vestibulum mattis ullamcorper velit. Euismod quis viverra nibh cras pulvinar mattis nunc sed blandit. In hac habitasse platea dictumst quisque. In aliquam sem fringilla ut morbi tincidunt augue interdum velit. At erat pellentesque adipiscing commodo elit at.\n\nNunc aliquet bibendum enim facilisis gravida neque. Libero id faucibus nisl tincidunt eget nullam. Ullamcorper dignissim cras tincidunt lobortis. Condimentum id venenatis a condimentum. Sit amet dictum sit amet justo. Ac placerat vestibulum lectus mauris ultrices eros in. Cras sed felis eget velit aliquet sagittis id consectetur purus. Auctor urna nunc id cursus. Arcu non odio euismod lacinia at. Neque convallis a cras semper auctor neque vitae. Adipiscing elit pellentesque habitant morbi tristique. Commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Scelerisque eu ultrices vitae auctor eu augue ut lectus. In vitae turpis massa sed elementum. Nibh sit amet commodo nulla facilisi nullam vehicula ipsum a. Vitae suscipit tellus mauris a diam maecenas sed enim. Volutpat blandit aliquam etiam erat velit scelerisque.", + name: "Tom Meyer", + cityCoordinate: { + latitude: 38.936519622802734, + longitude: -77.18428039550781 + }, + c_allstateLadyCTA: { + label: "Learn More", + linkType: "URL", + link: "http://yext.com" + }, + c_employeeCity: "Tysons Corner", + c_employeeCountry: "United States", + c_employeeDepartment: "Technology", + c_employeeRegion: "Virginia", + c_employeeTitle: "Software Engineer", + c_myRichTextField: "++Underlined stuff++ \n**Bold stuff**\n\n1. One\n2. Two\n3. *Three*", + c_popularity: "1.0", + c_startDate: "2017-09-05", + displayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + emails: [ + "tmeyer@yext.com" + ], + firstName: "Tom", + geocodedCoordinate: { + latitude: 38.924654, + longitude: -77.216891 + }, + headshot: { + url: "https://a.mktgcdn.com/p/cb9jR0A19ADuKc7ExlWmHaDchPW_61IR5TZ5G7NApjY/139x140.jpg", + width: 139, + height: 140, + sourceUrl: "https://a.mktgcdn.com/p/cb9jR0A19ADuKc7ExlWmHaDchPW_61IR5TZ5G7NApjY/139x140.jpg" + }, + lastName: "Meyer", + routableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + yextDisplayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + yextRoutableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + uid: "18716864" + }, + highlightedFields: {}, + distance: 29722, + distanceFromFilter: 184938 + }, + { + data: { + id: "4880570941877725281", + type: "ce_person", + outdoorPoolCount: 5, + time: { + start: "2020-04-08T13:00", + end: "2020-04-15T14:00" + }, + address: { + line1: "7900 Westpark Drive", + city: "Mclean", + region: "VA", + postalCode: "22102", + countryCode: "US" + }, + description: "hai hai kazuma-desu the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team the cowboys are the best team", + name: "Oliver Shi", + cityCoordinate: { + latitude: 38.936519622802734, + longitude: -77.18428039550781 + }, + c_allstateLadyCTA: { + label: "Learn More", + linkType: "URL", + link: "http://yext.com" + }, + c_myRichTextField: "++Underlined++ \n**Bolded**\n\n1. **One**\n2. *Two*\n3. Three\n\n\\\\u003cdiv\\\\u003eDang\\\\u003c/div\\\\u003e", + c_nestedBoy: { + waifus: { + anime: { + a: "waifu", + b: "koko" + }, + games: { + c: "waifu", + d: "hi" + }, + life: { + e: "kaga", + f: "baba" + } + }, + husbandos: { + a: { + a: "yaga", + b: "haha" + }, + b: { + c: "lala", + d: "kaka" + }, + c: { + e: "kaga", + f: "lala" + } + } + }, + c_nestedTextLists: [ + { + textlist: [ + "weeb", + "lord", + "poggers" + ], + struct: { + nested_list1: [ + "weeber", + "weeb" + ], + nested_list2: [ + "weeb", + "weweb weeb weeb", + "haha ohaiyo", + "weeber" + ] + } + } + ], + c_oliverCta: { + url: "https://google.com", + icon: "yext", + label: "please update" + }, + c_secondCTA: true, + c_startDate: "2019-08-05", + c_string1: "stringy", + c_terminationDate: "2020-01-01", + displayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + firstName: "Oliver", + gender: "Male", + geocodedCoordinate: { + latitude: 38.924654, + longitude: -77.216891 + }, + headshot: { + url: "https://a.mktgcdn.com/p/3Obc6RedOylBSTOj_DOT8BHMwp6_-ltsE0lCrZS6PGc/1200x1200.jpg", + width: 1200, + height: 1200, + sourceUrl: "https://a.mktgcdn.com/p/3Obc6RedOylBSTOj_DOT8BHMwp6_-ltsE0lCrZS6PGc/1200x1200.jpg", + thumbnails: [ + { + url: "https://a.mktgcdn.com/p/3Obc6RedOylBSTOj_DOT8BHMwp6_-ltsE0lCrZS6PGc/619x619.jpg", + width: 619, + height: 619 + }, + { + url: "https://a.mktgcdn.com/p/3Obc6RedOylBSTOj_DOT8BHMwp6_-ltsE0lCrZS6PGc/450x450.jpg", + width: 450, + height: 450 + }, + { + url: "https://a.mktgcdn.com/p/3Obc6RedOylBSTOj_DOT8BHMwp6_-ltsE0lCrZS6PGc/196x196.jpg", + width: 196, + height: 196 + } + ] + }, + keywords: [ + "otaku", + "aKeyword", + "highlight me!" + ], + languages: [ + "english", + "englangdo", + "chinese" + ], + lastName: "Shi", + routableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + yextDisplayCoordinate: { + latitude: 38.924648, + longitude: -77.216859 + }, + yextRoutableCoordinate: { + latitude: 38.9242966, + longitude: -77.2177549 + }, + uid: "18717130" + }, + highlightedFields: {}, + distance: 29722, + distanceFromFilter: 184938 + }, + { + data: { + id: "Employee-718", + type: "ce_person", + address: { + line1: "1481 N Dupont Hwy", + city: "Dover", + region: "DE", + postalCode: "19901", + countryCode: "US" + }, + name: "Mike Peralta", + cityCoordinate: { + latitude: 39.161544, + longitude: -75.513578 + }, + c_allstateLadyCTA: { + label: "Learn More", + linkType: "URL", + link: "http://yext.com" + }, + c_employeeCountry: "United States", + c_employeeDepartment: "Business Applications", + c_employeeRegion: "New York", + c_employeeTitle: "Program Manager, Business Insights", + c_startDate: "2013-07-30", + displayCoordinate: { + latitude: 39.1940793, + longitude: -75.5465376 + }, + emails: [ + "mperalta@yext.com" + ], + firstName: "Mike", + geocodedCoordinate: { + latitude: 39.1940793, + longitude: -75.5465376 + }, + headshot: { + url: "http://a.mktgcdn.com/p/8a6ix28DnWPS4CrO0GTRs3WFjvlBdLAkTs8UqgXZdQI/400x400.jpg", + width: 400, + height: 400, + sourceUrl: "http://a.mktgcdn.com/p/8a6ix28DnWPS4CrO0GTRs3WFjvlBdLAkTs8UqgXZdQI/400x400.jpg", + thumbnails: [ + { + url: "http://a.mktgcdn.com/p/8a6ix28DnWPS4CrO0GTRs3WFjvlBdLAkTs8UqgXZdQI/196x196.jpg", + width: 196, + height: 196 + } + ] + }, + lastName: "Peralta", + routableCoordinate: { + latitude: 39.1938245, + longitude: -75.54686 + }, + yextDisplayCoordinate: { + latitude: 39.1940793, + longitude: -75.5465376 + }, + yextRoutableCoordinate: { + latitude: 39.1938245, + longitude: -75.54686 + }, + uid: "18717550" + }, + highlightedFields: {}, + distance: 173038, + distanceFromFilter: 316300 + } + ], + appliedQueryFilters: [ + { + displayKey: "Location", + displayValue: "Virginia", + filter: { + "builtin.location": { + $eq: "P-region.7919684583758790" + } + }, + type: "PLACE", + details: { + latitude: 37.677592044, + longitude: -78.6190526172645, + placeName: "Virginia, United States", + featureTypes: [ + "region" + ], + boundingBox: { + minLatitude: 36.540855, + minLongitude: -83.6753959969438, + maxLatitude: 39.4660129984577, + maxLongitude: -75.165704098375 + } + } + } + ], + facets: [ + { + fieldId: "c_puppyPreference", + displayName: "Puppy Preference", + options: [] + }, + { + fieldId: "brands", + displayName: "Brands", + options: [] + }, + { + fieldId: "c_employeeDepartment", + displayName: "Employee Department", + options: [ + { + displayName: "Business Applications", + count: 1, + selected: false, + filter: { + c_employeeDepartment: { + $eq: "Business Applications" + } + } + }, + { + displayName: "Consulting", + count: 1, + selected: false, + filter: { + c_employeeDepartment: { + $eq: "Consulting" + } + } + }, + { + displayName: "Technology", + count: 1, + selected: false, + filter: { + c_employeeDepartment: { + $eq: "Technology" + } + } + } + ] + }, + { + fieldId: "c_popularity", + displayName: "Popularity", + options: [] + }, + { + fieldId: "c_terminationDate", + displayName: "Termination Date", + options: [] + }, + { + fieldId: "c_cellPhone", + displayName: "Cell Phone", + options: [] + }, + { + fieldId: "c_startDate", + displayName: "Start Date", + options: [] + }, + { + fieldId: "languages", + displayName: "Languages", + options: [ + { + displayName: "chinese", + count: 1, + selected: false, + filter: { + languages: { + $eq: "chinese" + } + } + }, + { + displayName: "englangdo", + count: 1, + selected: false, + filter: { + languages: { + $eq: "englangdo" + } + } + }, + { + displayName: "english", + count: 1, + selected: false, + filter: { + languages: { + $eq: "english" + } + } + }, + { + displayName: "German", + count: 1, + selected: false, + filter: { + languages: { + $eq: "German" + } + } + } + ] + }, + { + fieldId: "services", + displayName: "Services", + options: [] + }, + { + fieldId: "specialities", + displayName: "Specialties", + options: [ + { + displayName: "Coding", + count: 1, + selected: false, + filter: { + specialities: { + $eq: "Coding" + } + } + }, + { + displayName: "Documentation", + count: 1, + selected: false, + filter: { + specialities: { + $eq: "Documentation" + } + } + } + ] + }, + { + fieldId: "products", + displayName: "Products", + options: [] + } + ], + source: "KNOWLEDGE_MANAGER", + searchIntents: [], + locationBias: { + latitude: 39.0437, + longitude: -77.4875, + locationDisplayName: "Ashburn, Virginia, United States", + accuracy: "IP" + } + } +} + +export const verticalQueryResponse = { meta: { uuid: uuid(), errors: [] @@ -64,4 +638,4 @@ export const createVerticalQueryResponse = () => ({ accuracy: 'IP' } } -}); +}; diff --git a/tests/setup/server.ts b/tests/setup/server.ts index c5e1c430..72969c2a 100644 --- a/tests/setup/server.ts +++ b/tests/setup/server.ts @@ -1,14 +1,22 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { createVerticalQueryResponse } from './responses/vertical-query'; +import { verticalQueryResponse, verticalQueryResponseWithNlpFilters } from './responses/vertical-query'; import { universalQueryResponse, universalQueryResponseWithFilters } from './responses/universal-query'; // Any unhandled requests are dropped and logged as warnings const handlers = [ rest.get(/answers\/vertical\/query/, (req, res, ctx) => { - return res( - ctx.json(createVerticalQueryResponse()), - ); + const input = req.url.searchParams.get('input'); + switch(input) { + case 'resultsWithNlpFilter': + return res( + ctx.json(verticalQueryResponseWithNlpFilters), + ); + default: + return res( + ctx.json(verticalQueryResponse), + ); + } }), rest.get(/answers\/query/, (req, res, ctx) => { const input = req.url.searchParams.get('input'); diff --git a/tsconfig.json b/tsconfig.json index d224776f..162069e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "@yext/answers-headless-react": ["src"] + }, "target": "es2015", "outDir": "lib", "esModuleInterop": true, @@ -15,6 +19,7 @@ "jsx": "react-jsx" }, "include": [ - "src" + "src", + "react-app.d.ts" ] } \ No newline at end of file