I am very new to React and want to be able to test that a function was called when a button is clicked, however I cannot get this working. I have tried the answers at posts such as: How to test a function is called in react jest? and others but to no avail.
My component snippet is:
// Import React core components
import React from 'react';
class ScrollToTop extends React.Component {
/**
* Method to scroll top the top of the page on click
*/
scrollToTopEvent() {
window.scrollTo(0, 0);
};
render() {
return(
<div id="scroll-to-top-container">
{
this.state.showScrollToTop ? (
<button id="scroll-to-top" className="button button-secondary" onClick={ this.scrollToTopEvent }>Scroll to top</button>
) : (
null
)
}
</div>
);
}
export default ScrollToTop;
}
And here is my test that I am attempting to get working, cobbled together from various Stackoverflows and articles:
import React from 'react';
import { shallow } from 'enzyme';
import ScrollToTop from "./scroll-to-top";
describe(`Tests the scroll-to-top.js file content`, () => {
const scrollToTop = shallow(<ScrollToTop />);
const instance = scrollToTop.instance();
describe(`Tests scrollToTopEvent`, () => {
it(`should ensure the scrollToTop method was called on button click`, () => {
scrollToTop.setState({ showScrollToTop: true });
jest.spyOn(instance, 'scrollToTopEvent');
const scrollToTopButton = scrollToTop.find(`#scroll-to-top`);
scrollToTopButton.simulate('click');
scrollToTop.update();
expect(instance.scrollToTopEvent).toHaveBeenCalled();
});
});
});
The error I get in the console is:
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
> 96 | expect(instance.scrollToTopEvent).toHaveBeenCalled();
Any help for this n00b would be much appreciated!
To be enable enzyme to track a function call you have to spy on that function, like this:
const scrollToTopEventSpy = jest.spyOn(ScrollToTop.prototype, 'scrollToTopEvent);
Then you can test the calls like this:
expect(scrollToTopEventSpy).toHaveBeenCalled();
Related
I'm using React Context API to create a game.
In one of my components (GameStatus) I pull in some methods from the provider:
const context = React.useContext(MyContext);
const { setGameStart, setGameEnd, setScore } = context;
And in the component I invoke these three methods onClick of the start game button, which in turn sets the state back in the provider.
GameStatus Component
import React from 'react';
import { MyContext } from './Provider';
const GameStatus = ({ gameStart, gameEnd, score, total, getPlayers }: { gameStart: boolean, gameEnd: boolean, getPlayers: () => void, score: number, total: number }) => {
const context = React.useContext(MyContext);
const { setGameStart, setGameEnd, setScore } = context;
return (
<>
{!gameStart && (
<button onClick={() => {
getPlayers();
setScore(0);
setGameStart(true);
setGameEnd(false);
}}>
Start game
</button>
)}
{gameEnd && (
<p>Game end - You scored {score} out {total}</p>
)}
</>
)
}
export default GameStatus;
Then in my test file below I want to test that when the start game button is clicked the game is started (check the DOM has removed the button and is now showing the game).
But I'm not sure how to pull in the methods in to the test file as I get:
Result: TypeError: setScore is not a function
I tried just copying:
const context = React.useContext(MyContext);
const { setGameStart, setGameEnd, setScore } = context;
But then I get an invalid hook call as I can't use React hooks inside the test.
Any ideas? Or a better approach to testing this? Thanks
GameStatus Test
import React from 'react';
import { shallow } from 'enzyme';
import { MyContext } from '../components/Provider';
import GameStatus from '../components/GameStatus';
test('should start the game', () => {
const getPlayers = jest.fn();
const uut = shallow(
<MyContext.Provider>
<GameStatus
getPlayers={getPlayers}
/>
</MyContext.Provider>
).dive().find('button');
uut.simulate('click');
console.log(uut.debug());
});
I'm not sure if this helps but since your first approach was to use hooks inside your test you could try to use a library for rendering hooks. I'm not familiar with enzyme but I used React Hooks Testing Library in order to render my hooks inside my tests.
You can use it like that:
let testYourHook= renderHook(yourHook);
testYourHook value is not dynamic, so you have to get the value everytime with:
testYourHook.result.current
When I used the same method in another project then It was working well but I decided to use the same method for my current project then I am having an issue with the given below
react-dom.development.js:14724 Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See some tips for tips about how to debug and fix this problem.
at Object.throwInvalidHookError (react-dom.development.js:14724:13)
at useState (react.development.js:1497:21)
Below is my first component name is GetWindowWidth.js. this component is related to apply the screen with for desktop, tab, and mob screen
GetWindowWidth.js
import {useState,useEffect} from "react";
const GetWindowWidth = () => {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
});
const updateWidth = () => {
setWidth(window.innerWidth);
};
return width;
}
export default GetWindowWidth;
Below is another component where I am trying to call the above component to apply the screen width.
AnotherComponent.js
import React, { Component } from 'react'
import GetWindowWidth from './GetWindowWidth';
export class AnotherComponent extends Component {
render() {
const width = GetWindowWidth();
return (
<div color={width<600? '#161625':"#f3990f"}>UserCatalogTwo</div>
)
}
}
export default AnotherComponent
I don't why this is coming even it's working on other projects.
GetWindowWidth is a hook, not a component, since it doesn't render anything. (And for that reason its name should start with use.) You can't use hooks in class components. You'll have to either rewrite the class component as a function component, or write a non-hook version of GetWindowWidth.
For instance, you might have a module with a function that sets up the resize handler:
// watchwidth.js
export function watchWidth(callback) {
const handler = () => {
callback(window.innerWidth);
};
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}
...then import it:
import { watchWidth } from "./watchwidth.js";
...and use it in your class component:
componentDidMount() {
this.stopWatchingWidth = watchWidth(width => this.setState({width}));
}
componentWillUnmount() {
this.stopWatchingWidth();
}
That's off-the-cuff and untested, but gives you an idea.
I have a very simple class based component. Which looks like the following:
class MyComponent extends React.Component {
onPressButton () {
console.warn('button pressed')
const { contextFunction } = this.context
contextFunction()
}
render () {
return (
<div>
My Component
<button onClick={() => onPressButton()}>Press button</button>
</div>
)
}
}
MyComponent.contextType = SomeContext
That is all fine and well and works as expected. However, I am having trouble adding unit tests with jest and enzyme. My current code looks as follows:
test('should render test Press button', async () => {
const contextFunctionMock = jest.fn()
const wrapper = shallow(<MyComponent {...props} />)
wrapper.instance().context = { contextFunction: contextFunctionMock }
console.log('wrapper.instance()', wrapper.instance())
await wrapper.instance().onPressButton() // this works just fine
expect(contextFunctionMock).toHaveBeenCalled() // this errors, basically because ti complains contextFunction is not a function
})
As you can see above, I console.logged my wrapper.instance() to see what is going on.
Interestingly enough, the context on the root of the instance object is indeed what I expected it to be based on setting the context, which is something like:
context: {
contextFunction: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
[...Other mock function properties]
}
...
However, there is a second context, which is in the updater property of the wrapper.instance(), and it is an empty object. Basically looks like the following:
updater: <ref *2> Updater {
_renderer: ReactShallowRenderer {
_context: {},
...
}
Not exactly sure if this is the context being used for my component's unit test, but it is currently just an empty object, which makes me think this may be the one being used for it.
Anyway, how can I properly mock my context functions to run on this particular unit tests? Also, why is this happening but does not happen in others with a similar set of circumstances?
Problem
A fundamental problem with your code above is that there's no way to assert that the context function is successfully/failing to be called. Right now, you're clicking a button, but there isn't any indication on what's happening after the button is clicked (nothing is being changed/updated within the context/component to reflect any sort of UI change). So asserting that a contextual function is called won't be beneficial if there's no result of clicking the button.
In addition to the above, the enzyme-adapter doesn't support context that uses the createContext method.
However, there's a work-around for this limitation! Instead of unit testing the component, you'll want to create an integration test with the context. Instead of asserting that a contextual function was called, you'll make assertions against the result of clicking on the button that changes context and how it affects the component.
Solution
Since the component is tied to what's in context, you'll create an integration test. For example, you'll wrap the component with context in your test and make assertions against the result:
import * as React from "react";
import { mount } from "enzyme";
import Component from "./path/to/Component";
import ContextProvider from "./path/to/ContextProvider";
const wrapper = mount(
<ContextProvider>
<Component />
</ContextProvider>
);
it("updates the UI when the button is clicked", () => {
wrapper.find("button").simulate("click");
expect(wrapper.find(...)).toEqual(...);
})
By doing the above, you can make assertions against contextual updates within the Component. In addition, by using mount, you won't have to dive into the ContextProvider to view the Component markup.
Demo Example
This demo utilizes context to toggle a theme from "light" to "dark" and vice versa. Click on the Tests tab to run the App.test.js integration test.
Code Example
App.js
import * as React from "react";
import { ThemeContext } from "./ThemeProvider";
import "./styles.css";
class App extends React.PureComponent {
render() {
const { theme, toggleTheme } = this.context;
return (
<div className="app">
<h1>Current Theme</h1>
<h2 data-testid="theme" className={`${theme}-text`}>
{theme}
</h2>
<button
className={`${theme}-button button`}
data-testid="change-theme-button"
type="button"
onClick={toggleTheme}
>
Change Theme
</button>
</div>
);
}
}
App.contextType = ThemeContext;
export default App;
ThemeProvider.js
import * as React from "react";
export const ThemeContext = React.createContext();
class ThemeProvider extends React.Component {
state = {
theme: "light"
};
toggleTheme = () => {
this.setState((prevState) => ({
theme: prevState.theme === "light" ? "dark" : "light"
}));
};
render = () => (
<ThemeContext.Provider
value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}
>
{this.props.children}
</ThemeContext.Provider>
);
}
export default ThemeProvider;
index.js
import * as React from "react";
import ReactDOM from "react-dom";
import ThemeProvider from "./ThemeProvider";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
Test Example
An example of how to test against the demo example above.
withTheme.js (an optional reusable testing factory function to wrap a component with context -- especially useful for when you may want to call wrapper.setProps() on the root to update a component's props)
import * as React from "react";
import { mount } from "enzyme";
import ThemeProvider from "./ThemeProvider";
/**
* Factory function to create a mounted wrapper with context for a React component
*
* #param Component - Component to be mounted
* #param options - Optional options for enzyme's mount function.
* #function createElement - Creates a wrapper around passed in component with incoming props (now we can use wrapper.setProps on root)
* #returns ReactWrapper - a mounted React component with context.
*/
export const withTheme = (Component, options = {}) =>
mount(
React.createElement((props) => (
<ThemeProvider>{React.cloneElement(Component, props)}</ThemeProvider>
)),
options
);
export default withTheme;
App.test.js
import * as React from "react";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import withTheme from "./withTheme";
import App from "./App";
configure({ adapter: new Adapter() });
// wrapping "App" with some context
const wrapper = withTheme(<App />);
/*
THIS "findByTestId" FUNCTION IS OPTIONAL!
I'm using "data-testid" attributes, since they're static properties in
the DOM that are easier to find within a "wrapper".
This is 100% optional, but easier to use when a "className" may be
dynamic -- such as when using css modules that create dynamic class names.
*/
const findByTestId = (id) => wrapper.find(`[data-testid='${id}']`);
describe("App", () => {
it("initially displays a light theme", () => {
expect(findByTestId("theme").text()).toEqual("light");
expect(findByTestId("theme").prop("className")).toEqual("light-text");
expect(findByTestId("change-theme-button").prop("className")).toContain(
"light-button"
);
});
it("clicking on the 'Change Theme' button toggles the theme between 'light' and 'dark'", () => {
// change theme to "dark"
findByTestId("change-theme-button").simulate("click");
expect(findByTestId("theme").text()).toEqual("dark");
expect(findByTestId("theme").prop("className")).toEqual("dark-text");
expect(findByTestId("change-theme-button").prop("className")).toContain(
"dark-button"
);
// change theme to "light"
findByTestId("change-theme-button").simulate("click");
expect(findByTestId("theme").text()).toEqual("light");
});
});
As for today, the new context API is not supported by enzyme, the only solution I found is to use this utility https://www.npmjs.com/package/shallow-with-context
import { configure, shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { withContext } from "shallow-with-context";
import MyComponent from "./MyComponent";
configure({ adapter: new Adapter() });
describe("Context", () => {
it("should render test Press button", async () => {
const contextFunctionMock = jest.fn();
const context = { contextFunction: contextFunctionMock };
const MyComponentWithContext = withContext(MyComponent, context);
const wrapper = shallow(<MyComponentWithContext />, { context });
await wrapper.instance().onPressButton();
expect(contextFunctionMock).toHaveBeenCalled();
});
});
https://codesandbox.io/s/enzyme-context-test-xhfj3?file=/src/MyComponent.test.tsx
Where's the right place to put code that interacts with the DOM in a gatsby site? I want to toggle the visibility of some components by adding/removing a class when another element is clicked.
The gatsby-browser.js file seems like it should contain this code but the API doesn't seem to call any of the functions after the DOM has loaded.
Similarly, using Helmet calls it too soon. Using window.onload never seems to trigger at all regardless of where it's included.
window.onload = function () {
// add event listener to the toggle control
}
Is there an event I can use to run my code when the DOM is ready?
Do you really need to wait for the DOM to be ready? When working in react you need to change the way you think about these things. For example you could add an on click that changes state and then reflect the state change in your classname prop.
Code Example:
import React, { useState } from "react"
const MyApp = () => {
const [visible, setVisible] = useState(true) // true is the initial state
return (
<div>
<div className={visible ? "visible-class" : "hidden-class"}>
My content
</div>
<button onClick={() => setVisible(!visible)}>Click me!</button>
</div>
)
}
export default MyApp
Or you could take it a step further and not even render that content to the DOM until you want to.
Example:
import React, { useState } from "react"
const MyApp = () => {
const [visible, setVisible] = useState(true) // true is the inital state
return (
<div>
<button onClick={() => setVisible(!visible)}>Click me!</button>
{visible && <div>My content here</div>}
</div>
)
}
export default MyApp
You can use the React cyclelife with componentDidMount().
This need to update your component like that :
import React from 'react'
class YourComponent extends React.Component {
componentDidMount() {
// Your Javascript function here
}
render() {
return(
<div className="YourComponentHere"></div>
)
}
}
export default YourComponent
Hope that help you!
If your component is a functional component, try using React Hook useEffect, which will guarantee the execution after the component is rendered.
import React, { useEffect } from 'react'
const MyComponent = () => {
useEffect(() => {
console.log("Document loaded");
});
return (
<main>
<text>Pretty much the component's body code around here</text>
</main>
)
}
export default MyComponent
I'm trying to test a simple component that take some props (it have no state, or redux connection) with Enzyme, it works for the plain elements like <div /> and so on, but when i try to test if the element rendered by the child component exists, it fails.
I'm trying to use mount but it spit me a lot of errors, i'm new in this so, here is my code:
import React, { Component } from 'react';
import WordCloud from 'react-d3-cloud';
class PredictWordCloud extends Component {
render() {
const fontSizeMapper = word => Math.log2(word.value) * 3.3;
const { size, data, show } = this.props;
if (!show)
return <h3 className='text-muted text-center'>No data</h3>
return (
<section id='predict-word-cloud'>
<div className='text-center'>
<WordCloud
data={data}
fontSizeMapper={fontSizeMapper}
width={size}
height={300} />
</div>
</section>
)
}
}
export default PredictWordCloud;
It's just a wrapper for <WordCloud />, and it just recieves 3 props directly from his parent: <PredictWordCloud data={wordcloud} size={cloudSize} show={wordcloud ? true : false} />, anything else.
The tests is very very simple for now:
import React from 'react';
import { shallow } from 'enzyme';
import PredictWordCloud from '../../components/PredictWordCloud.component';
import cloudData from '../../helpers/cloudData.json';
describe('<PredictWordCloud />', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<PredictWordCloud data={cloudData} size={600} show={true} />)
});
it('Render without problems', () => {
const selector = wrapper.find('#predict-word-cloud');
expect(selector.exists()).toBeTruthy();
});
});
For now it pass but if we change the selector to: const selector = wrapper.find('#predict-word-cloud svg'); where the svg tag is the return of <Wordcloud /> component, the tests fails because the assertion returns false.
I tried to use mount instead of shallow, exactly the same test, but i get a big error fomr react-d3-cloud:
PredictWordCloud Render without problems TypeError: Cannot read property 'getImageData' of null.
This is specially weird because it just happens in the test environment, the UI and all behaviors works perfectly in the browser.
You can find your component directly by Component name.
Then you can use find inside your sub-component as well.
e.g
it('Render without problems', () => {
const selector = wrapper.find('WordCloud').first();
expect(selector.find('svg')).to.have.length(1);
});
or
You can compare generated html structure as well via
it('Render without problems', () => {
const selector = wrapper.find('WordCloud').first();
expect(selector.html()).to.equal('<svg> Just an example </svg>');
});