Regarding the unit test case implementation in jest for redux containers - javascript

How to write the unit test case for the given container which is associated with the component given as below?
Here the PopupNotification.jsx is a component file which has PopupNotification.js as a container file.
I want a jest unit test case implementation for the container file.
PopupNotification.jsx --> component file
import React from "react";
import PropTypes from "prop-types";
import { notyIcons, notyTimeout } from "../helpers/constants";
class PopupNotification extends React.Component {
constructor(props) {
super(props);
this.state = {
showNoty: props.showNoty
};
this.notyTimer = null;
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
showNoty: nextProps.showNoty
};
}
shouldComponentUpdate() {
return true;
}
startNotyTimer() {
let __self = this;
if (this.notyTimer) {
clearTimeout(this.notyTimer);
}
this.notyTimer = setTimeout(function() {
__self.closeNoty();
}, notyTimeout);
}
componentWillUnmount() {
if (this.notyTimer) {
clearTimeout(this.notyTimer);
}
this.setState({ showNoty: false });
}
closeNoty() {
let { id } = this.props;
this.props.clearNotification(id);
this.setState({
showNoty: false
});
}
getNotyIcon() {
return <i className={`notification-popup__icon pos-l-m ${notyIcons[this.props.type]}`} />;
}
getNotyLayout() {
let notyIcon = this.getNotyIcon();
let { message: notyMessage, type } = this.props;
return (
<div className={`pure-g notification-popup ${type}`}>
<div className="pure-u-1-8 pos">{notyIcon}</div>
<div className="pure-u-7-8">
<div className="u-margin-8--left">{notyMessage}</div>
</div>
</div>
);
}
render() {
var notyLayout = null;
if (this.state.showNoty) {
this.startNotyTimer();
notyLayout = this.getNotyLayout();
}
return notyLayout;
}
}
PopupNotification.propTypes = {
message: PropTypes.string,
type: PropTypes.string,
id: PropTypes.number,
showNoty: PropTypes.bool,
clearNotification: PropTypes.func
};
PopupNotification.defaultProps = {
message: "",
type: ""
};
export default PopupNotification;
PopupNotification.js --> container File
import { connect } from "react-redux";
import { actions } from "../ducks/common";
import PopupNotification from "../components/PopupNotification";
const mapStateToProps = state => {
let { common: { popupNotifications = [] } } = state;
let showNoty = false;
if (popupNotifications.length) {
showNoty = true;
}
return {
showNoty,
...popupNotifications[popupNotifications.length-1]
};
};
const mapDispatchToProps = dispatch => {
return {
clearNotification: notyId => {
dispatch(actions.clearPopupNotification({id: notyId}));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PopupNotification);

I suggest using redux-mock-store to gain full control over your store. Then you can do things like
const actions = store.getActions()
expect(actions).toEqual([expectedPayload])
Copied from their docs
Since this is a unit test, you just need to test what this container is responsible for, and from what I see from your example that it is exposing the redux connected component, so you should test that your mapStateToProps & mapDispatchToProps are executing correctly.

Related

React - Slide out component on unmount

I want to slide out a React component when it umounts. I am using CSSTransition for the animation which works great for mounting, but not unmounting. Somehow I need to delay the unmount process. All the off-the-shelf solutions sadly do not work for me. I am removing an element by doing a post request and then actually removing it in the UI with a SignalR callback.
To make my sequence more clear, I created a sequence diagram:
This is my code right now:
Board.tsx
import React from 'react';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { AppState } from 'store';
import { connect } from 'react-redux';
import { BoardState } from 'store/board/types';
import { BoardHubService } from 'services/hubs/boardHub.service';
import { BoardElementViewModel } from 'models/BoardElementViewModel';
import { BoardViewModel } from 'models/BoardViewModel';
import { BoardElement } from './boardElement/boardElement';
import { HttpService } from 'services/http.service';
import { setActiveBoard } from 'store/board/actions';
import './board.scss'
import { mapToType } from 'helpers/helpers';
import { TransitionGroup } from 'react-transition-group';
interface BoardProps {
activeBoardState: BoardState;
setActiveBoard: typeof setActiveBoard;
}
interface LocalBoardState {
boardElements: Array<BoardElementViewModel>
}
class Board extends React.Component<BoardProps, LocalBoardState> {
private config: Config;
private httpService: HttpService;
private boardHubService: BoardHubService;
constructor(props: any) {
super(props);
this.config = container.resolve(Config);
this.boardHubService = container.resolve(BoardHubService);
this.httpService = container.resolve(HttpService);
this.state = {
boardElements: []
}
}
async componentDidMount() {
// If there was any active board on page load...
if (this.props.activeBoardState.boardId) {
await this.loadBoardElements();
}
this.boardHubService.getConnection().on('SwitchedBoard', (response: BoardViewModel | null) => {
console.log(response);
this.setState({
boardElements: (response) ? response.elements : []
});
this.updateSiteTitle(response);
});
this.boardHubService.getConnection().on('ReceiveElement', (response: BoardElementViewModel) => {
let elements = this.state.boardElements;
elements.unshift(response);
this.setState(() => ({
boardElements: elements
}))
});
this.boardHubService.getConnection().on('RemoveElement', (response: string) => {
let elements = this.state.boardElements;
let element = mapToType<BoardElementViewModel>(elements.find(x => x.id === response));
elements.splice(elements.indexOf(element), 1);
this.setState(() => ({
boardElements: elements
}))
});
}
/**
* Load the elements from the board that was already active on page load.
*/
private async loadBoardElements() {
await this.httpService.getWithAuthorization<Array<BoardElementViewModel>>(`/boards/${this.props.activeBoardState.boardId}/elements`)
.then((response: Array<BoardElementViewModel>) => {
this.setState({
boardElements: response
});
})
.catch((e) => console.warn(e));
}
private updateSiteTitle(board: BoardViewModel | null) {
if (board != null) {
document.title = `${board.name} | ${this.config.siteName}`;
}
else {
document.title = this.config.siteName;
}
}
render() {
return (
<>
{this.props.activeBoardState.boardId != null
?
<div className="board-elements">
{this.state.boardElements.map((element: BoardElementViewModel, index) => {
return (
<BoardElement
key={index}
id={element.id}
// TODO: Use number from server
number={element.elementNumber}
user={element.user}
direction={element.direction}
note={element.note}
imageId={element.imageId}
createdAt={element.createdAt}
/>
)
})}
</div>
:
<div className="select-board-instruction">
<h1>Please select or create a board.</h1>
</div>
}
</>
)
}
}
const mapStateToProps = (state: AppState) => ({
activeBoardState: state.activeBoard
});
export default connect(mapStateToProps, { setActiveBoard })(Board);
BoardElement.tsx
import React from 'react';
import { UserViewModel } from 'models/UserViewModel';
import { Direction } from 'models/Direction';
import './boardElement.scss';
import { dateToReadableString } from 'helpers/helpers';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { HttpService } from 'services/http.service';
import $ from 'jquery'
import { CSSTransition } from 'react-transition-group';
interface BoardElementProps {
id: string;
number: number;
user: UserViewModel;
// TODO: Use direction Enum
direction?: Direction;
note?: string;
imageId?: string;
createdAt: Date;
}
interface BoardElementState {
show: boolean;
}
export class BoardElement extends React.Component<BoardElementProps, BoardElementState> {
private config: Config;
private httpService: HttpService;
private ref: any;
constructor(props: BoardElementProps) {
super(props);
this.state = {
show: false,
}
this.config = container.resolve(Config);
this.httpService = container.resolve(HttpService);
}
getReadableDirection(direction: Direction) {
// TODO: Richtingen vertalen
switch (direction) {
case Direction.North: return 'Noord';
case Direction.NorthEast: return 'Noordoost';
case Direction.East: return 'Oost';
case Direction.SouthEast: return 'Zuidoost';
case Direction.South: return 'Zuid';
case Direction.SouthWest: return 'Zuidwest';
case Direction.West: return 'West';
case Direction.NorthWest: return 'Noordwest';
}
}
removeElement() {
this.httpService.deleteWithAuthorization(`/boards/elements/${this.props.id}`).then(() => {
}, (error) => {
console.warn(error);
});
}
componentDidMount() {
setTimeout(() => {
this.setState(() => ({
show: true
}));
}, 500);
}
componentWillUnmount() {
this.setState(() => ({
show: false
}));
}
render() {
return (
<CSSTransition in={this.state.show} timeout={200} classNames={{
enter: 'animation-height',
enterDone: 'animation-height',
exit: ''
}}>
<div className="animation-wrapper animation-height-0" >
<div className="board-element" >
<div className="board-element-header">
<span className="board-element-number">{this.props.number}</span>
<span className="board-element-creator">{this.props.user.username}</span>
<i className="fas fa-trash ml-auto delete-icon" onClick={() => this.removeElement()}></i>
</div>
<div className="board-element-body">
{this.props.imageId
? <img className="board-element-image" src={`${this.config.apiUrl}/content/${this.props.imageId}`} />
: <p className="board-element-message">{this.props.note}</p>
}
</div>
<div className="board-element-footer">
{this.props.direction &&
<div className="board-element-direction">
<i className="fas fa-location-arrow direction mr-2"></i>{this.getReadableDirection(this.props.direction)}
</div>
}
<time className="board-element-timestamp" dateTime={this.props.createdAt.toString()}>{dateToReadableString(this.props.createdAt)}</time>
</div>
</div>
</div>
</CSSTransition >
)
}
}
For better illustration take a look at this GIF:
https://gyazo.com/3c933851ecec39029f25d4df3a136c2a
That is using jQuery in another project of mine. That is what I want to achive in React.
You can delay unmounting the component. Write a hoc and use a setTimeout. Maintain a state say shouldRender.
hoc
function delayUnmounting(Component) {
return class extends React.Component {
state = {
shouldRender: this.props.isMounted
};
componentDidUpdate(prevProps) {
if (prevProps.isMounted && !this.props.isMounted) {
setTimeout(
() => this.setState({ shouldRender: false }),
this.props.delayTime
);
} else if (!prevProps.isMounted && this.props.isMounted) {
this.setState({ shouldRender: true });
}
}
render() {
return this.state.shouldRender ? <Component {...this.props} /> : null;
}
};
}
usage
function Box(props) {
return (
<BoxWrapper isMounted={props.isMounted} delay={props.delay}>
✨🎶✨🎶✨🎶✨🎶✨
</BoxWrapper>
);
}
const DelayedComponent = delayUnmounting(Box);
See complete code in the demo
Read this article on medium

React-diagrams custom node widget not updating on engine.repaintCanvas()

I have copied the react-diagrams demo project and done some changes to the TSCustomNodeWidget.tsx, and when I call engine.repaintCanvas() it does not update the values I have set (whereas the name of the DefaultNodeModel does).
TSCustomNodeModel.ts
import { NodeModel, DefaultPortModel, DefaultNodeModel } from '#projectstorm/react-diagrams';
import { BaseModelOptions } from '#projectstorm/react-canvas-core';
export interface TSCustomNodeModelOptions extends BaseModelOptions {
color?: string;
value?: number;
name?: string;
}
export class TSCustomNodeModel extends DefaultNodeModel {
color: string;
name: string;
value: number;
constructor(options: TSCustomNodeModelOptions = {}) {
super({
...options,
type: 'ts-custom-node'
});
this.color = options.color || 'red';
this.value = options.value || 0;
this.name = options.name || 'name';
// setup an in and out port
this.addPort(
new DefaultPortModel({
in: true,
name: 'IN'
})
);
this.addPort(
new DefaultPortModel({
in: false,
name: 'OUT'
})
);
}
serialize() {
return {
...super.serialize(),
color: this.color
};
}
deserialize(event:any): void {
super.deserialize(event);
this.color = event.data.color;
}
}
TSCustomNodeFactory.tsx
import * as React from 'react';
import { TSCustomNodeModel } from './TSCustomNodeModel';
import { TSCustomNodeWidget } from './TSCustomNodeWidget';
import { AbstractReactFactory } from '#projectstorm/react-canvas-core';
import { DiagramEngine } from '#projectstorm/react-diagrams-core';
export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel, DiagramEngine> {
constructor() {
super('ts-custom-node');
}
generateModel(initialConfig:any) {
return new TSCustomNodeModel();
}
generateReactWidget(event:any): JSX.Element {
return <TSCustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} />;
}
}
TSCustomNodeWidget.tsx
import * as React from 'react';
import { DiagramEngine, PortWidget } from '#projectstorm/react-diagrams-core';
import { TSCustomNodeModel } from './TSCustomNodeModel';
export interface TSCustomNodeWidgetProps {
node: TSCustomNodeModel;
engine: DiagramEngine;
}
export interface TSCustomNodeWidgetState {}
export class TSCustomNodeWidget extends React.Component<TSCustomNodeWidgetProps, TSCustomNodeWidgetState> {
constructor(props: TSCustomNodeWidgetProps) {
super(props);
this.state = {};
}
render() {
return (
<div className="custom-node">
<PortWidget engine={this.props.engine} port={this.props.node.getPort('IN')}>
<div className="circle-port" />
</PortWidget>
<PortWidget engine={this.props.engine} port={this.props.node.getPort('OUT')}>
<div className="circle-port" />
</PortWidget>
<div className="custom-node-color"
style={{ backgroundColor: this.props.node.color }}>
{this.props.node.value}
</div>
</div>
);
}
}
App.tsx
Setup
// create an instance of the engine with all the defaults
const engine = createEngine();
const state = engine.getStateMachine().getCurrentState();
if (state instanceof DefaultDiagramState) {
state.dragNewLink.config.allowLooseLinks = false;
}
const model = new DiagramModel();
engine.getNodeFactories().registerFactory(new TSCustomNodeFactory());
model.setGridSize(5);
engine.setModel(model);
Adding the node with button (repaintCanvas works)
function addConstNode() {
const node = new TSCustomNodeModel({
name: "CONST",
value: 0
});
node.setPosition(50, 50);
model.addAll(node);
engine.repaintCanvas(); //This works
return node;
}
Updating the node (repaintCanvas does not work)
currentNode.value = value;
...
engine.repaintCanvas();
I had a similar issue, where changing nodes would not update canvas immediately, and I managed to get around it by forcing the react component to re-render by doing this:
function useForceUpdate(){
const [, setValue] = useState(0);
return () => setValue(value => ++value);
}
const MyComponent = () => {
const forceUpdate = useForceUpdate();
// do some update to diagram
...
forceUpdate();
};
Can't remember if I also had to call engine.repaintCanvas() also or not.
Bit late to the party but hope this helps.

Trying to make the todo list update every time a new todo is entered

I am creating a basic todo application in react with flux. I am trying to make it so that every time a new todo is entered in the API, the list is update in real time rather than me having to reload the page the and application making another request. I have tried using componentDidUpdate; however that doesn't seem to work in this case. How should I go about implementing that?
My Todos.js
import React from "react";
import * as TodoActions from "../actions/TodoActions";
import TodoStore from "../stores/TodoStore";
import './Todos.css'
export default class Todos extends React.Component {
constructor() {
super();
this.addItem = this.addItem.bind(this);
this.deleteTodo = this.deleteTodo.bind(this)
this.getTodos = this.getTodos.bind(this);
this.state = {
todos: TodoStore.getAll(),
};
TodoActions.receiveTodos()
}
componentWillMount() {
TodoStore.addChangeListener(this.getTodos);
}
componentWillUnmount() {
TodoStore.removeChangeListener(this.getTodos);
}
componentDidMount() {
TodoActions.receiveTodos();
}
componentDidUpdate() {
TodoActions.receiveTodos();
}
getTodos() {
this.setState({
todos: TodoStore.getAll(),
});
}
deleteTodo(id) {
TodoActions.deleteTodo(id);
}
addItem(e) {
e.preventDefault();
TodoActions.createTodo(this._inputElement.value)
}
render() {
const { todos } = this.state;
const TodoComponents = todos.map((todo) => {
return (
<div key={todo.id} className="todo-list">
<div className="todo-name">{todo.name}</div>
<div className="todo-btn"><button type="button" onClick={() => this.deleteTodo(todo.id)}>Delete</button></div>
</div>
)
});
return (
<div className="main-container">
<h1 className="title">Todos</h1>
<ul>{TodoComponents}</ul>
<form onSubmit={this.addItem}>
<input ref={(a) => this._inputElement = a} placeholder="Enter Task" className="input-form"/>
<button type="submit" className="input-btn">Add</button>
</form>
</div>
);
}
}
My TodoStore.js
import { EventEmitter } from "events";
import AppDispatcher from "../dispatcher";
let _todos = []
function setTodos(todos) {
_todos = todos;
}
class TodoStore extends EventEmitter {
emitChange = () => {
this.emit("change");
}
addChangeListener = (callback) => {
this.on("change", callback)
}
removeChangeListener = (callback) => {
this.removeListener("change", callback)
}
getAll = () => {
return _todos;
}
handleActions = (action) => {
switch(action.type) {
case "RECEIVE_TODOS": {
setTodos(action.todos);
this.emit("change")
break;
}
}
}
}
const todoStore = new TodoStore;
AppDispatcher.register(todoStore.handleActions.bind(todoStore));
export default todoStore;
My TodoActions.js
import AppDispatcher from "../dispatcher";
import apiClient from "../api-client"
export function createTodo(name) {
apiClient.createTodo({name: name}).then((result) => {
AppDispatcher.dispatch({
type: "CREATE_TODO",
name: result,
});
})
}
export function deleteTodo(id) {
apiClient.deleteTodo(id).then((result) => {
AppDispatcher.dispatch({
type: "DELETE_TODO",
id,
});
})
}
export function receiveTodos() {
apiClient.getAllTodos().then(result => {
AppDispatcher.dispatch({
type: "RECEIVE_TODOS",
todos: result.payload
});
})
}

React draft wysiwyg - Can't able to type text in editor after clearing editorState

I'm trying to reset editor content after some action completed using react-draft-wysiwyg editor. All contents cleared by using clearEditorContent method from draftjs-utils. But after clearing contents I can't able to type nothing in editor. Added the code below. Please help to solve this problem.
import React, { Component } from 'react';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { clearEditorContent } from 'draftjs-utils'
import { Editor } from 'react-draft-wysiwyg';
import '../../../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
export default class RTEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
this.setDomEditorRef = ref => this.domEditor = ref;
}
onEditorStateChange: Function = (editorState) => {
this.setState({
editorState,
}, () => {
this.props.sendResult(draftToHtml(convertToRaw(this.state.editorState.getCurrentContent())));
});
};
componentWillReceiveProps(nextProps) {
if(nextProps.reset) {
this.reset();
}
}
componentDidMount() {
if(this.props.text) {
const html = `${this.props.text}`;
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.setState({ editorState, });
}
}
this.domEditor.focusEditor();
}
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
};
render() {
const { editorState } = this.state;
return (
<Editor
ref={this.setDomEditorRef}
editorState={editorState}
wrapperClassName="rte-wrapper"
editorClassName="rte-editor"
onEditorStateChange={this.onEditorStateChange}
toolbarCustomButtons={[this.props.UploadHandler]}
/>
)
}
}
My parent component code is below,
import React, { Component } from 'react'
import { addThreadPost } from '../../../api/thread-api'
import { isEmpty } from '../../../api/common-api'
import RTEditor from './Loadable'
export default class ThreadActivity extends Component {
constructor(props) {
super(props)
this.state = {
clearEditor: false,
threadPost: ''
}
}
setPost = (post) => {
this.setState({ threadPost: post })
}
addThreadPost = () => {
let postText = this.state.threadPost.replace(/<[^>]+>/g, '');
if(!isEmpty(postText)) {
addThreadPost(this.props.match.params.id_thread, this.state.threadPost, this.state.postAttachments).then(response => {
this.setState({
clearEditor: true,
postAttachments: [],
})
});
}
else {
alert("Please enter some text in box.");
}
}
render() {
return [
<div className="commentbox-container">
<div className="form-group">
<div className="form-control">
<RTEditor
ref={node => { this.threadPost = node }}
text={""}
sendResult={this.setPost.bind(this)}
reset={this.state.clearEditor}
/>
<button className="btn-add-post" onClick={this.addThreadPost}>Add Post</button>
</div>
</div>
</div>
]
}
}
Your problem is probably that once you set ThreadActivity's state.clearEditor to true, you never set it back to false. So your this.reset() is getting called every time the component receives props. Which, incidentally, is going to be every time you try to type because you're invoking that this.props.sendResult.
The simplest fix is to make sure you change state.clearEditor back to false once the clearing is done.
Add to ThreadActivity.js:
constructor(props) {
...
this.completeClear = this.completeClear.bind(this);
}
...
completeClear = () => {
this.setState({clearEditor: false});
}
render() {
...
<RTEditor
...
completeClear={this.completeClear}
/>
...
}
And in RTEditor.js:
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
this.props.completeClear();
};

mapStateToProps object undefined

I´m making a call to my API to get a specific object using its id.
As I call console.log in the mapStateToProps method, the entire object is printed, but the state's "anuncio" property is undefined when I try to access it with this.props.anuncio. What am I doing wrong?
Below is how I'm using the reducer and the action.
import React from 'react'
import PropTypes from 'prop-types'
import { fetchAnuncio } from '../../actions/anuncios';
import { connect } from 'react-redux';
import Avatar from 'react-avatar';
import star from '../../imgs/star.png';
class AnuncioDetalhes extends React.Component {
constructor(props) {
super(props);
this.state = {
id: this.props.anuncio ? this.props.anuncio.id : null,
img: this.props.anuncio ? this.props.anuncio.img : null
}
}
componentDidMount() {
if (this.props.match.params.id) {
this.props.fetchAnuncio(this.props.match.params.id);
}
}
render () {
const {id, img} = this.state;
return (
<div>
</div>
);
}
}
AnuncioDetalhes.propTypes = {
fetchAnuncio: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
}
function mapStateToProps(state, props) {
if(props.match.params.id) {
console.log(state.anuncios.find(item => item.id === 4))
return {
anuncio: state.anuncios.find(item => item.id === props.match.params.id)
}
}
return { anuncio: null };
}
export default connect(mapStateToProps, {fetchAnuncio})(AnuncioDetalhes);
reducer:
import { SET_ANUNCIOS, ANUNCIO_FETCHED } from '../actions/types';
export default function anuncios(state = [], action = {}) {
switch(action.type) {
case ANUNCIO_FETCHED:
const index = state.findIndex(item => item.id === action.anuncio.id);
if(index > -1) {
return state.map(item => {
if (item.id === action.anuncio.id) return action.anuncio;
return item;
});
} else {
return [
...state,
action.anuncio
];
}
case SET_ANUNCIOS:
return action.anuncios;
default: return state;
}
}
action:
import axios from 'axios';
import Constants from '../components/Constants'
import { SET_ANUNCIOS, ANUNCIO_FETCHED } from './types';
export function setAnuncios(anuncios) {
return {
type: SET_ANUNCIOS,
anuncios
}
}
export function anuncioFetched(anuncio) {
return {
type: ANUNCIO_FETCHED,
anuncio
};
}
export function fetchAnuncios(cidade) {
return dispatch => {
return axios.get(Constants.BASE_URL + "anuncios/?user__cidade=" + cidade + "&status=2").then(res => {
dispatch(setAnuncios(res.data.results));
});
}
}
export function fetchAnuncio(id) {
return dispatch => {
return axios.get(Constants.BASE_URL + "anuncios/" +`${id}`).then(res => {
dispatch(anuncioFetched(res.data));
});
}
}
I think you should use componentWillReceiveProps lifecycle method to get your updated props.
componentWillReceiveProps = (nextProps) => {
console.log(nextProps)
}

Categories

Resources