React how to test props passed to component passed as prop - reactjs

Okay, this might sound complicated but it will be easy if you read the following example. The main purpose of this is to separate the logic from the actual render code. Making the component smaller and (in theory) easier to test.
class NameProvider {
public getName(): Promise<string> {
return Promise.resolve("Cool name");
}
}
interface RenderProps {
name: string;
onGetNamePress(): void;
}
interface LogicProps {
nameProvider: NameProvider;
render: React.ComponentType<RenderProps>
}
function Render({name, onGetNamePress}: RenderProps): React.ReactElement {
return <>
<p>{name}</p>
<button title="Get name!" onClick={onGetNamePress} />
</>
}
function Logic({nameProvider, render: Render}: LogicProps): React.ReactElement {
const [name, setName] = React.useState<string>();
return <Render
name={name}
onGetNamePress={fetch}
/>
async function fetch() {
setName(await nameProvider.getName());
}
}
Testing the render component is rather easy, but how do I test that the props passed to the render component are correct? Especially after the state changed.
Consider the following:
it('fetches the name after the button was pressed', () => {
const mnp = new MockNameProvider();
render(<Logic
nameProvider={mnp}
render={({name, onGetNamePress}) => {
act(async () => {
await onGetNamePress();
expect(name).toBe(mockName);
})
}}
/>)
})
This will cause an infinite loop, as the state keeps getting changed and the name fetched. I also couldn't imagine how to get the new props. This current code will test the old ones to my understanding. So my question is, how do I test if the props are correctly passed (also after updates).
(Important) Notes:
I'm actually writing a react native app, so maybe the issue is specific to native testing but I didn't think so.
This is not code from our codebase and just cobbled together. Thus also the React prefix, vscode just liked that better in an unsaved file.

Related

Create Dynamic Components

I want to dynamically create a component, when I implement something like this:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = () => {
const Comp = gen_Comp("Hello");
return (
<Comp txt="World" />
);
}
Something goes wrong (what exactly goes wrong is hard to explain because it's specific to my app, point is that I must be doing something wrong, because I seem to be losing state as my component gets rerendered). I also tried this with React.createElement, but the problem remains.
So, what is the proper way to create components at runtime?
The main way that react tells whether it needs to mount/unmount components is by the component type (the second way is keys). Every time App renders, you call gen_Comp and create a new type of component. It may have the same functionality as the previous one, but it's a new component and so react is forced to unmount the instance of the old component type and mount one of the new type.
You need to create your component types just once. If you can, i recommend you use your factory outside of rendering, so it runs just when the module loads:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const Comp = gen_Comp("Hello");
const App = () => {
return (
<Comp txt="World" />
);
}
If it absolutely needs to be done inside the rendering of a component (say, it depends on props), then you will need to memoize it:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = ({ spec }) => {
const Comp = useMemo(() => {
return gen_Comp(spec);
}, [spec]);
return (
<Comp txt="World" />
);
}

Destructure React component into "building blocks"

I am using IonSlides in my app but due to a bug with them, dynamically adding slides can prove difficult.
Because IonSlides is built upon SwiperJS, it has some methods to add and remove slides. The downside to those is that they take a string with HTML in it. In my case, I need to be able to pass in JSX elements so that I can use event listeners on them. Originally, this was my code:
private bindEvents(el: JSX.Element): void {
if (el.props.children) { //Checking if the element actually has children
const children = this.toArray(el.props.children); //If it has only 1 child, it is an object, so this just converts it to an array
children.forEach((c: any) => {
if (!c.props) return; //Ignore if it has no props
const propNames = this.toArray(Object.keys(c.props)); //Get the key names of the props of the child
const el = $(`.${c.props.className}`); //Find the element in the DOM using the class name of the child
propNames.forEach(p => { //Binds the actuall events to the child.
if (Events[p] !== undefined) {
el.on(Events[p], c.props[p]); //`c.props[p]` is the handler part of the event
}
});
});
}
}
Which was called through:
appendSlide(slides: JSX.Element | JSX.Element[]): void {
if (this.slideRef.current === null) return;
this.slideRef.current.getSwiper().then(sw => {
slides = this.toArray(slides);
slides.forEach(s => {
sw.appendSlide(ReactDOMServer.renderToString(s));
this.bindEvents(s);
});
});
}
This worked perfectly when appendSlide was called with an IonSlide:
x.appendSlide(<IonSlide>
<div onClick={() => console.log("Clicked!")}</div>Click me!</IonSlide>
If you clicked the div, it would print "Clicked!".
However, if you pass in a custom component, it breaks. That is because the custom component does not show the children under props. Take this component:
interface Props {
test: string,
}
const TestSlide: React.FC<Props> = (props) => {
return (
<IonSlide>
<div>
{props.string}
</div>
</IonSlide>
);
}
If you were to print that component's props, you get:
props: {test: "..."}
rather than being able to access the children of the component, like I did in the bindEvents function.
There's two ways that I could do fix this. One is getting the JS object representation of the component, like this (I remember doing this ages ago by accident, but I can't remember how I got it):
{
type: 'IonSlide',
props: {
children: [{
type: 'div',
props: {
children: ["..."],
},
}
},
}
or, a slight compromise, destructuring the custom component into its "building blocks". In terms of TestSlide that would be destructuring it into the IonSlide component.
I been trying out things for a few hours but I haven't done anything successful. I would really appreciate some help on this.
For whatever reason that someone needs this, I found you can do el.type(el.props) where el is a JSX element.
This creates an instance of the element so under children instead of seeing the props, you can see the actual child components of the component.

How to write a unit test to cover custom PropTypes in reactjs?

I have a reactjs component with a custom properties, that actually represents just a list of allowed extensions.
For example, my component can receive as a prop something like this:
<CustomComponent allowedTypes={['.pdf', '.txt']} />
and the prop types are defined like this
CustomComponent.propTypes = {
allowedTypes: PropTypes.arrayOf(
(propValue, key, componentName, location, propFullName) => {
if (!new RegExp(/^\.[^.]+$/).test(propValue[key])) {
return new Error(
`Invalid prop ${propFullName} for component ${componentName}.`
)
}
}
)
I need to full cover the component with unit test, so also the code in the prop type definition must be covered.
I tried something similar, but it doesn't work
beforeEach(() => {
console.error = jest.fn()
});
it('should check wrong allowed types', () => {
const wrongAllowedTypes = false
try {
const component = new CustomComponent(Object.assign(defaultProps, { allowedTypes: wrongAllowedTypes } ))
} catch (error) {
expect(console.error).toBeCalled()
}
})
Any ideas, thanks in advance
I suggest pulling out the fat arrow function and giving it a name. Then you can call it directly from a test.

How to test props that are updated by an onChange handler in react testing library?

I've got an onChange handler on an input that I'm trying to test based on what I've read in the Dom Testing Library docs here and here.
One difference in my code is that rather than using local state to control the input value, I'm using props. So the onChange function is actually calling another function (also received via props), which updates the state which has been "lifted up" to another component. Ultimately, the value for the input is received as a prop by the component and the input value is updated.
I'm mocking the props and trying to do a few simple tests to prove that the onChange handler is working as expected.
I expect that the function being called in the change handler to be called the same number of times that fireEvent.change is used in the test, and this works with:
const { input } = setup();
fireEvent.change(input, { target: { value: "" } });
expect(handleInstantSearchInputChange).toHaveBeenCalledTimes(1);
I expect that the input.value is read from the original mock prop setup, and this works with:
const { input } = setup();
expect(input.value).toBe("bacon");
However, I'm doing something stupid (not understanding mock functions at all, it would seem), and I can't figure out why the following block does not update the input.value, and continues to read the input.value setup from the original mock prop setup.
This fails with expecting "" / received "bacon" <= set in original prop
fireEvent.change(input, { target: { value: "" } });
expect(input.value).toBe("");
QUESTION: How can I write a test to prove that the input.value has been changed given the code below? I assume that I need the mock handleInstantSearchInputChange function to do something, but I don't really know what I'm doing here quite yet.
Thanks for any advice on how to do and/or better understand this.
Test File
import React from "react";
import InstantSearchForm from "../../components/InstantSearchForm";
import { render, cleanup, fireEvent } from "react-testing-library";
afterEach(cleanup);
let handleInstantSearchInputChange, props;
handleInstantSearchInputChange = jest.fn();
props = {
foodSearch: "bacon",
handleInstantSearchInputChange: handleInstantSearchInputChange
};
const setup = () => {
const utils = render(<InstantSearchForm {...props} />);
const input = utils.getByLabelText("food-search-input");
return {
input,
...utils
};
};
it("should render InstantSearchForm correctly with provided foodSearch prop", () => {
const { input } = setup();
expect(input.value).toBe("bacon");
});
it("should handle change", () => {
const { input } = setup();
fireEvent.change(input, { target: { value: "" } });
expect(input.value).toBe("");
fireEvent.change(input, { target: { value: "snickerdoodle" } });
expect(input.value).toBe("snickerdoodle");
});
Component
import React from "react";
import PropTypes from "prop-types";
const InstantSearchForm = props => {
const handleChange = e => {
props.handleInstantSearchInputChange(e.target.value);
};
return (
<div className="form-group">
<label className="col-form-label col-form-label-lg" htmlFor="food-search">
What did you eat, fatty?
</label>
<input
aria-label="food-search-input"
className="form-control form-control-lg"
onChange={handleChange}
placeholder="e.g. i ate bacon and eggs for breakfast with a glass of whole milk."
type="text"
value={props.foodSearch}
/>
</div>
);
};
InstantSearchForm.propTypes = {
foodSearch: PropTypes.string.isRequired,
handleInstantSearchInputChange: PropTypes.func.isRequired
};
export default InstantSearchForm;
The way you are thinking about your tests is slightly incorrect. The behavior of this component is purely the following:
When passed a text as a prop foodSearch renders it correctly.
Component calls the appropriate handler on change.
So only test for the above.
What happens to the foodSearch prop after the change event is triggered is not the responsibility of this component(InstantSearchForm). That responsibility lies with the method that handles that state. So, you would want to test that handler method specifically as a separate test.

React Component Function Comparison

I have a form that has gotten really intense. Roughly 20 inputs that include multiple datepickers, google locations api, rrule values etc. Since very few of these inputs can update directly without going through some sort of a transform. I've successfully converted the component to from a stateful component that was doing way too much in the lifecycle methods (now using formik to manage values), but I'm trying to determine what the best way to define the necessary helper functions (e.g. updatedDateWithTime, formatAddress) in terms of performance and style, and can only think of a few options.
Option one: function expressions within the functional component:
const MyHugeForm = () => {
const helper1 = () => { console.log("thing1") }
const helper2 = () => {console.log("thing2") }
return() {...}
}
Option 2: as "globals" defined in the file, outside of the function:
helper1() => console.log("thing1");
helper2() => console.log("thing2");
const MyHugeForm = () => {
return() {...}
}
Option 3: as inline arrow functions used inside child components (i.e. break each input into it's own component and pass props down)
const MyHugeForm = (props) => {
return() {
<div>
<DateInput startDate={props.startDate} />
<LocationInput location={props.googleLocation} />
</div>
}
}
const DateInput = (props) => {
<DatePicker onChange={() => console.log("thing1")} />
}
const LocationInput = (props) => {
<input onChange={() => console.log("thing2")} />
}
It feels wrong to define 20 or so of these helper functions outside of (but in the same file as) the functional component, but defining them as function expressions inside the component seems worse and of the two options worse for performance. Breaking the pieces into child components feels like the right pattern in terms of reducing the complexity of a 600 line functional component, but if the children just end up defining the same functions inline in their renders, isn't it effectively the same?
My suggestion would be to create a helper class with some static methods where you can pass the input html events as parameters:
export default class MyHugeFormHelper {
static onChangeHandler(e) {
// do stuff here
}
static onInputHandler(e) {}
static onSubmit(e, callback) {
// you could pass a callback function from the logic of your component
}
}
Then in your component invoke this class method like this:
import MyHugeFormHelper from './MyHugeFormHelper';
const DateInput = (props) => {
<DatePicker onChange={MyHugeFormHelper.onChangeHandler} />
}

Resources