Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Commit

Permalink
test(hooks): add tests for re-renders
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Jun 6, 2022
1 parent 81bbdf2 commit 2f38c97
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { act, render, waitFor } from '@testing-library/react';
import React, { Suspense, version as ReactVersion } from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { history } from 'instantsearch.js/es/lib/routers';
import { simple } from 'instantsearch.js/es/lib/stateMappings';
import React, { StrictMode, Suspense, version as ReactVersion } from 'react';
import { SearchBox } from 'react-instantsearch-hooks-web';

import { createSearchClient } from '../../../../../test/mock';
import { wait } from '../../../../../test/utils';
import { useRefinementList } from '../../connectors/useRefinementList';
import { useSearchBox } from '../../connectors/useSearchBox';
import { IndexContext } from '../../lib/IndexContext';
import { InstantSearchContext } from '../../lib/InstantSearchContext';
import version from '../../version';
Expand All @@ -15,11 +18,6 @@ import type { UseRefinementListProps } from '../../connectors/useRefinementList'
import type { InstantSearch as InstantSearchType } from 'instantsearch.js';
import type { IndexWidget } from 'instantsearch.js/es/widgets/index/index';

function SearchBox() {
useSearchBox();
return null;
}

function RefinementList(props: UseRefinementListProps) {
useRefinementList(props);
return null;
Expand Down Expand Up @@ -167,14 +165,14 @@ describe('InstantSearch', () => {

act(() => {
render(
<React.StrictMode>
<StrictMode>
<InstantSearch indexName="indexName" searchClient={searchClient}>
<SearchBox />
<Index indexName="subIndexName">
<RefinementList attribute="brand" />
</Index>
</InstantSearch>
</React.StrictMode>
</StrictMode>
);
});

Expand Down Expand Up @@ -203,7 +201,7 @@ describe('InstantSearch', () => {

act(() => {
render(
<React.StrictMode>
<StrictMode>
<InstantSearch indexName="indexName" searchClient={searchClient}>
<SearchBox />
<Suspense fallback={null}>
Expand All @@ -212,7 +210,7 @@ describe('InstantSearch', () => {
</Index>
</Suspense>
</InstantSearch>
</React.StrictMode>
</StrictMode>
);
});

Expand All @@ -236,6 +234,66 @@ describe('InstantSearch', () => {
});
});

test('renders with state from router in Strict Mode', async () => {
const searchClient = createSearchClient({});
const routing = {
stateMapping: simple(),
router: history({
getLocation() {
return new URL(
`http://localhost/?indexName[query]=iphone`
) as unknown as Location;
},
}),
};

function App() {
return (
<StrictMode>
<InstantSearch
searchClient={searchClient}
indexName="indexName"
routing={routing}
>
<SearchBox />
</InstantSearch>
</StrictMode>
);
}

const { rerender } = render(<App />);

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(1);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({ query: 'iphone' }),
},
]);
expect(screen.getByRole('searchbox')).toHaveValue('iphone');
});

rerender(<App />);

expect(screen.getByRole('searchbox')).toHaveValue('iphone');

userEvent.type(screen.getByRole('searchbox'), ' case', {
initialSelectionStart: 6,
});

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(6);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({ query: 'iphone case' }),
},
]);
expect(screen.getByRole('searchbox')).toHaveValue('iphone case');
});
});

test('renders components with a Suspense boundary', async () => {
const searchClient = createSearchClient({});

Expand Down Expand Up @@ -271,4 +329,174 @@ describe('InstantSearch', () => {
]);
});
});

test('catches up with lifecycle on re-renders', async () => {
const searchClient = createSearchClient({});

function App() {
return (
<InstantSearch searchClient={searchClient} indexName="indexName">
<SearchBox />
</InstantSearch>
);
}

const { rerender } = render(<App />);

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(1);
});

userEvent.type(screen.getByRole('searchbox'), 'iphone');

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(7);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone',
}),
},
]);
});

rerender(<App />);

userEvent.type(screen.getByRole('searchbox'), ' case', {
initialSelectionStart: 6,
});

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(12);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone case',
}),
},
]);
});
});

test('catches up with lifecycle on re-renders with a stable onStateChange', async () => {
const searchClient = createSearchClient({});
const onStateChange = ({ uiState, setUiState }) => {
setUiState(uiState);
};

function App() {
return (
<InstantSearch
searchClient={searchClient}
indexName="indexName"
onStateChange={onStateChange}
>
<SearchBox />
</InstantSearch>
);
}

const { rerender } = render(<App />);

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(1);
});

userEvent.type(screen.getByRole('searchbox'), 'iphone');

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(2);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone',
}),
},
]);
});

rerender(<App />);

userEvent.type(screen.getByRole('searchbox'), ' case', {
initialSelectionStart: 6,
});

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(3);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone case',
}),
},
]);
});
});

// This test shows that giving an unstable `onStateChange` reference (or any
// unstable prop) remounts the <InstantSearch> component and therefore resets
// the state after the remount.
// Users need to provide stable references for re-renders to keep the state.
test('catches up with lifecycle on re-renders with an unstable onStateChange', async () => {
const searchClient = createSearchClient({});

function App() {
return (
<InstantSearch
searchClient={searchClient}
indexName="indexName"
onStateChange={({ uiState, setUiState }) => {
setUiState(uiState);
}}
>
<SearchBox />
</InstantSearch>
);
}

const { rerender } = render(<App />);

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(1);
});

userEvent.type(screen.getByRole('searchbox'), 'iphone');

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(2);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone',
}),
},
]);
});

// After this rerender, the UI state is reset to the initial state because
// the `onStateChange` reference has changed.
rerender(<App />);

userEvent.type(screen.getByRole('searchbox'), ' case', {
initialSelectionStart: 6,
});

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(3);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
// The query was reset because of the remount
query: ' case',
}),
},
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { history } from 'instantsearch.js/es/lib/routers';
import { simple } from 'instantsearch.js/es/lib/stateMappings';
import React from 'react';
Expand Down Expand Up @@ -150,7 +151,7 @@ describe('InstantSearchSSRProvider', () => {
router: history({
getLocation() {
return new URL(
`https://website.com/?indexName[query]=iphone`
`http://localhost/?indexName[query]=iphone`
) as unknown as Location;
},
}),
Expand Down Expand Up @@ -343,4 +344,78 @@ describe('InstantSearchSSRProvider', () => {

expect(searchClient.search).toHaveBeenCalledTimes(0);
});

test('catches up with lifecycle on re-renders', async () => {
const searchClient = createSearchClient({});
const initialResults = {
indexName: {
state: {},
results: [
{
exhaustiveFacetsCount: true,
exhaustiveNbHits: true,
hits: [{ objectID: '1' }, { objectID: '2' }, { objectID: '3' }],
hitsPerPage: 20,
index: 'indexName',
nbHits: 0,
nbPages: 0,
page: 0,
params: '',
processingTimeMS: 0,
query: '',
},
],
},
};

function App() {
return (
<InstantSearchSSRProvider initialResults={initialResults}>
<InstantSearch searchClient={searchClient} indexName="indexName">
<SearchBox />
</InstantSearch>
</InstantSearchSSRProvider>
);
}

const { rerender } = render(<App />);

await wait(0);

expect(searchClient.search).toHaveBeenCalledTimes(0);

rerender(<App />);

userEvent.type(screen.getByRole('searchbox'), 'iphone');

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(6);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone',
}),
},
]);
});

rerender(<App />);

userEvent.type(screen.getByRole('searchbox'), ' case', {
initialSelectionStart: 6,
});

await waitFor(() => {
expect(searchClient.search).toHaveBeenCalledTimes(11);
expect(searchClient.search).toHaveBeenLastCalledWith([
{
indexName: 'indexName',
params: expect.objectContaining({
query: 'iphone case',
}),
},
]);
});
});
});

0 comments on commit 2f38c97

Please sign in to comment.