Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1.5.0 #428

Merged
merged 16 commits into from
Feb 23, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -6,9 +6,36 @@ name: Run Tests
on: [push, pull_request]

jobs:
call_run_tests-react-18:
uses: yext/slapshot-reusable-workflows/.github/workflows/run_tests.yml@v1
with:
node_matrix: '["16.x", "18.x"]'
secrets:
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}

call_run_tests-react-17:
uses: yext/slapshot-reusable-workflows/.github/workflows/run_tests.yml@v1
with:
# We have to install these swc libraries manually because
# the post install script doesn't seem to run properly
# after we install specific versions of packages.
# More info at https://github.com/swc-project/swc/issues/5616#issuecomment-1651214641
build_script: |
npm i \
react@^17.0.2 \
react-dom@^17.0.2 \
@testing-library/react@^12.1.3 \
@testing-library/react-hooks@^8.0.0
npm install --save-optional \
"@swc/core-linux-arm-gnueabihf" \
"@swc/core-linux-arm64-gnu" \
"@swc/core-linux-arm64-musl" \
"@swc/core-linux-x64-gnu" \
"@swc/core-linux-x64-musl" \
"@swc/core-win32-arm64-msvc" \
"@swc/core-win32-ia32-msvc" \
"@swc/core-win32-x64-msvc"
npm run build
node_matrix: '["16.x", "18.x"]'
secrets:
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
@@ -17,7 +44,20 @@ jobs:
uses: yext/slapshot-reusable-workflows/.github/workflows/run_tests.yml@v1
with:
build_script: |
npm i -D react@16.14 react-dom@16.14
npm i \
react@^16.14 \
react-dom@^16.14 \
@testing-library/react@^12.1.3 \
@testing-library/react-hooks@^8.0.0
npm install --save-optional \
"@swc/core-linux-arm-gnueabihf" \
"@swc/core-linux-arm64-gnu" \
"@swc/core-linux-arm64-musl" \
"@swc/core-linux-x64-gnu" \
"@swc/core-linux-x64-musl" \
"@swc/core-win32-arm64-msvc" \
"@swc/core-win32-ia32-msvc" \
"@swc/core-win32-x64-msvc"
npm run build
node_matrix: '["16.x", "18.x"]'
secrets:
4 changes: 3 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import React from 'react';

const config: StorybookConfig = {
stories: [
@@ -39,7 +40,8 @@ const config: StorybookConfig = {
'./SearchCore': require.resolve('../tests/__fixtures__/core/SearchCore.ts'),
'../utils/location-operations': require.resolve('../tests/__fixtures__/utils/location-operations.ts')
},
}
},
...(!React.version.startsWith('18') && { externals: ["react-dom/client"] })
}),

env: (config) => {
Binary file modified .storybook/snapshots/__snapshots__/mapboxmap--custom-pin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .storybook/snapshots/__snapshots__/mapboxmap--primary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 19 additions & 50 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
@@ -328,36 +328,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

-----------

The following NPM packages may be included in this product:

- @reach/auto-id@0.18.0
- @reach/utils@0.18.0

These packages each contain the following license and notice below:

The MIT License (MIT)

Copyright (c) 2018-2022, React Training LLC

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-aria/ssr@3.8.0
@@ -2777,7 +2747,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI

The following NPM package may be included in this product:

- nanoid@3.3.6
- nanoid@3.3.7

This package contains the following license and notice below:

@@ -3195,7 +3165,7 @@ OTHER DEALINGS IN THE SOFTWARE.

The following NPM package may be included in this product:

- postcss@8.4.30
- postcss@8.4.35

This package contains the following license and notice below:

@@ -3357,40 +3327,39 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI

The following NPM package may be included in this product:

- react-collapsed@3.6.0
- react-collapsed@4.1.2

This package contains the following license and notice below:

MIT License
The MIT License (MIT)

Copyright (c) 2019-2020 Rogin Farrer
Copyright (c) 2019-2023, Rogin Farrer

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:
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.
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 packages may be included in this product:

- react-dom@17.0.2
- react-dom@18.2.0
- react-is@16.13.1
- react@17.0.2
- scheduler@0.20.2
- react@18.2.0
- scheduler@0.23.0
- use-sync-external-store@1.2.0

These packages each contain the following license and notice below:
4 changes: 2 additions & 2 deletions docs/search-ui-react.filtersearch.md
Original file line number Diff line number Diff line change
@@ -9,14 +9,14 @@ A component which allows a user to search for filters associated with specific e
**Signature:**

```typescript
declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| { searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | |
| { searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | |

**Returns:**

1 change: 1 addition & 0 deletions docs/search-ui-react.filtersearchprops.md
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ interface FilterSearchProps
| --- | --- | --- | --- |
| [customCssClasses?](./search-ui-react.filtersearchprops.customcssclasses.md) | | [FilterSearchCssClasses](./search-ui-react.filtersearchcssclasses.md) | _(Optional)_ CSS classes for customizing the component styling. |
| [label?](./search-ui-react.filtersearchprops.label.md) | | string | _(Optional)_ The display label for the component. |
| [onDropdownInputChange?](./search-ui-react.filtersearchprops.ondropdowninputchange.md) | | (params: [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md)<!-- -->) =&gt; void | _(Optional)_ A function which is called when the input element's value changes. Replaces the default behavior. |
| [onSelect?](./search-ui-react.filtersearchprops.onselect.md) | | (params: [OnSelectParams](./search-ui-react.onselectparams.md)<!-- -->) =&gt; void | _(Optional)_ A function which is called when a filter is selected. |
| [placeholder?](./search-ui-react.filtersearchprops.placeholder.md) | | string | _(Optional)_ The search input's placeholder text when no text has been entered by the user. Defaults to "Search here...". |
| [searchFields](./search-ui-react.filtersearchprops.searchfields.md) | | Omit&lt;SearchParameterField, 'fetchEntities'&gt;\[\] | An array of fieldApiName and entityType which indicates what to perform the filter search against. |
13 changes: 13 additions & 0 deletions docs/search-ui-react.filtersearchprops.ondropdowninputchange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [FilterSearchProps](./search-ui-react.filtersearchprops.md) &gt; [onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md)

## FilterSearchProps.onDropdownInputChange property

A function which is called when the input element's value changes. Replaces the default behavior.

**Signature:**

```typescript
onDropdownInputChange?: (params: OnDropdownInputChangeProps) => void;
```
4 changes: 2 additions & 2 deletions docs/search-ui-react.mapboxmap.md
Original file line number Diff line number Diff line change
@@ -9,14 +9,14 @@ A component that renders a map with markers to show result locations using Mapbo
**Signature:**

```typescript
declare function MapboxMap<T>({ mapboxAccessToken, mapboxOptions, PinComponent, getCoordinate, onDrag }: MapboxMapProps<T>): JSX.Element;
declare function MapboxMap<T>({ mapboxAccessToken, mapboxOptions, PinComponent, renderPin, getCoordinate, onDrag }: MapboxMapProps<T>): JSX.Element;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| { mapboxAccessToken, mapboxOptions, PinComponent, getCoordinate, onDrag } | [MapboxMapProps](./search-ui-react.mapboxmapprops.md)<!-- -->&lt;T&gt; | |
| { mapboxAccessToken, mapboxOptions, PinComponent, renderPin, getCoordinate, onDrag } | [MapboxMapProps](./search-ui-react.mapboxmapprops.md)<!-- -->&lt;T&gt; | |

**Returns:**

3 changes: 2 additions & 1 deletion docs/search-ui-react.mapboxmapprops.md
Original file line number Diff line number Diff line change
@@ -20,5 +20,6 @@ interface MapboxMapProps<T>
| [mapboxAccessToken](./search-ui-react.mapboxmapprops.mapboxaccesstoken.md) | | string | Mapbox access token. |
| [mapboxOptions?](./search-ui-react.mapboxmapprops.mapboxoptions.md) | | Omit&lt;mapboxgl.MapboxOptions, 'container'&gt; | _(Optional)_ Interface for map customization derived from Mapbox GL's Map options. |
| [onDrag?](./search-ui-react.mapboxmapprops.ondrag.md) | | [OnDragHandler](./search-ui-react.ondraghandler.md) | _(Optional)_ A function which is called when user drag the map. |
| [PinComponent?](./search-ui-react.mapboxmapprops.pincomponent.md) | | [PinComponent](./search-ui-react.pincomponent.md)<!-- -->&lt;T&gt; | _(Optional)_ Custom Pin component to render for markers on the map. By default, the built-in marker image from Mapbox GL is used. |
| [PinComponent?](./search-ui-react.mapboxmapprops.pincomponent.md) | | [PinComponent](./search-ui-react.pincomponent.md)<!-- -->&lt;T&gt; | _(Optional)_ Custom Pin component to render for markers on the map. By default, the built-in marker image from Mapbox GL is used. This prop should not be used with [renderPin](./search-ui-react.mapboxmapprops.renderpin.md)<!-- -->. If both are provided, only PinComponent will be used. |
| [renderPin?](./search-ui-react.mapboxmapprops.renderpin.md) | | (props: [PinComponentProps](./search-ui-react.pincomponentprops.md)<!-- -->&lt;T&gt; &amp; { container: HTMLElement; }) =&gt; void | _(Optional)_ Render function for a custom marker on the map. This function takes in an HTML element and is responible for rendering the pin into that element, which will be used as the marker. By default, the built-in marker image from Mapbox GL is used. This prop should not be used with [PinComponent](./search-ui-react.mapboxmapprops.pincomponent.md)<!-- -->. If both are provided, only PinComponent will be used. |

2 changes: 1 addition & 1 deletion docs/search-ui-react.mapboxmapprops.pincomponent.md
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

## MapboxMapProps.PinComponent property

Custom Pin component to render for markers on the map. By default, the built-in marker image from Mapbox GL is used.
Custom Pin component to render for markers on the map. By default, the built-in marker image from Mapbox GL is used. This prop should not be used with [renderPin](./search-ui-react.mapboxmapprops.renderpin.md)<!-- -->. If both are provided, only PinComponent will be used.

**Signature:**

15 changes: 15 additions & 0 deletions docs/search-ui-react.mapboxmapprops.renderpin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [MapboxMapProps](./search-ui-react.mapboxmapprops.md) &gt; [renderPin](./search-ui-react.mapboxmapprops.renderpin.md)

## MapboxMapProps.renderPin property

Render function for a custom marker on the map. This function takes in an HTML element and is responible for rendering the pin into that element, which will be used as the marker. By default, the built-in marker image from Mapbox GL is used. This prop should not be used with [PinComponent](./search-ui-react.mapboxmapprops.pincomponent.md)<!-- -->. If both are provided, only PinComponent will be used.

**Signature:**

```typescript
renderPin?: (props: PinComponentProps<T> & {
container: HTMLElement;
}) => void;
```
6 changes: 4 additions & 2 deletions docs/search-ui-react.md
Original file line number Diff line number Diff line change
@@ -18,15 +18,15 @@
| [executeSearch(searchActions)](./search-ui-react.executesearch.md) | Executes a universal/vertical search. |
| [Facets(props)](./search-ui-react.facets.md) | A component that displays all facets applicable to the current vertical search. |
| [FilterDivider({ className })](./search-ui-react.filterdivider.md) | A divider component used to separate NumericalFacets, HierarchicalFacets, StandardFacets, and StaticFilters. |
| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. |
| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. |
| [Geolocation\_2({ geolocationOptions, radius, label, GeolocationIcon, handleClick, customCssClasses, })](./search-ui-react.geolocation_2.md) | A React Component which collects location information to create a location filter and perform a new search. |
| [getSearchIntents(searchActions)](./search-ui-react.getsearchintents.md) | Get search intents of the current query stored in headless using autocomplete request. |
| [getUserLocation(geolocationOptions)](./search-ui-react.getuserlocation.md) | Retrieves user's location using navigator.geolocation API. |
| [HierarchicalFacet(props)](./search-ui-react.hierarchicalfacet.md) | A component that displays a single hierarchical facet, in a tree level structure, applicable to the current vertical search. Use this to override the default rendering. |
| [HierarchicalFacets({ searchOnChange, collapsible, defaultExpanded, includedFieldIds, customCssClasses, delimiter, showMoreLimit })](./search-ui-react.hierarchicalfacets.md) | A component that displays hierarchical facets, in a tree level structure, applicable to the current vertical search. |
| [isCtaData(data)](./search-ui-react.isctadata.md) | Type guard for CtaData. |
| [LocationBias({ geolocationOptions, customCssClasses })](./search-ui-react.locationbias.md) | A React Component which displays and collects location information in order to bias searches. |
| [MapboxMap({ mapboxAccessToken, mapboxOptions, PinComponent, getCoordinate, onDrag })](./search-ui-react.mapboxmap.md) | A component that renders a map with markers to show result locations using Mapbox GL. |
| [MapboxMap({ mapboxAccessToken, mapboxOptions, PinComponent, renderPin, getCoordinate, onDrag })](./search-ui-react.mapboxmap.md) | A component that renders a map with markers to show result locations using Mapbox GL. |
| [NumericalFacet(props)](./search-ui-react.numericalfacet.md) | A component that displays a single numerical facet. Use this to override the default rendering. |
| [NumericalFacets({ searchOnChange, includedFieldIds, getFilterDisplayName, inputPrefix, customCssClasses, ...filterGroupProps })](./search-ui-react.numericalfacets.md) | A component that displays numerical facets applicable to the current vertical search. |
| [Pagination(props)](./search-ui-react.pagination.md) | Renders a component that divide a series of vertical results into chunks across multiple pages and enable user to navigate between those pages. |
@@ -84,6 +84,7 @@
| [NumericalFacetProps](./search-ui-react.numericalfacetprops.md) | Props for the [StandardFacet()](./search-ui-react.standardfacet.md) component. |
| [NumericalFacetsCssClasses](./search-ui-react.numericalfacetscssclasses.md) | The CSS class interface for [NumericalFacets()](./search-ui-react.numericalfacets.md)<!-- -->. |
| [NumericalFacetsProps](./search-ui-react.numericalfacetsprops.md) | Props for the [NumericalFacets()](./search-ui-react.numericalfacets.md) component. |
| [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) | The parameters that are passed into [FilterSearchProps.onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md)<!-- -->. |
| [OnSelectParams](./search-ui-react.onselectparams.md) | The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md)<!-- -->. |
| [PaginationCssClasses](./search-ui-react.paginationcssclasses.md) | The CSS classes used for pagination. |
| [PaginationProps](./search-ui-react.paginationprops.md) | Props for [Pagination()](./search-ui-react.pagination.md) component |
@@ -141,6 +142,7 @@
| [OnDragHandler](./search-ui-react.ondraghandler.md) | A function which is called when user drag the map. |
| [onSearchFunc](./search-ui-react.onsearchfunc.md) | The interface of a function which is called on a search. |
| [PinComponent](./search-ui-react.pincomponent.md) | A functional component that can be used to render a custom marker on the map. |
| [PinComponentProps](./search-ui-react.pincomponentprops.md) | Props for rendering a custom marker on the map. |
| [RenderEntityPreviews](./search-ui-react.renderentitypreviews.md) | The type of a functional React component which renders entity previews using a map of vertical key to the corresponding VerticalResults data. |
| [SectionComponent](./search-ui-react.sectioncomponent.md) | A component that can be used to render a section template for vertical results. |
| [StaticFilterOptionConfig](./search-ui-react.staticfilteroptionconfig.md) | The configuration data for a field value static filter option. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) &gt; [executeFilterSearch](./search-ui-react.ondropdowninputchangeprops.executefiltersearch.md)

## OnDropdownInputChangeProps.executeFilterSearch property

A function that executes a filter search and updates the input and dropdown options with the response.

**Signature:**

```typescript
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>;
```
21 changes: 21 additions & 0 deletions docs/search-ui-react.ondropdowninputchangeprops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md)

## OnDropdownInputChangeProps interface

The parameters that are passed into [FilterSearchProps.onDropdownInputChange](./search-ui-react.filtersearchprops.ondropdowninputchange.md)<!-- -->.

**Signature:**

```typescript
interface OnDropdownInputChangeProps
```

## Properties

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [executeFilterSearch](./search-ui-react.ondropdowninputchangeprops.executefiltersearch.md) | | (query?: string) =&gt; Promise&lt;FilterSearchResponse \| undefined&gt; | A function that executes a filter search and updates the input and dropdown options with the response. |
| [value](./search-ui-react.ondropdowninputchangeprops.value.md) | | string | The input element's new value after the change |

13 changes: 13 additions & 0 deletions docs/search-ui-react.ondropdowninputchangeprops.value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [OnDropdownInputChangeProps](./search-ui-react.ondropdowninputchangeprops.md) &gt; [value](./search-ui-react.ondropdowninputchangeprops.value.md)

## OnDropdownInputChangeProps.value property

The input element's new value after the change

**Signature:**

```typescript
value: string;
```
8 changes: 3 additions & 5 deletions docs/search-ui-react.pincomponent.md
Original file line number Diff line number Diff line change
@@ -9,9 +9,7 @@ A functional component that can be used to render a custom marker on the map.
**Signature:**

```typescript
type PinComponent<T> = (props: {
index: number;
mapbox: mapboxgl.Map;
result: Result<T>;
}) => JSX.Element;
type PinComponent<T> = (props: PinComponentProps<T>) => JSX.Element;
```
**References:** [PinComponentProps](./search-ui-react.pincomponentprops.md)

17 changes: 17 additions & 0 deletions docs/search-ui-react.pincomponentprops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/search-ui-react](./search-ui-react.md) &gt; [PinComponentProps](./search-ui-react.pincomponentprops.md)

## PinComponentProps type

Props for rendering a custom marker on the map.

**Signature:**

```typescript
type PinComponentProps<T> = {
index: number;
mapbox: mapboxgl.Map;
result: Result<T>;
};
```
21 changes: 17 additions & 4 deletions etc/search-ui-react.api.md
Original file line number Diff line number Diff line change
@@ -268,7 +268,7 @@ export interface FilterOptionConfig {
}

// @public
export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;
export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, onDropdownInputChange, sectioned, customCssClasses }: FilterSearchProps): JSX.Element;

// @public
export interface FilterSearchCssClasses extends AutocompleteResultCssClasses {
@@ -290,6 +290,7 @@ export interface FilterSearchCssClasses extends AutocompleteResultCssClasses {
export interface FilterSearchProps {
customCssClasses?: FilterSearchCssClasses;
label?: string;
onDropdownInputChange?: (params: OnDropdownInputChangeProps) => void;
onSelect?: (params: OnSelectParams) => void;
placeholder?: string;
searchFields: Omit<SearchParameterField, 'fetchEntities'>[];
@@ -417,7 +418,7 @@ export interface LocationBiasProps {
}

// @public
export function MapboxMap<T>({ mapboxAccessToken, mapboxOptions, PinComponent, getCoordinate, onDrag }: MapboxMapProps<T>): JSX.Element;
export function MapboxMap<T>({ mapboxAccessToken, mapboxOptions, PinComponent, renderPin, getCoordinate, onDrag }: MapboxMapProps<T>): JSX.Element;

// @public
export interface MapboxMapProps<T> {
@@ -426,6 +427,9 @@ export interface MapboxMapProps<T> {
mapboxOptions?: Omit<mapboxgl_2.MapboxOptions, 'container'>;
onDrag?: OnDragHandler;
PinComponent?: PinComponent<T>;
renderPin?: (props: PinComponentProps<T> & {
container: HTMLElement;
}) => void;
}

// @public
@@ -461,6 +465,12 @@ export interface NumericalFacetsProps extends Omit<StandardFacetsProps, 'exclude
// @public
export type OnDragHandler = (center: mapboxgl_2.LngLat, bounds: mapboxgl_2.LngLatBounds) => void;

// @public
export interface OnDropdownInputChangeProps {
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>;
value: string;
}

// @public
export type onSearchFunc = (searchEventData: {
verticalKey?: string;
@@ -504,11 +514,14 @@ export interface PaginationProps {
}

// @public
export type PinComponent<T> = (props: {
export type PinComponent<T> = (props: PinComponentProps<T>) => JSX.Element;

// @public
export type PinComponentProps<T> = {
index: number;
mapbox: mapboxgl_2.Map;
result: Result<T>;
}) => JSX.Element;
};

// @public
export interface RangeInputCssClasses {
6,033 changes: 4,567 additions & 1,466 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yext/search-ui-react",
"version": "1.4.1",
"version": "1.5.0",
"description": "A library of React Components for powering Yext Search integrations",
"author": "slapshot@yext.com",
"license": "BSD-3-Clause",
@@ -75,9 +75,8 @@
"@storybook/test-runner": "^0.15.2",
"@storybook/testing-library": "^0.2.1",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^13.5.0",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/classnames": "^2.3.1",
"@types/jest": "^29.1.0",
"@types/jest-image-snapshot": "^6.2.3",
@@ -99,18 +98,19 @@
"jest-environment-jsdom": "^29.1.1",
"jest-image-snapshot": "^6.2.0",
"msw": "^0.36.8",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^7.4.5",
"tailwindcss": "^3.0.23",
"ts-jest": "^29.1.1",
"tsup": "^8.0.1",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"util": "^0.12.5"
},
"peerDependencies": {
"@yext/search-headless-react": "^2.4.0",
"react": "^16.14 || ^17",
"react-dom": "^16.14 || ^17"
"react": "^16.14 || ^17 || ^18",
"react-dom": "^16.14 || ^17 || ^18"
},
"jest": {
"bail": 0,
@@ -153,7 +153,6 @@
"restoreMocks": true
},
"dependencies": {
"@reach/auto-id": "^0.18.0",
"@restart/ui": "^1.0.1",
"@tailwindcss/forms": "^0.5.0",
"@yext/analytics": "^0.6.5",
@@ -162,7 +161,7 @@
"lodash-es": "^4.17.21",
"mapbox-gl": "^2.9.2",
"prop-types": "^15.8.1",
"react-collapsed": "3.6.0",
"react-collapsed": "^4.1.2",
"recent-searches": "^1.0.5",
"tailwind-merge": "^1.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
7 changes: 2 additions & 5 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -16,13 +16,10 @@ import { FocusContext, FocusContextType } from './FocusContext';
import { ScreenReader } from '../ScreenReader';
import { recursivelyMapChildren } from '../utils/recursivelyMapChildren';
import { DropdownItem, DropdownItemProps, DropdownItemWithIndex } from './DropdownItem';
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
import { useId } from '@reach/auto-id';
import { useLayoutEffect } from '../../hooks/useLayoutEffect';
import { useId } from '../../hooks/useId';

const useRootClose = typeof useRootClosePkg === 'function' ? useRootClosePkg : useRootClosePkg['default'];
const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function'
? useIsomorphicLayoutEffect
: useIsomorphicLayoutEffect['default'];

interface DropdownItemData {
value: string,
30 changes: 29 additions & 1 deletion src/components/FilterSearch.tsx
Original file line number Diff line number Diff line change
@@ -55,6 +55,21 @@ export interface OnSelectParams {
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>
}

/**
* The parameters that are passed into {@link FilterSearchProps.onDropdownInputChange}.
*
* @public
*/
export interface OnDropdownInputChangeProps {
/** The input element's new value after the change */
value: string,
/**
* A function that executes a filter search and updates the input and dropdown options
* with the response.
*/
executeFilterSearch: (query?: string) => Promise<FilterSearchResponse | undefined>
}

/**
* The props for the {@link FilterSearch} component.
*
@@ -78,6 +93,8 @@ export interface FilterSearchProps {
searchOnSelect?: boolean,
/** A function which is called when a filter is selected. */
onSelect?: (params: OnSelectParams) => void,
/** A function which is called when the input element's value changes. Replaces the default behavior. */
onDropdownInputChange?: (params: OnDropdownInputChangeProps) => void,
/** Determines whether or not the results of the filter search are separated by field. Defaults to false. */
sectioned?: boolean,
/** CSS classes for customizing the component styling. */
@@ -98,6 +115,7 @@ export function FilterSearch({
placeholder = 'Search here...',
searchOnSelect,
onSelect,
onDropdownInputChange,
sectioned = false,
customCssClasses
}: FilterSearchProps): JSX.Element {
@@ -224,6 +242,16 @@ export function FilterSearch({
matchingFieldIds
]);

const handleInputChange = useCallback((value) => {
onDropdownInputChange ? onDropdownInputChange({
value,
executeFilterSearch
}) : executeFilterSearch(value);
}, [
onDropdownInputChange,
executeFilterSearch
]);

const meetsSubmitCritera = useCallback(index => index >= 0, []);

const itemDataMatrix = useMemo(() => {
@@ -279,7 +307,7 @@ export function FilterSearch({
<DropdownInput
className={cssClasses.inputElement}
placeholder={placeholder}
onChange={executeFilterSearch}
onChange={handleInputChange}
onFocus={handleInputFocus}
submitCriteria={meetsSubmitCritera}
/>
12 changes: 9 additions & 3 deletions src/components/Filters/CheckboxOption.tsx
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import { useFiltersContext } from './FiltersContext';
import { useFilterGroupContext } from './FilterGroupContext';
import { useComposedCssClasses } from '../../hooks';
import { findSelectableFieldValueFilter } from '../../utils/filterutils';
import { findSelectableFieldValueFilter, isNumberRangeValue, getDefaultFilterDisplayName } from '../../utils/filterutils';
import classNames from 'classnames';
import { useId } from '@reach/auto-id';
import { useId } from '../../hooks/useId';

/**
* The configuration data for a field value filter option.
*
@@ -117,7 +118,12 @@ export function CheckboxOption(props: CheckboxOptionProps): JSX.Element | null {

const isSelected = existingStoredFilter ? existingStoredFilter.selected : false;

const labelText = resultsCount ? `${displayName} (${resultsCount})` : displayName;
const displayNameString = isNumberRangeValue(displayName)
? getDefaultFilterDisplayName(displayName)
: displayName.toString();
const labelText = resultsCount
? `${displayNameString} (${resultsCount})`
: displayNameString;

const inputClasses = classNames(cssClasses.input, {
[cssClasses.input___disabled ?? '']: isOptionsDisabled
4 changes: 3 additions & 1 deletion src/components/Filters/FilterGroupContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createContext, useContext } from 'react';
import { UseCollapseOutput } from 'react-collapsed/dist/types';
import { useCollapse } from 'react-collapsed';

type UseCollapseOutput = ReturnType<typeof useCollapse>;

/**
* FilterGroupContext is responsible for searchable filters and collapsible filter groups.
2 changes: 1 addition & 1 deletion src/components/Filters/FilterGroupProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { PropsWithChildren, useMemo, useState } from 'react';
import useCollapse from 'react-collapsed';
import { useCollapse } from 'react-collapsed';
import { FilterGroupContext } from './FilterGroupContext';

/**
18 changes: 1 addition & 17 deletions src/components/Filters/RangeInput.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { Matcher, NumberRangeValue, useSearchActions, useSearchState } from '@ye
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useFilterGroupContext } from './FilterGroupContext';
import { useComposedCssClasses } from '../../hooks';
import { clearStaticRangeFilters, findSelectableFieldValueFilter, getSelectableFieldValueFilters, parseNumberRangeInput } from '../../utils/filterutils';
import { getDefaultFilterDisplayName, clearStaticRangeFilters, findSelectableFieldValueFilter, getSelectableFieldValueFilters, parseNumberRangeInput } from '../../utils/filterutils';
import { executeSearch } from '../../utils';
import classNames from 'classnames';
import { useFiltersContext } from './FiltersContext';
@@ -242,22 +242,6 @@ export function RangeInput(props: RangeInputProps): JSX.Element | null {
);
}

/**
* Creates the filter's display name based on the number range.
*/
function getDefaultFilterDisplayName(numberRange: NumberRangeValue) {
const start = numberRange.start;
const end = numberRange.end;

if (start && end) {
return `${start.value} - ${end.value}`;
} else if (start && !end) {
return `Over ${start.value}`;
} else if (end && !start) {
return `Up to ${end.value}`;
}
return '';
}

/**
* Returns true only if the provided string passes the numeric validation.
38 changes: 35 additions & 3 deletions src/components/MapboxMap.tsx
Original file line number Diff line number Diff line change
@@ -5,15 +5,25 @@ import { useDebouncedFunction } from '../hooks/useDebouncedFunction';
import ReactDOM from 'react-dom';

/**
* A functional component that can be used to render a custom marker on the map.
* Props for rendering a custom marker on the map.
*
* @public
*/
export type PinComponent<T> = (props: {
export type PinComponentProps<T> = {
/** The index of the pin. */
index: number,
/** The Mapbox map. */
mapbox: mapboxgl.Map,
/** The search result corresponding to the pin. */
result: Result<T>
}) => JSX.Element;
};

/**
* A functional component that can be used to render a custom marker on the map.
*
* @public
*/
export type PinComponent<T> = (props: PinComponentProps<T>) => JSX.Element;

/**
* A function use to derive a result's coordinate.
@@ -55,8 +65,21 @@ export interface MapboxMapProps<T> {
/**
* Custom Pin component to render for markers on the map.
* By default, the built-in marker image from Mapbox GL is used.
* This prop should not be used with
* {@link MapboxMapProps.renderPin | renderPin}. If both are provided,
* only PinComponent will be used.
*/
PinComponent?: PinComponent<T>,
/**
* Render function for a custom marker on the map. This function takes in an
* HTML element and is responible for rendering the pin into that element,
* which will be used as the marker.
* By default, the built-in marker image from Mapbox GL is used.
* This prop should not be used with
* {@link MapboxMapProps.PinComponent | PinComponent}. If both are provided,
* only PinComponent will be used.
*/
renderPin?: (props: PinComponentProps<T> & { container: HTMLElement }) => void,
/**
* A function to derive a result's coordinate for the corresponding marker's location on the map.
* By default, "yextDisplayCoordinate" field is used as the result's display coordinate.
@@ -89,6 +112,7 @@ export function MapboxMap<T>({
mapboxAccessToken,
mapboxOptions,
PinComponent,
renderPin,
getCoordinate = getDefaultCoordinate,
onDrag
}: MapboxMapProps<T>): JSX.Element {
@@ -136,12 +160,20 @@ export function MapboxMap<T>({
const el = document.createElement('div');
const markerOptions: mapboxgl.MarkerOptions = {};
if (PinComponent) {
if (renderPin) {
console.warn(
'Found both PinComponent and renderPin props. Using PinComponent.'
);
}
ReactDOM.render(<PinComponent
index={i}
mapbox={mapbox}
result={result}
/>, el);
markerOptions.element = el;
} else if (renderPin) {
renderPin({ index: i, mapbox, result, container: el });
markerOptions.element = el;
}
const marker = new mapboxgl.Marker(markerOptions)
.setLngLat({ lat: latitude, lng: longitude })
5 changes: 1 addition & 4 deletions src/components/ThumbsFeedback.tsx
Original file line number Diff line number Diff line change
@@ -2,10 +2,7 @@ import { useSearchState } from '@yext/search-headless-react';
import React, { useCallback, useState } from 'react';
import { ThumbIcon } from '../icons/ThumbIcon';
import { useComposedCssClasses } from '../hooks';
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function'
? useIsomorphicLayoutEffect
: useIsomorphicLayoutEffect['default'];
import { useLayoutEffect } from '../hooks/useLayoutEffect';

/**
* Analytics event types for quality feedback.
4 changes: 3 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,8 @@ export {
FilterSearch,
FilterSearchCssClasses,
FilterSearchProps,
OnSelectParams
OnSelectParams,
OnDropdownInputChangeProps
} from './FilterSearch';

export {
@@ -159,6 +160,7 @@ export {
export {
MapboxMap,
PinComponent,
PinComponentProps,
MapboxMapProps,
OnDragHandler,
CoordinateGetter,
61 changes: 61 additions & 0 deletions src/hooks/useId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copied with minor modifications from
// https://github.com/reach/reach-ui/blob/dev/packages/auto-id/src/reach-auto-id.ts

import React, { useEffect, useState } from "react";
import { useLayoutEffect } from "./useLayoutEffect";

let serverHandoffComplete = false;
let id = 0;
function genId(): string {
++id;
return id.toString();
}

// Workaround for https://github.com/webpack/webpack/issues/14814
// https://github.com/eps1lon/material-ui/blob/8d5f135b4d7a58253a99ab56dce4ac8de61f5dc1/packages/mui-utils/src/useId.ts#L21
const maybeReactUseId: undefined | (() => string) = (React as any)[
"useId".toString()
];

/**
* useId
*
* Autogenerate IDs to facilitate WAI-ARIA and server rendering.
*
* Note: The returned ID will initially be empty string and will update after a
* component mounts.
*
* @see Docs https://reach.tech/auto-id
*/

export function useId(): string {
if (maybeReactUseId !== undefined) {
return maybeReactUseId();
}

// If this instance isn't part of the initial render, we don't have to do the
// double render/patch-up dance. We can just generate the ID and return it.
const initialId = (serverHandoffComplete ? genId() : '');
const [id, setId] = useState(initialId);

useLayoutEffect(() => {
if (id === '') {
// Patch the ID after render. We do this in `useLayoutEffect` to avoid any
// rendering flicker, though it'll make the first render slower (unlikely
// to matter, but you're welcome to measure your app and let us know if
// it's a problem).
setId(genId());
}
}, [id]);

useEffect(() => {
if (serverHandoffComplete === false) {
// Flag all future uses of `useId` to skip the update dance. This is in
// `useEffect` because it goes after `useLayoutEffect`, ensuring we don't
// accidentally bail out of the patch-up dance prematurely.
serverHandoffComplete = true;
}
}, []);

return id;
}
5 changes: 5 additions & 0 deletions src/hooks/useLayoutEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';

export const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function'
? useIsomorphicLayoutEffect
: useIsomorphicLayoutEffect['default'];
17 changes: 17 additions & 0 deletions src/utils/filterutils.tsx
Original file line number Diff line number Diff line change
@@ -171,3 +171,20 @@ export function getSelectableFieldValueFilters(
return undefined;
}).filter((s): s is SelectableFieldValueFilter => !!s);
}

/**
* Creates the filter's display name based on the number range.
*/
export function getDefaultFilterDisplayName(numberRange: NumberRangeValue) {
const start = numberRange.start;
const end = numberRange.end;

if (start && end) {
return `${start.value} - ${end.value}`;
} else if (start && !end) {
return `Over ${start.value}`;
} else if (end && !start) {
return `Up to ${end.value}`;
}
return '';
}
702 changes: 350 additions & 352 deletions test-site/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions test-site/package.json
Original file line number Diff line number Diff line change
@@ -15,8 +15,8 @@
"devDependencies": {
"@types/mapbox-gl": "^2.7.6",
"@types/node": "^16.11.26",
"@types/react": "^17.0.42",
"@types/react-dom": "^17.0.14",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.0",
"@types/uuid": "^8.3.4",
"autoprefixer": "^10.4.4",
"eslint-config-react-app": "file:../node_modules/eslint-config-react-app",
2 changes: 2 additions & 0 deletions test-site/sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REACT_APP_LIVE_API_KEY=[[Your Live API Key]]
REACT_APP_MAPBOX_API_KEY=[[Your Mapbox API Key]]
2 changes: 1 addition & 1 deletion test-site/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CloudRegion, Environment } from '@yext/search-headless-react';

export const config = {
apiKey: '2d8c550071a64ea23e263118a2b0680b',
apiKey: process.env.REACT_APP_LIVE_API_KEY || 'REPLACE_ME',
experienceKey: 'slanswers-hier-facets',
locale: 'en',
experienceVersion: 'STAGING',
15 changes: 8 additions & 7 deletions test-site/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import 'mapbox-gl/dist/mapbox-gl.css';
import App from './App';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<StrictMode>
<App/>
</StrictMode>
);
40 changes: 37 additions & 3 deletions test-site/src/pages/PeoplePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useLayoutEffect } from 'react';
import { useSearchActions } from '@yext/search-headless-react';
import { FieldValueStaticFilter, SelectableStaticFilter, useSearchActions } from '@yext/search-headless-react';
import {
AppliedFilters,
FilterSearch,
@@ -19,12 +19,14 @@ import {
NumericalFacets,
AlternativeVerticals,
StandardFacet,
NumericalFacet
NumericalFacet,
OnDropdownInputChangeProps
} from '@yext/search-ui-react';
// import { CustomCard } from '../components/CustomCard';

const hierarchicalFacetFieldIds = ['c_hierarchicalFacet'];
const filterSearchFields = [{ fieldApiName: 'name', entityType: 'ce_person' }];
const employeeFilterSearchFields = [{fieldApiName: 'c_employeeDepartment', entityType: 'ce_person'}];
const employeeFilterConfigs = [
{ value: 'Consulting' },
{ value: 'Technology' }
@@ -43,6 +45,31 @@ export function PeoplePage() {
searchActions.executeVerticalQuery();
});

/**
* This example function that's being used for onDropdownInputChange allows for clearing the filter in the search state when the input is empty.
* This is especially useful for implementations that have multiple FilterSearch components.
* Ex. a user can search using both inputs initially, but then wants to clear one of the FilterSearch inputs and re-run a search.
*/
const removeAssociatedFilterWhenInputIsEmpty = (searchFields: { fieldApiName: string; entityType: string; }[]) => (params: OnDropdownInputChangeProps) => {
const { value, executeFilterSearch } = params;
// If there is still an input value, execute the filter search as normal
if (value !== "") {
executeFilterSearch(value);
}
// When the input is empty, remove the associated filter from the search state while keeping any other filters that are applied.
else {
const fieldIds = searchFields.map((field: {fieldApiName: string, entityType: string}) => field.fieldApiName);
const filtersToKeep: SelectableStaticFilter[] = [];
searchActions.state.filters.static?.forEach((staticFilter) => {
const filter = staticFilter.filter as FieldValueStaticFilter;
if (!fieldIds.includes(filter.fieldId)) {
filtersToKeep.push(staticFilter);
}
});
searchActions.setStaticFilters(filtersToKeep);
}
}

return (
<div>
<SearchBar />
@@ -51,7 +78,14 @@ export function PeoplePage() {
<FilterSearch
searchFields={filterSearchFields}
searchOnSelect={true}
label='Filters'
label='FilterSearch Name Filter'
onDropdownInputChange={removeAssociatedFilterWhenInputIsEmpty(filterSearchFields)}
/>
<FilterSearch
searchFields={employeeFilterSearchFields}
searchOnSelect={true}
label='FilterSearch Department Filter'
onDropdownInputChange={removeAssociatedFilterWhenInputIsEmpty(employeeFilterSearchFields)}
/>
<FilterDivider />
<StaticFilters
26 changes: 0 additions & 26 deletions tests/__fixtures__/data/filtercontext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FiltersContextType } from '../../../src/components/Filters/FiltersContext';
import { SelectableFieldValueFilter } from '../../../src/models/SelectableFieldValueFilter';
import { FilterGroupContextType } from '../../../src/components/Filters/FilterGroupContext';
import { Matcher } from '@yext/search-headless-react';

const selectableFilter: SelectableFieldValueFilter = {
@@ -21,28 +20,3 @@ export const filterContextValueDisabled: FiltersContextType = {
applyFilters: () => null,
filters: [selectableFilter]
};

export const filterGroupContextValue: FilterGroupContextType = {
searchValue: '',
fieldId: '123',
setSearchValue: () => null,
getCollapseProps: () => ({
id: 'id',
onTransitionEnd: () => null,
style: {},
'aria-hidden': 'false'
}),
getToggleProps: () => ({
disabled: false,
type: 'button',
role: 'role',
id: 'id',
'aria-controls': 'element',
'aria-expanded': 'true',
tabIndex: 0,
onClick: () => null
}),
isExpanded: true,
isOptionsDisabled: false,
setIsOptionsDisabled: () => null
};
3 changes: 3 additions & 0 deletions tests/__setup__/setup-env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import '@testing-library/jest-dom';
import { server } from './server';
import { TextEncoder} from 'util';

global.TextEncoder = TextEncoder;

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
17 changes: 16 additions & 1 deletion tests/__utils__/mocks.ts
Original file line number Diff line number Diff line change
@@ -63,4 +63,19 @@ export function mockAnswersHooks({
mockedUtils && mockAnswersUtils(mockedUtils);
mockedState && mockAnswersState(mockedState);
mockedActions && mockSearchActions(mockedActions);
}
}

const originalConsoleError = console.error.bind(console.error);

export function ignoreLinkClickErrors() {
jest.spyOn(global.console, 'error')
.mockImplementation((msg, ...params) => {
/**
* Suppress errors about 'navigation' not being defined in jsdom when
* clicking on links.
*/
if (!msg.toString().match(/Error: Not implemented: navigation/)) {
originalConsoleError(msg, ...params);
}
});
};
17 changes: 17 additions & 0 deletions tests/__utils__/renderReact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';

export function renderReact(children: JSX.Element, container: Element) {
try {
if (!React.version.startsWith('18')) {
throw new Error('Using React <18. Skip createRoot rendering.');
}
// @ts-ignore
import('react-dom/client').then(({ createRoot }) => {
const root = createRoot(container);
root.render(children);
});
} catch {
ReactDOM.render(children, container);
}
}
20 changes: 10 additions & 10 deletions tests/components/AppliedFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -126,35 +126,35 @@ describe('AppliedFilters', () => {
expect(screen.queryByText(staticFilterDisplayName)).toBeFalsy();
});

it('The "X" button for an applied static filter deselects the filter option', () => {
it('The "X" button for an applied static filter deselects the filter option', async () => {
const actions = spyOnActions();

render(<AppliedFilters />);
const removeFilterButton = screen.getByRole('button', { name: 'Remove "Yext Sites" filter' });
userEvent.click(removeFilterButton);
await userEvent.click(removeFilterButton);

expect(actions.setFilterOption).toHaveBeenCalledWith(expect.objectContaining({
selected: false
}));
});

it('The "X" button for an applied facet deselects the facet option', () => {
it('The "X" button for an applied facet deselects the facet option', async () => {
const actions = spyOnActions();

render(<AppliedFilters />);
const removeFilterButton = screen.getByRole('button', { name: 'Remove "Yext Reviews" filter' });
userEvent.click(removeFilterButton);
await userEvent.click(removeFilterButton);

const isSelected = actions.setFacetOption.mock.calls[0][2];
expect(isSelected).toBe(false);
});

it('The clear all button unselects all filters', () => {
it('The clear all button unselects all filters', async () => {
const actions = spyOnActions();

render(<AppliedFilters />);
const clearAllButton = screen.getByRole('button', { name: 'Clear All' });
userEvent.click(clearAllButton);
await userEvent.click(clearAllButton);

expect(actions.resetFacets).toHaveBeenCalled();
expect(actions.setStaticFilters).toHaveBeenCalledWith(expect.not.objectContaining({ selected: true }));
@@ -235,7 +235,7 @@ describe('AppliedFilters with hierarchical facets', () => {
expect(filterPills[1]).toHaveAttribute('aria-label', 'Remove "steinsgate" filter');
});

it('removing a hierarchical applied filter removes the facet and all descendants in the hierarchy', () => {
it('removing a hierarchical applied filter removes the facet and all descendants in the hierarchy', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -261,7 +261,7 @@ describe('AppliedFilters with hierarchical facets', () => {
expect(filterPills).toHaveLength(3);

const fruitButton = screen.getByLabelText('Remove "food" filter');
userEvent.click(fruitButton);
await userEvent.click(fruitButton);

expect(actions.setFacetOption).toHaveBeenCalledTimes(2);
expect(actions.setFacetOption).toHaveBeenCalledWith(
@@ -276,7 +276,7 @@ describe('AppliedFilters with hierarchical facets', () => {
);
});

it('removing a hierarchical applied filter selects its parent', () => {
it('removing a hierarchical applied filter selects its parent', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -294,7 +294,7 @@ describe('AppliedFilters with hierarchical facets', () => {
expect(filterPills).toHaveLength(3);

const fruitButton = screen.getByLabelText('Remove "banana" filter');
userEvent.click(fruitButton);
await userEvent.click(fruitButton);

expect(actions.setFacetOption).toHaveBeenCalledWith(
'hier',
15 changes: 8 additions & 7 deletions tests/components/DirectAnswer.test.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import { DirectAnswerState } from '@yext/search-headless-react';
import { useAnalytics } from '../../src/hooks/useAnalytics';
import { DirectAnswer } from '../../src/components/DirectAnswer';
import { RecursivePartial, mockAnswersState } from '../__utils__/mocks';
import { RecursivePartial, ignoreLinkClickErrors, mockAnswersState } from '../__utils__/mocks';
import { fieldValueDAState, featuredSnippetDAState } from '../__fixtures__/data/directanswers';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -33,10 +33,11 @@ describe('Featured snippet direct answer analytics', () => {
});

function runAnalyticsTestSuite() {
it('reports link click analytics', () => {
it('reports link click analytics', async () => {
render(<DirectAnswer />);
ignoreLinkClickErrors();
const link = screen.getByRole('link');
userEvent.click(link);
await userEvent.click(link);
expect(useAnalytics()?.report).toHaveBeenCalledTimes(1);
expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({
type: 'CTA_CLICK',
@@ -46,10 +47,10 @@ function runAnalyticsTestSuite() {
}));
});

it('reports THUMBS_UP feedback', () => {
it('reports THUMBS_UP feedback', async () => {
render(<DirectAnswer />);
const thumbsUp = screen.queryAllByRole('button')[0];
userEvent.click(thumbsUp);
await userEvent.click(thumbsUp);
expect(useAnalytics()?.report).toHaveBeenCalledTimes(1);
expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({
type: 'THUMBS_UP',
@@ -59,10 +60,10 @@ function runAnalyticsTestSuite() {
}));
});

it('reports THUMBS_DOWN feedback', () => {
it('reports THUMBS_DOWN feedback', async () => {
render(<DirectAnswer />);
const thumbsDown = screen.queryAllByRole('button')[1];
userEvent.click(thumbsDown);
await userEvent.click(thumbsDown);
expect(useAnalytics()?.report).toHaveBeenCalledTimes(1);
expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({
type: 'THUMBS_DOWN',
86 changes: 43 additions & 43 deletions tests/components/Dropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Dropdown, DropdownProps } from '../../src/components/Dropdown/Dropdown';
import { DropdownInput } from '../../src/components/Dropdown/DropdownInput';
import { DropdownMenu } from '../../src/components/Dropdown/DropdownMenu';
import { DropdownItem } from '../../src/components/Dropdown/DropdownItem';
import { testSSR } from '../ssr/utils';
import React from 'react';

describe('Dropdown', () => {
it('renders identical content between the server and the client.', () => {
@@ -21,7 +21,7 @@ describe('Dropdown', () => {
);
});

it('can toggle hide/display', () => {
it('can toggle hide/display', async () => {
const mockedOnToggleFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here',
@@ -44,22 +44,22 @@ describe('Dropdown', () => {
expect(screen.queryByText('item1')).toBeNull();

// display when click into dropdown input
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(screen.getByText('item1')).toBeDefined();
expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined);

// hidden when click elsewhere outside of dropdown component
userEvent.click(screen.getByText('external div'));
await userEvent.click(screen.getByText('external div'));
expect(screen.queryByText('item1')).toBeNull();
expect(mockedOnToggleFn).toBeCalledWith(false, '', '', -1, undefined);

// display when tab into dropdown input
userEvent.tab();
await userEvent.tab();
expect(screen.getByText('item1')).toBeDefined();
expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined);
});

it('handles arrowkey navigation properly and focuses on the option and input text', () => {
it('handles arrowkey navigation properly and focuses on the option and input text', async () => {
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here'
};
@@ -74,19 +74,19 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
const itemNode = screen.getByText('item1');

userEvent.keyboard('{arrowdown}');
await userEvent.keyboard('{arrowdown}');
expect(itemNode.className).toContain('FocusedItem1');
expect(inputNode).toHaveValue('item1');

userEvent.keyboard('{arrowup}');
await userEvent.keyboard('{arrowup}');
expect(itemNode.className).not.toContain('FocusedItem1');
expect(inputNode).not.toHaveValue('item1');
});

it('handles tab navigation properly and focuses on the option and input text', () => {
it('handles tab navigation properly and focuses on the option and input text', async () => {
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here'
};
@@ -101,19 +101,19 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
const itemNode = screen.getByText('item1');

userEvent.keyboard('{Tab}');
await userEvent.keyboard('{Tab}');
expect(itemNode.className).toContain('FocusedItem1');
expect(inputNode).toHaveValue('item1');

userEvent.keyboard('{Shift}{Tab}');
await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
expect(itemNode.className).not.toContain('FocusedItem1');
expect(inputNode).not.toHaveValue('item1');
});

it('closes the dropdown menu when tabbing on last option', () => {
it('closes the dropdown menu when tabbing on last option', async () => {
const mockedOnToggleFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here',
@@ -130,13 +130,13 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
userEvent.keyboard('{Tab}{Tab}');
await userEvent.click(inputNode);
await userEvent.keyboard('{Tab}{Tab}');

expect(mockedOnToggleFn).toHaveBeenLastCalledWith(false, '', 'item1', 0, undefined);
});

it('selects when an option is focused and enter is pressed', () => {
it('selects when an option is focused and enter is pressed', async () => {
const mockedOnSelectFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here',
@@ -153,20 +153,20 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
expect(screen.getByTestId('item1')).toBeDefined();
expect(inputNode).toHaveValue('');

userEvent.keyboard('{arrowdown}');
userEvent.keyboard('{enter}');
await userEvent.keyboard('{arrowdown}');
await userEvent.keyboard('{enter}');

expect(inputNode).toHaveValue('item1');
expect(screen.queryByTestId('item1')).toBeNull();
expect(mockedOnSelectFn).toBeCalledTimes(1);
expect(mockedOnSelectFn).toHaveBeenCalledWith('item1', 0, undefined);
});

it('selects an option on click', () => {
it('selects an option on click', async () => {
const mockedOnSelectFn = jest.fn();
const mockedOnClickFn = jest.fn();
const dropdownProps: DropdownProps = {
@@ -184,12 +184,12 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
expect(screen.getByTestId('item1')).toBeDefined();
expect(inputNode).toHaveValue('');

userEvent.keyboard('{arrowdown}');
userEvent.click(screen.getByTestId('item1'));
await userEvent.keyboard('{arrowdown}');
await userEvent.click(screen.getByTestId('item1'));

expect(inputNode).toHaveValue('item1');
expect(screen.queryByTestId('item1')).toBeNull();
@@ -199,7 +199,7 @@ describe('Dropdown', () => {
expect(mockedOnSelectFn).toHaveBeenCalledWith('item1', 0, undefined);
});

it('selects when an option is focused on toggle', () => {
it('selects when an option is focused on toggle', async () => {
const mockedOnToggleFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here',
@@ -219,21 +219,21 @@ describe('Dropdown', () => {
</div>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
expect(screen.getByTestId('item1')).toBeDefined();
expect(inputNode).toHaveValue('');

userEvent.keyboard('i');
userEvent.keyboard('{arrowdown}');
userEvent.click(screen.getByText('external div'));
await userEvent.keyboard('i');
await userEvent.keyboard('{arrowdown}');
await userEvent.click(screen.getByText('external div'));

expect(inputNode).toHaveValue('item1');
expect(screen.queryByTestId('item1')).toBeNull();
expect(mockedOnToggleFn).toBeCalledTimes(3);
expect(mockedOnToggleFn).toHaveBeenCalledWith(false, 'i', 'item1', 0, undefined);
});

it('updates options when user provide new input', () => {
it('updates options when user provide new input', async () => {
const mockedOnChangeFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here'
@@ -249,25 +249,25 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
const itemNode = screen.getByText('item1');
expect(itemNode).toBeDefined();
expect(inputNode).toHaveValue('');

userEvent.keyboard('{arrowdown}');
await userEvent.keyboard('{arrowdown}');
expect(itemNode.className).toContain('FocusedItem1');
expect(inputNode).toHaveValue('item1');

const userInput = ' someText';
userEvent.type(inputNode, userInput);
await userEvent.type(inputNode, userInput);
expect(inputNode).toHaveValue('item1' + userInput);
expect(mockedOnChangeFn).toBeCalledTimes(userInput.length);
expect(mockedOnChangeFn).toHaveBeenCalledWith('item1' + userInput);
// item should no longer be in focus after dropdown update
expect(itemNode.className).not.toContain('FocusedItem1');
});

it('submits on "Enter" in the input box', () => {
it('submits on "Enter" in the input box', async () => {
const mockedOnSubmitFn = jest.fn();
const mockedOnSelectFn = jest.fn();
const dropdownProps: DropdownProps = {
@@ -285,8 +285,8 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.type(inputNode, 'someText');
userEvent.keyboard('{enter}');
await userEvent.type(inputNode, 'someText');
await userEvent.keyboard('{enter}');

expect(inputNode).toHaveValue('someText');
expect(mockedOnSelectFn).toBeCalledTimes(0);
@@ -295,7 +295,7 @@ describe('Dropdown', () => {
expect(screen.queryByText('item1')).toBeNull();
});

it('prevents submission if submission criteria failed', () => {
it('prevents submission if submission criteria failed', async () => {
const mockedSubmitCriteriaFn = jest.fn().mockImplementation(() => false);
const mockedOnSubmitFn = jest.fn();
const dropdownProps: DropdownProps = {
@@ -315,8 +315,8 @@ describe('Dropdown', () => {
</Dropdown>
);
const inputNode = screen.getByRole('textbox');
userEvent.type(inputNode, 'someText');
userEvent.keyboard('{enter}');
await userEvent.type(inputNode, 'someText');
await userEvent.keyboard('{enter}');

expect(inputNode).toHaveValue('someText');
expect(mockedSubmitCriteriaFn).toBeCalledTimes(1);
@@ -326,7 +326,7 @@ describe('Dropdown', () => {
});
});
describe('Always Select Option', () => {
it('clicking out without interacting with dropdown does not select a filter', () => {
it('clicking out without interacting with dropdown does not select a filter', async () => {
const mockedOnSelectFn = jest.fn();
const dropdownProps: DropdownProps = {
screenReaderText: 'screen reader text here',
@@ -347,12 +347,12 @@ describe('Always Select Option', () => {
</div>
);
const inputNode = screen.getByRole('textbox');
userEvent.click(inputNode);
await userEvent.click(inputNode);
expect(screen.getByTestId('item1')).toBeDefined();
expect(inputNode).toHaveValue('');

userEvent.keyboard('i');
userEvent.click(screen.getByText('external div'));
await userEvent.keyboard('i');
await userEvent.click(screen.getByText('external div'));

expect(inputNode).toHaveValue('i');
expect(screen.queryByTestId('item1')).toBeNull();
8 changes: 4 additions & 4 deletions tests/components/Facets.test.tsx
Original file line number Diff line number Diff line change
@@ -164,7 +164,7 @@ describe('Facets', () => {
expect(screen.queryByText(facet.displayName)).toBeNull();
});

it('Clicking a facet option executes a search by default', () => {
it('Clicking a facet option executes a search by default', async () => {
mockAnswersState({
...mockedState,
filters: { facets: [DisplayableFacets[0]] }
@@ -178,12 +178,12 @@ describe('Facets', () => {
);
expect(coffeeCheckbox.checked).toBeFalsy();

userEvent.click(coffeeCheckbox);
await userEvent.click(coffeeCheckbox);
expectFacetOptionSet(actions, facet.fieldId, facet.options[0], true);
expect(actions.executeVerticalQuery).toBeCalled();
});

it('Clicking a facet option does not execute a search when searchOnChange is false', () => {
it('Clicking a facet option does not execute a search when searchOnChange is false', async () => {
mockAnswersState({
...mockedState,
filters: { facets: [DisplayableFacets[0]] }
@@ -197,7 +197,7 @@ describe('Facets', () => {
);
expect(coffeeCheckbox.checked).toBeFalsy();

userEvent.click(coffeeCheckbox);
await userEvent.click(coffeeCheckbox);
expectFacetOptionSet(actions, facet.fieldId, facet.options[0], true);
expect(actions.executeVerticalQuery).not.toBeCalled();
});
6 changes: 4 additions & 2 deletions tests/components/FeaturedSnippetDirectAnswer.test.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { FeaturedSnippetDirectAnswer } from '../../src/components/FeaturedSnippe
import { featuredSnippetDAState } from '../__fixtures__/data/directanswers';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ignoreLinkClickErrors } from '../__utils__/mocks';

const featuredSnippetDAResult = featuredSnippetDAState.result as FeaturedSnippetDirectAnswerType;

@@ -33,13 +34,14 @@ describe('FeaturedSnippet direct answer', () => {
expect(directAnswerLink).toHaveAttribute('href', '[landingPageUrl]');
});

it('executes readMoreClickHandler when click on "Read more about" link', () => {
it('executes readMoreClickHandler when click on "Read more about" link', async () => {
const readMoreClickHandler = jest.fn();
render(<FeaturedSnippetDirectAnswer
result={featuredSnippetDAResult}
readMoreClickHandler={readMoreClickHandler}
/>);
userEvent.click(screen.getByRole('link', { name: '[relatedResult.name]' }));
ignoreLinkClickErrors();
await userEvent.click(screen.getByRole('link', { name: '[relatedResult.name]' }));
expect(readMoreClickHandler).toHaveBeenCalledTimes(1);
});

9 changes: 6 additions & 3 deletions tests/components/FieldValueDirectAnswer.test.tsx
Original file line number Diff line number Diff line change
@@ -10,18 +10,21 @@ import userEvent from '@testing-library/user-event';
import { fieldValueDAState } from '../__fixtures__/data/directanswers';
import { UnknownFieldTypeDisplayComponent } from '../../src/components/DirectAnswer';
import React from 'react';
import { ignoreLinkClickErrors } from '../__utils__/mocks';

const fieldValueDAResult = fieldValueDAState.result as FieldValueDirectAnswerType;

describe('FieldValue direct answer', () => {

it('executes viewDetailsClickHandler when click on "View Details" link', () => {
it('executes viewDetailsClickHandler when click on "View Details" link', async () => {
const viewDetailsClickHandler = jest.fn();
render(<FieldValueDirectAnswer
result={fieldValueDAResult}
viewDetailsClickHandler={viewDetailsClickHandler}/>
);
userEvent.click(screen.getByRole('link', { name: 'View Details' }));
ignoreLinkClickErrors();
const viewDetailsLink = screen.getByRole('link', { name: 'View Details' });
await userEvent.click(viewDetailsLink);

expect(viewDetailsClickHandler).toHaveBeenCalledTimes(1);
});

3 changes: 2 additions & 1 deletion tests/components/FilterSearch.stories.tsx
Original file line number Diff line number Diff line change
@@ -40,7 +40,8 @@ const meta: Meta<typeof FilterSearch> = {
}
},
args: {
label: 'Filter'
label: 'Filter',
onDropdownInputChange: undefined,
}
};
export default meta;
371 changes: 170 additions & 201 deletions tests/components/FilterSearch.test.tsx

Large diffs are not rendered by default.

58 changes: 23 additions & 35 deletions tests/components/Geolocation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Geolocation } from '../../src/components/Geolocation';
import { Matcher, SelectableStaticFilter, State } from '@yext/search-headless-react';
@@ -139,10 +139,9 @@ describe('custom click handler', () => {
const mockedHandleClickFn = jest.fn();
const actions = spyOnActions();
render(<Geolocation handleClick={mockedHandleClickFn} />);
clickUpdateLocation();
await waitFor(() => {
expect(mockedHandleClickFn).toHaveBeenCalledWith(newGeoPosition);
});
await clickUpdateLocation();

expect(mockedHandleClickFn).toHaveBeenCalledWith(newGeoPosition);
expect(actions.executeVerticalQuery).not.toBeCalled();
});

@@ -151,10 +150,9 @@ describe('custom click handler', () => {
jest.spyOn(locationOperations, 'getUserLocation').mockRejectedValue('mocked error!');
const mockedHandleClickFn = jest.fn();
render(<Geolocation handleClick={mockedHandleClickFn} />);
clickUpdateLocation();
await waitFor(() => {
expect(consoleWarnSpy).toBeCalledWith('mocked error!');
});
await clickUpdateLocation();

expect(consoleWarnSpy).toBeCalledWith('mocked error!');
expect(mockedHandleClickFn).not.toBeCalled();
});
});
@@ -163,31 +161,27 @@ describe('default click handler', () => {
it('sets a location filter using provided radius', async () => {
const actions = spyOnActions();
render(<Geolocation radius={10} />);
clickUpdateLocation();
await clickUpdateLocation();

const expectedLocationFilter: SelectableStaticFilter = createLocationFilter(10 * 1609.344);
await waitFor(() => {
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});

it('sets a location filter with user\'s coordinates in static filters state when clicked', async () => {
const actions = spyOnActions();
render(<Geolocation />);
clickUpdateLocation();
await clickUpdateLocation();

const expectedLocationFilter: SelectableStaticFilter = createLocationFilter();
expect(locationOperations.getUserLocation).toBeCalled();
await waitFor(() => {
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});

it('replaces existing location filters with a new location filter in static filters state', async () => {
mockAnswersState(mockedStateWithFilters);
const actions = spyOnActions();
render(<Geolocation />);
clickUpdateLocation();
await clickUpdateLocation();

const expectedStaticFilters = [
{
@@ -202,51 +196,45 @@ describe('default click handler', () => {
},
createLocationFilter()
];
await waitFor(() => {
expect(actions.setStaticFilters).toBeCalledWith(expectedStaticFilters);
});
expect(actions.setStaticFilters).toBeCalledWith(expectedStaticFilters);
});

it('sets a location filter using a larger radius than provided value due to low accuracy of user coordinate', async () => {
jest.spyOn(locationOperations, 'getUserLocation').mockResolvedValue(newGeoPositionWithLowAccuracy);
const actions = spyOnActions();
render(<Geolocation radius={10} />);
clickUpdateLocation();
await clickUpdateLocation();

const accuracy = newGeoPositionWithLowAccuracy.coords.accuracy;
const expectedLocationFilter: SelectableStaticFilter = createLocationFilter(accuracy);
await waitFor(() => {
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});
expect(actions.setStaticFilters).toBeCalledWith([expectedLocationFilter]);
});

it('executes a new search when clicked', async () => {
const actions = spyOnActions();
render(<Geolocation />);
clickUpdateLocation();
await clickUpdateLocation();

await waitFor(() => {
expect(actions.executeVerticalQuery).toBeCalled();
});
expect(actions.executeVerticalQuery).toBeCalled();
});

it('does not execute default handleClick when error occurs from collecting user\'s location', async () => {
const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation();
jest.spyOn(locationOperations, 'getUserLocation').mockRejectedValue('mocked error!');
const actions = spyOnActions();
render(<Geolocation />);
clickUpdateLocation();
await waitFor(() => {
expect(consoleWarnSpy).toBeCalledWith('mocked error!');
});
await clickUpdateLocation();

expect(consoleWarnSpy).toBeCalledWith('mocked error!');

expect(actions.setStaticFilters).not.toBeCalled();
expect(actions.executeVerticalQuery).not.toBeCalled();
});
});

function clickUpdateLocation() {
async function clickUpdateLocation() {
const updateLocationButton = screen.getByRole('button');
userEvent.click(updateLocationButton);
await userEvent.click(updateLocationButton);
}

function createLocationFilter(radius: number = 50 * 1609.344): SelectableStaticFilter {
20 changes: 10 additions & 10 deletions tests/components/HierarchicalFacetContent.test.tsx
Original file line number Diff line number Diff line change
@@ -65,38 +65,38 @@ describe('HierarchicalFacetsContent', () => {
expect(screen.getByRole('button', { name: /banana/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /apple/i })).toBeTruthy();
});
it('Clicking the currently selected option deselects it and selects its parent', () => {
it('Clicking the currently selected option deselects it and selects its parent', async () => {
const actions = spyOnActions();
render(mockHierarchicalFacet());

const bananaButton = screen.getByRole('button', { name: /banana/i });
userEvent.click(bananaButton);
await userEvent.click(bananaButton);

expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
expectFacetOptionSet(actions, { value: 'food > fruit', selected: true });
});
it('Clicking a non-selected option selects it and deselects its siblings', () => {
it('Clicking a non-selected option selects it and deselects its siblings', async () => {
const actions = spyOnActions();
render(mockHierarchicalFacet());

const appleButton = screen.getByRole('button', { name: /apple/i });
userEvent.click(appleButton);
await userEvent.click(appleButton);

expectFacetOptionSet(actions, { value: 'food > fruit > apple', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
});
it('Clicking the current category with a selected child selects the category and deselects the ' +
'child', () => {
'child', async () => {
const actions = spyOnActions();
render(mockHierarchicalFacet());

const currentCategoryButton = screen.getAllByRole('button', { name: /fruit/i })[1];
userEvent.click(currentCategoryButton);
await userEvent.click(currentCategoryButton);

expectFacetOptionSet(actions, { value: 'food > fruit', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
});
it('Clicking a selected current category deselects it and selects its parent', () => {
it('Clicking a selected current category deselects it and selects its parent', async () => {
const facets = [
createHierarchicalFacet([
'food',
@@ -115,17 +115,17 @@ describe('HierarchicalFacetsContent', () => {
render(mockHierarchicalFacet());

const currentCategoryButton = screen.getAllByRole('button', { name: /fruit/i })[1];
userEvent.click(currentCategoryButton);
await userEvent.click(currentCategoryButton);

expectFacetOptionSet(actions, { value: 'food > fruit', selected: false });
expectFacetOptionSet(actions, { value: 'food', selected: true });
});
it('Clicking a parent category selects it and deselects its children', () => {
it('Clicking a parent category selects it and deselects its children', async () => {
const actions = spyOnActions();
render(mockHierarchicalFacet());

const parentCategoryButton = screen.getByRole('button', { name: /food/i });
userEvent.click(parentCategoryButton);
await userEvent.click(parentCategoryButton);

expectFacetOptionSet(actions, { value: 'food', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit', selected: false });
20 changes: 10 additions & 10 deletions tests/components/HierarchicalFacets.test.tsx
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ describe('Hierarchical facets', () => {
expect(screen.getByRole('button', { name: /apple/i })).toBeTruthy();
});

it('Clicking the currently selected option deselects it and selects its parent', () => {
it('Clicking the currently selected option deselects it and selects its parent', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -82,12 +82,12 @@ describe('Hierarchical facets', () => {
render(<HierarchicalFacets includedFieldIds={hierarchicalFacetFieldIds} />);

const bananaButton = screen.getByRole('button', { name: /banana/i });
userEvent.click(bananaButton);
await userEvent.click(bananaButton);

expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
expectFacetOptionSet(actions, { value: 'food > fruit', selected: true });
});
it('Clicking a non-selected option selects it and deselects its siblings', () => {
it('Clicking a non-selected option selects it and deselects its siblings', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -103,12 +103,12 @@ describe('Hierarchical facets', () => {
render(<HierarchicalFacets includedFieldIds={hierarchicalFacetFieldIds} />);

const appleButton = screen.getByRole('button', { name: /apple/i });
userEvent.click(appleButton);
await userEvent.click(appleButton);

expectFacetOptionSet(actions, { value: 'food > fruit > apple', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
});
it('Clicking the current category with a selected child selects the category and deselects the child', () => {
it('Clicking the current category with a selected child selects the category and deselects the child', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -124,13 +124,13 @@ describe('Hierarchical facets', () => {
render(<HierarchicalFacets includedFieldIds={hierarchicalFacetFieldIds} />);

const currentCategoryButton = screen.getByRole('button', { name: /fruit/i });
userEvent.click(currentCategoryButton);
await userEvent.click(currentCategoryButton);

expectFacetOptionSet(actions, { value: 'food > fruit', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit > banana', selected: false });
});

it('Clicking a selected current category deselects it and selects its parent', () => {
it('Clicking a selected current category deselects it and selects its parent', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -146,12 +146,12 @@ describe('Hierarchical facets', () => {
render(<HierarchicalFacets includedFieldIds={hierarchicalFacetFieldIds} />);

const currentCategoryButton = screen.getByRole('button', { name: /fruit/i });
userEvent.click(currentCategoryButton);
await userEvent.click(currentCategoryButton);

expectFacetOptionSet(actions, { value: 'food > fruit', selected: false });
expectFacetOptionSet(actions, { value: 'food', selected: true });
});
it('Clicking a parent category selects it and deselects its children', () => {
it('Clicking a parent category selects it and deselects its children', async () => {
mockFiltersState({
facets: [
createHierarchicalFacet([
@@ -167,7 +167,7 @@ describe('Hierarchical facets', () => {
render(<HierarchicalFacets includedFieldIds={hierarchicalFacetFieldIds} />);

const parentCategoryButton = screen.getByRole('button', { name: /food/i });
userEvent.click(parentCategoryButton);
await userEvent.click(parentCategoryButton);

expectFacetOptionSet(actions, { value: 'food', selected: true });
expectFacetOptionSet(actions, { value: 'food > fruit', selected: false });
18 changes: 7 additions & 11 deletions tests/components/LocationBias.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LocationBias } from '../../src/components/LocationBias';
import { State, LocationBiasMethod, LocationBias as LocationBiasType } from '@yext/search-headless-react';
@@ -85,17 +85,15 @@ it('renders the proper text (location DisplayName, method, and update btn)', ()
it('calls setUserLocation with coordinates returned by getUserLocation as params when update location is clicked', async () => {
const actions = spyOnActions();
render(<LocationBias />);
clickUpdateLocation();
await clickUpdateLocation();

const expectedCoordinates = {
latitude: newGeoPosition.coords.latitude,
longitude: newGeoPosition.coords.longitude
};

expect(locationOperations.getUserLocation).toBeCalled();
await waitFor(() => {
expect(actions.setUserLocation).toBeCalledWith(expectedCoordinates);
});
expect(actions.setUserLocation).toBeCalledWith(expectedCoordinates);
});

it('updates rendered DisplayName if location changes and update button is clicked', async () => {
@@ -105,11 +103,9 @@ it('updates rendered DisplayName if location changes and update button is clicke
expect(locationNameElement).toBeDefined();

mockAnswersState(mockedStateNyIP);
clickUpdateLocation();
await clickUpdateLocation();

await waitFor(() => {
expect(searchOperations.executeSearch).toBeCalled();
});
expect(searchOperations.executeSearch).toBeCalled();

const newExpectedLocationName = mockedStateNyIP.location.locationBias.displayName;
const newLocationNameElement = screen.getByText(newExpectedLocationName);
@@ -138,7 +134,7 @@ it('renders nothing if there is no display name', () => {
expect(container).toBeEmptyDOMElement();
});

function clickUpdateLocation() {
async function clickUpdateLocation() {
const updateLocationButton = screen.getByRole('button', { name: 'Update your location' });
userEvent.click(updateLocationButton);
await userEvent.click(updateLocationButton);
}
19 changes: 19 additions & 0 deletions tests/components/MapboxMap.stories.tsx
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { generateMockedHeadless } from '../__fixtures__/search-headless';
import { MapboxMap, MapboxMapProps } from '../../src/components/MapboxMap';
import { MapPin } from '../../test-site/src/components/MapPin';
import { Location } from '../../test-site/src/pages/LocationsPage';
import { renderReact } from '../__utils__/renderReact';
import { locationVerticalSingle, locationVerticalMultiple } from '../__fixtures__/data/mapbox';
import React from 'react';

@@ -20,6 +21,9 @@ const meta: Meta<typeof MapboxMap> = {
PinComponent: {
control: false,
},
renderPin: {
control: false,
},
},
args: {
mapboxAccessToken: process.env.REACT_APP_MAPBOX_API_KEY,
@@ -59,3 +63,18 @@ CustomPin.play = async ({ canvasElement }) => {
fireEvent.click(mapPin);
await canvas.findByText('title1');
};

export const CustomRenderPin: StoryFn<MapboxMapProps<Location>> = Template.bind({});

CustomRenderPin.args = {
renderPin: (props) => renderReact(<MapPin {...props} />, props.container),
};

CustomRenderPin.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const mapPin = await canvas.findByLabelText('Show pin details', undefined, {
timeout: 30000
});
fireEvent.click(mapPin);
await canvas.findByText('title1');
};
21 changes: 21 additions & 0 deletions tests/components/MapboxMap.test.tsx
Original file line number Diff line number Diff line change
@@ -113,3 +113,24 @@ it('registers "onDrag" callback to Mapbox\'s event listener for "drag to pan" in
jest.advanceTimersByTime(100); //debounce time
expect(onDragFn).toBeCalledTimes(1);
});

it('uses PinComponent and logs warning if both PinComponent and renderPin are provided', () => {
mockAnswersState(mockedStateDefaultCoordinate);
jest.spyOn(Marker.prototype, 'setLngLat').mockReturnValue(Marker.prototype);
const PinComponent = jest.fn().mockImplementation(() => null);
const renderPin = jest.fn();
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();

render(<MapboxMap
mapboxAccessToken='TEST_KEY'
PinComponent={PinComponent}
renderPin={renderPin}
/>);

expect(warnSpy).toBeCalledTimes(1);
expect(warnSpy).toBeCalledWith(
'Found both PinComponent and renderPin props. Using PinComponent.'
);
expect(PinComponent).toBeCalledTimes(1);
expect(renderPin).not.toBeCalled();
});
16 changes: 8 additions & 8 deletions tests/components/NumericalFacetContent.test.tsx
Original file line number Diff line number Diff line change
@@ -68,31 +68,31 @@ describe('NumericalFacetContent', () => {
});
});

it('Clicking a selected number range facet option checkbox unselects it', () => {
it('Clicking a selected number range facet option checkbox unselects it', async () => {
const actions = spyOnActions();
render(mockNumericalFacet());

const expensiveCheckbox: HTMLInputElement =
screen.getByLabelText(numericalFacet.options[0].displayName);
expect(expensiveCheckbox.checked).toBeTruthy();

userEvent.click(expensiveCheckbox);
await userEvent.click(expensiveCheckbox);
expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[0], false);
});

it('Clicking an unselected number range facet option checkbox selects it', () => {
it('Clicking an unselected number range facet option checkbox selects it', async () => {
const actions = spyOnActions();
render(mockNumericalFacet());

const cheapCheckbox: HTMLInputElement =
screen.getByLabelText(numericalFacet.options[1].displayName);
expect(cheapCheckbox.checked).toBeFalsy();

userEvent.click(cheapCheckbox);
await userEvent.click(cheapCheckbox);
expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[1], true);
});

it('getFilterDisplayName field works as expected', () => {
it('getFilterDisplayName field works as expected', async () => {
const facets = [{
...numericalFacet,
options: numericalFacet.options.map(o => ({ ...o, selected: false }))
@@ -108,9 +108,9 @@ describe('NumericalFacetContent', () => {
render(mockNumericalFacet(
{ fieldId: numericalFacet.fieldId, getFilterDisplayName: getFilterDisplayName }));

userEvent.type(screen.getByPlaceholderText('Min'), '1');
userEvent.type(screen.getByPlaceholderText('Max'), '5');
userEvent.click(screen.getByText('Apply'));
await userEvent.type(screen.getByPlaceholderText('Min'), '1');
await userEvent.type(screen.getByPlaceholderText('Max'), '5');
await userEvent.click(screen.getByText('Apply'));

const expectedSelectableFilter: SelectableStaticFilter = {
displayName: 'start-1 end-5',
16 changes: 8 additions & 8 deletions tests/components/NumericalFacets.test.tsx
Original file line number Diff line number Diff line change
@@ -63,31 +63,31 @@ describe('NumericalFacets', () => {
});
});

it('Clicking a selected number range facet option checkbox unselects it', () => {
it('Clicking a selected number range facet option checkbox unselects it', async () => {
const actions = spyOnActions();
render(<NumericalFacets />);

const priceFacet = DisplayableFacets[1];
const expensiveCheckbox: HTMLInputElement = screen.getByLabelText(priceFacet.options[0].displayName);
expect(expensiveCheckbox.checked).toBeTruthy();

userEvent.click(expensiveCheckbox);
await userEvent.click(expensiveCheckbox);
expectFacetOptionSet(actions, priceFacet.fieldId, priceFacet.options[0], false);
});

it('Clicking an unselected number range facet option checkbox selects it', () => {
it('Clicking an unselected number range facet option checkbox selects it', async () => {
const actions = spyOnActions();
render(<NumericalFacets />);

const priceFacet = DisplayableFacets[1];
const cheapCheckbox: HTMLInputElement = screen.getByLabelText(priceFacet.options[1].displayName);
expect(cheapCheckbox.checked).toBeFalsy();

userEvent.click(cheapCheckbox);
await userEvent.click(cheapCheckbox);
expectFacetOptionSet(actions, priceFacet.fieldId, priceFacet.options[1], true);
});

it('getFilterDisplayName field works as expected', () => {
it('getFilterDisplayName field works as expected', async () => {
const displayableFacets = [{
...DisplayableFacets[1],
options: DisplayableFacets[1].options.map(o => ({ ...o, selected: false }))
@@ -103,9 +103,9 @@ describe('NumericalFacets', () => {
};
const actions = spyOnActions();
render(<NumericalFacets getFilterDisplayName={getFilterDisplayName}/>);
userEvent.type(screen.getByPlaceholderText('Min'), '1');
userEvent.type(screen.getByPlaceholderText('Max'), '5');
userEvent.click(screen.getByText('Apply'));
await userEvent.type(screen.getByPlaceholderText('Min'), '1');
await userEvent.type(screen.getByPlaceholderText('Max'), '5');
await userEvent.click(screen.getByText('Apply'));

const expectedSelectableFilter: SelectableStaticFilter = {
displayName: 'start-1 end-5',
4 changes: 2 additions & 2 deletions tests/components/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -142,7 +142,7 @@ describe('results are returned from search', () => {
expect(screen.getByText('...')).toBeDefined();
});

it('checks that navigation buttons trigger a new search', () => {
it('checks that navigation buttons trigger a new search', async () => {
const mockedResultsCount = 5;
const mockedVerticalSearchState: VerticalSearchState = {
resultsCount: mockedResultsCount,
@@ -155,7 +155,7 @@ describe('results are returned from search', () => {
render(<Pagination />);

// navigate to the last results page
userEvent.click(screen.getByText(`${mockedResultsCount}`));
await userEvent.click(screen.getByText(`${mockedResultsCount}`));
expect(actions.setOffset).toHaveBeenCalledWith(mockedResultsCount - 1);
expect(actions.executeVerticalQuery).toHaveBeenCalledTimes(1);
});
8 changes: 4 additions & 4 deletions tests/components/RangeInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -3,9 +3,9 @@ import { RangeInput, RangeInputProps } from '../../src/components/Filters/RangeI
import { SearchHeadlessContext } from '@yext/search-headless-react';
import { generateMockedHeadless } from '../__fixtures__/search-headless';
import { FiltersContext } from '../../src/components/Filters/FiltersContext';
import { FilterGroupContext } from '../../src/components/Filters/FilterGroupContext';
import { FilterGroupProvider } from '../../src/components/Filters/FilterGroupProvider';
import { userEvent, within } from '@storybook/testing-library';
import { filterContextValue, filterContextValueDisabled, filterGroupContextValue } from '../__fixtures__/data/filtercontext';
import { filterContextValue, filterContextValueDisabled } from '../__fixtures__/data/filtercontext';
import React from 'react';

const meta: Meta<typeof RangeInput> = {
@@ -18,9 +18,9 @@ const meta: Meta<typeof RangeInput> = {
},
decorators: [(Story) => (
<SearchHeadlessContext.Provider value={generateMockedHeadless()}>
<FilterGroupContext.Provider value={filterGroupContextValue}>
<FilterGroupProvider fieldId={'123'}>
<Story />
</FilterGroupContext.Provider>
</FilterGroupProvider>
</SearchHeadlessContext.Provider>
)]
};
74 changes: 32 additions & 42 deletions tests/components/RangeInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import userEvent from '@testing-library/user-event';
import { State } from '@yext/search-headless-react';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { RangeInput } from '../../src/components/Filters';
import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks';
import { FiltersContext, FiltersContextType } from '../../src/components/Filters/FiltersContext';
import { FilterGroupContext } from '../../src/components/Filters/FilterGroupContext';
import { filterContextValue, filterContextValueDisabled, filterGroupContextValue } from '../__fixtures__/data/filtercontext';
import { filterContextValue, filterContextValueDisabled } from '../__fixtures__/data/filtercontext';
import { FilterGroupProvider } from '../../src/components/Filters/FilterGroupProvider';
import React from 'react';

const mockedState: Partial<State> = {
@@ -44,13 +44,11 @@ describe('Renders correctly for min input', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const minTextbox = screen.getAllByRole('textbox')[0];
userEvent.type(minTextbox, '10');
await waitFor(() => {
expect(minTextbox).toHaveValue('10');
});
await userEvent.type(minTextbox, '10');
expect(minTextbox).toHaveValue('10');
expect(screen.getByText('Clear min and max')).toBeDefined();
expect(screen.getByText('Apply')).toBeDefined();
userEvent.click(screen.getByText('Apply'));
await userEvent.click(screen.getByText('Apply'));
expect(actions.setFilterOption).toHaveBeenCalledWith({
displayName: 'Over 10',
selected: true,
@@ -72,11 +70,10 @@ describe('Renders correctly for min input', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const minTextbox = screen.getAllByRole('textbox')[0];
userEvent.type(minTextbox, '10');
await waitFor(() => {
expect(minTextbox).toHaveValue('10');
});
userEvent.click(screen.getByText('Clear min and max'));
await userEvent.type(minTextbox, '10');
expect(minTextbox).toHaveValue('10');

await userEvent.click(screen.getByText('Clear min and max'));
expect(minTextbox).toHaveValue('');
expect(actions.setFilterOption).toHaveBeenCalledWith({
displayName: 'Over 10',
@@ -105,13 +102,11 @@ describe('Renders correctly for max input', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const maxTextbox = screen.getAllByRole('textbox')[1];
userEvent.type(maxTextbox, '20');
await waitFor(() => {
expect(maxTextbox).toHaveValue('20');
});
await userEvent.type(maxTextbox, '20');
expect(maxTextbox).toHaveValue('20');
expect(screen.getByText('Clear min and max')).toBeDefined();
expect(screen.getByText('Apply')).toBeDefined();
userEvent.click(screen.getByText('Apply'));
await userEvent.click(screen.getByText('Apply'));
expect(actions.setFilterOption).toHaveBeenCalledWith({
displayName: 'Up to 20',
selected: true,
@@ -133,11 +128,10 @@ describe('Renders correctly for max input', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const maxTextbox = screen.getAllByRole('textbox')[1];
userEvent.type(maxTextbox, '20');
await waitFor(() => {
expect(maxTextbox).toHaveValue('20');
});
userEvent.click(screen.getByText('Clear min and max'));
await userEvent.type(maxTextbox, '20');
expect(maxTextbox).toHaveValue('20');

await userEvent.click(screen.getByText('Clear min and max'));
expect(maxTextbox).toHaveValue('');
expect(actions.setFilterOption).toHaveBeenCalledWith({
displayName: 'Up to 20',
@@ -166,15 +160,13 @@ describe('Renders correctly for min and max inputs', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const [minTextbox, maxTextbox] = screen.getAllByRole('textbox');
userEvent.type(minTextbox, '10');
userEvent.type(maxTextbox, '20');
await waitFor(() => {
expect(minTextbox).toHaveValue('10');
});
await userEvent.type(minTextbox, '10');
await userEvent.type(maxTextbox, '20');
expect(minTextbox).toHaveValue('10');
expect(maxTextbox).toHaveValue('20');
expect(screen.getByText('Apply')).toBeDefined();
expect(screen.getByText('Clear min and max')).toBeDefined();
userEvent.click(screen.getByText('Apply'));
await userEvent.click(screen.getByText('Apply'));
expect(actions.setFilterOption).toHaveBeenCalledWith({
displayName: '10 - 20',
selected: true,
@@ -200,13 +192,12 @@ describe('Renders correctly for min and max inputs', () => {
renderRangeInput(filterContextValue);
const actions = spyOnActions();
const [minTextbox, maxTextbox] = screen.getAllByRole('textbox');
userEvent.type(minTextbox, '10');
userEvent.type(maxTextbox, '20');
await waitFor(() => {
expect(minTextbox).toHaveValue('10');
});
await userEvent.type(minTextbox, '10');
await userEvent.type(maxTextbox, '20');

expect(minTextbox).toHaveValue('10');
expect(maxTextbox).toHaveValue('20');
userEvent.click(screen.getByText('Clear min and max'));
await userEvent.click(screen.getByText('Clear min and max'));
expect(minTextbox).toHaveValue('');
expect(maxTextbox).toHaveValue('');
expect(actions.setFilterOption).toHaveBeenCalledWith({
@@ -233,12 +224,11 @@ describe('Renders correctly for min and max inputs', () => {
it('renders correctly when input range is invalid and no filter is set in state', async () => {
renderRangeInput(filterContextValue);
const [minTextbox, maxTextbox] = screen.getAllByRole('textbox');
userEvent.type(minTextbox, '20');
userEvent.type(maxTextbox, '10');
await userEvent.type(minTextbox, '20');
await userEvent.type(maxTextbox, '10');
const actions = spyOnActions();
await waitFor(() => {
expect(minTextbox).toHaveValue('20');
});

expect(minTextbox).toHaveValue('20');
expect(maxTextbox).toHaveValue('10');
expect(screen.getByText('Invalid range')).toBeDefined();
expect(actions.setFilterOption).toHaveBeenCalledTimes(0);
@@ -256,10 +246,10 @@ it('renders correctly when disabled', () => {
function renderRangeInput(filtersContextValue: FiltersContextType) {
return (
render(
<FilterGroupContext.Provider value={filterGroupContextValue}>
<FilterGroupProvider fieldId={'123'}>
<FiltersContext.Provider value={filtersContextValue}>
<RangeInput />
</FiltersContext.Provider>
</FilterGroupContext.Provider>)
</FilterGroupProvider>)
);
}
112 changes: 55 additions & 57 deletions tests/components/SearchBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SearchIntent, QuerySource, SearchCore, SearchHeadlessContext, State } from '@yext/search-headless-react';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { SearchBar } from '../../src/components/SearchBar';
import userEvent from '@testing-library/user-event';
import { generateMockedHeadless } from '../__fixtures__/search-headless';
@@ -48,7 +48,7 @@ describe('SearchBar', () => {

expect(screen.queryByText('query suggestion 1')).not.toBeInTheDocument();
expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument();
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion 1')).toBeInTheDocument();
expect(await screen.findByText('query suggestion 2')).toBeInTheDocument();
expect(mockedUniversalAutocomplete).toBeCalledTimes(1);
@@ -75,7 +75,7 @@ describe('SearchBar', () => {

expect(screen.queryByText('query suggestion 1')).not.toBeInTheDocument();
expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument();
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion 1')).toBeInTheDocument();
expect(await screen.findByText('query suggestion 2')).toBeInTheDocument();
expect(mockedVerticalAutocomplete).toBeCalledTimes(1);
@@ -102,10 +102,10 @@ describe('SearchBar', () => {
<SearchBar hideRecentSearches={true}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion 1')).toBeInTheDocument();
expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument();
userEvent.type(screen.getByRole('textbox'), 't');
await userEvent.type(screen.getByRole('textbox'), 't');
expect(await screen.findByText('query suggestion 2')).toBeInTheDocument();
expect(screen.queryByText('query suggestion 1')).not.toBeInTheDocument();
expect(mockedUniversalAutocomplete).toBeCalledTimes(2);
@@ -122,15 +122,15 @@ describe('SearchBar', () => {
<SearchBar hideRecentSearches={true}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion 1')).toBeInTheDocument();
userEvent.keyboard('{arrowdown}');
userEvent.keyboard('{enter}');
await userEvent.keyboard('{arrowdown}');
await userEvent.keyboard('{enter}');
expect(await screen.findByRole('textbox')).toHaveDisplayValue('query suggestion 1');
await waitFor(() => expect(mockedUniversalSearch).toHaveBeenCalledTimes(1));
await waitFor(() => expect(mockedUniversalSearch).toHaveBeenCalledWith(expect.objectContaining({
expect(mockedUniversalSearch).toHaveBeenCalledTimes(1)
expect(mockedUniversalSearch).toHaveBeenCalledWith(expect.objectContaining({
query: 'query suggestion 1'
})));
}));
});
});

@@ -156,7 +156,7 @@ describe('SearchBar', () => {
<SearchBar showVerticalLinks={true}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion')).toBeInTheDocument();
expect(await screen.findByText('in verticalKey1')).toBeInTheDocument();
expect(await screen.findByText('in verticalKey2')).toBeInTheDocument();
@@ -168,7 +168,7 @@ describe('SearchBar', () => {
<SearchBar />
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion')).toBeInTheDocument();
expect(screen.queryByText('in verticalKey1')).not.toBeInTheDocument();
expect(screen.queryByText('in verticalKey2')).not.toBeInTheDocument();
@@ -183,7 +183,7 @@ describe('SearchBar', () => {
}}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion')).toBeInTheDocument();
expect(await screen.findByText('in Vertical One')).toBeInTheDocument();
expect(await screen.findByText('in Vertical Two')).toBeInTheDocument();
@@ -196,9 +196,9 @@ describe('SearchBar', () => {
<SearchBar showVerticalLinks={true} onSelectVerticalLink={mockedOnSelectVerticalLink}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('in verticalKey1')).toBeInTheDocument();
userEvent.click(screen.getByText('in verticalKey1'));
await userEvent.click(screen.getByText('in verticalKey1'));
expect(mockedOnSelectVerticalLink).toBeCalledTimes(1);
expect(mockedOnSelectVerticalLink).toHaveBeenCalledWith({
verticalLink: {
@@ -217,12 +217,12 @@ describe('SearchBar', () => {
<SearchBar />
</SearchHeadlessContext.Provider>
);
userEvent.type(screen.getByRole('textbox'), 'yext');
userEvent.keyboard('{enter}');
userEvent.clear(screen.getByRole('textbox'));
userEvent.type(screen.getByRole('textbox'), 'answers');
userEvent.keyboard('{enter}');
userEvent.clear(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.keyboard('{enter}');
await userEvent.clear(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), 'answers');
await userEvent.keyboard('{enter}');
await userEvent.clear(screen.getByRole('textbox'));
expect(await screen.findByText('answers')).toBeInTheDocument();
expect(await screen.findByText('yext')).toBeInTheDocument();
});
@@ -233,13 +233,13 @@ describe('SearchBar', () => {
<SearchBar recentSearchesLimit={1}/>
</SearchHeadlessContext.Provider>
);
userEvent.type(screen.getByRole('textbox'), 'yext');
userEvent.keyboard('{enter}');
userEvent.clear(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.keyboard('{enter}');
await userEvent.clear(screen.getByRole('textbox'));
expect(await screen.findByText('yext')).toBeInTheDocument();
userEvent.type(screen.getByRole('textbox'), 'answers');
userEvent.keyboard('{enter}');
userEvent.clear(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), 'answers');
await userEvent.keyboard('{enter}');
await userEvent.clear(screen.getByRole('textbox'));
expect(await screen.findByText('answers')).toBeInTheDocument();
expect(screen.queryByText('yext')).not.toBeInTheDocument();
});
@@ -251,10 +251,10 @@ describe('SearchBar', () => {
</SearchHeadlessContext.Provider>
);

userEvent.type(screen.getByRole('textbox'), 'yext');
userEvent.keyboard('{enter}');
await userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.keyboard('{enter}');
expect(await screen.findByRole('textbox')).toHaveDisplayValue('yext');
userEvent.clear(screen.getByRole('textbox'));
await userEvent.clear(screen.getByRole('textbox'));
expect(await screen.findByRole('textbox')).toHaveDisplayValue('');
expect(screen.queryByText('yext')).not.toBeInTheDocument();
});
@@ -268,8 +268,8 @@ describe('SearchBar', () => {
);
const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch');
const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' });
userEvent.click(submitSearchButton);
await waitFor(() => expect(mockedUniversalSearch).toHaveBeenCalledTimes(1));
await userEvent.click(submitSearchButton);
expect(mockedUniversalSearch).toHaveBeenCalledTimes(1);
});

it('clear button deletes text in input element', async () => {
@@ -278,10 +278,10 @@ describe('SearchBar', () => {
<SearchBar />
</SearchHeadlessContext.Provider>
);
userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.type(screen.getByRole('textbox'), 'yext');
expect(await screen.findByRole('textbox')).toHaveDisplayValue('yext');
const clearSearchButton = screen.getByRole('button', { name: 'Clear the search bar' });
userEvent.click(clearSearchButton);
await userEvent.click(clearSearchButton);
expect(await screen.findByRole('textbox')).toHaveDisplayValue('');
});

@@ -293,15 +293,15 @@ describe('SearchBar', () => {
</SearchHeadlessContext.Provider>
);
const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch');
userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.type(screen.getByRole('textbox'), 'yext');
const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' });
userEvent.click(submitSearchButton);
await waitFor(() => expect(mockedOnSearch).toHaveBeenCalledTimes(1));
await waitFor(() => expect(mockedOnSearch).toHaveBeenCalledWith({
await userEvent.click(submitSearchButton);
expect(mockedOnSearch).toHaveBeenCalledTimes(1);
expect(mockedOnSearch).toHaveBeenCalledWith({
verticalKey: '',
query: 'yext'
}));
await waitFor(() => expect(mockedUniversalSearch).toHaveBeenCalledTimes(0));
});
expect(mockedUniversalSearch).toHaveBeenCalledTimes(0);
});

describe('executes search with near me location handling', () => {
@@ -324,12 +324,11 @@ describe('SearchBar', () => {
</SearchHeadlessContext.Provider>
);
const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' });
userEvent.click(submitSearchButton);
await waitFor(() => expect(mockedUniversalSearch)
await userEvent.click(submitSearchButton);
expect(mockedUniversalSearch)
.toHaveBeenCalledWith(expect.objectContaining({
location: userLocation
}))
);
}));
});

it('search with near me location handling using nagivator.geolocation API', async () => {
@@ -356,12 +355,11 @@ describe('SearchBar', () => {
</SearchHeadlessContext.Provider>
);
const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' });
userEvent.click(submitSearchButton);
await waitFor(() => expect(mockedUniversalSearch)
await userEvent.click(submitSearchButton);
expect(mockedUniversalSearch)
.toHaveBeenCalledWith(expect.objectContaining({
location: userLocation
}))
);
}));
});
});

@@ -387,10 +385,10 @@ describe('SearchBar', () => {
<SearchBar hideRecentSearches={true}/>
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText('query suggestion')).toBeInTheDocument();
userEvent.keyboard('{arrowdown}');
userEvent.keyboard('{enter}');
await userEvent.keyboard('{arrowdown}');
await userEvent.keyboard('{enter}');
expect(await screen.findByRole('textbox')).toHaveDisplayValue('query suggestion');
expect(mockedReport).toHaveBeenCalledTimes(1);
expect(mockedReport).toHaveBeenCalledWith({
@@ -413,7 +411,7 @@ describe('SearchBar', () => {
</SearchHeadlessContext.Provider>
);
const clearSearchButton = screen.getByRole('button', { name: 'Clear the search bar' });
userEvent.click(clearSearchButton);
await userEvent.click(clearSearchButton);
expect(await screen.findByRole('textbox')).toHaveDisplayValue('');
expect(mockedReport).toHaveBeenCalledTimes(1);
expect(mockedReport).toHaveBeenCalledWith({
@@ -452,7 +450,7 @@ describe('SearchBar', () => {
<SearchBar />
</SearchHeadlessContext.Provider>
);
userEvent.click(screen.getByRole('textbox'));
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText(
'2 autocomplete suggestions found.'
)).toBeInTheDocument();
@@ -464,9 +462,9 @@ describe('SearchBar', () => {
<SearchBar />
</SearchHeadlessContext.Provider>
);
userEvent.type(screen.getByRole('textbox'), 'yext');
userEvent.keyboard('{enter}');
userEvent.click(screen.getByRole('textbox'));
await userEvent.type(screen.getByRole('textbox'), 'yext');
await userEvent.keyboard('{enter}');
await userEvent.click(screen.getByRole('textbox'));
expect(await screen.findByText(
'1 recent search found.'
)).toBeInTheDocument();
8 changes: 4 additions & 4 deletions tests/components/SpellCheck.test.tsx
Original file line number Diff line number Diff line change
@@ -49,26 +49,26 @@ describe('SpellCheck', () => {
expect(label).toEqual(mockedState.spellCheck?.correctedQuery);
});

it('Fires onClick when provided', () => {
it('Fires onClick when provided', async () => {
const props = {
onClick: jest.fn()
};
const onClick = jest.spyOn(props, 'onClick');
const actions = spyOnActions();

render(<SpellCheck {...props} />);
userEvent.click(screen.getByRole('button'));
await userEvent.click(screen.getByRole('button'));

const verticalKey = mockedState.vertical?.verticalKey;
const correctedQuery = mockedState.spellCheck?.correctedQuery;
expect(actions.setQuery).toHaveBeenCalledWith(correctedQuery);
expect(onClick).toHaveBeenCalledWith({ correctedQuery, verticalKey });
});

it('Fires executeSearch when no onClick is provided', () => {
it('Fires executeSearch when no onClick is provided', async () => {
const actions = spyOnActions();
render(<SpellCheck />);
userEvent.click(screen.getByRole('button'));
await userEvent.click(screen.getByRole('button'));

const correctedQuery = mockedState.spellCheck?.correctedQuery;
expect(actions.setQuery).toHaveBeenCalledWith(correctedQuery);
8 changes: 4 additions & 4 deletions tests/components/StandardFacetContent.test.tsx
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ describe('StandardFacetContent', () => {
expect(coffeeLabelAndCount).toBeNull();
});

it('Clicking an unselected facet option label selects it', () => {
it('Clicking an unselected facet option label selects it', async() => {
const actions = spyOnActions();
render(mockStandardFacet());

@@ -90,19 +90,19 @@ describe('StandardFacetContent', () => {
expect(coffeeCheckbox.checked).toBeFalsy();

const coffeeLabel = screen.getByText(labelText);
userEvent.click(coffeeLabel);
await userEvent.click(coffeeLabel);
expectFacetOptionSet(actions, standardFacet.fieldId, coffeeFacetOption, true);
});

it('Clicking a selected facet option checkbox unselects it', () => {
it('Clicking a selected facet option checkbox unselects it', async() => {
const actions = spyOnActions();
render(mockStandardFacet());

const teaCheckbox: HTMLInputElement = screen.getByLabelText(
getOptionLabelTextWithCount(standardFacet.options[1]));
expect(teaCheckbox.checked).toBeTruthy();

userEvent.click(teaCheckbox);
await userEvent.click(teaCheckbox);
expectFacetOptionSet(actions, standardFacet.fieldId, standardFacet.options[1], false);
});
});
20 changes: 10 additions & 10 deletions tests/components/StandardFacets.test.tsx
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ describe('StandardFacets', () => {
});
});

it('Clicking an unselected facet option checkbox selects it', () => {
it('Clicking an unselected facet option checkbox selects it', async () => {
const actions = spyOnActions();
render(<StandardFacets />);

@@ -73,7 +73,7 @@ describe('StandardFacets', () => {
);
expect(coffeeCheckbox.checked).toBeFalsy();

userEvent.click(coffeeCheckbox);
await userEvent.click(coffeeCheckbox);
expectFacetOptionSet(actions, productFacet.fieldId, productFacet.options[0], true);
});

@@ -90,7 +90,7 @@ describe('StandardFacets', () => {
expect(coffeeLabelAndCount).toBeNull();
});

it('Clicking an unselected facet option label selects it', () => {
it('Clicking an unselected facet option label selects it', async () => {
const actions = spyOnActions();
render(<StandardFacets />);

@@ -101,11 +101,11 @@ describe('StandardFacets', () => {
expect(coffeeCheckbox.checked).toBeFalsy();

const coffeeLabel = screen.getByText(labelText);
userEvent.click(coffeeLabel);
await userEvent.click(coffeeLabel);
expectFacetOptionSet(actions, productFacet.fieldId, coffeeFacetOption, true);
});

it('Clicking a selected facet option checkbox unselects it', () => {
it('Clicking a selected facet option checkbox unselects it', async () => {
const actions = spyOnActions();
render(<StandardFacets />);

@@ -114,11 +114,11 @@ describe('StandardFacets', () => {
getOptionLabelTextWithCount(productFacet.options[1]));
expect(teaCheckbox.checked).toBeTruthy();

userEvent.click(teaCheckbox);
await userEvent.click(teaCheckbox);
expectFacetOptionSet(actions, productFacet.fieldId, productFacet.options[1], false);
});

it('Clicking a facet option executes a search when searchOnChange is true', () => {
it('Clicking a facet option executes a search when searchOnChange is true', async () => {
const actions = spyOnActions();
render(<StandardFacets />);

@@ -128,12 +128,12 @@ describe('StandardFacets', () => {
);
expect(coffeeCheckbox.checked).toBeFalsy();

userEvent.click(coffeeCheckbox);
await userEvent.click(coffeeCheckbox);
expectFacetOptionSet(actions, productFacet.fieldId, productFacet.options[0], true);
expect(actions.executeVerticalQuery).toBeCalled();
});

it('Clicking a facet option does not execute a search when searchOnChange is false', () => {
it('Clicking a facet option does not execute a search when searchOnChange is false', async () => {
const actions = spyOnActions();
render(<StandardFacets searchOnChange={false} />);

@@ -143,7 +143,7 @@ describe('StandardFacets', () => {
);
expect(coffeeCheckbox.checked).toBeFalsy();

userEvent.click(coffeeCheckbox);
await userEvent.click(coffeeCheckbox);
expectFacetOptionSet(actions, productFacet.fieldId, productFacet.options[0], true);
expect(actions.executeVerticalQuery).not.toBeCalled();
});
22 changes: 11 additions & 11 deletions tests/components/StaticFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { SearchActions, State } from '@yext/search-headless-react';
import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks';
@@ -6,7 +7,6 @@ import userEvent from '@testing-library/user-event';
import { StaticFilters } from '../../src/components';
import { staticFilters, staticFiltersProps } from '../__fixtures__/data/filters';
import { testSSR } from '../ssr/utils';
import React from 'react';

const mockedState: Partial<State> = {
filters: {
@@ -58,7 +58,7 @@ describe('Static Filters', () => {
expect(screen.getByText('Clifford')).toBeDefined();
});

it('Clicking an unselected filter option checkbox selects it', () => {
it('Clicking an unselected filter option checkbox selects it', async () => {
const actions = spyOnActions();
render(<StaticFilters {...staticFiltersProps} />);

@@ -68,19 +68,19 @@ describe('Static Filters', () => {
);
expect(martyCheckbox.checked).toBeFalsy();

userEvent.click(martyCheckbox);
await userEvent.click(martyCheckbox);
expectFilterOptionSet(actions, staticFiltersProps.fieldId, martyFilter, true);
});

it('Clicking a selected filter option checkbox unselects it', () => {
it('Clicking a selected filter option checkbox unselects it', async () => {
const actions = spyOnActions();
render(<StaticFilters {...staticFiltersProps} />);

const bleeckerFilter = staticFiltersProps.filterOptions[2];
const bleeckerCheckbox: HTMLInputElement = screen.getByLabelText(bleeckerFilter.value.toString());
expect(bleeckerCheckbox.checked).toBeTruthy();

userEvent.click(bleeckerCheckbox);
await userEvent.click(bleeckerCheckbox);
expectFilterOptionSet(actions, staticFiltersProps.fieldId, bleeckerFilter, false);
});

@@ -129,31 +129,31 @@ describe('Static Filters', () => {
expect(screen.getByRole('checkbox', { name: 'Clifford' })).toBeDefined();
});

it('Search input is added when searchable is true', () => {
it('Search input is added when searchable is true', async () => {
render(<StaticFilters {...staticFiltersProps} searchable={true} />);

const searchInput = screen.getByRole('textbox');
expect(searchInput).toBeDefined();
expect(screen.getAllByRole('checkbox')).toHaveLength(4);
userEvent.type(searchInput, 'dog');
await userEvent.type(searchInput, 'dog');
expect(screen.queryByRole('checkbox')).toBeNull();
});

it('Clicking a filter option executes a search when searchOnChange is true', () => {
it('Clicking a filter option executes a search when searchOnChange is true', async () => {
const actions = spyOnActions();
render(<StaticFilters {...staticFiltersProps} />);

const martyCheckbox: HTMLInputElement = screen.getByLabelText('MARTY!');
userEvent.click(martyCheckbox);
await userEvent.click(martyCheckbox);
expect(actions.executeVerticalQuery).toBeCalled();
});

it('Clicking a filter option does not execute a search when searchOnChange is false', () => {
it('Clicking a filter option does not execute a search when searchOnChange is false', async () => {
const actions = spyOnActions();
render(<StaticFilters {...staticFiltersProps} searchOnChange={false} />);

const martyCheckbox: HTMLInputElement = screen.getByLabelText('MARTY!');
userEvent.click(martyCheckbox);
await userEvent.click(martyCheckbox);
expect(actions.executeVerticalQuery).not.toBeCalled();
});
});
12 changes: 12 additions & 0 deletions tests/hooks/getRenderHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function getRenderHook() {
try {
// Attempt to import testing-library/react-hooks
const testingLibraryHooks = require('@testing-library/react-hooks');
return testingLibraryHooks.renderHook;
} catch (error) {
// Fallback to using testing-library/react
return require('@testing-library/react').renderHook;
}
}

export const renderHook = getRenderHook();
2 changes: 1 addition & 1 deletion tests/hooks/useComposedCssClasses.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useComposedCssClasses } from '../../src/hooks/useComposedCssClasses';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from './getRenderHook';
import * as tailwindMerge from 'tailwind-merge';

describe('when there are no custom classes', () => {
2 changes: 1 addition & 1 deletion tests/hooks/useRecentSearches.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRecentSearches } from '../../src/hooks/useRecentSearches';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from './getRenderHook';
import { act } from '@testing-library/react';

beforeEach(() => {
23 changes: 7 additions & 16 deletions tests/hooks/useSynchronizedRequest.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useSynchronizedRequest } from '../../src/hooks/useSynchronizedRequest';
import { renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/react';
import { renderHook } from './getRenderHook';

it('returns an updated execute request function with the same reference', async () => {
let requestFunction = async () => 0;
@@ -9,17 +8,13 @@ it('returns an updated execute request function with the same reference', async
);

const oldReturnedRequestFunction = result.current[1];
await waitFor(async () => {
expect(await oldReturnedRequestFunction()).toBe(0);
});
expect(await oldReturnedRequestFunction()).toBe(0);

requestFunction = async () => 1;
rerender();

const newReturnedRequestFunction = result.current[1];
await waitFor(async () => {
expect(await newReturnedRequestFunction()).toBe(1);
});
expect(await newReturnedRequestFunction()).toBe(1);

expect(oldReturnedRequestFunction).toBe(newReturnedRequestFunction);
});
@@ -34,19 +29,15 @@ it('uses a new error function while returning same execute request reference', a
);

const oldReturnedRequestFunction = result.current[1];
oldReturnedRequestFunction();
await waitFor(() => {
expect(mockedErrorFunction).toBeCalledTimes(1);
});
await oldReturnedRequestFunction();
expect(mockedErrorFunction).toBeCalledTimes(1);

mockedErrorFunction = jest.fn().mockReturnValue(1);
rerender();

const newReturnedRequestFunction = result.current[1];
newReturnedRequestFunction();
await waitFor(() => {
expect(mockedErrorFunction).toBeCalledTimes(1);
});
await newReturnedRequestFunction();
expect(mockedErrorFunction).toBeCalledTimes(1);

expect(oldReturnedRequestFunction).toBe(newReturnedRequestFunction);
});