How to Jest/Unit Test a Component Reliant on TableRefs - reactjs

I have a table which given the downloadRequested it will use the tableRef provided and return the sortedData within the table. When trying to add unit tests to make sure that the <CSVLink> is rendered on downloadRequested = true I keep hitting the below error:
TypeError: Cannot read property 'getResolvedState' of null
Am I supposed to mock the ref? or do I need to somehow provide it? How do I get around this issue?
Code:
import React, { useRef } from "react";
import { CSVLink } from "react-csv";
function MyTable(props) {
const tableRef = useRef(null);
const getColumns = () => {
return [{ Header: "Name", accessor: "name" }, { Header: "Id", accessor: "id" }];
};
const getCsvData = () => {
const keys = ["name", "id"];
return tableRef.current.getResolvedState().sortedData.map(row => getCsvDataFromTable(keys, row));
};
return (
<>
{props.downloadRequested && (
<CSVLink data={getCsvData()} target="_blank" filename={`myTable.csv`} data-testid="csvLink">
<div
data-testid="csvLinkDiv"
ref={e => {
if (e) {
e.click();
}
}}
/>
</CSVLink>
)}
<Table columns={getColumns(props)} filterable forwardedRef={tableRef} {...props} />
</>
);
}
export default MyTable;
Test Suite:
import React from "react";
import { render } from "#testing-library/react";
import MyTable from "./MyTable";
describe("MyTable", () => {
const sampleData = [{ id: "123", name: "John Doe" }, { id: "456", name: "Doe John" }];
it("Should render MyTable with CSV Link correctly", () => {
const { queryByTestId } = render(<MyTable data={sampleData} downloadRequested={true} />);
expect(queryByTestId("csvLink")).toBeTruthy();
});
});

Well, I figured this out. My problem stems from the fact that the CSV-Link needs to have data at run time, and the way this is set up it doesnt get that till later. Anyways, what was happening is that getCsvData() was being called before the table was initialized, hence the ref was always null. One work around I used was to render it with downloadRequested = false first, then rerender it (now a table exists) with the value true.

Related

Maintaining multiple user inputs to antd Select in a single useState variable

I want to store the information of multiple inputs entered into antd Select components in a single state variable but am having trouble getting the below to work.
This example is solved here for a form but the same solution doesn't seem to work for antd Select component. There are two inputs: a first name and a last name that I want to remember. The below code doesn't work because e doesn't have an attribute called name is what the console tells me. I also tried e.target.name and e.target.value but I get an error that e doesn't have an attribute called a target either. What is the right way to do this?
import React, { useState } from 'react';
import { Select } from 'antd';
const App = () =>{
const [varState, setVarState] = useState({firstName:'Jack', lastName:'Smith'});
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const changeState = (e) => {
setVarState( prevState => ({ ...prevState, [e.name]: e.value}));
console.log(varState)
};
return ( <>
<div>
<Select name={'firstName'} defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState} />
<Select name={'lastName'} defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState} />
</div>
</>
);
}
export default App;
At the heart of it, it seems that I don't know how to name the Select components in such a way that their names can be passed on to the onChange handler.
More generally, given a component like antd Select, how can I figure out what the right "name field" is for this component so that it's value can be passed on to an onChange handler? For instance, what in the documentation for select gives this information?
The Select component seems to be sending the value instead of the events object. So, You can just make a closure and pass the name of the select. Also, for consoling you can make use of a useEffect which only consoles when the state has been updated. Otherwise, you could see previous state as state updates are asynchronous. Below is a working solution.
import React, { useEffect, useState } from "react";
import { Select } from "antd";
const App = () => {
const [varState, setVarState] = useState({
firstName: "Jack",
lastName: "Smith"
});
const firstNameOptions = [
{ label: "Jack", value: "Jack" },
{ label: "Jill", value: "Jill" },
{ label: "Bill", value: "Bill" }
];
const lastNameOptions = [
{ label: "Smith", value: "Smith" },
{ label: "Potter", value: "Potter" },
{ label: "Bach", value: "Bach" }
];
// for consoling when the state updates
useEffect(() => {
console.log(varState);
}, [varState.firstName, varState.lastName]);
const changeState = (value, identifier) => {
// console.log(value, identifier);
setVarState((prevState) => ({ ...prevState, [identifier]: value }));
};
return (
<>
<div>
<Select
name={"firstName"}
defaultValue={"Pick One"}
options={firstNameOptions}
onChange={(val) => changeState(val, "firstName")}
/>
<Select
name={"lastName"}
defaultValue={"Pick One"}
options={lastNameOptions}
onChange={(val) => changeState(val, "lastName")}
/>
</div>
</>
);
};
export default App;
yes, Actually antd doesn't have attribute name for input fields. antdesign directly gives the selected value, we need to do some tweeks to achieve this.
Here is the solution:
import React, { useState } from 'react';
import { Select } from 'antd';
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const App = () =>{
const [varState, setVarState] = useState(null);
const changeState = (fieldName) => (value) => {
setVarState( prevState => ({ ...prevState, [fieldName]: value}));
console.log(varState)
};
return ( <>
<div>
<Select defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState('firstName')} />
<Select defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState('lastName')} />
</div>
</>
);
}
export default App;
I hope this helps 😊

React fetch-api and react table

I am trying to fetch data and render it on to a react table. I have no idea why it's not working. Could someone help me? I get the data in to the Network response, but I don't know why it's not rendered to the website. Here is my code:
import React, { useState, useEffect } from "react";
import 'react-table/react-table.css';
import ReactTable from 'react-table';
export default function Traininglist() {
const [trainings, setTrainings] = useState([]);
useEffect(() => fetchData(), []);
const fetchData = () => {
fetch("https://customerrest.herokuapp.com/api/trainings")
.then(response => response.json())
.then(data => setTrainings(data.content))
.catch(err => console.error(err))
};
const columns = [
{
title: 'Date',
field: 'date',
},
{
title: 'Duration (min)',
field: 'duration'
},
{
title: 'Activity',
field: 'activity'
},
];
return (
<div>
<h1>Trainings</h1>
<ReactTable filterable={true} data={trainings} columns={columns} />
</div>
);
and here you can see what the data looks like:
You may want to read through the Docs. Your columns should at the very least have a key called accessor that targets a key of your data set from which it will render a value. Looks like you have nested data here, so you may need to use a Cell field to render what you need.

Testing React Context update

I have been struggling to test a React Context update for a while. I can predefine the value of the Context.Provider however, the solution is not ideal because an update to the context which is supposed to happen within component utilising the context is not actually happening.
When I test this manually the text 'Account name: abc' changes to 'Account name: New account name' but not in the test. The context value remains the same.
The reason I predefine the value is because the component relies on a fetch response from a parent and I am unit testing <ImportAccounts /> only.
In a test I am rendering a component with predefined value
test('accountName value is updated on click', () => {
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
expect(getByText('Account name: abc')).toBeInTheDocument();
const input = getByLabelText('Account name');
fireEvent.change(input, { target: { value: 'New account name' } });
fireEvent.click(getByRole('button', { name: 'Update' }));
expect(getByText('Account name: New account name')).toBeInTheDocument();
});
Here's my context
import React, { createContext, useContext, useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface StateVariable<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
}
export interface ImportAccountsState {
accountName: StateVariable<string>;
}
export const ImportAccountsContext = createContext<ImportAccountsState>(
{} as ImportAccountsState,
);
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};
Import Accounts is as simple as
export const ImportAccounts = () => {
const { accountName } = useImportAccountsContext();
const [newAccountName, setNewAccountName] = useState(accountName.value);
const handleAccountNameChange = () => {
accountName.setValue(newAccountName);
};
return (
<>
<h1>Account name: {accountName.value}</h1>
<label htmlFor="accountName">Account name</label>
<input
value={newAccountName}
onChange={e => setNewAccountName(e.target.value)}
id="accountName"
/>
<button
type="button"
onClick={handleAccountNameChange}>
Update
</button>
</>
);
}
How can I test that accountName has actually updated?
If we don't need the default value for the ImportAccounts provider then we make the test pass easily. ImportAccountsProvider manages the state of the accountName within itself. In that provider, we are passing the accountName state of type ImportAccountsState to all our children through the context provider.
Now coming to your problem,
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
Here, the value: 'abc' is not a state value, it's simply a string constant 'abc' which will never be going to change. This is something that we should note. We must pass the state value to the context provider if we want to share the value with the children which is not going to be constant in the entire react lifecycle.
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
test('should update the context with existing provider', () => {
render(
<ImportAccountsProvider>
<ImportAccounts />
</ImportAccountsProvider>
);
// some constant hoisted to make it clean code
const accountInput = screen.getByRole('textbox', { name: /account name/i });
const accountInputValue = 'subrato patnaik';
expect(accountInput).toHaveAttribute('value', '');
// do some changes
userEvent.type(accountInput, accountInputValue);
//validate "changes"
expect(screen.getByDisplayValue(accountInputValue)).toBeTruthy();
expect(accountInput).toHaveAttribute('value', accountInputValue);
// update context
userEvent.click(screen.getByRole('button', { name: /update/i }));
// validate update
expect(screen.getByRole('heading')).toHaveTextContent(/subrato/i);
screen.debug();
});
Inside the ImportAccountsProvider we can do the fetch call and set the accountName state to the response of the fetch call.
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
// do the fetch call here and update the accountName state accordingly
});
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};

How to fix typescript error 'name' does not exist in type 'ByRoleOptions' when querying by accessible name using getByRole in react-testing-library

I have a component that renders a list of filters as removable chips which I am trying to test using react-testing-library. I am trying to do query by accessible name as explained here using getByRole.
component:
import Chip from '#material-ui/core/Chip';
import PersonIcon from '#material-ui/icons/Person';
import React from 'react';
import './FilterChips.less';
import { Filters } from './types';
export type FilterChipsProps = {
filters: Filters,
};
export const FilterChips = (props: FilterChipsProps) => {
const { filters } = props;
const people = filters.people
? filters.people.map((person: any) => (
<Chip
icon={<PersonIcon />}
label={`${person.Name} (${person.Id})`}
key={person.Id}
className='chips'
role='filter-chip'
/>
))
: [];
return people.length > 0
? (
<div className='filters'>
<span>Filters: </span>
{people}
</div>
)
:
null;
};
Test:
test('that filters are rendered properly', async () => {
const filters = {
people: [
{ Id: '1', Name: 'Hermione Granger' },
{ Id: '2', Name: 'Albus Dumbledore' },
],
};
const props = { filters };
const { getByRole } = render(<FilterChips {...props} />);
const PersonFilter = getByRole('filter-chip', { name: `${filters.people[0].Name} (${filters.people[0].Id})` });
expect(PersonFilter).toBeDefined();
});
But I am getting a typescript error:
Argument of type '{ name: string; }' is not assignable to parameter of type 'ByRoleOptions'.
Object literal may only specify known properties, and 'name' does not exist in type 'ByRoleOptions'
How do I fix this?
I tried a couple of things to fix this. I imported getByRole directly from #testing-library/dom and deconstructed container from rendered component
const { container } = render(<FilterChips {...props} />);
and then tried to do query by accessible name as following
const PersonFilter = getByRole(container, 'filter-chip', { name: '${filters.people[0].Name} (${filters.people[0].Id})' });
But this is also throwing the same error. Why am I getting this error and how do I fix it?
You can simply ignore ts preceding the problematic line with:
//#ts-ignore
const PersonFilter = getByRole('filter-chip', { name: `${filters.people[0].Name} (${filters.people[0].Id})` });
This will ignore all typescript alerts and treat that next line as if it were plain javascript
following this example in the docs (scroll to the end of the section linked and click on the 'React' tab):
import { render } from '#testing-library/react'
const { getByRole } = render(<MyComponent />)
const dialogContainer = getByRole('dialog')
your code should be:
const { getByRole } = render(<FilterChips {...props} />);
const PersonFilter = getByRole(`${filters.people[0].Name} (${filters.people[0].Id})`);

React-Select with React-Apollo does not work

We are using react-select and fetching the items as the user types. I am not able to make it work with react-apollo.
Can someone help me provide a guideline?
Here is my unsuccessful attempt:
class PatientSearchByPhone extends Component {
updateProp = mobile => {
if (mobile.length < 10) return;
this.props.data.refetch({ input: { mobile } });
};
render() {
console.log(this.props.data);
return <AsyncSelect cacheOptions loadOptions={this.updateProp} />;
}
}
const FETCH_PATIENT = gql`
query Patient($input: PatientSearchInput) {
getPatients(input: $input) {
id
first_name
}
}
`;
export default graphql(FETCH_PATIENT, {
options: ({ mobile }) => ({ variables: { input: { mobile } } })
})(PatientSearchByPhone);
Versions:
"react-apollo": "^2.1.11",
"react-select": "^2.1.0"
Thanks for your time.
I got an e-mail asking a response to this question. It reminds me of this XKCD comics:
I do not recall the exact solution I implemented, so I setup a complete example for this.
This app (code snippet below) kickstarts searching as soon as you type 4 characters or more in the input box (You are expected to type artist's name. Try vinci?). Here is the code:
import React, { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import ApolloClient, { gql } from "apollo-boost";
const client = new ApolloClient({
uri: "https://metaphysics-production.artsy.net"
});
const fetchArtists = async (input: string, cb: any) => {
if (input && input.trim().length < 4) {
return [];
}
const res = await client.query({
query: gql`
query {
match_artist(term: "${input}") {
name
imageUrl
}
}
`
});
if (res.data && res.data.match_artist) {
return res.data.match_artist.map(
(a: { name: string; imageUrl: string }) => ({
label: a.name,
value: a.imageUrl
})
);
}
return [];
};
const App: React.FC = () => {
const [artist, setArtist] = useState({
label: "No Name",
value: "https://dummyimage.com/200x200/000/fff&text=No+Artist"
});
return (
<div className="App">
<header className="App-header">
<h4>Search artists and their image (type 4 char or more)</h4>
<AsyncSelect
loadOptions={fetchArtists}
onChange={(opt: any) => setArtist(opt)}
placeholder="Search an Artist"
className="select"
/>
<div>
<img alt={artist.label} src={artist.value} className="aimage" />
</div>
</header>
</div>
);
};
export default App;
You can clone https://github.com/naishe/react-select-apollo it is a working example. I have deployed the app here: https://apollo-select.naishe.in/, may be play a little?
The other option is to execute the graphql query manually using the client that is exposed by wrapping the base component with withApollo.
In the example below, we have,
BaseComponnent which renders the AsyncSelect react-select component
loadOptionsIndexes which executes the async graphql fetch via the client
BaseComponent.propTypes describes the required client prop
withApollo wraps the base component to give us the actual component we'll use elsewhere in the react app.
const BaseComponent = (props) => {
const loadOptionsIndexes = (inputValue) => {
let graphqlQueryExpression = {
query: QUERY_INDEXES,
variables: {
name: inputValue
}
}
const transformDataIntoValueLabel = (data) => {
return data.indexes.indexes.map(ix => { return { value: ix.id, label: ix.name }})
}
return new Promise(resolve => {
props.client.query(graphqlQueryExpression).then(response => {
resolve(transformDataIntoValueLabel(response.data))
})
});
}
return (
<>
<div className="chart-buttons-default">
<div className="select-index-input" style={{width: 400, display: "inline-block"}}>
<AsyncSelect
isMulti={true}
cacheOptions={true}
defaultOptions={true}
loadOptions={loadOptionsIndexes} />
</div>
</div>
</>
)
}
BaseComponent.propTypes = {
client: PropTypes.any,
}
const ComplementComponent = withApollo(BaseComponent);
Sorry if the example is a little off - copy and pasted what I had working rather than moving on without giving back.

Resources