How to test interaction with Ant Design's Popover content? - reactjs

I have a React Component that wraps an Ant Design Popover. This component gets a callback that is being called by user interaction (say click) in dynamically generated content. Something similar to this:
const { Popover, Button } = antd;
const PopoverExtended = ({ onWhatever, children }) => {
const handleClick = (event) => {
if (event.target.className === 'some-class') {
onWhatever(event.target.dataset.value);
}
};
const dynamic = () => '<span class="some-class" data-value="42">Click this text</span>';
const content = () => {
return (
<div>
<p>Some HTML</p>
<div dangerouslySetInnerHTML={{ __html: dynamic() }} onClick={handleClick}></div>
</div>
);
};
return (
<Popover content={content()} placement="right" trigger="click">
{children}
</Popover>
);
};
ReactDOM.render(
<PopoverExtended onWhatever={(x) => console.log(x)}>
<Button>Click me</Button>
</PopoverExtended>,
document.getElementById('root')
);
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd.css" rel="stylesheet"/>
<div id="root" style="margin: 2em 0 0 2em"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd-with-locales.js" crossorigin="anonymous"></script>
Everything works as expected, but using Jest and Enzyme I'm trying to test that the onWhatever callback is being called, but so far I haven't been able to target the dynamic content neither as a ShallowWrapper nor ReactWrapper. I've tried:
describe(`<PopoverExtended /> interaction`, () => {
const mockChildren = <Button>Mock me</Button>;
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{mockChildren}</PopoverExtended>);
// Try 1
const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.
// Try 2
const content = mount(<>{wrapper.find(Popover).prop('content')}</>);
console.log(content.html()); // Is apparently the correct Popover content HTML
const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.
// Try 3
const content = mount(<>{wrapper.find(Popover).prop('content')}</>);
const rendered = content.render();
const trigger = wrapper.find('.some-class[data-value="42"]'); // Node found, but
// it's a CheerioWrapper, so I cannot call trigger.simulate('click');
});
Any ideas on how to properly test that the callback is being called?

Enzyme does not see the dynamic content and hence there's no way for you to simulate click on the elements inside of dynamic content. You can verify this by doing console.log(wrapper.debug()) which will show you what Enzyme sees. After trying:
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
const trigger = wrapper.find("button");
trigger.simulate("click");
Enzyme only goes as far as the hosting div:
...
<Content trigger={{...}} prefixCls="ant-popover" id={[undefined]} overlay={{...}}>
<div className="ant-popover-inner" id={[undefined]} role="tooltip">
<div>
<div className="ant-popover-inner-content">
<div>
<p>
Some HTML
</p>
<div dangerouslySetInnerHTML={{...}} onClick={[Function: handleClick]} />
</div>
</div>
</div>
</div>
</Content>
...
Now calling wrapper.html() actually returns the full DOM including the dynamic content, but that's pretty useless for our case as you've mentioned. On defense of Enzyme, the dynamic content uses html flavor instead of JSX (class instead of className) making it even more difficult to wrap.
With that out of the way, I don't see why you need to include the dynamic content in your test scenario. Simply simulate the click on the host div. In fact I'd argue that's the right way of doing it since you define the event handler on the host div and not inside the dynamic content:
it(`<PopoverExtended /> interaction`, () => {
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
const mockEvent = {
type: "click",
target: {
dataset: { value: 42 },
className: "some-class"
}
};
const trigger = wrapper.find("button");
trigger.simulate("click");
const hostDiv = wrapper.find("div.trigger-wrapper");
hostDiv.simulate("click", mockEvent);
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockCallback.mock.calls[0][0]).toBe(42);
});
Option 2
Interestingly the react testing library has no problem including your dynamic content so you may want to use it instead of Enzyme:
import React from "react";
import { render, fireEvent, screen } from '#testing-library/react'
import { Button } from "antd";
import PopoverExtended from "./PopOverExtended";
it(`<PopoverExtended /> interaction`, async () => {
const mockCallback = jest.fn();
render(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
fireEvent.click(screen.getByText('Mock me'))
fireEvent.click(screen.getByText('Click this text'))
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockCallback.mock.calls[0][0]).toBe("42");
});
Argument for this is that the dataset is defined by the dynamic content and so you have to consider it in your test.

Related

how to render or create new elements in react js

so i am trying to make a program where i can make or create new elements in react js on a click i tried few things but it gives me few errors
import React from 'react'
import { ReactDOM } from 'react'
export default class Create extends React.Component{
render(){
ReactDOM.render(<h1>Hello</h1>,document.getElementById('box'))
return(
<>
<div id='box'></div>
</>
)
}
}
this is what i tried to do where i tried to add a new HEADING element in box element but it gives me a few error
i am new to react so i am sorry for some rookie mistakes
There are tons of way of achieving this behaviour.
One way is to keep track of an Integer, for which you will render an Element (header). Pressing a button will then increase the number, so a new one is rendered.
The same idea can be achieved using arrays, objects etc...
Example using an simple Integer:
const { useState } = React;
const Heading = (props) => {
return <h3>{'Header: #' + (props.id + 1)}</h3>;
}
const Example = () => {
const [num, setNum] = useState(1);
const bumpNum = () => setNum(num + 1);
return (
<div>
<h1 onClick={bumpNum}>{'Press me to bump!'}</h1>
{Array(num).fill().map((_, i) => <Heading id={i} />)}
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Is there any difference in using React component as a (HTML tag)/ (JS function call)

I have a component, something like,
const ComponentA = ({ heading })=> {
return (<h1>{ heading }</h>);
};
Is there any difference in rendering this component using below two options,
Option 1
const ComponentB = ()=> {
return ComponentA({ heading: 'Heading test' });
};
Option 2
const ComponentB = ()=> {
return <ComponentA heading='Heading test' />;
};
Yes. There is a very important difference. In option 1, ComponentA is not actually a component as far as React is concerned. In option 2 it is.
The best way to illustrate the difference is to put state or another hook inside of ComponentA. Option 1 will not work as expected. (Note: if it does happen to work as expected, you still shouldn't trust it. This is where bugs can sneak in because they don't cause issues until later).
Here is an example where using hooks appears to work, but breaks after you get the counter past 5. This is because React is treating the hooks inside ComponentA as if they belong to Example. Notice how the JSX version works as expected even after it disappears.
const {useState, useEffect} = React;
const ComponentA = ({ id, heading })=> {
React.useEffect(() => {
console.log(id, 'mounted');
}, []);
return (
<h1>
{ heading }
</h1>
);
};
const Example = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Re-render me</button> {count}
{count < 5 ? ComponentA({ heading: 'Heading test1', id: 1 }) : null}
{count < 3 ? <ComponentA heading='Heading test2' id={2} /> : null}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The reason is JSX (< /> syntax) is actually just a syntax for calling React.createElement. If this is not called, the function component does not get it's own lifecycle, etc. You've bypassed the way React tracks components.

Using the right pattern in react to add an id without passing it directly using props but through map?

I have this code on Codesandbox the goal is to be able to pass 5 Divs, on load using use Effect.
and a second option to add a div on click when if the user feels like it. the code is partially working, but it has a anti-patter issue which is putting the component in the state instead of changing the state using map to pass the changes..
please take a look I would like to hear your opinion on this, what I do understand is importing the Div element like this could affect performance, I want to avoid bad practice as much as possible.
import React, { useEffect, useState } from "react";
import Div from "./Div";
import "./styles.css";
import { v4 as uuidv4 } from "uuid";
export default function App() {
useEffect(() => {
// on start add 5 divs in to the local state Array on the frist load
});
const [div, setDiv] = useState([]);
const addDiv = () => {
// add an extra div on click if needed with id using the right pattern
setDiv([...div, <Div id={uuidv4()} />]);
};
return (
<div className="App">
{div}
<button onClick={addDiv} type="button">
Click Me!
</button>
</div>
);
}
//Dev dependencise
"uuidv4": "6.2.12"
Codesandbox
Putting JSX elements into state is a bad idea because they won't be reactive - you won't be able to (reliably) pass down state, state setters, and other useful things as props.
It's not so much a performance issue as a code maintainability issue - if you add additional functionality to your Div component and to your App you may find that your current approach won't work due to stale values that the JSX elements in state close over.
If you need the ability to delete a value, use the index of the div in the array and pass it down as needed. For a quick and dirty example:
function App() {
const [texts, setTexts] = React.useState([]);
const [text, setText] = React.useState('');
React.useEffect(() => {
setTexts(['a', 'b', 'c', 'd', 'e']);
}, []);
const addDiv = () => {
setTexts([...texts, text]);
setText('');
};
return (
<div className="App">
{
texts.map((text, i) => (
<div>
<span>{text}</span>
<button onClick={() => setTexts(texts.filter((_, j) => j !== i))}>delete</button>
</div>
))
}
<button onClick={addDiv} type="button">
Click Me!
</button>
<input value={text} onChange={e => setText(e.target.value)} />
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
Just add id in the array and using map to render
{div.map(id => (
<Div key={id} id={id} />
))}
const addDiv = () => {
setDiv([...div, uuidv4()]);
};

How would I test this using Jest & React Testing library?

I have a component that I would like to test using Jest and React Testing Library. When I say test, I'm basically saying that I want to check if the content shows up on the screen. However, I'm running into a serious problem because I'm dealing with an async operation that updates the state, so the content is not appearing immediately. How would I approach this problem? A code snippet would be much appreciated.
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
const Home = () => {
const [tv, setTv] = useState([]);
const [tvLoading, setTvLoading] = useState(true);
// Go and fetch popular TV shows
const getPopularTv = async () => {
axios.get( ... )
setTv(data);
setTvLoading(false);
};
// This will run once. As soon as the component gets rendered for the 1st time
useEffect(() => {
getPopularTv();
}, []);
let TvData, loading;
const img_path = 'https://image.tmdb.org/t/p/w500/';
// If we have TV shows, set the 'TvData' variable to a pre-defined block of JSX using it.
if (tv && tv.total_results > 0) {
TvData = (
<div className="row animated fadeIn ">
{tv.results.slice(0, 10).map((show) => {
return (
// I WANT TO TEST IF THIS DIV APPEARS ON THE SCREEN
// SO, ON THIS DIV I'M SETTING UP THE 'data-testid'
// HOWEVER THIS IS A ASYNC OPERATION AND THE CONTENT
// WON'T SHOW UP IMMEDIATELY. HOW WOULD I TEST THIS???
<div
data-testid="home-shows" // HERE'S THE ID THAT I WANT TO USE IN MY TEST
className="col s6 m6 l6"
key={show.id}
>
<Link to={'/tvs/' + show.id}>
<img
className="responsive-img z-depth-3 poster tooltipped"
data-tooltip={show.name}
data-position="top"
src={img_path + show.poster_path}
alt={show.name}
/>
</Link>
</div>
);
})}
</div>
);
}
// Set up the 'loading' screen
loading = (
<div className="progress">
<div className="indeterminate"></div>
</div>
);
return (
<div className="container">
{tvLoading ? loading : TvData}
</div>
);
};
export default Home;
I've tried a combination of act, findByTestId, waitFor, etc. But I can't get it to work properly.
For example, I tried something like this:
it('should display TV shows', async () => {
const { getByText, findByTestId } =
render(
<BrowserRouter>
<Home />
</BrowserRouter>
)
await findByTestId('home-shows')
expect(getByText('More Info')).toBeInTheDocument();
});
My thinking was, if the content appears then it should contain the text of "More Info". If that's not the case the content is not visible, so the test should fail. however, the test fails regards if the content appears or not and I'm getting an error that I should wrap my test inside of an act() callback.
Thanks to #EstusFlask I came to a breakthrough. The solution was to use waitFor.
This is how I solved the problem:
it('should display movies', async () => {
render(
<BrowserRouter>
<Home />
</BrowserRouter>
);
const data = await waitFor(() => screen.findByTestId('home-shows'));
expect(data).toBeTruthy();
});

document.getElementById() equivalent in React 2020

I have a component called Button.js that has a button that when clicked i simply would like to know if i am accessing the a div in another component called Timer.js. In vanilla javascript i would simply use document.getElementById() to capture the DOM node. How is this done in React?
I came across callback-refs in the docs but it isn't working. If using a ref isn't the React way of accessing DOM elements please refer me to the best way to do this. thanks in advance.
Button.js
function Button() {
const getHtml = () => {
const node = test.current;
console.log(node);
}
return (
<button onClick={getHtml}>GetHtml</button>
)
}
Timer.js
function Timer() {
const test = useRef(null);
return (
<div ref={test}>... </div>
<Button />
}
I would not use a reference to check if a component is rendered inside of another one.
You could get what you're looking for with createContext and useContext.
(It could work like you tried it. If you'd pass the ref to the button as a prop.)
With the context: You create a TimerContext.Provider in your Timer component and in your button you can check with useContext(TimerContext) if the expected key is in the object. If it's not there then the button is not inside of your Timer.
Please have a look at the snippet below or in the following Codesandbox.
//import React, { useContext, createContext } from "react";
//import "./styles.css";
const { useContext, createContext } = React;
const ContainerContext = createContext({
isInContainer: null
});
const Container = () => {
return (
<ContainerContext.Provider value={{ isInContainer: true }}>
<p>
In container:
<Button />
</p>
</ContainerContext.Provider>
);
};
const Button = () => {
const { isInContainer } = useContext(ContainerContext);
console.log(isInContainer);
const isInside = () => {
alert(isInContainer ? "clicked inside" : "not in container");
};
return <button onClick={isInside}>Click me</button>;
};
function App() {
return (
<div className="App">
<Container />
<Button />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Update 15.04.2020
The question was not clear to me at first but now I understand the use-case. The idea is to have an Editor component where you're writing markup that can be used to generate a copied snippet view and/or a html markup output.
For this the best is to use a reference to the Editor component and pass it as prop to the preview/output component - it would be also possible with a context but passing it is easier.
Like in the following Sandbox.

Resources