diff --git a/src/components/MapboxMap.tsx b/src/components/MapboxMap.tsx index d86ef3b9c..18ea2c32c 100644 --- a/src/components/MapboxMap.tsx +++ b/src/components/MapboxMap.tsx @@ -3,6 +3,7 @@ import mapboxgl from 'mapbox-gl'; import { Result, useSearchState } from '@yext/search-headless-react'; import { useDebouncedFunction } from '../hooks/useDebouncedFunction'; import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; /** * Props for rendering a custom marker on the map. @@ -158,6 +159,7 @@ export function MapboxMap({ if (markerLocation) { const { latitude, longitude } = markerLocation; const el = document.createElement('div'); + const root = createRoot(el); const markerOptions: mapboxgl.MarkerOptions = {}; if (PinComponent) { if (renderPin) { @@ -165,11 +167,11 @@ export function MapboxMap({ 'Found both PinComponent and renderPin props. Using PinComponent.' ); } - ReactDOM.render(, el); + />); markerOptions.element = el; } else if (renderPin) { renderPin({ index: i, mapbox, result, container: el }); diff --git a/tests/components/DirectAnswer.test.tsx b/tests/components/DirectAnswer.test.tsx index c05439229..a296d3d56 100644 --- a/tests/components/DirectAnswer.test.tsx +++ b/tests/components/DirectAnswer.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { DirectAnswerState } from '@yext/search-headless-react'; import { useAnalytics } from '../../src/hooks/useAnalytics'; import { DirectAnswer } from '../../src/components/DirectAnswer'; @@ -7,6 +7,14 @@ import { fieldValueDAState, featuredSnippetDAState } from '../__fixtures__/data/ import userEvent from '@testing-library/user-event'; import React from 'react'; +function renderElement(element: React.ReactElement) { + return React.act(() => render(element)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + jest.mock('@yext/search-headless-react'); jest.mock('../../src/hooks/useAnalytics', () => { @@ -34,10 +42,10 @@ describe('Featured snippet direct answer analytics', () => { function runAnalyticsTestSuite() { it('reports link click analytics', async () => { - render(); + await renderElement(); ignoreLinkClickErrors(); const link = screen.getByRole('link'); - await userEvent.click(link); + await click(link); expect(useAnalytics()?.report).toHaveBeenCalledTimes(1); expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({ type: 'CTA_CLICK', @@ -48,9 +56,9 @@ function runAnalyticsTestSuite() { }); it('reports THUMBS_UP feedback', async () => { - render(); + await renderElement(); const thumbsUp = screen.queryAllByRole('button')[0]; - await userEvent.click(thumbsUp); + await click(thumbsUp); expect(useAnalytics()?.report).toHaveBeenCalledTimes(1); expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({ type: 'THUMBS_UP', @@ -61,9 +69,9 @@ function runAnalyticsTestSuite() { }); it('reports THUMBS_DOWN feedback', async () => { - render(); + await renderElement(); const thumbsDown = screen.queryAllByRole('button')[1]; - await userEvent.click(thumbsDown); + await click(thumbsDown); expect(useAnalytics()?.report).toHaveBeenCalledTimes(1); expect(useAnalytics()?.report).toHaveBeenCalledWith(expect.objectContaining({ type: 'THUMBS_DOWN', diff --git a/tests/components/Dropdown.test.tsx b/tests/components/Dropdown.test.tsx index 6bb0fa9a1..7b318a347 100644 --- a/tests/components/Dropdown.test.tsx +++ b/tests/components/Dropdown.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } 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'; @@ -7,6 +7,22 @@ import { DropdownMenu } from '../../src/components/Dropdown/DropdownMenu'; import { DropdownItem } from '../../src/components/Dropdown/DropdownItem'; import { testSSR } from '../ssr/utils'; +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + +async function tab() { + return waitFor(() => userEvent.tab()); +} + +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function keyboard(input: string) { + return waitFor(() => userEvent.keyboard(input)); +} + describe('Dropdown', () => { it('renders identical content between the server and the client.', () => { testSSR( @@ -27,7 +43,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onToggle: mockedOnToggleFn }; - render( + await React.act(() => render(
@@ -39,31 +55,31 @@ describe('Dropdown', () => {
external div
- ); + )); // hidden by default expect(screen.queryByText('item1')).toBeNull(); // display when click into dropdown input - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(screen.getByText('item1')).toBeDefined(); - expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined); + await waitFor(() => expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined)); // hidden when click elsewhere outside of dropdown component - await userEvent.click(screen.getByText('external div')); + await click(screen.getByText('external div')); expect(screen.queryByText('item1')).toBeNull(); - expect(mockedOnToggleFn).toBeCalledWith(false, '', '', -1, undefined); + await waitFor(() => expect(mockedOnToggleFn).toBeCalledWith(false, '', '', -1, undefined)); // display when tab into dropdown input - await userEvent.tab(); + await tab(); expect(screen.getByText('item1')).toBeDefined(); - expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined); + await waitFor(() => expect(mockedOnToggleFn).toBeCalledWith(true, '', '', -1, undefined)); }); it('handles arrowkey navigation properly and focuses on the option and input text', async () => { const dropdownProps: DropdownProps = { screenReaderText: 'screen reader text here' }; - render( + await React.act(() => render( @@ -72,16 +88,16 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); const itemNode = screen.getByText('item1'); - await userEvent.keyboard('{arrowdown}'); + await keyboard('{arrowdown}'); expect(itemNode.className).toContain('FocusedItem1'); expect(inputNode).toHaveValue('item1'); - await userEvent.keyboard('{arrowup}'); + await keyboard('{arrowup}'); expect(itemNode.className).not.toContain('FocusedItem1'); expect(inputNode).not.toHaveValue('item1'); }); @@ -90,7 +106,7 @@ describe('Dropdown', () => { const dropdownProps: DropdownProps = { screenReaderText: 'screen reader text here' }; - render( + await React.act(() => render( @@ -99,16 +115,16 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); const itemNode = screen.getByText('item1'); - await userEvent.keyboard('{Tab}'); + await keyboard('{Tab}'); expect(itemNode.className).toContain('FocusedItem1'); expect(inputNode).toHaveValue('item1'); - await userEvent.keyboard('{Shift>}{Tab}{/Shift}'); + await keyboard('{Shift>}{Tab}{/Shift}'); expect(itemNode.className).not.toContain('FocusedItem1'); expect(inputNode).not.toHaveValue('item1'); }); @@ -119,7 +135,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onToggle: mockedOnToggleFn }; - render( + await React.act(() => render( @@ -128,10 +144,10 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); - await userEvent.keyboard('{Tab}{Tab}'); + await click(inputNode); + await keyboard('{Tab}{Tab}'); expect(mockedOnToggleFn).toHaveBeenLastCalledWith(false, '', 'item1', 0, undefined); }); @@ -142,7 +158,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onSelect: mockedOnSelectFn }; - render( + await React.act(() => render( @@ -151,14 +167,14 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); expect(screen.getByTestId('item1')).toBeDefined(); expect(inputNode).toHaveValue(''); - await userEvent.keyboard('{arrowdown}'); - await userEvent.keyboard('{enter}'); + await keyboard('{arrowdown}'); + await keyboard('{enter}'); expect(inputNode).toHaveValue('item1'); expect(screen.queryByTestId('item1')).toBeNull(); @@ -173,7 +189,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onSelect: mockedOnSelectFn }; - render( + await React.act(() => render( @@ -182,14 +198,14 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); expect(screen.getByTestId('item1')).toBeDefined(); expect(inputNode).toHaveValue(''); - await userEvent.keyboard('{arrowdown}'); - await userEvent.click(screen.getByTestId('item1')); + await keyboard('{arrowdown}'); + await click(screen.getByTestId('item1')); expect(inputNode).toHaveValue('item1'); expect(screen.queryByTestId('item1')).toBeNull(); @@ -205,7 +221,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onToggle: mockedOnToggleFn }; - render( + await React.act(() => render(
@@ -217,15 +233,15 @@ describe('Dropdown', () => {
external div
- ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); expect(screen.getByTestId('item1')).toBeDefined(); expect(inputNode).toHaveValue(''); - await userEvent.keyboard('i'); - await userEvent.keyboard('{arrowdown}'); - await userEvent.click(screen.getByText('external div')); + await keyboard('i'); + await keyboard('{arrowdown}'); + await click(screen.getByText('external div')); expect(inputNode).toHaveValue('item1'); expect(screen.queryByTestId('item1')).toBeNull(); @@ -238,7 +254,7 @@ describe('Dropdown', () => { const dropdownProps: DropdownProps = { screenReaderText: 'screen reader text here' }; - render( + await React.act(() => render( @@ -247,19 +263,19 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); const itemNode = screen.getByText('item1'); expect(itemNode).toBeDefined(); expect(inputNode).toHaveValue(''); - await userEvent.keyboard('{arrowdown}'); + await keyboard('{arrowdown}'); expect(itemNode.className).toContain('FocusedItem1'); expect(inputNode).toHaveValue('item1'); const userInput = ' someText'; - await userEvent.type(inputNode, userInput); + await type(inputNode, userInput); expect(inputNode).toHaveValue('item1' + userInput); expect(mockedOnChangeFn).toBeCalledTimes(userInput.length); expect(mockedOnChangeFn).toHaveBeenCalledWith('item1' + userInput); @@ -274,7 +290,7 @@ describe('Dropdown', () => { screenReaderText: 'screen reader text here', onSelect: mockedOnSelectFn }; - render( + await React.act(() => render( @@ -283,10 +299,10 @@ describe('Dropdown', () => { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.type(inputNode, 'someText'); - await userEvent.keyboard('{enter}'); + await type(inputNode, 'someText'); + await keyboard('{enter}'); expect(inputNode).toHaveValue('someText'); expect(mockedOnSelectFn).toBeCalledTimes(0); @@ -301,7 +317,7 @@ describe('Dropdown', () => { const dropdownProps: DropdownProps = { screenReaderText: 'screen reader text here' }; - render( + await React.act(() => render( { - ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.type(inputNode, 'someText'); - await userEvent.keyboard('{enter}'); + await type(inputNode, 'someText'); + await keyboard('{enter}'); expect(inputNode).toHaveValue('someText'); expect(mockedSubmitCriteriaFn).toBeCalledTimes(1); @@ -333,7 +349,7 @@ describe('Always Select Option', () => { onSelect: mockedOnSelectFn, alwaysSelectOption: true }; - render( + await React.act(() => render(
@@ -345,14 +361,14 @@ describe('Always Select Option', () => {
external div
- ); + )); const inputNode = screen.getByRole('textbox'); - await userEvent.click(inputNode); + await click(inputNode); expect(screen.getByTestId('item1')).toBeDefined(); expect(inputNode).toHaveValue(''); - await userEvent.keyboard('i'); - await userEvent.click(screen.getByText('external div')); + await keyboard('i'); + await click(screen.getByText('external div')); expect(inputNode).toHaveValue('i'); expect(screen.queryByTestId('item1')).toBeNull(); diff --git a/tests/components/Geolocation.test.tsx b/tests/components/Geolocation.test.tsx index 2b3e35ba5..baca6f139 100644 --- a/tests/components/Geolocation.test.tsx +++ b/tests/components/Geolocation.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } 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'; @@ -131,14 +131,18 @@ beforeEach(() => { jest.spyOn(locationOperations, 'getUserLocation').mockResolvedValue(newGeoPosition); }); +function renderElement(element: React.ReactElement) { + return React.act(() => render(element)); +} + it('renders custom label when provided', () => { - render(); + renderElement(); const updateLocationButton = screen.getByRole('button', { name: 'Click me!' }); expect(updateLocationButton).toBeDefined(); }); it('renders custom icon when provided', () => { - render( Custom Icon} />); + renderElement( Custom Icon} />); const LocationIcon = screen.getByAltText('Custom Icon'); expect(LocationIcon).toBeDefined(); }); @@ -147,7 +151,7 @@ describe('custom click handler', () => { it('executes handleClick when user\'s location is successfully determined', async () => { const mockedHandleClickFn = jest.fn(); const actions = spyOnActions(); - render(); + await renderElement(); await clickUpdateLocation(); expect(mockedHandleClickFn).toHaveBeenCalledWith(newGeoPosition); @@ -243,7 +247,7 @@ describe('default click handler', () => { async function clickUpdateLocation() { const updateLocationButton = screen.getByRole('button'); - await userEvent.click(updateLocationButton); + await waitFor(() => userEvent.click(updateLocationButton)); } function createLocationFilter(radius: number = 50 * 1609.344): SelectableStaticFilter { diff --git a/tests/components/HierarchicalFacetContent.test.tsx b/tests/components/HierarchicalFacetContent.test.tsx index 401172ce5..2b575cf36 100644 --- a/tests/components/HierarchicalFacetContent.test.tsx +++ b/tests/components/HierarchicalFacetContent.test.tsx @@ -48,7 +48,11 @@ const mockHierarchicalFacet = (props?: HierarchicalFacetProps) => { return ( {facets => facets.map(facet => ( - ))} + ))} ); }; diff --git a/tests/components/LocationBias.test.tsx b/tests/components/LocationBias.test.tsx index 3e8d4e959..42ccd9f99 100644 --- a/tests/components/LocationBias.test.tsx +++ b/tests/components/LocationBias.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } 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'; @@ -142,5 +142,5 @@ it('renders nothing if there is no display name', () => { async function clickUpdateLocation() { const updateLocationButton = screen.getByRole('button', { name: 'Update your location' }); - await userEvent.click(updateLocationButton); + await waitFor(() => userEvent.click(updateLocationButton)); } diff --git a/tests/components/NumericalFacetContent.test.tsx b/tests/components/NumericalFacetContent.test.tsx index f5dfd2eca..7ab55a18d 100644 --- a/tests/components/NumericalFacetContent.test.tsx +++ b/tests/components/NumericalFacetContent.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { SearchActions, FacetOption, Matcher, NumberRangeValue, SelectableStaticFilter, Source, State } from '@yext/search-headless-react'; import { mockAnswersHooks, mockAnswersState, spyOnActions } from '../__utils__/mocks'; import userEvent from '@testing-library/user-event'; @@ -50,10 +50,22 @@ const mockNumericalFacet = (props?: NumericalFacetProps) => { return ( {facets => facets.map(facet => ( - ))} + ))} ); }; +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + describe('NumericalFacetContent', () => { beforeEach(() => { mockAnswersHooks({ mockedState, mockedActions, mockedUtils }); @@ -76,7 +88,7 @@ describe('NumericalFacetContent', () => { screen.getByLabelText(numericalFacet.options[0].displayName); expect(expensiveCheckbox.checked).toBeTruthy(); - await userEvent.click(expensiveCheckbox); + await click(expensiveCheckbox); expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[0], false); }); @@ -88,7 +100,7 @@ describe('NumericalFacetContent', () => { screen.getByLabelText(numericalFacet.options[1].displayName); expect(cheapCheckbox.checked).toBeFalsy(); - await userEvent.click(cheapCheckbox); + await click(cheapCheckbox); expectFacetOptionSet(actions, numericalFacet.fieldId, numericalFacet.options[1], true); }); @@ -108,9 +120,9 @@ describe('NumericalFacetContent', () => { render(mockNumericalFacet( { fieldId: numericalFacet.fieldId, getFilterDisplayName: getFilterDisplayName })); - await userEvent.type(screen.getByPlaceholderText('Min'), '1'); - await userEvent.type(screen.getByPlaceholderText('Max'), '5'); - await userEvent.click(screen.getByText('Apply')); + await type(screen.getByPlaceholderText('Min'), '1'); + await type(screen.getByPlaceholderText('Max'), '5'); + await click(screen.getByText('Apply')); const expectedSelectableFilter: SelectableStaticFilter = { displayName: 'start-1 end-5', diff --git a/tests/components/NumericalFacets.test.tsx b/tests/components/NumericalFacets.test.tsx index f8dba7a1d..b4904ed1d 100644 --- a/tests/components/NumericalFacets.test.tsx +++ b/tests/components/NumericalFacets.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { SearchActions, FacetOption, Matcher, NumberRangeValue, SelectableStaticFilter, Source, State } from '@yext/search-headless-react'; import { mockAnswersHooks, mockAnswersState, spyOnActions } from '../__utils__/mocks'; import userEvent from '@testing-library/user-event'; @@ -41,6 +41,14 @@ const mockedUtils = { jest.mock('@yext/search-headless-react'); +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + describe('NumericalFacets', () => { beforeEach(() => { mockAnswersHooks({ mockedState, mockedActions, mockedUtils }); @@ -71,7 +79,7 @@ describe('NumericalFacets', () => { const expensiveCheckbox: HTMLInputElement = screen.getByLabelText(priceFacet.options[0].displayName); expect(expensiveCheckbox.checked).toBeTruthy(); - await userEvent.click(expensiveCheckbox); + await click(expensiveCheckbox); expectFacetOptionSet(actions, priceFacet.fieldId, priceFacet.options[0], false); }); @@ -83,7 +91,7 @@ describe('NumericalFacets', () => { const cheapCheckbox: HTMLInputElement = screen.getByLabelText(priceFacet.options[1].displayName); expect(cheapCheckbox.checked).toBeFalsy(); - await userEvent.click(cheapCheckbox); + await click(cheapCheckbox); expectFacetOptionSet(actions, priceFacet.fieldId, priceFacet.options[1], true); }); @@ -103,9 +111,9 @@ describe('NumericalFacets', () => { }; const actions = spyOnActions(); render(); - await userEvent.type(screen.getByPlaceholderText('Min'), '1'); - await userEvent.type(screen.getByPlaceholderText('Max'), '5'); - await userEvent.click(screen.getByText('Apply')); + await type(screen.getByPlaceholderText('Min'), '1'); + await type(screen.getByPlaceholderText('Max'), '5'); + await click(screen.getByText('Apply')); const expectedSelectableFilter: SelectableStaticFilter = { displayName: 'start-1 end-5', diff --git a/tests/components/RangeInput.test.tsx b/tests/components/RangeInput.test.tsx index c5eb51c3e..85684fe47 100644 --- a/tests/components/RangeInput.test.tsx +++ b/tests/components/RangeInput.test.tsx @@ -1,6 +1,6 @@ import userEvent from '@testing-library/user-event'; import { State } from '@yext/search-headless-react'; -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { RangeInput } from '../../src/components/Filters'; import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks'; import { FiltersContext, FiltersContextType } from '../../src/components/Filters/FiltersContext'; @@ -27,6 +27,14 @@ const mockedActions = { jest.mock('@yext/search-headless-react'); +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + it('renders the correct inital state', () => { renderRangeInput(filterContextValue); const [minTextbox, maxTextbox] = screen.getAllByRole('textbox'); @@ -44,11 +52,11 @@ describe('Renders correctly for min input', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const minTextbox = screen.getAllByRole('textbox')[0]; - await userEvent.type(minTextbox, '10'); + await type(minTextbox, '10'); expect(minTextbox).toHaveValue('10'); expect(screen.getByText('Clear min and max')).toBeDefined(); expect(screen.getByText('Apply')).toBeDefined(); - await userEvent.click(screen.getByText('Apply')); + await click(screen.getByText('Apply')); expect(actions.setFilterOption).toHaveBeenCalledWith({ displayName: 'Over 10', selected: true, @@ -70,10 +78,10 @@ describe('Renders correctly for min input', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const minTextbox = screen.getAllByRole('textbox')[0]; - await userEvent.type(minTextbox, '10'); + await type(minTextbox, '10'); expect(minTextbox).toHaveValue('10'); - - await userEvent.click(screen.getByText('Clear min and max')); + + await click(screen.getByText('Clear min and max')); expect(minTextbox).toHaveValue(''); expect(actions.setFilterOption).toHaveBeenCalledWith({ displayName: 'Over 10', @@ -102,11 +110,11 @@ describe('Renders correctly for max input', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const maxTextbox = screen.getAllByRole('textbox')[1]; - await userEvent.type(maxTextbox, '20'); + await type(maxTextbox, '20'); expect(maxTextbox).toHaveValue('20'); expect(screen.getByText('Clear min and max')).toBeDefined(); expect(screen.getByText('Apply')).toBeDefined(); - await userEvent.click(screen.getByText('Apply')); + await click(screen.getByText('Apply')); expect(actions.setFilterOption).toHaveBeenCalledWith({ displayName: 'Up to 20', selected: true, @@ -128,10 +136,10 @@ describe('Renders correctly for max input', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const maxTextbox = screen.getAllByRole('textbox')[1]; - await userEvent.type(maxTextbox, '20'); + await type(maxTextbox, '20'); expect(maxTextbox).toHaveValue('20'); - await userEvent.click(screen.getByText('Clear min and max')); + await click(screen.getByText('Clear min and max')); expect(maxTextbox).toHaveValue(''); expect(actions.setFilterOption).toHaveBeenCalledWith({ displayName: 'Up to 20', @@ -160,13 +168,13 @@ describe('Renders correctly for min and max inputs', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const [minTextbox, maxTextbox] = screen.getAllByRole('textbox'); - await userEvent.type(minTextbox, '10'); - await userEvent.type(maxTextbox, '20'); + await type(minTextbox, '10'); + await type(maxTextbox, '20'); expect(minTextbox).toHaveValue('10'); expect(maxTextbox).toHaveValue('20'); expect(screen.getByText('Apply')).toBeDefined(); expect(screen.getByText('Clear min and max')).toBeDefined(); - await userEvent.click(screen.getByText('Apply')); + await click(screen.getByText('Apply')); expect(actions.setFilterOption).toHaveBeenCalledWith({ displayName: '10 - 20', selected: true, @@ -192,12 +200,12 @@ describe('Renders correctly for min and max inputs', () => { renderRangeInput(filterContextValue); const actions = spyOnActions(); const [minTextbox, maxTextbox] = screen.getAllByRole('textbox'); - await userEvent.type(minTextbox, '10'); - await userEvent.type(maxTextbox, '20'); - + await type(minTextbox, '10'); + await type(maxTextbox, '20'); + expect(minTextbox).toHaveValue('10'); expect(maxTextbox).toHaveValue('20'); - await userEvent.click(screen.getByText('Clear min and max')); + await click(screen.getByText('Clear min and max')); expect(minTextbox).toHaveValue(''); expect(maxTextbox).toHaveValue(''); expect(actions.setFilterOption).toHaveBeenCalledWith({ @@ -224,10 +232,10 @@ 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'); - await userEvent.type(minTextbox, '20'); - await userEvent.type(maxTextbox, '10'); + await type(minTextbox, '20'); + await type(maxTextbox, '10'); const actions = spyOnActions(); - + expect(minTextbox).toHaveValue('20'); expect(maxTextbox).toHaveValue('10'); expect(screen.getByText('Invalid range')).toBeDefined(); diff --git a/tests/components/SearchBar.test.tsx b/tests/components/SearchBar.test.tsx index 28d68ed91..535c947c9 100644 --- a/tests/components/SearchBar.test.tsx +++ b/tests/components/SearchBar.test.tsx @@ -1,5 +1,5 @@ import { SearchIntent, QuerySource, SearchCore, SearchHeadlessContext, State } from '@yext/search-headless-react'; -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { SearchBar } from '../../src/components/SearchBar'; import userEvent from '@testing-library/user-event'; import { generateMockedHeadless } from '../__fixtures__/search-headless'; @@ -24,6 +24,26 @@ const mockedState: Partial = { location: {} }; +function renderElement(element: React.ReactElement) { + return React.act(() => render(element)); +} + +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + +async function keyboard(input: string) { + return waitFor(() => userEvent.keyboard(input)); +} + +async function clear(element: HTMLElement) { + return waitFor(() => userEvent.clear(element)); +} + describe('SearchBar', () => { describe('query suggestions', () => { const mockedAutocompleteResult = { @@ -40,7 +60,7 @@ describe('SearchBar', () => { .spyOn(SearchCore.prototype, 'universalAutocomplete') .mockResolvedValue(mockedAutocompleteResult); - render( + renderElement( @@ -48,7 +68,7 @@ describe('SearchBar', () => { expect(screen.queryByText('query suggestion 1')).not.toBeInTheDocument(); expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument(); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('query suggestion 1')).toBeInTheDocument(); expect(await screen.findByText('query suggestion 2')).toBeInTheDocument(); expect(mockedUniversalAutocomplete).toBeCalledTimes(1); @@ -59,7 +79,7 @@ describe('SearchBar', () => { .spyOn(SearchCore.prototype, 'verticalAutocomplete') .mockResolvedValue(mockedAutocompleteResult); - render( + renderElement( { expect(screen.queryByText('query suggestion 1')).not.toBeInTheDocument(); expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument(); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('query suggestion 1')).toBeInTheDocument(); expect(await screen.findByText('query suggestion 2')).toBeInTheDocument(); expect(mockedVerticalAutocomplete).toBeCalledTimes(1); @@ -97,15 +117,15 @@ describe('SearchBar', () => { .mockResolvedValueOnce(mockedUniversalAutocompleteResultOne) .mockResolvedValueOnce(mockedUniversalAutocompleteResultTwo); - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('query suggestion 1')).toBeInTheDocument(); expect(screen.queryByText('query suggestion 2')).not.toBeInTheDocument(); - await userEvent.type(screen.getByRole('textbox'), 't'); + await 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); @@ -117,15 +137,15 @@ describe('SearchBar', () => { const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch'); - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('query suggestion 1')).toBeInTheDocument(); - await userEvent.keyboard('{arrowdown}'); - await userEvent.keyboard('{enter}'); + await keyboard('{arrowdown}'); + await keyboard('{enter}'); expect(await screen.findByRole('textbox')).toHaveDisplayValue('query suggestion 1'); expect(mockedUniversalSearch).toHaveBeenCalledTimes(1) expect(mockedUniversalSearch).toHaveBeenCalledWith(expect.objectContaining({ @@ -151,31 +171,31 @@ describe('SearchBar', () => { }); it('displays vertical links as part of the query suggestions when showVerticalLinks is set to true', async () => { - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await 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(); }); it('does not display vertical links on default', async () => { - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await 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(); }); it('vertical links use display labels from verticalKeyToLabel when it is specified', async () => { - render( + renderElement( { const verticalLabels = { verticalKey1: 'Vertical One', verticalKey2: 'Vertical Two' }; @@ -183,7 +203,7 @@ describe('SearchBar', () => { }}/> ); - await userEvent.click(screen.getByRole('textbox')); + await 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(); @@ -191,14 +211,14 @@ describe('SearchBar', () => { it('executes onSelectVerticalLink callback when a vertical link is selected', async () => { const mockedOnSelectVerticalLink = jest.fn(); - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('in verticalKey1')).toBeInTheDocument(); - await userEvent.click(screen.getByText('in verticalKey1')); + await click(screen.getByText('in verticalKey1')); expect(mockedOnSelectVerticalLink).toBeCalledTimes(1); expect(mockedOnSelectVerticalLink).toHaveBeenCalledWith({ verticalLink: { @@ -212,90 +232,90 @@ describe('SearchBar', () => { describe('recent searches', () => { it('displays recent searches in dropdown after performing searches', async () => { - render( + renderElement( ); - 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')); + await type(screen.getByRole('textbox'), 'yext'); + await keyboard('{enter}'); + await clear(screen.getByRole('textbox')); + await type(screen.getByRole('textbox'), 'answers'); + await keyboard('{enter}'); + await clear(screen.getByRole('textbox')); expect(await screen.findByText('answers')).toBeInTheDocument(); expect(await screen.findByText('yext')).toBeInTheDocument(); }); it('displays limited recent search results in dropdown based on recentSearchesLimit', async () => { - render( + renderElement( ); - await userEvent.type(screen.getByRole('textbox'), 'yext'); - await userEvent.keyboard('{enter}'); - await userEvent.clear(screen.getByRole('textbox')); + await type(screen.getByRole('textbox'), 'yext'); + await keyboard('{enter}'); + await clear(screen.getByRole('textbox')); expect(await screen.findByText('yext')).toBeInTheDocument(); - await userEvent.type(screen.getByRole('textbox'), 'answers'); - await userEvent.keyboard('{enter}'); - await userEvent.clear(screen.getByRole('textbox')); + await type(screen.getByRole('textbox'), 'answers'); + await keyboard('{enter}'); + await clear(screen.getByRole('textbox')); expect(await screen.findByText('answers')).toBeInTheDocument(); expect(screen.queryByText('yext')).not.toBeInTheDocument(); }); it('hides recent searches when hideRecentSearches is true', async () => { - render( + renderElement( ); - await userEvent.type(screen.getByRole('textbox'), 'yext'); - await userEvent.keyboard('{enter}'); + await type(screen.getByRole('textbox'), 'yext'); + await keyboard('{enter}'); expect(await screen.findByRole('textbox')).toHaveDisplayValue('yext'); - await userEvent.clear(screen.getByRole('textbox')); + await clear(screen.getByRole('textbox')); expect(await screen.findByRole('textbox')).toHaveDisplayValue(''); expect(screen.queryByText('yext')).not.toBeInTheDocument(); }); }); it('submit button executes a new search', async () => { - render( + renderElement( ); const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch'); const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' }); - await userEvent.click(submitSearchButton); + await click(submitSearchButton); expect(mockedUniversalSearch).toHaveBeenCalledTimes(1); }); it('clear button deletes text in input element', async () => { - render( + renderElement( ); - await userEvent.type(screen.getByRole('textbox'), 'yext'); + await type(screen.getByRole('textbox'), 'yext'); expect(await screen.findByRole('textbox')).toHaveDisplayValue('yext'); const clearSearchButton = screen.getByRole('button', { name: 'Clear the search bar' }); - await userEvent.click(clearSearchButton); + await click(clearSearchButton); expect(await screen.findByRole('textbox')).toHaveDisplayValue(''); }); it('executes onSearch callback when click on submit button', async () => { const mockedOnSearch = jest.fn(); - render( + renderElement( ); const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch'); - await userEvent.type(screen.getByRole('textbox'), 'yext'); + await type(screen.getByRole('textbox'), 'yext'); const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' }); - await userEvent.click(submitSearchButton); + await click(submitSearchButton); expect(mockedOnSearch).toHaveBeenCalledTimes(1); expect(mockedOnSearch).toHaveBeenCalledWith({ verticalKey: '', @@ -318,13 +338,13 @@ describe('SearchBar', () => { } }; const mockedUniversalSearch = jest.spyOn(SearchCore.prototype, 'universalSearch'); - render( + renderElement( ); const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' }); - await userEvent.click(submitSearchButton); + await click(submitSearchButton); expect(mockedUniversalSearch) .toHaveBeenCalledWith(expect.objectContaining({ location: userLocation @@ -349,13 +369,13 @@ describe('SearchBar', () => { Object.defineProperty(window.navigator, 'geolocation', { value: { getCurrentPosition: mockedGetCurrentPosition } }); - render( + renderElement( ); const submitSearchButton = screen.getByRole('button', { name: 'Submit Search' }); - await userEvent.click(submitSearchButton); + await click(submitSearchButton); expect(mockedUniversalSearch) .toHaveBeenCalledWith(expect.objectContaining({ location: userLocation @@ -380,15 +400,15 @@ describe('SearchBar', () => { jest.spyOn(SearchCore.prototype, 'universalAutocomplete') .mockResolvedValue(mockedAutocompleteResult); - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText('query suggestion')).toBeInTheDocument(); - await userEvent.keyboard('{arrowdown}'); - await userEvent.keyboard('{enter}'); + await keyboard('{arrowdown}'); + await keyboard('{enter}'); expect(await screen.findByRole('textbox')).toHaveDisplayValue('query suggestion'); expect(mockedReport).toHaveBeenCalledTimes(1); expect(mockedReport).toHaveBeenCalledWith({ @@ -405,13 +425,13 @@ describe('SearchBar', () => { input: 't' } }; - render( + renderElement( ); const clearSearchButton = screen.getByRole('button', { name: 'Clear the search bar' }); - await userEvent.click(clearSearchButton); + await click(clearSearchButton); expect(await screen.findByRole('textbox')).toHaveDisplayValue(''); expect(mockedReport).toHaveBeenCalledTimes(1); expect(mockedReport).toHaveBeenCalledWith({ @@ -424,7 +444,7 @@ describe('SearchBar', () => { describe('Screen reader text', () => { it('search bar instruction text for screen reader is present in DOM', () => { - render( + renderElement( @@ -445,29 +465,29 @@ describe('SearchBar', () => { }; jest.spyOn(SearchCore.prototype, 'universalAutocomplete') .mockResolvedValue(mockedAutocompleteResponse); - render( + renderElement( ); - await userEvent.click(screen.getByRole('textbox')); + await click(screen.getByRole('textbox')); expect(await screen.findByText( '2 autocomplete suggestions found.' )).toBeInTheDocument(); }); it('description text of number of available recent search options is present in DOM', async () => { - render( + renderElement( ); - await userEvent.type(screen.getByRole('textbox'), 'yext'); - await userEvent.keyboard('{enter}'); - await userEvent.click(screen.getByRole('textbox')); + await type(screen.getByRole('textbox'), 'yext'); + await keyboard('{enter}'); + await click(screen.getByRole('textbox')); expect(await screen.findByText( '1 recent search found.' )).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/tests/components/StandardFacetContent.test.tsx b/tests/components/StandardFacetContent.test.tsx index 46447f074..f3cc56913 100644 --- a/tests/components/StandardFacetContent.test.tsx +++ b/tests/components/StandardFacetContent.test.tsx @@ -49,7 +49,11 @@ const mockStandardFacet = (props?: StandardFacetProps) => { return ( {facets => facets.map(facet => ( - ))} + ))} ); }; @@ -118,4 +122,4 @@ function expectFacetOptionSet( { matcher: option.matcher, value: option.value }, selected ); -} \ No newline at end of file +} diff --git a/tests/components/StaticFilters.test.tsx b/tests/components/StaticFilters.test.tsx index 3e58d394c..754785ec7 100644 --- a/tests/components/StaticFilters.test.tsx +++ b/tests/components/StaticFilters.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import { SearchActions, State } from '@yext/search-headless-react'; import { mockAnswersHooks, spyOnActions } from '../__utils__/mocks'; import { FilterOptionConfig } from '../../src/components/Filters'; @@ -34,6 +34,14 @@ const mockedUtils = { } }; +async function type(element : HTMLElement, input: string) { + return waitFor(() => userEvent.type(element, input)); +} + +async function click(element : HTMLElement) { + return waitFor(() => userEvent.click(element)); +} + jest.mock('@yext/search-headless-react'); describe('Static Filters', () => { @@ -68,7 +76,7 @@ describe('Static Filters', () => { ); expect(martyCheckbox.checked).toBeFalsy(); - await userEvent.click(martyCheckbox); + await click(martyCheckbox); expectFilterOptionSet(actions, staticFiltersProps.fieldId, martyFilter, true); }); @@ -80,7 +88,7 @@ describe('Static Filters', () => { const bleeckerCheckbox: HTMLInputElement = screen.getByLabelText(bleeckerFilter.value.toString()); expect(bleeckerCheckbox.checked).toBeTruthy(); - await userEvent.click(bleeckerCheckbox); + await click(bleeckerCheckbox); expectFilterOptionSet(actions, staticFiltersProps.fieldId, bleeckerFilter, false); }); @@ -135,7 +143,7 @@ describe('Static Filters', () => { const searchInput = screen.getByRole('textbox'); expect(searchInput).toBeDefined(); expect(screen.getAllByRole('checkbox')).toHaveLength(4); - await userEvent.type(searchInput, 'dog'); + await type(searchInput, 'dog'); expect(screen.queryByRole('checkbox')).toBeNull(); }); @@ -144,7 +152,7 @@ describe('Static Filters', () => { render(); const martyCheckbox: HTMLInputElement = screen.getByLabelText('MARTY!'); - await userEvent.click(martyCheckbox); + await click(martyCheckbox); expect(actions.executeVerticalQuery).toBeCalled(); }); @@ -153,7 +161,7 @@ describe('Static Filters', () => { render(); const martyCheckbox: HTMLInputElement = screen.getByLabelText('MARTY!'); - await userEvent.click(martyCheckbox); + await click(martyCheckbox); expect(actions.executeVerticalQuery).not.toBeCalled(); }); }); diff --git a/tests/hooks/useSynchronizedRequest.test.tsx b/tests/hooks/useSynchronizedRequest.test.tsx index 9d7a61519..518bc12f2 100644 --- a/tests/hooks/useSynchronizedRequest.test.tsx +++ b/tests/hooks/useSynchronizedRequest.test.tsx @@ -1,5 +1,6 @@ import { useSynchronizedRequest } from '../../src/hooks/useSynchronizedRequest'; import { renderHook } from './getRenderHook'; +import {waitFor} from '@testing-library/react'; it('returns an updated execute request function with the same reference', async () => { let requestFunction = async () => 0; @@ -8,13 +9,13 @@ it('returns an updated execute request function with the same reference', async ); const oldReturnedRequestFunction = result.current[1]; - expect(await oldReturnedRequestFunction()).toBe(0); + expect(await waitFor(() => oldReturnedRequestFunction())).toBe(0); requestFunction = async () => 1; rerender(); const newReturnedRequestFunction = result.current[1]; - expect(await newReturnedRequestFunction()).toBe(1); + expect(await waitFor(() => newReturnedRequestFunction())).toBe(1); expect(oldReturnedRequestFunction).toBe(newReturnedRequestFunction); }); @@ -40,4 +41,4 @@ it('uses a new error function while returning same execute request reference', a expect(mockedErrorFunction).toBeCalledTimes(1); expect(oldReturnedRequestFunction).toBe(newReturnedRequestFunction); -}); \ No newline at end of file +});