How to test this functional component - reactjs

I need the help with testing this component using expect library with karma and mocha.
import React from 'react';
import {Clock} from 'Clock';
import {CountdownForm} from "CountdownForm";
import {Controls} from "Controls";
export class Countdown extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
countdownStatus: 'stopped'
}
}
componentDidUpdate(prevProps, prevState) {
if(prevState.countdownStatus !== this.state.countdownStatus) {
switch (this.state.countdownStatus) {
case 'started':
this.timerID = setInterval(
this.tick,
1000
);
break;
case 'stopped':
console.log('stopped fireeed');
this.setState({count: 0});
case 'paused':
console.log('paused fireeed');
clearInterval(this.timerID)
this.timerID = undefined;
break;
}
}
}
componentWillUnmount() {
clearInterval(this.timerID)
this.timerID = undefined;
}
handleSetCountdown = (seconds) => {
this.setState({
count: seconds,
countdownStatus: 'started'
});
}
tick = () => {
console.log('tick fireeeed');
let newMoment = this.state.count - 1;
let newState = 'started'
if(newMoment === 0) {
newState = 'stopped'
}
this.setState(() => {
return {
count: newMoment >= 0 ? newMoment : 0,
countdownStatus: newState
}
})
}
handleStatusChange = (newStatus) => {
this.setState({
countdownStatus: newStatus,
})
}
render() {
const renderControlArea = () => {
if(this.state.countdownStatus !== 'stopped') {
return <Controls countdownStatus={this.state.countdownStatus} onStatusChange={this.handleStatusChange}/>
} else {
return <CountdownForm onSetCountdown={this.handleSetCountdown}/>
}
}
const count = this.state.count
return (
<div>
<Clock totalSeconds={count}/>
{renderControlArea()}
{/* <CountdownForm onSetCountdown={this.handleSetCountdown}/> */}
</div>
);
}
}
const Controls = (props) => {
const renderStartStopButton = (countdownStatus) => {
if(countdownStatus === 'started') {
return <button className="button secondary" onClick={() => props.onStatusChange('paused')}>Pause</button>
}
else if (countdownStatus === 'paused'){
return <button className="button primary" onClick={() => props.onStatusChange('started')}>Start</button>
}
}
return (
<div className="controls">
{renderStartStopButton(props.countdownStatus)}
<button className="button alert hollow" onClick={() => props.onStatusChange('stopped')}>Clear</button>
</div>
)
}
export {Controls}
I tried something like this but it didn't work, it seems like I should test if the function is called when countdownStatus is passed. Here is the error log:
import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import $ from 'jQuery';
import TestUtils from 'react-addons-test-utils';
import { Controls } from "Controls"
describe('Controls', () => {
it('should exist', () => {
expect(Controls).toExist()
});
describe('render', () => {
it('should render pause button when started', () => {
var controls = TestUtils.renderIntoDocument(<Controls countdownStatus={'started'}/>);
var $el = $(ReactDOM.findDOMNode(controls));
var $PauseBtn = $el.find('button:contains(Pause)');
expect($PauseBtn.length).toBe(1);
});
it('should render start button when paused', () => {
var controls = TestUtils.renderIntoDocument(<Controls countdownStatus={'paused'}/>);
var $el = $(ReactDOM.findDOMNode(controls));
var $StartBtn = $el.find('button:contains(Start)');
expect($StartBtn.length).toBe(1);
});
});
})

Well, the issue was that I couldn't node into the document before it got returned from the Control function. I figured it out, just in case someone is dealing with the similar issue keep in mind you are testing function not Class here is the code that fixes problem
import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import $ from 'jQuery';
import TestUtils from 'react-addons-test-utils';
import { Controls } from "Controls"
describe('Controls', () => {
it('should exist', () => {
expect(Controls).toExist()
});
describe('render', () => {
it('should render pause button when started', () => {
var controls = TestUtils.renderIntoDocument(Controls({countdownStatus:'started'}));
var $el = $(ReactDOM.findDOMNode(controls));
var $PauseBtn = $el.find('button:contains(Pause)');
expect($PauseBtn.length).toBe(1);
});
it('should render start button when paused', () => {
var controls = TestUtils.renderIntoDocument(Controls({countdownStatus:'paused'}));
var $el = $(ReactDOM.findDOMNode(controls));
var $StartBtn = $el.find('button:contains(Start)');
expect($StartBtn.length).toBe(1);
});
});
})

Related

Using typed.js with React function components

typed.js doesn't offer an example for this in the docs, only for class components:
class TypedReactDemo extends React.Component {
componentDidMount() {
const options = { ... };
this.typed = new Typed(this.el, options );
}
render() {
return (
<span ref={(el) => { this.el = el; }} />
);
}
}
import React, { useRef, useEffect } from "react";
import Typed from "typed.js";
const Example = () => {
const typeTarget = useRef(null);
useEffect(() => {
const typed = new Typed(typeTarget.current, {
strings: ["<i>First</i> sentence.", "& a second sentence."],
typeSpeed: 40,
});
return () => {
typed.destroy();
};
}, []);
return <span ref={typeTarget} />;
};
export default Example;

value must be a mock or spy function when using jest.fn

Getting this error
Matcher error: received value must be a mock or spy function
Received has type: object
Received has value: {}
However, i think i shouldn't be getting this error because im using jest.fn. So im mocking the function.
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
// add the name of the prop, which in this case ites called onItemAdded prop,
// then use jest.fn()
const wrapper = shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
// console.log('props',wrapper.find('button').props());
wrapper.find('button').simulate('click');
expect(wrapper).toHaveBeenCalled(); // error happens when this executes
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js (using the component in this file)
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
I'm not 100% sure, but I believe you should do something like this:
describe('should simulate button click', () => {
it('should simulate button click', () => {
const mockedFunction = jest.fn();
const wrapper = shallow(<TodoAddItem onItemAdded={ mockedFunction } />);
wrapper.find('button').simulate('click');
expect(mockedFunction).toHaveBeenCalled();
});
});
You are testing if the onItemAdded function gets called when you click the <TodoAddItem /> component. So you have to mock it first using jest.fn and then check if the mocked function got called after you simulated the click.
For me works replacing the next one:
const setCategories = () => jest.fn();
With this one:
const setCategories = jest.fn();
I suppose that you should to set just jest.fn or jest.fn() in your code.

How to test button prop in enzyme

Trying to test this component, and im getting this
error
TypeError: this.props.onItemAdded is not a function
I've referenced this but this solution doesn't really apply to my problem
Enzyme test: TypeError: expect(...).find is not a function
How would i test the button functionality being that the button is a prop ?
todo-add-item.test.js
import React from "react";
import { shallow } from "enzyme";
import TodoAddItem from './todo-add-item';
describe('Should render add item component', ()=> {
it('should render add item component', () => {
const wrapper = shallow(<TodoAddItem/>)
})
})
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
const wrapper =shallow(<TodoAddItem/>)
wrapper.find('button').simulate('click') // getting the type error here.
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
You don't pass any props to your component.
const wrapper =shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
You can check props with .props()
Eg:
console.log('props',wrapper.find('button').props());

The state of a functional component does not change very well

I am creating a domain integrated search site now and I need to animate the TLD part of the design that the designer gave me.
However, when entering the keyword to be searched by the functional component, the state did not change while the class component was changed.
Code
functional component
import * as React from 'react';
import classNames from 'classnames';
import './SearchBarTLD.scss';
const tldList:string[] = [
'.com',
'.co.kr',
'.se',
'.me',
'.xyz',
'.kr',
'.dev',
'.xxx',
'.rich',
'.org',
'.io',
'.shop',
'.ga',
'.gg',
'.net'
];
const SearchBarTLD = () => {
const [ selectIndex, setSelectIndex ] = React.useState(0);
const changeIndex = () => {
if (selectIndex === 14) {
setSelectIndex(0)
} else {
setSelectIndex(selectIndex + 1)
}
}
React.useLayoutEffect(() => {
const interval = setInterval(() => changeIndex(), 1500);
return () => {
clearInterval(interval)
}
})
const renderList = () =>{
return tldList.map((tld:string, index:number) => {
return (
<span
className={
classNames(
"SearchBarTLD__tld", {
"SearchBarTLD__tld--visual": selectIndex === index
}
)
}
key={tld}
>
{tldList[index]}
</span>
)
})
}
return (
<div className="SearchBarTLD">
{renderList()}
</div>
)
}
export default SearchBarTLD;
class components
import * as React from 'react';
import classNames from 'classnames';
import './SearchBarTLD.scss';
export interface SearchBarTLDProps {
}
export interface SearchBarTLDState {
selectIndex: number,
tldList: string[]
}
class SearchBarTLD extends React.Component<SearchBarTLDProps, SearchBarTLDState> {
state: SearchBarTLDState;
intervalId: any;
constructor(props: SearchBarTLDProps) {
super(props);
this.state = {
selectIndex: 0,
tldList: [
'.com',
'.co.kr',
'.se',
'.me',
'.xyz',
'.kr',
'.dev',
'.xxx',
'.rich',
'.org',
'.io',
'.shop',
'.ga',
'.gg',
'.net'
]
};
this.intervalId = 0;
}
changeIndex() {
const { selectIndex } = this.state;
if (selectIndex === 14) {
this.setState({selectIndex: 0});
} else {
this.setState({selectIndex: selectIndex + 1});
}
}
renderList = () =>{
const { selectIndex, tldList } = this.state;
return tldList.map((tld:string, index:number) => {
return (
<span
className={
classNames(
"SearchBarTLD__tld", {
"SearchBarTLD__tld--visual": selectIndex === index
}
)
}
key={tld}
>
{tldList[index]}
</span>
)
})
}
componentDidMount() {
this.intervalId = setInterval(() => this.changeIndex(), 1500);
}
componentWillUnmount() {
clearInterval(this.intervalId)
}
render() {
return (
<div className="SearchBarTLD">
{this.renderList()}
</div>
);
}
}
export default SearchBarTLD;
The results of the functional component and the results of the class component are shown below.
funcational component
https://user-images.githubusercontent.com/28648915/56777865-8c0cf100-680e-11e9-93ad-cb7b59cd54e9.gif
class component
https://user-images.githubusercontent.com/28648915/56777941-e4dc8980-680e-11e9-8a47-be0a14a44ed9.gif
Can you guys tell me why this happens?

Console.log in ReactJS keeps running

I'm importing a child component and rendering it in my app.js file. The component has a console.log for debugging but it keeps running the log, seemingly without end. Worried something might be wrong, new to ReactJS and wondering if this is a common issue and how to resolve it.
App.js:
import React, { Component } from 'react';
import {BrowserRouter as Router, Link} from 'react-router-dom';
import './App.css';
import axios from 'axios'
import Header from './components/header';
import Page from './components/page';
class App extends Component {
constructor(props) {
super(props);
this.state = {
title: 'John Doe',
nav: {},
currentPage: "",
pageContent: "",
pageTitle: "",
pageTemplate: "",
pageId: 0,
pageCustomMeta: {},
archiveData: []
}
}
getMainMenu(){
axios.get('http://admin.sitedata.co/menus/5')
.then((response) => {
this.setState({nav:response.data});
})
.catch((error) => {
console.log(error);
});
}
isHome(){
//console.log(document.location.pathname);
if(document.location.pathname === "/") {
document.body.classList.add('home');
} else {
document.body.classList.remove('home');
}
}
componentDidMount(){
/*
* get current page content
* get the main menu
* allow pageChange function to be ran
* allow isHome to be ran
*/
var slug = "";
if(document.location.pathname === "/") {
slug = "home";
} else {
slug = document.location.pathname.substr(1);
}
this.getPageData(slug);
this.getMainMenu();
this.pageChange = this.pageChange.bind(this);
this.isHome = this.isHome.bind(this);
this.triggerMenu = this.triggerMenu.bind(this);
this.triggerHire = this.triggerHire.bind(this);
this.navigate = this.navigate.bind(this);
this.setArchiveData = this.setArchiveData.bind(this);
this.resetArchiveData = this.resetArchiveData.bind(this);
this.madeChange = this.madeChange.bind(this);
//document.getElementById('loadingOverlay').classList.add('remove');
}
getPageData(slug){
console.log(this.state);
axios.get('http://admin.sitedata.co/pages?slug='+slug)
.then((response) => {
console.log(response.data);
this.setState({
pageContent:response.data[0].content.rendered,
currentPage:slug,
pageTitle:response.data[0].title.rendered,
pageTemplate:response.data[0].template,
pageId:response.data[0].id,
pageCustomMeta:response.data[0].post_meta,
archiveData:[]
},function(){
console.log(this.state);
/*
* set the page title
* check if the page is at home
* get page custom meta
*/
document.title = this.state.pageTitle;
this.isHome();
});
})
.catch((error) => {
console.log(error);
});
}
pageChange(e){
var slug = e.target.getAttribute('data-link');
var classes = e.target.classList.contains('trigger-hire');
if(classes){
this.triggerHire();
e.preventDefault();
} else {
this.getPageData(slug);
}
}
setArchiveData(archives) {
this.setState({archiveData:archives});
}
resetArchiveData() {
}
navigate (event) {
event.preventDefault()
console.log(event.target.tagName);
if (event.target.tagName === 'A') {
this.props.router.push(event.target.getAttribute('href'));
console.log('boom');
}
event.preventDefault();
}
triggerMenu(e){
var menuOverlay = document.getElementById('menuOverlay');
if(menuOverlay.classList.contains('active')){
menuOverlay.classList.remove('active');
} else {
menuOverlay.classList.add('active');
}
}
triggerHire(e){
var hireOverlay = document.getElementById('hireOverlay');
if(hireOverlay.classList.contains('active')){
hireOverlay.classList.remove('active');
} else {
hireOverlay.classList.add('active');
}
e.stopPropagation();
e.preventDefault();
}
madeChange(){
alert('changed');
}
render() {
return (
<div className="App">
<Header nav={this.state.nav} pageChange={this.pageChange} triggerMenu={this.triggerMenu} triggerHire={this.triggerHire}/>
<Page madeChange={this.madeChange}
currentPage={this.state.currentPage}
nav={this.state.nav}
pageChange={this.pageChange}
isHome={this.isHome}
pageContent={this.state.pageContent}
pageTitle={this.state.pageTitle}
pageTemplate={this.state.pageTemplate}
pageId={this.state.pageId}
pageCustomMeta={this.state.pageCustomMeta}
archiveData={this.state.archiveData}
triggerHire={this.triggerHire}
navigate={this.navigate}
setArchiveData={this.setArchiveData}
/>
</div>
);
}
}
export default App;
Page.js
import React, { Component } from 'react';
import {BrowserRouter as Router, Link} from 'react-router-dom';
import axios from 'axios'
class Page extends Component {
render() {
if(this.props.currentPage){
var currentPage = this.props.currentPage;
var pageChange = this.props.pageChange;
var customMeta = this.props.pageCustomMeta;
var pageTempalate = this.props.pageTemplate.substr(0,this.props.pageTemplate.length-4);
var pageTitle = this.props.pageTitle;
var newTitle = <h1><span><i>{pageTitle}</i></span></h1>;
var isArchive = "";
var archiveName = "";
var firstSpace = pageTitle.indexOf(' ');
if(firstSpace > -1){
var firstWord = pageTitle.substr(0, firstSpace);
var titleLast = pageTitle.substr(firstSpace);
newTitle = <h1><span><i>{firstWord}</i>{titleLast}</span></h1>
}
if(currentPage === "home"){
if(this.props.nav.items){
var navData = this.props.nav;
var navHomeItems = navData.items.map(function(navItem){
return <li key={navItem.id}><Link to={'/'+navItem.object_slug} className={navItem.classes} onClick={pageChange} data-link={navItem.object_slug}>{navItem.title}</Link></li>;
});
}
}
document.title = this.props.pageTitle;
var isArchive = customMeta.isArchive;
var archiveName = customMeta.archiveName
var worksArchive = "";
if(customMeta.isArchive && customMeta.isArchive == "true"){
if(customMeta.archiveName) {
axios.get('http://admin.sitedata.co/'+customMeta.archiveName)
.then((response) => {
var archivePages = response.data;
console.log(archivePages);
if(archiveName == "works"){
worksArchive = archivePages.map(function(work){
//console.log(worksArchive);
return <Link key={work.id} className="work-item" to="/" ><img src={work.post_meta.targetimg} /></Link>;
});
this.props.setArchiveData(worksArchive);
}
})
.catch((error) => {
console.log(error);
});
}
}
if(customMeta.pageColor){
document.body.classList.add(customMeta.pageColor);
}
if(customMeta.bgimg){
document.body.setAttribute('style', "background-image:url('"+customMeta.bgimg+"');");
}
}
return (
<Router onEnter={this.props.madeChange}>
<div className="container">
<div className={(pageTempalate !== "") ? pageTempalate : ''}>
{newTitle}
<div dangerouslySetInnerHTML={{__html:this.props.pageContent}}></div>
{this.props.archiveData}
</div>
</div>
</Router>
);
}
}
export default Page;
Distilling your code down to the essential, it looks like this:
class Page extends Component {
render() {
axios.get(url).then(response=> {
this.setState({archiveData: response.data})
})
return (
<div className="container">
{this.state.archiveData}
</div>
)
}
}
(You are doing it differently with a callback that causes the parents to send new props, but the effect is the same).
You should be able to see problem now: the render method causes a delayed change to state (or props) which in react triggers a new render. So you now have an infinite loop, just delayed by the time taken for an ajax request.
To fix this, you need to remove the ajax request from the render method. In your case, it should probably be in the parent App component.

Resources